5 function notify_callback2(transport, sticky) {
6 notify_info(transport.responseText, sticky);
9 function updateFeedList() {
11 const user_search = $("feed_search");
13 if (user_search) { search = user_search.value; }
15 xhrPost("backend.php", { op: "pref-feeds", search: search }, (transport) => {
16 dijit.byId('feedConfigTab').attr('content', transport.responseText);
17 selectTab("feedConfig", true);
22 function checkInactiveFeeds() {
23 xhrPost("backend.php", { op: "pref-feeds", method: "getinactivefeeds" }, (transport) => {
24 if (parseInt(transport.responseText) > 0) {
25 Element.show(dijit.byId("pref_feeds_inactive_btn").domNode);
30 function updateUsersList(sort_key) {
31 const user_search = $("user_search");
32 const search = user_search ? user_search.value : "";
34 const query = { op: "pref-users", sort: sort_key, search: search };
36 xhrPost("backend.php", query, (transport) => {
37 dijit.byId('userConfigTab').attr('content', transport.responseText);
38 selectTab("userConfig", true)
44 const login = prompt(__("Please enter login:"), "");
51 alert(__("Can't create user: no login specified."));
55 notify_progress("Adding user...");
57 xhrPost("backend.php", { op: "pref-users", method: "add", login: login }, (transport) => {
58 notify_callback2(transport);
64 function editUser(id) {
66 const query = "backend.php?op=pref-users&method=edit&id=" +
69 if (dijit.byId("userEditDlg"))
70 dijit.byId("userEditDlg").destroyRecursive();
72 const dialog = new dijit.Dialog({
74 title: __("User Editor"),
75 style: "width: 600px",
76 execute: function () {
77 if (this.validate()) {
78 notify_progress("Saving data...", true);
80 xhrPost("backend.php", dojo.formToObject("user_edit_form"), (transport) => {
92 function editFilter(id) {
94 const query = "backend.php?op=pref-filters&method=edit&id=" + param_escape(id);
96 if (dijit.byId("feedEditDlg"))
97 dijit.byId("feedEditDlg").destroyRecursive();
99 if (dijit.byId("filterEditDlg"))
100 dijit.byId("filterEditDlg").destroyRecursive();
102 const dialog = new dijit.Dialog({
104 title: __("Edit Filter"),
105 style: "width: 600px",
108 const query = "backend.php?" + dojo.formToQuery("filter_edit_form") + "&savemode=test";
110 editFilterTest(query);
112 selectRules: function (select) {
113 $$("#filterDlg_Matches input[type=checkbox]").each(function (e) {
116 e.parentNode.addClassName("Selected");
118 e.parentNode.removeClassName("Selected");
121 selectActions: function (select) {
122 $$("#filterDlg_Actions input[type=checkbox]").each(function (e) {
126 e.parentNode.addClassName("Selected");
128 e.parentNode.removeClassName("Selected");
132 editRule: function (e) {
133 const li = e.parentNode;
134 const rule = li.getElementsByTagName("INPUT")[1].value;
135 addFilterRule(li, rule);
137 editAction: function (e) {
138 const li = e.parentNode;
139 const action = li.getElementsByTagName("INPUT")[1].value;
140 addFilterAction(li, action);
142 removeFilter: function () {
143 const msg = __("Remove filter?");
148 notify_progress("Removing filter...");
150 const query = { op: "pref-filters", method: "remove", ids: this.attr('value').id };
152 xhrPost("backend.php", query, () => {
157 addAction: function () {
160 addRule: function () {
163 deleteAction: function () {
164 $$("#filterDlg_Actions li[class*=Selected]").each(function (e) {
165 e.parentNode.removeChild(e)
168 deleteRule: function () {
169 $$("#filterDlg_Matches li[class*=Selected]").each(function (e) {
170 e.parentNode.removeChild(e)
173 execute: function () {
174 if (this.validate()) {
176 notify_progress("Saving data...", true);
178 xhrPost("backend.php", dojo.formToObject("filter_edit_form"), () => {
191 function getSelectedLabels() {
192 const tree = dijit.byId("labelTree");
193 const items = tree.model.getCheckedItems();
196 items.each(function(item) {
197 rv.push(tree.model.store.getValue(item, 'bare_id'));
203 function getSelectedUsers() {
204 return getSelectedTableRowIds("prefUserList");
207 function getSelectedFeeds() {
208 const tree = dijit.byId("feedTree");
209 const items = tree.model.getCheckedItems();
212 items.each(function(item) {
213 if (item.id[0].match("FEED:"))
214 rv.push(tree.model.store.getValue(item, 'bare_id'));
220 function getSelectedCategories() {
221 const tree = dijit.byId("feedTree");
222 const items = tree.model.getCheckedItems();
225 items.each(function(item) {
226 if (item.id[0].match("CAT:"))
227 rv.push(tree.model.store.getValue(item, 'bare_id'));
233 function getSelectedFilters() {
234 const tree = dijit.byId("filterTree");
235 const items = tree.model.getCheckedItems();
238 items.each(function(item) {
239 rv.push(tree.model.store.getValue(item, 'bare_id'));
246 function removeSelectedLabels() {
248 const sel_rows = getSelectedLabels();
250 if (sel_rows.length > 0) {
252 const ok = confirm(__("Remove selected labels?"));
255 notify_progress("Removing selected labels...");
257 const query = { op: "pref-labels", method: "remove",
258 ids: sel_rows.toString() };
260 xhrPost("backend.php", query, () => {
265 alert(__("No labels are selected."));
271 function removeSelectedUsers() {
273 const sel_rows = getSelectedUsers();
275 if (sel_rows.length > 0) {
277 const ok = confirm(__("Remove selected users? Neither default admin nor your account will be removed."));
280 notify_progress("Removing selected users...");
282 const query = { op: "pref-users", method: "remove",
283 ids: sel_rows.toString() };
285 xhrPost("backend.php", query, () => {
291 alert(__("No users are selected."));
297 function removeSelectedFilters() {
299 const sel_rows = getSelectedFilters();
301 if (sel_rows.length > 0) {
303 const ok = confirm(__("Remove selected filters?"));
306 notify_progress("Removing selected filters...");
308 const query = { op: "pref-filters", method: "remove",
309 ids: sel_rows.toString() };
311 xhrPost("backend.php", query, () => {
316 alert(__("No filters are selected."));
322 function removeSelectedFeeds() {
324 const sel_rows = getSelectedFeeds();
326 if (sel_rows.length > 0) {
328 const ok = confirm(__("Unsubscribe from selected feeds?"));
332 notify_progress("Unsubscribing from selected feeds...", true);
334 const query = { op: "pref-feeds", method: "remove",
335 ids: sel_rows.toString() };
337 xhrPost("backend.php", query, () => {
343 alert(__("No feeds are selected."));
349 function editSelectedUser() {
350 const rows = getSelectedUsers();
352 if (rows.length == 0) {
353 alert(__("No users are selected."));
357 if (rows.length > 1) {
358 alert(__("Please select only one user."));
367 function resetSelectedUserPass() {
369 const rows = getSelectedUsers();
371 if (rows.length == 0) {
372 alert(__("No users are selected."));
376 if (rows.length > 1) {
377 alert(__("Please select only one user."));
381 const ok = confirm(__("Reset password of selected user?"));
384 notify_progress("Resetting password for selected user...");
388 xhrPost("backend.php", { op: "pref-users", method: "resetPass", id: id }, (transport) => {
389 notify_info(transport.responseText, true);
395 function selectedUserDetails() {
397 const rows = getSelectedUsers();
399 if (rows.length == 0) {
400 alert(__("No users are selected."));
404 if (rows.length > 1) {
405 alert(__("Please select only one user."));
409 const query = "backend.php?op=pref-users&method=userdetails&id=" + param_escape(rows[0]);
411 if (dijit.byId("userDetailsDlg"))
412 dijit.byId("userDetailsDlg").destroyRecursive();
414 const dialog = new dijit.Dialog({
415 id: "userDetailsDlg",
416 title: __("User details"),
417 style: "width: 600px",
418 execute: function () {
428 function editSelectedFilter() {
429 const rows = getSelectedFilters();
431 if (rows.length == 0) {
432 alert(__("No filters are selected."));
436 if (rows.length > 1) {
437 alert(__("Please select only one filter."));
447 function joinSelectedFilters() {
448 const rows = getSelectedFilters();
450 if (rows.length == 0) {
451 alert(__("No filters are selected."));
455 const ok = confirm(__("Combine selected filters?"));
458 notify_progress("Joining filters...");
460 xhrPost("backend.php", { op: "pref-filters", method: "join", ids: rows.toString() }, () => {
466 function editSelectedFeed() {
467 const rows = getSelectedFeeds();
469 if (rows.length == 0) {
470 alert(__("No feeds are selected."));
474 if (rows.length > 1) {
475 return editSelectedFeeds();
480 editFeed(rows[0], {});
484 function editSelectedFeeds() {
485 const rows = getSelectedFeeds();
487 if (rows.length == 0) {
488 alert(__("No feeds are selected."));
492 notify_progress("Loading, please wait...");
494 if (dijit.byId("feedEditDlg"))
495 dijit.byId("feedEditDlg").destroyRecursive();
497 xhrPost("backend.php", { op: "pref-feeds", method: "editfeeds", ids: rows.toString() }, (transport) => {
500 const dialog = new dijit.Dialog({
502 title: __("Edit Multiple Feeds"),
503 style: "width: 600px",
504 getChildByName: function (name) {
506 this.getChildren().each(
508 if (child.name == name) {
515 toggleField: function (checkbox, elem, label) {
516 this.getChildByName(elem).attr('disabled', !checkbox.checked);
519 if (checkbox.checked)
520 $(label).removeClassName('insensitive');
522 $(label).addClassName('insensitive');
525 execute: function () {
526 if (this.validate() && confirm(__("Save changes to selected feeds?"))) {
527 const query = this.attr('value');
529 /* normalize unchecked checkboxes because [] is not serialized */
531 Object.keys(query).each((key) => {
532 let val = query[key];
534 if (typeof val == "object" && val.length == 0)
535 query[key] = ["off"];
538 notify_progress("Saving data...", true);
540 xhrPost("backend.php", query, () => {
546 content: transport.responseText
553 function opmlImportComplete(iframe) {
554 if (!iframe.contentDocument.body.innerHTML) return false;
556 Element.show(iframe);
560 if (dijit.byId('opmlImportDlg'))
561 dijit.byId('opmlImportDlg').destroyRecursive();
563 const content = iframe.contentDocument.body.innerHTML;
565 const dialog = new dijit.Dialog({
567 title: __("OPML Import"),
568 style: "width: 600px",
569 onCancel: function () {
570 window.location.reload();
572 execute: function () {
573 window.location.reload();
581 function opmlImport() {
583 const opml_file = $("opml_file");
585 if (opml_file.value.length == 0) {
586 alert(__("Please choose an OPML file first."));
589 notify_progress("Importing, please wait...", true);
591 Element.show("upload_iframe");
598 function updateFilterList() {
599 const user_search = $("filter_search");
601 if (user_search) { search = user_search.value; }
603 xhrPost("backend.php", { op: "pref-filters", search: search }, (transport) => {
604 dijit.byId('filterConfigTab').attr('content', transport.responseText);
609 function updateLabelList() {
610 xhrPost("backend.php", { op: "pref-labels" }, (transport) => {
611 dijit.byId('labelConfigTab').attr('content', transport.responseText);
616 function updatePrefsList() {
617 xhrPost("backend.php", { op: "pref-prefs" }, (transport) => {
618 dijit.byId('genConfigTab').attr('content', transport.responseText);
623 function updateSystemList() {
624 xhrPost("backend.php", { op: "pref-system" }, (transport) => {
625 dijit.byId('systemConfigTab').attr('content', transport.responseText);
630 function selectTab(id, noupdate) {
632 notify_progress("Loading, please wait...");
654 console.warn("unknown tab", id);
657 const tab = dijit.byId(id + "Tab");
658 dijit.byId("pref-tabs").selectChild(tab);
663 function init_second_stage() {
664 document.onkeydown = pref_hotkey_handler;
665 loading_set_progress(50);
668 let tab = getURLParam('tab');
671 tab = dijit.byId(tab + "Tab");
672 if (tab) dijit.byId("pref-tabs").selectChild(tab);
675 const method = getURLParam('method');
677 if (method == 'editFeed') {
678 const param = getURLParam('methodparam');
680 window.setTimeout(function() { editFeed(param) }, 100);
683 setInterval(hotkey_prefix_timeout, 5*1000);
687 window.onerror = function (message, filename, lineno, colno, error) {
688 report_error(message, filename, lineno, colno, error);
691 require(["dojo/_base/kernel",
696 "dijit/ColorPalette",
699 "dijit/form/CheckBox",
700 "dijit/form/DropDownButton",
701 "dijit/form/FilteringSelect",
702 "dijit/form/MultiSelect",
704 "dijit/form/RadioButton",
705 "dijit/form/ComboButton",
707 "dijit/form/SimpleTextarea",
708 "dijit/form/TextBox",
709 "dijit/form/ValidationTextBox",
710 "dijit/InlineEditBox",
711 "dijit/layout/AccordionContainer",
712 "dijit/layout/AccordionPane",
713 "dijit/layout/BorderContainer",
714 "dijit/layout/ContentPane",
715 "dijit/layout/TabContainer",
720 "dijit/tree/dndSource",
721 "dojo/data/ItemFileWriteStore",
722 "lib/CheckBoxStoreModel",
725 "fox/PrefFilterStore",
727 "fox/PrefFilterTree",
728 "fox/PrefLabelTree"], function (dojo, ready, parser) {
734 loading_set_progress(50);
736 const clientTzOffset = new Date().getTimezoneOffset() * 60;
738 new Ajax.Request("backend.php", {
740 op: "rpc", method: "sanityCheck",
741 clientTzOffset: clientTzOffset
743 onComplete: function (transport) {
744 backend_sanity_check_callback(transport);
755 function validatePrefsReset() {
756 if (confirm(__("Reset to defaults?"))) {
758 const query = "?op=pref-prefs&method=resetconfig";
760 xhrPost("backend.php", { op: "pref-prefs", method: "resetconfig" }, (transport) => {
762 notify_info(transport.responseText);
769 function pref_hotkey_handler(e) {
770 if (e.target.nodeName == "INPUT" || e.target.nodeName == "TEXTAREA") return;
772 const action_name = keyevent_to_action(e);
775 switch (action_name) {
776 case "feed_subscribe":
782 case "create_filter":
789 console.log("unhandled action: " + action_name + "; keycode: " + e.which);
794 function removeCategory(id, item) {
796 const ok = confirm(__("Remove category %s? Any nested feeds would be placed into Uncategorized.").replace("%s", item.name));
799 const query = "?op=pref-feeds&method=removeCat&ids=" +
802 notify_progress("Removing category...");
804 new Ajax.Request("backend.php", {
806 onComplete: function (transport) {
814 function removeSelectedCategories() {
816 const sel_rows = getSelectedCategories();
818 if (sel_rows.length > 0) {
820 const ok = confirm(__("Remove selected categories?"));
823 notify_progress("Removing selected categories...");
825 const query = "?op=pref-feeds&method=removeCat&ids="+
826 param_escape(sel_rows.toString());
828 new Ajax.Request("backend.php", {
830 onComplete: function(transport) {
836 alert(__("No categories are selected."));
842 function createCategory() {
843 const title = prompt(__("Category title:"));
847 notify_progress("Creating category...");
849 const query = "?op=pref-feeds&method=addCat&cat=" +
852 new Ajax.Request("backend.php", {
854 onComplete: function (transport) {
862 function showInactiveFeeds() {
863 const query = "backend.php?op=pref-feeds&method=inactiveFeeds";
865 if (dijit.byId("inactiveFeedsDlg"))
866 dijit.byId("inactiveFeedsDlg").destroyRecursive();
868 const dialog = new dijit.Dialog({
869 id: "inactiveFeedsDlg",
870 title: __("Feeds without recent updates"),
871 style: "width: 600px",
872 getSelectedFeeds: function () {
873 return getSelectedTableRowIds("prefInactiveFeedList");
875 removeSelected: function () {
876 const sel_rows = this.getSelectedFeeds();
878 console.log(sel_rows);
880 if (sel_rows.length > 0) {
881 const ok = confirm(__("Remove selected feeds?"));
884 notify_progress("Removing selected feeds...", true);
886 const query = "?op=pref-feeds&method=remove&ids=" +
887 param_escape(sel_rows.toString());
889 new Ajax.Request("backend.php", {
891 onComplete: function (transport) {
900 alert(__("No feeds are selected."));
903 execute: function () {
904 if (this.validate()) {
913 function opmlRegenKey() {
914 const ok = confirm(__("Replace current OPML publishing address with a new one?"));
918 notify_progress("Trying to change address...", true);
920 const query = "?op=pref-feeds&method=regenOPMLKey";
922 new Ajax.Request("backend.php", {
924 onComplete: function (transport) {
925 const reply = JSON.parse(transport.responseText);
927 const new_link = reply.link;
929 const e = $('pub_opml_url');
933 e.innerHTML = new_link;
935 new Effect.Highlight(e);
940 notify_error("Could not change feed URL.");
948 function labelColorReset() {
949 const labels = getSelectedLabels();
951 if (labels.length > 0) {
952 const ok = confirm(__("Reset selected labels to default colors?"));
955 const query = "?op=pref-labels&method=colorreset&ids=" +
956 param_escape(labels.toString());
958 new Ajax.Request("backend.php", {
960 onComplete: function (transport) {
967 alert(__("No labels are selected."));
971 function inPreferences() {
975 function editProfiles() {
977 if (dijit.byId("profileEditDlg"))
978 dijit.byId("profileEditDlg").destroyRecursive();
980 const query = "backend.php?op=pref-prefs&method=editPrefProfiles";
982 const dialog = new dijit.Dialog({
983 id: "profileEditDlg",
984 title: __("Settings Profiles"),
985 style: "width: 600px",
986 getSelectedProfiles: function () {
987 return getSelectedTableRowIds("prefFeedProfileList");
989 removeSelected: function () {
990 const sel_rows = this.getSelectedProfiles();
992 if (sel_rows.length > 0) {
993 const ok = confirm(__("Remove selected profiles? Active and default profiles will not be removed."));
996 notify_progress("Removing selected profiles...", true);
998 const query = { op: "rpc", method: "remprofiles",
999 ids: sel_rows.toString() };
1001 xhrPost("backend.php", query, () => {
1008 alert(__("No profiles are selected."));
1011 activateProfile: function () {
1012 const sel_rows = this.getSelectedProfiles();
1014 if (sel_rows.length == 1) {
1016 const ok = confirm(__("Activate selected profile?"));
1019 notify_progress("Loading, please wait...");
1021 xhrPost("backend.php", { op: "rpc", method: "setprofile", id: sel_rows.toString() }, () => {
1022 window.location.reload();
1027 alert(__("Please choose a profile to activate."));
1030 addProfile: function () {
1031 if (this.validate()) {
1032 notify_progress("Creating profile...", true);
1034 const query = { op: "rpc", method: "addprofile", title: dialog.attr('value').newprofile };
1036 xhrPost("backend.php", query, () => {
1043 execute: function () {
1044 if (this.validate()) {
1054 function activatePrefProfile() {
1056 const sel_rows = getSelectedFeedCats();
1058 if (sel_rows.length == 1) {
1060 const ok = confirm(__("Activate selected profile?"));
1063 notify_progress("Loading, please wait...");
1065 xhrPost("backend.php", { op: "rpc", method: "setprofile", id: sel_rows.toString() }, () => {
1066 window.location.reload();
1071 alert(__("Please choose a profile to activate."));
1077 function clearFeedAccessKeys() {
1079 const ok = confirm(__("This will invalidate all previously generated feed URLs. Continue?"));
1082 notify_progress("Clearing URLs...");
1084 xhrPost("backend.php", { op: "pref-feeds", method: "clearKeys" }, () => {
1085 notify_info("Generated URLs cleared.");
1092 function resetFilterOrder() {
1093 notify_progress("Loading, please wait...");
1095 xhrPost("backend.php", { op: "pref-filters", method: "filtersortreset" }, () => {
1101 function resetFeedOrder() {
1102 notify_progress("Loading, please wait...");
1104 xhrPost("backend.php", { op: "pref-feeds", method: "feedsortreset" }, () => {
1109 function resetCatOrder() {
1110 notify_progress("Loading, please wait...");
1112 xhrPost("backend.php", { op: "pref-feeds", method: "catsortreset" }, () => {
1117 function editCat(id, item) {
1118 const new_name = prompt(__('Rename category to:'), item.name);
1120 if (new_name && new_name != item.name) {
1122 notify_progress("Loading, please wait...");
1124 xhrPost("backend.php", { op: 'pref-feeds', method: 'renamecat', id: id, title: new_name }, () => {
1130 function editLabel(id) {
1131 const query = "backend.php?op=pref-labels&method=edit&id=" +
1134 if (dijit.byId("labelEditDlg"))
1135 dijit.byId("labelEditDlg").destroyRecursive();
1137 const dialog = new dijit.Dialog({
1139 title: __("Label Editor"),
1140 style: "width: 600px",
1141 setLabelColor: function (id, fg, bg) {
1156 const e = $("LICID-" + id);
1159 if (fg) e.style.color = fg;
1160 if (bg) e.style.backgroundColor = bg;
1163 const query = { op: "pref-labels", method: "colorset", kind: kind,
1164 ids: id, fg: fg, bg: bg, color: color };
1166 xhrPost("backend.php", query, () => {
1167 updateFilterList(); // maybe there's labels in there
1171 execute: function () {
1172 if (this.validate()) {
1173 const caption = this.attr('value').caption;
1174 const fg_color = this.attr('value').fg_color;
1175 const bg_color = this.attr('value').bg_color;
1177 dijit.byId('labelTree').setNameById(id, caption);
1178 this.setLabelColor(id, fg_color, bg_color);
1181 xhrPost("backend.php", this.attr('value'), () => {
1182 updateFilterList(); // maybe there's labels in there
1193 function customizeCSS() {
1194 const query = "backend.php?op=pref-prefs&method=customizeCSS";
1196 if (dijit.byId("cssEditDlg"))
1197 dijit.byId("cssEditDlg").destroyRecursive();
1199 const dialog = new dijit.Dialog({
1201 title: __("Customize stylesheet"),
1202 style: "width: 600px",
1203 execute: function () {
1204 notify_progress('Saving data...', true);
1206 xhrPost("backend.php", this.attr('value'), () => {
1207 window.location.reload();
1217 function insertSSLserial(value) {
1218 dijit.byId("SSL_CERT_SERIAL").attr('value', value);
1221 function gotoExportOpml(filename, settings) {
1222 const tmp = settings ? 1 : 0;
1223 document.location.href = "backend.php?op=opml&method=export&filename=" + filename + "&settings=" + tmp;
1227 function batchSubscribe() {
1228 const query = "backend.php?op=pref-feeds&method=batchSubscribe";
1230 // overlapping widgets
1231 if (dijit.byId("batchSubDlg")) dijit.byId("batchSubDlg").destroyRecursive();
1232 if (dijit.byId("feedAddDlg")) dijit.byId("feedAddDlg").destroyRecursive();
1234 const dialog = new dijit.Dialog({
1236 title: __("Batch subscribe"),
1237 style: "width: 600px",
1238 execute: function () {
1239 if (this.validate()) {
1240 notify_progress(__("Subscribing to feeds..."), true);
1242 xhrPost("backend.php", this.attr('value'), () => {
1255 function clearPluginData(name) {
1256 if (confirm(__("Clear stored data for this plugin?"))) {
1257 notify_progress("Loading, please wait...");
1259 xhrPost("backend.php", { op: "pref-prefs", method: "clearplugindata", name: name }, () => {
1266 function clearSqlLog() {
1268 if (confirm(__("Clear all messages in the error log?"))) {
1270 notify_progress("Loading, please wait...");
1272 xhrPost("backend.php", { op: "pref-system", method: "clearLog" }, () => {
1279 function updateSelectedPrompt() {
1280 // no-op shim for toggleSelectedRow()