diff --git a/i18n/english.js b/i18n/english.js index 9391d679..3ecf258a 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -219,7 +219,15 @@ const ui = { blockquote: "Click on hotkey to update", goto: "Goto", openCloseWiki: "Open/Close wiki", - lock: "Lock/Unlock network" + lock: "Lock/Unlock network", + views: { + home: "Home view", + network: "Network view", + search: "Search view", + settings: "Settings view", + tree: "Tree view", + warnings: "Warnings view" + } } }, network: { diff --git a/i18n/french.js b/i18n/french.js index 58d79131..1744b5b2 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -219,7 +219,15 @@ const ui = { blockquote: "Cliquer sur le raccourci clavier pour mettre à jour", goto: "Ouvrir", openCloseWiki: "Ouverture/Fermeture du wiki", - lock: "Verrouiller/Déverrouiller le réseau" + lock: "Verrouiller/Déverrouiller le réseau", + views: { + home: "vue Home", + network: "vue Réseau", + search: "vue Recherche", + settings: "vue Paramètres", + tree: "vue Arbre", + warnings: "vue Avertissements" + } } }, network: { diff --git a/public/components/views/settings/settings.js b/public/components/views/settings/settings.js index fcffb3f7..8de1967a 100644 --- a/public/components/views/settings/settings.js +++ b/public/components/views/settings/settings.js @@ -1,10 +1,12 @@ // Import Third-party Dependencies +import { LitElement, html, css, nothing } from "lit"; import { getJSON } from "@nodesecure/vis-network"; import { warnings } from "@nodesecure/js-x-ray/warnings"; +import { getManifest } from "@nodesecure/flags/web"; // Import Internal Dependencies -import * as utils from "../../../common/utils.js"; import { EVENTS } from "../../../core/events.js"; +import { currentLang } from "../../../common/utils.js"; // CONSTANTS const kAllowedHotKeys = new Set([ @@ -23,236 +25,561 @@ const kDefaultHotKeys = { warnings: "A" }; const kShortcutInputTargetIds = new Set(Object.keys(kDefaultHotKeys)); - -export class Settings { +const kIgnorableFlags = new Set([ + "hasManyPublishers", + "hasIndirectDependencies", + "hasMissingOrUnusedDependency", + "isDead", + "isOutdated", + "hasDuplicate" +]); +const kFlags = Object.values(getManifest()) + .filter(({ title }) => kIgnorableFlags.has(title)) + .map(({ title, emoji }) => { + return { value: title, emoji }; + }); +const kShortcuts = [ + { id: "home", labelKey: "goto", viewKey: "home" }, + { id: "network", labelKey: "goto", viewKey: "network" }, + { id: "search", labelKey: "goto", viewKey: "search" }, + { id: "settings", labelKey: "goto", viewKey: "settings" }, + { id: "tree", labelKey: "goto", viewKey: "tree" }, + { id: "warnings", labelKey: "goto", viewKey: "warnings" }, + { id: "wiki", labelKey: "openCloseWiki", viewKey: null }, + { id: "lock", labelKey: "lock", viewKey: null } +]; + +export class SettingsView extends LitElement { static defaultMenuName = "info"; - constructor() { - this.#generateWarningCheckboxes(); - this.saveEnabled = false; - this.dom = { - /** @type {HTMLSelectElement} */ - defaultPackageMenu: document.getElementById("default_package_menu"), - /** @type {HTMLInputElement[]} */ - warningsCheckbox: document.querySelectorAll("input[name='warnings']"), - /** @type {HTMLInputElement[]} */ - flagsCheckbox: document.querySelectorAll("input[name='flags']"), - /** @type {HTMLInputElement} */ - shortcutsSection: document.querySelector(".shortcuts"), - /** @type {HTMLInputElement} */ - showFriendlyDependenciesCheckbox: document.querySelector("#show-friendly"), - themeSelector: document.querySelector("#theme_selector"), - disableExternalRequestsCheckbox: document.querySelector("#disable-external") - }; + static properties = { + config: { attribute: false }, + _saveEnabled: { state: true }, + _hotkeys: { state: true } + }; + + static styles = css` + :host { + flex-direction: column; + z-index: 10; + padding: 60px; + box-sizing: border-box; + height: 100%; + overflow-y: auto; + } - this.saveButton = document.querySelector(".save"); - this.saveButton.addEventListener("click", () => this.saveSettings().catch(console.error)); - this.saveButton.classList.add("disabled"); - - this.dom.defaultPackageMenu.addEventListener("change", () => this.enableSaveButton()); - const formFields = [ - ...this.dom.warningsCheckbox, - ...this.dom.flagsCheckbox, - this.dom.showFriendlyDependenciesCheckbox, - this.dom.themeSelector, - this.dom.disableExternalRequestsCheckbox - ]; - for (const formField of formFields) { - formField.addEventListener("change", () => this.enableSaveButton()); - } - - const self = this; - this.dom.shortcutsSection.querySelectorAll(".hotkey").forEach((input) => { - input.addEventListener("click", () => { - if (!input.readOnly) { - return; - } + :host > h1, + :host h2 { + height: 40px; + border-bottom: 2px solid var(--primary); + margin-bottom: 20px; + display: flex; + align-items: center; + color: var(--primary); + font-size: 24px; + font-family: mononoki; + } - const currentValue = input.value; - input.readOnly = false; - input.value = ""; - - const onKeyDown = (event) => { - if (kShortcutInputTargetIds.has(event.target.id) === false) { - return; - } - - // Prevent the app to change view if key is equal to view's hotkey - event.preventDefault(); - event.stopPropagation(); - - function setValue(value) { - input.value = value; - input.readOnly = true; - input.blur(); - input.removeEventListener("keydown", onKeyDown); - self.updateHotKeys(); - } - if (event.key === currentValue) { - setValue(currentValue); - } - - if (kAllowedHotKeys.has(event.key.toLowerCase())) { - const isHotKeyAlreadyUsed = [...this.dom.shortcutsSection.querySelectorAll(".hotkey")] - .find((input) => input.value === event.key.toUpperCase()); - - setValue(isHotKeyAlreadyUsed ? currentValue : event.key.toUpperCase()); - } - }; - input.addEventListener("keydown", onKeyDown); - }); - }); + :host-context(body.dark) h1, + :host-context(body.dark) h2 { + color: var(--dark-theme-secondary-color); + border-bottom: 2px solid var(--dark-theme-secondary-color); + } + + :host h2 { + margin-top: 30px; + } + + :host .icon-keyboard { + background: url("../../../img/keyboard-solid.svg"); + background-position: 10% center; + background-repeat: no-repeat; + width: 34px; + height: 20px; + margin-right: 2px; + filter: invert(14%) sepia(80%) saturate(5663%) hue-rotate(252deg) brightness(69%) contrast(98%); + } + + :host-context(body.dark) .icon-keyboard { + filter: invert(75%) sepia(81%) saturate(3055%) hue-rotate(178deg) brightness(86%) contrast(88%); + } + + :host > h1 i { + margin-right: 5px; + } + + :host > form { + display: flex; + flex-wrap: wrap; + } + + :host > form .line { + display: flex; + flex-basis: 340px; + flex-direction: column; + min-height: 30px; + margin-bottom: 30px; + padding-right: 30px; + box-sizing: border-box; + } + + :host > form .line p, + :host > form .line label { + font-size: 15px; + color: #4a5e68; + letter-spacing: 0.5px; + } + + :host-context(body.dark) form .line p, + :host-context(body.dark) form .line label { + color: var(--dark-theme-secondary-lighter); + } + + :host > form .line > p, + :host > form .line > label { + margin-bottom: 6px; + font-weight: bold; + } + + :host .shortcuts div:nth-child(n+1) { + margin-top: 10px; + } + + :host .shortcuts .note { + border-left: 3px solid #01579B; + padding: 10px 15px; + background: #81d4fa59; + color: #283593; + font-weight: 400; + border-radius: 2px; + box-sizing: border-box; + margin-bottom: 20px; + } + + :host-context(body.dark) .shortcuts .note { + background: var(--dark-theme-accent-darker); + color: white; + } + + :host .shortcuts label { + color: #4a5e68; + margin-left: 10px; + font-weight: 500; + } + + :host-context(body.dark) .shortcuts label { + color: var(--dark-theme-secondary-lighter); + } + + :host .shortcuts input:read-only { + background: transparent; + border-color: rgb(168 168 168); + color: rgb(141 140 140); + border-style: solid; + } + + :host-context(body.dark) .shortcuts input:read-only { + border-color: var(--dark-theme-secondary-lighter); + color: var(--dark-theme-secondary-lighter); + } + + :host .shortcuts input { + width: 36px; + height: 36px; + border-radius: 6px; + text-align: center; + font-family: system-ui; + font-size: 20px; + font-weight: 500; + border-bottom-width: 4px; + } + + :host > form .line > div { + display: flex; + height: 22px; + color: #334148; + align-items: center; + margin-left: 10px; + } + + :host-context(body.dark) form .line > div { + color: var(--dark-theme-secondary-lighter); + } + + :host > form .line select { + margin-left: 10px; + } + + :host > form .line input[type="checkbox"] { + margin-left: 0; + } + + :host > form .line > div + div { + margin-top: 5px; + } + + :host > form .line > div > p { + margin-left: 5px; + } + + input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + + label { + color: #4a5e68; + } + + button.save { + height: 30px; + width: 100px; + background: #27a845; + color: #FFF; + border-radius: 4px; + margin-top: 30px; + border: none; + outline: none; + font-family: mononoki; + font-weight: bold; + letter-spacing: 0.5px; + text-shadow: 2px 2px 5px #00000061; + } + + button.save.disabled { + background: #334148; + opacity: 0.35; + } + + button.save:not(.disabled):hover { + background-color: var(--secondary-darker); + cursor: pointer; + } + + select { + max-width: 200px; + border: 1px solid #B0BEC5; + height: 30px; + border-radius: 4px; + font-family: mononoki; + color: #0c5a9b; + } + + :host-context(body.dark) select { + color: var(--dark-theme-secondary-lighter); + border: var(--dark-theme-secondary-darker) 1px solid; + background: var(--dark-theme-primary-darker); + } + + .settings-line-title { + font-size: 15px; + color: #4a5e68; + letter-spacing: 0.5px; + margin: 30px 0 6px; + font-weight: bold; + } + + :host-context(body.dark) .settings-line-title { + color: var(--dark-theme-secondary-color); + } + + .mt-10 { + margin-top: 10px; + } + `; + + constructor() { + super(); + this.config = null; + this._saveEnabled = false; - if (localStorage.getItem("hotkeys") === null) { + const storedHotkeys = localStorage.getItem("hotkeys"); + if (storedHotkeys === null) { localStorage.setItem("hotkeys", JSON.stringify(kDefaultHotKeys)); + this._hotkeys = { ...kDefaultHotKeys }; + } + else { + this._hotkeys = JSON.parse(storedHotkeys); } + } - const hotkeys = JSON.parse(localStorage.getItem("hotkeys")); - this.updateNavigationHotKey(hotkeys); - this.updateFormHotKeys(hotkeys); + firstUpdated() { + this.updateNavigationHotKey(this._hotkeys); } - #generateWarningCheckboxes() { - const warningsSettings = document.getElementById("warnings-settings"); - const checkboxes = Object.keys(warnings).map((id) => utils.createDOMElement("div", { - childs: [ - utils.createDOMElement("input", { - attributes: { - id, - value: id, - type: "checkbox", - checked: true, - name: "warnings" - } - }), - utils.createDOMElement("label", { - attributes: { - for: id - }, - text: id.replaceAll("-", " ") - }) - ] - }) - ); - warningsSettings.append(...checkboxes); + async fetchUserConfig() { + const config = await getJSON("/config"); + this.setNewConfig(config); + + return this; + } + + setNewConfig(config) { + this.config = { + ...config, + ignore: { + warnings: new Set(config.ignore.warnings), + flags: new Set(config.ignore.flags) + }, + theme: config.theme ?? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") + }; } updateNavigationHotKey(hotkeys) { const navigationElement = document.getElementById("view-navigation"); - navigationElement.querySelectorAll("span").forEach((span) => { + if (navigationElement === null) { + return; + } + + for (const span of navigationElement.querySelectorAll("span")) { // network--view -> network const viewName = span.parentElement.getAttribute("data-menu").split("--")[0]; - const hotkey = hotkeys[viewName]; - span.textContent = hotkey; - }); + span.textContent = hotkeys[viewName]; + } } - updateFormHotKeys(hotkeys) { - const hotkeysInputs = [...this.dom.shortcutsSection.querySelectorAll(".hotkey")]; - - for (const input of hotkeysInputs) { - const viewName = input.getAttribute("id"); - const hotkey = hotkeys[viewName]; - input.value = hotkey; - } + #enableSaveButton() { + this._saveEnabled = true; } - updateHotKeys() { + #updateHotKeys() { const hotkeys = {}; - const hotkeysInputs = this.dom.shortcutsSection.querySelectorAll(".hotkey"); - - for (const input of hotkeysInputs) { - const hotkeyName = input.getAttribute("id"); - hotkeys[hotkeyName] = input.value; + for (const input of this.renderRoot.querySelectorAll(".hotkey")) { + hotkeys[input.id] = input.value; } + this._hotkeys = hotkeys; this.updateNavigationHotKey(hotkeys); localStorage.setItem("hotkeys", JSON.stringify(hotkeys)); } - enableSaveButton() { - if (this.saveButton.classList.contains("disabled")) { - this.saveButton.classList.remove("disabled"); + #onHotkeyClick(event) { + const input = event.currentTarget; + if (!input.readOnly) { + return; } - } - setNewConfig(config) { - this.config = config; - this.warnings = new Set(this.config.ignore.warnings); - this.flags = new Set(this.config.ignore.flags); - this.config.ignore.warnings = this.warnings; - this.config.ignore.flags = this.flags; - // this.config.theme = config.theme ?? window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - if (this.config.theme === void 0) { - this.config.theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - } - } + const currentValue = input.value; + input.readOnly = false; + input.value = ""; - async fetchUserConfig() { - const config = await getJSON("/config"); - this.setNewConfig(config); - this.updateSettings(); + const onKeyDown = (keyEvent) => { + if (!kShortcutInputTargetIds.has(keyEvent.target.id)) { + return; + } - return this; + // Prevent the app to change view if key is equal to view's hotkey + keyEvent.preventDefault(); + keyEvent.stopPropagation(); + + const setValue = (value) => { + input.value = value; + input.readOnly = true; + input.blur(); + input.removeEventListener("keydown", onKeyDown); + this.#updateHotKeys(); + }; + + if (keyEvent.key === currentValue) { + setValue(currentValue); + + return; + } + + if (kAllowedHotKeys.has(keyEvent.key.toLowerCase())) { + const isHotKeyAlreadyUsed = [...this.renderRoot.querySelectorAll(".hotkey")] + .find((existingInput) => existingInput.value === keyEvent.key.toUpperCase()); + + setValue(isHotKeyAlreadyUsed ? currentValue : keyEvent.key.toUpperCase()); + } + }; + input.addEventListener("keydown", onKeyDown); } - async saveSettings() { - if (this.saveButton.classList.contains("disabled")) { + async #saveSettings() { + if (!this._saveEnabled) { return; } + const defaultPackageMenu = this.renderRoot.querySelector("#default_package_menu"); + const themeSelector = this.renderRoot.querySelector("#theme_selector"); + const showFriendly = this.renderRoot.querySelector("#show-friendly"); + const disableExternal = this.renderRoot.querySelector("#disable-external"); + const newConfig = { - defaultPackageMenu: this.dom.defaultPackageMenu.value || Settings.defaultMenuName, + defaultPackageMenu: defaultPackageMenu.value || SettingsView.defaultMenuName, ignore: { flags: new Set(), warnings: new Set() }, - showFriendlyDependencies: this.dom.showFriendlyDependenciesCheckbox.checked, - theme: this.dom.themeSelector.value, - disableExternalRequests: this.dom.disableExternalRequestsCheckbox.checked + showFriendlyDependencies: showFriendly.checked, + theme: themeSelector.value, + disableExternalRequests: disableExternal.checked }; - for (const checkbox of this.dom.warningsCheckbox) { + for (const checkbox of this.renderRoot.querySelectorAll("input[name='warnings']")) { if (checkbox.checked) { - newConfig.ignore.warnings.add(checkbox.getAttribute("value")); + newConfig.ignore.warnings.add(checkbox.value); } } - for (const checkbox of this.dom.flagsCheckbox) { + for (const checkbox of this.renderRoot.querySelectorAll("input[name='flags']")) { if (checkbox.checked) { - newConfig.ignore.flags.add(checkbox.getAttribute("value")); + newConfig.ignore.flags.add(checkbox.value); } } - newConfig.ignore.warnings = [...newConfig.ignore.warnings]; - newConfig.ignore.flags = [...newConfig.ignore.flags]; - await fetch("/config", { method: "put", - body: JSON.stringify(newConfig), + body: JSON.stringify({ + ...newConfig, + ignore: { + warnings: [...newConfig.ignore.warnings], + flags: [...newConfig.ignore.flags] + } + }), headers: { "content-type": "application/json" } }); + this.config = newConfig; - this.saveButton.classList.add("disabled"); + this._saveEnabled = false; window.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_SAVED, { detail: this.config })); } - updateSettings() { - this.dom.defaultPackageMenu.value = this.config.defaultPackageMenu; - this.dom.themeSelector.value = this.config.theme; + #renderWarningCheckboxes() { + return Object.keys(warnings).map((id) => html` +