summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/App.js64
-rw-r--r--js/Article.js41
-rw-r--r--js/CommonDialogs.js2
-rw-r--r--js/CommonFilters.js34
-rw-r--r--js/FeedStoreModel.js39
-rwxr-xr-xjs/FeedTree.js158
-rw-r--r--js/Feeds.js72
-rwxr-xr-xjs/Headlines.js108
-rw-r--r--js/PluginHost.js6
-rw-r--r--js/PrefHelpers.js75
-rwxr-xr-xjs/common.js2
11 files changed, 326 insertions, 275 deletions
diff --git a/js/App.js b/js/App.js
index 20498e692..ecf8c46c1 100644
--- a/js/App.js
+++ b/js/App.js
@@ -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 = "&nbsp;";
+ // 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'>&nbsp;
- <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";