diff options
-rw-r--r-- | res/about/version.gmi | 1 | ||||
-rw-r--r-- | src/app.c | 1 | ||||
-rw-r--r-- | src/gmdocument.c | 2 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 169 |
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}. |
@@ -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 | ||
1267 | static 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 | |||
1267 | static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | 1339 | static 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); |