From 9eb27c0930017dd60d27d2e7f24561c369f5ac65 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 30 Jul 2020 10:03:24 +0300 Subject: History of visited URLs; visualize time of last link visit --- src/app.c | 19 ++--- src/app.h | 4 +- src/gmdocument.c | 30 ++++++- src/gmdocument.h | 4 +- src/history.c | 208 +++++++++++++++++++++++++++++------------------- src/history.h | 5 +- src/ui/documentwidget.c | 22 +++-- 7 files changed, 184 insertions(+), 108 deletions(-) diff --git a/src/app.c b/src/app.c index cff7c7d6..f7f0b044 100644 --- a/src/app.c +++ b/src/app.c @@ -41,7 +41,6 @@ static const char *dataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange"; static const char *dataDir_App_ = "~/.config/lagrange"; #endif static const char *prefsFileName_App_ = "prefs.cfg"; -static const char *historyFileName_App_ = "history.txt"; struct Impl_App { iCommandLine args; @@ -96,10 +95,6 @@ static const iString *prefsFileName_(void) { return collect_String(concatCStr_Path(&iStringLiteral(dataDir_App_), prefsFileName_App_)); } -static const iString *historyFileName_(void) { - return collect_String(concatCStr_Path(&iStringLiteral(dataDir_App_), historyFileName_App_)); -} - static void loadPrefs_App_(iApp *d) { iUnused(d); /* Create the data dir if it doesn't exist yet. */ @@ -148,7 +143,7 @@ static void init_App_(iApp *d, int argc, char **argv) { d->pendingRefresh = iFalse; d->history = new_History(); loadPrefs_App_(d); - load_History(d->history, historyFileName_()); + load_History(d->history, dataDir_App_); #if defined (iHaveLoadEmbed) /* Load the resources from a file. */ { if (!load_Embed( @@ -166,7 +161,7 @@ static void init_App_(iApp *d, int argc, char **argv) { static void deinit_App(iApp *d) { savePrefs_App_(d); - save_History(d->history, historyFileName_()); + save_History(d->history, dataDir_App_); delete_History(d->history); deinit_SortedArray(&d->tickers); delete_Window(d->window); @@ -310,6 +305,10 @@ void addTicker_App(void (*ticker)(iAny *), iAny *context) { insert_SortedArray(&d->tickers, &(iTicker){ context, ticker }); } +const iHistory *history_App(void) { + return app_.history; +} + static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) { setUiScale_Window(get_Window(), @@ -327,11 +326,7 @@ iBool handleCommand_App(const char *cmd) { const iString *url = collect_String(newCStr_String(suffixPtr_Command(cmd, "url"))); if (!argLabel_Command(cmd, "history")) { if (argLabel_Command(cmd, "redirect")) { - /* Update in the history. */ - iHistoryItem *item = item_History(d->history); - if (item) { - set_String(&item->url, url); - } + replace_History(d->history, url); } else { addUrl_History(d->history, url); diff --git a/src/app.h b/src/app.h index 43de8b5a..777692b0 100644 --- a/src/app.h +++ b/src/app.h @@ -6,6 +6,7 @@ #include iDeclareType(Window) +iDeclareType(History) enum iAppEventMode { waitForNewEvents_AppEventMode, @@ -24,7 +25,8 @@ void processEvents_App (enum iAppEventMode mode); iBool handleCommand_App (const char *cmd); void refresh_App (void); -iTime urlVisitTime_App (const iString *url); +const iHistory *history_App (void); + iAny * findWidget_App (const char *id); void addTicker_App (void (*ticker)(iAny *), iAny *context); void postRefresh_App (void); diff --git a/src/gmdocument.c b/src/gmdocument.c index f7016ba4..bd54e6e5 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -4,6 +4,8 @@ #include "ui/text.h" #include "ui/metrics.h" #include "ui/window.h" +#include "history.h" +#include "app.h" #include #include @@ -16,11 +18,13 @@ iDeclareType(GmLink) struct Impl_GmLink { iString url; + iTime when; int flags; }; void init_GmLink(iGmLink *d) { init_String(&d->url); + iZap(d->when); d->flags = 0; } @@ -69,10 +73,13 @@ void deinit_GmImage(iGmImage *d) { iDefineTypeConstructionArgs(GmImage, (const iBlock *data), data) +/*----------------------------------------------------------------------------------------------*/ + struct Impl_GmDocument { iObject object; enum iGmDocumentFormat format; iString source; + iString url; /* for resolving relative links */ iString localHost; iInt2 size; iArray layout; /* contents of source, laid out in document space */ @@ -200,6 +207,12 @@ static iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li delete_String(path); } } + /* Check if visited. */ { + link->when = urlVisitTime_History(history_App(), &link->url); + if (isValid_Time(&link->when)) { + link->flags |= visited_GmLinkFlag; + } + } pushBack_PtrArray(&d->links, link); *linkId = size_PtrArray(&d->links); /* index + 1 */ iRangecc desc = capturedRange_RegExpMatch(&m, 2); @@ -474,6 +487,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { void init_GmDocument(iGmDocument *d) { d->format = gemini_GmDocumentFormat; init_String(&d->source); + init_String(&d->url); init_String(&d->localHost); d->size = zero_I2(); init_Array(&d->layout, sizeof(iGmRun)); @@ -488,6 +502,7 @@ void deinit_GmDocument(iGmDocument *d) { deinit_PtrArray(&d->links); deinit_Array(&d->layout); deinit_String(&d->localHost); + deinit_String(&d->url); deinit_String(&d->source); } @@ -499,6 +514,8 @@ void reset_GmDocument(iGmDocument *d) { clear_PtrArray(&d->images); clearLinks_GmDocument_(d); clear_Array(&d->layout); + clear_String(&d->url); + clear_String(&d->localHost); } void setFormat_GmDocument(iGmDocument *d, enum iGmDocumentFormat format) { @@ -570,8 +587,11 @@ static void normalize_GmDocument(iGmDocument *d) { set_String(&d->source, collect_String(normalized)); } -void setHost_GmDocument(iGmDocument *d, const iString *host) { - set_String(&d->localHost, host); +void setUrl_GmDocument(iGmDocument *d, const iString *url) { + set_String(&d->url, url); + iUrl parts; + init_Url(&parts, url); + setRange_String(&d->localHost, parts.host); } void setSource_GmDocument(iGmDocument *d, const iString *source, int width) { @@ -698,6 +718,12 @@ int linkFlags_GmDocument(const iGmDocument *d, iGmLinkId linkId) { return link ? link->flags : 0; } +const iTime *linkTime_GmDocument(const iGmDocument *d, iGmLinkId linkId) { + const iGmLink *link = link_GmDocument_(d, linkId); + return link ? &link->when : NULL; +} + + uint16_t linkImage_GmDocument(const iGmDocument *d, iGmLinkId linkId) { size_t index = findLinkImage_GmDocument_(d, linkId); if (index != iInvalidPos) { diff --git a/src/gmdocument.h b/src/gmdocument.h index 4997398b..92b9e1cb 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -59,7 +60,7 @@ enum iGmDocumentFormat { void setFormat_GmDocument (iGmDocument *, enum iGmDocumentFormat format); void setWidth_GmDocument (iGmDocument *, int width); -void setHost_GmDocument (iGmDocument *, const iString *host); /* local host name */ +void setUrl_GmDocument (iGmDocument *, const iString *url); void setSource_GmDocument (iGmDocument *, const iString *source, int width); void setImage_GmDocument (iGmDocument *, iGmLinkId linkId, const iString *mime, const iBlock *data); @@ -80,6 +81,7 @@ const iString * linkUrl_GmDocument (const iGmDocument *, iGmLinkId linkId); uint16_t linkImage_GmDocument (const iGmDocument *, iGmLinkId linkId); int linkFlags_GmDocument (const iGmDocument *, iGmLinkId linkId); enum iColorId linkColor_GmDocument (const iGmDocument *, iGmLinkId linkId); +const iTime * linkTime_GmDocument (const iGmDocument *, iGmLinkId linkId); iBool isMediaLink_GmDocument (const iGmDocument *, iGmLinkId linkId); const iString * title_GmDocument (const iGmDocument *); diff --git a/src/history.c b/src/history.c index 0bd23149..884e8592 100644 --- a/src/history.c +++ b/src/history.c @@ -2,9 +2,11 @@ #include "app.h" #include +#include #include -static const size_t maxSize_History_ = 5000; +static const size_t maxStack_History_ = 50; /* back/forward navigable items */ +static const size_t maxAgeVisited_History_ = 3600 * 24 * 30; /* one month */ void init_HistoryItem(iHistoryItem *d) { initCurrent_Time(&d->when); @@ -16,104 +18,119 @@ void deinit_HistoryItem(iHistoryItem *d) { } struct Impl_History { - iArray history; - size_t historyPos; /* zero at the latest item */ + iArray stack; /* TODO: should be specific to a DocumentWidget */ + size_t stackPos; /* zero at the latest item */ iSortedArray visitedUrls; }; iDefineTypeConstruction(History) -static int cmp_VisitedUrls_(const void *a, const void *b) { - const iHistoryItem *elem[2] = { a, b }; - return cmpString_String(&elem[0]->url, &elem[1]->url); +static int cmpUrl_HistoryItem_(const void *a, const void *b) { + return cmpString_String(&((const iHistoryItem *) a)->url, &((const iHistoryItem *) b)->url); } -static int cmpWhen_HistoryItem_(const void *old, const void *ins) { - const iHistoryItem *elem[2] = { old, ins }; - const double tOld = seconds_Time(&elem[0]->when), tIns = seconds_Time(&elem[1]->when); - return tIns > tOld; -} - -static void updateVisitedUrls_History_(iHistory *d) { - clear_SortedArray(&d->visitedUrls); - iConstForEach(Array, i, &d->history) { - insertIf_SortedArray(&d->visitedUrls, &i.value, cmpWhen_HistoryItem_); - } +static int cmpNewer_HistoryItem_(const void *insert, const void *existing) { + return seconds_Time(&((const iHistoryItem *) insert )->when) > + seconds_Time(&((const iHistoryItem *) existing)->when); } void init_History(iHistory *d) { - init_Array(&d->history, sizeof(iHistoryItem)); - d->historyPos = 0; - init_SortedArray(&d->visitedUrls, sizeof(const iHistoryItem *), cmp_VisitedUrls_); + init_Array(&d->stack, sizeof(iHistoryItem)); + d->stackPos = 0; + init_SortedArray(&d->visitedUrls, sizeof(iHistoryItem), cmpUrl_HistoryItem_); } void deinit_History(iHistory *d) { - deinit_SortedArray(&d->visitedUrls); clear_History(d); - deinit_Array(&d->history); + deinit_Array(&d->stack); +} + +static void writeItems_(const iArray *items, iFile *f) { + iString *line = new_String(); + iConstForEach(Array, i, items) { + const iHistoryItem *item = i.value; + iDate date; + init_Date(&date, &item->when); + format_String(line, + "%04d-%02d-%02dT%02d:%02d:%02d %s\n", + date.year, + date.month, + date.day, + date.hour, + date.minute, + date.second, + cstr_String(&item->url)); + writeData_File(f, cstr_String(line), size_String(line)); + } + delete_String(line); } -void save_History(const iHistory *d, const iString *path) { - iFile *f = new_File(path); +void save_History(const iHistory *d, const char *dirPath) { + iFile *f = new_File(concatPath_CStr(dirPath, "recent.txt")); if (open_File(f, writeOnly_FileMode | text_FileMode)) { - iString *line = new_String(); - iConstForEach(Array, i, &d->history) { - const iHistoryItem *item = i.value; - iDate date; - init_Date(&date, &item->when); - format_String(line, - "%04d-%02d-%02dT%02d:%02d:%02d %s\n", - date.year, - date.month, - date.day, - date.hour, - date.minute, - date.second, - cstr_String(&item->url)); - writeData_File(f, cstr_String(line), size_String(line)); - } - delete_String(line); + writeItems_(&d->stack, f); + } + iRelease(f); + f = new_File(concatPath_CStr(dirPath, "visited.txt")); + if (open_File(f, writeOnly_FileMode | text_FileMode)) { + writeItems_(&d->visitedUrls.values, f); } iRelease(f); } -void load_History(iHistory *d, const iString *path) { - iFile *f = new_File(path); - if (open_File(f, readOnly_FileMode | text_FileMode)) { - iString *src = newBlock_String(collect_Block(readAll_File(f))); - const iRangecc range = range_String(src); - iRangecc line = iNullRange; - while (nextSplit_Rangecc(&range, "\n", &line)) { - int y, m, D, H, M, S; - sscanf(line.start, "%04d-%02d-%02dT%02d:%02d:%02d", &y, &m, &D, &H, &M, &S); - if (!y) break; - iHistoryItem item; - init_HistoryItem(&item); - init_Time( - &item.when, - &(iDate){ .year = y, .month = m, .day = D, .hour = H, .minute = M, .second = S }); - initRange_String(&item.url, (iRangecc){ line.start + 20, line.end }); - pushBack_Array(&d->history, &item); +static void loadItems_(iArray *items, iFile *f, double maxAge) { + const iRangecc src = range_Block(collect_Block(readAll_File(f))); + iRangecc line = iNullRange; + iTime now; + initCurrent_Time(&now); + while (nextSplit_Rangecc(&src, "\n", &line)) { + int y, m, D, H, M, S; + sscanf(line.start, "%04d-%02d-%02dT%02d:%02d:%02d", &y, &m, &D, &H, &M, &S); + if (!y) break; + iHistoryItem item; + init_HistoryItem(&item); + init_Time( + &item.when, + &(iDate){ .year = y, .month = m, .day = D, .hour = H, .minute = M, .second = S }); + if (maxAge > 0.0 && secondsSince_Time(&now, &item.when) > maxAge) { + continue; /* Too old. */ } - delete_String(src); + initRange_String(&item.url, (iRangecc){ line.start + 20, line.end }); + pushBack_Array(items, &item); + } +} + +void load_History(iHistory *d, const char *dirPath) { + iFile *f = new_File(concatPath_CStr(dirPath, "recent.txt")); + if (open_File(f, readOnly_FileMode | text_FileMode)) { + loadItems_(&d->stack, f, 0); + } + iRelease(f); + f = new_File(concatPath_CStr(dirPath, "visited.txt")); + if (open_File(f, readOnly_FileMode | text_FileMode)) { + loadItems_(&d->visitedUrls.values, f, maxAgeVisited_History_); } iRelease(f); } void clear_History(iHistory *d) { - iForEach(Array, i, &d->history) { - deinit_HistoryItem(i.value); + iForEach(Array, s, &d->stack) { + deinit_HistoryItem(s.value); } - clear_Array(&d->history); + clear_Array(&d->stack); + iForEach(Array, v, &d->visitedUrls.values) { + deinit_HistoryItem(v.value); + } + clear_SortedArray(&d->visitedUrls); } iHistoryItem *itemAtPos_History(iHistory *d, size_t pos) { - if (isEmpty_Array(&d->history)) return NULL; - return &value_Array(&d->history, size_Array(&d->history) - 1 - pos, iHistoryItem); + if (isEmpty_Array(&d->stack)) return NULL; + return &value_Array(&d->stack, size_Array(&d->stack) - 1 - pos, iHistoryItem); } iHistoryItem *item_History(iHistory *d) { - return itemAtPos_History(d, d->historyPos); + return itemAtPos_History(d, d->stackPos); } const iString *url_History(iHistory *d, size_t pos) { @@ -124,15 +141,39 @@ const iString *url_History(iHistory *d, size_t pos) { return collectNew_String(); } +static void addVisited_History_(iHistory *d, const iString *url) { + iHistoryItem visit; + init_HistoryItem(&visit); + set_String(&visit.url, url); + size_t pos; + if (locate_SortedArray(&d->visitedUrls, &visit, &pos)) { + iHistoryItem *old = at_SortedArray(&d->visitedUrls, pos); + if (cmpNewer_HistoryItem_(&visit, old)) { + old->when = visit.when; + deinit_HistoryItem(&visit); + return; + } + } + insert_SortedArray(&d->visitedUrls, &visit); +} + +void replace_History(iHistory *d, const iString *url) { + /* Update in the history. */ + iHistoryItem *item = item_History(d); + if (item) { + set_String(&item->url, url); + } + addVisited_History_(d, url); +} + void addUrl_History(iHistory *d, const iString *url ){ /* Cut the trailing history items. */ - if (d->historyPos > 0) { - for (size_t i = 0; i < d->historyPos - 1; i++) { + if (d->stackPos > 0) { + for (size_t i = 0; i < d->stackPos - 1; i++) { deinit_HistoryItem(itemAtPos_History(d, i)); } - removeN_Array( - &d->history, size_Array(&d->history) - d->historyPos, iInvalidSize); - d->historyPos = 0; + removeN_Array(&d->stack, size_Array(&d->stack) - d->stackPos, iInvalidSize); + d->stackPos = 0; } /* Insert new item. */ const iHistoryItem *lastItem = itemAtPos_History(d, 0); @@ -140,30 +181,29 @@ void addUrl_History(iHistory *d, const iString *url ){ iHistoryItem item; init_HistoryItem(&item); set_String(&item.url, url); - pushBack_Array(&d->history, &item); - /* Don't make it too long. */ - if (size_Array(&d->history) > maxSize_History_) { - deinit_HistoryItem(front_Array(&d->history)); - remove_Array(&d->history, 0); + pushBack_Array(&d->stack, &item); + /* Limit the number of items. */ + if (size_Array(&d->stack) > maxStack_History_) { + deinit_HistoryItem(front_Array(&d->stack)); + remove_Array(&d->stack, 0); } } + addVisited_History_(d, url); } iBool goBack_History(iHistory *d) { - if (d->historyPos < size_Array(&d->history) - 1) { - d->historyPos++; - postCommandf_App("open history:1 url:%s", - cstr_String(url_History(d, d->historyPos))); + if (d->stackPos < size_Array(&d->stack) - 1) { + d->stackPos++; + postCommandf_App("open history:1 url:%s", cstr_String(url_History(d, d->stackPos))); return iTrue; } return iFalse; } iBool goForward_History(iHistory *d) { - if (d->historyPos > 0) { - d->historyPos--; - postCommandf_App("open history:1 url:%s", - cstr_String(url_History(d, d->historyPos))); + if (d->stackPos > 0) { + d->stackPos--; + postCommandf_App("open history:1 url:%s", cstr_String(url_History(d, d->stackPos))); return iTrue; } return iFalse; @@ -174,7 +214,7 @@ iTime urlVisitTime_History(const iHistory *d, const iString *url) { size_t pos; iZap(item); initCopy_String(&item.url, url); - if (locate_SortedArray(&d->visitedUrls, &(const void *){ &item }, &pos)) { + if (locate_SortedArray(&d->visitedUrls, &item, &pos)) { item.when = ((const iHistoryItem *) constAt_SortedArray(&d->visitedUrls, pos))->when; } deinit_String(&item.url); diff --git a/src/history.h b/src/history.h index 786096f2..4074b1aa 100644 --- a/src/history.h +++ b/src/history.h @@ -17,8 +17,8 @@ iDeclareTypeConstruction(History) void clear_History (iHistory *); -void load_History (iHistory *, const iString *path); -void save_History (const iHistory *, const iString *path); +void load_History (iHistory *, const char *dirPath); +void save_History (const iHistory *, const char *dirPath); iHistoryItem * itemAtPos_History (iHistory *, size_t pos); iHistoryItem * item_History (iHistory *); @@ -27,6 +27,7 @@ iTime urlVisitTime_History(const iHistory *, const iString *url); void print_History (const iHistory *); void addUrl_History (iHistory *, const iString *url); +void replace_History (iHistory *, const iString *url); iBool goBack_History (iHistory *); iBool goForward_History (iHistory *); diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 07761aa7..9bf40e5c 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -264,9 +264,7 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { } static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) { - iUrl parts; - init_Url(&parts, d->url); - setHost_GmDocument(d->doc, collect_String(newRange_String(parts.host))); + setUrl_GmDocument(d->doc, d->url); setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); d->foundMark = iNullRange; d->selectMark = iNullRange; @@ -326,7 +324,7 @@ static void updateSource_DocumentWidget_(iDocumentWidget *d) { iUrl parts; init_Url(&parts, url_GmRequest(d->request)); if (!isEmpty_Range(&parts.path)) { - imageTitle = baseName_Path(collect_String(newRange_String(parts.path))); + imageTitle = baseName_Path(collect_String(newRange_String(parts.path))).start; } format_String( &str, "=> %s %s\n", cstr_String(url_GmRequest(d->request)), imageTitle); @@ -851,7 +849,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { fillRange_DrawContext_(d, run, teal_ColorId, d->widget->foundMark, &d->inFoundMark); fillRange_DrawContext_(d, run, brown_ColorId, d->widget->selectMark, &d->inSelectMark); if (run->linkId && !isEmpty_Rect(run->bounds)) { - fg = white_ColorId; + const int flags = linkFlags_GmDocument(doc, run->linkId); + fg = /*flags & visited_GmLinkFlag ? gray88_ColorId :*/ white_ColorId; if (isHover || linkFlags_GmDocument(doc, run->linkId) & content_GmLinkFlag) { fg = linkColor_GmDocument(doc, run->linkId); } @@ -862,6 +861,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { if (run->linkId) { /* TODO: Show status of an ongoing media request. */ const int flags = linkFlags_GmDocument(doc, run->linkId); + const iRect linkRect = moved_Rect(run->visBounds, origin); if (flags & content_GmLinkFlag) { fg = linkColor_GmDocument(doc, run->linkId); if (!isEmpty_Rect(run->bounds)) { @@ -894,7 +894,6 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { const iBool showHost = (!isEmpty_String(host) && flags & userFriendly_GmLinkFlag); const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; - iRect linkRect = moved_Rect(run->visBounds, origin); if (run->flags & endOfLine_GmRunFlag && (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) || showHost)) { @@ -925,6 +924,17 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { deinit_String(&str); } } + else if (run->flags & endOfLine_GmRunFlag && flags & visited_GmLinkFlag) { + iDate date; + init_Date(&date, linkTime_GmDocument(doc, run->linkId)); + draw_Text(default_FontId, + topRight_Rect(linkRect), + linkColor_GmDocument(doc, run->linkId) - 1, + " \u2014 Visited on %04d-%02d-%02d", + date.year, + date.month, + date.day); + } } // drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId); -- cgit v1.2.3