-
+
diff --git a/src/views/options/components/command-multi-select/main.mjs b/src/views/options/components/command-multi-select/main.mjs
index 13d8ebf7b..dce99f4ce 100644
--- a/src/views/options/components/command-multi-select/main.mjs
+++ b/src/views/options/components/command-multi-select/main.mjs
@@ -1,16 +1,17 @@
import { SortableMultiSelect } from "/views/options/components/sortable-multi-select/main.mjs";
-import { fetchJSONAsObject, fetchHTMLAsFragment } from "/core/utils/commons.mjs";
+import { fetchHTMLAsFragment } from "/core/utils/commons.mjs";
import Command from "/core/models/command.mjs";
+import CommandDefinitions from "/resources/configs/commands.mjs";
+
// getter for module path
const MODULE_DIR = (() => {
const urlPath = new URL(import.meta.url).pathname;
return urlPath.slice(0, urlPath.lastIndexOf("/") + 1);
})();
-const COMMAND_ITEMS = fetchJSONAsObject(browser.runtime.getURL("/resources/json/commands.json"));
const COMMAND_SETTING_TEMPLATES = fetchHTMLAsFragment(browser.runtime.getURL("/views/options/fragments/command-setting-templates.inc"));
/**
@@ -32,15 +33,13 @@ class CommandMultiSelect extends SortableMultiSelect {
this.shadowRoot.append(settingsConainer);
// build/fill command selection list
- COMMAND_ITEMS.then((commandItems) => {
- const commandMultiSelectItems = commandItems.map((commandItem) => {
- const commandMultiSelectItem = document.createElement("sortable-multi-select-item");
- commandMultiSelectItem.dataset.command = commandItem.command;
- commandMultiSelectItem.textContent = browser.i18n.getMessage(`commandLabel${commandItem.command}`);
- return commandMultiSelectItem;
- });
- this.append(...commandMultiSelectItems);
+ const commandMultiSelectItems = CommandDefinitions.map((commandItem) => {
+ const commandMultiSelectItem = document.createElement("sortable-multi-select-item");
+ commandMultiSelectItem.dataset.command = commandItem.command;
+ commandMultiSelectItem.textContent = browser.i18n.getMessage(`commandLabel${commandItem.command}`);
+ return commandMultiSelectItem;
});
+ this.append(...commandMultiSelectItems);
this._commandSettingsRelation = new WeakMap();
}
@@ -229,7 +228,7 @@ class CommandMultiSelect extends SortableMultiSelect {
if (commandSelectItem) {
// get command object
- const commandObject = (await COMMAND_ITEMS).find((element) => {
+ const commandObject = CommandDefinitions.find((element) => {
return element.command === commandSelectItem.dataset.command;
});
diff --git a/src/views/options/components/command-select/main.mjs b/src/views/options/components/command-select/main.mjs
index b3e946407..574fc3876 100644
--- a/src/views/options/components/command-select/main.mjs
+++ b/src/views/options/components/command-select/main.mjs
@@ -1,14 +1,15 @@
-import { fetchJSONAsObject, fetchHTMLAsFragment } from "/core/utils/commons.mjs";
+import { fetchHTMLAsFragment } from "/core/utils/commons.mjs";
import Command from "/core/models/command.mjs";
+import CommandDefinitions from "/resources/configs/commands.mjs";
+
// getter for module path
const MODULE_DIR = (() => {
const urlPath = new URL(import.meta.url).pathname;
return urlPath.slice(0, urlPath.lastIndexOf("/") + 1);
})();
-const COMMAND_ITEMS = fetchJSONAsObject(browser.runtime.getURL("/resources/json/commands.json"));
const COMMAND_SETTING_TEMPLATES = fetchHTMLAsFragment(browser.runtime.getURL("/views/options/fragments/command-setting-templates.inc"));
/**
@@ -185,7 +186,7 @@ class CommandSelect extends HTMLElement {
// build command list
const groups = new Map();
- for (let commandItem of await COMMAND_ITEMS) {
+ for (let commandItem of CommandDefinitions) {
const item = document.createElement("li");
item.classList.add("cb-command-item");
item.dataset.command = commandItem.command;
@@ -491,7 +492,7 @@ class CommandSelect extends HTMLElement {
commandItemInfo.style.removeProperty("height");
// get command item
- const commandItem = (await COMMAND_ITEMS).find((element) => {
+ const commandItem = CommandDefinitions.find((element) => {
return element.command === event.currentTarget.dataset.command;
});
diff --git a/src/views/options/data-management.mjs b/src/views/options/data-management.mjs
deleted file mode 100644
index 1ce87a95f..000000000
--- a/src/views/options/data-management.mjs
+++ /dev/null
@@ -1,157 +0,0 @@
-import { fetchJSONAsObject } from "/core/utils/commons.mjs";
-
-import { ContentLoaded, Config } from "/views/options/main.mjs";
-
-ContentLoaded.then(main);
-
-/**
- * main function
- * run code that depends on async resources
- **/
-function main () {
- // data management buttons
- const resetButton = document.getElementById("resetButton");
- resetButton.onclick = onResetButton;
- const backupButton = document.getElementById("backupButton");
- backupButton.onclick = onBackupButton;
- const restoreButton = document.getElementById("restoreButton");
- restoreButton.onchange = onRestoreButton;
-}
-
-
-/**
- * clears the current config so the defaults will be used
- * resets all optional permissions
- * reloads the options page afterwards
- **/
-function onResetButton () {
- const popup = document.getElementById("resetConfirm");
- popup.addEventListener("close", (event) => {
- if (event.detail) {
- const manifest = browser.runtime.getManifest();
- const removePermissions = browser.permissions.remove(
- { permissions: manifest.optional_permissions }
- );
- removePermissions.then((values) => {
- Config.clear();
- // reload option page to update the ui
- window.location.reload();
- });
- }
- }, { once: true });
- popup.open = true;
-}
-
-
-/**
- * saves the current config as a json file
- **/
-function onBackupButton () {
- const manifest = browser.runtime.getManifest();
- const linkElement = document.createElement("a");
- linkElement.download = `${manifest.name} ${manifest.version} ${ new Date().toDateString() }.json`;
- // creates a json file with the current config
- linkElement.href = URL.createObjectURL(
- new Blob([JSON.stringify(Config.get(), null, ' ')], {type: 'application/json'})
- );
- document.body.appendChild(linkElement);
- linkElement.click();
- document.body.removeChild(linkElement);
-}
-
-
-/**
- * overwrites the current config with the selected config
- * and resets all optional permissions
- * reloads the options page afterwards
- **/
-async function onRestoreButton (event) {
- if (this.files[0].type !== "application/json") {
- const popup = document.getElementById("restoreAlertWrongFile");
- popup.open = true;
- // terminate function
- return;
- }
-
- // catch rejected promises and errors
- try {
- // load file data
- const file = await new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onloadend = () => {
- try {
- const restoredConfig = JSON.parse(reader.result);
- resolve(restoredConfig)
- }
- catch (e) {
- reject();
- }
- }
- reader.onerror = reject;
- reader.readAsText(this.files[0]);
- });
-
- // load the commands data in order to request the right permissions
- const commands = await fetchJSONAsObject(browser.runtime.getURL("/resources/json/commands.json"));
-
- // get the necessary permissions
- const requiredPermissions = [];
- // combine all commands to one array
- const usedCommands = [];
- if (file.Gestures && file.Gestures.length > 0) {
- file.Gestures.forEach(gesture => usedCommands.push(gesture.command));
- }
- if (file.Settings && file.Settings.Rocker) {
- if (file.Settings.Rocker.rightMouseClick) usedCommands.push(file.Settings.Rocker.rightMouseClick);
- if (file.Settings.Rocker.leftMouseClick) usedCommands.push(file.Settings.Rocker.leftMouseClick);
- }
- if (file.Settings && file.Settings.Wheel) {
- if (file.Settings.Wheel.wheelUp) usedCommands.push(file.Settings.Wheel.wheelUp);
- if (file.Settings.Wheel.wheelDown) usedCommands.push(file.Settings.Wheel.wheelDown);
- }
-
- for (let command of usedCommands) {
- const commandItem = commands.find((element) => {
- return element.command === command.name;
- });
- if (commandItem.permissions) commandItem.permissions.forEach((permission) => {
- if (!requiredPermissions.includes(permission)) requiredPermissions.push(permission);
- });
- }
-
- // display popup because permission request requires user interaction
- // also to ensure that the user really wants to override the current config
- const popup = document.getElementById("restoreConfirm");
- popup.addEventListener("close", (event) => {
- // if user declined exit function
- if (!event.detail) return;
- // if optional permissions are required request them
- if (requiredPermissions.length > 0) {
- const permissionRequest = browser.permissions.request({
- permissions: requiredPermissions,
- });
- permissionRequest.then((granted) => {
- if (granted) proceed();
- });
- }
- else proceed();
- }, { once: true });
- popup.open = true;
-
- // helper function to finish the process
- // reload option page to update the ui
- function proceed () {
- Config.clear();
- Config.set(file);
- const popup = document.getElementById("restoreAlertSuccess");
- popup.addEventListener("close", () => window.location.reload(), { once: true });
- popup.open = true;
- }
- }
- catch (e) {
- const popup = document.getElementById("restoreAlertNoConfigFile");
- popup.open = true;
- // terminate function
- return;
- }
-}
\ No newline at end of file
diff --git a/src/views/options/data.mjs b/src/views/options/data.mjs
new file mode 100644
index 000000000..02ff46830
--- /dev/null
+++ b/src/views/options/data.mjs
@@ -0,0 +1,246 @@
+import { ContentLoaded, Config } from "/views/options/main.mjs";
+
+import ConfigManager from "/core/services/config-manager.mjs";
+
+import CommandDefinitions from "/resources/configs/commands.mjs";
+
+ContentLoaded.then(main);
+
+
+/**
+ * main function
+ * run code that depends on async resources
+ **/
+function main () {
+ // data management buttons
+ document.getElementById("fileBackupButton").onclick = onFileBackupButton;
+ document.getElementById("fileRestoreButton").onchange = onFileRestoreButton;
+ document.getElementById("cloudBackupButton").onclick = onCloudBackupButton;
+ document.getElementById("cloudRestoreButton").onclick = onCloudRestoreButton;
+ document.getElementById("configResetButton").onclick = onConfigResetButton;
+
+ handleButtonStates();
+ browser.storage.sync.onChanged.addListener(handleButtonStates);
+}
+
+
+/**
+ * enabled/disables buttons according to different rules
+ **/
+async function handleButtonStates() {
+ // disable download button when sync storage is empty
+ const bytesInUse = await browser.storage.sync.getBytesInUse();
+ const button = document.getElementById("cloudRestoreButton");
+ button.disabled = bytesInUse <= 0;
+}
+
+
+/**
+ * saves the current config as a json file
+ **/
+function onFileBackupButton () {
+ const manifest = browser.runtime.getManifest();
+ const linkElement = document.createElement("a");
+ linkElement.download = `${manifest.name} ${manifest.version} ${ new Date().toDateString() }.json`;
+ // creates a json file with the current config
+ linkElement.href = URL.createObjectURL(
+ new Blob([JSON.stringify(Config.get(), null, ' ')], {type: 'application/json'})
+ );
+ document.body.appendChild(linkElement);
+ linkElement.click();
+ document.body.removeChild(linkElement);
+}
+
+
+/**
+ * overwrites the current config with the selected config
+ * and resets all optional permissions
+ * reloads the options page afterwards
+ **/
+async function onFileRestoreButton (event) {
+ if (this.files[0].type !== "application/json") {
+ prompt("restoreAlertWrongFile");
+ // terminate function
+ return;
+ }
+
+ // catch rejected promises and errors
+ try {
+ // load file data
+ const json = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ try {
+ const restoredConfig = JSON.parse(reader.result);
+ resolve(restoredConfig)
+ }
+ catch (e) {
+ reject(e);
+ }
+ }
+ reader.onerror = reject;
+ reader.readAsText(this.files[0]);
+ });
+
+ const proceed = await new Promise((resolve) => {
+ // display popup because permission request requires user interaction
+ // also to ensure that the user really wants to override the current config
+ const popup = document.getElementById("restoreConfirm");
+ popup.addEventListener("close", async (event) => {
+ // if user declined exit function
+ if (!event.detail) return resolve(false);
+ // if optional permissions are required request them
+ const permissionsGranted = await requestPermissionsForConfig(json);
+ resolve(permissionsGranted);
+ }, { once: true });
+ popup.open = true;
+ });
+
+ if (proceed) {
+ Config.clear();
+ Config.set(json);
+ await prompt("restoreAlertSuccess");
+ // reload option page to update the ui
+ window.location.reload();
+ }
+ }
+ catch (e) {
+ prompt("restoreAlertNoConfigFile");
+ // terminate function
+ return;
+ }
+}
+
+
+/**
+ * uploads the current config to the sync storage
+ **/
+async function onCloudBackupButton () {
+ const bytesInUse = await browser.storage.sync.getBytesInUse();
+ // check whether there is any pre-existing data in the sync storage and warn user
+ if (bytesInUse > 0) {
+ const proceed = await prompt("uploadConfirm");
+ if (!proceed) return;
+ }
+ // create sync config manager and write current config to it
+ const cloudConfig = new ConfigManager({
+ storageArea: 'sync',
+ });
+ await cloudConfig.loaded;
+ cloudConfig.set(
+ Config.get(),
+ );
+ prompt("uploadAlertSuccess");
+}
+
+
+/**
+ * loads and applies the config from the sync storage
+ * reloads the options page afterwards
+ **/
+async function onCloudRestoreButton () {
+ const cloudConfig = new ConfigManager({
+ storageArea: 'sync',
+ });
+ await cloudConfig.loaded;
+ const json = cloudConfig.get();
+
+ const proceed = await new Promise((resolve) => {
+ // display popup because permission request requires user interaction
+ // also to ensure that the user really wants to override the current config
+ const popup = document.getElementById("downloadConfirm");
+ popup.addEventListener("close", async (event) => {
+ // if user declined exit function
+ if (!event.detail) return resolve(false);
+ // if optional permissions are required request them
+ const permissionsGranted = await requestPermissionsForConfig(json);
+ resolve(permissionsGranted);
+ }, { once: true });
+ popup.open = true;
+ });
+
+ if (proceed) {
+ Config.clear();
+ Config.set(json);
+ await prompt("downloadAlertSuccess");
+ // reload option page to update the ui
+ window.location.reload();
+ }
+}
+
+
+/**
+ * clears the current config so the defaults will be used
+ * resets all optional permissions
+ * reloads the options page afterwards
+ **/
+async function onConfigResetButton () {
+ const proceed = await prompt("resetConfirm");
+ if (proceed) {
+ const manifest = browser.runtime.getManifest();
+ await browser.permissions.remove(
+ { permissions: manifest.optional_permissions }
+ );
+ Config.clear();
+ // reload option page to update the ui
+ window.location.reload();
+ }
+}
+
+
+/**
+ * Collects and requests all permissions that are required for the given config file
+ * This returns a promise that will either fulfill with true or false
+ * If no permissions are required this fulfills with true
+ **/
+function requestPermissionsForConfig (json) {
+ // get the necessary permissions
+ const requiredPermissions = [];
+ // combine all commands to one array
+ const usedCommands = [];
+ if (json.Gestures && json.Gestures.length > 0) {
+ json.Gestures.forEach(gesture => usedCommands.push(gesture.command));
+ }
+ if (json.Settings && json.Settings.Rocker) {
+ if (json.Settings.Rocker.rightMouseClick) usedCommands.push(json.Settings.Rocker.rightMouseClick);
+ if (json.Settings.Rocker.leftMouseClick) usedCommands.push(json.Settings.Rocker.leftMouseClick);
+ }
+ if (json.Settings && json.Settings.Wheel) {
+ if (json.Settings.Wheel.wheelUp) usedCommands.push(json.Settings.Wheel.wheelUp);
+ if (json.Settings.Wheel.wheelDown) usedCommands.push(json.Settings.Wheel.wheelDown);
+ }
+
+ for (let command of usedCommands) {
+ const commandItem = CommandDefinitions.find((element) => {
+ return element.command === command.name;
+ });
+ if (commandItem.permissions) commandItem.permissions.forEach((permission) => {
+ if (!requiredPermissions.includes(permission)) requiredPermissions.push(permission);
+ });
+ }
+
+ // if optional permissions are required request them
+ if (requiredPermissions.length > 0) {
+ return browser.permissions.request({
+ permissions: requiredPermissions,
+ });
+ }
+ // if no permissions are required resolve to true
+ return Promise.resolve(true);
+}
+
+
+/**
+ * Helper function to open popups by their ids.
+ * Returns the result of the popup.
+ **/
+function prompt (popupId) {
+ return new Promise((resolve) => {
+ const popup = document.getElementById(popupId);
+ popup.addEventListener("close",
+ async (event) => resolve(event.detail),
+ { once: true },
+ );
+ popup.open = true;
+ });
+}
diff --git a/src/views/options/exclusions.mjs b/src/views/options/exclusions.mjs
deleted file mode 100644
index afff2de2d..000000000
--- a/src/views/options/exclusions.mjs
+++ /dev/null
@@ -1,162 +0,0 @@
-import { ContentLoaded, Config } from "/views/options/main.mjs";
-
-ContentLoaded.then(main);
-
-/**
- * main function
- * run code that depends on async resources
- **/
-function main () {
- const exclusionsContainer = document.getElementById('exclusionsContainer');
- exclusionsContainer.dataset.noEntriesHint = browser.i18n.getMessage('exclusionsHintNoEntries');
- const exclusionsForm = document.getElementById('exclusionsForm');
- exclusionsForm.onsubmit = onFormSubmit;
- exclusionsForm.elements.urlPattern.placeholder = browser.i18n.getMessage('exclusionsPlaceholderURL');
- exclusionsForm.elements.urlPattern.title = browser.i18n.getMessage('exclusionsPlaceholderURL');
- exclusionsForm.elements.urlPattern.onchange = onInputChange;
- // add existing exclusions entries
- for (const urlPattern of Config.get("Exclusions")) {
- const exclusionsEntry = createExclusionsEntry(urlPattern);
- exclusionsContainer.appendChild(exclusionsEntry);
- }
-}
-
-
-/**
- * Creates a exclusions entry html element by a given url pattern and returns it
- **/
-function createExclusionsEntry (urlPattern) {
- const exclusionsEntry = document.createElement('li');
- exclusionsEntry.classList.add('excl-entry');
- exclusionsEntry.dataset.urlPattern = urlPattern;
- exclusionsEntry.onclick = onEntryClick;
- const inputURLEntry = document.createElement('div');
- inputURLEntry.classList.add('excl-url-pattern');
- inputURLEntry.textContent = urlPattern;
- const deleteButton = document.createElement('button');
- deleteButton.type = "button";
- deleteButton.classList.add('excl-remove-button', 'icon-delete');
- exclusionsEntry.append(inputURLEntry, deleteButton);
- return exclusionsEntry;
-}
-
-
-/**
- * Adds a given exclusions entry element to the exclusions ui
- **/
-function addExclusionsEntry (exclusionsEntry) {
- const exclusionsContainer = document.getElementById('exclusionsContainer');
- // append entry, hide it and move it out of flow to calculate its dimensions
- exclusionsContainer.prepend(exclusionsEntry);
- exclusionsEntry.style.setProperty('visibility', 'hidden');
- exclusionsEntry.style.setProperty('position', 'absolute');
- // calculate total entry height
- const computedStyle = window.getComputedStyle(exclusionsEntry);
- const outerHeight = parseInt(computedStyle.marginTop) + exclusionsEntry.offsetHeight + parseInt(computedStyle.marginBottom);
-
- // move all entries up by one entry including the new one
- for (const node of exclusionsContainer.children) {
- node.style.setProperty('transform', `translateY(-${outerHeight}px)`);
- // remove ongoing transitions if existing
- node.style.removeProperty('transition');
- }
- // show new entry and bring it back to flow, which pushes all elements down by the height of one entry
- exclusionsEntry.style.removeProperty('visibility', 'hidden');
- exclusionsEntry.style.removeProperty('position', 'absolute');
-
- // trigger reflow
- exclusionsContainer.offsetHeight;
-
- exclusionsEntry.addEventListener('animationend', (event) => {
- event.currentTarget.classList.remove('excl-entry-animate-add');
- }, {once: true });
- exclusionsEntry.classList.add('excl-entry-animate-add');
-
- // move all entries down including the new one
- for (const node of exclusionsContainer.children) {
- node.addEventListener('transitionend', (event) => event.currentTarget.style.removeProperty('transition'), {once: true });
- node.style.setProperty('transition', 'transform 0.3s');
- node.style.removeProperty('transform');
- }
-}
-
-
-/**
- * Removes a given exclusions entry element from the exclusions ui
- **/
-function removeExclusionsEntry (exclusionsEntry) {
- // calculate total entry height
- const computedStyle = window.getComputedStyle(exclusionsEntry);
- const outerHeight = parseInt(computedStyle.marginTop) + exclusionsEntry.offsetHeight + parseInt(computedStyle.marginBottom);
-
- let node = exclusionsEntry.nextElementSibling;
- while (node) {
- node.addEventListener('transitionend', (event) => {
- event.currentTarget.style.removeProperty('transition');
- event.currentTarget.style.removeProperty('transform');
- }, {once: true });
- node.style.setProperty('transition', 'transform 0.3s');
- node.style.setProperty('transform', `translateY(-${outerHeight}px)`);
- node = node.nextElementSibling;
- }
- exclusionsEntry.addEventListener('animationend', (event) => event.currentTarget.remove(), {once: true });
- exclusionsEntry.classList.add('excl-entry-animate-remove');
-}
-
-
-/**
- * Handles the url pattern submit event
- * Adds the new url pattern to the config and calls the exclusions entry create function
- **/
-function onFormSubmit (event) {
- event.preventDefault();
- // remove spaces and cancel the function if the value is empty
- const urlPattern = this.elements.urlPattern.value.trim();
- if (!urlPattern) return;
- // create and add entry to the exclusions
- const exclusionsEntry = createExclusionsEntry(urlPattern);
- addExclusionsEntry(exclusionsEntry);
- // add new url pattern to the beginning of the array
- const exclusionsArray = Config.get("Exclusions");
- exclusionsArray.unshift(urlPattern);
- Config.set("Exclusions", exclusionsArray);
- // clear input field
- this.elements.urlPattern.value = '';
-}
-
-
-/**
- * Handles the url pattern input changes
- * Marks the field as invalide if the entry already exists
- **/
-function onInputChange () {
- if (Config.get("Exclusions").indexOf(this.value.trim()) !== -1) {
- this.setCustomValidity(browser.i18n.getMessage('exclusionsNotificationAlreadyExists'));
- }
- else if (this.validity.customError) this.setCustomValidity('');
-}
-
-
-/**
- * Handles the exclusions entry click
- * Calls the remove exclusions entry function on remove button click and removes it from the config
- **/
-function onEntryClick (event) {
- // if delete button received the click
- if (event.target.classList.contains('excl-remove-button')) {
- removeExclusionsEntry(this);
-
- const exclusionsForm = document.getElementById('exclusionsForm');
- // remove input field invaldility if it was previously a duplicate
- if (this.dataset.urlPattern === exclusionsForm.elements.urlPattern.value.trim()) {
- exclusionsForm.elements.urlPattern.setCustomValidity('');
- }
- const exclusionsArray = Config.get("Exclusions");
- // remove url pattern from array
- const index = exclusionsArray.indexOf(this.dataset.urlPattern);
- if (index !== -1) {
- exclusionsArray.splice(index, 1);
- Config.set("Exclusions", exclusionsArray);
- }
- }
-}
diff --git a/src/views/options/fragments/about.inc b/src/views/options/fragments/about.inc
index 126ff4a46..49e4f8b08 100644
--- a/src/views/options/fragments/about.inc
+++ b/src/views/options/fragments/about.inc
@@ -23,39 +23,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/views/options/fragments/data.inc b/src/views/options/fragments/data.inc
new file mode 100644
index 000000000..97a264698
--- /dev/null
+++ b/src/views/options/fragments/data.inc
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/options/fragments/exclusions.inc b/src/views/options/fragments/exclusions.inc
deleted file mode 100644
index 3bf1a55aa..000000000
--- a/src/views/options/fragments/exclusions.inc
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/src/views/options/gestures.mjs b/src/views/options/gestures.mjs
index 8e27dbd90..2ed1a1bc3 100644
--- a/src/views/options/gestures.mjs
+++ b/src/views/options/gestures.mjs
@@ -212,8 +212,8 @@ function addGestureListItem (gestureListItem) {
// prepend new entry, this pushes all elements by the height / width of one entry
gestureAddButtonItem.after(gestureListItem);
- // select all visible gesture items
- const gestureItems = gestureList.querySelectorAll(".gl-item:not([hidden])");
+ // select all gesture items
+ const gestureItems = gestureList.querySelectorAll(".gl-item");
// check if at least one node already exists
if (gestureItems.length > 0) {
@@ -267,6 +267,8 @@ function addGestureListItem (gestureListItem) {
gestureListItem.offsetHeight;
gestureListItem.style.setProperty('transition', 'transform .3s');
gestureListItem.style.transform = gestureListItem.style.transform.replace("scale(1.6)", "");
+ // hide new item in case search is active
+ onSearchInput();
}
@@ -287,6 +289,8 @@ function updateGestureListItem (gestureListItem, gesture) {
const commandField = gestureListItem.querySelector(".gl-command");
commandField.textContent = gesture.toString();
+ // hide updated item in case search is active
+ onSearchInput();
}
@@ -297,8 +301,8 @@ function removeGestureListItem (gestureListItem) {
const gestureList = document.getElementById("gestureContainer");
// get child index for current gesture item
const gestureItemIndex = Array.prototype.indexOf.call(gestureList.children, gestureListItem);
- // select all visible gesture items starting from given gesture item index
- const gestureItems = gestureList.querySelectorAll(`.gl-item:not([hidden]):nth-child(n + ${gestureItemIndex + 1})`);
+ // select all gesture items starting from given gesture item index
+ const gestureItems = gestureList.querySelectorAll(`.gl-item:nth-child(n + ${gestureItemIndex + 1})`);
// for performance improvements: read and cache all grid item positions first
const itemPositionCache = new Map();
@@ -426,7 +430,6 @@ function onItemPointerleave (event) {
**/
function onSearchInput () {
const gestureList = document.getElementById("gestureContainer");
- const gestureAddButtonItem = gestureList.firstElementChild;
const searchQuery = document.getElementById("gestureSearchInput").value.toLowerCase().trim();
const searchQueryKeywords = searchQuery.split(" ");
@@ -436,14 +439,10 @@ function onSearchInput () {
// check if all keywords are matching the command name
const isMatching = searchQueryKeywords.every(keyword => gestureString.includes(keyword));
// hide all unmatching commands and show all matching commands
- gestureListItem.hidden = !isMatching;
+ gestureListItem.classList.toggle("hidden", !isMatching);
}
- // hide gesture add button item on search input
- gestureAddButtonItem.hidden = !!searchQuery;
-
- // toggle "no search results" hint if all items are hidden
- gestureList.classList.toggle("empty", !gestureList.querySelectorAll(".gl-item:not([hidden])").length);
+ gestureList.classList.toggle("searching", !!searchQuery.length);
}
diff --git a/src/views/options/index.html b/src/views/options/index.html
index cbd98eead..8d491ceac 100644
--- a/src/views/options/index.html
+++ b/src/views/options/index.html
@@ -3,7 +3,7 @@
Gesturefy
-
+
@@ -14,13 +14,12 @@
-
-
+
diff --git a/src/views/options/layout.css b/src/views/options/layout.css
index eb238a01f..a1b2c7eba 100644
--- a/src/views/options/layout.css
+++ b/src/views/options/layout.css
@@ -158,6 +158,11 @@ h2 {
font-family: Icons;
}
+.icon-data::before {
+ content: "d";
+ font-family: Icons;
+}
+
.icon-arrows::before {
content: "a";
font-family: Icons;
@@ -355,14 +360,6 @@ h2 {
margin-bottom: 20px;
}
-.align-content-right {
- display: flex;
- justify-content: flex-end;
- flex-wrap: wrap;
- row-gap: 15px;
- column-gap: 15px;
-}
-
.justify-text {
text-align: justify;
}
@@ -598,11 +595,16 @@ label.button > span {
color: inherit;
}
-.button:hover {
+.button:not(:disabled):hover {
box-shadow: 0 0 10px -4px var(--shadow-color);
text-decoration: none;
}
+.button:disabled {
+ cursor: default;
+ opacity: 0.5;
+}
+
.button.danger:hover *,
.button.danger:hover {
border-color: var(--warning-color);
@@ -727,46 +729,6 @@ label.button > span {
color: var(--highlighted-color);
}
-.gesture-list::before {
- position: absolute;
- display: block;
- margin-top: 30vh;
- visibility: hidden;
- opacity: 0;
- color: var(--text-color);
- content: "m";
- font-family: Icons;
- text-align: center;
- font-size: 36px;
- grid-column: 1 / -1;
-}
-
-.gesture-list.empty::before {
- position: static;
- visibility: visible;
- opacity: .5;
- transition: opacity .3s;
-}
-
-.gesture-list::after {
- position: absolute;
- display: block;
- visibility: hidden;
- opacity: 0;
- color: var(--text-color);
- content: attr(data-no-results-hint);
- text-align: center;
- font-size: 22px;
- grid-column: 1 / -1;
-}
-
-.gesture-list.empty::after {
- position: static;
- visibility: visible;
- opacity: .5;
- transition: opacity .3s;
-}
-
/**
* Gesture list add button
**/
@@ -811,14 +773,16 @@ label.button > span {
background-color: var(--base-color);
box-shadow: none;
cursor: pointer;
- transition: box-shadow .3s;
+ transition: box-shadow .3s, opacity .3s;
}
-.gl-item[hidden] {
- display: none;
+.gl-item.hidden {
+ opacity: 0.2;
+ pointer-events: none;
}
-.gl-item:hover {
+.gl-item:hover,
+.searching > .gl-item:not(.hidden) {
box-shadow: 0 0 10px -4px var(--shadow-color);
}
@@ -1108,135 +1072,3 @@ label.button > span {
#gesturePopupPatternContainer .gl-thumbnail-arrow {
--arrowScale: 0.6;
}
-
-
-/**
- * Exclusions layout
- **/
-
-.excl-form {
- display: flex;
- align-items: stretch;
-}
-
-.excl-url-pattern-input {
- flex: 1;
- box-sizing: border-box;
- padding: 8px 10px 6px 10px;
- border: 1px solid var(--border-color);
- border-right: none;
- border-radius: 2px 0 0 2px;
- background: var(--base-color);
- color: var(--text-color);
- text-overflow: ellipsis ellipsis;
-}
-
-.excl-url-pattern-input:invalid {
- box-shadow: none;
-}
-
-.excl-add-button {
- padding: 0 20px;
- border-radius: 0 2px 2px 0;
- background: var(--highlighted-color);
- color: var(--textSecond-color);
- text-align: center;
- line-height: 100%;
-}
-
-.exclusions {
- padding-top: 20px;
-}
-
-.exclusions::after {
- display: block;
- visibility: hidden;
- opacity: 0;
- color: var(--text-color);
- content: attr(data-no-entries-hint);
- text-align: center;
- font-size: 18px;
-}
-
-.exclusions:empty::after {
- visibility: visible;
- opacity: .5;
- transition: opacity .3s;
-}
-
-.excl-entry {
- display: flex;
- align-items: center;
- box-sizing: border-box;
- padding-bottom: 10px;
- cursor: default;
-}
-
-.excl-entry:not(:last-child) {
- margin-bottom: 10px;
- border-bottom: 1px dashed var(--borderSecond-color);
-}
-
-.excl-url-pattern {
- flex: 1;
- padding-right: 10px;
- word-break: break-all;
-}
-
-.excl-remove-button {
- width: 18px;
- height: 18px;
- border-radius: 50%;
- background-color: var(--border-color);
- color: var(--base-color);
- text-align: center;
- font-size: 6px;
- font-family: Icons;
- line-height: 6px;
-}
-
-.excl-remove-button:hover {
- background-color: var(--warning-color);
-}
-
-.excl-entry-animate-add {
- z-index: -1;
- animation-name: animateAddEntry;
- animation-duration: .3s;
- animation-timing-function: ease;
-}
-
-@keyframes animateAddEntry {
- from {
- opacity: 0;
- }
- 50% {
- opacity: .3;
- }
- to {
- opacity: 1;
- }
-}
-
-.excl-entry-animate-remove {
- animation-name: animateDeleteEntry;
- animation-duration: .3s;
- animation-timing-function: ease;
-}
-
-@keyframes animateDeleteEntry {
- from {
- opacity: 1;
- transform: scale(1);
- }
- 30% {
- visibility: hidden;
- opacity: 0;
- transform: scale(.9);
- }
- to {
- visibility: hidden;
- opacity: 0;
- transform: scale(.9);
- }
-}
\ No newline at end of file
diff --git a/src/views/options/main.mjs b/src/views/options/main.mjs
index f7562a61e..190702cf5 100644
--- a/src/views/options/main.mjs
+++ b/src/views/options/main.mjs
@@ -1,9 +1,12 @@
import { fetchHTMLAsFragment } from "/core/utils/commons.mjs";
-import ConfigManager from "/core/helpers/config-manager.mjs";
+import ConfigManager from "/core/services/config-manager.mjs";
-export const Config = new ConfigManager("local", browser.runtime.getURL("/resources/json/defaults.json"));
+import DefaultConfig from "/resources/configs/defaults.mjs";
+export const Config = new ConfigManager({
+ defaults: DefaultConfig
+});
const Resources = [ Config.loaded ];
@@ -45,7 +48,7 @@ function main () {
input.checked = input.value === value;
}
else input.value = value;
- input.addEventListener('change', onChage);
+ input.addEventListener('change', onChange);
}
// toggle collapsables and add their event function
@@ -117,7 +120,7 @@ function onThemeButtonChange () {
/**
* save input value if valid
**/
- function onChage () {
+ function onChange () {
// check if valid, if there is no validity property check if value is set
if ((this.validity && this.validity.valid) || (!this.validity && this.value)) {
let value;
diff --git a/src/views/popup/index.html b/src/views/popup/index.html
new file mode 100644
index 000000000..cf1c9e854
--- /dev/null
+++ b/src/views/popup/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mozilla Add-ons
+
+
+
+ Github
+
+
+
\ No newline at end of file
diff --git a/src/views/popup/layout.css b/src/views/popup/layout.css
new file mode 100644
index 000000000..2791cc976
--- /dev/null
+++ b/src/views/popup/layout.css
@@ -0,0 +1,192 @@
+@font-face {
+ font-weight: normal;
+ font-style: normal;
+ font-family: "Icons";
+ src: url("/resources/fonts/icons.woff");
+}
+
+@font-face {
+ font-weight: normal;
+ font-style: normal;
+ font-family: "NunitoSans Regular";
+ src: url("/resources/fonts/NunitoSans-Regular.woff");
+}
+
+/* Light Theme */
+
+.light-theme:root, :root {
+ --base-color: #FBFBFB;
+ --text-color: #555555;
+ --surface-color: #ddd;
+ --on-surface-color: var(--text-color);
+ --shadowSecond-color:rgba(0,0,0,0.15);
+ --highlighted-color: #00AAA0;
+ --warning-color: #FF6347;
+}
+
+/* Dark Theme */
+
+.dark-theme:root {
+ --base-color: #252a32;
+ --text-color: #EBEBEB;
+ --surface-color: #464e5e;
+ --on-surface-color: var(--text-color);
+ --shadowSecond-color: rgba(200,200,200, 0.15);
+ --highlighted-color: #00AAA0;
+ --warning-color: #FF6347;
+}
+
+/* High Contrast Theme */
+
+.highContrast-theme:root {
+ --base-color: #000;
+ --text-color:#FFF;
+ --surface-color:#FFF;
+ --on-surface-color: var(--base-color);
+ --shadowSecond-color: rgba(255,255,255,0.15);
+ --highlighted-color: #00ff00;
+ --warning-color: #FF0000;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ border: none;
+ outline: none;
+}
+
+html {
+ font-family: "NunitoSans Regular", "Arial", sans-serif;
+ color: var(--text-color);
+}
+
+body {
+ background: var(--base-color);
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+ padding: 0.5em;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+/**
+ * Icons classes
+ **/
+
+.icon-gear::before {
+ content: "g";
+ font-family: Icons;
+ vertical-align: top;
+}
+
+.icon-info::before {
+ content: "I";
+ font-family: Icons;
+}
+
+.favicon::before {
+ content: '';
+ display: block;
+ background-image: var(--favicon-url);
+ width: 1em;
+ height: 1em;
+ background-size: contain;
+}
+
+.favicon.favicon-amo::before {
+ background-image: url('https://addons.mozilla.org/favicon.ico?v=2');
+}
+
+.favicon.favicon-github::before {
+ background-image: url('https://github.githubassets.com/favicons/favicon.svg');
+}
+
+.highContrast-theme:root .favicon.favicon-github::before,
+.dark-theme:root .favicon.favicon-github::before {
+ background-image: url('https://github.githubassets.com/favicons/favicon-dark.svg');
+}
+
+/**
+ * Item list
+ **/
+
+hr {
+ border-bottom: solid 1px var(--surface-color);
+ margin: 0.5em 0em;
+}
+
+.item {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ column-gap: 0.5em;
+ padding: 0 0.7em;
+ border-radius: 0.2em;
+ background: none;
+ color: var(--text-color);
+ font-weight: normal;
+ text-decoration: none;
+ font-size: 1em;
+ line-height: 1em;
+ text-align: left;
+}
+
+.item:hover,
+.item:focus-visible {
+ background: var(--surface-color);
+ text-decoration: none;
+ color: var(--on-surface-color);
+}
+
+.item > span:last-of-type {
+ padding: 0.7em 0;
+ flex: 1;
+}
+
+a.item::after {
+ content: "E";
+ text-decoration: none;
+ font-family: Icons;
+ font-size: 0.75em;
+}
+
+.item.warning {
+ color: var(--warning-color);
+}
+
+/**
+ * Toggle button
+ **/
+
+input[type="checkbox"] {
+ appearance: none;
+ height: 1rem;
+ padding: 0.15em;
+ box-sizing: content-box;
+ aspect-ratio: 2 / 1;
+ border-radius: 2rem;
+ background-color: var(--surface-color);
+ box-shadow: inset 0px 0px 3px var(--shadowSecond-color);
+ transition: background-color 0.2s;
+}
+
+input[type="checkbox"]::before {
+ content: "";
+ display: block;
+ border-radius: 50%;
+ height: 100%;
+ aspect-ratio: 1 / 1;
+ background: var(--base-color);
+ transition: transform 0.2s ease;
+}
+
+input[type="checkbox"]:checked {
+ background-color: var(--highlighted-color);
+}
+
+input[type="checkbox"]:checked::before {
+ transform: translateX(100%)
+}
\ No newline at end of file
diff --git a/src/views/popup/main.mjs b/src/views/popup/main.mjs
new file mode 100644
index 000000000..4ded958cd
--- /dev/null
+++ b/src/views/popup/main.mjs
@@ -0,0 +1,134 @@
+import { getActiveTab } from "/core/utils/commons.mjs";
+
+import ExclusionService from "/core/services/exclusion-service.mjs";
+
+import HostPermissionService from "/core/services/host-permission-service.mjs";
+
+import ConfigManager from "/core/services/config-manager.mjs";
+
+import DefaultConfig from "/resources/configs/defaults.mjs";
+
+const Config = new ConfigManager({
+ defaults: DefaultConfig
+});
+
+const Exclusions = new ExclusionService();
+
+const HostPermissions = new HostPermissionService();
+
+Promise.all([
+ getActiveTab(),
+ Config.loaded,
+ Exclusions.loaded,
+]).then(main);
+
+let activeTab;
+
+function main(args) {
+ [activeTab] = args;
+ // insert text from language files
+ for (let element of document.querySelectorAll('[data-i18n]')) {
+ element.textContent = browser.i18n.getMessage(element.dataset.i18n);
+ }
+ // register permission change handler and run it initially
+ HostPermissions.addEventListener('change', onPermissionChange);
+ Exclusions.addEventListener('change', onPermissionChange);
+ onPermissionChange();
+ // apply theme class
+ const themeValue = Config.get("Settings.General.theme");
+ document.documentElement.classList.add(`${themeValue}-theme`);
+ // register button event listeners
+ const settingsButton = document.getElementById('settingsButton');
+ settingsButton.addEventListener('click', openSettings);
+ const permissionRequestButton = document.getElementById('permissionRequestButton');
+ permissionRequestButton.title = browser.i18n.getMessage('popupMissingPermissionButtonTooltip');
+ permissionRequestButton.addEventListener('click', HostPermissions.requestGlobalPermission);
+ const restrictedPageWarningText = document.getElementById('restrictedPageWarningText');
+ // omit passing the short url here because we only reliably get the url for tabs where the add-on has host permissions
+ // we never get host permissions for e.g. about: or moz-extension: so we cannot retrieve/show the url
+ // we would require the "tabs" permission to consistently retrieve all urls
+ restrictedPageWarningText.textContent = browser.i18n.getMessage('popupProhibitedPageWarning');
+ const domainActivationButton = document.getElementById('domainActivationButton');
+ domainActivationButton.style.setProperty('--favicon-url', `url(${activeTab.favIconUrl})`);
+ const domainActivationButtonText = document.getElementById('domainActivationButtonText');
+ domainActivationButtonText.textContent = browser.i18n.getMessage(
+ 'popupExclusionsToggleButton', toShortURL(activeTab.url)
+ );
+ // use click instead of change to prevent default
+ const domainActivationButtonToggle = document.getElementById('domainActivationButtonToggle');
+ domainActivationButtonToggle.addEventListener('click', onDomainToggle);
+}
+
+// handlers \\
+
+async function onPermissionChange() {
+ const [
+ _hasGlobalPermission,
+ _hasTabPermission,
+ ] = await Promise.all([
+ HostPermissions.hasGlobalPermission(),
+ HostPermissions.hasTabPermission(activeTab),
+ ]);
+
+ // warnings:
+ let hasWarning = false;
+
+ const permissionRequestButton = document.getElementById('permissionRequestButton');
+ permissionRequestButton.hidden = _hasGlobalPermission;
+ hasWarning ||= !permissionRequestButton.hidden;
+
+ const restrictedPageWarning = document.getElementById('restrictedPageWarning');
+ restrictedPageWarning.hidden = hasWarning || _hasTabPermission;
+ hasWarning ||= !restrictedPageWarning.hidden;
+
+ // exclusion toggle (only show when no warnings):
+ const isActive = Exclusions.isEnabledFor(activeTab.url);
+ const domainActivationButton = document.getElementById('domainActivationButton');
+ domainActivationButton.hidden = hasWarning;
+ domainActivationButton.title = browser.i18n.getMessage(
+ isActive
+ ? 'popupExclusionsToggleButtonOnTooltip'
+ : 'popupExclusionsToggleButtonOffTooltip',
+ toShortURL(activeTab.url)
+ );
+ const domainActivationButtonToggle = document.getElementById('domainActivationButtonToggle');
+ domainActivationButtonToggle.checked = isActive;
+}
+
+function onDomainToggle(event) {
+ if (Exclusions.isEnabledFor(activeTab.url)) {
+ Exclusions.disableFor(activeTab.url);
+ }
+ else {
+ Exclusions.enableFor(activeTab.url);
+ }
+ event.preventDefault();
+}
+
+// methods \\
+
+function openSettings() {
+ browser.runtime.openOptionsPage();
+ window.close();
+}
+
+function toShortURL(url) {
+ try {
+ url = new URL(url);
+ }
+ catch(e) {
+ return url;
+ }
+ if (url.protocol === 'about:') {
+ return url.protocol + url.pathname;
+ }
+ else if (url.protocol === 'moz-extension:') {
+ return url.protocol;
+ }
+ else if (url.protocol === 'chrome:') {
+ return url.origin;
+ }
+ else {
+ return url.hostname || url.origin;
+ }
+}