tinytinyrss/js/App.js

1333 lines
42 KiB
JavaScript

'use strict';
/* eslint-disable new-cap */
/* global __, Article, Headlines, Filters, fox */
/* global xhr, dojo, dijit, PluginHost, Notify, Feeds, Cookie */
/* global CommonDialogs, Plugins */
const App = {
_initParams: [],
_rpc_seq: 0,
hotkey_prefix: 0,
hotkey_prefix_pressed: false,
hotkey_prefix_timeout: 0,
global_unread: -1,
_widescreen_mode: false,
_loading_progress: 0,
hotkey_actions: {},
is_prefs: false,
LABEL_BASE_INDEX: -1024,
UserAccessLevels: {
ACCESS_LEVEL_READONLY: -1
},
_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);
},
__: function(msg) {
return App._translations[msg] ? App._translations[msg] : msg;
}
},
FormFields: {
attributes_to_string: function(attributes) {
return Object.keys(attributes).map((k) =>
`${App.escapeHtml(k)}="${App.escapeHtml(attributes[k])}"`)
.join(" ");
},
hidden_tag: function(name, value, attributes = {}, id = "") {
return `<input id="${App.escapeHtml(id)}" dojoType="dijit.form.TextBox" ${this.attributes_to_string(attributes)}
style="display : none" name="${name}" value="${App.escapeHtml(value)}"></input>`
},
// allow html inside because of icons
button_tag: function(value, type, attributes = {}) {
return `<button dojoType="dijit.form.Button" ${this.attributes_to_string(attributes)}
type="${type}">${value}</button>`
},
icon: function(icon, attributes = {}) {
return `<i class="material-icons" ${this.attributes_to_string(attributes)}>${icon}</i>`;
},
submit_tag: function(value, attributes = {}) {
return this.button_tag(value, "submit", {...{class: "alt-primary"}, ...attributes});
},
cancel_dialog_tag: function(value, attributes = {}) {
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 !== '0' && checked ? "checked" : ""}
${value ? `value="${App.escapeHtml(value)}"` : ""}
${this.attributes_to_string(attributes)} id="${App.escapeHtml(id)}">`
},
select_tag: function(name, value, values = [], attributes = {}, id = "") {
return `
<select name="${name}" dojoType="fox.form.Select" id="${App.escapeHtml(id)}" ${this.attributes_to_string(attributes)}>
${values.map((v) =>
`<option ${v == value ? 'selected="selected"' : ''} value="${App.escapeHtml(v)}">${App.escapeHtml(v)}</option>`
).join("")}
</select>
`
},
select_hash: function(name, value, values = {}, attributes = {}, id = "", params = {}) {
let keys = Object.keys(values);
if (params.numeric_sort)
keys = keys.sort((a,b) => a - b);
return `
<select name="${name}" dojoType="fox.form.Select" id="${App.escapeHtml(id)}" ${this.attributes_to_string(attributes)}>
${keys.map((vk) =>
`<option ${vk == value ? 'selected="selected"' : ''} value="${App.escapeHtml(vk)}">${App.escapeHtml(values[vk])}</option>`
).join("")}
</select>
`
}
},
Scrollable: {
scrollByPages: function (elem, page_offset) {
if (!elem) return;
/* keep a line or so from the previous page */
const offset = (elem.offsetHeight - (page_offset > 0 ? 50 : -50)) * page_offset;
this.scroll(elem, offset);
},
scroll: function(elem, offset) {
if (!elem) return;
elem.scrollTop += offset;
},
isChildVisible: function(elem, ctr) {
if (!elem) return;
const ctop = ctr.scrollTop;
const cbottom = ctop + ctr.offsetHeight;
const etop = elem.offsetTop;
const ebottom = etop + elem.offsetHeight;
return etop >= ctop && ebottom <= cbottom ||
etop < ctop && ebottom > ctop || ebottom > cbottom && etop < cbottom;
},
fitsInContainer: function (elem, ctr) {
if (!elem) return;
return elem.offsetTop + elem.offsetHeight <= ctr.scrollTop + ctr.offsetHeight &&
elem.offsetTop >= ctr.scrollTop;
},
scrollTo: function (elem, ctr, params = {}) {
const force_to_top = params.force_to_top || false;
if (!elem || !ctr) return;
if (force_to_top || !App.Scrollable.fitsInContainer(elem, ctr)) {
ctr.scrollTop = elem.offsetTop;
}
}
},
byId: function(id) {
return document.getElementById(id);
},
find: function(query) {
return document.querySelector(query)
},
findAll: function(query) {
return document.querySelectorAll(query);
},
dialogOf: function (elem) {
// elem could be a Dijit widget
elem = elem.domNode ? elem.domNode : elem;
return dijit.getEnclosingWidget(elem.closest('.dijitDialog'));
},
getPhArgs(plugin, method, args = {}) {
return {...{op: "PluginHandler", plugin: plugin, method: method}, ...args};
},
label_to_feed_id: function(label) {
return this.LABEL_BASE_INDEX - 1 - Math.abs(label);
},
feed_to_label_id: function(feed) {
return this.LABEL_BASE_INDEX - 1 + Math.abs(feed);
},
getInitParam: function(k) {
return this._initParams[k];
},
setInitParam: function(k, v) {
this._initParams[k] = v;
},
nightModeChanged: function(is_night, link) {
console.log("night mode changed to", is_night);
if (link) {
const css_override = is_night ? "themes/night.css" : "themes/light.css";
link.setAttribute("href", css_override + "?" + Date.now());
}
},
setupNightModeDetection: function(callback) {
if (!App.byId("theme_css")) {
const mql = window.matchMedia('(prefers-color-scheme: dark)');
try {
mql.addEventListener("change", () => {
this.nightModeChanged(mql.matches, App.byId("theme_auto_css"));
});
} catch (e) {
console.warn("exception while trying to set MQL event listener");
}
const link = document.createElement("link");
link.rel = "stylesheet";
link.id = "theme_auto_css";
if (callback) {
link.onload = function() {
document.querySelector("body").removeClassName("css_loading");
callback();
};
link.onerror = function() {
alert("Fatal error while loading application stylesheet: " + link.getAttribute("href"));
}
}
this.nightModeChanged(mql.matches, link);
document.querySelector("head").appendChild(link);
} else {
document.querySelector("body").removeClassName("css_loading");
if (callback) callback();
}
},
postCurrentWindow: function(target, params) {
const form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", App.getInitParam("self_url_prefix") + "/" + target);
for (const [k,v] of Object.entries(params)) {
const field = document.createElement("input");
field.setAttribute("name", k);
field.setAttribute("value", v);
field.setAttribute("type", "hidden");
form.appendChild(field);
}
document.body.appendChild(form);
form.submit();
form.parentNode.removeChild(form);
},
postOpenWindow: function(target, params) {
const w = window.open("");
if (w) {
w.opener = null;
const form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", App.getInitParam("self_url_prefix") + "/" + target);
for (const [k,v] of Object.entries(params)) {
const field = document.createElement("input");
field.setAttribute("name", k);
field.setAttribute("value", v);
field.setAttribute("type", "hidden");
form.appendChild(field);
}
w.document.body.appendChild(form);
form.submit();
}
},
urlParam: function(name) {
try {
const results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href);
return decodeURIComponent(results[1].replace(/\+/g, " ")) || 0;
} catch (e) {
return 0;
}
},
next_seq: function() {
this._rpc_seq += 1;
return this._rpc_seq;
},
get_seq: function() {
return this._rpc_seq;
},
setLoadingProgress: function(p) {
this._loading_progress += p;
if (dijit.byId("loading_bar"))
dijit.byId("loading_bar").update({progress: this._loading_progress});
if (this._loading_progress >= 90) {
App.byId("overlay").hide();
}
},
isCombinedMode: function() {
return !!this.getInitParam("combined_display_mode");
},
setCombinedMode: function(combined) {
const value = combined ? "true" : "false";
xhr.post("backend.php", {op: "RPC", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
this.setInitParam("combined_display_mode",
!this.getInitParam("combined_display_mode"));
Article.close();
Headlines.renderAgain();
})
},
isExpandedMode: function() {
return !!this.getInitParam("cdm_expanded");
},
setExpandedMode: function(expand) {
if (App.isCombinedMode()) {
const value = expand ? "true" : "false";
xhr.post("backend.php", {op: "RPC", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
this.setInitParam("cdm_expanded", !this.getInitParam("cdm_expanded"));
Headlines.renderAgain();
});
} else {
alert(__("This function is only available in combined mode."));
}
},
getActionByHotkeySequence: function(sequence) {
const hotkeys_map = this.getInitParam("hotkeys");
for (const seq in hotkeys_map[1]) {
if (hotkeys_map[1].hasOwnProperty(seq)) {
if (seq == sequence) {
return hotkeys_map[1][seq];
}
}
}
},
keyeventToAction: function(event) {
const hotkeys_map = this.getInitParam("hotkeys");
const keycode = event.which;
const keychar = String.fromCharCode(keycode);
if (keycode == 27) { // escape and drop prefix
this.hotkey_prefix = false;
}
if (!this.hotkey_prefix && hotkeys_map[0].indexOf(keychar) != -1) {
this.hotkey_prefix = keychar;
App.byId("cmdline").innerHTML = keychar;
Element.show("cmdline");
window.clearTimeout(this.hotkey_prefix_timeout);
this.hotkey_prefix_timeout = window.setTimeout(() => {
this.hotkey_prefix = false;
Element.hide("cmdline");
}, 3 * 1000);
event.stopPropagation();
return false;
}
Element.hide("cmdline");
let hotkey_name = "";
if (event.type == "keydown") {
hotkey_name = "(" + keycode + ")";
// ensure ^*char notation
if (event.shiftKey) hotkey_name = "*" + hotkey_name;
if (event.ctrlKey) hotkey_name = "^" + hotkey_name;
if (event.altKey) hotkey_name = "+" + hotkey_name;
if (event.metaKey) hotkey_name = "%" + hotkey_name;
} else {
hotkey_name = keychar ? keychar : "(" + keycode + ")";
}
let hotkey_full = this.hotkey_prefix ? this.hotkey_prefix + " " + hotkey_name : hotkey_name;
this.hotkey_prefix = false;
let action_name = this.getActionByHotkeySequence(hotkey_full);
// check for mode-specific hotkey
if (!action_name) {
hotkey_full = (this.isCombinedMode() ? "{C}" : "{3}") + hotkey_full;
action_name = this.getActionByHotkeySequence(hotkey_full);
}
console.log('keyeventToAction', hotkey_full, '=>', action_name);
return action_name;
},
cleanupMemory: function(root) {
const dijits = dojo.query("[widgetid]", dijit.byId(root).domNode).map(dijit.byNode);
dijits.forEach(function (d) {
dojo.destroy(d.domNode);
});
App.findAll("#" + root + " *").forEach(function (i) {
i.parentNode ? i.parentNode.removeChild(i) : true;
});
},
// htmlspecialchars()-alike for headlines data-content attribute
escapeHtml: function(p) {
if (typeof p == "string") {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return p.replace(/[&<>"']/g, function(m) { return map[m]; });
} else {
return p;
}
},
// http://stackoverflow.com/questions/6251937/how-to-get-selecteduser-highlighted-text-in-contenteditable-element-and-replac
getSelectedText: function() {
let text = "";
if (typeof window.getSelection != "undefined") {
const sel = window.getSelection();
if (sel.rangeCount) {
const container = document.createElement("div");
for (let i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
text = container.innerHTML;
}
} else if (typeof document.selection != "undefined") {
if (document.selection.type == "Text") {
text = document.selection.createRange().textText;
}
}
return text.stripTags();
},
displayIfChecked: function(checkbox, elemId) {
if (checkbox.checked) {
Element.show(elemId);
} else {
Element.hide(elemId);
}
},
hotkeyHelp: function() {
xhr.post("backend.php", {op: "RPC", method: "hotkeyHelp"}, (reply) => {
const dialog = new fox.SingleUseDialog({
title: __("Keyboard shortcuts"),
content: reply,
});
dialog.show();
});
},
handleRpcJson: function(reply) {
const netalert = App.find(".net-alert");
if (reply) {
const error = reply['error'];
const seq = reply['seq'];
const message = reply['message'];
const counters = reply['counters'];
const runtime_info = reply['runtime-info'];
if (error && error.code && error.code != App.Error.E_SUCCESS) {
console.warn("handleRpcJson: fatal error", error);
this.Error.fatal(error.code, error.params);
return false;
}
if (seq && this.get_seq() != seq) {
console.warn("handleRpcJson: sequence mismatch: ", seq, '!=', this.get_seq());
return false;
}
// not in preferences
if (typeof Feeds != "undefined") {
if (message == "UPDATE_COUNTERS") {
console.log("need to refresh counters for", reply.feeds);
Feeds.requestCounters(reply.feeds);
}
if (counters)
Feeds.parseCounters(counters);
}
if (runtime_info)
this.parseRuntimeInfo(runtime_info);
if (netalert) netalert.hide();
return true;
} else {
if (netalert) netalert.show();
Notify.error("Communication problem with server.");
return false;
}
},
parseRuntimeInfo: function(data) {
Object.keys(data).forEach((k) => {
const v = data[k];
console.log("RI:", k, "=>", v);
if (k == "daemon_is_running" && v != 1) {
Notify.error("Update daemon is not running.", true);
return;
}
if (k == "recent_log_events") {
const alert = App.find(".log-alert");
if (alert) {
v > 0 ? alert.show() : alert.hide();
}
}
if (k == "daemon_stamp_ok" && v != 1) {
Notify.error("Update daemon is not updating feeds.", true);
return;
}
if (typeof Feeds != "undefined") {
if (k == "max_feed_id" || k == "num_feeds") {
if (this.getInitParam(k) && this.getInitParam(k) != v) {
console.log("feed count changed, need to reload feedlist:", this.getInitParam(k), v);
Feeds.reload();
}
}
}
this.setInitParam(k, v);
});
PluginHost.run(PluginHost.HOOK_RUNTIME_INFO_LOADED, data);
},
backendSanityCallback: function(reply) {
console.log("sanity check ok");
const params = reply['init-params'];
if (params) {
console.log('reading init-params...');
Object.keys(params).forEach((k) => {
switch (k) {
case "label_base_index":
this.LABEL_BASE_INDEX = parseInt(params[k]);
break;
case "cdm_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":
// filter mnemonic definitions (used for help panel) from hotkeys map
// i.e. *(191)|Ctrl-/ -> *(191)
{
const tmp = [];
Object.keys(params[k][1]).forEach((sequence) => {
const filtered = sequence.replace(/\|.*$/, "");
tmp[filtered] = params[k][1][sequence];
});
params[k][1] = tmp;
}
break;
}
console.log("IP:", k, "=>", params[k]);
this.setInitParam(k, params[k]);
});
// PluginHost might not be available on non-index pages
if (typeof PluginHost !== 'undefined')
PluginHost.run(PluginHost.HOOK_PARAMS_LOADED, this._initParams);
}
const translations = reply['translations'];
if (translations) {
console.log('reading translations...');
App._translations = translations;
}
this.initSecondStage();
},
Error: {
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";
return;
} 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(error,
{...{title: __("Fatal error")}, ...params});
},
report: function(error, params = {}) {
if (!error) return;
console.error("error.report:", error, params);
const message = params.message ? params.message : error.toString();
try {
xhr.post("backend.php",
{op: "RPC", method: "log",
file: params.filename ? params.filename : error.fileName,
line: params.lineno ? params.lineno : error.lineNumber,
msg: message,
context: error.stack},
(reply) => {
console.warn("[Error.report] log response", reply);
});
} catch (re) {
console.error("[Error.report] exception while saving logging error on server", re);
}
try {
const dialog = new fox.SingleUseDialog({
title: params.title || __("Unhandled exception"),
content: `
<div class='exception-contents'>
<h3>${message}</h3>
${params.description ? `<p>${params.description}</p>` : ''}
${error.stack ?
`<header>${__('Stack trace')}</header>
<section>
<textarea readonly='readonly'>${error.stack}</textarea>
</section>` : ''}
${params && params.info ?
`
<header>${__('Additional information')}</header>
<section>
<textarea readonly='readonly'>${params.info}</textarea>
</section>
` : ''}
</div>
<footer class='text-center'>
<button dojoType="dijit.form.Button" class='alt-primary' type='submit'>
${__('Close this window')}
</button>
</footer>
</div>`
});
dialog.show();
} catch (de) {
console.error("[Error.report] exception while showing error dialog", de);
alert(error.stack ? error.stack : message);
}
},
onWindowError: function (message, filename, lineno, colno, error) {
// called without context (this) from window.onerror
App.Error.report(error,
{message: message, filename: filename, lineno: lineno, colno: colno});
},
},
isPrefs() {
return this.is_prefs;
},
audioCanPlay: function(ctype) {
const a = document.createElement('audio');
return a.canPlayType(ctype);
},
init: function(parser, is_prefs) {
this.is_prefs = is_prefs;
window.onerror = this.Error.onWindowError;
this.setInitParam("csrf_token", __csrf_token);
this.setupNightModeDetection(() => {
parser.parse();
console.log('is_prefs', this.is_prefs);
if (!this.checkBrowserFeatures())
return;
this.setLoadingProgress(30);
this.initHotkeyActions();
const params = {
op: "RPC",
method: "sanityCheck",
clientTzOffset: new Date().getTimezoneOffset() * 60,
hasSandbox: "sandbox" in document.createElement("iframe"),
clientLocation: window.location.href
};
xhr.json("backend.php", params, (reply) => {
try {
this.backendSanityCallback(reply);
} catch (e) {
this.Error.report(e);
}
});
});
},
checkBrowserFeatures: function() {
let errorMsg = "";
['MutationObserver', 'requestIdleCallback'].forEach((t) => {
if (!(t in window)) {
errorMsg = `Browser check failed: <code>window.${t}</code> not found.`;
throw new Error(errorMsg);
}
});
if (typeof Promise.allSettled == "undefined") {
errorMsg = `Browser check failed: <code>Promise.allSettled</code> is not defined.`;
throw new Error(errorMsg);
}
return errorMsg == "";
},
updateRuntimeInfo: function() {
xhr.json("backend.php", {op: "RPC", method: "getruntimeinfo"}, () => {
// handled by xhr.json()
});
},
initSecondStage: function() {
document.onkeydown = (event) => this.hotkeyHandler(event);
document.onkeypress = (event) => this.hotkeyHandler(event);
if (this.is_prefs) {
this.setLoadingProgress(70);
Notify.close();
let tab = this.urlParam('tab');
if (tab) {
tab = dijit.byId(tab + "Tab");
if (tab) {
dijit.byId("pref-tabs").selectChild(tab);
const method = this.urlParam("method");
if (method) {
switch (method) {
case "editfeed":
window.setTimeout(() => {
CommonDialogs.editFeed(this.urlParam('methodparam'))
}, 100);
break;
default:
console.warn("initSecondStage, unknown method:", method);
}
}
}
} else {
let tab = localStorage.getItem("ttrss:prefs-tab");
if (tab) {
tab = dijit.byId(tab);
if (tab) {
dijit.byId("pref-tabs").selectChild(tab);
}
}
}
dojo.connect(dijit.byId("pref-tabs"), "selectChild", function (elem) {
localStorage.setItem("ttrss:prefs-tab", elem.id);
App.updateRuntimeInfo();
});
} else {
Feeds.reload();
Article.close();
if (parseInt(Cookie.get("ttrss_fh_width")) > 0) {
dijit.byId("feeds-holder").domNode.setStyle(
{width: Cookie.get("ttrss_fh_width") + "px"});
}
dijit.byId("main").resize();
dojo.connect(dijit.byId('feeds-holder'), 'resize',
(args) => {
if (args && args.w >= 0) {
Cookie.set("ttrss_fh_width", args.w, this.getInitParam("cookie_lifetime"));
}
});
dojo.connect(dijit.byId('content-insert'), 'resize',
(args) => {
if (args && args.w >= 0 && args.h >= 0) {
const cookie_suffix = this._widescreen_mode ? "wide" : "normal";
Cookie.set("ttrss_ci_width:" + cookie_suffix, args.w, this.getInitParam("cookie_lifetime"));
Cookie.set("ttrss_ci_height:" + cookie_suffix, args.h, this.getInitParam("cookie_lifetime"));
}
});
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.setWideScreenMode(this.isWideScreenMode(), true);
Headlines.initScrollHandler();
if (this.getInitParam("simple_update")) {
console.log("scheduling simple feed updater...");
window.setInterval(() => { Feeds.updateRandom() }, 30 * 1000);
}
if (this.getInitParam('check_for_updates')) {
window.setInterval(() => {
this.checkForUpdates();
}, 3600 * 1000);
}
PluginHost.run(PluginHost.HOOK_INIT_COMPLETE, null);
}
if (!this.getInitParam("bw_limit"))
window.setInterval(() => {
App.updateRuntimeInfo();
}, 60 * 1000)
if (App.getInitParam("safe_mode") && this.isPrefs()) {
CommonDialogs.safeModeWarning();
}
console.log("second stage ok");
},
checkForUpdates: function() {
console.log('checking for updates...');
xhr.json("backend.php", {op: 'RPC', method: 'checkforupdates'})
.then((reply) => {
console.log('update reply', reply);
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 {
icon.hide();
}
});
},
updateTitle: function() {
let tmp = "Tiny Tiny RSS";
if (this.global_unread > 0) {
tmp = "(" + this.global_unread + ") " + tmp;
}
document.title = tmp;
},
hotkeyHandler: function(event) {
if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA") return;
// Arrow buttons and escape are not reported via keypress, handle them via keydown.
// escape = 27, left = 37, up = 38, right = 39, down = 40, pgup = 33, pgdn = 34, insert = 45, delete = 46
if (event.type == "keydown" && event.which != 27 && (event.which < 33 || event.which > 46)) return;
const action_name = this.keyeventToAction(event);
if (action_name) {
const action_func = this.hotkey_actions[action_name];
if (action_func != null) {
action_func(event);
event.stopPropagation();
return false;
}
}
},
isWideScreenMode: function() {
return !!this._widescreen_mode;
},
setWideScreenMode: function(wide, quiet = false) {
if (this.isCombinedMode() && !quiet) {
alert(__("Widescreen is not available in combined mode."));
return;
}
this._widescreen_mode = 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');
content_insert.attr("region", "trailing");
content_insert.domNode.setStyle({width: '50%',
height: 'auto',
borderTopWidth: '0px' });
if (parseInt(Cookie.get("ttrss_ci_width:wide")) > 0) {
content_insert.domNode.setStyle(
{width: Cookie.get("ttrss_ci_width:wide") + "px" });
}
headlines_frame.setStyle({ borderBottomWidth: '0px' });
} else {
content_insert.attr("region", "bottom");
content_insert.domNode.setStyle({width: 'auto',
height: '50%',
borderTopWidth: '0px'});
if (parseInt(Cookie.get("ttrss_ci_height:normal")) > 0) {
content_insert.domNode.setStyle(
{height: Cookie.get("ttrss_ci_height:normal") + "px" });
}
headlines_frame.setStyle({ borderBottomWidth: '1px' });
}
headlines_frame.setAttribute("data-is-wide-screen", wide ? "true" : "false");
Article.close();
if (article_id) Article.view(article_id);
xhr.post("backend.php", {op: "RPC", method: "setWidescreen", wide: wide ? 1 : 0});
},
initHotkeyActions: function() {
if (this.is_prefs) {
this.hotkey_actions["feed_subscribe"] = () => {
CommonDialogs.subscribeToFeed();
};
this.hotkey_actions["create_label"] = () => {
CommonDialogs.addLabel();
};
this.hotkey_actions["create_filter"] = () => {
Filters.edit();
};
this.hotkey_actions["help_dialog"] = () => {
this.hotkeyHelp();
};
} else {
this.hotkey_actions["next_feed"] = () => {
const [feed, is_cat] = Feeds.getNextFeed(
Feeds.getActive(), Feeds.activeIsCat());
if (feed !== false)
Feeds.open({feed: feed, is_cat: is_cat, delayed: true})
};
this.hotkey_actions["next_unread_feed"] = () => {
const [feed, is_cat] = Feeds.getNextFeed(
Feeds.getActive(), Feeds.activeIsCat(), true);
if (feed !== false)
Feeds.open({feed: feed, is_cat: is_cat, delayed: true})
};
this.hotkey_actions["prev_feed"] = () => {
const [feed, is_cat] = Feeds.getPreviousFeed(
Feeds.getActive(), Feeds.activeIsCat());
if (feed !== false)
Feeds.open({feed: feed, is_cat: is_cat, delayed: true})
};
this.hotkey_actions["prev_unread_feed"] = () => {
const [feed, is_cat] = Feeds.getPreviousFeed(
Feeds.getActive(), Feeds.activeIsCat(), 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())
Headlines.scroll(Headlines.line_scroll_offset, event);
else
Headlines.move('next');
};
this.hotkey_actions["prev_article_or_scroll"] = (event) => {
if (this.isCombinedMode())
Headlines.scroll(-Headlines.line_scroll_offset, event);
else
Headlines.move('prev');
};
this.hotkey_actions["next_article_noscroll"] = () => {
Headlines.move('next');
};
this.hotkey_actions["prev_article_noscroll"] = () => {
Headlines.move('prev');
};
this.hotkey_actions["next_article_noexpand"] = () => {
Headlines.move('next', {no_expand: true});
};
this.hotkey_actions["prev_article_noexpand"] = () => {
Headlines.move('prev', {no_expand: true});
};
this.hotkey_actions["search_dialog"] = () => {
Feeds.search();
};
this.hotkey_actions["cancel_search"] = () => {
Feeds.cancelSearch();
};
this.hotkey_actions["toggle_mark"] = () => {
Headlines.selectionToggleMarked();
};
this.hotkey_actions["toggle_publ"] = () => {
Headlines.selectionTogglePublished();
};
this.hotkey_actions["toggle_unread"] = () => {
Headlines.selectionToggleUnread({no_error: 1});
};
this.hotkey_actions["edit_tags"] = () => {
const id = Article.getActive();
if (id) {
Article.editTags(id);
}
};
this.hotkey_actions["open_in_new_window"] = () => {
if (Article.getActive()) {
Article.openInNewWindow(Article.getActive());
}
};
this.hotkey_actions["catchup_below"] = () => {
Headlines.catchupRelativeTo(1);
};
this.hotkey_actions["catchup_above"] = () => {
Headlines.catchupRelativeTo(0);
};
this.hotkey_actions["article_scroll_down"] = (event) => {
if (this.isCombinedMode())
Headlines.scroll(Headlines.line_scroll_offset, event);
else
Article.scroll(Headlines.line_scroll_offset, event);
};
this.hotkey_actions["article_scroll_up"] = (event) => {
if (this.isCombinedMode())
Headlines.scroll(-Headlines.line_scroll_offset, event);
else
Article.scroll(-Headlines.line_scroll_offset, event);
};
this.hotkey_actions["next_headlines_page"] = (event) => {
Headlines.scrollByPages(1, event);
};
this.hotkey_actions["prev_headlines_page"] = (event) => {
Headlines.scrollByPages(-1, event);
};
this.hotkey_actions["article_page_down"] = (event) => {
if (this.isCombinedMode())
Headlines.scrollByPages(1, event);
else
Article.scrollByPages(1, event);
};
this.hotkey_actions["article_page_up"] = (event) => {
if (this.isCombinedMode())
Headlines.scrollByPages(-1, event);
else
Article.scrollByPages(-1, event);
};
this.hotkey_actions["close_article"] = () => {
if (this.isCombinedMode()) {
Article.cdmUnsetActive();
} else {
Article.close();
}
};
this.hotkey_actions["email_article"] = () => {
if (typeof Plugins.Mail != "undefined") {
Plugins.Mail.onHotkey(Headlines.getSelected());
} else {
alert(__("Please enable mail or mailto plugin first."));
}
};
this.hotkey_actions["select_all"] = () => {
Headlines.select('all');
};
this.hotkey_actions["select_unread"] = () => {
Headlines.select('unread');
};
this.hotkey_actions["select_marked"] = () => {
Headlines.select('marked');
};
this.hotkey_actions["select_published"] = () => {
Headlines.select('published');
};
this.hotkey_actions["select_invert"] = () => {
Headlines.select('invert');
};
this.hotkey_actions["select_none"] = () => {
Headlines.select('none');
};
this.hotkey_actions["feed_refresh"] = () => {
if (typeof Feeds.getActive() != "undefined") {
Feeds.open({feed: Feeds.getActive(), is_cat: Feeds.activeIsCat()});
}
};
this.hotkey_actions["feed_unhide_read"] = () => {
Feeds.toggleUnread();
};
this.hotkey_actions["feed_subscribe"] = () => {
CommonDialogs.subscribeToFeed();
};
this.hotkey_actions["feed_debug_update"] = () => {
if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) {
/* global __csrf_token */
App.postOpenWindow("backend.php", {op: "Feeds", method: "updatedebugger",
feed_id: Feeds.getActive(), csrf_token: __csrf_token});
} else {
alert("You can't debug this kind of feed.");
}
};
this.hotkey_actions["feed_debug_viewfeed"] = () => {
App.postOpenWindow("backend.php", {op: "Feeds", method: "view",
feed: Feeds.getActive(), timestamps: 1, debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token});
};
this.hotkey_actions["feed_edit"] = () => {
if (Feeds.activeIsCat())
alert(__("You can't edit this kind of feed."));
else
CommonDialogs.editFeed(Feeds.getActive());
};
this.hotkey_actions["feed_catchup"] = () => {
if (typeof Feeds.getActive() != "undefined") {
Feeds.catchupCurrent();
}
};
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();
})
};
this.hotkey_actions["catchup_all"] = () => {
Feeds.catchupAll();
};
this.hotkey_actions["cat_toggle_collapse"] = () => {
if (Feeds.activeIsCat()) {
dijit.byId("feedTree").collapseCat(Feeds.getActive());
}
};
this.hotkey_actions["goto_read"] = () => {
Feeds.open({feed: Feeds.FEED_RECENTLY_READ});
};
this.hotkey_actions["goto_all"] = () => {
Feeds.open({feed: Feeds.FEED_ALL});
};
this.hotkey_actions["goto_fresh"] = () => {
Feeds.open({feed: Feeds.FEED_FRESH});
};
this.hotkey_actions["goto_marked"] = () => {
Feeds.open({feed: Feeds.FEED_STARRED});
};
this.hotkey_actions["goto_published"] = () => {
Feeds.open({feed: Feeds.FEED_PUBLISHED});
};
this.hotkey_actions["goto_prefs"] = () => {
App.openPreferences();
};
this.hotkey_actions["select_article_cursor"] = () => {
const id = Article.getUnderPointer();
if (id) {
const row = App.byId(`RROW-${id}`);
if (row)
row.toggleClassName("Selected");
}
};
this.hotkey_actions["create_label"] = () => {
CommonDialogs.addLabel();
};
this.hotkey_actions["create_filter"] = () => {
Filters.edit();
};
this.hotkey_actions["collapse_sidebar"] = () => {
Feeds.toggle();
};
this.hotkey_actions["toggle_full_text"] = () => {
if (typeof Plugins.Af_Readability != "undefined") {
if (Article.getActive())
Plugins.Af_Readability.embed(Article.getActive());
} else {
alert(__("Please enable af_readability first."));
}
};
this.hotkey_actions["toggle_widescreen"] = () => {
this.setWideScreenMode(!this.isWideScreenMode());
};
this.hotkey_actions["help_dialog"] = () => {
this.hotkeyHelp();
};
this.hotkey_actions["toggle_combined_mode"] = () => {
App.setCombinedMode(!App.isCombinedMode());
};
this.hotkey_actions["toggle_cdm_expanded"] = () => {
App.setExpandedMode(!App.isExpandedMode());
};
this.hotkey_actions["article_span_grid"] = () => {
Article.cdmToggleGridSpan(Article.getActive());
};
}
},
openPreferences: function(tab) {
document.location.href = "prefs.php" + (tab ? "?tab=" + tab : "");
},
onActionSelected: function(opid) {
switch (opid) {
case "qmcPrefs":
App.openPreferences();
break;
case "qmcLogout":
App.postCurrentWindow("public.php", {op: "logout", csrf_token: __csrf_token});
break;
case "qmcSearch":
Feeds.search();
break;
case "qmcFilterFeeds":
Feeds.filter();
break;
case "qmcAddFeed":
CommonDialogs.subscribeToFeed();
break;
case "qmcDigest":
window.location.href = "backend.php?op=Digest";
break;
case "qmcEditFeed":
if (Feeds.activeIsCat())
alert(__("You can't edit this kind of feed."));
else
CommonDialogs.editFeed(Feeds.getActive());
break;
case "qmcRemoveFeed":
{
const actid = Feeds.getActive();
if (!actid) {
alert(__("Please select some feed first."));
return;
}
if (Feeds.activeIsCat()) {
alert(__("You can't unsubscribe from the category."));
return;
}
const fn = Feeds.getName(actid);
if (confirm(__("Unsubscribe from %s?").replace("%s", fn))) {
CommonDialogs.unsubscribeFeed(actid);
}
}
break;
case "qmcCatchupAll":
Feeds.catchupAll();
break;
case "qmcShowOnlyUnread":
Feeds.toggleUnread();
break;
case "qmcToggleWidescreen":
App.setWideScreenMode(!App.isWideScreenMode());
break;
case "qmcToggleCombined":
App.setCombinedMode(!App.isCombinedMode());
break;
case "qmcToggleExpanded":
App.setExpandedMode(!App.isExpandedMode());
break;
case "qmcHKhelp":
this.hotkeyHelp()
break;
default:
console.log("quickMenuGo: unknown action: " + opid);
}
},
}