summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--res/about/version.gmi1
-rw-r--r--src/app.c1
-rw-r--r--src/gmdocument.c2
-rw-r--r--src/ui/documentwidget.c169
4 files changed, 101 insertions, 72 deletions
diff --git a/res/about/version.gmi b/res/about/version.gmi
index 48b4cbd1..c2c47fe4 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -8,6 +8,7 @@
8 8
9## 0.10 9## 0.10
10* 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. 10* 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.
11* Added context menu item to save inline images to Downloads.
11* Added an option to use a proxy server for Gemini requests. 12* Added an option to use a proxy server for Gemini requests.
12* Added a new keyboard link navigation mode focusing on the home row keys. The default keybinding for this is "F". 13* Added a new keyboard link navigation mode focusing on the home row keys. The default keybinding for this is "F".
13* 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}. 14* 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}.
diff --git a/src/app.c b/src/app.c
index b53666c8..1440bab9 100644
--- a/src/app.c
+++ b/src/app.c
@@ -1083,6 +1083,7 @@ iBool handleCommand_App(const char *cmd) {
1083 } 1083 }
1084 setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll")); 1084 setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll"));
1085 setRedirectCount_DocumentWidget(doc, redirectCount); 1085 setRedirectCount_DocumentWidget(doc, redirectCount);
1086 setFlags_Widget(findWidget_App("document.progress"), hidden_WidgetFlag, iTrue);
1086 setUrlFromCache_DocumentWidget(doc, url, isHistory); 1087 setUrlFromCache_DocumentWidget(doc, url, isHistory);
1087 /* Optionally, jump to a text in the document. This will only work if the document 1088 /* Optionally, jump to a text in the document. This will only work if the document
1088 is already available, e.g., it's from "about:" or restored from cache. */ 1089 is already available, e.g., it's from "about:" or restored from cache. */
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 80ddfc9d..c8fc9869 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -1377,7 +1377,7 @@ iBool isMediaLink_GmDocument(const iGmDocument *d, iGmLinkId linkId) {
1377 const iString *dstUrl = absoluteUrl_String(&d->url, linkUrl_GmDocument(d, linkId)); 1377 const iString *dstUrl = absoluteUrl_String(&d->url, linkUrl_GmDocument(d, linkId));
1378 const iRangecc scheme = urlScheme_String(dstUrl); 1378 const iRangecc scheme = urlScheme_String(dstUrl);
1379 if (equalCase_Rangecc(scheme, "gemini") || equalCase_Rangecc(scheme, "gopher") || 1379 if (equalCase_Rangecc(scheme, "gemini") || equalCase_Rangecc(scheme, "gopher") ||
1380 willUseProxy_App(scheme)) { 1380 equalCase_Rangecc(scheme, "file") || willUseProxy_App(scheme)) {
1381 return (linkFlags_GmDocument(d, linkId) & 1381 return (linkFlags_GmDocument(d, linkId) &
1382 (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag)) != 0; 1382 (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag)) != 0;
1383 } 1383 }
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 3cf564ac..a10aa409 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -1264,6 +1264,78 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
1264 return iFalse; 1264 return iFalse;
1265} 1265}
1266 1266
1267static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) {
1268 /* Figure out a file name from the URL. */
1269 iUrl parts;
1270 init_Url(&parts, url);
1271 while (startsWith_Rangecc(parts.path, "/")) {
1272 parts.path.start++;
1273 }
1274 while (endsWith_Rangecc(parts.path, "/")) {
1275 parts.path.end--;
1276 }
1277 iString *name = collectNewCStr_String("pagecontent");
1278 if (isEmpty_Range(&parts.path)) {
1279 if (!isEmpty_Range(&parts.host)) {
1280 setRange_String(name, parts.host);
1281 replace_Block(&name->chars, '.', '_');
1282 }
1283 }
1284 else {
1285 iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,
1286 parts.path.end };
1287 if (!isEmpty_Range(&fn)) {
1288 setRange_String(name, fn);
1289 }
1290 }
1291 if (startsWith_String(name, "~")) {
1292 /* This would be interpreted as a reference to a home directory. */
1293 remove_Block(&name->chars, 0, 1);
1294 }
1295 iString *savePath = concat_Path(downloadDir_App(), name);
1296 if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {
1297 /* No extension specified in URL. */
1298 if (startsWith_String(mime, "text/gemini")) {
1299 appendCStr_String(savePath, ".gmi");
1300 }
1301 else if (startsWith_String(mime, "text/")) {
1302 appendCStr_String(savePath, ".txt");
1303 }
1304 else if (startsWith_String(mime, "image/")) {
1305 appendCStr_String(savePath, cstr_String(mime) + 6);
1306 }
1307 }
1308 if (fileExists_FileInfo(savePath)) {
1309 /* Make it unique. */
1310 iDate now;
1311 initCurrent_Date(&now);
1312 size_t insPos = lastIndexOfCStr_String(savePath, ".");
1313 if (insPos == iInvalidPos) {
1314 insPos = size_String(savePath);
1315 }
1316 const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));
1317 insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));
1318 }
1319 /* Write the file. */ {
1320 iFile *f = new_File(savePath);
1321 if (open_File(f, writeOnly_FileMode)) {
1322 write_File(f, content);
1323 const size_t size = size_Block(content);
1324 const iBool isMega = size >= 1000000;
1325 makeMessage_Widget(uiHeading_ColorEscape "FILE SAVED",
1326 format_CStr("%s\nSize: %.3f %s", cstr_String(path_File(f)),
1327 isMega ? size / 1.0e6f : (size / 1.0e3f),
1328 isMega ? "MB" : "KB"));
1329 }
1330 else {
1331 makeMessage_Widget(uiTextCaution_ColorEscape "ERROR SAVING FILE",
1332 strerror(errno));
1333 }
1334 iRelease(f);
1335 }
1336 delete_String(savePath);
1337}
1338
1267static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 1339static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
1268 iWidget *w = as_Widget(d); 1340 iWidget *w = as_Widget(d);
1269 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) { 1341 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) {
@@ -1471,82 +1543,21 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1471 return iTrue; 1543 return iTrue;
1472 } 1544 }
1473 } 1545 }
1546 else if (equalWidget_Command(cmd, w, "document.media.save")) {
1547 const iGmLinkId linkId = argLabel_Command(cmd, "link");
1548 const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId);
1549 if (media) {
1550 saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req),
1551 body_GmRequest(media->req));
1552 }
1553 }
1474 else if (equal_Command(cmd, "document.save") && document_App() == d) { 1554 else if (equal_Command(cmd, "document.save") && document_App() == d) {
1475 if (d->request) { 1555 if (d->request) {
1476 makeMessage_Widget(uiTextCaution_ColorEscape "PAGE INCOMPLETE", 1556 makeMessage_Widget(uiTextCaution_ColorEscape "PAGE INCOMPLETE",
1477 "The page contents are still being downloaded."); 1557 "The page contents are still being downloaded.");
1478 } 1558 }
1479 else if (!isEmpty_Block(&d->sourceContent)) { 1559 else if (!isEmpty_Block(&d->sourceContent)) {
1480 /* Figure out a file name from the URL. */ 1560 saveToDownloads_(d->mod.url, &d->sourceMime, &d->sourceContent);
1481 /* TODO: Make this a utility function. */
1482 iUrl parts;
1483 init_Url(&parts, d->mod.url);
1484 while (startsWith_Rangecc(parts.path, "/")) {
1485 parts.path.start++;
1486 }
1487 while (endsWith_Rangecc(parts.path, "/")) {
1488 parts.path.end--;
1489 }
1490 iString *name = collectNewCStr_String("pagecontent");
1491 if (isEmpty_Range(&parts.path)) {
1492 if (!isEmpty_Range(&parts.host)) {
1493 setRange_String(name, parts.host);
1494 replace_Block(&name->chars, '.', '_');
1495 }
1496 }
1497 else {
1498 iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,
1499 parts.path.end };
1500 if (!isEmpty_Range(&fn)) {
1501 setRange_String(name, fn);
1502 }
1503 }
1504 if (startsWith_String(name, "~")) {
1505 /* This would be interpreted as a reference to a home directory. */
1506 remove_Block(&name->chars, 0, 1);
1507 }
1508 iString *savePath = concat_Path(downloadDir_App(), name);
1509 if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {
1510 /* No extension specified in URL. */
1511 if (startsWith_String(&d->sourceMime, "text/gemini")) {
1512 appendCStr_String(savePath, ".gmi");
1513 }
1514 else if (startsWith_String(&d->sourceMime, "text/")) {
1515 appendCStr_String(savePath, ".txt");
1516 }
1517 else if (startsWith_String(&d->sourceMime, "image/")) {
1518 appendCStr_String(savePath, cstr_String(&d->sourceMime) + 6);
1519 }
1520 }
1521 if (fileExists_FileInfo(savePath)) {
1522 /* Make it unique. */
1523 iDate now;
1524 initCurrent_Date(&now);
1525 size_t insPos = lastIndexOfCStr_String(savePath, ".");
1526 if (insPos == iInvalidPos) {
1527 insPos = size_String(savePath);
1528 }
1529 const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));
1530 insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));
1531 }
1532 /* Write the file. */ {
1533 iFile *f = new_File(savePath);
1534 if (open_File(f, writeOnly_FileMode)) {
1535 write_File(f, &d->sourceContent);
1536 const size_t size = size_Block(&d->sourceContent);
1537 const iBool isMega = size >= 1000000;
1538 makeMessage_Widget(uiHeading_ColorEscape "PAGE SAVED",
1539 format_CStr("%s\nSize: %.3f %s", cstr_String(path_File(f)),
1540 isMega ? size / 1.0e6f : (size / 1.0e3f),
1541 isMega ? "MB" : "KB"));
1542 }
1543 else {
1544 makeMessage_Widget(uiTextCaution_ColorEscape "ERROR SAVING PAGE",
1545 strerror(errno));
1546 }
1547 iRelease(f);
1548 }
1549 delete_String(savePath);
1550 } 1561 }
1551 return iTrue; 1562 return iTrue;
1552 } 1563 }
@@ -1568,6 +1579,12 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1568 return iTrue; 1579 return iTrue;
1569 } 1580 }
1570 else if (equal_Command(cmd, "navigate.back") && document_App() == d) { 1581 else if (equal_Command(cmd, "navigate.back") && document_App() == d) {
1582 if (d->request) {
1583 postCommandf_App(
1584 "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));
1585 iReleasePtr(&d->request);
1586 updateFetchProgress_DocumentWidget_(d);
1587 }
1571 goBack_History(d->mod.history); 1588 goBack_History(d->mod.history);
1572 return iTrue; 1589 return iTrue;
1573 } 1590 }
@@ -2093,6 +2110,17 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2093 (iMenuItem[]){ { "---", 0, 0, NULL }, 2110 (iMenuItem[]){ { "---", 0, 0, NULL },
2094 { "Copy Link", 0, 0, "document.copylink" } }, 2111 { "Copy Link", 0, 0, "document.copylink" } },
2095 2); 2112 2);
2113 iMediaRequest *mediaReq;
2114 if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL) {
2115 if (isFinished_GmRequest(mediaReq->req)) {
2116 pushBack_Array(&items,
2117 &(iMenuItem){ "Save to Downloads",
2118 0,
2119 0,
2120 format_CStr("document.media.save link:%u",
2121 d->contextLink->linkId) });
2122 }
2123 }
2096 } 2124 }
2097 else { 2125 else {
2098 if (!isEmpty_Range(&d->selectMark)) { 2126 if (!isEmpty_Range(&d->selectMark)) {
@@ -2778,7 +2806,6 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
2778 } 2806 }
2779 } 2807 }
2780 endTarget_Paint(&ctx.paint); 2808 endTarget_Paint(&ctx.paint);
2781// fflush(stdout);
2782 } 2809 }
2783 validate_VisBuf(visBuf); 2810 validate_VisBuf(visBuf);
2784 clear_PtrSet(d->invalidRuns); 2811 clear_PtrSet(d->invalidRuns);