aboutsummaryrefslogtreecommitdiff
path: root/html
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2021-08-14 10:07:28 +0200
committerWojtek Kosior <koszko@koszko.org>2021-08-14 10:07:28 +0200
commit443bc095a72949adb4a007c9a19a43da7dd8843d (patch)
tree4b3e392057d8939961a3e707ae596cd85e6fa623 /html
parentae1844f9ac935eb3c89314cd402b4ec2c3d0f537 (diff)
parent2fbab2f07d3cebde1fba0e801df4f3e9129e463b (diff)
downloadbrowser-extension-443bc095a72949adb4a007c9a19a43da7dd8843d.tar.gz
browser-extension-443bc095a72949adb4a007c9a19a43da7dd8843d.zip
merge facility to install from Hydrilla
Diffstat (limited to 'html')
-rw-r--r--html/DOM_helpers.js41
-rw-r--r--html/base.css44
-rw-r--r--html/display-panel.html122
-rw-r--r--html/display-panel.js395
-rw-r--r--html/import_frame.html27
-rw-r--r--html/import_frame.js163
-rw-r--r--html/options.html65
-rw-r--r--html/options_main.js209
8 files changed, 793 insertions, 273 deletions
diff --git a/html/DOM_helpers.js b/html/DOM_helpers.js
new file mode 100644
index 0000000..2bff966
--- /dev/null
+++ b/html/DOM_helpers.js
@@ -0,0 +1,41 @@
+/**
+ * Hachette operations on DOM elements
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+function by_id(id)
+{
+ return document.getElementById(id);
+}
+
+function clone_template(template_id)
+{
+ const clone = document.getElementById(template_id).cloneNode(true);
+ const result_object = {};
+ const to_process = [clone];
+
+ while (to_process.length > 0) {
+ const element = to_process.pop();
+ const template_key = element.getAttribute("data-template");
+
+ if (template_key)
+ result_object[template_key] = element;
+
+ element.removeAttribute("id");
+ element.removeAttribute("template_key");
+
+ for (const child of element.children)
+ to_process.push(child);
+ }
+
+ return result_object;
+}
+
+/*
+ * EXPORTS_START
+ * EXPORT by_id
+ * EXPORT clone_template
+ * EXPORTS_END
+ */
diff --git a/html/base.css b/html/base.css
new file mode 100644
index 0000000..2256833
--- /dev/null
+++ b/html/base.css
@@ -0,0 +1,44 @@
+/**
+ * Hachette base styles
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Copyright (C) 2021 Nicholas Johnson
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+input[type="checkbox"], input[type="radio"], .hide {
+ display: none;
+}
+
+.show_next:not(:checked)+* {
+ display: none;
+}
+
+.show_hide_next2:not(:checked)+* {
+ display: none;
+}
+
+.show_hide_next2:checked+*+* {
+ display: none;
+}
+
+button, .button {
+ background-color: #4CAF50;
+ border: none;
+ border-radius: 8px;
+ color: white;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ padding: 6px 12px;
+ margin: 2px 0px;
+}
+
+button.slimbutton, .button.slimbutton {
+ padding: 2px 4px;
+ margin: 0;
+}
+
+button:hover, .button:hover {
+ box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
+}
diff --git a/html/display-panel.html b/html/display-panel.html
index 9b6d619..1b9c77b 100644
--- a/html/display-panel.html
+++ b/html/display-panel.html
@@ -7,37 +7,31 @@
<head>
<meta charset="utf-8"/>
<title>Hachette - page settings</title>
+ <link type="text/css" rel="stylesheet" href="base.css" />
<style>
- input[type="radio"], input[type="checkbox"] {
- display: none;
- }
-
body {
width: 300px;
height: 300px;
}
- .show_next:not(:checked)+* {
- display: none;
+ ul {
+ padding-inline-start: 15px;
}
- .hide {
- display: none;
+ .bold {
+ font-weight: bold;
}
- #possible_patterns_chbx:not(:checked)+label span#triangle:first-child+span,
- #possible_patterns_chbx:not(:checked)+label+*,
- #possible_patterns_chbx:checked+label span#triangle:first-child {
+ .unroll_chbx:not(:checked)+*+label span.triangle:first-child+span.triangle,
+ .unroll_chbx:checked+*+label span.triangle:first-child,
+ .unroll_chbx:not(:checked)+*,
+ .unroll_chbx:not(:checked)+*+label+* {
display: none;
}
#container_for_injected>#none_injected:not(:last-child) {
display: none;
}
-
- input#connected_chbx:checked+div+h3 {
- display: none;
- }
</style>
</head>
<body>
@@ -47,49 +41,85 @@
<span></span>
<button>View in settings</button>
</li>
+ <li id="query_match_li_template" class="queried_pattern_match" data-template="li">
+ <div>
+ <span>pattern:</span>
+ <span class="bold" data-template="pattern"></span>
+ <button data-template="btn">Install</button>
+ </div>
+ <div id="unrollable_component_template" data-template="unroll_container">
+ <span data-template="component_label">payload:</span>
+ <input type="checkbox" class="unroll_chbx" data-template="chbx"></input>
+ <br data-template="br"/>
+ <label class="bold" data-template="lbl">
+ <span data-template="triangle">
+ <span class="triangle">&#x23F5;</span>
+ <span class="triangle">&#x23F7;</span>
+ </span>
+ <span data-template="component"></span>
+ </label>
+ <div data-template="unroll"></div>
+ </div>
+ </li>
</div>
- <h2 id="page_url_heading"></h2>
-
- <input id="show_privileged_notice_chbx" type="checkbox" class="show_next"></input>
- <h3>Privileged page</h3>
+ <input id="show_install_view_chbx" type="checkbox" class="show_hide_next2"></input>
+ <div id="install_view">
+ <IMPORT html/import_frame.html />
+ <!--
+ <div id="install_status"></div>
+ <label for="show_install_chbx" class="bold">Cancel install</label>
+ <button id="commit_install_but">Commit install</button>
+ -->
+ </div>
+ <div id="main_view">
+ <h2 id="page_url_heading"></h2>
- <input id="show_page_state_chbx" type="checkbox" class="show_next"></input>
- <div>
- <input id="possible_patterns_chbx" type="checkbox"></input>
- <label for="possible_patterns_chbx">
- <h3>
- <span id="triangle">&#x23F5;</span><span>&#x23F7;</span>
- Possible patterns
- </h3>
- </label>
- <ul id="possible_patterns"></ul>
+ <input id="show_privileged_notice_chbx" type="checkbox" class="show_next"></input>
+ <h3>Privileged page</h3>
- <input id="connected_chbx" type="checkbox" class="show_next"></input>
+ <input id="show_page_state_chbx" type="checkbox" class="show_next"></input>
<div>
- <h3>
- Matched pattern: <span id="pattern">...</span>
+ <input id="possible_patterns_chbx" type="checkbox" class="unroll_chbx"></input>
+ <span></span>
+ <label for="possible_patterns_chbx">
+ <h3>
+ <span class="triangle">&#x23F5;</span>
+ <span class="triangle">&#x23F7;</span>
+ Possible patterns
+ </h3>
+ </label>
+ <ul id="possible_patterns"></ul>
+
+ <input id="connected_chbx" type="checkbox" class="show_hide_next2"></input>
+ <div>
+ Matched pattern: <span id="pattern" class="bold">...</span>
<button id="view_pattern" class="hide">
- View in settings
+ View in settings
</button>
- </h3>
- <h3>
- Blocked: <span id="blocked">...</span>
- </h3>
- <h3>
- Payload: <span id="payload">...</span>
+ <br/>
+ Blocked: <span id="blocked" class="bold">...</span>
+ <br/>
+ Payload: <span id="payload" class="bold">...</span>
<button id="view_payload" class="hide">
View in settings
</button>
- </h3>
- <h3>Injected</h3>
- <div id="container_for_injected">
- <span id="none_injected">None</span>
+ <h3>Injected</h3>
+ <div id="container_for_injected">
+ <span id="none_injected">None</span>
+ </div>
+ <input id="query_started_chbx" type="checkbox" class="show_hide_next2"></input>
+ <div id="container_for_repo_responses">
+ <h3>Queried from repositories</h3>
+ </div>
+ <button id="query_pattern">
+ Search for matching patterns
+ </button>
</div>
+ <h3>Trying to connect..<input id="loading_chbx" type="checkbox" class="show_next"></input><span>.</span></h3>
</div>
- <h3>Trying to connect..<input id="loading_chbx" type="checkbox" class="show_next"></input><span>.</span></h3>
- </div>
- <button id="settings_but" type="button" style="margin-top: 20px;">Settings</button>_POPUPSCRIPTS_
+ <button id="settings_but" type="button" style="margin-top: 20px;">Settings</button>
+ </div>_POPUPSCRIPTS_
</body>
</html>
diff --git a/html/display-panel.js b/html/display-panel.js
index 650c234..b4d9abb 100644
--- a/html/display-panel.js
+++ b/html/display-panel.js
@@ -10,6 +10,11 @@
* IMPORT browser
* IMPORT is_chrome
* IMPORT is_mozilla
+ *** Using remote storage here seems inefficient, we only resort to that
+ *** temporarily, before all storage access gets reworked.
+ * IMPORT get_remote_storage
+ * IMPORT get_import_frame
+ * IMPORT query_all
* IMPORT CONNECTION_TYPE
* IMPORT url_item
* IMPORT is_privileged_url
@@ -17,13 +22,13 @@
* IMPORT nice_name
* IMPORT open_in_settings
* IMPORT for_each_possible_pattern
+ * IMPORT by_id
+ * IMPORT clone_template
* IMPORTS_END
*/
-function by_id(id)
-{
- return document.getElementById(id);
-}
+let storage;
+let tab_url;
const tab_query = {currentWindow: true, active: true};
@@ -55,28 +60,19 @@ async function show_page_activity_info()
return;
}
- const url = url_item(tab.url);
- page_url_heading.textContent = url;
- if (is_privileged_url(url)) {
+ tab_url = url_item(tab.url);
+ page_url_heading.textContent = tab_url;
+ if (is_privileged_url(tab_url)) {
show_privileged_notice_chbx.checked = true;
return;
}
- populate_possible_patterns_list(url);
+ populate_possible_patterns_list(tab_url);
show_page_state_chbx.checked = true;
try_to_connect(tab.id);
}
-function populate_possible_patterns_list(url)
-{
- for_each_possible_pattern(url, add_pattern_to_list);
-
- const port = browser.runtime.connect({name: CONNECTION_TYPE.PAGE_INFO});
- port.onMessage.addListener(handle_page_info);
- port.postMessage(["subscribe", url]);
-}
-
const possible_patterns_ul = by_id("possible_patterns");
const pattern_li_template = by_id("pattern_li_template");
pattern_li_template.removeAttribute("id");
@@ -121,51 +117,69 @@ function set_pattern_li_button_text(li_id, text)
by_id(li_id).firstElementChild.nextElementSibling.textContent = text;
}
-function handle_page_info(message)
+function handle_page_change(change)
{
- const [type, data] = message;
+ const li_id = ensure_pattern_exists(change.item);
+ if (change.old_val === undefined)
+ set_pattern_li_button_text(li_id, "Edit in settings");
+ if (change.new_val === undefined)
+ set_pattern_li_button_text(li_id, "Add setting");
+}
- if (type === "change") {
- const li_id = ensure_pattern_exists(data.item);
- if (data.old_val === undefined)
- set_pattern_li_button_text(li_id, "Edit in settings");
- if (data.new_val === undefined)
- set_pattern_li_button_text(li_id, "Add setting");
- }
+function populate_possible_patterns_list(url)
+{
+ for_each_possible_pattern(url, add_pattern_to_list);
- if (type === "new_url") {
- for (const li_id of known_patterns.values())
- set_pattern_li_button_text(li_id, "Add setting");
- for (const [pattern, settings] of data) {
- set_pattern_li_button_text(ensure_pattern_exists(pattern),
- "Edit in settings")
- }
+ for (const [pattern, settings] of query_all(storage, url)) {
+ set_pattern_li_button_text(ensure_pattern_exists(pattern),
+ "Edit in settings");
}
+
+ storage.add_change_listener(handle_page_change, [TYPE_PREFIX.PAGE]);
}
const connected_chbx = by_id("connected_chbx");
+const query_pattern_but = by_id("query_pattern");
+
+var content_script_port;
function try_to_connect(tab_id)
{
/* This won't connect to iframes. We'll add support for them later */
const connect_info = {name: CONNECTION_TYPE.ACTIVITY_INFO, frameId: 0};
- const port = browser.tabs.connect(tab_id, connect_info);
+ content_script_port = browser.tabs.connect(tab_id, connect_info);
+
+ const disconnect_cb = () => handle_disconnect(tab_id, start_querying_repos);
+ content_script_port.onDisconnect.addListener(disconnect_cb);
+ content_script_port.onMessage.addListener(handle_activity_report);
- port.onDisconnect.addListener(port => handle_disconnect(tab_id));
- port.onMessage.addListener(handle_activity_report);
+ query_pattern_but.addEventListener("click", start_querying_repos);
if (is_mozilla)
- setTimeout(() => monitor_connecting(port, tab_id), 1000);
+ setTimeout(() => monitor_connecting(tab_id), 1000);
+}
+
+const query_started_chbx = by_id("query_started_chbx");
+
+function start_querying_repos(port)
+{
+ const repo_urls = storage.get_all_names(TYPE_PREFIX.REPO);
+ if (content_script_port)
+ content_script_port.postMessage([TYPE_PREFIX.URL, tab_url, repo_urls]);
+ query_started_chbx.checked = true;
}
const loading_chbx = by_id("loading_chbx");
-function handle_disconnect(tab_id)
+function handle_disconnect(tab_id, button_cb)
{
+ query_pattern_but.removeEventListener("click", button_cb);
+ content_script_port = null;
+
if (is_chrome && !browser.runtime.lastError)
return;
- /* return if there was no connection initialization failure */
+ /* return if error was not during connection initialization */
if (connected_chbx.checked)
return;
@@ -173,12 +187,16 @@ function handle_disconnect(tab_id)
setTimeout(() => try_to_connect(tab_id), 1000);
}
-function monitor_connecting(port, tab_id)
+function monitor_connecting(tab_id)
{
if (connected_chbx.checked)
return;
- port.disconnect();
+ if (content_script_port)
+ content_script_port.disconnect();
+ else
+ return;
+
loading_chbx.checked = !loading_chbx.checked;
try_to_connect(tab_id);
}
@@ -190,6 +208,8 @@ const payload_span = by_id("payload");
const view_payload_but = by_id("view_payload");
const container_for_injected = by_id("container_for_injected");
+const queried_items = new Map();
+
function handle_activity_report(message)
{
connected_chbx.checked = true;
@@ -231,9 +251,300 @@ function handle_activity_report(message)
container_for_injected.appendChild(h4);
container_for_injected.appendChild(pre);
}
+ if (type === "repo_query_action") {
+ query_started_chbx.checked = true;
+
+ const key = data.prefix + data.item;
+ const results = queried_items.get(key) || {};
+ Object.assign(results, data.results);
+ queried_items.set(key, results);
+
+ const action = data.prefix === TYPE_PREFIX.URL ?
+ show_query_result : record_fetched_install_dep;
+
+ for (const [repo_url, result] of Object.entries(data.results))
+ action(data.prefix, data.item, repo_url, result);
+ }
+}
+
+const container_for_repo_responses = by_id("container_for_repo_responses");
+
+const results_lists = new Map();
+
+function create_results_list(url)
+{
+ const list_div = document.createElement("div");
+ const list_head = document.createElement("h4");
+ const list = document.createElement("ul");
+
+ list_head.textContent = url;
+ list_div.appendChild(list_head);
+ list_div.appendChild(list);
+ container_for_repo_responses.appendChild(list_div);
+
+ const list_object = {list, by_repo: new Map()};
+
+ results_lists.set(url, list_object);
+
+ return list_object;
+}
+
+function create_result_item(list_object, repo_url, result)
+{
+ const result_li = document.createElement("li");
+ const repo_url_span = document.createElement("span");
+ const result_item = {result_li, appended: null};
+
+ repo_url_span.textContent = repo_url;
+ result_li.appendChild(repo_url_span);
+
+ list_object.list.appendChild(result_li);
+ list_object.by_repo.set(repo_url, result_item);
+
+ return result_item;
+}
+
+function set_appended(result_item, element)
+{
+ if (result_item.appended)
+ result_item.appended.remove();
+ result_item.appended = element;
+ result_item.result_li.appendChild(element);
+}
+
+function show_message(result_item, text)
+{
+ const div = document.createElement("div");
+ div.textContent = text;
+ set_appended(result_item, div);
+}
+
+function showcb(text)
+{
+ return item => show_message(item, text);
+}
+
+function unroll_chbx_first_checked(entry_object)
+{
+ if (!entry_object.chbx.checked)
+ return;
+
+ entry_object.chbx.removeEventListener("change", entry_object.unroll_cb);
+ delete entry_object.unroll_cb;
+
+ entry_object.unroll.textContent = "preview not implemented...";
+}
+
+const show_install_chbx = by_id("show_install_view_chbx");
+
+let import_frame;
+let install_target = null;
+
+function install_abort(error_state)
+{
+ import_frame.show_error(`Error: ${error_state}`);
+ install_target = null;
+}
+
+/*
+ * Translate objects from the format in which they are sent by Hydrilla to the
+ * format in which they are stored in settings.
+ */
+
+function translate_script(script_object, repo_url)
+{
+ return {
+ [TYPE_PREFIX.SCRIPT + script_object.name]: {
+ hash: script_object.sha256,
+ url: `${repo_url}/content/${script_object.location}`
+ }
+ };
+}
+
+function translate_bag(bag_object)
+{
+ return {
+ [TYPE_PREFIX.BAG + bag_object.name]: bag_object.components
+ };
+}
+
+const format_translators = {
+ [TYPE_PREFIX.BAG]: translate_bag,
+ [TYPE_PREFIX.SCRIPT]: translate_script
+};
+
+function install_check_ready()
+{
+ if (install_target.to_fetch.size > 0)
+ return;
+
+ const page_key = [TYPE_PREFIX.PAGE + install_target.pattern];
+ const to_install = [{[page_key]: {components: install_target.payload}}];
+
+ for (const key of install_target.fetched) {
+ const old_object =
+ queried_items.get(key)[install_target.repo_url].response;
+ const new_object =
+ format_translators[key[0]](old_object, install_target.repo_url);
+ to_install.push(new_object);
+ }
+
+ import_frame.show_selection(to_install);
+}
+
+const possible_errors = ["connection_error", "parse_error"];
+
+function fetch_install_deps(components)
+{
+ const needed = [...components];
+ const processed = new Set();
+
+ while (needed.length > 0) {
+ const [prefix, item] = needed.pop();
+ const key = prefix + item;
+ processed.add(key);
+ const results = queried_items.get(key);
+ let relevant_result = null;
+
+ if (results)
+ relevant_result = results[install_target.repo_url];
+
+ if (!relevant_result) {
+ content_script_port.postMessage([prefix, item,
+ [install_target.repo_url]]);
+ install_target.to_fetch.add(key);
+ continue;
+ }
+
+ if (possible_errors.includes(relevant_result.state)) {
+ install_abort(relevant_result.state);
+ return false;
+ }
+
+ install_target.fetched.add(key);
+
+ if (prefix !== TYPE_PREFIX.BAG)
+ continue;
+
+ for (const dependency of relevant_result.response.components) {
+ if (processed.has(dependency.join('')))
+ continue;
+ needed.push(dependency);
+ }
+ }
+}
+
+function record_fetched_install_dep(prefix, item, repo_url, result)
+{
+ const key = prefix + item;
+
+ if (!install_target || repo_url !== install_target.repo_url ||
+ !install_target.to_fetch.has(key))
+ return;
+
+ if (possible_errors.includes(result.state)) {
+ install_abort(result.state);
+ return;
+ }
+
+ if (result.state !== "completed")
+ return;
+
+ install_target.to_fetch.delete(key);
+ install_target.fetched.add(key);
+
+ if (prefix === TYPE_PREFIX.BAG &&
+ fetch_install_deps(result.response.components) === false)
+ return;
+
+ install_check_ready();
+}
+
+function install_clicked(entry_object)
+{
+ show_install_chbx.checked = true;
+ import_frame.show_loading();
+
+ install_target = {
+ repo_url: entry_object.repo_url,
+ pattern: entry_object.match_object.pattern,
+ payload: entry_object.match_object.payload,
+ fetched: new Set(),
+ to_fetch: new Set()
+ };
+
+ fetch_install_deps([install_target.payload]);
+
+ install_check_ready();
+}
+
+var max_query_result_id = 0;
+
+function show_query_successful_result(result_item, repo_url, result)
+{
+ const ul = document.createElement("ul");
+
+ set_appended(result_item, ul);
+
+ for (const match of result) {
+ const entry_object = clone_template("query_match_li_template");
+
+ entry_object.pattern.textContent = match.pattern;
+
+ ul.appendChild(entry_object.li);
+
+ if (!match.payload) {
+ entry_object.payload.textContent = "(none)";
+ for (const key of ["chbx", "br", "triangle", "unroll"])
+ entry_object[key].remove();
+ continue;
+ }
+
+ entry_object.component.textContent = nice_name(...match.payload);
+
+ const install_cb = () => install_clicked(entry_object);
+ entry_object.btn.addEventListener("click", install_cb);
+
+ const chbx_id = `query_result_${max_query_result_id++}`;
+ entry_object.chbx.id = chbx_id;
+ entry_object.lbl.setAttribute("for", chbx_id);
+
+ entry_object.unroll_cb = () => unroll_chbx_first_checked(entry_object);
+ entry_object.chbx.addEventListener("change", entry_object.unroll_cb);
+
+ entry_object.component_object = match.payload;
+ entry_object.match_object = match;
+ entry_object.repo_url = repo_url;
+ }
+}
+
+function show_query_result(url_prefix, url, repo_url, result)
+{
+ const results_list_object = results_lists.get(url) ||
+ create_results_list(url);
+ const result_item = results_list_object.by_repo.get(repo_url) ||
+ create_result_item(results_list_object, repo_url, result);
+
+ const completed_cb =
+ item => show_query_successful_result(item, repo_url, result.response);
+ const possible_actions = {
+ completed: completed_cb,
+ started: showcb("loading..."),
+ connection_error: showcb("Error when querying repository."),
+ parse_error: showcb("Bad data format received.")
+ };
+ possible_actions[result.state](result_item, repo_url);
}
by_id("settings_but")
.addEventListener("click", (e) => browser.runtime.openOptionsPage());
-show_page_activity_info();
+async function main()
+{
+ storage = await get_remote_storage();
+ import_frame = await get_import_frame();
+ import_frame.onclose = () => show_install_chbx.checked = false;
+ show_page_activity_info();
+}
+
+main();
diff --git a/html/import_frame.html b/html/import_frame.html
new file mode 100644
index 0000000..c86c3de
--- /dev/null
+++ b/html/import_frame.html
@@ -0,0 +1,27 @@
+<div style="display: none;">
+ <li id="import_li_template">
+ <span></span>
+ <input type="checkbox" style="display: inline;" checked></input>
+ <span></span>
+ </li>
+</div>
+<h2> Settings import </h2>
+<input id="import_loading_radio" type="radio" name="import_window_content" class="show_next"></input>
+<span> Loading... </span>
+<input id="import_failed_radio" type="radio" name="import_window_content" class="show_next"></input>
+<div>
+ <span id="import_errormsg"></span>
+ <input id="import_errordetail_chbx" type="checkbox" class="show_next"></input>
+ <pre id="import_errordetail"></pre>
+ <button id="import_failok_but"> OK </button>
+</div>
+<input id="import_selection_radio" type="radio" name="import_window_content" class="show_next"></input>
+<div>
+ <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>
diff --git a/html/import_frame.js b/html/import_frame.js
new file mode 100644
index 0000000..4075433
--- /dev/null
+++ b/html/import_frame.js
@@ -0,0 +1,163 @@
+/**
+ * Hachette HTML import frame script
+ *
+ * Copyright (C) 2021 Wojtek Kosior
+ * Redistribution terms are gathered in the `copyright' file.
+ */
+
+/*
+ * IMPORTS_START
+ * IMPORT get_remote_storage
+ * IMPORT by_id
+ * IMPORT nice_name
+ * IMPORT make_once
+ * IMPORTS_END
+ */
+
+let storage;
+
+const import_li_template = by_id("import_li_template");
+import_li_template.removeAttribute("id");
+
+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 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;
+}
+
+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);
+ }
+
+ deactivate();
+}
+
+const import_loading_radio = by_id("import_loading_radio");
+
+function show_loading()
+{
+ import_loading_radio.checked = true;
+}
+
+const import_failed_radio = by_id("import_failed_radio");
+const import_errormsg = by_id("import_errormsg");
+const import_errordetail_chbx = by_id("import_errordetail_chbx");
+const import_errordetail = by_id("import_errordetail");
+
+function show_error(errormsg, errordetail)
+{
+ import_failed_radio.checked = true;
+ import_errormsg.textContent = errormsg;
+ import_errordetail_chbx.checked = errordetail;
+ import_errordetail.textContent = errordetail;
+}
+
+const import_selection_radio = by_id("import_selection_radio");
+
+function show_selection(settings)
+{
+ import_selection_radio.checked = true;
+
+ 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 deactivate()
+{
+ /* Let GC free some memory */
+ import_chbxs_colliding = undefined;
+ settings_import_map = undefined;
+
+ if (exports.onclose)
+ exports.onclose();
+}
+
+const exports = {show_loading, show_error, show_selection, deactivate};
+
+async function init()
+{
+ storage = await get_remote_storage();
+
+ by_id("commit_import_but").addEventListener("click", commit_import);
+ by_id("check_all_import_but").addEventListener("click", check_all_imports);
+ by_id("uncheck_all_import_but")
+ .addEventListener("click", uncheck_all_imports);
+ by_id("uncheck_colliding_import_but")
+ .addEventListener("click", uncheck_colliding_imports);
+ by_id("cancel_import_but").addEventListener("click", deactivate);
+ by_id("import_failok_but").addEventListener("click", deactivate);
+
+ return exports;
+}
+
+const get_import_frame = make_once(init);
+
+/*
+ * EXPORTS_START
+ * EXPORT get_import_frame
+ * EXPORTS_END
+ */
diff --git a/html/options.html b/html/options.html
index bfb9e52..2246f9a 100644
--- a/html/options.html
+++ b/html/options.html
@@ -8,11 +8,8 @@
<head>
<meta charset="utf-8"/>
<title>Hachette options</title>
+ <link type="text/css" rel="stylesheet" href="base.css" />
<style>
- input[type="checkbox"], input[type="radio"], .hide, .popup.hide {
- display: none;
- }
-
/* pages list */
#page_components_ul {
max-height: 80vh;
@@ -33,12 +30,14 @@
}
/* tabbed view */
+ #show_repos:not(:checked) ~ #repos,
#show_pages:not(:checked) ~ #pages,
#show_bags:not(:checked) ~ #bags,
#show_scripts:not(:checked) ~ #scripts {
display: none;
}
+ #show_repos:checked ~ #repos_lbl,
#show_pages:checked ~ #pages_lbl,
#show_bags:checked ~ #bags_lbl,
#show_scripts:checked ~ #scripts_lbl {
@@ -76,23 +75,6 @@
input[type="radio"]:not(:checked)+.import_window_content {
display: none;
}
-
- /* buttons */
- button {
- background-color: #4CAF50;
- border: none;
- border-radius: 8px;
- color: white;
- padding: 6px 12px;
- text-align: center;
- text-decoration: none;
- display: inline-block;
- margin: 2px 0px;
- }
-
- button:hover {
- box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);
- }
</style>
</head>
<body>
@@ -116,17 +98,15 @@
<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>
<!-- Mind the show_*s ids below - their format is assumed in js code -->
+ <input type="radio" name="tabs" id="show_repos"></input>
<input type="radio" name="tabs" id="show_pages" checked></input>
<input type="radio" name="tabs" id="show_bags"></input>
<input type="radio" name="tabs" id="show_scripts"></input>
+ <label for="show_repos" id="repos_lbl"
+ class="tab_head"> Repos </label>
<label for="show_pages" id="pages_lbl"
class="tab_head"> Pages </label>
<label for="show_bags" id="bags_lbl"
@@ -134,6 +114,19 @@
<label for="show_scripts" id="scripts_lbl"
class="tab_head"> Scripts </label>
+ <div id="repos">
+ <ul id="repos_ul">
+ <li id="work_repo_li" class="hide">
+ <label for="repo_url_field">URL: </label>
+ <input id="repo_url_field"></input>
+ <br/>
+ <button id="save_repo_but" type="button"> Save </button>
+ <button id="discard_repo_but" type="button"> Cancel </button>
+ </li>
+ </ul>
+ <button id="add_repo_but" type="button"> Add repository </button>
+ </div>
+
<div id="pages">
<ul id="pages_ul">
<li id="work_page_li" class="hide">
@@ -225,25 +218,7 @@
<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>
+ <IMPORT html/import_frame.html />
</div>
</div>
diff --git a/html/options_main.js b/html/options_main.js
index e1e6cbe..830c860 100644
--- a/html/options_main.js
+++ b/html/options_main.js
@@ -12,33 +12,31 @@
* IMPORT TYPE_NAME
* IMPORT list_prefixes
* IMPORT nice_name
+ * IMPORT parse_json_with_schema
+ * IMPORT by_id
+ * IMPORT matchers
+ * IMPORT get_import_frame
* IMPORTS_END
*/
var storage;
-function by_id(id)
-{
- return document.getElementById(id);
-}
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)
{
return `li_${prefix}_${item}`;
}
-/* Insert into list of bags/pages/scripts */
+/* Insert into list of bags/pages/scripts/repos */
function add_li(prefix, item, at_the_end=false)
{
let ul = ul_by_prefix[prefix];
@@ -58,6 +56,8 @@ function add_li(prefix, item, at_the_end=false)
let export_button = remove_button.nextElementSibling;
export_button.addEventListener("click",
() => export_item(prefix, item));
+ if (prefix === TYPE_PREFIX.REPO)
+ export_button.remove();
if (!at_the_end) {
for (let element of ul.ul.children) {
@@ -89,7 +89,7 @@ function radio_li_id(prefix, item)
function add_chbx_li(prefix, name)
{
- if (prefix === TYPE_PREFIX.PAGE)
+ if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
return;
let li = chbx_component_li_template.cloneNode(true);
@@ -109,7 +109,7 @@ var radio_component_none_li = by_id("radio_component_none_li");
function add_radio_li(prefix, name)
{
- if (prefix === TYPE_PREFIX.PAGE)
+ if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
return;
let li = radio_component_li_template.cloneNode(true);
@@ -125,6 +125,18 @@ function add_radio_li(prefix, name)
radio_components_ul.insertBefore(li, radio_component_none_li);
}
+/* Used to reset edited repo. */
+function reset_work_repo_li(ul, item, _)
+{
+ ul.work_name_input.value = maybe_string(item);
+}
+
+/* Used to get repo data for saving */
+function work_repo_li_data(ul)
+{
+ return [ul.work_name_input.value, {}];
+}
+
const page_payload_span = by_id("page_payload");
function set_page_components(components)
@@ -461,6 +473,15 @@ const UL_STATE = {
};
const ul_by_prefix = {
+ [TYPE_PREFIX.REPO] : {
+ ul : by_id("repos_ul"),
+ work_li : by_id("work_repo_li"),
+ work_name_input : by_id("repo_url_field"),
+ reset_work_li : reset_work_repo_li,
+ get_work_li_data : work_repo_li_data,
+ state : UL_STATE.IDLE,
+ edited_item : undefined,
+ },
[TYPE_PREFIX.PAGE] : {
ul : by_id("pages_ul"),
work_li : by_id("work_page_li"),
@@ -494,12 +515,6 @@ const ul_by_prefix = {
}
}
-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.
@@ -520,6 +535,31 @@ function read_file(file)
_read_file(file, resolve, reject));
}
+const url_regex = /^[a-z0-9]+:\/\/[^/]+\.[^/]{2,}(\/[^?#]*)?$/;
+const empty_regex = /^$/;
+
+const settings_schema = [
+ [{}, "matchentry", "minentries", 1,
+ new RegExp(`^${TYPE_PREFIX.SCRIPT}`), {
+ /* script data */
+ "url": ["optional", url_regex, "or", empty_regex],
+ "sha256": ["optional", matchers.sha256, "or", empty_regex],
+ "text": ["optional", "string"]
+ },
+ new RegExp(`^${TYPE_PREFIX.BAG}`), [
+ "optional",
+ [matchers.component, "repeat"],
+ "default", undefined
+ ],
+ new RegExp(`^${TYPE_PREFIX.PAGE}`), {
+ /* page data */
+ "components": ["optional", matchers.component]
+ }], "repeat"
+];
+
+const import_window = by_id("import_window");
+let import_frame;
+
async function import_from_file(event)
{
let files = event.target.files;
@@ -527,98 +567,17 @@ async function import_from_file(event)
return;
import_window.classList.remove("hide");
- import_loading_radio.checked = true;
-
- let result = undefined;
+ import_frame.show_loading();
try {
- result = JSON.parse(await read_file(files[0]));
+ const file = await read_file(files[0]);
+ var result = parse_json_with_schema(settings_schema, file);
} 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;
+ import_frame.show_error("Bad file :(", "" + e);
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;
+ import_frame.show_selection(result);
}
const file_opener_form = by_id("file_opener_form");
@@ -626,9 +585,6 @@ 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
@@ -637,43 +593,16 @@ function hide_import_window()
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()
+async 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);
+
+ import_frame = await get_import_frame();
+ import_frame.onclose = hide_import_window;
}
/*
@@ -727,7 +656,7 @@ async function main()
discard_but.addEventListener("click", () => cancel_work(prefix));
save_but.addEventListener("click", () => save_work(prefix));
- if (prefix === TYPE_PREFIX.SCRIPT)
+ if ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix))
continue;
let ul = ul_by_prefix[prefix];
@@ -745,9 +674,9 @@ async function main()
jump_to_item(document.URL);
- initialize_import_facility();
-
storage.add_change_listener(handle_change);
+
+ await initialize_import_facility();
}
function handle_change(change)
@@ -772,7 +701,7 @@ function handle_change(change)
let uls_creators = [[ul.ul, item_li_id]];
- if (change.prefix !== TYPE_PREFIX.PAGE) {
+ if ([TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(change.prefix)) {
uls_creators.push([chbx_components_ul, chbx_li_id]);
uls_creators.push([radio_components_ul, radio_li_id]);
}