diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/App.js | 64 | ||||
-rw-r--r-- | js/Article.js | 41 | ||||
-rw-r--r-- | js/CommonDialogs.js | 2 | ||||
-rw-r--r-- | js/CommonFilters.js | 34 | ||||
-rw-r--r-- | js/FeedStoreModel.js | 39 | ||||
-rwxr-xr-x | js/FeedTree.js | 158 | ||||
-rw-r--r-- | js/Feeds.js | 72 | ||||
-rwxr-xr-x | js/Headlines.js | 108 | ||||
-rw-r--r-- | js/PluginHost.js | 6 | ||||
-rw-r--r-- | js/PrefHelpers.js | 75 | ||||
-rwxr-xr-x | js/common.js | 2 |
11 files changed, 326 insertions, 275 deletions
@@ -514,9 +514,12 @@ const App = { this.LABEL_BASE_INDEX = parseInt(params[k]); break; case "cdm_auto_catchup": - if (params[k] == 1) { - const hl = App.byId("headlines-frame"); - if (hl) hl.addClassName("auto_catchup"); + { + const headlines = App.byId("headlines-frame"); + + // we could be in preferences + if (headlines) + headlines.setAttribute("data-auto-catchup", params[k] ? "true" : "false"); } break; case "hotkeys": @@ -685,15 +688,16 @@ const App = { checkBrowserFeatures: function() { let errorMsg = ""; - ['MutationObserver'].forEach(function(wf) { - if (!(wf in window)) { - errorMsg = `Browser feature check failed: <code>window.${wf}</code> not found.`; + ['MutationObserver', 'requestIdleCallback'].forEach((t) => { + if (!(t in window)) { + errorMsg = `Browser check failed: <code>window.${t}</code> not found.`; throw new Error(errorMsg); } }); - if (errorMsg) { - this.Error.fatal(errorMsg, {info: navigator.userAgent}); + if (typeof Promise.allSettled == "undefined") { + errorMsg = `Browser check failed: <code>Promise.allSettled</code> is not defined.`; + throw new Error(errorMsg); } return errorMsg == ""; @@ -868,41 +872,44 @@ const App = { }, setWidescreen: function(wide) { const article_id = Article.getActive(); + const headlines_frame = App.byId("headlines-frame"); + const content_insert = dijit.byId("content-insert"); + + // TODO: setStyle stuff should probably be handled by CSS if (wide) { dijit.byId("headlines-wrap-inner").attr("design", 'sidebar'); - dijit.byId("content-insert").attr("region", "trailing"); + content_insert.attr("region", "trailing"); - dijit.byId("content-insert").domNode.setStyle({width: '50%', + content_insert.domNode.setStyle({width: '50%', height: 'auto', borderTopWidth: '0px' }); if (parseInt(Cookie.get("ttrss_ci_width")) > 0) { - dijit.byId("content-insert").domNode.setStyle( + content_insert.domNode.setStyle( {width: Cookie.get("ttrss_ci_width") + "px" }); } - App.byId("headlines-frame").setStyle({ borderBottomWidth: '0px' }); - App.byId("headlines-frame").addClassName("wide"); + headlines_frame.setStyle({ borderBottomWidth: '0px' }); } else { - dijit.byId("content-insert").attr("region", "bottom"); + content_insert.attr("region", "bottom"); - dijit.byId("content-insert").domNode.setStyle({width: 'auto', + content_insert.domNode.setStyle({width: 'auto', height: '50%', borderTopWidth: '0px'}); if (parseInt(Cookie.get("ttrss_ci_height")) > 0) { - dijit.byId("content-insert").domNode.setStyle( + content_insert.domNode.setStyle( {height: Cookie.get("ttrss_ci_height") + "px" }); } - App.byId("headlines-frame").setStyle({ borderBottomWidth: '1px' }); - App.byId("headlines-frame").removeClassName("wide"); - + headlines_frame.setStyle({ borderBottomWidth: '1px' }); } + headlines_frame.setAttribute("data-is-wide-screen", wide ? "true" : "false"); + Article.close(); if (article_id) Article.view(article_id); @@ -931,16 +938,18 @@ const App = { } else { this.hotkey_actions["next_feed"] = () => { - const rv = dijit.byId("feedTree").getNextFeed( + const [feed, is_cat] = Feeds.getNextFeed( Feeds.getActive(), Feeds.activeIsCat()); - if (rv) Feeds.open({feed: rv[0], is_cat: rv[1], delayed: true}) + if (feed !== false) + Feeds.open({feed: feed, is_cat: is_cat, delayed: true}) }; this.hotkey_actions["prev_feed"] = () => { - const rv = dijit.byId("feedTree").getPreviousFeed( + const [feed, is_cat] = Feeds.getPreviousFeed( Feeds.getActive(), Feeds.activeIsCat()); - if (rv) Feeds.open({feed: rv[0], is_cat: rv[1], delayed: true}) + if (feed !== false) + Feeds.open({feed: feed, is_cat: is_cat, delayed: true}) }; this.hotkey_actions["next_article_or_scroll"] = (event) => { if (this.isCombinedMode()) @@ -1102,6 +1111,12 @@ const App = { this.hotkey_actions["feed_reverse"] = () => { Headlines.reverse(); }; + this.hotkey_actions["feed_toggle_grid"] = () => { + xhr.json("backend.php", {op: "rpc", method: "togglepref", key: "CDM_ENABLE_GRID"}, (reply) => { + App.setInitParam("cdm_enable_grid", reply.value); + Headlines.renderAgain(); + }) + }; this.hotkey_actions["feed_toggle_vgroup"] = () => { xhr.post("backend.php", {op: "rpc", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => { Feeds.reloadCurrent(); @@ -1194,6 +1209,9 @@ const App = { Headlines.renderAgain(); }); }; + this.hotkey_actions["article_span_grid"] = () => { + Article.cdmToggleGridSpan(Article.getActive()); + }; } }, openPreferences: function(tab) { diff --git a/js/Article.js b/js/Article.js index ed74051a6..a3a75ba21 100644 --- a/js/Article.js +++ b/js/Article.js @@ -93,6 +93,16 @@ const Article = { w.opener = null; w.location = url; }, + cdmToggleGridSpan: function(id) { + const row = App.byId(`RROW-${id}`); + + if (row) { + row.toggleClassName('grid-span-row'); + + this.setActive(id); + this.cdmMoveToId(id); + } + }, cdmUnsetActive: function (event) { const row = App.byId(`RROW-${Article.getActive()}`); @@ -245,12 +255,12 @@ const Article = { return comments; }, unpack: function(row) { - if (row.hasAttribute("data-content")) { + if (row.getAttribute("data-is-packed") == "1") { console.log("unpacking: " + row.id); const container = row.querySelector(".content-inner"); - container.innerHTML = row.getAttribute("data-content").trim(); + container.innerHTML = row.getAttribute("data-content").trim() + row.getAttribute("data-rendered-enclosures").trim(); dojo.parser.parse(container); @@ -262,18 +272,23 @@ const Article = { if (App.isCombinedMode() && App.byId("main").hasClassName("expandable")) row.setAttribute("data-content-original", row.getAttribute("data-content")); - row.removeAttribute("data-content"); + row.setAttribute("data-is-packed", "0"); PluginHost.run(PluginHost.HOOK_ARTICLE_RENDERED_CDM, row); } }, pack: function(row) { - if (row.hasAttribute("data-content-original")) { + if (row.getAttribute("data-is-packed") != "1") { console.log("packing", row.id); - row.setAttribute("data-content", row.getAttribute("data-content-original")); - row.removeAttribute("data-content-original"); + row.setAttribute("data-is-packed", "1"); + + const content_inner = row.querySelector(".content-inner"); - row.querySelector(".content-inner").innerHTML = " "; + // missing in unexpanded mode + if (content_inner) + content_inner.innerHTML = `<div class="text-center text-muted"> + ${__("Loading, please wait...")} + </div>` } }, view: function (id, no_expand) { @@ -389,10 +404,12 @@ const Article = { const ctr = App.byId("headlines-frame"); const row = App.byId(`RROW-${id}`); - if (!row || !ctr) return; + if (ctr && row) { + const grid_gap = parseInt(window.getComputedStyle(ctr).gridGap) || 0; - if (force_to_top || !App.Scrollable.fitsInContainer(row, ctr)) { - ctr.scrollTop = row.offsetTop; + if (force_to_top || !App.Scrollable.fitsInContainer(row, ctr)) { + ctr.scrollTop = row.offsetTop - grid_gap; + } } }, setActive: function (id) { @@ -401,7 +418,9 @@ const Article = { App.findAll("div[id*=RROW][class*=active]").forEach((row) => { row.removeClassName("active"); - Article.pack(row); + + if (App.isCombinedMode() && !App.getInitParam("cdm_expanded")) + Article.pack(row); }); const row = App.byId(`RROW-${id}`); diff --git a/js/CommonDialogs.js b/js/CommonDialogs.js index ab8441cac..a68dc8068 100644 --- a/js/CommonDialogs.js +++ b/js/CommonDialogs.js @@ -33,7 +33,7 @@ const CommonDialogs = { <section> <fieldset> - <div style='float : right'><img style='display : none' id='feed_add_spinner' src='images/indicator_white.gif'></div> + <div class='pull-right'><img style='display : none' id='feed_add_spinner' src='${App.getInitParam('icon_oval')}'></div> <input style='font-size : 16px; width : 500px;' placeHolder="${__("Feed or site URL")}" dojoType='dijit.form.ValidationTextBox' diff --git a/js/CommonFilters.js b/js/CommonFilters.js index 1450458f8..8a20480f0 100644 --- a/js/CommonFilters.js +++ b/js/CommonFilters.js @@ -38,16 +38,19 @@ const Filters = { console.log("got results:" + result.length); - App.byId("prefFilterProgressMsg").innerHTML = __("Looking for articles (%d processed, %f found)...") - .replace("%f", test_dialog.results) - .replace("%d", offset); + const loading_message = test_dialog.domNode.querySelector(".loading-message"); + const results_list = test_dialog.domNode.querySelector(".filter-results-list"); + + loading_message.innerHTML = __("Looking for articles (%d processed, %f found)...") + .replace("%f", test_dialog.results) + .replace("%d", offset); console.log(offset + " " + test_dialog.max_offset); for (let i = 0; i < result.length; i++) { - const tmp = dojo.create("table", { innerHTML: result[i]}); + const tmp = dojo.create("div", { innerHTML: result[i]}); - App.byId("prefFilterTestResultList").innerHTML += tmp.innerHTML; + results_list.innerHTML += tmp.innerHTML; } if (test_dialog.results < 30 && offset < test_dialog.max_offset) { @@ -60,14 +63,15 @@ const Filters = { } else { // all done - Element.hide("prefFilterLoadingIndicator"); + test_dialog.domNode.querySelector(".loading-indicator").hide(); if (test_dialog.results == 0) { - App.byId("prefFilterTestResultList").innerHTML = `<tr><td align='center'> - ${__('No recent articles matching this filter have been found.')}</td></tr>`; - App.byId("prefFilterProgressMsg").innerHTML = "Articles matching this filter:"; + results_list.innerHTML = `<li class="text-center text-muted"> + ${__('No recent articles matching this filter have been found.')}</li>`; + + loading_message.innerHTML = __("Articles matching this filter:"); } else { - App.byId("prefFilterProgressMsg").innerHTML = __("Found %d articles matching this filter:") + loading_message.innerHTML = __("Found %d articles matching this filter:") .replace("%d", test_dialog.results); } @@ -75,7 +79,7 @@ const Filters = { } else if (!result) { console.log("getTestResults: can't parse results object"); - Element.hide("prefFilterLoadingIndicator"); + test_dialog.domNode.querySelector(".loading-indicator").hide(); Notify.error("Error while trying to get filter test results."); } else { console.log("getTestResults: dialog closed, bailing out."); @@ -86,12 +90,12 @@ const Filters = { }); }, content: ` - <div> - <img id='prefFilterLoadingIndicator' src='images/indicator_tiny.gif'> - <span id='prefFilterProgressMsg'>Looking for articles...</span> + <div class="text-muted"> + <img class="loading-indicator icon-three-dots" src="${App.getInitParam("icon_three_dots")}"> + <span class="loading-message">${__("Looking for articles...")}</span> </div> - <ul class='panel panel-scrollable list list-unstyled' id='prefFilterTestResultList'></ul> + <ul class='panel panel-scrollable list list-unstyled filter-results-list'></ul> <footer class='text-center'> <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>${__('Close this window')}</button> diff --git a/js/FeedStoreModel.js b/js/FeedStoreModel.js index 736bfbed6..befc441af 100644 --- a/js/FeedStoreModel.js +++ b/js/FeedStoreModel.js @@ -54,45 +54,6 @@ define(["dojo/_base/declare", "dijit/tree/ForestStoreModel"], function (declare) if (treeItem) return this.store.setValue(treeItem, key, value); }, - getNextUnreadFeed: function (feed, is_cat) { - if (!this.store._itemsByIdentity) - return null; - - let treeItem; - - if (is_cat) { - treeItem = this.store._itemsByIdentity['CAT:' + feed]; - } else { - treeItem = this.store._itemsByIdentity['FEED:' + feed]; - } - - const items = this.store._arrayOfAllItems; - - for (let i = 0; i < items.length; i++) { - if (items[i] == treeItem) { - - for (let j = i + 1; j < items.length; j++) { - const unread = this.store.getValue(items[j], 'unread'); - const id = this.store.getValue(items[j], 'id'); - - if (unread > 0 && ((is_cat && id.match("CAT:")) || (!is_cat && id.match("FEED:")))) { - if (!is_cat || !(this.store.hasAttribute(items[j], 'parent_id') && this.store.getValue(items[j], 'parent_id') == feed)) return items[j]; - } - } - - for (let j = 0; j < i; j++) { - const unread = this.store.getValue(items[j], 'unread'); - const id = this.store.getValue(items[j], 'id'); - - if (unread > 0 && ((is_cat && id.match("CAT:")) || (!is_cat && id.match("FEED:")))) { - if (!is_cat || !(this.store.hasAttribute(items[j], 'parent_id') && this.store.getValue(items[j], 'parent_id') == feed)) return items[j]; - } - } - } - } - - return null; - }, hasCats: function () { if (this.store && this.store._itemsByIdentity) return this.store._itemsByIdentity['CAT:-1'] != undefined; diff --git a/js/FeedTree.js b/js/FeedTree.js index 17cd3deea..b81638c39 100755 --- a/js/FeedTree.js +++ b/js/FeedTree.js @@ -82,6 +82,9 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co } if (id.match("FEED:")) { + tnode.rowNode.setAttribute('data-feed-id', bare_id); + tnode.rowNode.setAttribute('data-is-cat', "false"); + const menu = new dijit.Menu(); menu.row_id = bare_id; @@ -132,10 +135,18 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co } if (id.match("CAT:")) { - tnode.loadingNode = dojo.create('img', { className: 'loadingNode', src: 'images/blank_icon.gif'}); + tnode.rowNode.setAttribute('data-feed-id', bare_id); + tnode.rowNode.setAttribute('data-is-cat', "true"); + + tnode.loadingNode = dojo.create('img', { className: 'loadingNode', src: App.getInitParam('icon_blank')}); domConstruct.place(tnode.loadingNode, tnode.labelNode, 'after'); } + if (id.match("FEED:")) { + tnode.loadingNode = dojo.create('img', { className: 'loadingNode', src: App.getInitParam('icon_blank')}); + domConstruct.place(tnode.loadingNode, tnode.expandoNode, 'only'); + } + if (id.match("CAT:") && bare_id == -1) { const menu = new dijit.Menu(); menu.row_id = bare_id; @@ -191,10 +202,15 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co return (item.unread <= 0) ? "dijitTreeLabel" : "dijitTreeLabel Unread"; }, getRowClass: function (item/*, opened */) { - let rc = "dijitTreeRow"; + let rc = "dijitTreeRow dijitTreeRowFlex"; const is_cat = String(item.id).indexOf('CAT:') != -1; + if (is_cat) + rc += " Is_Cat"; + else + rc += " Is_Feed"; + if (!is_cat && item.error != '') rc += " Error"; if (item.unread > 0) rc += " Unread"; if (item.auxcounter > 0) rc += " Has_Aux"; @@ -303,7 +319,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co }, 0); } }, - setFeedIcon: function(feed, is_cat, src) { + setIcon: function(feed, is_cat, src) { let treeNode; if (is_cat) @@ -313,13 +329,19 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co if (treeNode) { treeNode = treeNode[0]; - const icon = dojo.create('img', { src: src, className: 'icon' }); - domConstruct.place(icon, treeNode.iconNode, 'only'); - return true; + + // could be <i material> + const icon = treeNode.iconNode.querySelector('img.icon'); + + if (icon) { + icon.src = src; + + return true; + } } return false; }, - setFeedExpandoIcon: function(feed, is_cat, src) { + showLoading: function(feed, is_cat, show) { let treeNode; if (is_cat) @@ -329,14 +351,17 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co if (treeNode) { treeNode = treeNode[0]; - if (treeNode.loadingNode) { - treeNode.loadingNode.src = src; - return true; + + if (show) { + treeNode.loadingNode.addClassName("visible"); + treeNode.loadingNode.setAttribute("src", + is_cat ? App.getInitParam("icon_three_dots") : App.getInitParam("icon_oval")); } else { - const icon = dojo.create('img', { src: src, className: 'loadingExpando' }); - domConstruct.place(icon, treeNode.expandoNode, 'only'); - return true; + treeNode.loadingNode.removeClassName("visible"); + treeNode.loadingNode.setAttribute("src", App.getInitParam("icon_blank")) } + + return true } return false; @@ -360,7 +385,28 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co } }, - getNextFeed: function (feed, is_cat) { + getNextUnread: function(feed, is_cat) { + return this.getNextFeed(feed, is_cat, true); + }, + _nextTreeItemFromIndex: function (start, unread_only) { + const items = this.model.store._arrayOfAllItems; + + for (let i = start+1; i < items.length; i++) { + const id = String(items[i].id); + const box = this._itemNodesMap[id]; + const unread = parseInt(items[i].unread); + + if (box && (!unread_only || unread > 0)) { + const row = box[0].rowNode; + const cat = box[0].rowNode.parentNode.parentNode; + + if (Element.visible(cat) && Element.visible(row)) { + return items[i]; + } + } + } + }, + getNextFeed: function (feed, is_cat, unread_only = false) { let treeItem; if (is_cat) { @@ -370,35 +416,40 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co } const items = this.model.store._arrayOfAllItems; - let item = items[0]; + const start = items.indexOf(treeItem); - for (let i = 0; i < items.length; i++) { - if (items[i] == treeItem) { + if (start != -1) { + let item = this._nextTreeItemFromIndex(start, unread_only); - for (let j = i+1; j < items.length; j++) { - const id = String(items[j].id); - const box = this._itemNodesMap[id]; + // let's try again from the top + // 0 (instead of -1) to skip Special category + if (!item) { + item = this._nextTreeItemFromIndex(0, unread_only); + } - if (box) { - const row = box[0].rowNode; - const cat = box[0].rowNode.parentNode.parentNode; + if (item) + return [this.model.store.getValue(item, 'bare_id'), + !this.model.store.getValue(item, 'id').match('FEED:')]; + } - if (Element.visible(cat) && Element.visible(row)) { - item = items[j]; - break; - } - } + return [false, false]; + }, + _prevTreeItemFromIndex: function (start) { + const items = this.model.store._arrayOfAllItems; + + for (let i = start-1; i > 0; i--) { + const id = String(items[i].id); + const box = this._itemNodesMap[id]; + + if (box) { + const row = box[0].rowNode; + const cat = box[0].rowNode.parentNode.parentNode; + + if (Element.visible(cat) && Element.visible(row)) { + return items[i]; } - break; } } - - if (item) { - return [this.model.store.getValue(item, 'bare_id'), - !this.model.store.getValue(item, 'id').match('FEED:')]; - } else { - return false; - } }, getPreviousFeed: function (feed, is_cat) { let treeItem; @@ -410,37 +461,22 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co } const items = this.model.store._arrayOfAllItems; - let item = items[0] == treeItem ? items[items.length-1] : items[0]; - - for (let i = 0; i < items.length; i++) { - if (items[i] == treeItem) { - - for (let j = i-1; j > 0; j--) { - const id = String(items[j].id); - const box = this._itemNodesMap[id]; - - if (box) { - const row = box[0].rowNode; - const cat = box[0].rowNode.parentNode.parentNode; + const start = items.indexOf(treeItem); - if (Element.visible(cat) && Element.visible(row)) { - item = items[j]; - break; - } - } + if (start != -1) { + let item = this._prevTreeItemFromIndex(start); - } - break; + // wrap from the bottom + if (!item) { + item = this._prevTreeItemFromIndex(items.length); } - } - if (item) { - return [this.model.store.getValue(item, 'bare_id'), - !this.model.store.getValue(item, 'id').match('FEED:')]; - } else { - return false; + if (item) + return [this.model.store.getValue(item, 'bare_id'), + !this.model.store.getValue(item, 'id').match('FEED:')]; } + return [false, false]; }, getFeedCategory: function(feed) { try { diff --git a/js/Feeds.js b/js/Feeds.js index 33a1fa3dc..befd7e46e 100644 --- a/js/Feeds.js +++ b/js/Feeds.js @@ -113,7 +113,7 @@ const Feeds = { this.hideOrShowFeeds(App.getInitParam("hide_read_feeds")); this._counters_prev = elems; - PluginHost.run(PluginHost.HOOK_COUNTERS_PROCESSED); + PluginHost.run(PluginHost.HOOK_COUNTERS_PROCESSED, elems); }, reloadCurrent: function(method) { if (this.getActive() != undefined) { @@ -133,9 +133,10 @@ const Feeds = { return Feeds.reloadCurrent(''); }, openNextUnread: function() { - const is_cat = this.activeIsCat(); - const nuf = this.getNextUnread(this.getActive(), is_cat); - if (nuf) this.open({feed: nuf, is_cat: is_cat}); + const [feed, is_cat] = this.getNextUnread(this.getActive(), this.activeIsCat()); + + if (feed !== false) + this.open({feed: feed, is_cat: is_cat}); }, toggle: function() { Element.toggle("feeds-holder"); @@ -311,18 +312,22 @@ const Feeds = { setActive: function(id, is_cat) { console.log('setActive', id, is_cat); - if ('requestIdleCallback' in window) - window.requestIdleCallback(() => { - App.Hash.set({f: id, c: is_cat ? 1 : 0}); - }); - else + window.requestIdleCallback(() => { App.Hash.set({f: id, c: is_cat ? 1 : 0}); + }); this._active_feed_id = id; this._active_feed_is_cat = is_cat; - App.byId("headlines-frame").setAttribute("feed-id", id); - App.byId("headlines-frame").setAttribute("is-cat", is_cat ? 1 : 0); + const container = App.byId("headlines-frame"); + + // TODO @deprecated: these two should be removed (replaced with data- attributes below) + container.setAttribute("feed-id", id); + container.setAttribute("is-cat", is_cat ? 1 : 0); + // ^ + + container.setAttribute("data-feed-id", id); + container.setAttribute("data-is-cat", is_cat ? "true" : "false"); this.select(id, is_cat); @@ -395,21 +400,20 @@ const Feeds = { query.m = "ForceUpdate"; } - if (!delayed) - if (!this.setExpando(feed, is_cat, - (is_cat) ? 'images/indicator_tiny.gif' : 'images/indicator_white.gif')) - Notify.progress("Loading, please wait...", true); - query.cat = is_cat; this.setActive(feed, is_cat); window.clearTimeout(this._viewfeed_wait_timeout); this._viewfeed_wait_timeout = window.setTimeout(() => { + + this.showLoading(feed, is_cat, true); + //Notify.progress("Loading, please wait...", true);*/ + xhr.json("backend.php", query, (reply) => { try { window.clearTimeout(this._infscroll_timeout); - this.setExpando(feed, is_cat, 'images/blank_icon.gif'); + this.showLoading(feed, is_cat, false); Headlines.onLoaded(reply, offset, append); PluginHost.run(PluginHost.HOOK_FEED_LOADED, [feed, is_cat]); } catch (e) { @@ -475,10 +479,10 @@ const Feeds = { // only select next unread feed if catching up entirely (as opposed to last week etc) if (show_next_feed && !mode) { - const nuf = this.getNextUnread(feed, is_cat); + const [next_feed, next_is_cat] = this.getNextUnread(feed, is_cat); - if (nuf) { - this.open({feed: nuf, is_cat: is_cat}); + if (next_feed !== false) { + this.open({feed: next_feed, is_cat: next_is_cat}); } } else if (feed == this.getActive() && is_cat == this.activeIsCat()) { this.reloadCurrent(); @@ -522,7 +526,7 @@ const Feeds = { const tree = dijit.byId("feedTree"); if (tree && tree.model) - return tree._cat_of_feed(feed); + return tree.getFeedCategory(feed); } catch (e) { // @@ -570,21 +574,35 @@ const Feeds = { setIcon: function(feed, is_cat, src) { const tree = dijit.byId("feedTree"); - if (tree) return tree.setFeedIcon(feed, is_cat, src); + if (tree) return tree.setIcon(feed, is_cat, src); }, - setExpando: function(feed, is_cat, src) { + showLoading: function(feed, is_cat, show) { const tree = dijit.byId("feedTree"); - if (tree) return tree.setFeedExpandoIcon(feed, is_cat, src); + if (tree) return tree.showLoading(feed, is_cat, show); return false; }, + getNextFeed: function(feed, is_cat) { + const tree = dijit.byId("feedTree"); + + if (tree) return tree.getNextFeed(feed, is_cat, false); + + return [false, false]; + }, + getPreviousFeed: function(feed, is_cat) { + const tree = dijit.byId("feedTree"); + + if (tree) return tree.getPreviousFeed(feed, is_cat); + + return [false, false]; + }, getNextUnread: function(feed, is_cat) { const tree = dijit.byId("feedTree"); - const nuf = tree.model.getNextUnreadFeed(feed, is_cat); - if (nuf) - return tree.model.store.getValue(nuf, 'bare_id'); + if (tree) return tree.getNextUnread(feed, is_cat); + + return [false, false]; }, search: function() { xhr.json("backend.php", diff --git a/js/Headlines.js b/js/Headlines.js index 28e43be1f..d01993838 100755 --- a/js/Headlines.js +++ b/js/Headlines.js @@ -17,17 +17,27 @@ const Headlines = { sticky_header_observer: new IntersectionObserver( (entries, observer) => { entries.forEach((entry) => { - const header = entry.target.nextElementSibling; + const header = entry.target.closest('.cdm').querySelector(".header"); - if (entry.intersectionRatio == 0) { - header.setAttribute("stuck", "1"); - - } else if (entry.intersectionRatio == 1) { - header.removeAttribute("stuck"); + if (entry.isIntersecting) { + header.removeAttribute("data-is-stuck"); + } else { + header.setAttribute("data-is-stuck", "true"); } - //console.log(entry.target, header, entry.intersectionRatio); + //console.log(entry.target, entry.intersectionRatio, entry.isIntersecting, entry.boundingClientRect.top); + }); + }, + {threshold: [0, 1], root: document.querySelector("#headlines-frame")} + ), + sticky_content_observer: new IntersectionObserver( + (entries, observer) => { + entries.forEach((entry) => { + const header = entry.target.closest('.cdm').querySelector(".header"); + + header.style.position = entry.isIntersecting ? "sticky" : "unset"; + //console.log(entry.target, entry.intersectionRatio, entry.isIntersecting, entry.boundingClientRect.top); }); }, {threshold: [0, 1], root: document.querySelector("#headlines-frame")} @@ -72,14 +82,13 @@ const Headlines = { } }); + PluginHost.run(PluginHost.HOOK_HEADLINE_MUTATIONS, mutations); + Headlines.updateSelectedPrompt(); - if ('requestIdleCallback' in window) - window.requestIdleCallback(() => { - Headlines.syncModified(modified); - }); - else + window.requestIdleCallback(() => { Headlines.syncModified(modified); + }); }), syncModified: function (modified) { const ops = { @@ -173,14 +182,14 @@ const Headlines = { }); } - Promise.all(promises).then((results) => { + Promise.allSettled(promises).then((results) => { let feeds = []; let labels = []; results.forEach((res) => { if (res) { try { - const obj = JSON.parse(res); + const obj = JSON.parse(res.value); if (obj.feeds) feeds = feeds.concat(obj.feeds); @@ -198,6 +207,8 @@ const Headlines = { console.log('requesting counters for', feeds, labels); Feeds.requestCounters(feeds, labels); } + + PluginHost.run(PluginHost.HOOK_HEADLINE_MUTATIONS_SYNCED, results); }); }, click: function (event, id, in_body) { @@ -340,8 +351,7 @@ const Headlines = { // invoke lazy load if last article in buffer is nearly visible OR is active if (Article.getActive() == last_row.getAttribute("data-article-id") || last_row.offsetTop - 250 <= container.scrollTop + container.offsetHeight) { - hsp.innerHTML = "<span class='loading'><img src='images/indicator_tiny.gif'> " + - __("Loading, please wait...") + "</span>"; + hsp.innerHTML = `<span class='text-muted text-small text-center'><img class="icon-three-dots" src="${App.getInitParam('icon_three_dots')}"> ${__("Loading, please wait...")}</span>`; Headlines.loadMore(); return; @@ -371,6 +381,9 @@ const Headlines = { } } } + + PluginHost.run(PluginHost.HOOK_HEADLINES_SCROLL_HANDLER); + } catch (e) { console.warn("scrollHandler", e); } @@ -378,11 +391,17 @@ const Headlines = { objectById: function (id) { return this.headlines[id]; }, - setCommonClasses: function () { - App.byId("headlines-frame").removeClassName("cdm"); - App.byId("headlines-frame").removeClassName("normal"); + setCommonClasses: function (headlines_count) { + const container = App.byId("headlines-frame"); + + container.removeClassName("cdm"); + container.removeClassName("normal"); - App.byId("headlines-frame").addClassName(App.isCombinedMode() ? "cdm" : "normal"); + container.addClassName(App.isCombinedMode() ? "cdm" : "normal"); + container.setAttribute("data-enable-grid", App.getInitParam("cdm_enable_grid") ? "true" : "false"); + container.setAttribute("data-headlines-count", parseInt(headlines_count)); + container.setAttribute("data-is-cdm", App.isCombinedMode() ? "true" : "false"); + container.setAttribute("data-is-cdm-expanded", App.getInitParam("cdm_expanded")); // for floating title because it's placed outside of headlines-frame App.byId("main").removeClassName("expandable"); @@ -393,7 +412,7 @@ const Headlines = { }, renderAgain: function () { // TODO: wrap headline elements into a knockoutjs model to prevent all this stuff - Headlines.setCommonClasses(); + Headlines.setCommonClasses(this.headlines.filter((h) => h.id).length); App.findAll("#headlines-frame > div[id*=RROW]").forEach((row) => { const id = row.getAttribute("data-article-id"); @@ -422,11 +441,18 @@ const Headlines = { this.sticky_header_observer.observe(e) }); + App.findAll(".cdm .content").forEach((e) => { + this.sticky_content_observer.observe(e) + }); + if (App.getInitParam("cdm_expanded")) App.findAll("#headlines-frame > div[id*=RROW].cdm").forEach((e) => { this.unpack_observer.observe(e) }); + dijit.byId('main').resize(); + + PluginHost.run(PluginHost.HOOK_HEADLINES_RENDERED); }, render: function (headlines, hl) { let row = null; @@ -464,7 +490,9 @@ const Headlines = { id="RROW-${hl.id}" data-article-id="${hl.id}" data-orig-feed-id="${hl.feed_id}" + data-is-packed="1" data-content="${App.escapeHtml(hl.content)}" + data-rendered-enclosures="${App.escapeHtml(Article.renderEnclosures(hl.enclosures))}" data-score="${hl.score}" data-article-title="${App.escapeHtml(hl.title)}" onmouseover="Article.mouseIn(${hl.id})" @@ -494,9 +522,10 @@ const Headlines = { <span class="updated" title="${hl.imported}">${hl.updated}</span> <div class="right"> + <i class="material-icons icon-grid-span" title="${__("Span all columns")}" onclick="Article.cdmToggleGridSpan(${hl.id})">fullscreen</i> <i class="material-icons icon-score" title="${hl.score}" onclick="Article.setScore(${hl.id}, this)">${Article.getScorePic(hl.score)}</i> - <span style="cursor : pointer" title="${App.escapeHtml(hl.feed_title)}" onclick="Feeds.open({feed:${hl.feed_id}})"> + <span class="icon-feed" title="${App.escapeHtml(hl.feed_title)}" onclick="Feeds.open({feed:${hl.feed_id}})"> ${Feeds.renderIcon(hl.feed_id, hl.has_icon)} </span> </div> @@ -506,11 +535,14 @@ const Headlines = { <div class="content" onclick="return Headlines.click(event, ${hl.id}, true);"> ${Article.renderNote(hl.id, hl.note)} <div class="content-inner" lang="${hl.lang ? hl.lang : 'en'}"> - <img src="${App.getInitParam('icon_indicator_white')}"> - </div> - <div class="intermediate"> - ${Article.renderEnclosures(hl.enclosures)} + <div class="text-center text-muted"> + ${__("Loading, please wait...")} + </div> </div> + + <!-- intermediate: unstyled, kept for compatibility --> + <div class="intermediate"></div> + <div class="footer" onclick="event.stopPropagation()"> <div class="left"> @@ -560,7 +592,7 @@ const Headlines = { </div> <div class="right"> <i class="material-icons icon-score" title="${hl.score}" onclick="Article.setScore(${hl.id}, this)">${Article.getScorePic(hl.score)}</i> - <span onclick="Feeds.open({feed:${hl.feed_id}})" style="cursor : pointer" title="${App.escapeHtml(hl.feed_title)}">${Feeds.renderIcon(hl.feed_id, hl.has_icon)}</span> + <span onclick="Feeds.open({feed:${hl.feed_id}})" class="icon-feed" title="${App.escapeHtml(hl.feed_title)}">${Feeds.renderIcon(hl.feed_id, hl.has_icon)}</span> </div> </div> `; @@ -614,7 +646,7 @@ const Headlines = { </span> <span class='right'> <span id='selected_prompt'></span> - <div dojoType='fox.form.DropDownButton' title='"${__('Select articles')}'> + <div class='select-articles-dropdown' dojoType='fox.form.DropDownButton' title='"${__('Select articles')}'> <span>${__("Select...")}</span> <div dojoType='dijit.Menu' style='display: none;'> <div dojoType='dijit.MenuItem' onclick='Headlines.select("all")'>${__('All')}</div> @@ -671,11 +703,15 @@ const Headlines = { console.log('infscroll_disabled=', Feeds.infscroll_disabled); // also called in renderAgain() after view mode switch - Headlines.setCommonClasses(); + Headlines.setCommonClasses(headlines_count); + /** TODO: remove @deprecated */ App.byId("headlines-frame").setAttribute("is-vfeed", reply['headlines']['is_vfeed'] ? 1 : 0); + App.byId("headlines-frame").setAttribute("data-is-vfeed", + reply['headlines']['is_vfeed'] ? "true" : "false"); + Article.setActive(0); try { @@ -716,6 +752,9 @@ const Headlines = { hsp.id = "headlines-spacer"; } + // clear out hsp contents in case there's a power-hungry svg icon rotating there + hsp.innerHTML = ""; + dijit.byId('headlines-frame').domNode.appendChild(hsp); this.initHeadlinesMenu(); @@ -767,6 +806,9 @@ const Headlines = { hsp.id = "headlines-spacer"; } + // clear out hsp contents in case there's a power-hungry svg icon rotating there + hsp.innerHTML = ""; + c.domNode.appendChild(hsp); this.initHeadlinesMenu(); @@ -799,6 +841,10 @@ const Headlines = { this.sticky_header_observer.observe(e) }); + App.findAll(".cdm .content").forEach((e) => { + this.sticky_content_observer.observe(e) + }); + if (App.getInitParam("cdm_expanded")) App.findAll("#headlines-frame > div[id*=RROW].cdm").forEach((e) => { this.unpack_observer.observe(e) @@ -816,6 +862,10 @@ const Headlines = { // unpack visible articles, fill buffer more, etc this.scrollHandler(); + dijit.byId('main').resize(); + + PluginHost.run(PluginHost.HOOK_HEADLINES_RENDERED); + Notify.close(); }, reverse: function () { diff --git a/js/PluginHost.js b/js/PluginHost.js index caee79d58..deb7c0645 100644 --- a/js/PluginHost.js +++ b/js/PluginHost.js @@ -17,6 +17,10 @@ const PluginHost = { HOOK_HEADLINE_RENDERED: 12, HOOK_COUNTERS_RECEIVED: 13, HOOK_COUNTERS_PROCESSED: 14, + HOOK_HEADLINE_MUTATIONS: 15, + HOOK_HEADLINE_MUTATIONS_SYNCED: 16, + HOOK_HEADLINES_RENDERED: 17, + HOOK_HEADLINES_SCROLL_HANDLER: 18, hooks: [], register: function (name, callback) { if (typeof(this.hooks[name]) == 'undefined') @@ -25,7 +29,7 @@ const PluginHost = { this.hooks[name].push(callback); }, run: function (name, args) { - //console.warn('PluginHost::run ' + name); + //console.warn('PluginHost.run', name); if (typeof(this.hooks[name]) != 'undefined') for (let i = 0; i < this.hooks[name].length; i++) { diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js index 3f738aa95..361b653b6 100644 --- a/js/PrefHelpers.js +++ b/js/PrefHelpers.js @@ -368,15 +368,16 @@ const Helpers = { // only user-enabled actually counts in the checkbox when saving because system plugin checkboxes are disabled (see below) container.innerHTML += ` - <li data-row-value="${App.escapeHtml(plugin.name)}" data-plugin-local="${plugin.is_local}" data-plugin-name="${App.escapeHtml(plugin.name)}" title="${plugin.is_system ? __("System plugins are enabled using global configuration.") : ""}"> + <li data-row-value="${App.escapeHtml(plugin.name)}" data-plugin-local="${plugin.is_local}" + data-plugin-name="${App.escapeHtml(plugin.name)}" title="${plugin.is_system ? __("System plugins are enabled using global configuration.") : ""}"> <label class="checkbox ${plugin.is_system ? "system text-info" : ""}"> ${App.FormFields.checkbox_tag("plugins[]", plugin.user_enabled || plugin.system_enabled, plugin.name, {disabled: plugin.is_system})}</div> <span class='name'>${plugin.name}:</span> + <span class="description ${plugin.is_system ? "text-info" : ""}"> + ${plugin.description} + </span> </label> - <div class="description ${plugin.is_system ? "text-info" : ""}"> - ${plugin.description} - </div> <div class='actions'> ${plugin.is_system ? App.FormFields.button_tag(App.FormFields.icon("security"), "", @@ -510,12 +511,10 @@ const Helpers = { search: function() { this.search_query = this.attr('value').search.toLowerCase(); - if ('requestIdleCallback' in window) - window.requestIdleCallback(() => { - this.render_contents(); - }); - else + window.requestIdleCallback(() => { this.render_contents(); + }); + }, render_contents: function() { const container = dialog.domNode.querySelector(".contents"); @@ -809,63 +808,5 @@ const Helpers = { console.log("export"); window.open("backend.php?op=opml&method=export&" + dojo.formToQuery("opmlExportForm")); }, - publish: function() { - Notify.progress("Loading, please wait...", true); - - xhr.json("backend.php", {op: "pref-feeds", method: "getOPMLKey"}, (reply) => { - try { - const dialog = new fox.SingleUseDialog({ - title: __("Public OPML URL"), - regenOPMLKey: function() { - if (confirm(__("Replace current OPML publishing address with a new one?"))) { - Notify.progress("Trying to change address...", true); - - xhr.json("backend.php", {op: "pref-feeds", method: "regenOPMLKey"}, (reply) => { - if (reply) { - const new_link = reply.link; - const target = this.domNode.querySelector('.generated_url'); - - if (new_link && target) { - target.href = new_link; - target.innerHTML = new_link; - - Notify.close(); - - } else { - Notify.error("Could not change feed URL."); - } - } - }); - } - return false; - }, - content: ` - <header>${__("Your Public OPML URL is:")}</header> - <section> - <div class='panel text-center'> - <a class='generated_url' href="${App.escapeHtml(reply.link)}" target='_blank'>${App.escapeHtml(reply.link)}</a> - </div> - </section> - <footer class='text-center'> - <button dojoType='dijit.form.Button' onclick="return App.dialogOf(this).regenOPMLKey()"> - ${App.FormFields.icon("refresh")} - ${__('Generate new URL')} - </button> - <button dojoType='dijit.form.Button' type='submit' class='alt-primary'> - ${__('Close this window')} - </button> - </footer> - ` - }); - - dialog.show(); - - Notify.close(); - - } catch (e) { - App.Error.report(e); - } - }); - }, } }; diff --git a/js/common.js b/js/common.js index 1f8318862..1299a0c64 100755 --- a/js/common.js +++ b/js/common.js @@ -432,7 +432,7 @@ const Notify = { break; case this.KIND_PROGRESS: notify.addClassName("notify_progress"); - icon = App.getInitParam("icon_indicator_white") + icon = App.getInitParam("icon_oval") break; default: icon = "notifications"; |