summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/App.js81
-rw-r--r--js/Article.js17
-rw-r--r--js/CommonDialogs.js55
-rw-r--r--js/CommonFilters.js20
-rw-r--r--js/Feeds.js36
-rwxr-xr-xjs/Headlines.js18
-rw-r--r--js/PrefFeedTree.js4
-rw-r--r--js/PrefHelpers.js589
-rw-r--r--js/PrefLabelTree.js9
-rw-r--r--js/PrefUsers.js46
-rwxr-xr-xjs/common.js83
-rw-r--r--js/tt-rss.js12
12 files changed, 758 insertions, 212 deletions
diff --git a/js/App.js b/js/App.js
index bb8da578d..20498e692 100644
--- a/js/App.js
+++ b/js/App.js
@@ -18,6 +18,15 @@ const App = {
is_prefs: false,
LABEL_BASE_INDEX: -1024,
_translations: {},
+ Hash: {
+ get: function() {
+ return dojo.queryToObject(window.location.hash.substring(1));
+ },
+ set: function(params) {
+ const obj = dojo.queryToObject(window.location.hash.substring(1));
+ window.location.hash = dojo.objectToQuery({...obj, ...params});
+ }
+ },
l10n: {
ngettext: function(msg1, msg2, n) {
return self.__((parseInt(n) > 1) ? msg2 : msg1);
@@ -52,8 +61,9 @@ const App = {
return this.button_tag(value, "", {...{onclick: "App.dialogOf(this).hide()"}, ...attributes});
},
checkbox_tag: function(name, checked = false, value = "", attributes = {}, id = "") {
+ // checked !== '0' prevents mysql "boolean" false to be implicitly cast as true
return `<input dojoType="dijit.form.CheckBox" type="checkbox" name="${App.escapeHtml(name)}"
- ${checked ? "checked" : ""}
+ ${checked !== '0' && checked ? "checked" : ""}
${value ? `value="${App.escapeHtml(value)}"` : ""}
${this.attributes_to_string(attributes)} id="${App.escapeHtml(id)}">`
},
@@ -418,7 +428,7 @@ const App = {
if (error && error.code && error.code != App.Error.E_SUCCESS) {
console.warn("handleRpcJson: fatal error", error);
- this.Error.fatal(error.code);
+ this.Error.fatal(error.code, error.params);
return false;
}
@@ -547,6 +557,7 @@ const App = {
E_SUCCESS: "E_SUCCESS",
E_UNAUTHORIZED: "E_UNAUTHORIZED",
E_SCHEMA_MISMATCH: "E_SCHEMA_MISMATCH",
+ E_URL_SCHEME_MISMATCH: "E_URL_SCHEME_MISMATCH",
fatal: function (error, params = {}) {
if (error == App.Error.E_UNAUTHORIZED) {
window.location.href = "index.php";
@@ -554,9 +565,14 @@ const App = {
} else if (error == App.Error.E_SCHEMA_MISMATCH) {
window.location.href = "public.php?op=dbupdate";
return;
+ } else if (error == App.Error.E_URL_SCHEME_MISMATCH) {
+ params.description = __("URL scheme reported by your browser (%a) doesn't match server-configured SELF_URL_PATH (%b), check X-Forwarded-Proto.")
+ .replace("%a", params.client_scheme)
+ .replace("%b", params.server_scheme);
+ params.info = `SELF_URL_PATH: ${params.self_url_path}\nCLIENT_LOCATION: ${document.location.href}`
}
- return this.report(__("Fatal error: %s").replace("%s", error),
+ return this.report(error,
{...{title: __("Fatal error")}, ...params});
},
report: function(error, params = {}) {
@@ -587,10 +603,13 @@ const App = {
<div class='exception-contents'>
<h3>${message}</h3>
- <header>${__('Stack trace')}</header>
+ ${params.description ? `<p>${params.description}</p>` : ''}
+
+ ${error.stack ?
+ `<header>${__('Stack trace')}</header>
<section>
<textarea readonly='readonly'>${error.stack}</textarea>
- </section>
+ </section>` : ''}
${params && params.info ?
`
@@ -650,7 +669,8 @@ const App = {
op: "rpc",
method: "sanityCheck",
clientTzOffset: new Date().getTimezoneOffset() * 60,
- hasSandbox: "sandbox" in document.createElement("iframe")
+ hasSandbox: "sandbox" in document.createElement("iframe"),
+ clientLocation: window.location.href
};
xhr.json("backend.php", params, (reply) => {
@@ -757,18 +777,15 @@ const App = {
}
});
- const toolbar = document.forms["toolbar-main"];
-
- dijit.getEnclosingWidget(toolbar.view_mode).attr('value',
- this.getInitParam("default_view_mode"));
-
- dijit.getEnclosingWidget(toolbar.order_by).attr('value',
- this.getInitParam("default_view_order_by"));
+ dijit.byId('toolbar-main').setValues({
+ view_mode: this.getInitParam("default_view_mode"),
+ order_by: this.getInitParam("default_view_order_by")
+ });
this.setLoadingProgress(50);
this._widescreen_mode = this.getInitParam("widescreen");
- this.switchPanelMode(this._widescreen_mode);
+ this.setWidescreen(this._widescreen_mode);
Headlines.initScrollHandler();
@@ -801,10 +818,23 @@ const App = {
.then((reply) => {
console.log('update reply', reply);
- if (reply.id) {
- App.byId("updates-available").show();
+ const icon = App.byId("updates-available");
+
+ if (reply.changeset.id || reply.plugins.length > 0) {
+ icon.show();
+
+ const tips = [];
+
+ if (reply.changeset.id)
+ tips.push(__("Updates for Tiny Tiny RSS are available."));
+
+ if (reply.plugins.length > 0)
+ tips.push(__("Updates for some local plugins are available."));
+
+ icon.setAttribute("title", tips.join("\n"));
+
} else {
- App.byId("updates-available").hide();
+ icon.hide();
}
});
},
@@ -817,13 +847,6 @@ const App = {
document.title = tmp;
},
- onViewModeChanged: function() {
- const view_mode = document.forms["toolbar-main"].view_mode.value;
-
- App.findAll("body")[0].setAttribute("view-mode", view_mode);
-
- return Feeds.reloadCurrent('');
- },
hotkeyHandler: function(event) {
if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA") return;
@@ -843,7 +866,7 @@ const App = {
}
}
},
- switchPanelMode: function(wide) {
+ setWidescreen: function(wide) {
const article_id = Article.getActive();
if (wide) {
@@ -884,7 +907,7 @@ const App = {
if (article_id) Article.view(article_id);
- xhr.post("backend.php", {op: "rpc", method: "setpanelmode", wide: wide ? 1 : 0});
+ xhr.post("backend.php", {op: "rpc", method: "setWidescreen", wide: wide ? 1 : 0});
},
initHotkeyActions: function() {
if (this.is_prefs) {
@@ -1144,7 +1167,7 @@ const App = {
Cookie.set("ttrss_ci_width", 0);
Cookie.set("ttrss_ci_height", 0);
- this.switchPanelMode(this._widescreen_mode);
+ this.setWidescreen(this._widescreen_mode);
} else {
alert(__("Widescreen is not available in combined mode."));
}
@@ -1234,7 +1257,7 @@ const App = {
Cookie.set("ttrss_ci_width", 0);
Cookie.set("ttrss_ci_height", 0);
- this.switchPanelMode(this._widescreen_mode);
+ this.setWidescreen(this._widescreen_mode);
} else {
alert(__("Widescreen is not available in combined mode."));
}
@@ -1245,6 +1268,6 @@ const App = {
default:
console.log("quickMenuGo: unknown action: " + opid);
}
- }
+ },
}
diff --git a/js/Article.js b/js/Article.js
index 5f695561c..ed74051a6 100644
--- a/js/Article.js
+++ b/js/Article.js
@@ -144,10 +144,15 @@ const Article = {
).join(", ") : `${__("no tags")}`}</span>`;
},
renderLabels: function(id, labels) {
- return `<span class="labels" data-labels-for="${id}">${labels.map((label) => `
- <span class="label" data-label-id="${label[0]}"
- style="color : ${label[2]}; background-color : ${label[3]}">${App.escapeHtml(label[1])}</span>`
- ).join("")}</span>`;
+ return `<span class="labels" data-labels-for="${id}">
+ ${labels.map((label) => `
+ <a href="#" class="label" data-label-id="${label[0]}"
+ style="color : ${label[2]}; background-color : ${label[3]}"
+ onclick="event.stopPropagation(); Feeds.open({feed:'${label[0]}'})">
+ ${App.escapeHtml(label[1])}
+ </a>`
+ ).join("")}
+ </span>`;
},
renderEnclosures: function (enclosures) {
return `
@@ -317,7 +322,7 @@ const Article = {
},
editTags: function (id) {
const dialog = new fox.SingleUseDialog({
- title: __("Edit article Tags"),
+ title: __("Article tags"),
content: `
${App.FormFields.hidden_tag("id", id.toString())}
${App.FormFields.hidden_tag("op", "article")}
@@ -329,7 +334,7 @@ const Article = {
<section>
<textarea dojoType='dijit.form.SimpleTextarea' rows='4' disabled='true'
- id='tags_str' name='tags_str'></textarea>
+ id='tags_str' name='tags_str'>${__("Loading, please wait...")}</textarea>
<div class='autocomplete' id='tags_choices' style='display:none'></div>
</section>
diff --git a/js/CommonDialogs.js b/js/CommonDialogs.js
index 321ddf6d3..ab8441cac 100644
--- a/js/CommonDialogs.js
+++ b/js/CommonDialogs.js
@@ -3,7 +3,7 @@
/* eslint-disable new-cap */
/* eslint-disable no-new */
-/* global __, dojo, dijit, Notify, App, Feeds, xhrPost, xhr, Tables, fox */
+/* global __, dojo, dijit, Notify, App, Feeds, xhr, Tables, fox */
/* exported CommonDialogs */
const CommonDialogs = {
@@ -16,7 +16,7 @@ const CommonDialogs = {
{op: "feeds", method: "subscribeToFeed"},
(reply) => {
const dialog = new fox.SingleUseDialog({
- title: __("Subscribe to Feed"),
+ title: __("Subscribe to feed"),
content: `
<form onsubmit='return false'>
@@ -181,7 +181,7 @@ const CommonDialogs = {
}
} catch (e) {
- console.error(transport.responseText);
+ console.error(reply);
App.Error.report(e);
}
});
@@ -248,7 +248,7 @@ const CommonDialogs = {
${reply.map((row) => `
<tr data-row-id='${row.id}'>
- <td width='5%' align='center'>
+ <td class='checkbox'>
<input onclick='Tables.onRowChecked(this)' dojoType="dijit.form.CheckBox"
type="checkbox">
</td>
@@ -333,8 +333,12 @@ const CommonDialogs = {
const dialog = new fox.SingleUseDialog({
id: "feedEditDlg",
- title: __("Edit Feed"),
+ title: __("Edit feed"),
feed_title: "",
+ E_ICON_FILE_TOO_LARGE: 'E_ICON_FILE_TOO_LARGE',
+ E_ICON_RENAME_FAILED: 'E_ICON_RENAME_FAILED',
+ E_ICON_UPLOAD_FAILED: 'E_ICON_UPLOAD_FAILED',
+ E_ICON_UPLOAD_SUCCESS: 'E_ICON_UPLOAD_SUCCESS',
unsubscribe: function() {
if (confirm(__("Unsubscribe from %s?").replace("%s", this.feed_title))) {
dialog.hide();
@@ -361,20 +365,18 @@ const CommonDialogs = {
xhr.open( 'POST', 'backend.php', true );
xhr.onload = function () {
- console.log(this.responseText);
+ const ret = JSON.parse(this.responseText);
// TODO: make a notice box within panel content
- switch (parseInt(this.responseText)) {
- case 1:
- Notify.error("Upload failed: icon is too big.");
+ switch (ret.rc) {
+ case dialog.E_ICON_FILE_TOO_LARGE:
+ alert(__("Icon file is too large."));
break;
- case 2:
- Notify.error("Upload failed.");
+ case dialog.E_ICON_UPLOAD_FAILED:
+ alert(__("Upload failed."));
break;
- default:
+ case dialog.E_ICON_UPLOAD_SUCCESS:
{
- Notify.info("Upload complete.");
-
if (App.isPrefs())
dijit.byId("feedTree").reload();
else
@@ -383,12 +385,16 @@ const CommonDialogs = {
const icon = dialog.domNode.querySelector(".feedIcon");
if (icon) {
- icon.src = this.responseText;
+ icon.src = ret.icon_url;
icon.show();
}
input.value = "";
}
+ break;
+ default:
+ alert(this.responseText);
+ break;
}
};
@@ -400,9 +406,7 @@ const CommonDialogs = {
if (confirm(__("Remove stored feed icon?"))) {
Notify.progress("Removing feed icon...", true);
- const query = {op: "pref-feeds", method: "removeicon", feed_id: id};
-
- xhr.post("backend.php", query, () => {
+ xhr.post("backend.php", {op: "pref-feeds", method: "removeicon", feed_id: id}, () => {
Notify.info("Feed icon removed.");
if (App.isPrefs())
@@ -473,8 +477,8 @@ const CommonDialogs = {
<section>
<fieldset>
<input dojoType='dijit.form.ValidationTextBox' required='1'
- placeHolder="${__("Feed Title")}"
- style='font-size : 16px; width: 500px' name='title' value="${App.escapeHtml(feed.title)}">
+ placeHolder="${__("Feed title")}"
+ style='font-size : 16px; width: 530px' name='title' value="${App.escapeHtml(feed.title)}">
</fieldset>
<fieldset>
@@ -565,19 +569,21 @@ const CommonDialogs = {
<div dojoType="dijit.layout.ContentPane" title="${__('Icon')}">
<div><img class='feedIcon' style="${feed.icon ? "" : "display : none"}" src="${feed.icon ? App.escapeHtml(feed.icon) : ""}"></div>
- <label class="dijitButton">${__("Upload new icon...")}
+ <label class="dijitButton">
+ ${App.FormFields.icon("file_upload")}
+ ${__("Upload new icon...")}
<input style="display: none" type="file" onchange="App.dialogOf(this).uploadIcon(this)">
</label>
- ${App.FormFields.submit_tag(__("Remove"), {class: "alt-danger", onclick: "App.dialogOf(this).removeIcon("+feed_id+")"})}
+ ${App.FormFields.submit_tag(App.FormFields.icon("delete") + " " + __("Remove"), {class: "alt-danger", onclick: "App.dialogOf(this).removeIcon("+feed_id+")"})}
</div>
<div dojoType="dijit.layout.ContentPane" title="${__('Plugins')}">
${reply.plugin_data}
</div>
</div>
<footer>
- ${App.FormFields.button_tag(__("Unsubscribe"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).unsubscribe()"})}
- ${App.FormFields.submit_tag(__("Save"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.button_tag(App.FormFields.icon("delete") + " " + __("Unsubscribe"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).unsubscribe()"})}
+ ${App.FormFields.submit_tag(App.FormFields.icon("save") + " " + __("Save"), {onclick: "App.dialogOf(this).execute()"})}
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
</footer>
</form>
@@ -634,6 +640,7 @@ const CommonDialogs = {
onclick='window.open("https://tt-rss.org/wiki/GeneratedFeeds")'>
<i class='material-icons'>help</i> ${__("More info...")}</button>
<button dojoType='dijit.form.Button' onclick="return App.dialogOf(this).regenFeedKey('${feed}', '${is_cat}')">
+ ${App.FormFields.icon("refresh")}
${__('Generate new URL')}
</button>
<button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
diff --git a/js/CommonFilters.js b/js/CommonFilters.js
index 0c138760d..1450458f8 100644
--- a/js/CommonFilters.js
+++ b/js/CommonFilters.js
@@ -11,7 +11,7 @@ const Filters = {
const dialog = new fox.SingleUseDialog({
id: "filterEditDlg",
- title: filter_id ? __("Edit Filter") : __("Create Filter"),
+ title: filter_id ? __("Edit filter") : __("Create new filter"),
ACTION_TAG: 4,
ACTION_SCORE: 6,
ACTION_LABEL: 7,
@@ -115,7 +115,7 @@ const Filters = {
const li = document.createElement('li');
li.addClassName("rule");
- li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})}
+ li.innerHTML = `${App.FormFields.checkbox_tag("", false, "", {onclick: 'Lists.onRowChecked(this)'})}
<span class="name" onclick='App.dialogOf(this).onRuleClicked(this)'>${reply}</span>
<span class="payload" >${App.FormFields.hidden_tag("rule[]", rule)}</span>`;
@@ -147,7 +147,7 @@ const Filters = {
const li = document.createElement('li');
li.addClassName("action");
- li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})}
+ li.innerHTML = `${App.FormFields.checkbox_tag("", false, "", {onclick: 'Lists.onRowChecked(this)'})}
<span class="name" onclick='App.dialogOf(this).onActionClicked(this)'>${reply}</span>
<span class="payload">${App.FormFields.hidden_tag("action[]", action)}</span>`;
@@ -229,7 +229,7 @@ const Filters = {
<footer>
${App.FormFields.button_tag(App.FormFields.icon("help") + " " + __("More info"), "", {class: 'pull-left alt-info',
onclick: "window.open('https://tt-rss.org/wiki/ContentFilters')"})}
- ${App.FormFields.submit_tag(__("Save rule"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.submit_tag(App.FormFields.icon("save") + " " + __("Save"), {onclick: "App.dialogOf(this).execute()"})}
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
</footer>
@@ -313,7 +313,7 @@ const Filters = {
"filterDlg_actionParamPlugin")}
</section>
<footer>
- ${App.FormFields.submit_tag(__("Save action"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.submit_tag(App.FormFields.icon("save") + " " + __("Save"), {onclick: "App.dialogOf(this).execute()"})}
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
</footer>
</form>
@@ -511,13 +511,13 @@ const Filters = {
<footer>
${filter_id ?
`
- ${App.FormFields.button_tag(__("Remove"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).removeFilter()"})}
- ${App.FormFields.button_tag(__("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})}
- ${App.FormFields.submit_tag(__("Save"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.button_tag(App.FormFields.icon("delete") + " " + __("Remove"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).removeFilter()"})}
+ ${App.FormFields.button_tag(App.FormFields.icon("check_circle") + " " + __("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})}
+ ${App.FormFields.submit_tag(App.FormFields.icon("save") + " " + __("Save"), {onclick: "App.dialogOf(this).execute()"})}
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
` : `
- ${App.FormFields.button_tag(__("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})}
- ${App.FormFields.submit_tag(__("Create"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.button_tag(App.FormFields.icon("check_circle") + " " + __("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})}
+ ${App.FormFields.submit_tag(App.FormFields.icon("add") + " " + __("Create"), {onclick: "App.dialogOf(this).execute()"})}
${App.FormFields.cancel_dialog_tag(__("Cancel"))}
`}
</footer>
diff --git a/js/Feeds.js b/js/Feeds.js
index 5a2dee5cf..33a1fa3dc 100644
--- a/js/Feeds.js
+++ b/js/Feeds.js
@@ -117,15 +117,21 @@ const Feeds = {
},
reloadCurrent: function(method) {
if (this.getActive() != undefined) {
- console.log("reloadCurrent: " + method);
+ console.log("reloadCurrent", this.getActive(), this.activeIsCat(), method);
this.open({feed: this.getActive(), is_cat: this.activeIsCat(), method: method});
}
- return false; // block unneeded form submits
},
openDefaultFeed: function() {
this.open({feed: this._default_feed_id});
},
+ onViewModeChanged: function() {
+ // TODO: is this still needed?
+ App.find("body").setAttribute("view-mode",
+ dijit.byId("toolbar-main").getValues().view_mode);
+
+ return Feeds.reloadCurrent('');
+ },
openNextUnread: function() {
const is_cat = this.activeIsCat();
const nuf = this.getNextUnread(this.getActive(), is_cat);
@@ -236,12 +242,12 @@ const Feeds = {
//document.onkeypress = (event) => { return App.hotkeyHandler(event) };
window.onresize = () => { Headlines.scrollHandler(); }
- /* global hash_get */
- const hash_feed_id = hash_get('f');
- const hash_feed_is_cat = hash_get('c') == "1";
+ const hash = App.Hash.get();
+
+ console.log('got hash', hash);
- if (hash_feed_id != undefined) {
- this.open({feed: hash_feed_id, is_cat: hash_feed_is_cat});
+ if (hash.f != undefined) {
+ this.open({feed: parseInt(hash.f), is_cat: parseInt(hash.c)});
} else {
this.openDefaultFeed();
}
@@ -305,9 +311,12 @@ const Feeds = {
setActive: function(id, is_cat) {
console.log('setActive', id, is_cat);
- /* global hash_set */
- hash_set('f', id);
- hash_set('c', is_cat ? 1 : 0);
+ if ('requestIdleCallback' in window)
+ window.requestIdleCallback(() => {
+ App.Hash.set({f: id, c: is_cat ? 1 : 0});
+ });
+ else
+ App.Hash.set({f: id, c: is_cat ? 1 : 0});
this._active_feed_id = id;
this._active_feed_is_cat = is_cat;
@@ -366,10 +375,7 @@ const Feeds = {
}, 10 * 1000);
}
- //Form.enable("toolbar-main");
-
- let query = Object.assign({op: "feeds", method: "view", feed: feed},
- dojo.formToObject("toolbar-main"));
+ let query = {...{op: "feeds", method: "view", feed: feed}, ...dojo.formToObject("toolbar-main")};
if (method) query.m = method;
@@ -612,7 +618,7 @@ const Feeds = {
{class: 'alt-info pull-left', onclick: "window.open('https://tt-rss.org/wiki/SearchSyntax')"})}
` : ''}
- ${App.FormFields.submit_tag(__('Search'), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.submit_tag(App.FormFields.icon("search") + " " + __('Search'), {onclick: "App.dialogOf(this).execute()"})}
${App.FormFields.cancel_dialog_tag(__('Cancel'))}
</footer>
</form>
diff --git a/js/Headlines.js b/js/Headlines.js
index fd9bc6661..28e43be1f 100755
--- a/js/Headlines.js
+++ b/js/Headlines.js
@@ -278,7 +278,7 @@ const Headlines = {
}
},
loadMore: function () {
- const view_mode = document.forms["toolbar-main"].view_mode.value;
+ const view_mode = dijit.byId("toolbar-main").getValues().view_mode;
const unread_in_buffer = App.findAll("#headlines-frame > div[id*=RROW][class*=Unread]").length;
const num_all = App.findAll("#headlines-frame > div[id*=RROW]").length;
const num_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat());
@@ -819,19 +819,15 @@ const Headlines = {
Notify.close();
},
reverse: function () {
- const toolbar = document.forms["toolbar-main"];
- const order_by = dijit.getEnclosingWidget(toolbar.order_by);
+ const toolbar = dijit.byId("toolbar-main");
+ let order_by = toolbar.getValues().order_by;
- let value = order_by.attr('value');
-
- if (value != "date_reverse")
- value = "date_reverse";
+ if (order_by != "date_reverse")
+ order_by = "date_reverse";
else
- value = "default";
-
- order_by.attr('value', value);
+ order_by = App.getInitParam("default_view_order_by");
- Feeds.reloadCurrent();
+ toolbar.setValues({order_by: order_by});
},
selectionToggleUnread: function (params = {}) {
const cmode = params.cmode != undefined ? params.cmode : 2;
diff --git a/js/PrefFeedTree.js b/js/PrefFeedTree.js
index bb5d25e67..013c01262 100644
--- a/js/PrefFeedTree.js
+++ b/js/PrefFeedTree.js
@@ -300,7 +300,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
try {
const dialog = new fox.SingleUseDialog({
- title: __("Edit Multiple Feeds"),
+ title: __("Edit multiple feeds"),
/*getChildByName: function (name) {
let rv = null;
this.getChildren().forEach(
@@ -513,7 +513,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b
<div class='panel panel-scrollable'>
<table width='100%' id='inactive-feeds-list'>
${reply.map((row) => `<tr data-row-id='${row.id}'>
- <td width='5%' align='center'>
+ <td class='checkbox'>
<input onclick='Tables.onRowChecked(this)' dojoType='dijit.form.CheckBox' type='checkbox'>
</td>
<td>
diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js
index 62f6d91b1..3f738aa95 100644
--- a/js/PrefHelpers.js
+++ b/js/PrefHelpers.js
@@ -1,7 +1,7 @@
'use strict';
/* eslint-disable no-new */
-/* global __, dijit, dojo, Tables, xhrPost, Notify, xhr, App, fox */
+/* global __, dijit, dojo, Tables, Notify, xhr, App, fox */
const Helpers = {
AppPasswords: {
@@ -19,7 +19,7 @@ const Helpers = {
alert("No passwords selected.");
} else if (confirm(__("Remove selected app passwords?"))) {
- xhr.post("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (reply) => {
+ xhr.post("backend.php", {op: "pref-prefs", method: "deleteAppPasswords", "ids[]": rows}, (reply) => {
this.updateContent(reply);
Notify.close();
});
@@ -53,6 +53,33 @@ const Helpers = {
return false;
},
},
+ Digest: {
+ preview: function() {
+ const dialog = new fox.SingleUseDialog({
+ title: __("Digest preview"),
+ content: `
+ <div class='panel panel-scrollable digest-preview'>
+ <div class='text-center'>${__("Loading, please wait...")}</div>
+ </div>
+
+ <footer class='text-center'>
+ ${App.FormFields.submit_tag(__('Close this window'))}
+ </footer>
+ `
+ });
+
+ const tmph = dojo.connect(dialog, 'onShow', function () {
+ dojo.disconnect(tmph);
+
+ xhr.json("backend.php", {op: "pref-prefs", method: "previewDigest"}, (reply) => {
+ dialog.domNode.querySelector('.digest-preview').innerHTML = reply[0];
+ });
+ });
+
+ dialog.show();
+
+ }
+ },
System: {
//
},
@@ -97,7 +124,7 @@ const Helpers = {
edit: function() {
const dialog = new fox.SingleUseDialog({
id: "profileEditDlg",
- title: __("Settings Profiles"),
+ title: __("Manage profiles"),
getSelectedProfiles: function () {
return Tables.getSelected("pref-profiles-list");
},
@@ -108,12 +135,7 @@ const Helpers = {
if (confirm(__("Remove selected profiles? Active and default profiles will not be removed."))) {
Notify.progress("Removing selected profiles...", true);
- const query = {
- op: "pref-prefs", method: "remprofiles",
- ids: sel_rows.toString()
- };
-
- xhr.post("backend.php", query, () => {
+ xhr.post("backend.php", {op: "pref-prefs", method: "remprofiles", "ids[]": sel_rows}, () => {
Notify.close();
dialog.refresh();
});
@@ -161,7 +183,7 @@ const Helpers = {
<table width='100%' id='pref-profiles-list'>
${reply.map((profile) => `
<tr data-row-id="${profile.id}">
- <td width='5%'>
+ <td class='checkbox'>
${App.FormFields.checkbox_tag("", false, "", {onclick: 'Tables.onRowChecked(this)'})}
</td>
<td>
@@ -183,9 +205,9 @@ const Helpers = {
</div>
<footer>
- ${App.FormFields.button_tag(__('Remove selected profiles'), "",
+ ${App.FormFields.button_tag(App.FormFields.icon("delete") + " " +__('Remove selected profiles'), "",
{class: 'pull-left alt-danger', onclick: 'App.dialogOf(this).removeSelected()'})}
- ${App.FormFields.submit_tag(__('Activate profile'), {onclick: 'App.dialogOf(this).execute()'})}
+ ${App.FormFields.submit_tag(App.FormFields.icon("check") + " " + __('Activate profile'), {onclick: 'App.dialogOf(this).execute()'})}
${App.FormFields.cancel_dialog_tag(__('Cancel'))}
</footer>
</form>
@@ -217,58 +239,70 @@ const Helpers = {
},
Prefs: {
customizeCSS: function() {
- xhr.json("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => {
-
- const dialog = new fox.SingleUseDialog({
- title: __("Customize stylesheet"),
- apply: function() {
- xhr.post("backend.php", this.attr('value'), () => {
- Element.show("css_edit_apply_msg");
- App.byId("user_css_style").innerText = this.attr('value');
- });
- },
- execute: function () {
- Notify.progress('Saving data...', true);
+ const dialog = new fox.SingleUseDialog({
+ title: __("Customize stylesheet"),
+ apply: function() {
+ xhr.post("backend.php", this.attr('value'), () => {
+ Element.show("css_edit_apply_msg");
+ App.byId("user_css_style").innerText = this.attr('value');
+ });
+ },
+ execute: function () {
+ Notify.progress('Saving data...', true);
- xhr.post("backend.php", this.attr('value'), () => {
- window.location.reload();
- });
- },
- content: `
- <div class='alert alert-info'>
- ${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")}
+ xhr.post("backend.php", this.attr('value'), () => {
+ window.location.reload();
+ });
+ },
+ content: `
+ <div class='alert alert-info'>
+ ${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")}
+ </div>
+
+ ${App.FormFields.hidden_tag('op', 'rpc')}
+ ${App.FormFields.hidden_tag('method', 'setpref')}
+ ${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')}
+
+ <div id='css_edit_apply_msg' style='display : none'>
+ <div class='alert alert-warning'>
+ ${__("User CSS has been applied, you might need to reload the page to see all changes.")}
</div>
+ </div>
+
+ <textarea class='panel user-css-editor' disabled='true' dojoType='dijit.form.SimpleTextarea'
+ style='font-size : 12px;' name='value'>${__("Loading, please wait...")}</textarea>
+
+ <footer>
+ <button dojoType='dijit.form.Button' class='alt-success' onclick="App.dialogOf(this).apply()">
+ ${App.FormFields.icon("check")}
+ ${__('Apply')}
+ </button>
+ <button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
+ ${App.FormFields.icon("refresh")}
+ ${__('Save and reload')}
+ </button>
+ <button dojoType='dijit.form.Button' onclick="App.dialogOf(this).hide()">
+ ${__('Cancel')}
+ </button>
+ </footer>
+ `
+ });
- ${App.FormFields.hidden_tag('op', 'rpc')}
- ${App.FormFields.hidden_tag('method', 'setpref')}
- ${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')}
+ const tmph = dojo.connect(dialog, 'onShow', function () {
+ dojo.disconnect(tmph);
- <div id='css_edit_apply_msg' style='display : none'>
- <div class='alert alert-warning'>
- ${__("User CSS has been applied, you might need to reload the page to see all changes.")}
- </div>
- </div>
+ xhr.json("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => {
- <textarea class='panel user-css-editor' dojoType='dijit.form.SimpleTextarea'
- style='font-size : 12px;' name='value'>${reply.value}</textarea>
-
- <footer>
- <button dojoType='dijit.form.Button' class='alt-success' onclick="App.dialogOf(this).apply()">
- ${__('Apply')}
- </button>
- <button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
- ${__('Save and reload')}
- </button>
- <button dojoType='dijit.form.Button' onclick="App.dialogOf(this).hide()">
- ${__('Cancel')}
- </button>
- </footer>
- `
- });
+ const editor = dijit.getEnclosingWidget(dialog.domNode.querySelector(".user-css-editor"));
- dialog.show();
+ editor.attr('value', reply.value);
+ editor.attr('disabled', false);
+ });
});
+
+ dialog.show();
+
},
confirmReset: function() {
if (confirm(__("Reset to defaults?"))) {
@@ -278,20 +312,448 @@ const Helpers = {
});
}
},
- clearPluginData: function(name) {
- if (confirm(__("Clear stored data for this plugin?"))) {
+ refresh: function() {
+ xhr.post("backend.php", { op: "pref-prefs" }, (reply) => {
+ dijit.byId('prefsTab').attr('content', reply);
+ Notify.close();
+ });
+ },
+ },
+ Plugins: {
+ _list_of_plugins: [],
+ _search_query: "",
+ enableSelected: function() {
+ const form = dijit.byId("changePluginsForm");
+
+ if (form.validate()) {
+ xhr.post("backend.php", form.getValues(), () => {
+ Notify.close();
+ if (confirm(__('Selected plugins have been enabled. Reload?'))) {
+ window.location.reload();
+ }
+ })
+ }
+ },
+ search: function() {
+ this._search_query = dijit.byId("changePluginsForm").getValues().search;
+ this.render_contents();
+ },
+ reload: function() {
+ xhr.json("backend.php", {op: "pref-prefs", method: "getPluginsList"}, (reply) => {
+ this._list_of_plugins = reply;
+ this.render_contents();
+ });
+ },
+ render_contents: function() {
+ const container = document.querySelector(".prefs-plugin-list");
+
+ container.innerHTML = "";
+ let results_rendered = 0;
+
+ const is_admin = this._list_of_plugins.is_admin;
+
+ const search_tokens = this._search_query
+ .split(/ {1,}/)
+ .filter((stoken) => (stoken.length > 0 ? stoken : null));
+
+ this._list_of_plugins.plugins.forEach((plugin) => {
+
+ if (search_tokens.length == 0 ||
+ Object.values(plugin).filter((pval) =>
+ search_tokens.filter((stoken) =>
+ (pval.toString().indexOf(stoken) != -1 ? stoken : null)
+ ).length == search_tokens.length).length > 0) {
+
+ ++results_rendered;
+
+ // 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.") : ""}">
+ <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>
+ </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"), "",
+ {disabled: true}) : ''}
+ ${plugin.more_info ?
+ App.FormFields.button_tag(App.FormFields.icon("help"), "",
+ {class: 'alt-info', onclick: `window.open("${App.escapeHtml(plugin.more_info)}")`}) : ''}
+ ${is_admin && plugin.is_local ?
+ App.FormFields.button_tag(App.FormFields.icon("update"), "",
+ {title: __("Update"), class: 'alt-warning', "data-update-btn-for-plugin": plugin.name, style: 'display : none',
+ onclick: `Helpers.Plugins.update("${App.escapeHtml(plugin.name)}")`}) : ''}
+ ${is_admin && plugin.has_data ?
+ App.FormFields.button_tag(App.FormFields.icon("clear"), "",
+ {title: __("Clear data"), onclick: `Helpers.Plugins.clearData("${App.escapeHtml(plugin.name)}")`}) : ''}
+ ${is_admin && plugin.is_local ?
+ App.FormFields.button_tag(App.FormFields.icon("delete"), "",
+ {title: __("Uninstall"), onclick: `Helpers.Plugins.uninstall("${App.escapeHtml(plugin.name)}")`}) : ''}
+ </div>
+ <div class='version text-muted'>${plugin.version}</div>
+ </li>
+ `;
+ } else {
+ // if plugin is outside of search scope, keep current value in case of saving (only user-enabled is needed)
+ container.innerHTML += App.FormFields.checkbox_tag("plugins[]", plugin.user_enabled, plugin.name, {style: 'display : none'});
+ }
+ });
+
+ if (results_rendered == 0) {
+ container.innerHTML += `<li class='text-center text-info'>${__("Could not find any plugins for this search query.")}</li>`;
+ }
+
+ dojo.parser.parse(container);
+
+ },
+ clearData: function(name) {
+ if (confirm(__("Clear stored data for %s?").replace("%s", name))) {
Notify.progress("Loading, please wait...");
- xhr.post("backend.php", {op: "pref-prefs", method: "clearplugindata", name: name}, () => {
+ xhr.post("backend.php", {op: "pref-prefs", method: "clearPluginData", name: name}, () => {
Helpers.Prefs.refresh();
});
}
},
- refresh: function() {
- xhr.post("backend.php", { op: "pref-prefs" }, (reply) => {
- dijit.byId('prefsTab').attr('content', reply);
- Notify.close();
+ uninstall: function(plugin) {
+ const msg = __("Uninstall plugin %s?").replace("%s", plugin);
+
+ if (confirm(msg)) {
+ Notify.progress("Loading, please wait...");
+
+ xhr.json("backend.php", {op: "pref-prefs", method: "uninstallPlugin", plugin: plugin}, (reply) => {
+ if (reply && reply.status == 1)
+ Helpers.Prefs.refresh();
+ else {
+ Notify.error("Plugin uninstallation failed.");
+ }
+ });
+
+ }
+ },
+ install: function() {
+ const dialog = new fox.SingleUseDialog({
+ PI_RES_ALREADY_INSTALLED: "PI_RES_ALREADY_INSTALLED",
+ PI_RES_SUCCESS: "PI_RES_SUCCESS",
+ PI_ERR_NO_CLASS: "PI_ERR_NO_CLASS",
+ PI_ERR_NO_INIT_PHP: "PI_ERR_NO_INIT_PHP",
+ PI_ERR_EXEC_FAILED: "PI_ERR_EXEC_FAILED",
+ PI_ERR_NO_TEMPDIR: "PI_ERR_NO_TEMPDIR",
+ PI_ERR_PLUGIN_NOT_FOUND: "PI_ERR_PLUGIN_NOT_FOUND",
+ PI_ERR_NO_WORKDIR: "PI_ERR_NO_WORKDIR",
+ title: __("Available plugins"),
+ need_refresh: false,
+ entries: false,
+ search_query: "",
+ installed_plugins: [],
+ onHide: function() {
+ if (this.need_refresh) {
+ Helpers.Prefs.refresh();
+ }
+ },
+ performInstall: function(plugin) {
+
+ const install_dialog = new fox.SingleUseDialog({
+ title: __("Plugin installer"),
+ content: `
+ <ul class="panel panel-scrollable contents">
+ <li class='text-center'>${__("Installing %s, please wait...").replace("%s", plugin)}</li>
+ </ul>
+
+ <footer class='text-center'>
+ ${App.FormFields.submit_tag(__("Close this window"))}
+ </footer>`
+ });
+
+ const tmph = dojo.connect(install_dialog, 'onShow', function () {
+ dojo.disconnect(tmph);
+
+ const container = install_dialog.domNode.querySelector(".contents");
+
+ xhr.json("backend.php", {op: "pref-prefs", method: "installPlugin", plugin: plugin}, (reply) => {
+ if (!reply) {
+ container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`;
+ } else {
+ switch (reply.result) {
+ case dialog.PI_RES_SUCCESS:
+ container.innerHTML = `<li class='text-success text-center'>${__("Plugin has been installed.")}</li>`
+ dialog.need_refresh = true;
+ break;
+ case dialog.PI_RES_ALREADY_INSTALLED:
+ container.innerHTML = `<li class='text-success text-center'>${__("Plugin is already installed.")}</li>`
+ break;
+ default:
+ container.innerHTML = `
+ <li>
+ <h3 style="margin-top: 0">${plugin}</h3>
+ <div class='text-error'>${reply.result}</div>
+ ${reply.stderr ? `<pre class="small text-error pre-wrap">${reply.stderr}</pre>` : ''}
+ ${reply.stdour ? `<pre class="small text-success pre-wrap">${reply.stdout}</pre>` : ''}
+ <p class="small">
+ ${App.FormFields.icon("error_outline") + " " + __("Exited with RC: %d").replace("%d", reply.git_status)}
+ </p>
+ </li>
+ `;
+ }
+ }
+ });
+ });
+
+ install_dialog.show();
+
+ },
+ search: function() {
+ this.search_query = this.attr('value').search.toLowerCase();
+
+ if ('requestIdleCallback' in window)
+ window.requestIdleCallback(() => {
+ this.render_contents();
+ });
+ else
+ this.render_contents();
+ },
+ render_contents: function() {
+ const container = dialog.domNode.querySelector(".contents");
+
+ if (!dialog.entries) {
+ container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`;
+ } else {
+ container.innerHTML = "";
+
+ let results_rendered = 0;
+
+ const search_tokens = dialog.search_query
+ .split(/ {1,}/)
+ .filter((stoken) => (stoken.length > 0 ? stoken : null));
+
+ dialog.entries.forEach((plugin) => {
+ const is_installed = (dialog.installed_plugins
+ .filter((p) => plugin.topics.map((t) => t.replace(/-/g, "_")).includes(p))).length > 0;
+
+ if (search_tokens.length == 0 ||
+ Object.values(plugin).filter((pval) =>
+ search_tokens.filter((stoken) =>
+ (pval.indexOf(stoken) != -1 ? stoken : null)
+ ).length == search_tokens.length).length > 0) {
+
+ ++results_rendered;
+
+ container.innerHTML += `
+ <li data-row-value="${App.escapeHtml(plugin.name)}" class="${is_installed ? "plugin-installed" : ""}">
+ ${App.FormFields.button_tag((is_installed ?
+ App.FormFields.icon("check") + " " +__("Already installed") :
+ App.FormFields.icon("file_download") + " " +__('Install')), "", {class: 'alt-primary pull-right',
+ disabled: is_installed,
+ onclick: `App.dialogOf(this).performInstall("${App.escapeHtml(plugin.name)}")`})}
+
+ <h3>${plugin.name}
+ <a target="_blank" href="${App.escapeHtml(plugin.html_url)}">
+ ${App.FormFields.icon("open_in_new_window")}
+ </a>
+ </h3>
+
+ <div class='small text-muted'>${__("Updated: %s").replace("%s", plugin.last_update)}</div>
+
+ <div class='description'>${plugin.description}</div>
+ </li>
+ `
+ }
+ });
+
+ if (results_rendered == 0) {
+ container.innerHTML = `<li class='text-center text-info'>${__("Could not find any plugins for this search query.")}</li>`;
+ }
+
+ dojo.parser.parse(container);
+ }
+ },
+ reload: function() {
+ const container = dialog.domNode.querySelector(".contents");
+ container.innerHTML = `<li class='text-center'>${__("Looking for plugins...")}</li>`;
+
+ xhr.json("backend.php", {op: "pref-prefs", method: "getAvailablePlugins"}, (reply) => {
+ dialog.entries = reply;
+ dialog.render_contents();
+ });
+ },
+ content: `
+ <div dojoType='fox.Toolbar'>
+ <div class='pull-right'>
+ <input name="search" placeholder="${__("Search...")}" type="search" dojoType="dijit.form.TextBox" onkeyup="App.dialogOf(this).search()">
+ </div>
+ <div style='height : 16px'>&nbsp;</div> <!-- disgusting -->
+ </div>
+
+ <ul style='clear : both' class="panel panel-scrollable-400px contents plugin-installer-list"> </ul>
+
+ <footer>
+ ${App.FormFields.button_tag(App.FormFields.icon("refresh") + " " +__("Refresh"), "", {class: 'alt-primary', onclick: 'App.dialogOf(this).reload()'})}
+ ${App.FormFields.cancel_dialog_tag(__("Close"))}
+ </footer>
+ `,
+ });
+
+ const tmph = dojo.connect(dialog, 'onShow', function () {
+ dojo.disconnect(tmph);
+
+ dialog.installed_plugins = [...document.querySelectorAll('*[data-plugin-name]')].map((p) => p.getAttribute('data-plugin-name'));
+
+ dialog.reload();
+ });
+
+ dialog.show();
+ },
+ update: function(name = null) {
+
+ const dialog = new fox.SingleUseDialog({
+ title: __("Update plugins"),
+ need_refresh: false,
+ plugins_to_update: [],
+ plugins_to_check: [],
+ onHide: function() {
+ if (this.need_refresh) {
+ Helpers.Prefs.refresh();
+ }
+ },
+ performUpdate: function() {
+ const container = dialog.domNode.querySelector(".update-results");
+
+ console.log('updating', dialog.plugins_to_update);
+ dialog.attr('title', __('Updating...'));
+
+ container.innerHTML = `<li class='text-center'>${__("Updating, please wait...")}</li>`;
+ let enable_update_btn = false;
+
+ xhr.json("backend.php", {op: "pref-prefs", method: "updateLocalPlugins", plugins: dialog.plugins_to_update.join(",")}, (reply) => {
+
+ if (!reply) {
+ container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`;
+ } else {
+ container.innerHTML = "";
+
+ reply.forEach((p) => {
+ if (p.rv.git_status == 0)
+ dialog.need_refresh = true;
+ else
+ enable_update_btn = true;
+
+ container.innerHTML +=
+ `
+ <li>
+ <h3>${p.plugin}</h3>
+ ${p.rv.stderr ? `<pre class="small text-error pre-wrap">${p.rv.stderr}</pre>` : ''}
+ ${p.rv.stdout ? `<pre class="small text-success pre-wrap">${p.rv.stdout}</pre>` : ''}
+ <div class="small">
+ ${p.rv.git_status ? App.FormFields.icon("error_outline") + " " + __("Exited with RC: %d").replace("%d", p.rv.git_status) :
+ App.FormFields.icon("check") + " " + __("Update done.")}
+ </div>
+ </li>
+ `
+ });
+ }
+
+ dialog.attr('title', __('Updates complete'));
+ dijit.getEnclosingWidget(dialog.domNode.querySelector(".update-btn")).attr('disabled', !enable_update_btn);
+ });
+ },
+ checkNextPlugin: function() {
+ const name = dialog.plugins_to_check.shift();
+
+ if (name) {
+ this.checkUpdates(name);
+ } else {
+ const num_updated = dialog.plugins_to_update.length;
+
+ if (num_updated > 0)
+ dialog.attr('title',
+ App.l10n.ngettext('Updates pending for %d plugin', 'Updates pending for %d plugins', num_updated)
+ .replace("%d", num_updated));
+ else
+ dialog.attr('title', __("No updates available"));
+
+ dijit.getEnclosingWidget(dialog.domNode.querySelector(".update-btn"))
+ .attr('disabled', num_updated == 0);
+
+ }
+ },
+ checkUpdates: function(name) {
+ console.log('checkUpdates', name);
+
+ const container = dialog.domNode.querySelector(".update-results");
+
+ dialog.attr('title', __("Checking: %s").replace("%s", name));
+
+ //container.innerHTML = `<li class='text-center'>${__("Checking: %s...").replace("%s", name)}</li>`;
+
+ xhr.json("backend.php", {op: "pref-prefs", method: "checkForPluginUpdates", name: name}, (reply) => {
+
+ if (!reply) {
+ container.innerHTML += `<li class='text-error'>${__("%s: Operation failed: check event log.").replace("%s", name)}</li>`;
+ } else {
+
+ reply.forEach((p) => {
+ if (p.rv) {
+ if (p.rv.need_update) {
+ dialog.plugins_to_update.push(p.plugin);
+
+ const update_button = dijit.getEnclosingWidget(
+ App.find(`*[data-update-btn-for-plugin="${p.plugin}"]`));
+
+ if (update_button)
+ update_button.domNode.show();
+ }
+
+ if (p.rv.need_update || p.rv.git_status != 0) {
+ container.innerHTML +=
+ `
+ <li><h3>${p.plugin}</h3>
+ ${p.rv.stderr ? `<pre class="small text-error pre-wrap">${p.rv.stderr}</pre>` : ''}
+ ${p.rv.stdout ? `<pre class="small text-success pre-wrap">${p.rv.stdout}</pre>` : ''}
+ <div class="small">
+ ${p.rv.git_status ? App.FormFields.icon("error_outline") + " " + __("Exited with RC: %d").replace("%d", p.rv.git_status) :
+ App.FormFields.icon("check") + " " + __("Ready to update")}
+ </div>
+ </li>
+ `
+ }
+ }
+ dialog.checkNextPlugin();
+ });
+ }
+
+ });
+
+ },
+ content: `
+ <ul class="panel panel-scrollable plugin-updater-list update-results">
+ </ul>
+
+ <footer>
+ ${App.FormFields.button_tag(App.FormFields.icon("update") + " " + __("Update"), "", {disabled: true, class: "update-btn alt-primary", onclick: "App.dialogOf(this).performUpdate()"})}
+ ${App.FormFields.cancel_dialog_tag(__("Close"))}
+ </footer>
+ `,
});
+
+ const tmph = dojo.connect(dialog, 'onShow', function () {
+ dojo.disconnect(tmph);
+
+ dialog.plugins_to_update = [];
+
+ if (name) {
+ dialog.checkUpdates(name);
+ } else {
+ dialog.plugins_to_check = [...document.querySelectorAll('*[data-plugin-name][data-plugin-local=true]')].map((p) => p.getAttribute('data-plugin-name'));
+ dialog.checkNextPlugin();
+ }
+ });
+
+ dialog.show();
},
},
OPML: {
@@ -386,6 +848,7 @@ const Helpers = {
</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'>
diff --git a/js/PrefLabelTree.js b/js/PrefLabelTree.js
index 2b78927c2..39e3f8315 100644
--- a/js/PrefLabelTree.js
+++ b/js/PrefLabelTree.js
@@ -68,8 +68,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
const dialog = new fox.SingleUseDialog({
id: "labelEditDlg",
- title: __("Label Editor"),
- style: "width: 650px",
+ title: __("Edit label"),
setLabelColor: function (id, fg, bg) {
let kind = '';
@@ -121,10 +120,10 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
content: `
<form onsubmit='return false'>
- <header>${__("Caption")}</header>
<section>
- <input style='font-size : 16px; color : ${fg_color}; background : ${bg_color}; transition : background 0.1s linear'
+ <input style='font-size : 16px; width : 550px; color : ${fg_color}; background : ${bg_color}; transition : background 0.1s linear'
id='labelEdit_caption'
+ placeholder="${__("Caption")}"
name='caption'
dojoType='dijit.form.ValidationTextBox'
required='true'
@@ -138,7 +137,6 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
${App.FormFields.hidden_tag('fg_color', fg_color, {}, 'labelEdit_fgColor')}
${App.FormFields.hidden_tag('bg_color', bg_color, {}, 'labelEdit_bgColor')}
- <header>${__("Colors")}</header>
<section>
<table width='100%'>
<tr>
@@ -168,6 +166,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
<footer>
<button dojoType='dijit.form.Button' type='submit' class='alt-primary' onclick='App.dialogOf(this).execute()'>
+ ${App.FormFields.icon("save")}
${__('Save')}
</button>
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
diff --git a/js/PrefUsers.js b/js/PrefUsers.js
index 3eb83b02a..7ce3cae94 100644
--- a/js/PrefUsers.js
+++ b/js/PrefUsers.js
@@ -1,16 +1,18 @@
'use strict'
-/* global __ */
-/* global xhrPost, xhr, dijit, Notify, Tables, App, fox */
+/* global __, xhr, dijit, Notify, Tables, App, fox */
const Users = {
reload: function(sort) {
- const user_search = App.byId("user_search");
- const search = user_search ? user_search.value : "";
+ return new Promise((resolve, reject) => {
+ const user_search = App.byId("user_search");
+ const search = user_search ? user_search.value : "";
- xhr.post("backend.php", { op: "pref-users", sort: sort, search: search }, (reply) => {
- dijit.byId('usersTab').attr('content', reply);
- Notify.close();
+ xhr.post("backend.php", { op: "pref-users", sort: sort, search: search }, (reply) => {
+ dijit.byId('usersTab').attr('content', reply);
+ Notify.close();
+ resolve();
+ }, (e) => { reject(e) });
});
},
add: function() {
@@ -20,8 +22,9 @@ const Users = {
Notify.progress("Adding user...");
xhr.post("backend.php", {op: "pref-users", method: "add", login: login}, (reply) => {
- alert(reply);
- Users.reload();
+ Users.reload().then(() => {
+ Notify.info(reply);
+ })
});
}
@@ -33,14 +36,16 @@ const Users = {
const dialog = new fox.SingleUseDialog({
id: "userEditDlg",
- title: __("User Editor"),
+ title: __("Edit user"),
execute: function () {
if (this.validate()) {
Notify.progress("Saving data...", true);
- xhr.post("backend.php", this.attr('value'), () => {
+ xhr.post("backend.php", this.attr('value'), (reply) => {
dialog.hide();
- Users.reload();
+ Users.reload().then(() => {
+ Notify.info(reply);
+ });
});
}
},
@@ -54,8 +59,6 @@ const Users = {
<div dojoType="dijit.layout.TabContainer" style="height : 400px">
<div dojoType="dijit.layout.ContentPane" title="${__('Edit user')}">
- <header>${__("User")}</header>
-
<section>
<fieldset>
<label>${__("Login:")}</label>
@@ -66,11 +69,9 @@ const Users = {
${admin_disabled ? App.FormFields.hidden_tag("login", user.login) : ''}
</fieldset>
- </section>
- <header>${__("Authentication")}</header>
+ <hr/>
- <section>
<fieldset>
<label>${__('Access level: ')}</label>
${App.FormFields.select_hash("access_level",
@@ -84,11 +85,15 @@ const Users = {
<input dojoType='dijit.form.TextBox' type='password' size='20'
placeholder='${__("Change password")}' name='password'>
</fieldset>
- </section>
+ <fieldset>
+ <label></label>
+ <label class="checkbox">
+ ${App.FormFields.checkbox_tag("otp_enabled", user.otp_enabled)}
+ ${__('OTP enabled')}
+ </fieldset>
- <header>${__("Options")}</header>
+ <hr/>
- <section>
<fieldset>
<label>${__("E-mail:")}</label>
<input dojoType='dijit.form.TextBox' size='30' name='email'
@@ -110,6 +115,7 @@ const Users = {
<footer>
<button dojoType='dijit.form.Button' class='alt-primary' type='submit' onclick='App.dialogOf(this).execute()'>
+ ${App.FormFields.icon("save")}
${__('Save')}
</button>
<button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
diff --git a/js/common.js b/js/common.js
index 1544e6d0b..1f8318862 100755
--- a/js/common.js
+++ b/js/common.js
@@ -100,7 +100,7 @@ Element.prototype.fadeIn = function(display = undefined){
};
Element.prototype.visible = function() {
- return this.style.display != "none" && this.offsetHeight != 0 && this.offsetWidth != 0;
+ return window.getComputedStyle(this).display != "none"; //&& this.offsetHeight != 0 && this.offsetWidth != 0;
}
Element.visible = function(elem) {
@@ -154,7 +154,10 @@ String.prototype.stripTags = function() {
/* exported xhr */
const xhr = {
- post: function(url, params = {}, complete = undefined) {
+ _ts: 0,
+ post: function(url, params = {}, complete = undefined, failed = undefined) {
+ this._ts = new Date().getTime();
+
console.log('xhr.post', '>>>', params);
return new Promise((resolve, reject) => {
@@ -165,10 +168,13 @@ const xhr = {
postData: dojo.objectToQuery(params),
handleAs: "text",
error: function(error) {
+ if (failed != undefined)
+ failed(error);
+
reject(error);
},
load: function(data, ioargs) {
- console.log('xhr.post', '<<<', ioargs.xhr);
+ console.log('xhr.post', '<<<', ioargs.xhr, (new Date().getTime() - xhr._ts) + " ms");
if (complete != undefined)
complete(data, ioargs.xhr);
@@ -178,7 +184,7 @@ const xhr = {
);
});
},
- json: function(url, params = {}, complete = undefined) {
+ json: function(url, params = {}, complete = undefined, failed = undefined) {
return new Promise((resolve, reject) =>
this.post(url, params).then((data) => {
let obj = null;
@@ -187,13 +193,21 @@ const xhr = {
obj = JSON.parse(data);
} catch (e) {
console.error("xhr.json", e, xhr);
+
+ if (failed != undefined)
+ failed(e);
+
reject(e);
}
- console.log('xhr.json', '<<<', obj);
+ console.log('xhr.json', '<<<', obj, (new Date().getTime() - xhr._ts) + " ms");
if (obj && typeof App != "undefined")
if (!App.handleRpcJson(obj)) {
+
+ if (failed != undefined)
+ failed(obj);
+
reject(obj);
return;
}
@@ -248,8 +262,11 @@ const Lists = {
if (row)
checked ? row.addClassName("Selected") : row.removeClassName("Selected");
},
- select: function(elemId, selected) {
- $(elemId).querySelectorAll("li").forEach((row) => {
+ select: function(elem, selected) {
+ if (typeof elem == "string")
+ elem = document.getElementById(elem);
+
+ elem.querySelectorAll("li").forEach((row) => {
const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]");
if (checkNode) {
const widget = dijit.getEnclosingWidget(checkNode);
@@ -264,6 +281,30 @@ const Lists = {
}
});
},
+ getSelected: function(elem) {
+ const rv = [];
+
+ if (typeof elem == "string")
+ elem = document.getElementById(elem);
+
+ elem.querySelectorAll("li").forEach((row) => {
+ if (row.hasClassName("Selected")) {
+ const rowVal = row.getAttribute("data-row-value");
+
+ if (rowVal) {
+ rv.push(rowVal);
+ } else {
+ // either older prefix-XXX notation or separate attribute
+ const rowId = row.getAttribute("data-row-id") || row.id.replace(/^[A-Z]*?-/, "");
+
+ if (!isNaN(rowId))
+ rv.push(parseInt(rowId));
+ }
+ }
+ });
+
+ return rv;
+ }
};
/* exported Tables */
@@ -279,8 +320,11 @@ const Tables = {
checked ? row.addClassName("Selected") : row.removeClassName("Selected");
},
- select: function(elemId, selected) {
- $(elemId).querySelectorAll("tr").forEach((row) => {
+ select: function(elem, selected) {
+ if (typeof elem == "string")
+ elem = document.getElementById(elem);
+
+ elem.querySelectorAll("tr").forEach((row) => {
const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]");
if (checkNode) {
const widget = dijit.getEnclosingWidget(checkNode);
@@ -295,16 +339,25 @@ const Tables = {
}
});
},
- getSelected: function(elemId) {
+ getSelected: function(elem) {
const rv = [];
- $(elemId).querySelectorAll("tr").forEach((row) => {
+ if (typeof elem == "string")
+ elem = document.getElementById(elem);
+
+ elem.querySelectorAll("tr").forEach((row) => {
if (row.hasClassName("Selected")) {
- // either older prefix-XXX notation or separate attribute
- const rowId = row.getAttribute("data-row-id") || row.id.replace(/^[A-Z]*?-/, "");
+ const rowVal = row.getAttribute("data-row-value");
- if (!isNaN(rowId))
- rv.push(parseInt(rowId));
+ if (rowVal) {
+ rv.push(rowVal);
+ } else {
+ // either older prefix-XXX notation or separate attribute
+ const rowId = row.getAttribute("data-row-id") || row.id.replace(/^[A-Z]*?-/, "");
+
+ if (!isNaN(rowId))
+ rv.push(parseInt(rowId));
+ }
}
});
diff --git a/js/tt-rss.js b/js/tt-rss.js
index 4a7f2e643..10fafc447 100644
--- a/js/tt-rss.js
+++ b/js/tt-rss.js
@@ -69,15 +69,3 @@ require(["dojo/_base/kernel",
});
});
-/* exported hash_get */
-function hash_get(key) {
- const obj = dojo.queryToObject(window.location.hash.substring(1));
- return obj[key];
-}
-
-/* exported hash_set */
-function hash_set(key, value) {
- const obj = dojo.queryToObject(window.location.hash.substring(1));
- obj[key] = value;
- window.location.hash = dojo.objectToQuery(obj);
-}