]> git.wh0rd.org Git - tt-rss.git/blob - js/prefs.js
e67d80df45bc5166abf7de4b3f581642378350b4
[tt-rss.git] / js / prefs.js
1 var init_params = new Array();
2
3 var hotkey_prefix = false;
4 var hotkey_prefix_pressed = false;
5
6 var seq = "";
7
8 function notify_callback2(transport, sticky) {
9         notify_info(transport.responseText, sticky);
10 }
11
12 function updateFeedList() {
13
14         const user_search = $("feed_search");
15         let search = "";
16         if (user_search) { search = user_search.value; }
17
18         new Ajax.Request("backend.php", {
19                 parameters: "?op=pref-feeds&search=" + param_escape(search),
20                 onComplete: function(transport) {
21                         dijit.byId('feedConfigTab').attr('content', transport.responseText);
22                         selectTab("feedConfig", true);
23                         notify("");
24                 } });
25 }
26
27 function checkInactiveFeeds() {
28         new Ajax.Request("backend.php", {
29                 parameters: "?op=pref-feeds&method=getinactivefeeds",
30                 onComplete: function (transport) {
31                         if (parseInt(transport.responseText) > 0) {
32                                 Element.show(dijit.byId("pref_feeds_inactive_btn").domNode);
33                         }
34                 }
35         });
36 }
37
38 function updateUsersList(sort_key) {
39         const user_search = $("user_search");
40         let search = "";
41         if (user_search) {
42                 search = user_search.value;
43         }
44
45         const query = "?op=pref-users&sort=" +
46                 param_escape(sort_key) +
47                 "&search=" + param_escape(search);
48
49         new Ajax.Request("backend.php", {
50                 parameters: query,
51                 onComplete: function (transport) {
52                         dijit.byId('userConfigTab').attr('content', transport.responseText);
53                         selectTab("userConfig", true)
54                         notify("");
55                 }
56         });
57 }
58
59 function addUser() {
60         const login = prompt(__("Please enter login:"), "");
61
62         if (login == null) {
63                 return false;
64         }
65
66         if (login == "") {
67                 alert(__("Can't create user: no login specified."));
68                 return false;
69         }
70
71         notify_progress("Adding user...");
72
73         const query = "?op=pref-users&method=add&login=" +
74                 param_escape(login);
75
76         new Ajax.Request("backend.php", {
77                 parameters: query,
78                 onComplete: function (transport) {
79                         notify_callback2(transport);
80                         updateUsersList();
81                 }
82         });
83
84 }
85
86 function editUser(id) {
87
88         const query = "backend.php?op=pref-users&method=edit&id=" +
89                 param_escape(id);
90
91         if (dijit.byId("userEditDlg"))
92                 dijit.byId("userEditDlg").destroyRecursive();
93
94         var dialog = new dijit.Dialog({
95                 id: "userEditDlg",
96                 title: __("User Editor"),
97                 style: "width: 600px",
98                 execute: function () {
99                         if (this.validate()) {
100                                 notify_progress("Saving data...", true);
101
102                                 const query = dojo.formToQuery("user_edit_form");
103
104                                 new Ajax.Request("backend.php", {
105                                         parameters: query,
106                                         onComplete: function (transport) {
107                                                 dialog.hide();
108                                                 updateUsersList();
109                                         }
110                                 });
111                         }
112                 },
113                 href: query
114         });
115
116         dialog.show();
117 }
118
119 function editFilter(id) {
120
121         const query = "backend.php?op=pref-filters&method=edit&id=" + param_escape(id);
122
123         if (dijit.byId("feedEditDlg"))
124                 dijit.byId("feedEditDlg").destroyRecursive();
125
126         if (dijit.byId("filterEditDlg"))
127                 dijit.byId("filterEditDlg").destroyRecursive();
128
129         var dialog = new dijit.Dialog({
130                 id: "filterEditDlg",
131                 title: __("Edit Filter"),
132                 style: "width: 600px",
133
134                 test: function () {
135                         const query = "backend.php?" + dojo.formToQuery("filter_edit_form") + "&savemode=test";
136
137                         editFilterTest(query);
138                 },
139                 selectRules: function (select) {
140                         $$("#filterDlg_Matches input[type=checkbox]").each(function (e) {
141                                 e.checked = select;
142                                 if (select)
143                                         e.parentNode.addClassName("Selected");
144                                 else
145                                         e.parentNode.removeClassName("Selected");
146                         });
147                 },
148                 selectActions: function (select) {
149                         $$("#filterDlg_Actions input[type=checkbox]").each(function (e) {
150                                 e.checked = select;
151
152                                 if (select)
153                                         e.parentNode.addClassName("Selected");
154                                 else
155                                         e.parentNode.removeClassName("Selected");
156
157                         });
158                 },
159                 editRule: function (e) {
160                         const li = e.parentNode;
161                         const rule = li.getElementsByTagName("INPUT")[1].value;
162                         addFilterRule(li, rule);
163                 },
164                 editAction: function (e) {
165                         const li = e.parentNode;
166                         const action = li.getElementsByTagName("INPUT")[1].value;
167                         addFilterAction(li, action);
168                 },
169                 removeFilter: function () {
170                         const msg = __("Remove filter?");
171
172                         if (confirm(msg)) {
173                                 this.hide();
174
175                                 notify_progress("Removing filter...");
176
177                                 const id = this.attr('value').id;
178
179                                 const query = "?op=pref-filters&method=remove&ids=" +
180                                         param_escape(id);
181
182                                 new Ajax.Request("backend.php", {
183                                         parameters: query,
184                                         onComplete: function (transport) {
185                                                 updateFilterList();
186                                         }
187                                 });
188                         }
189                 },
190                 addAction: function () {
191                         addFilterAction();
192                 },
193                 addRule: function () {
194                         addFilterRule();
195                 },
196                 deleteAction: function () {
197                         $$("#filterDlg_Actions li[class*=Selected]").each(function (e) {
198                                 e.parentNode.removeChild(e)
199                         });
200                 },
201                 deleteRule: function () {
202                         $$("#filterDlg_Matches li[class*=Selected]").each(function (e) {
203                                 e.parentNode.removeChild(e)
204                         });
205                 },
206                 execute: function () {
207                         if (this.validate()) {
208
209                                 notify_progress("Saving data...", true);
210
211                                 const query = dojo.formToQuery("filter_edit_form");
212
213                                 console.log(query);
214
215                                 new Ajax.Request("backend.php", {
216                                         parameters: query,
217                                         onComplete: function (transport) {
218                                                 dialog.hide();
219                                                 updateFilterList();
220                                         }
221                                 });
222                         }
223                 },
224                 href: query
225         });
226
227         dialog.show();
228 }
229
230
231 function getSelectedLabels() {
232         const tree = dijit.byId("labelTree");
233         const items = tree.model.getCheckedItems();
234         const rv = [];
235
236         items.each(function(item) {
237                 rv.push(tree.model.store.getValue(item, 'bare_id'));
238         });
239
240         return rv;
241 }
242
243 function getSelectedUsers() {
244         return getSelectedTableRowIds("prefUserList");
245 }
246
247 function getSelectedFeeds() {
248         const tree = dijit.byId("feedTree");
249         const items = tree.model.getCheckedItems();
250         const rv = [];
251
252         items.each(function(item) {
253                 if (item.id[0].match("FEED:"))
254                         rv.push(tree.model.store.getValue(item, 'bare_id'));
255         });
256
257         return rv;
258 }
259
260 function getSelectedCategories() {
261         const tree = dijit.byId("feedTree");
262         const items = tree.model.getCheckedItems();
263         const rv = [];
264
265         items.each(function(item) {
266                 if (item.id[0].match("CAT:"))
267                         rv.push(tree.model.store.getValue(item, 'bare_id'));
268         });
269
270         return rv;
271 }
272
273 function getSelectedFilters() {
274         const tree = dijit.byId("filterTree");
275         const items = tree.model.getCheckedItems();
276         const rv = [];
277
278         items.each(function(item) {
279                 rv.push(tree.model.store.getValue(item, 'bare_id'));
280         });
281
282         return rv;
283
284 }
285
286 function removeSelectedLabels() {
287
288         const sel_rows = getSelectedLabels();
289
290         if (sel_rows.length > 0) {
291
292                 const ok = confirm(__("Remove selected labels?"));
293
294                 if (ok) {
295                         notify_progress("Removing selected labels...");
296
297                         const query = "?op=pref-labels&method=remove&ids="+
298                                 param_escape(sel_rows.toString());
299
300                         new Ajax.Request("backend.php", {
301                                 parameters: query,
302                                 onComplete: function(transport) {
303                                                 updateLabelList();
304                                         } });
305
306                 }
307         } else {
308                 alert(__("No labels are selected."));
309         }
310
311         return false;
312 }
313
314 function removeSelectedUsers() {
315
316         const sel_rows = getSelectedUsers();
317
318         if (sel_rows.length > 0) {
319
320                 const ok = confirm(__("Remove selected users? Neither default admin nor your account will be removed."));
321
322                 if (ok) {
323                         notify_progress("Removing selected users...");
324
325                         const query = "?op=pref-users&method=remove&ids=" +
326                                 param_escape(sel_rows.toString());
327
328                         new Ajax.Request("backend.php", {
329                                 parameters: query,
330                                 onComplete: function (transport) {
331                                         updateUsersList();
332                                 }
333                         });
334
335                 }
336
337         } else {
338                 alert(__("No users are selected."));
339         }
340
341         return false;
342 }
343
344 function removeSelectedFilters() {
345
346         const sel_rows = getSelectedFilters();
347
348         if (sel_rows.length > 0) {
349
350                 const ok = confirm(__("Remove selected filters?"));
351
352                 if (ok) {
353                         notify_progress("Removing selected filters...");
354
355                         const query = "?op=pref-filters&method=remove&ids=" +
356                                 param_escape(sel_rows.toString());
357
358                         new Ajax.Request("backend.php", {
359                                 parameters: query,
360                                 onComplete: function (transport) {
361                                         updateFilterList();
362                                 }
363                         });
364                 }
365         } else {
366                 alert(__("No filters are selected."));
367         }
368
369         return false;
370 }
371
372 function removeSelectedFeeds() {
373
374         const sel_rows = getSelectedFeeds();
375
376         if (sel_rows.length > 0) {
377
378                 const ok = confirm(__("Unsubscribe from selected feeds?"));
379
380                 if (ok) {
381
382                         notify_progress("Unsubscribing from selected feeds...", true);
383
384                         const query = "?op=pref-feeds&method=remove&ids=" +
385                                 param_escape(sel_rows.toString());
386
387                         console.log(query);
388
389                         new Ajax.Request("backend.php", {
390                                 parameters: query,
391                                 onComplete: function (transport) {
392                                         updateFeedList();
393                                 }
394                         });
395                 }
396
397         } else {
398                 alert(__("No feeds are selected."));
399         }
400
401         return false;
402 }
403
404 function editSelectedUser() {
405         const rows = getSelectedUsers();
406
407         if (rows.length == 0) {
408                 alert(__("No users are selected."));
409                 return;
410         }
411
412         if (rows.length > 1) {
413                 alert(__("Please select only one user."));
414                 return;
415         }
416
417         notify("");
418
419         editUser(rows[0]);
420 }
421
422 function resetSelectedUserPass() {
423
424         const rows = getSelectedUsers();
425
426         if (rows.length == 0) {
427                 alert(__("No users are selected."));
428                 return;
429         }
430
431         if (rows.length > 1) {
432                 alert(__("Please select only one user."));
433                 return;
434         }
435
436         const ok = confirm(__("Reset password of selected user?"));
437
438         if (ok) {
439                 notify_progress("Resetting password for selected user...");
440
441                 const id = rows[0];
442
443                 const query = "?op=pref-users&method=resetPass&id=" +
444                         param_escape(id);
445
446                 new Ajax.Request("backend.php", {
447                         parameters: query,
448                         onComplete: function (transport) {
449                                 notify_info(transport.responseText, true);
450                         }
451                 });
452
453         }
454 }
455
456 function selectedUserDetails() {
457
458         const rows = getSelectedUsers();
459
460         if (rows.length == 0) {
461                 alert(__("No users are selected."));
462                 return;
463         }
464
465         if (rows.length > 1) {
466                 alert(__("Please select only one user."));
467                 return;
468         }
469
470         const id = rows[0];
471
472         const query = "backend.php?op=pref-users&method=userdetails&id=" + id;
473
474         if (dijit.byId("userDetailsDlg"))
475                 dijit.byId("userDetailsDlg").destroyRecursive();
476
477         var dialog = new dijit.Dialog({
478                 id: "userDetailsDlg",
479                 title: __("User details"),
480                 style: "width: 600px",
481                 execute: function () {
482                         dialog.hide();
483                 },
484                 href: query
485         });
486
487         dialog.show();
488 }
489
490
491 function editSelectedFilter() {
492         const rows = getSelectedFilters();
493
494         if (rows.length == 0) {
495                 alert(__("No filters are selected."));
496                 return;
497         }
498
499         if (rows.length > 1) {
500                 alert(__("Please select only one filter."));
501                 return;
502         }
503
504         notify("");
505
506         editFilter(rows[0]);
507
508 }
509
510 function joinSelectedFilters() {
511         const rows = getSelectedFilters();
512
513         if (rows.length == 0) {
514                 alert(__("No filters are selected."));
515                 return;
516         }
517
518         const ok = confirm(__("Combine selected filters?"));
519
520         if (ok) {
521                 notify_progress("Joining filters...");
522
523                 const query = "?op=pref-filters&method=join&ids="+
524                         param_escape(rows.toString());
525
526                 console.log(query);
527
528                 new Ajax.Request("backend.php", {
529                         parameters: query,
530                         onComplete: function(transport) {
531                                         updateFilterList();
532                         } });
533         }
534 }
535
536 function editSelectedFeed() {
537         const rows = getSelectedFeeds();
538
539         if (rows.length == 0) {
540                 alert(__("No feeds are selected."));
541                 return;
542         }
543
544         if (rows.length > 1) {
545                 return editSelectedFeeds();
546         }
547
548         notify("");
549
550         editFeed(rows[0], {});
551
552 }
553
554 function editSelectedFeeds() {
555         const rows = getSelectedFeeds();
556
557         if (rows.length == 0) {
558                 alert(__("No feeds are selected."));
559                 return;
560         }
561
562         notify_progress("Loading, please wait...");
563
564         const query = "backend.php?op=pref-feeds&method=editfeeds&ids=" +
565                 param_escape(rows.toString());
566
567         console.log(query);
568
569         if (dijit.byId("feedEditDlg"))
570                 dijit.byId("feedEditDlg").destroyRecursive();
571
572         new Ajax.Request("backend.php", {
573                 parameters: query,
574                 onComplete: function (transport) {
575
576                         notify("");
577
578                         var dialog = new dijit.Dialog({
579                                 id: "feedEditDlg",
580                                 title: __("Edit Multiple Feeds"),
581                                 style: "width: 600px",
582                                 getChildByName: function (name) {
583                                         let rv = null;
584                                         this.getChildren().each(
585                                                 function (child) {
586                                                         if (child.name == name) {
587                                                                 rv = child;
588                                                                 return;
589                                                         }
590                                                 });
591                                         return rv;
592                                 },
593                                 toggleField: function (checkbox, elem, label) {
594                                         this.getChildByName(elem).attr('disabled', !checkbox.checked);
595
596                                         if ($(label))
597                                                 if (checkbox.checked)
598                                                         $(label).removeClassName('insensitive');
599                                                 else
600                                                         $(label).addClassName('insensitive');
601
602                                 },
603                                 execute: function () {
604                                         if (this.validate() && confirm(__("Save changes to selected feeds?"))) {
605                                                 let query = dojo.objectToQuery(this.attr('value'));
606
607                                                 /* Form.serialize ignores unchecked checkboxes */
608
609                                                 if (!query.match("&private=") &&
610                                                         this.getChildByName('private').attr('disabled') == false) {
611                                                         query = query + "&private=false";
612                                                 }
613
614                                                 try {
615                                                         if (!query.match("&cache_images=") &&
616                                                                 this.getChildByName('cache_images').attr('disabled') == false) {
617                                                                 query = query + "&cache_images=false";
618                                                         }
619                                                 } catch (e) {
620                                                 }
621
622                                                 try {
623                                                         if (!query.match("&hide_images=") &&
624                                                                 this.getChildByName('hide_images').attr('disabled') == false) {
625                                                                 query = query + "&hide_images=false";
626                                                         }
627                                                 } catch (e) {
628                                                 }
629
630                                                 if (!query.match("&include_in_digest=") &&
631                                                         this.getChildByName('include_in_digest').attr('disabled') == false) {
632                                                         query = query + "&include_in_digest=false";
633                                                 }
634
635                                                 if (!query.match("&always_display_enclosures=") &&
636                                                         this.getChildByName('always_display_enclosures').attr('disabled') == false) {
637                                                         query = query + "&always_display_enclosures=false";
638                                                 }
639
640                                                 if (!query.match("&mark_unread_on_update=") &&
641                                                         this.getChildByName('mark_unread_on_update').attr('disabled') == false) {
642                                                         query = query + "&mark_unread_on_update=false";
643                                                 }
644
645                                                 console.log(query);
646
647                                                 notify_progress("Saving data...", true);
648
649                                                 new Ajax.Request("backend.php", {
650                                                         parameters: query,
651                                                         onComplete: function (transport) {
652                                                                 dialog.hide();
653                                                                 updateFeedList();
654                                                         }
655                                                 });
656                                         }
657                                 },
658                                 content: transport.responseText
659                         });
660
661                         dialog.show();
662
663                 }
664         });
665 }
666
667 function opmlImportComplete(iframe) {
668         if (!iframe.contentDocument.body.innerHTML) return false;
669
670         Element.show(iframe);
671
672         notify('');
673
674         if (dijit.byId('opmlImportDlg'))
675                 dijit.byId('opmlImportDlg').destroyRecursive();
676
677         const content = iframe.contentDocument.body.innerHTML;
678
679         const dialog = new dijit.Dialog({
680                 id: "opmlImportDlg",
681                 title: __("OPML Import"),
682                 style: "width: 600px",
683                 onCancel: function () {
684             window.location.reload();
685                 },
686                 execute: function () {
687                         window.location.reload();
688                 },
689                 content: content
690         });
691
692         dialog.show();
693 }
694
695 function opmlImport() {
696
697         const opml_file = $("opml_file");
698
699         if (opml_file.value.length == 0) {
700                 alert(__("Please choose an OPML file first."));
701                 return false;
702         } else {
703                 notify_progress("Importing, please wait...", true);
704
705                 Element.show("upload_iframe");
706
707                 return true;
708         }
709 }
710
711
712 function updateFilterList() {
713         const user_search = $("filter_search");
714         let search = "";
715         if (user_search) { search = user_search.value; }
716
717         new Ajax.Request("backend.php", {
718                 parameters: "?op=pref-filters&search=" + param_escape(search),
719                 onComplete: function(transport) {
720                         dijit.byId('filterConfigTab').attr('content', transport.responseText);
721                         notify("");
722                 } });
723 }
724
725 function updateLabelList() {
726         new Ajax.Request("backend.php", {
727                 parameters: "?op=pref-labels",
728                 onComplete: function(transport) {
729                         dijit.byId('labelConfigTab').attr('content', transport.responseText);
730                         notify("");
731                 } });
732 }
733
734 function updatePrefsList() {
735         new Ajax.Request("backend.php", {
736                 parameters: "?op=pref-prefs",
737                 onComplete: function(transport) {
738                         dijit.byId('genConfigTab').attr('content', transport.responseText);
739                         notify("");
740                 } });
741 }
742
743 function updateSystemList() {
744         new Ajax.Request("backend.php", {
745                 parameters: "?op=pref-system",
746                 onComplete: function(transport) {
747                         dijit.byId('systemConfigTab').attr('content', transport.responseText);
748                         notify("");
749                 } });
750 }
751
752 function selectTab(id, noupdate) {
753         if (!noupdate) {
754                 notify_progress("Loading, please wait...");
755
756                 if (id == "feedConfig") {
757                         updateFeedList();
758                 } else if (id == "filterConfig") {
759                         updateFilterList();
760                 } else if (id == "labelConfig") {
761                         updateLabelList();
762                 } else if (id == "genConfig") {
763                         updatePrefsList();
764                 } else if (id == "userConfig") {
765                         updateUsersList();
766                 } else if (id == "systemConfig") {
767                         updateSystemList();
768                 }
769
770                 const tab = dijit.byId(id + "Tab");
771                 dijit.byId("pref-tabs").selectChild(tab);
772
773         }
774 }
775
776 function init_second_stage() {
777         document.onkeydown = pref_hotkey_handler;
778         loading_set_progress(50);
779         notify("");
780
781         let tab = getURLParam('tab');
782
783         if (tab) {
784                 tab = dijit.byId(tab + "Tab");
785                 if (tab) dijit.byId("pref-tabs").selectChild(tab);
786         }
787
788         const method = getURLParam('method');
789
790         if (method == 'editFeed') {
791                 const param = getURLParam('methodparam');
792
793                 window.setTimeout(function() { editFeed(param) }, 100);
794         }
795
796         setTimeout(hotkey_prefix_timeout, 5*1000);
797 }
798
799 function init() {
800         window.onerror = function (message, filename, lineno, colno, error) {
801                 report_error(message, filename, lineno, colno, error);
802         };
803
804         require(["dojo/_base/kernel",
805                 "dojo/ready",
806                 "dojo/parser",
807                 "dojo/_base/loader",
808                 "dojo/_base/html",
809                 "dijit/ColorPalette",
810                 "dijit/Dialog",
811                 "dijit/form/Button",
812                 "dijit/form/CheckBox",
813                 "dijit/form/DropDownButton",
814                 "dijit/form/FilteringSelect",
815         "dijit/form/MultiSelect",
816                 "dijit/form/Form",
817                 "dijit/form/RadioButton",
818                 "dijit/form/ComboButton",
819                 "dijit/form/Select",
820                 "dijit/form/SimpleTextarea",
821                 "dijit/form/TextBox",
822                 "dijit/form/ValidationTextBox",
823                 "dijit/InlineEditBox",
824                 "dijit/layout/AccordionContainer",
825                 "dijit/layout/AccordionPane",
826                 "dijit/layout/BorderContainer",
827                 "dijit/layout/ContentPane",
828                 "dijit/layout/TabContainer",
829                 "dijit/Menu",
830                 "dijit/ProgressBar",
831                 "dijit/Toolbar",
832                 "dijit/Tree",
833                 "dijit/tree/dndSource",
834                 "dojo/data/ItemFileWriteStore",
835                 "lib/CheckBoxStoreModel",
836                 "lib/CheckBoxTree",
837                 "fox/PrefFeedStore",
838                 "fox/PrefFilterStore",
839                 "fox/PrefFeedTree",
840                 "fox/PrefFilterTree",
841                 "fox/PrefLabelTree"], function (dojo, ready, parser) {
842
843                 ready(function () {
844                         try {
845                                 parser.parse();
846
847                                 loading_set_progress(50);
848
849                                 const clientTzOffset = new Date().getTimezoneOffset() * 60;
850
851                                 new Ajax.Request("backend.php", {
852                                         parameters: {
853                                                 op: "rpc", method: "sanityCheck",
854                                                 clientTzOffset: clientTzOffset
855                                         },
856                                         onComplete: function (transport) {
857                                                 backend_sanity_check_callback(transport);
858                                         }
859                                 });
860                         } catch (e) {
861                                 exception_error(e);
862                         }
863                 });
864         });
865 }
866
867
868 function validatePrefsReset() {
869         const ok = confirm(__("Reset to defaults?"));
870
871         if (ok) {
872
873                 const query = "?op=pref-prefs&method=resetconfig";
874                 console.log(query);
875
876                 new Ajax.Request("backend.php", {
877                         parameters: query,
878                         onComplete: function(transport) {
879                                 updatePrefsList();
880                                 notify_info(transport.responseText);
881                         } });
882
883         }
884
885         return false;
886
887 }
888
889 function pref_hotkey_handler(e) {
890
891         if (e.target.nodeName == "INPUT" || e.target.nodeName == "TEXTAREA") return;
892
893         let keycode = false;
894         let shift_key = false;
895
896         const cmdline = $('cmdline');
897
898         try {
899                 shift_key = e.shiftKey;
900         } catch (e) {
901
902         }
903
904         if (window.event) {
905                 keycode = window.event.keyCode;
906         } else if (e) {
907                 keycode = e.which;
908         }
909
910         let keychar = String.fromCharCode(keycode);
911
912         if (keycode == 27) { // escape
913                 hotkey_prefix = false;
914         }
915
916         if (keycode == 16) return; // ignore lone shift
917         if (keycode == 17) return; // ignore lone ctrl
918
919         if (!shift_key) keychar = keychar.toLowerCase();
920
921         var hotkeys = getInitParam("hotkeys");
922
923         if (!hotkey_prefix && hotkeys[0].indexOf(keychar) != -1) {
924
925                 const date = new Date();
926                 const ts = Math.round(date.getTime() / 1000);
927
928                 hotkey_prefix = keychar;
929                 hotkey_prefix_pressed = ts;
930
931                 cmdline.innerHTML = keychar;
932                 Element.show(cmdline);
933
934                 return true;
935         }
936
937         Element.hide(cmdline);
938
939         let hotkey = keychar.search(/[a-zA-Z0-9]/) != -1 ? keychar : "(" + keycode + ")";
940         hotkey = hotkey_prefix ? hotkey_prefix + " " + hotkey : hotkey;
941         hotkey_prefix = false;
942
943         let hotkey_action = false;
944         var hotkeys = getInitParam("hotkeys");
945
946         for (const sequence in hotkeys[1]) {
947                 if (sequence == hotkey) {
948                         hotkey_action = hotkeys[1][sequence];
949                         break;
950                 }
951         }
952
953         switch (hotkey_action) {
954                 case "feed_subscribe":
955                         quickAddFeed();
956                         return false;
957                 case "create_label":
958                         addLabel();
959                         return false;
960                 case "create_filter":
961                         quickAddFilter();
962                         return false;
963                 case "help_dialog":
964                         //helpDialog("prefs");
965                         return false;
966                 default:
967                         console.log("unhandled action: " + hotkey_action + "; hotkey: " + hotkey);
968         }
969 }
970
971 function removeCategory(id, item) {
972
973         const ok = confirm(__("Remove category %s? Any nested feeds would be placed into Uncategorized.").replace("%s", item.name));
974
975         if (ok) {
976                 const query = "?op=pref-feeds&method=removeCat&ids=" +
977                         param_escape(id);
978
979                 notify_progress("Removing category...");
980
981                 new Ajax.Request("backend.php", {
982                         parameters: query,
983                         onComplete: function (transport) {
984                                 notify('');
985                                 updateFeedList();
986                         }
987                 });
988         }
989 }
990
991 function removeSelectedCategories() {
992
993         const sel_rows = getSelectedCategories();
994
995         if (sel_rows.length > 0) {
996
997                 const ok = confirm(__("Remove selected categories?"));
998
999                 if (ok) {
1000                         notify_progress("Removing selected categories...");
1001
1002                         const query = "?op=pref-feeds&method=removeCat&ids="+
1003                                 param_escape(sel_rows.toString());
1004
1005                         new Ajax.Request("backend.php", {
1006                                 parameters: query,
1007                                 onComplete: function(transport) {
1008                                                 updateFeedList();
1009                                         } });
1010
1011                 }
1012         } else {
1013                 alert(__("No categories are selected."));
1014         }
1015
1016         return false;
1017 }
1018
1019 function createCategory() {
1020         const title = prompt(__("Category title:"));
1021
1022         if (title) {
1023
1024                 notify_progress("Creating category...");
1025
1026                 const query = "?op=pref-feeds&method=addCat&cat=" +
1027                         param_escape(title);
1028
1029                 new Ajax.Request("backend.php", {
1030                         parameters: query,
1031                         onComplete: function (transport) {
1032                                 notify('');
1033                                 updateFeedList();
1034                         }
1035                 });
1036         }
1037 }
1038
1039 function showInactiveFeeds() {
1040         const query = "backend.php?op=pref-feeds&method=inactiveFeeds";
1041
1042         if (dijit.byId("inactiveFeedsDlg"))
1043                 dijit.byId("inactiveFeedsDlg").destroyRecursive();
1044
1045         var dialog = new dijit.Dialog({
1046                 id: "inactiveFeedsDlg",
1047                 title: __("Feeds without recent updates"),
1048                 style: "width: 600px",
1049                 getSelectedFeeds: function () {
1050                         return getSelectedTableRowIds("prefInactiveFeedList");
1051                 },
1052                 removeSelected: function () {
1053                         const sel_rows = this.getSelectedFeeds();
1054
1055                         console.log(sel_rows);
1056
1057                         if (sel_rows.length > 0) {
1058                                 const ok = confirm(__("Remove selected feeds?"));
1059
1060                                 if (ok) {
1061                                         notify_progress("Removing selected feeds...", true);
1062
1063                                         const query = "?op=pref-feeds&method=remove&ids=" +
1064                                                 param_escape(sel_rows.toString());
1065
1066                                         new Ajax.Request("backend.php", {
1067                                                 parameters: query,
1068                                                 onComplete: function (transport) {
1069                                                         notify('');
1070                                                         dialog.hide();
1071                                                         updateFeedList();
1072                                                 }
1073                                         });
1074                                 }
1075
1076                         } else {
1077                                 alert(__("No feeds are selected."));
1078                         }
1079                 },
1080                 execute: function () {
1081                         if (this.validate()) {
1082                         }
1083                 },
1084                 href: query
1085         });
1086
1087         dialog.show();
1088 }
1089
1090 function opmlRegenKey() {
1091         const ok = confirm(__("Replace current OPML publishing address with a new one?"));
1092
1093         if (ok) {
1094
1095                 notify_progress("Trying to change address...", true);
1096
1097                 const query = "?op=pref-feeds&method=regenOPMLKey";
1098
1099                 new Ajax.Request("backend.php", {
1100                         parameters: query,
1101                         onComplete: function (transport) {
1102                                 const reply = JSON.parse(transport.responseText);
1103
1104                                 const new_link = reply.link;
1105
1106                                 const e = $('pub_opml_url');
1107
1108                                 if (new_link) {
1109                                         e.href = new_link;
1110                                         e.innerHTML = new_link;
1111
1112                                         new Effect.Highlight(e);
1113
1114                                         notify('');
1115
1116                                 } else {
1117                                         notify_error("Could not change feed URL.");
1118                                 }
1119                         }
1120                 });
1121         }
1122         return false;
1123 }
1124
1125 function labelColorReset() {
1126         const labels = getSelectedLabels();
1127
1128         if (labels.length > 0) {
1129                 const ok = confirm(__("Reset selected labels to default colors?"));
1130
1131                 if (ok) {
1132                         const query = "?op=pref-labels&method=colorreset&ids=" +
1133                                 param_escape(labels.toString());
1134
1135                         new Ajax.Request("backend.php", {
1136                                 parameters: query,
1137                                 onComplete: function (transport) {
1138                                         updateLabelList();
1139                                 }
1140                         });
1141                 }
1142
1143         } else {
1144                 alert(__("No labels are selected."));
1145         }
1146 }
1147
1148 function inPreferences() {
1149         return true;
1150 }
1151
1152 function editProfiles() {
1153
1154         if (dijit.byId("profileEditDlg"))
1155                 dijit.byId("profileEditDlg").destroyRecursive();
1156
1157         const query = "backend.php?op=pref-prefs&method=editPrefProfiles";
1158
1159         var dialog = new dijit.Dialog({
1160                 id: "profileEditDlg",
1161                 title: __("Settings Profiles"),
1162                 style: "width: 600px",
1163                 getSelectedProfiles: function () {
1164                         return getSelectedTableRowIds("prefFeedProfileList");
1165                 },
1166                 removeSelected: function () {
1167                         const sel_rows = this.getSelectedProfiles();
1168
1169                         if (sel_rows.length > 0) {
1170                                 const ok = confirm(__("Remove selected profiles? Active and default profiles will not be removed."));
1171
1172                                 if (ok) {
1173                                         notify_progress("Removing selected profiles...", true);
1174
1175                                         const query = "?op=rpc&method=remprofiles&ids=" +
1176                                                 param_escape(sel_rows.toString());
1177
1178                                         new Ajax.Request("backend.php", {
1179                                                 parameters: query,
1180                                                 onComplete: function (transport) {
1181                                                         notify('');
1182                                                         editProfiles();
1183                                                 }
1184                                         });
1185
1186                                 }
1187
1188                         } else {
1189                                 alert(__("No profiles are selected."));
1190                         }
1191                 },
1192                 activateProfile: function () {
1193                         const sel_rows = this.getSelectedProfiles();
1194
1195                         if (sel_rows.length == 1) {
1196
1197                                 const ok = confirm(__("Activate selected profile?"));
1198
1199                                 if (ok) {
1200                                         notify_progress("Loading, please wait...");
1201
1202                                         const query = "?op=rpc&method=setprofile&id=" +
1203                                                 param_escape(sel_rows.toString());
1204
1205                                         new Ajax.Request("backend.php", {
1206                                                 parameters: query,
1207                                                 onComplete: function (transport) {
1208                                                         window.location.reload();
1209                                                 }
1210                                         });
1211                                 }
1212
1213                         } else {
1214                                 alert(__("Please choose a profile to activate."));
1215                         }
1216                 },
1217                 addProfile: function () {
1218                         if (this.validate()) {
1219                                 notify_progress("Creating profile...", true);
1220
1221                                 const query = "?op=rpc&method=addprofile&title=" +
1222                                         param_escape(dialog.attr('value').newprofile);
1223
1224                                 new Ajax.Request("backend.php", {
1225                                         parameters: query,
1226                                         onComplete: function (transport) {
1227                                                 notify('');
1228                                                 editProfiles();
1229                                         }
1230                                 });
1231
1232                         }
1233                 },
1234                 execute: function () {
1235                         if (this.validate()) {
1236                         }
1237                 },
1238                 href: query
1239         });
1240
1241         dialog.show();
1242 }
1243
1244 function activatePrefProfile() {
1245
1246         const sel_rows = getSelectedFeedCats();
1247
1248         if (sel_rows.length == 1) {
1249
1250                 const ok = confirm(__("Activate selected profile?"));
1251
1252                 if (ok) {
1253                         notify_progress("Loading, please wait...");
1254
1255                         const query = "?op=rpc&method=setprofile&id="+
1256                                 param_escape(sel_rows.toString());
1257
1258                         new Ajax.Request("backend.php", {
1259                                 parameters: query,
1260                                 onComplete: function(transport) {
1261                                         window.location.reload();
1262                                 } });
1263                 }
1264
1265         } else {
1266                 alert(__("Please choose a profile to activate."));
1267         }
1268
1269         return false;
1270 }
1271
1272 function clearFeedAccessKeys() {
1273
1274         const ok = confirm(__("This will invalidate all previously generated feed URLs. Continue?"));
1275
1276         if (ok) {
1277                 notify_progress("Clearing URLs...");
1278
1279                 const query = "?op=pref-feeds&method=clearKeys";
1280
1281                 new Ajax.Request("backend.php", {
1282                         parameters: query,
1283                         onComplete: function(transport) {
1284                                 notify_info("Generated URLs cleared.");
1285                         } });
1286         }
1287
1288         return false;
1289 }
1290
1291 function resetFilterOrder() {
1292         notify_progress("Loading, please wait...");
1293
1294         new Ajax.Request("backend.php", {
1295                 parameters: "?op=pref-filters&method=filtersortreset",
1296                 onComplete: function (transport) {
1297                         updateFilterList();
1298                 }
1299         });
1300 }
1301
1302
1303 function resetFeedOrder() {
1304         notify_progress("Loading, please wait...");
1305
1306         new Ajax.Request("backend.php", {
1307                 parameters: "?op=pref-feeds&method=feedsortreset",
1308                 onComplete: function (transport) {
1309                         updateFeedList();
1310                 }
1311         });
1312 }
1313
1314 function resetCatOrder() {
1315         notify_progress("Loading, please wait...");
1316
1317         new Ajax.Request("backend.php", {
1318                 parameters: "?op=pref-feeds&method=catsortreset",
1319                 onComplete: function (transport) {
1320                         updateFeedList();
1321                 }
1322         });
1323 }
1324
1325 function editCat(id, item) {
1326         const new_name = prompt(__('Rename category to:'), item.name);
1327
1328         if (new_name && new_name != item.name) {
1329
1330                 notify_progress("Loading, please wait...");
1331
1332                 new Ajax.Request("backend.php", {
1333                         parameters: {
1334                                 op: 'pref-feeds',
1335                                 method: 'renamecat',
1336                                 id: id,
1337                                 title: new_name,
1338                         },
1339                         onComplete: function (transport) {
1340                                 updateFeedList();
1341                         }
1342                 });
1343         }
1344 }
1345
1346 function editLabel(id) {
1347         const query = "backend.php?op=pref-labels&method=edit&id=" +
1348                 param_escape(id);
1349
1350         if (dijit.byId("labelEditDlg"))
1351                 dijit.byId("labelEditDlg").destroyRecursive();
1352
1353         const dialog = new dijit.Dialog({
1354                 id: "labelEditDlg",
1355                 title: __("Label Editor"),
1356                 style: "width: 600px",
1357                 setLabelColor: function (id, fg, bg) {
1358
1359                         let kind = '';
1360                         let color = '';
1361
1362                         if (fg && bg) {
1363                                 kind = 'both';
1364                         } else if (fg) {
1365                                 kind = 'fg';
1366                                 color = fg;
1367                         } else if (bg) {
1368                                 kind = 'bg';
1369                                 color = bg;
1370                         }
1371
1372                         const query = "?op=pref-labels&method=colorset&kind=" + kind +
1373                                 "&ids=" + param_escape(id) + "&fg=" + param_escape(fg) +
1374                                 "&bg=" + param_escape(bg) + "&color=" + param_escape(color);
1375
1376                         //              console.log(query);
1377
1378                         const e = $("LICID-" + id);
1379
1380                         if (e) {
1381                                 if (fg) e.style.color = fg;
1382                                 if (bg) e.style.backgroundColor = bg;
1383                         }
1384
1385                         new Ajax.Request("backend.php", {parameters: query});
1386
1387                         updateFilterList();
1388                 },
1389                 execute: function () {
1390                         if (this.validate()) {
1391                                 const caption = this.attr('value').caption;
1392                                 const fg_color = this.attr('value').fg_color;
1393                                 const bg_color = this.attr('value').bg_color;
1394                                 const query = dojo.objectToQuery(this.attr('value'));
1395
1396                                 dijit.byId('labelTree').setNameById(id, caption);
1397                                 this.setLabelColor(id, fg_color, bg_color);
1398                                 this.hide();
1399
1400                                 new Ajax.Request("backend.php", {
1401                                         parameters: query,
1402                                         onComplete: function (transport) {
1403                                                 updateFilterList();
1404                                         }
1405                                 });
1406                         }
1407                 },
1408                 href: query
1409         });
1410
1411         dialog.show();
1412 }
1413
1414
1415 function customizeCSS() {
1416         const query = "backend.php?op=pref-prefs&method=customizeCSS";
1417
1418         if (dijit.byId("cssEditDlg"))
1419                 dijit.byId("cssEditDlg").destroyRecursive();
1420
1421         const dialog = new dijit.Dialog({
1422                 id: "cssEditDlg",
1423                 title: __("Customize stylesheet"),
1424                 style: "width: 600px",
1425                 execute: function () {
1426                         notify_progress('Saving data...', true);
1427                         new Ajax.Request("backend.php", {
1428                                 parameters: dojo.objectToQuery(this.attr('value')),
1429                                 onComplete: function (transport) {
1430                                         notify('');
1431                                         window.location.reload();
1432                                 }
1433                         });
1434
1435                 },
1436                 href: query
1437         });
1438
1439         dialog.show();
1440 }
1441
1442 function insertSSLserial(value) {
1443         dijit.byId("SSL_CERT_SERIAL").attr('value', value);
1444 }
1445
1446 function gotoExportOpml(filename, settings) {
1447         const tmp = settings ? 1 : 0;
1448         document.location.href = "backend.php?op=opml&method=export&filename=" + filename + "&settings=" + tmp;
1449 }
1450
1451
1452 function batchSubscribe() {
1453         const query = "backend.php?op=pref-feeds&method=batchSubscribe";
1454
1455         // overlapping widgets
1456         if (dijit.byId("batchSubDlg")) dijit.byId("batchSubDlg").destroyRecursive();
1457         if (dijit.byId("feedAddDlg"))    dijit.byId("feedAddDlg").destroyRecursive();
1458
1459         var dialog = new dijit.Dialog({
1460                 id: "batchSubDlg",
1461                 title: __("Batch subscribe"),
1462                 style: "width: 600px",
1463                 execute: function () {
1464                         if (this.validate()) {
1465                                 console.log(dojo.objectToQuery(this.attr('value')));
1466
1467                                 notify_progress(__("Subscribing to feeds..."), true);
1468
1469                                 new Ajax.Request("backend.php", {
1470                                         parameters: dojo.objectToQuery(this.attr('value')),
1471                                         onComplete: function (transport) {
1472                                                 notify("");
1473                                                 updateFeedList();
1474                                                 dialog.hide();
1475                                         }
1476                                 });
1477                         }
1478                 },
1479                 href: query
1480         });
1481
1482         dialog.show();
1483 }
1484
1485 function clearPluginData(name) {
1486         if (confirm(__("Clear stored data for this plugin?"))) {
1487                 notify_progress("Loading, please wait...");
1488
1489                 new Ajax.Request("backend.php", {
1490                         parameters: "?op=pref-prefs&method=clearplugindata&name=" + param_escape(name),
1491                         onComplete: function(transport) {
1492                                 notify('');
1493                                 updatePrefsList();
1494                         } });
1495         }
1496 }
1497
1498 function clearSqlLog() {
1499
1500         if (confirm(__("Clear all messages in the error log?"))) {
1501
1502                 notify_progress("Loading, please wait...");
1503                 const query = "?op=pref-system&method=clearLog";
1504
1505                 new Ajax.Request("backend.php", {
1506                         parameters: query,
1507                         onComplete: function(transport) {
1508                                 updateSystemList();
1509                         } });
1510
1511         }
1512 }
1513
1514 function updateSelectedPrompt() {
1515         // no-op shim for toggleSelectedRow()
1516 }
1517