aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <wk@koszkonutek-tmp.pl.eu.org>2021-06-19 21:26:12 +0200
committerWojtek Kosior <wk@koszkonutek-tmp.pl.eu.org>2021-06-19 21:26:12 +0200
commit659f532eba94af1c77eb5961fc6706aa2eca3723 (patch)
tree524937c6531b04644797fb8eba40b00bda618d87
parent7c44b46ef66376caa46577b68dcac089ed49500c (diff)
downloadbrowser-extension-659f532eba94af1c77eb5961fc6706aa2eca3723.tar.gz
browser-extension-659f532eba94af1c77eb5961fc6706aa2eca3723.zip
add import/export functionality
-rw-r--r--TODOS.org5
-rw-r--r--html/options.html43
-rw-r--r--html/options_main.js247
3 files changed, 289 insertions, 6 deletions
diff --git a/TODOS.org b/TODOS.org
index f53efbe..026bf79 100644
--- a/TODOS.org
+++ b/TODOS.org
@@ -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);
}