]> git.wh0rd.org Git - tt-rss.git/blob - js/tt-rss.js
56e89addc7815c6074e0a5d4a0befab63dca5804
[tt-rss.git] / js / tt-rss.js
1 /* global dijit, __ */
2
3 let global_unread = -1;
4 let hotkey_prefix = false;
5 let hotkey_prefix_pressed = false;
6 let hotkey_actions = {};
7 let _widescreen_mode = false;
8 let _rpc_seq = 0;
9 let _active_feed_id = 0;
10 let _active_feed_is_cat = false;
11
12 function next_seq() {
13         _rpc_seq += 1;
14         return _rpc_seq;
15 }
16
17 function get_seq() {
18         return _rpc_seq;
19 }
20
21 function activeFeedIsCat() {
22         return !!_active_feed_is_cat;
23 }
24
25 function getActiveFeedId() {
26         return _active_feed_id;
27 }
28
29 function setActiveFeedId(id, is_cat) {
30         hash_set('f', id);
31         hash_set('c', is_cat ? 1 : 0);
32
33         _active_feed_id = id;
34         _active_feed_is_cat = is_cat;
35
36         $("headlines-frame").setAttribute("feed-id", id);
37         $("headlines-frame").setAttribute("is-cat", is_cat ? 1 : 0);
38
39         selectFeed(id, is_cat);
40
41         PluginHost.run(PluginHost.HOOK_FEED_SET_ACTIVE, _active_article_id);
42 }
43
44
45 function updateFeedList() {
46         try {
47                 Element.show("feedlistLoading");
48
49                 resetCounterCache();
50
51                 if (dijit.byId("feedTree")) {
52                         dijit.byId("feedTree").destroyRecursive();
53                 }
54
55                 const store = new dojo.data.ItemFileWriteStore({
56                         url: "backend.php?op=pref_feeds&method=getfeedtree&mode=2"
57                 });
58
59                 const treeModel = new fox.FeedStoreModel({
60                         store: store,
61                         query: {
62                                 "type": getInitParam('enable_feed_cats') == 1 ? "category" : "feed"
63                         },
64                         rootId: "root",
65                         rootLabel: "Feeds",
66                         childrenAttrs: ["items"]
67                 });
68
69                 const tree = new fox.FeedTree({
70                         model: treeModel,
71                         onClick: function (item, node) {
72                                 const id = String(item.id);
73                                 const is_cat = id.match("^CAT:");
74                                 const feed = id.substr(id.indexOf(":") + 1);
75                                 viewfeed({feed: feed, is_cat: is_cat});
76                                 return false;
77                         },
78                         openOnClick: false,
79                         showRoot: false,
80                         persist: true,
81                         id: "feedTree",
82                 }, "feedTree");
83
84                 var tmph = dojo.connect(dijit.byId('feedMenu'), '_openMyself', function (event) {
85                         console.log(dijit.getEnclosingWidget(event.target));
86                         dojo.disconnect(tmph);
87                 });
88
89                 $("feeds-holder").appendChild(tree.domNode);
90
91                 var tmph = dojo.connect(tree, 'onLoad', function () {
92                         dojo.disconnect(tmph);
93                         Element.hide("feedlistLoading");
94
95                         try {
96                                 feedlist_init();
97
98                                 loading_set_progress(25);
99                         } catch (e) {
100                                 exception_error(e);
101                         }
102                 });
103
104                 tree.startup();
105         } catch (e) {
106                 exception_error(e);
107         }
108 }
109
110 function catchupAllFeeds() {
111
112         const str = __("Mark all articles as read?");
113
114         if (getInitParam("confirm_feed_catchup") != 1 || confirm(str)) {
115
116                 notify_progress("Marking all feeds as read...");
117
118                 xhrPost("backend.php", {op: "feeds", method: "catchupAll"}, () => {
119                         request_counters(true);
120                         viewCurrentFeed();
121                 });
122
123                 global_unread = 0;
124                 updateTitle("");
125         }
126 }
127
128 function viewCurrentFeed(method) {
129         console.log("viewCurrentFeed: " + method);
130
131         if (getActiveFeedId() != undefined) {
132                 viewfeed({feed: getActiveFeedId(), is_cat: activeFeedIsCat(), method: method});
133         }
134         return false; // block unneeded form submits
135 }
136
137 function timeout() {
138         if (getInitParam("bw_limit") != "1") {
139                 request_counters(true);
140                 setTimeout(timeout, 60*1000);
141         }
142 }
143
144 function search() {
145         const query = "backend.php?op=feeds&method=search&param=" +
146                 param_escape(getActiveFeedId() + ":" + activeFeedIsCat());
147
148         if (dijit.byId("searchDlg"))
149                 dijit.byId("searchDlg").destroyRecursive();
150
151         const dialog = new dijit.Dialog({
152                 id: "searchDlg",
153                 title: __("Search"),
154                 style: "width: 600px",
155                 execute: function() {
156                         if (this.validate()) {
157                                 _search_query = dojo.objectToQuery(this.attr('value'));
158                                 this.hide();
159                                 viewCurrentFeed();
160                         }
161                 },
162                 href: query});
163
164         dialog.show();
165 }
166
167 function updateTitle() {
168         let tmp = "Tiny Tiny RSS";
169
170         if (global_unread > 0) {
171                 tmp = "(" + global_unread + ") " + tmp;
172         }
173
174         document.title = tmp;
175 }
176
177 function genericSanityCheck() {
178         setCookie("ttrss_test", "TEST");
179
180         if (getCookie("ttrss_test") != "TEST") {
181                 return fatalError(2);
182         }
183
184         return true;
185 }
186
187
188 function init() {
189
190         window.onerror = function(message, filename, lineno, colno, error) {
191                 report_error(message, filename, lineno, colno, error);
192         };
193
194         require(["dojo/_base/kernel",
195                         "dojo/ready",
196                         "dojo/parser",
197                         "dojo/_base/loader",
198                         "dojo/_base/html",
199                         "dojo/query",
200                         "dijit/ProgressBar",
201                         "dijit/ColorPalette",
202                         "dijit/Dialog",
203                         "dijit/form/Button",
204                         "dijit/form/ComboButton",
205                         "dijit/form/CheckBox",
206                         "dijit/form/DropDownButton",
207                         "dijit/form/FilteringSelect",
208                         "dijit/form/Form",
209                         "dijit/form/RadioButton",
210                         "dijit/form/Select",
211                 "dijit/form/MultiSelect",
212                         "dijit/form/SimpleTextarea",
213                         "dijit/form/TextBox",
214                         "dijit/form/ComboBox",
215                         "dijit/form/ValidationTextBox",
216                         "dijit/InlineEditBox",
217                         "dijit/layout/AccordionContainer",
218                         "dijit/layout/BorderContainer",
219                         "dijit/layout/ContentPane",
220                         "dijit/layout/TabContainer",
221                         "dijit/PopupMenuItem",
222                         "dijit/Menu",
223                         "dijit/Toolbar",
224                         "dijit/Tree",
225                         "dijit/tree/dndSource",
226                         "dijit/tree/ForestStoreModel",
227                         "dojo/data/ItemFileWriteStore",
228                         "fox/FeedStoreModel",
229                         "fox/FeedTree" ], function (dojo, ready, parser) {
230
231                         ready(function() {
232
233                                 try {
234                                         parser.parse();
235
236                                         if (!genericSanityCheck())
237                                                 return false;
238
239                                         loading_set_progress(30);
240
241                                         const a = document.createElement('audio');
242
243                                         const hasAudio = !!a.canPlayType;
244                                         const hasSandbox = "sandbox" in document.createElement("iframe");
245                                         const hasMp3 = !!(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, ''));
246                                         const clientTzOffset = new Date().getTimezoneOffset() * 60;
247
248                                         init_hotkey_actions();
249
250                                         const params = {
251                             op: "rpc", method: "sanityCheck", hasAudio: hasAudio,
252                             hasMp3: hasMp3,
253                             clientTzOffset: clientTzOffset,
254                             hasSandbox: hasSandbox
255                         };
256
257                                         xhrPost("backend.php", params, (transport) => {
258                         try {
259                             backend_sanity_check_callback(transport);
260                         } catch (e) {
261                             console.error(e);
262                         }
263                                         });
264
265                                 } catch (e) {
266                                         exception_error(e);
267                                 }
268
269                         });
270
271
272         });
273 }
274
275 function init_hotkey_actions() {
276         hotkey_actions["next_feed"] = function() {
277                 const rv = dijit.byId("feedTree").getNextFeed(
278                         getActiveFeedId(), activeFeedIsCat());
279
280                 if (rv) viewfeed({feed: rv[0], is_cat: rv[1], can_wait: true})
281         };
282         hotkey_actions["prev_feed"] = function() {
283                 const rv = dijit.byId("feedTree").getPreviousFeed(
284                         getActiveFeedId(), activeFeedIsCat());
285
286                 if (rv) viewfeed({feed: rv[0], is_cat: rv[1], can_wait: true})
287         };
288         hotkey_actions["next_article"] = function() {
289                 moveToPost('next');
290         };
291         hotkey_actions["prev_article"] = function() {
292                 moveToPost('prev');
293         };
294         hotkey_actions["next_article_noscroll"] = function() {
295                 moveToPost('next', true);
296         };
297         hotkey_actions["prev_article_noscroll"] = function() {
298                 moveToPost('prev', true);
299         };
300         hotkey_actions["next_article_noexpand"] = function() {
301                 moveToPost('next', true, true);
302         };
303         hotkey_actions["prev_article_noexpand"] = function() {
304                 moveToPost('prev', true, true);
305         };
306         hotkey_actions["collapse_article"] = function() {
307                 const id = getActiveArticleId();
308                 const elem = $("CICD-"+id);
309
310                 if (elem) {
311                         if (elem.visible()) {
312                                 cdmCollapseArticle(null, id);
313                         }
314                         else {
315                                 cdmExpandArticle(id);
316                         }
317                 }
318         };
319         hotkey_actions["toggle_expand"] = function() {
320                 const id = getActiveArticleId();
321                 const elem = $("CICD-"+id);
322
323                 if (elem) {
324                         if (elem.visible()) {
325                                 cdmCollapseArticle(null, id, false);
326                         }
327                         else {
328                                 cdmExpandArticle(id);
329                         }
330                 }
331         };
332         hotkey_actions["search_dialog"] = function() {
333                 search();
334         };
335         hotkey_actions["toggle_mark"] = function() {
336                 selectionToggleMarked(undefined, false, true);
337         };
338         hotkey_actions["toggle_publ"] = function() {
339                 selectionTogglePublished(undefined, false, true);
340         };
341         hotkey_actions["toggle_unread"] = function() {
342                 selectionToggleUnread(undefined, false, true);
343         };
344         hotkey_actions["edit_tags"] = function() {
345                 const id = getActiveArticleId();
346                 if (id) {
347                         editArticleTags(id);
348                 }
349         }
350         hotkey_actions["open_in_new_window"] = function() {
351                 if (getActiveArticleId()) {
352                         openArticleInNewWindow(getActiveArticleId());
353                 }
354         };
355         hotkey_actions["catchup_below"] = function() {
356                 catchupRelativeToArticle(1);
357         };
358         hotkey_actions["catchup_above"] = function() {
359                 catchupRelativeToArticle(0);
360         };
361         hotkey_actions["article_scroll_down"] = function() {
362                 scrollArticle(40);
363         };
364         hotkey_actions["article_scroll_up"] = function() {
365                 scrollArticle(-40);
366         };
367         hotkey_actions["close_article"] = function() {
368                 if (isCdmMode()) {
369                         if (!getInitParam("cdm_expanded")) {
370                                 cdmCollapseArticle(false, getActiveArticleId());
371                         }
372                 } else {
373                         closeArticlePanel();
374                 }
375         };
376         hotkey_actions["email_article"] = function() {
377                 if (typeof emailArticle != "undefined") {
378                         emailArticle();
379                 } else if (typeof mailtoArticle != "undefined") {
380                         mailtoArticle();
381                 } else {
382                         alert(__("Please enable mail plugin first."));
383                 }
384         };
385         hotkey_actions["select_all"] = function() {
386                 selectArticles('all');
387         };
388         hotkey_actions["select_unread"] = function() {
389                 selectArticles('unread');
390         };
391         hotkey_actions["select_marked"] = function() {
392                 selectArticles('marked');
393         };
394         hotkey_actions["select_published"] = function() {
395                 selectArticles('published');
396         };
397         hotkey_actions["select_invert"] = function() {
398                 selectArticles('invert');
399         };
400         hotkey_actions["select_none"] = function() {
401                 selectArticles('none');
402         };
403         hotkey_actions["feed_refresh"] = function() {
404                 if (getActiveFeedId() != undefined) {
405                         viewfeed({feed: getActiveFeedId(), is_cat: activeFeedIsCat()});
406                         return;
407                 }
408         };
409         hotkey_actions["feed_unhide_read"] = function() {
410                 toggleDispRead();
411         };
412         hotkey_actions["feed_subscribe"] = function() {
413                 quickAddFeed();
414         };
415         hotkey_actions["feed_debug_update"] = function() {
416                 if (!activeFeedIsCat() && parseInt(getActiveFeedId()) > 0) {
417                         window.open("backend.php?op=feeds&method=update_debugger&feed_id=" + getActiveFeedId() +
418                                 "&csrf_token=" + getInitParam("csrf_token"));
419                 } else {
420                         alert("You can't debug this kind of feed.");
421                 }
422         };
423
424         hotkey_actions["feed_debug_viewfeed"] = function() {
425                 viewfeed({feed: getActiveFeedId(), is_cat: activeFeedIsCat(), viewfeed_debug: true});
426         };
427
428         hotkey_actions["feed_edit"] = function() {
429                 if (activeFeedIsCat())
430                         alert(__("You can't edit this kind of feed."));
431                 else
432                         editFeed(getActiveFeedId());
433         };
434         hotkey_actions["feed_catchup"] = function() {
435                 if (getActiveFeedId() != undefined) {
436                         catchupCurrentFeed();
437                         return;
438                 }
439         };
440         hotkey_actions["feed_reverse"] = function() {
441                 reverseHeadlineOrder();
442         };
443         hotkey_actions["feed_toggle_vgroup"] = function() {
444                 xhrPost("backend.php", {op: "rpc", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
445                         viewCurrentFeed();
446                 })
447         };
448         hotkey_actions["catchup_all"] = function() {
449                 catchupAllFeeds();
450         };
451         hotkey_actions["cat_toggle_collapse"] = function() {
452                 if (activeFeedIsCat()) {
453                         dijit.byId("feedTree").collapseCat(getActiveFeedId());
454                         return;
455                 }
456         };
457         hotkey_actions["goto_all"] = function() {
458                 viewfeed({feed: -4});
459         };
460         hotkey_actions["goto_fresh"] = function() {
461                 viewfeed({feed: -3});
462         };
463         hotkey_actions["goto_marked"] = function() {
464                 viewfeed({feed: -1});
465         };
466         hotkey_actions["goto_published"] = function() {
467                 viewfeed({feed: -2});
468         };
469         hotkey_actions["goto_tagcloud"] = function() {
470                 displayDlg(__("Tag cloud"), "printTagCloud");
471         };
472         hotkey_actions["goto_prefs"] = function() {
473                 gotoPreferences();
474         };
475         hotkey_actions["select_article_cursor"] = function() {
476                 const id = getArticleUnderPointer();
477                 if (id) {
478                         const row = $("RROW-" + id);
479
480                         if (row) {
481                                 const cb = dijit.getEnclosingWidget(
482                                         row.getElementsByClassName("rchk")[0]);
483
484                                 if (cb) {
485                                         cb.attr("checked", !cb.attr("checked"));
486                                         toggleSelectRowById(cb, "RROW-" + id);
487                                         return false;
488                                 }
489                         }
490                 }
491         };
492         hotkey_actions["create_label"] = function() {
493                 addLabel();
494         };
495         hotkey_actions["create_filter"] = function() {
496                 quickAddFilter();
497         };
498         hotkey_actions["collapse_sidebar"] = function() {
499                 collapse_feedlist();
500         };
501         hotkey_actions["toggle_embed_original"] = function() {
502                 if (typeof embedOriginalArticle != "undefined") {
503                         if (getActiveArticleId())
504                                 embedOriginalArticle(getActiveArticleId());
505                 } else {
506                         alert(__("Please enable embed_original plugin first."));
507                 }
508         };
509         hotkey_actions["toggle_widescreen"] = function() {
510                 if (!isCdmMode()) {
511                         _widescreen_mode = !_widescreen_mode;
512
513                         // reset stored sizes because geometry changed
514                         setCookie("ttrss_ci_width", 0);
515                         setCookie("ttrss_ci_height", 0);
516
517                         switchPanelMode(_widescreen_mode);
518                 } else {
519                         alert(__("Widescreen is not available in combined mode."));
520                 }
521         };
522         hotkey_actions["help_dialog"] = function() {
523                 helpDialog("main");
524         };
525         hotkey_actions["toggle_combined_mode"] = function() {
526                 notify_progress("Loading, please wait...");
527
528                 const value = isCdmMode() ? "false" : "true";
529
530                 xhrPost("backend.php", {op: "rpc", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
531             setInitParam("combined_display_mode",
532                 !getInitParam("combined_display_mode"));
533
534             closeArticlePanel();
535             viewCurrentFeed();
536                 })
537         };
538         hotkey_actions["toggle_cdm_expanded"] = function() {
539                 notify_progress("Loading, please wait...");
540
541         const value = getInitParam("cdm_expanded") ? "false" : "true";
542
543                 xhrPost("backend.php", {op: "rpc", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
544             setInitParam("cdm_expanded", !getInitParam("cdm_expanded"));
545             viewCurrentFeed();
546         });
547         };
548 }
549
550 function init_second_stage() {
551         updateFeedList();
552         closeArticlePanel();
553
554         if (parseInt(getCookie("ttrss_fh_width")) > 0) {
555                 dijit.byId("feeds-holder").domNode.setStyle(
556                         {width: getCookie("ttrss_fh_width") + "px" });
557         }
558
559         dijit.byId("main").resize();
560
561         var tmph = dojo.connect(dijit.byId('feeds-holder'), 'resize',
562                 function (args) {
563                         if (args && args.w >= 0) {
564                                 setCookie("ttrss_fh_width", args.w, getInitParam("cookie_lifetime"));
565                         }
566         });
567
568         var tmph = dojo.connect(dijit.byId('content-insert'), 'resize',
569                 function (args) {
570                         if (args && args.w >= 0 && args.h >= 0) {
571                                 setCookie("ttrss_ci_width", args.w, getInitParam("cookie_lifetime"));
572                                 setCookie("ttrss_ci_height", args.h, getInitParam("cookie_lifetime"));
573                         }
574         });
575
576         delCookie("ttrss_test");
577
578         const toolbar = document.forms["main_toolbar_form"];
579
580         dijit.getEnclosingWidget(toolbar.view_mode).attr('value',
581                 getInitParam("default_view_mode"));
582
583         dijit.getEnclosingWidget(toolbar.order_by).attr('value',
584                 getInitParam("default_view_order_by"));
585
586         const hash_feed_id = hash_get('f');
587         const hash_feed_is_cat = hash_get('c') == "1";
588
589         if (hash_feed_id != undefined) {
590                 setActiveFeedId(hash_feed_id, hash_feed_is_cat);
591         }
592
593         loading_set_progress(50);
594
595         // can't use cache_clear() here because viewfeed might not have initialized yet
596         if ('sessionStorage' in window && window['sessionStorage'] !== null)
597                 sessionStorage.clear();
598
599         const hotkeys = getInitParam("hotkeys");
600         const tmp = [];
601
602         for (const sequence in hotkeys[1]) {
603                 const filtered = sequence.replace(/\|.*$/, "");
604                 tmp[filtered] = hotkeys[1][sequence];
605         }
606
607         hotkeys[1] = tmp;
608         setInitParam("hotkeys", hotkeys);
609
610         _widescreen_mode = getInitParam("widescreen");
611         switchPanelMode(_widescreen_mode);
612
613         console.log("second stage ok");
614
615         if (getInitParam("simple_update")) {
616                 console.log("scheduling simple feed updater...");
617                 window.setTimeout(update_random_feed, 30*1000);
618         }
619 }
620
621 function quickMenuGo(opid) {
622         switch (opid) {
623         case "qmcPrefs":
624                 gotoPreferences();
625                 break;
626         case "qmcLogout":
627                 gotoLogout();
628                 break;
629         case "qmcTagCloud":
630                 displayDlg(__("Tag cloud"), "printTagCloud");
631                 break;
632         case "qmcSearch":
633                 search();
634                 break;
635         case "qmcAddFeed":
636                 quickAddFeed();
637                 break;
638         case "qmcDigest":
639                 window.location.href = "backend.php?op=digest";
640                 break;
641         case "qmcEditFeed":
642                 if (activeFeedIsCat())
643                         alert(__("You can't edit this kind of feed."));
644                 else
645                         editFeed(getActiveFeedId());
646                 break;
647         case "qmcRemoveFeed":
648                 var actid = getActiveFeedId();
649
650                 if (activeFeedIsCat()) {
651                         alert(__("You can't unsubscribe from the category."));
652                         return;
653                 }
654
655                 if (!actid) {
656                         alert(__("Please select some feed first."));
657                         return;
658                 }
659
660                 var fn = getFeedName(actid);
661
662                 var pr = __("Unsubscribe from %s?").replace("%s", fn);
663
664                 if (confirm(pr)) {
665                         unsubscribeFeed(actid);
666                 }
667                 break;
668         case "qmcCatchupAll":
669                 catchupAllFeeds();
670                 break;
671         case "qmcShowOnlyUnread":
672                 toggleDispRead();
673                 break;
674         case "qmcToggleWidescreen":
675                 if (!isCdmMode()) {
676                         _widescreen_mode = !_widescreen_mode;
677
678                         // reset stored sizes because geometry changed
679                         setCookie("ttrss_ci_width", 0);
680                         setCookie("ttrss_ci_height", 0);
681
682                         switchPanelMode(_widescreen_mode);
683                 } else {
684                         alert(__("Widescreen is not available in combined mode."));
685                 }
686                 break;
687         case "qmcHKhelp":
688                 helpDialog("main");
689                 break;
690         default:
691                 console.log("quickMenuGo: unknown action: " + opid);
692         }
693 }
694
695 function toggleDispRead() {
696
697         const hide = !(getInitParam("hide_read_feeds") == "1");
698
699         xhrPost("backend.php", {op: "rpc", method: "setpref", key: "HIDE_READ_FEEDS", value: hide}, () => {
700         hideOrShowFeeds(hide);
701         setInitParam("hide_read_feeds", hide);
702         });
703 }
704
705 function parse_runtime_info(data) {
706
707         //console.log("parsing runtime info...");
708
709         for (const k in data) {
710                 const v = data[k];
711
712 //              console.log("RI: " + k + " => " + v);
713
714                 if (k == "dep_ts" && parseInt(getInitParam("dep_ts")) > 0) {
715                         if (parseInt(getInitParam("dep_ts")) < parseInt(v) && getInitParam("reload_on_ts_change")) {
716                                 window.location.reload();
717                         }
718                 }
719
720                 if (k == "daemon_is_running" && v != 1) {
721                         notify_error("<span onclick=\"explainError(1)\">Update daemon is not running.</span>", true);
722                         return;
723                 }
724
725                 if (k == "update_result") {
726                         const updatesIcon = dijit.byId("updatesIcon").domNode;
727
728                         if (v) {
729                                 Element.show(updatesIcon);
730                         } else {
731                                 Element.hide(updatesIcon);
732                         }
733                 }
734
735                 if (k == "daemon_stamp_ok" && v != 1) {
736                         notify_error("<span onclick=\"explainError(3)\">Update daemon is not updating feeds.</span>", true);
737                         return;
738                 }
739
740                 if (k == "max_feed_id" || k == "num_feeds") {
741                         if (init_params[k] != v) {
742                                 console.log("feed count changed, need to reload feedlist.");
743                                 updateFeedList();
744                         }
745                 }
746
747                 init_params[k] = v;
748                 notify('');
749         }
750
751         PluginHost.run(PluginHost.HOOK_RUNTIME_INFO_LOADED, data);
752 }
753
754 function collapse_feedlist() {
755         Element.toggle("feeds-holder");
756
757         const splitter = $("feeds-holder_splitter");
758
759         Element.visible("feeds-holder") ? splitter.show() : splitter.hide();
760
761         dijit.byId("main").resize();
762 }
763
764 function viewModeChanged() {
765         cache_clear();
766         return viewCurrentFeed('');
767 }
768
769 function hotkey_handler(e) {
770
771         if (e.target.nodeName == "INPUT" || e.target.nodeName == "TEXTAREA") return;
772
773         let keycode = e.which;
774
775         if (keycode == 27) { // escape and drop prefix
776                 hotkey_prefix = false;
777         }
778
779         if (keycode == 16 || keycode == 17) return; // ignore lone shift / ctrl
780
781         const hotkeys = getInitParam("hotkeys");
782         const keychar = String.fromCharCode(keycode).toLowerCase();
783
784         if (!hotkey_prefix && hotkeys[0].indexOf(keychar) != -1) {
785
786                 const date = new Date();
787                 const ts = Math.round(date.getTime() / 1000);
788
789                 hotkey_prefix = keychar;
790                 hotkey_prefix_pressed = ts;
791
792                 $("cmdline").innerHTML = keychar;
793                 Element.show("cmdline");
794
795                 e.stopPropagation();
796
797                 // returning false here literally disables ctrl-c in browser lol (because C is a valid prefix)
798                 return true;
799         }
800
801         Element.hide("cmdline");
802
803         let hotkey = keychar.search(/[a-zA-Z0-9]/) != -1 ? keychar : "(" + keycode + ")";
804
805         // ensure ^*char notation
806         if (e.shiftKey) hotkey = "*" + hotkey;
807         if (e.ctrlKey) hotkey = "^" + hotkey;
808         if (e.altKey) hotkey = "+" + hotkey;
809         if (e.metaKey) hotkey = "%" + hotkey;
810
811         hotkey = hotkey_prefix ? hotkey_prefix + " " + hotkey : hotkey;
812         hotkey_prefix = false;
813
814         let hotkey_action = false;
815
816         for (const sequence in hotkeys[1]) {
817                 if (sequence == hotkey) {
818                         hotkey_action = hotkeys[1][sequence];
819                         break;
820                 }
821         }
822
823         const action = hotkey_actions[hotkey_action];
824
825         if (action != null) {
826                 action();
827                 e.stopPropagation();
828                 return false;
829         }
830 }
831
832 function inPreferences() {
833         return false;
834 }
835
836 function reverseHeadlineOrder() {
837
838         const toolbar = document.forms["main_toolbar_form"];
839         const order_by = dijit.getEnclosingWidget(toolbar.order_by);
840
841         let value = order_by.attr('value');
842
843         if (value == "date_reverse")
844                 value = "default";
845         else
846                 value = "date_reverse";
847
848         order_by.attr('value', value);
849
850         viewCurrentFeed();
851
852 }
853
854 function handle_rpc_json(transport, scheduled_call) {
855
856         const netalert_dijit = dijit.byId("net-alert");
857         let netalert = false;
858
859         if (netalert_dijit) netalert = netalert_dijit.domNode;
860
861         try {
862                 const reply = JSON.parse(transport.responseText);
863
864                 if (reply) {
865
866                         const error = reply['error'];
867
868                         if (error) {
869                                 const code = error['code'];
870                                 const msg = error['msg'];
871
872                                 console.warn("[handle_rpc_json] received fatal error " + code + "/" + msg);
873
874                                 if (code != 0) {
875                                         fatalError(code, msg);
876                                         return false;
877                                 }
878                         }
879
880                         const seq = reply['seq'];
881
882                         if (seq && get_seq() != seq) {
883                                 console.log("[handle_rpc_json] sequence mismatch: " + seq +
884                                         " (want: " + get_seq() + ")");
885                                 return true;
886                         }
887
888                         const message = reply['message'];
889
890                         if (message == "UPDATE_COUNTERS") {
891                                 console.log("need to refresh counters...");
892                                 setInitParam("last_article_id", -1);
893                                 request_counters(true);
894                         }
895
896                         const counters = reply['counters'];
897
898                         if (counters)
899                                 parse_counters(counters, scheduled_call);
900
901                         const runtime_info = reply['runtime-info'];
902
903                         if (runtime_info)
904                                 parse_runtime_info(runtime_info);
905
906                         if (netalert) netalert.hide();
907
908                         return reply;
909
910                 } else {
911             if (netalert)
912                 netalert.show();
913             else
914                 notify_error("Communication problem with server.");
915         }
916
917         } catch (e) {
918                 if (netalert)
919                         netalert.show();
920                 else
921                         notify_error("Communication problem with server.");
922
923                 console.error(e);
924         }
925
926         return false;
927 }
928
929 function switchPanelMode(wide) {
930         if (isCdmMode()) return;
931
932         const article_id = getActiveArticleId();
933
934         if (wide) {
935                 dijit.byId("headlines-wrap-inner").attr("design", 'sidebar');
936                 dijit.byId("content-insert").attr("region", "trailing");
937
938                 dijit.byId("content-insert").domNode.setStyle({width: '50%',
939                         height: 'auto',
940                         borderTopWidth: '0px' });
941
942                 if (parseInt(getCookie("ttrss_ci_width")) > 0) {
943                         dijit.byId("content-insert").domNode.setStyle(
944                                 {width: getCookie("ttrss_ci_width") + "px" });
945                 }
946
947                 $("headlines-frame").setStyle({ borderBottomWidth: '0px' });
948                 $("headlines-frame").addClassName("wide");
949
950         } else {
951
952                 dijit.byId("content-insert").attr("region", "bottom");
953
954                 dijit.byId("content-insert").domNode.setStyle({width: 'auto',
955                         height: '50%',
956                         borderTopWidth: '0px'});
957
958                 if (parseInt(getCookie("ttrss_ci_height")) > 0) {
959                         dijit.byId("content-insert").domNode.setStyle(
960                                 {height: getCookie("ttrss_ci_height") + "px" });
961                 }
962
963                 $("headlines-frame").setStyle({ borderBottomWidth: '1px' });
964                 $("headlines-frame").removeClassName("wide");
965
966         }
967
968         closeArticlePanel();
969
970         if (article_id) view(article_id);
971
972         xhrPost("backend.php", {op: "rpc", method: "setpanelmode", wide: wide ? 1 : 0});
973 }
974
975 function update_random_feed() {
976         console.log("in update_random_feed");
977
978         xhrPost("backend.php", { op: "rpc", method: "updateRandomFeed" }, (transport) => {
979                 handle_rpc_json(transport, true);
980                 window.setTimeout(update_random_feed, 30*1000);
981         });
982 }
983
984 function hash_get(key) {
985         const kv = window.location.hash.substring(1).toQueryParams();
986         return kv[key];
987 }
988 function hash_set(key, value) {
989         const kv = window.location.hash.substring(1).toQueryParams();
990         kv[key] = value;
991         window.location.hash = $H(kv).toQueryString();
992 }