diff options
-rw-r--r-- | res/about/version.gmi | 1 | ||||
-rw-r--r-- | src/app.c | 6 | ||||
-rw-r--r-- | src/feeds.c | 127 | ||||
-rw-r--r-- | src/feeds.h | 9 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 35 | ||||
-rw-r--r-- | src/ui/sidebarwidget.c | 22 |
6 files changed, 161 insertions, 39 deletions
diff --git a/res/about/version.gmi b/res/about/version.gmi index d209d376..85490dc7 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi | |||
@@ -7,6 +7,7 @@ | |||
7 | # Release notes | 7 | # Release notes |
8 | 8 | ||
9 | ## 0.12 | 9 | ## 0.12 |
10 | * Subscribe to new headings on pages. | ||
10 | 11 | ||
11 | ## 0.11 | 12 | ## 0.11 |
12 | * Added feed subscriptions. A subscription is any bookmark with the "subscribed" tag. Subscribed feeds are refreshed in the background while Lagrange is running. | 13 | * Added feed subscriptions. A subscription is any bookmark with the "subscribed" tag. Subscribed feeds are refreshed in the background while Lagrange is running. |
@@ -1090,6 +1090,12 @@ iBool handleCommand_App(const char *cmd) { | |||
1090 | if (gotoHeading.start) { | 1090 | if (gotoHeading.start) { |
1091 | postCommandf_App("document.goto heading:%s", cstr_Rangecc(gotoHeading)); | 1091 | postCommandf_App("document.goto heading:%s", cstr_Rangecc(gotoHeading)); |
1092 | } | 1092 | } |
1093 | const iRangecc gotoUrlHeading = range_Command(cmd, "gotourlheading"); | ||
1094 | if (gotoUrlHeading.start) { | ||
1095 | postCommandf_App("document.goto heading:%s", | ||
1096 | cstrCollect_String(urlDecode_String( | ||
1097 | collect_String(newRange_String(gotoUrlHeading))))); | ||
1098 | } | ||
1093 | } | 1099 | } |
1094 | else if (equal_Command(cmd, "document.request.cancelled")) { | 1100 | else if (equal_Command(cmd, "document.request.cancelled")) { |
1095 | /* TODO: How should cancelled requests be treated in the history? */ | 1101 | /* TODO: How should cancelled requests be treated in the history? */ |
diff --git a/src/feeds.c b/src/feeds.c index 37a4a48b..9df94d05 100644 --- a/src/feeds.c +++ b/src/feeds.c | |||
@@ -27,10 +27,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
27 | #include "app.h" | 27 | #include "app.h" |
28 | 28 | ||
29 | #include <the_Foundation/file.h> | 29 | #include <the_Foundation/file.h> |
30 | #include <the_Foundation/mutex.h> | ||
31 | #include <the_Foundation/hash.h> | 30 | #include <the_Foundation/hash.h> |
32 | #include <the_Foundation/queue.h> | 31 | #include <the_Foundation/intset.h> |
32 | #include <the_Foundation/mutex.h> | ||
33 | #include <the_Foundation/path.h> | 33 | #include <the_Foundation/path.h> |
34 | #include <the_Foundation/queue.h> | ||
34 | #include <the_Foundation/regexp.h> | 35 | #include <the_Foundation/regexp.h> |
35 | #include <the_Foundation/stringset.h> | 36 | #include <the_Foundation/stringset.h> |
36 | #include <the_Foundation/thread.h> | 37 | #include <the_Foundation/thread.h> |
@@ -55,12 +56,36 @@ void deinit_FeedEntry(iFeedEntry *d) { | |||
55 | deinit_String(&d->url); | 56 | deinit_String(&d->url); |
56 | } | 57 | } |
57 | 58 | ||
59 | const iString *url_FeedEntry(const iFeedEntry *d) { | ||
60 | const size_t fragPos = indexOf_String(&d->url, '#'); | ||
61 | if (fragPos != iInvalidPos) { | ||
62 | return collect_String(newRange_String((iRangecc){ constBegin_String(&d->url), | ||
63 | constBegin_String(&d->url) + fragPos })); | ||
64 | } | ||
65 | return &d->url; | ||
66 | } | ||
67 | |||
68 | iBool isUnread_FeedEntry(const iFeedEntry *d) { | ||
69 | const size_t fragPos = indexOf_String(&d->url, '#'); | ||
70 | if (fragPos != iInvalidPos) { | ||
71 | /* Check if the entry is newer than the latest visit. */ | ||
72 | const iTime visTime = urlVisitTime_Visited(visited_App(), url_FeedEntry(d)); | ||
73 | return cmp_Time(&visTime, &d->posted) < 0; | ||
74 | } | ||
75 | if (!containsUrl_Visited(visited_App(), &d->url)) { | ||
76 | return iTrue; | ||
77 | } | ||
78 | return iFalse; | ||
79 | } | ||
80 | |||
58 | /*----------------------------------------------------------------------------------------------*/ | 81 | /*----------------------------------------------------------------------------------------------*/ |
59 | 82 | ||
60 | struct Impl_FeedJob { | 83 | struct Impl_FeedJob { |
61 | iString url; | 84 | iString url; |
62 | uint32_t bookmarkId; | 85 | uint32_t bookmarkId; |
63 | iTime startTime; | 86 | iTime startTime; |
87 | iBool isFirstUpdate; /* hasn't been checked ever before */ | ||
88 | iBool checkHeadings; | ||
64 | iGmRequest *request; | 89 | iGmRequest *request; |
65 | iPtrArray results; | 90 | iPtrArray results; |
66 | }; | 91 | }; |
@@ -71,6 +96,8 @@ static void init_FeedJob(iFeedJob *d, const iBookmark *bookmark) { | |||
71 | d->request = NULL; | 96 | d->request = NULL; |
72 | init_PtrArray(&d->results); | 97 | init_PtrArray(&d->results); |
73 | iZap(d->startTime); | 98 | iZap(d->startTime); |
99 | d->isFirstUpdate = iFalse; | ||
100 | d->checkHeadings = hasTag_Bookmark(bookmark, "headings"); | ||
74 | } | 101 | } |
75 | 102 | ||
76 | static void deinit_FeedJob(iFeedJob *d) { | 103 | static void deinit_FeedJob(iFeedJob *d) { |
@@ -92,6 +119,7 @@ static const int updateIntervalSeconds_Feeds_ = 4 * 60 * 60; | |||
92 | struct Impl_Feeds { | 119 | struct Impl_Feeds { |
93 | iMutex * mtx; | 120 | iMutex * mtx; |
94 | iString saveDir; | 121 | iString saveDir; |
122 | iIntSet previouslyCheckedFeeds; /* bookmark IDs */ | ||
95 | iTime lastRefreshedAt; | 123 | iTime lastRefreshedAt; |
96 | int refreshTimer; | 124 | int refreshTimer; |
97 | iThread * worker; | 125 | iThread * worker; |
@@ -113,7 +141,13 @@ static void submit_FeedJob_(iFeedJob *d) { | |||
113 | 141 | ||
114 | static iBool isSubscribed_(void *context, const iBookmark *bm) { | 142 | static iBool isSubscribed_(void *context, const iBookmark *bm) { |
115 | iUnused(context); | 143 | iUnused(context); |
116 | return indexOfCStr_String(&bm->tags, "subscribed") != iInvalidPos; /* TODO: RegExp with \b */ | 144 | static iRegExp *pattern_ = NULL; |
145 | if (!pattern_) { | ||
146 | pattern_ = new_RegExp("\\bsubscribed\\b", caseSensitive_RegExpOption); | ||
147 | } | ||
148 | iRegExpMatch m; | ||
149 | init_RegExpMatch(&m); | ||
150 | return matchString_RegExp(pattern_, &bm->tags, &m); | ||
117 | } | 151 | } |
118 | 152 | ||
119 | static const iPtrArray *listSubscriptions_(void) { | 153 | static const iPtrArray *listSubscriptions_(void) { |
@@ -181,6 +215,28 @@ static void parseResult_FeedJob_(iFeedJob *d) { | |||
181 | .year = year, .month = month, .day = day, .hour = 12 /* noon UTC */ }); | 215 | .year = year, .month = month, .day = day, .hour = 12 /* noon UTC */ }); |
182 | pushBack_PtrArray(&d->results, entry); | 216 | pushBack_PtrArray(&d->results, entry); |
183 | } | 217 | } |
218 | if (d->checkHeadings) { | ||
219 | init_RegExpMatch(&m); | ||
220 | if (startsWith_Rangecc(line, "#")) { | ||
221 | while (*line.start == '#' && line.start < line.end) { | ||
222 | line.start++; | ||
223 | } | ||
224 | trimStart_Rangecc(&line); | ||
225 | iFeedEntry *entry = new_FeedEntry(); | ||
226 | entry->posted = now; | ||
227 | if (!d->isFirstUpdate) { | ||
228 | entry->discovered = now; | ||
229 | } | ||
230 | entry->bookmarkId = d->bookmarkId; | ||
231 | iString *title = newRange_String(line); | ||
232 | set_String(&entry->title, title); | ||
233 | set_String(&entry->url, &d->url); | ||
234 | appendChar_String(&entry->url, '#'); | ||
235 | append_String(&entry->url, collect_String(urlEncode_String(title))); | ||
236 | delete_String(title); | ||
237 | pushBack_PtrArray(&d->results, entry); | ||
238 | } | ||
239 | } | ||
184 | } | 240 | } |
185 | deinit_String(&src); | 241 | deinit_String(&src); |
186 | iRelease(linkPattern); | 242 | iRelease(linkPattern); |
@@ -207,7 +263,8 @@ static void save_Feeds_(iFeeds *d) { | |||
207 | initCurrent_Time(&now); | 263 | initCurrent_Time(&now); |
208 | iConstForEach(Array, i, &d->entries.values) { | 264 | iConstForEach(Array, i, &d->entries.values) { |
209 | const iFeedEntry *entry = *(const iFeedEntry **) i.value; | 265 | const iFeedEntry *entry = *(const iFeedEntry **) i.value; |
210 | if (secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { | 266 | if (isValid_Time(&entry->discovered) && |
267 | secondsSince_Time(&now, &entry->discovered) > maxAge_Visited) { | ||
211 | continue; /* Forget entries discovered long ago. */ | 268 | continue; /* Forget entries discovered long ago. */ |
212 | } | 269 | } |
213 | format_String(str, "%x\n%llu\n%llu\n%s\n%s\n", | 270 | format_String(str, "%x\n%llu\n%llu\n%s\n%s\n", |
@@ -225,6 +282,10 @@ static void save_Feeds_(iFeeds *d) { | |||
225 | iRelease(f); | 282 | iRelease(f); |
226 | } | 283 | } |
227 | 284 | ||
285 | static iBool isHeadingEntry_FeedEntry_(const iFeedEntry *d) { | ||
286 | return contains_String(&d->url, '#'); | ||
287 | } | ||
288 | |||
228 | static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) { | 289 | static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) { |
229 | iBool gotNew = iFalse; | 290 | iBool gotNew = iFalse; |
230 | lock_Mutex(d->mtx); | 291 | lock_Mutex(d->mtx); |
@@ -234,23 +295,25 @@ static iBool updateEntries_Feeds_(iFeeds *d, iPtrArray *incoming) { | |||
234 | if (locate_SortedArray(&d->entries, &entry, &pos)) { | 295 | if (locate_SortedArray(&d->entries, &entry, &pos)) { |
235 | iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos); | 296 | iFeedEntry *existing = *(iFeedEntry **) at_SortedArray(&d->entries, pos); |
236 | /* Already known, but update it, maybe the time and label have changed. */ | 297 | /* Already known, but update it, maybe the time and label have changed. */ |
237 | iBool changed = iFalse; | 298 | if (!isHeadingEntry_FeedEntry_(existing)) { |
238 | iDate newDate; | 299 | iBool changed = iFalse; |
239 | iDate oldDate; | 300 | iDate newDate; |
240 | init_Date(&newDate, &entry->posted); | 301 | iDate oldDate; |
241 | init_Date(&oldDate, &existing->posted); | 302 | init_Date(&newDate, &entry->posted); |
242 | if (!equalCase_String(&existing->title, &entry->title) || | 303 | init_Date(&oldDate, &existing->posted); |
243 | (newDate.year != oldDate.year || newDate.month != oldDate.month || | 304 | if (!equalCase_String(&existing->title, &entry->title) || |
244 | newDate.day != oldDate.day)) { | 305 | (newDate.year != oldDate.year || newDate.month != oldDate.month || |
245 | changed = iTrue; | 306 | newDate.day != oldDate.day)) { |
246 | } | 307 | changed = iTrue; |
247 | set_String(&existing->title, &entry->title); | 308 | } |
248 | existing->posted = entry->posted; | 309 | set_String(&existing->title, &entry->title); |
249 | delete_FeedEntry(entry); | 310 | existing->posted = entry->posted; |
250 | if (changed) { | 311 | delete_FeedEntry(entry); |
251 | /* TODO: better to use a new flag for read feed entries? */ | 312 | if (changed) { |
252 | removeUrl_Visited(visited_App(), &existing->url); | 313 | /* TODO: better to use a new flag for read feed entries? */ |
253 | gotNew = iTrue; | 314 | removeUrl_Visited(visited_App(), &existing->url); |
315 | gotNew = iTrue; | ||
316 | } | ||
254 | } | 317 | } |
255 | } | 318 | } |
256 | else { | 319 | else { |
@@ -312,7 +375,14 @@ static iBool startWorker_Feeds_(iFeeds *d) { | |||
312 | } | 375 | } |
313 | /* Queue up all the subscriptions for the worker. */ | 376 | /* Queue up all the subscriptions for the worker. */ |
314 | iConstForEach(PtrArray, i, listSubscriptions_()) { | 377 | iConstForEach(PtrArray, i, listSubscriptions_()) { |
315 | iFeedJob* job = new_FeedJob(i.ptr); | 378 | const iBookmark *bm = i.ptr; |
379 | iFeedJob *job = new_FeedJob(bm); | ||
380 | if (!contains_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm))) { | ||
381 | job->isFirstUpdate = iTrue; | ||
382 | printf("first check of %x: %s\n", id_Bookmark(bm), cstr_String(&bm->title)); | ||
383 | fflush(stdout); | ||
384 | insert_IntSet(&d->previouslyCheckedFeeds, id_Bookmark(bm)); | ||
385 | } | ||
316 | pushBack_PtrArray(&d->jobs, job); | 386 | pushBack_PtrArray(&d->jobs, job); |
317 | } | 387 | } |
318 | if (!isEmpty_Array(&d->jobs)) { | 388 | if (!isEmpty_Array(&d->jobs)) { |
@@ -344,7 +414,7 @@ static void stopWorker_Feeds_(iFeeds *d) { | |||
344 | } | 414 | } |
345 | 415 | ||
346 | static int cmp_FeedEntryPtr_(const void *a, const void *b) { | 416 | static int cmp_FeedEntryPtr_(const void *a, const void *b) { |
347 | const iFeedEntry * const *elem[2] = { a, b }; | 417 | const iFeedEntry * const *elem[2] = { a, b }; |
348 | return cmpString_String(&(*elem[0])->url, &(*elem[1])->url); | 418 | return cmpString_String(&(*elem[0])->url, &(*elem[1])->url); |
349 | } | 419 | } |
350 | 420 | ||
@@ -391,6 +461,7 @@ static void load_Feeds_(iFeeds *d) { | |||
391 | node->node.key = id; | 461 | node->node.key = id; |
392 | node->bookmarkId = bookmarkId; | 462 | node->bookmarkId = bookmarkId; |
393 | insert_Hash(feeds, &node->node); | 463 | insert_Hash(feeds, &node->node); |
464 | insert_IntSet(&d->previouslyCheckedFeeds, id); | ||
394 | } | 465 | } |
395 | } | 466 | } |
396 | break; | 467 | break; |
@@ -409,8 +480,9 @@ static void load_Feeds_(iFeeds *d) { | |||
409 | if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { | 480 | if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { |
410 | goto aborted; | 481 | goto aborted; |
411 | } | 482 | } |
412 | const unsigned long long discovered = strtoull(line.start, NULL, 10); | 483 | char *endp = NULL; |
413 | if (discovered == 0) { | 484 | const unsigned long long discovered = strtoull(line.start, &endp, 10); |
485 | if (endp != line.end) { | ||
414 | goto aborted; | 486 | goto aborted; |
415 | } | 487 | } |
416 | if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { | 488 | if (!nextSplit_Rangecc(range_Block(src), "\n", &line)) { |
@@ -457,6 +529,7 @@ void init_Feeds(const char *saveDir) { | |||
457 | iFeeds *d = &feeds_; | 529 | iFeeds *d = &feeds_; |
458 | d->mtx = new_Mutex(); | 530 | d->mtx = new_Mutex(); |
459 | initCStr_String(&d->saveDir, saveDir); | 531 | initCStr_String(&d->saveDir, saveDir); |
532 | init_IntSet(&d->previouslyCheckedFeeds); | ||
460 | iZap(d->lastRefreshedAt); | 533 | iZap(d->lastRefreshedAt); |
461 | d->worker = NULL; | 534 | d->worker = NULL; |
462 | init_PtrArray(&d->jobs); | 535 | init_PtrArray(&d->jobs); |
@@ -483,6 +556,7 @@ void deinit_Feeds(void) { | |||
483 | iFeedEntry **entry = i.value; | 556 | iFeedEntry **entry = i.value; |
484 | delete_FeedEntry(*entry); | 557 | delete_FeedEntry(*entry); |
485 | } | 558 | } |
559 | deinit_IntSet(&d->previouslyCheckedFeeds); | ||
486 | deinit_SortedArray(&d->entries); | 560 | deinit_SortedArray(&d->entries); |
487 | } | 561 | } |
488 | 562 | ||
@@ -554,6 +628,9 @@ const iString *entryListPage_Feeds(void) { | |||
554 | iZap(on); | 628 | iZap(on); |
555 | iConstForEach(PtrArray, i, listEntries_Feeds()) { | 629 | iConstForEach(PtrArray, i, listEntries_Feeds()) { |
556 | const iFeedEntry *entry = i.ptr; | 630 | const iFeedEntry *entry = i.ptr; |
631 | if (isHidden_FeedEntry(entry)) { | ||
632 | continue; /* A hidden entry. */ | ||
633 | } | ||
557 | iDate entryDate; | 634 | iDate entryDate; |
558 | init_Date(&entryDate, &entry->posted); | 635 | init_Date(&entryDate, &entry->posted); |
559 | if (on.year != entryDate.year || on.month != entryDate.month || on.day != entryDate.day) { | 636 | if (on.year != entryDate.year || on.month != entryDate.month || on.day != entryDate.day) { |
diff --git a/src/feeds.h b/src/feeds.h index 85769ba8..7528d111 100644 --- a/src/feeds.h +++ b/src/feeds.h | |||
@@ -35,6 +35,15 @@ struct Impl_FeedEntry { | |||
35 | uint32_t bookmarkId; /* note: runtime only, not a persistent ID */ | 35 | uint32_t bookmarkId; /* note: runtime only, not a persistent ID */ |
36 | }; | 36 | }; |
37 | 37 | ||
38 | iLocalDef iBool isHidden_FeedEntry(const iFeedEntry *d) { | ||
39 | return !isValid_Time(&d->discovered); | ||
40 | } | ||
41 | |||
42 | const iString * url_FeedEntry (const iFeedEntry *); | ||
43 | iBool isUnread_FeedEntry (const iFeedEntry *); | ||
44 | |||
45 | /*----------------------------------------------------------------------------------------------*/ | ||
46 | |||
38 | void init_Feeds (const char *saveDir); | 47 | void init_Feeds (const char *saveDir); |
39 | void deinit_Feeds (void); | 48 | void deinit_Feeds (void); |
40 | void refresh_Feeds (void); | 49 | void refresh_Feeds (void); |
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index cca77c15..96440feb 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c | |||
@@ -171,6 +171,7 @@ struct Impl_DocumentWidget { | |||
171 | const iGmRun * firstVisibleRun; | 171 | const iGmRun * firstVisibleRun; |
172 | const iGmRun * lastVisibleRun; | 172 | const iGmRun * lastVisibleRun; |
173 | iClick click; | 173 | iClick click; |
174 | iString pendingGotoHeading; | ||
174 | float initNormScrollY; | 175 | float initNormScrollY; |
175 | iAnim scrollY; | 176 | iAnim scrollY; |
176 | iAnim sideOpacity; | 177 | iAnim sideOpacity; |
@@ -230,6 +231,7 @@ void init_DocumentWidget(iDocumentWidget *d) { | |||
230 | init_PtrArray(&d->visiblePlayers); | 231 | init_PtrArray(&d->visiblePlayers); |
231 | d->grabbedPlayer = NULL; | 232 | d->grabbedPlayer = NULL; |
232 | d->playerTimer = 0; | 233 | d->playerTimer = 0; |
234 | init_String(&d->pendingGotoHeading); | ||
233 | init_Click(&d->click, d, SDL_BUTTON_LEFT); | 235 | init_Click(&d->click, d, SDL_BUTTON_LEFT); |
234 | addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); | 236 | addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); |
235 | d->menu = NULL; /* created when clicking */ | 237 | d->menu = NULL; /* created when clicking */ |
@@ -259,6 +261,7 @@ void deinit_DocumentWidget(iDocumentWidget *d) { | |||
259 | deinit_Array(&d->outline); | 261 | deinit_Array(&d->outline); |
260 | iRelease(d->media); | 262 | iRelease(d->media); |
261 | iRelease(d->request); | 263 | iRelease(d->request); |
264 | deinit_String(&d->pendingGotoHeading); | ||
262 | deinit_Block(&d->sourceContent); | 265 | deinit_Block(&d->sourceContent); |
263 | deinit_String(&d->sourceMime); | 266 | deinit_String(&d->sourceMime); |
264 | iRelease(d->doc); | 267 | iRelease(d->doc); |
@@ -1070,6 +1073,16 @@ static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool ce | |||
1070 | scroll_DocumentWidget_(d, 0); /* clamp it */ | 1073 | scroll_DocumentWidget_(d, 0); /* clamp it */ |
1071 | } | 1074 | } |
1072 | 1075 | ||
1076 | static void scrollToHeading_DocumentWidget_(iDocumentWidget *d, const char *heading) { | ||
1077 | iConstForEach(Array, h, headings_GmDocument(d->doc)) { | ||
1078 | const iGmHeading *head = h.value; | ||
1079 | if (startsWithCase_Rangecc(head->text, heading)) { | ||
1080 | postCommandf_App("document.goto loc:%p", head->text.start); | ||
1081 | break; | ||
1082 | } | ||
1083 | } | ||
1084 | } | ||
1085 | |||
1073 | static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta, | 1086 | static void scrollWideBlock_DocumentWidget_(iDocumentWidget *d, iInt2 mousePos, int delta, |
1074 | int duration) { | 1087 | int duration) { |
1075 | if (delta == 0) { | 1088 | if (delta == 0) { |
@@ -1623,6 +1636,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1623 | updateSideIconBuf_DocumentWidget_(d); | 1636 | updateSideIconBuf_DocumentWidget_(d); |
1624 | updateOutline_DocumentWidget_(d); | 1637 | updateOutline_DocumentWidget_(d); |
1625 | postCommandf_App("document.changed url:%s", cstr_String(d->mod.url)); | 1638 | postCommandf_App("document.changed url:%s", cstr_String(d->mod.url)); |
1639 | /* Check for a pending goto. */ | ||
1640 | if (!isEmpty_String(&d->pendingGotoHeading)) { | ||
1641 | scrollToHeading_DocumentWidget_(d, cstr_String(&d->pendingGotoHeading)); | ||
1642 | clear_String(&d->pendingGotoHeading); | ||
1643 | } | ||
1626 | return iFalse; | 1644 | return iFalse; |
1627 | } | 1645 | } |
1628 | else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { | 1646 | else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { |
@@ -1779,17 +1797,14 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1779 | return iTrue; | 1797 | return iTrue; |
1780 | } | 1798 | } |
1781 | else if (equal_Command(cmd, "document.goto") && document_App() == d) { | 1799 | else if (equal_Command(cmd, "document.goto") && document_App() == d) { |
1782 | const iRangecc heading = range_Command(cmd, "heading"); | 1800 | const char *heading = suffixPtr_Command(cmd, "heading"); |
1783 | if (heading.start) { | 1801 | if (heading) { |
1784 | const char *target = cstr_Rangecc(heading); | 1802 | if (isRequestOngoing_DocumentWidget(d)) { |
1785 | iConstForEach(Array, h, headings_GmDocument(d->doc)) { | 1803 | /* Scroll position set when request finishes. */ |
1786 | const iGmHeading *head = h.value; | 1804 | setCStr_String(&d->pendingGotoHeading, heading); |
1787 | if (startsWithCase_Rangecc(head->text, target)) { | 1805 | return iTrue; |
1788 | /* TODO: A bit lazy here, the code is right down below. */ | ||
1789 | postCommandf_App("document.goto loc:%p", head->text.start); | ||
1790 | break; | ||
1791 | } | ||
1792 | } | 1806 | } |
1807 | scrollToHeading_DocumentWidget_(d, heading); | ||
1793 | return iTrue; | 1808 | return iTrue; |
1794 | } | 1809 | } |
1795 | const char *loc = pointerLabel_Command(cmd, "loc"); | 1810 | const char *loc = pointerLabel_Command(cmd, "loc"); |
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 6bb6d7a1..5baa08f7 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c | |||
@@ -123,6 +123,9 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { | |||
123 | iZap(on); | 123 | iZap(on); |
124 | iConstForEach(PtrArray, i, listEntries_Feeds()) { | 124 | iConstForEach(PtrArray, i, listEntries_Feeds()) { |
125 | const iFeedEntry *entry = i.ptr; | 125 | const iFeedEntry *entry = i.ptr; |
126 | if (isHidden_FeedEntry(entry)) { | ||
127 | continue; /* A hidden entry. */ | ||
128 | } | ||
126 | /* For more items, one can always see "about:feeds". A large number of items | 129 | /* For more items, one can always see "about:feeds". A large number of items |
127 | is a bit difficult to navigate in the sidebar. */ | 130 | is a bit difficult to navigate in the sidebar. */ |
128 | if (numItems_ListWidget(d->list) == 100) { | 131 | if (numItems_ListWidget(d->list) == 100) { |
@@ -151,9 +154,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { | |||
151 | if (equal_String(docUrl, &entry->url)) { | 154 | if (equal_String(docUrl, &entry->url)) { |
152 | item->listItem.isSelected = iTrue; /* currently being viewed */ | 155 | item->listItem.isSelected = iTrue; /* currently being viewed */ |
153 | } | 156 | } |
154 | if (!containsUrl_Visited(visited_App(), &entry->url)) { | 157 | item->indent = isUnread_FeedEntry(entry); |
155 | item->indent = 1; /* unread */ | ||
156 | } | ||
157 | set_String(&item->url, &entry->url); | 158 | set_String(&item->url, &entry->url); |
158 | set_String(&item->label, &entry->title); | 159 | set_String(&item->label, &entry->title); |
159 | const iBookmark *bm = get_Bookmarks(bookmarks_App(), entry->bookmarkId); | 160 | const iBookmark *bm = get_Bookmarks(bookmarks_App(), entry->bookmarkId); |
@@ -498,7 +499,20 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, const iSidebarItem *it | |||
498 | } | 499 | } |
499 | case feeds_SidebarMode: | 500 | case feeds_SidebarMode: |
500 | if (!isEmpty_String(&item->url)) { | 501 | if (!isEmpty_String(&item->url)) { |
501 | postCommandf_App("open url:%s", cstr_String(&item->url)); | 502 | const size_t fragPos = indexOf_String(&item->url, '#'); |
503 | if (fragPos != iInvalidPos) { | ||
504 | iString *head = collect_String( | ||
505 | newRange_String((iRangecc){ constBegin_String(&item->url) + fragPos + 1, | ||
506 | constEnd_String(&item->url) })); | ||
507 | postCommandf_App( | ||
508 | "open gotourlheading:%s url:%s", | ||
509 | cstr_String(head), | ||
510 | cstr_Rangecc((iRangecc){ constBegin_String(&item->url), | ||
511 | constBegin_String(&item->url) + fragPos })); | ||
512 | } | ||
513 | else { | ||
514 | postCommandf_App("open url:%s", cstr_String(&item->url)); | ||
515 | } | ||
502 | } | 516 | } |
503 | break; | 517 | break; |
504 | case bookmarks_SidebarMode: | 518 | case bookmarks_SidebarMode: |