diff options
-rw-r--r-- | TODOS.org | 5 | ||||
-rw-r--r-- | html/options.html | 43 | ||||
-rw-r--r-- | html/options_main.js | 247 |
3 files changed, 289 insertions, 6 deletions
@@ -10,7 +10,6 @@ TODO: those scripts are already free, as is often the case) - also, find some convenient way to automatically re-add "on" events ("onclick" & friends) - add some good, sane error handling -- make it possible to export page settings in some format -- CRUCIAL - get rid of those warnings and exceptions in console (many are not even related to this extension; who invented this thing?) (gecko-only) - make page settings easily and conveniently editable in popup -- CRUCIAL @@ -46,8 +45,12 @@ TODO: - besides blocking scripts through csp, also block connections that needlessly fetch those scripts - make extension's all html files proper XHTML +- split options_main.js into several smaller files +- find out what causes storage sometimes not to get initialized under IceCat 60 +- validate settings data on import DONE: +- make it possible to export page settings in some format -- DONE 2021-06-19 - make it possible to use wildcard urls in settings -- DONE 2021-05-14 - port to gecko-based browsers -- DONE 2021-05-13 - find a way to additionally block all other scripts using CSP -- DONE 2021-05-13 diff --git a/html/options.html b/html/options.html index 21ece29..cbbadec 100644 --- a/html/options.html +++ b/html/options.html @@ -89,15 +89,20 @@ background-color: white; width: 50vw; } + + input[type="radio"]:not(:checked)+.import_window_content { + display: none; + } </style> </head> <body> <!-- The invisible div below is for elements that will be cloned. --> - <div style="display: none;"> + <div class="hide"> <li id="item_li_template"> <span></span> <button> Edit </button> <button> Remove </button> + <button> Export </button> </li> <li id="bag_component_li_template"> <span></span> @@ -111,6 +116,11 @@ <input type="radio" style="display: inline;" name="page_components"></input> <span></span> </li> + <li id="import_li_template"> + <span></span> + <input type="checkbox" style="display: inline;" checked></input> + <span></span> + </li> </div> <input type="radio" name="tabs" id="show_pages" checked></input> @@ -186,6 +196,8 @@ <button id="add_script_but" type="button"> Add script </button> </div> + <button id="import_but" style="margin-top: 40px;"> Import </button> + <div id="chbx_components_window" class="hide popup" position="absolute"> <div class="popup_frame"> <ul id="chbx_components_ul"> @@ -210,6 +222,35 @@ </div> </div> + <div id="import_window" class="hide popup" position="absolute"> + <div class="popup_frame"> + <h2> Settings import </h2> + <input id="import_loading_radio" type="radio" name="import_window_content"></input> + <span class="import_window_content"> Loading... </span> + <input id="import_failed_radio" type="radio" name="import_window_content"></input> + <div class="import_window_content"> + <span> Bad file :( </span> + <pre id="bad_file_errormsg"></pre> + <button id="import_failok_but"> OK </button> + </div> + <input id="import_selection_radio" type="radio" name="import_window_content"></input> + <div class="import_window_content"> + <button id="check_all_import_but"> Check all </button> + <button id="uncheck_all_import_but"> Uncheck all </button> + <button id="uncheck_colliding_import_but"> Uncheck existing </button> + <ul id="import_ul"> + </ul> + <button id="commit_import_but"> OK </button> + <button id="cancel_import_but"> Cancel </button> + </div> + </div> + </div> + + <a id="file_downloader" class="hide"></a> + <form id="file_opener_form" style="visibility: hidden;"> + <input type="file" id="file_opener"></input> + </form> + <script src="/common/connection_types.js"></script> <script src="/common/stored_types.js"></script> <script src="/common/once.js"></script> diff --git a/html/options_main.js b/html/options_main.js index 03c5ca8..468b347 100644 --- a/html/options_main.js +++ b/html/options_main.js @@ -37,15 +37,22 @@ return document.getElementById(id); } + function nice_name(prefix, name) + { + return `${name} (${TYPE_NAME[prefix]})`; + } + const item_li_template = by_id("item_li_template"); const bag_component_li_template = by_id("bag_component_li_template"); const chbx_component_li_template = by_id("chbx_component_li_template"); const radio_component_li_template = by_id("radio_component_li_template"); + const import_li_template = by_id("import_li_template"); /* Make sure they are later cloned without id. */ item_li_template.removeAttribute("id"); bag_component_li_template.removeAttribute("id"); chbx_component_li_template.removeAttribute("id"); radio_component_li_template.removeAttribute("id"); + import_li_template.removeAttribute("id"); function item_li_id(prefix, item) { @@ -69,6 +76,10 @@ remove_button.addEventListener("click", () => storage.remove(prefix, item)); + let export_button = remove_button.nextElementSibling; + export_button.addEventListener("click", + () => export_item(prefix, item)); + if (!at_the_end) { for (let element of ul.ul.children) { if (element.id < li.id || element.id.startsWith("work_")) @@ -110,7 +121,7 @@ let chbx = li.firstElementChild; let span = chbx.nextElementSibling; - span.textContent = `${name} (${TYPE_NAME[prefix]})`; + span.textContent = nice_name(prefix, name); chbx_components_ul.appendChild(li); } @@ -130,7 +141,7 @@ let radio = li.firstElementChild; let span = radio.nextElementSibling; - span.textContent = `${name} (${TYPE_NAME[prefix]})`; + span.textContent = nice_name(prefix, name); radio_components_ul.insertBefore(li, radio_component_none_li); } @@ -147,7 +158,7 @@ let [prefix, name] = components; page_payload_span.setAttribute("data-prefix", prefix); page_payload_span.setAttribute("data-name", name); - page_payload_span.textContent = `${name} (${TYPE_NAME[prefix]})`; + page_payload_span.textContent = nice_name(prefix, name); } } @@ -197,7 +208,7 @@ li.setAttribute("data-prefix", prefix); li.setAttribute("data-name", name); let span = li.firstElementChild; - span.textContent = `${name} (${TYPE_NAME[prefix]})`; + span.textContent = nice_name(prefix, name); let remove_but = span.nextElementSibling; remove_but.addEventListener("click", () => bag_components_ul.removeChild(li)); @@ -319,6 +330,50 @@ ul.edited_item = item; } + const file_downloader = by_id("file_downloader"); + + function recursively_export_item(prefix, name, added_items, items_data) + { + let key = prefix + name; + + if (added_items.has(key)) + return; + + let data = storage.get(prefix, name); + if (data === undefined) { + console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`); + return; + } + + if (prefix !== TYPE_PREFIX.SCRIPT) { + let components = prefix === TYPE_PREFIX.BAG ? + data : [data.components]; + + for (let [comp_prefix, comp_name] of components) { + recursively_export_item(comp_prefix, comp_name, + added_items, items_data); + } + } + + items_data.push({[key]: data}); + added_items.add(key); + } + + function export_item(prefix, name) + { + let added_items = new Set(); + let items_data = []; + recursively_export_item(prefix, name, added_items, items_data); + let file = new Blob([JSON.stringify(items_data)], + {type: "application/json"}); + let url = URL.createObjectURL(file); + file_downloader.setAttribute("href", url); + file_downloader.setAttribute("download", prefix + name + ".json"); + file_downloader.click(); + file_downloader.removeAttribute("href"); + URL.revokeObjectURL(url); + } + function add_new_item(prefix) { cancel_work(prefix); @@ -452,6 +507,188 @@ } } + const import_window = by_id("import_window"); + const import_loading_radio = by_id("import_loading_radio"); + const import_failed_radio = by_id("import_failed_radio"); + const import_selection_radio = by_id("import_selection_radio"); + const bad_file_errormsg = by_id("bad_file_errormsg"); + + /* + * Newer browsers could utilise `text' method of File objects. + * Older ones require FileReader. + */ + + function _read_file(file, resolve, reject) + { + let reader = new FileReader(); + + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + reader.readAsText(file); + } + + function read_file(file) + { + return new Promise((resolve, reject) => + _read_file(file, resolve, reject)); + } + + async function import_from_file(event) + { + let files = event.target.files; + if (files.length < 1) + return; + + import_window.classList.remove("hide"); + import_loading_radio.checked = true; + + let result = undefined; + + try { + result = JSON.parse(await read_file(files[0])); + } catch(e) { + bad_file_errormsg.textContent = "" + e; + import_failed_radio.checked = true; + return; + } + + let errormsg = validate_settings(result); + if (errormsg !== false) { + bad_file_errormsg.textContent = errormsg; + import_failed_radio.checked = true; + return; + } + + populate_import_list(result); + import_selection_radio.checked = true; + } + + function validate_settings(settings) + { + // TODO + return false; + } + + function import_li_id(prefix, item) + { + return `ili_${prefix}_${item}`; + } + + let import_ul = by_id("import_ul"); + let import_chbxs_colliding = undefined; + let settings_import_map = undefined; + + function populate_import_list(settings) + { + let old_children = import_ul.children; + while (old_children[0] !== undefined) + import_ul.removeChild(old_children[0]); + + import_chbxs_colliding = []; + settings_import_map = new Map(); + + for (let setting of settings) { + let [key, value] = Object.entries(setting)[0]; + let prefix = key[0]; + let name = key.substring(1); + add_import_li(prefix, name); + settings_import_map.set(key, value); + } + } + + function add_import_li(prefix, name) + { + let li = import_li_template.cloneNode(true); + let name_span = li.firstElementChild; + let chbx = name_span.nextElementSibling; + let warning_span = chbx.nextElementSibling; + + li.setAttribute("data-prefix", prefix); + li.setAttribute("data-name", name); + li.id = import_li_id(prefix, name); + name_span.textContent = nice_name(prefix, name); + + if (storage.get(prefix, name) !== undefined) { + import_chbxs_colliding.push(chbx); + warning_span.textContent = "(will overwrite existing setting!)"; + } + + import_ul.appendChild(li); + } + + function check_all_imports() + { + for (let li of import_ul.children) + li.firstElementChild.nextElementSibling.checked = true; + } + + function uncheck_all_imports() + { + for (let li of import_ul.children) + li.firstElementChild.nextElementSibling.checked = false; + } + + function uncheck_colliding_imports() + { + for (let chbx of import_chbxs_colliding) + chbx.checked = false; + } + + const file_opener_form = by_id("file_opener_form"); + + function hide_import_window() + { + import_window.classList.add("hide"); + /* Let GC free some memory */ + import_chbxs_colliding = undefined; + settings_import_map = undefined; + + /* + * Reset file <input>. Without this, a second attempt to import the same + * file would result in "change" event on happening on <input> element. + */ + file_opener_form.reset(); + } + + function commit_import() + { + for (let li of import_ul.children) { + let chbx = li.firstElementChild.nextElementSibling; + + if (!chbx.checked) + continue; + + let prefix = li.getAttribute("data-prefix"); + let name = li.getAttribute("data-name"); + let key = prefix + name; + let value = settings_import_map.get(key); + storage.set(prefix, name, value); + } + + hide_import_window(); + } + + function initialize_import_facility() + { + let import_but = by_id("import_but"); + let file_opener = by_id("file_opener"); + let import_failok_but = by_id("import_failok_but"); + let check_all_import_but = by_id("check_all_import_but"); + let uncheck_all_import_but = by_id("uncheck_all_import_but"); + let uncheck_existing_import_but = by_id("uncheck_existing_import_but"); + let commit_import_but = by_id("commit_import_but"); + let cancel_import_but = by_id("cancel_import_but"); + import_but.addEventListener("click", () => file_opener.click()); + file_opener.addEventListener("change", import_from_file); + import_failok_but.addEventListener("click", hide_import_window); + check_all_import_but.addEventListener("click", check_all_imports); + uncheck_all_import_but.addEventListener("click", uncheck_all_imports); + uncheck_colliding_import_but + .addEventListener("click", uncheck_colliding_imports); + commit_import_but.addEventListener("click", commit_import); + cancel_import_but.addEventListener("click", hide_import_window); + } + async function main() { storage = await get_storage(); @@ -489,6 +726,8 @@ cancel_components_but.addEventListener("click", cancel_components); } + initialize_import_facility(); + storage.add_change_listener(handle_change); } |