Lagrange [release]
Saving inline media content to Downloads
[1mdiff --git a/res/about/version.gmi b/res/about/version.gmi[m
[1mindex 48b4cbd1..c2c47fe4 100644[m
[1m--- a/res/about/version.gmi[m
[1m+++ b/res/about/version.gmi[m
[36m@@ -8,6 +8,7 @@[m
[m
## 0.10[m
* Added option to load inline images when pressing Space or ↓ for a more focused reading experience — just keep tapping a single key to proceed. If an image link is visible, it will be loaded instead of scrolling. This option is disabled by default.[m
[32m+[m[32m* Added context menu item to save inline images to Downloads.[m
* Added an option to use a proxy server for Gemini requests.[m
* Added a new keyboard link navigation mode focusing on the home row keys. The default keybinding for this is "F".[m
* Added a keybinding to activate keyboard link modifier mode. The keyboard link keys are active while the modifier is held down. The default is ${ALT}.[m
[1mdiff --git a/src/app.c b/src/app.c[m
[1mindex b53666c8..1440bab9 100644[m
[1m--- a/src/app.c[m
[1m+++ b/src/app.c[m
[36m@@ -1083,6 +1083,7 @@[m [miBool handleCommand_App(const char *cmd) {[m
}[m
setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll"));[m
setRedirectCount_DocumentWidget(doc, redirectCount);[m
[32m+[m[32m setFlags_Widget(findWidget_App("document.progress"), hidden_WidgetFlag, iTrue);[m
setUrlFromCache_DocumentWidget(doc, url, isHistory);[m
/* Optionally, jump to a text in the document. This will only work if the document[m
is already available, e.g., it's from "about:" or restored from cache. */[m
[1mdiff --git a/src/gmdocument.c b/src/gmdocument.c[m
[1mindex 80ddfc9d..c8fc9869 100644[m
[1m--- a/src/gmdocument.c[m
[1m+++ b/src/gmdocument.c[m
[36m@@ -1377,7 +1377,7 @@[m [miBool isMediaLink_GmDocument(const iGmDocument *d, iGmLinkId linkId) {[m
const iString *dstUrl = absoluteUrl_String(&d->url, linkUrl_GmDocument(d, linkId));[m
const iRangecc scheme = urlScheme_String(dstUrl);[m
if (equalCase_Rangecc(scheme, "gemini") || equalCase_Rangecc(scheme, "gopher") ||[m
[31m- willUseProxy_App(scheme)) {[m
[32m+[m[32m equalCase_Rangecc(scheme, "file") || willUseProxy_App(scheme)) {[m
return (linkFlags_GmDocument(d, linkId) &[m
(imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag)) != 0;[m
}[m
[1mdiff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c[m
[1mindex 3cf564ac..a10aa409 100644[m
[1m--- a/src/ui/documentwidget.c[m
[1m+++ b/src/ui/documentwidget.c[m
[36m@@ -1264,6 +1264,78 @@[m [mstatic iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {[m
return iFalse;[m
}[m
[m
[32m+[m[32mstatic void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) {[m
[32m+[m[32m /* Figure out a file name from the URL. */[m
[32m+[m[32m iUrl parts;[m
[32m+[m[32m init_Url(&parts, url);[m
[32m+[m[32m while (startsWith_Rangecc(parts.path, "/")) {[m
[32m+[m[32m parts.path.start++;[m
[32m+[m[32m }[m
[32m+[m[32m while (endsWith_Rangecc(parts.path, "/")) {[m
[32m+[m[32m parts.path.end--;[m
[32m+[m[32m }[m
[32m+[m[32m iString *name = collectNewCStr_String("pagecontent");[m
[32m+[m[32m if (isEmpty_Range(&parts.path)) {[m
[32m+[m[32m if (!isEmpty_Range(&parts.host)) {[m
[32m+[m[32m setRange_String(name, parts.host);[m
[32m+[m[32m replace_Block(&name->chars, '.', '_');[m
[32m+[m[32m }[m
[32m+[m[32m }[m
[32m+[m[32m else {[m
[32m+[m[32m iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,[m
[32m+[m[32m parts.path.end };[m
[32m+[m[32m if (!isEmpty_Range(&fn)) {[m
[32m+[m[32m setRange_String(name, fn);[m
[32m+[m[32m }[m
[32m+[m[32m }[m
[32m+[m[32m if (startsWith_String(name, "~")) {[m
[32m+[m[32m /* This would be interpreted as a reference to a home directory. */[m
[32m+[m[32m remove_Block(&name->chars, 0, 1);[m
[32m+[m[32m }[m
[32m+[m[32m iString *savePath = concat_Path(downloadDir_App(), name);[m
[32m+[m[32m if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {[m
[32m+[m[32m /* No extension specified in URL. */[m
[32m+[m[32m if (startsWith_String(mime, "text/gemini")) {[m
[32m+[m[32m appendCStr_String(savePath, ".gmi");[m
[32m+[m[32m }[m
[32m+[m[32m else if (startsWith_String(mime, "text/")) {[m
[32m+[m[32m appendCStr_String(savePath, ".txt");[m
[32m+[m[32m }[m
[32m+[m[32m else if (startsWith_String(mime, "image/")) {[m
[32m+[m[32m appendCStr_String(savePath, cstr_String(mime) + 6);[m
[32m+[m[32m }[m
[32m+[m[32m }[m
[32m+[m[32m if (fileExists_FileInfo(savePath)) {[m
[32m+[m[32m /* Make it unique. */[m
[32m+[m[32m iDate now;[m
[32m+[m[32m initCurrent_Date(&now);[m
[32m+[m[32m size_t insPos = lastIndexOfCStr_String(savePath, ".");[m
[32m+[m[32m if (insPos == iInvalidPos) {[m
[32m+[m[32m insPos = size_String(savePath);[m
[32m+[m[32m }[m
[32m+[m[32m const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));[m
[32m+[m[32m insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));[m
[32m+[m[32m }[m
[32m+[m[32m /* Write the file. */ {[m
[32m+[m[32m iFile *f = new_File(savePath);[m
[32m+[m[32m if (open_File(f, writeOnly_FileMode)) {[m
[32m+[m[32m write_File(f, content);[m
[32m+[m[32m const size_t size = size_Block(content);[m
[32m+[m[32m const iBool isMega = size >= 1000000;[m
[32m+[m[32m makeMessage_Widget(uiHeading_ColorEscape "FILE SAVED",[m
[32m+[m[32m format_CStr("%s\nSize: %.3f %s", cstr_String(path_File(f)),[m
[32m+[m[32m isMega ? size / 1.0e6f : (size / 1.0e3f),[m
[32m+[m[32m isMega ? "MB" : "KB"));[m
[32m+[m[32m }[m
[32m+[m[32m else {[m
[32m+[m[32m makeMessage_Widget(uiTextCaution_ColorEscape "ERROR SAVING FILE",[m
[32m+[m[32m strerror(errno));[m
[32m+[m[32m }[m
[32m+[m[32m iRelease(f);[m
[32m+[m[32m }[m
[32m+[m[32m delete_String(savePath);[m
[32m+[m[32m}[m
[32m+[m
static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {[m
iWidget *w = as_Widget(d);[m
if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) {[m
[36m@@ -1471,82 +1543,21 @@[m [mstatic iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)[m
return iTrue;[m
}[m
}[m
[32m+[m[32m else if (equalWidget_Command(cmd, w, "document.media.save")) {[m
[32m+[m[32m const iGmLinkId linkId = argLabel_Command(cmd, "link");[m
[32m+[m[32m const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId);[m
[32m+[m[32m if (media) {[m
[32m+[m[32m saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req),[m
[32m+[m[32m body_GmRequest(media->req));[m
[32m+[m[32m }[m
[32m+[m[32m }[m
else if (equal_Command(cmd, "document.save") && document_App() == d) {[m
if (d->request) {[m
makeMessage_Widget(uiTextCaution_ColorEscape "PAGE INCOMPLETE",[m
"The page contents are still being downloaded.");[m
}[m
else if (!isEmpty_Block(&d->sourceContent)) {[m
[31m- /* Figure out a file name from the URL. */[m
[31m- /* TODO: Make this a utility function. */[m
[31m- iUrl parts;[m
[31m- init_Url(&parts, d->mod.url);[m
[31m- while (startsWith_Rangecc(parts.path, "/")) {[m
[31m- parts.path.start++;[m
[31m- }[m
[31m- while (endsWith_Rangecc(parts.path, "/")) {[m
[31m- parts.path.end--;[m
[31m- }[m
[31m- iString *name = collectNewCStr_String("pagecontent");[m
[31m- if (isEmpty_Range(&parts.path)) {[m
[31m- if (!isEmpty_Range(&parts.host)) {[m
[31m- setRange_String(name, parts.host);[m
[31m- replace_Block(&name->chars, '.', '_');[m
[31m- }[m
[31m- }[m
[31m- else {[m
[31m- iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,[m
[31m- parts.path.end };[m
[31m- if (!isEmpty_Range(&fn)) {[m
[31m- setRange_String(name, fn);[m
[31m- }[m
[31m- }[m
[31m- if (startsWith_String(name, "~")) {[m
[31m- /* This would be interpreted as a reference to a home directory. */[m
[31m- remove_Block(&name->chars, 0, 1);[m
[31m- }[m
[31m- iString *savePath = concat_Path(downloadDir_App(), name);[m
[31m- if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {[m
[31m- /* No extension specified in URL. */[m
[31m- if (startsWith_String(&d->sourceMime, "text/gemini")) {[m
[31m- appendCStr_String(savePath, ".gmi");[m
[31m- }[m
[31m- else if (startsWith_String(&d->sourceMime, "text/")) {[m
[31m- appendCStr_String(savePath, ".txt");[m
[31m- }[m
[31m- else if (startsWith_String(&d->sourceMime, "image/")) {[m
[31m- appendCStr_String(savePath, cstr_String(&d->sourceMime) + 6);[m
[31m- }[m
[31m- }[m
[31m- if (fileExists_FileInfo(savePath)) {[m
[31m- /* Make it unique. */[m
[31m- iDate now;[m
[31m- initCurrent_Date(&now);[m
[31m- size_t insPos = lastIndexOfCStr_String(savePath, ".");[m
[31m- if (insPos == iInvalidPos) {[m
[31m- insPos = size_String(savePath);[m
[31m- }[m
[31m- const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));[m
[31m- insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));[m
[31m- }[m
[31m- /* Write the file. */ {[m
[31m- iFile *f = new_File(savePath);[m
[31m- if (open_File(f, writeOnly_FileMode)) {[m
[31m- write_File(f, &d->sourceContent);[m
[31m- const size_t size = size_Block(&d->sourceContent);[m
[31m- const iBool isMega = size >= 1000000;[m
[31m- makeMessage_Widget(uiHeading_ColorEscape "PAGE SAVED",[m
[31m- format_CStr("%s\nSize: %.3f %s", cstr_String(path_File(f)),[m
[31m- isMega ? size / 1.0e6f : (size / 1.0e3f),[m
[31m- isMega ? "MB" : "KB"));[m
[31m- }[m
[31m- else {[m
[31m- makeMessage_Widget(uiTextCaution_ColorEscape "ERROR SAVING PAGE",[m
[31m- strerror(errno));[m
[31m- }[m
[31m- iRelease(f);[m
[31m- }[m
[31m- delete_String(savePath);[m
[32m+[m[32m saveToDownloads_(d->mod.url, &d->sourceMime, &d->sourceContent);[m
}[m
return iTrue;[m
}[m
[36m@@ -1568,6 +1579,12 @@[m [mstatic iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)[m
return iTrue;[m
}[m
else if (equal_Command(cmd, "navigate.back") && document_App() == d) {[m
[32m+[m[32m if (d->request) {[m
[32m+[m[32m postCommandf_App([m
[32m+[m[32m "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));[m
[32m+[m[32m iReleasePtr(&d->request);[m
[32m+[m[32m updateFetchProgress_DocumentWidget_(d);[m
[32m+[m[32m }[m
goBack_History(d->mod.history);[m
return iTrue;[m
}[m
[36m@@ -2093,6 +2110,17 @@[m [mstatic iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e[m
(iMenuItem[]){ { "---", 0, 0, NULL },[m
{ "Copy Link", 0, 0, "document.copylink" } },[m
2);[m
[32m+[m[32m iMediaRequest *mediaReq;[m
[32m+[m[32m if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL) {[m
[32m+[m[32m if (isFinished_GmRequest(mediaReq->req)) {[m
[32m+[m[32m pushBack_Array(&items,[m
[32m+[m[32m &(iMenuItem){ "Save to Downloads",[m
[32m+[m[32m 0,[m
[32m+[m[32m 0,[m
[32m+[m[32m format_CStr("document.media.save link:%u",[m
[32m+[m[32m d->contextLink->linkId) });[m
[32m+[m[32m }[m
[32m+[m[32m }[m
}[m
else {[m
if (!isEmpty_Range(&d->selectMark)) {[m
[36m@@ -2778,7 +2806,6 @@[m [mstatic void draw_DocumentWidget_(const iDocumentWidget *d) {[m
}[m
}[m
endTarget_Paint(&ctx.paint);[m
[31m-// fflush(stdout);[m
}[m
validate_VisBuf(visBuf);[m
clear_PtrSet(d->invalidRuns);[m