diff options
| -rw-r--r-- | ext/css/material.css | 9 | ||||
| -rw-r--r-- | ext/css/settings.css | 6 | ||||
| -rw-r--r-- | ext/js/display/display.js | 7 | ||||
| -rw-r--r-- | ext/js/display/option-toggle-hotkey-handler.js | 164 | ||||
| -rw-r--r-- | ext/js/pages/settings/keyboard-shortcuts-controller.js | 51 | ||||
| -rw-r--r-- | ext/popup.html | 1 | ||||
| -rw-r--r-- | ext/search.html | 1 | ||||
| -rw-r--r-- | ext/settings.html | 8 | 
8 files changed, 235 insertions, 12 deletions
| diff --git a/ext/css/material.css b/ext/css/material.css index efa5a730..703f1268 100644 --- a/ext/css/material.css +++ b/ext/css/material.css @@ -177,6 +177,15 @@  } +/* Text styles */ +.light { +    color: var(--text-color-light2); +} +.danger-text { +    color: var(--danger-color); +} + +  /* Icons */  .icon {      --icon-image: none; diff --git a/ext/css/settings.css b/ext/css/settings.css index 9701aa56..e2485925 100644 --- a/ext/css/settings.css +++ b/ext/css/settings.css @@ -158,15 +158,9 @@ pre {  /* Text styles */ -.light { -    color: var(--text-color-light2); -}  .warning-text {      color: var(--warning-color);  } -.danger-text { -    color: var(--danger-color); -}  /* Headings */ diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 3ae55da0..ee2448d6 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -27,6 +27,7 @@   * Frontend   * HotkeyHelpController   * MediaLoader + * OptionToggleHotkeyHandler   * PopupFactory   * PopupMenu   * QueryParser @@ -112,6 +113,7 @@ class Display extends EventDispatcher {          this._ankiNoteNotification = null;          this._ankiNoteNotificationEventListeners = null;          this._queryPostProcessor = null; +        this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this);          this._hotkeyHandler.registerActions([              ['close',             () => { this._onHotkeyClose(); }], @@ -201,6 +203,10 @@ class Display extends EventDispatcher {          return this._parentPopupId;      } +    get notificationContainer() { +        return this._footerNotificationContainer; +    } +      async prepare() {          // State setup          const {documentElement} = document; @@ -213,6 +219,7 @@ class Display extends EventDispatcher {          this._displayAudio.prepare();          this._queryParser.prepare();          this._history.prepare(); +        this._optionToggleHotkeyHandler.prepare();          // Event setup          this._history.on('stateChanged', this._onStateChanged.bind(this)); diff --git a/ext/js/display/option-toggle-hotkey-handler.js b/ext/js/display/option-toggle-hotkey-handler.js new file mode 100644 index 00000000..fae17f8d --- /dev/null +++ b/ext/js/display/option-toggle-hotkey-handler.js @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2021  Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +/* global + * DisplayNotification + */ + +class OptionToggleHotkeyHandler { +    constructor(display) { +        this._display = display; +        this._notification = null; +        this._notificationHideTimer = null; +        this._notificationHideTimeout = 5000; +    } + +    get notificationHideTimeout() { +        return this._notificationHideTimeout; +    } + +    set notificationHideTimeout(value) { +        this._notificationHideTimeout = value; +    } + +    prepare() { +        this._display.hotkeyHandler.registerActions([ +            ['toggleOption', this._onHotkeyActionToggleOption.bind(this)] +        ]); +    } + +    // Private + +    _onHotkeyActionToggleOption(argument) { +        this._toggleOption(argument); +    } + +    async _toggleOption(path) { +        let value; +        try { +            const optionsContext = this._display.getOptionsContext(); + +            const result = (await yomichan.api.getSettings([{ +                scope: 'profile', +                path, +                optionsContext +            }]))[0]; +            const {error} = result; +            if (typeof error !== 'undefined') { +                throw deserializeError(error); +            } + +            value = result.result; +            if (typeof value !== 'boolean') { +                throw new Error(`Option value of type ${typeof value} cannot be toggled`); +            } + +            value = !value; + +            const result2 = (await yomichan.api.modifySettings([{ +                scope: 'profile', +                action: 'set', +                path, +                value, +                optionsContext +            }]))[0]; +            const {error: error2} = result2; +            if (typeof error2 !== 'undefined') { +                throw deserializeError(error2); +            } + +            this._showNotification(this._createSuccessMessage(path, value), true); +        } catch (e) { +            this._showNotification(this._createErrorMessage(path, e), false); +        } +    } + +    _createSuccessMessage(path, value) { +        const fragment = document.createDocumentFragment(); +        const n1 = document.createElement('em'); +        n1.textContent = path; +        const n2 = document.createElement('strong'); +        n2.textContent = value; +        fragment.appendChild(document.createTextNode('Option ')); +        fragment.appendChild(n1); +        fragment.appendChild(document.createTextNode(' changed to ')); +        fragment.appendChild(n2); +        return fragment; +    } + +    _createErrorMessage(path, error) { +        let message; +        try { +            ({message} = error); +        } catch (e) { +            // NOP +        } +        if (typeof message !== 'string') { +            message = `${error}`; +        } + +        const fragment = document.createDocumentFragment(); +        const n1 = document.createElement('em'); +        n1.textContent = path; +        const n2 = document.createElement('div'); +        n2.textContent = message; +        n2.className = 'danger-text'; +        fragment.appendChild(document.createTextNode('Failed to toggle option ')); +        fragment.appendChild(n1); +        fragment.appendChild(document.createTextNode(': ')); +        fragment.appendChild(n2); +        return fragment; +    } + +    _showNotification(message, autoClose) { +        if (this._notification === null) { +            const node = this._display.displayGenerator.createEmptyFooterNotification(); +            node.addEventListener('click', this._onNotificationClick.bind(this), false); +            this._notification = new DisplayNotification(this._display.notificationContainer, node); +        } + +        this._notification.setContent(message); +        this._notification.open(); + +        this._stopHideNotificationTimer(); +        if (autoClose) { +            this._notificationHideTimer = setTimeout(this._onNotificationHideTimeout.bind(this), this._notificationHideTimeout); +        } +    } + +    _hideNotification(animate) { +        if (this._notification === null) { return; } +        this._notification.close(animate); +        this._stopHideNotificationTimer(); +    } + +    _stopHideNotificationTimer() { +        if (this._notificationHideTimer !== null) { +            clearTimeout(this._notificationHideTimer); +            this._notificationHideTimer = null; +        } +    } + +    _onNotificationHideTimeout() { +        this._notificationHideTimer = null; +        this._hideNotification(true); +    } + +    _onNotificationClick() { +        this._stopHideNotificationTimer(); +    } +} diff --git a/ext/js/pages/settings/keyboard-shortcuts-controller.js b/ext/js/pages/settings/keyboard-shortcuts-controller.js index 7dbf5aa2..aeff15b6 100644 --- a/ext/js/pages/settings/keyboard-shortcuts-controller.js +++ b/ext/js/pages/settings/keyboard-shortcuts-controller.js @@ -18,6 +18,7 @@  /* global   * DOMDataBinder   * KeyboardMouseInputField + * ObjectPropertyAccessor   */  class KeyboardShortcutController { @@ -50,7 +51,8 @@ class KeyboardShortcutController {              ['playAudio',                        {scopes: new Set(['popup', 'search'])}],              ['playAudioFromSource',              {scopes: new Set(['popup', 'search']), argument: {template: 'hotkey-argument-audio-source', default: 'jpod101'}}],              ['copyHostSelection',                {scopes: new Set(['popup'])}], -            ['scanSelectedText',                 {scopes: new Set(['web'])}] +            ['scanSelectedText',                 {scopes: new Set(['web'])}], +            ['toggleOption',                     {scopes: new Set(['popup', 'search']), argument: {template: 'hotkey-argument-setting-path', default: ''}}]          ]);      } @@ -316,14 +318,13 @@ class KeyboardShortcutHotkeyEntry {      _onArgumentValueChange(template, e) {          const node = e.currentTarget; -        const value = this._getArgumentInputValue(node); -        let newValue = value; +        let value = this._getArgumentInputValue(node);          switch (template) {              case 'hotkey-argument-move-offset': -                newValue = `${DOMDataBinder.convertToNumber(value, node)}`; +                value = `${DOMDataBinder.convertToNumber(value, node)}`;                  break;          } -        this._setArgument(newValue); +        this._setArgument(value);      }      async _delete() { @@ -467,6 +468,8 @@ class KeyboardShortcutHotkeyEntry {              this._setArgumentInputValue(node, value);          } +        this._updateArgumentInputValidity(); +          await this._modifyProfileSettings([{              action: 'set',              path: `${this._basePath}.argument`, @@ -540,6 +543,7 @@ class KeyboardShortcutHotkeyEntry {              if (inputNode !== null) {                  this._setArgumentInputValue(inputNode, argument);                  this._argumentInput = inputNode; +                this._updateArgumentInputValidity();                  this._argumentEventListeners.addEventListener(inputNode, 'change', this._onArgumentValueChange.bind(this, template), false);              }              this._argumentContainer.appendChild(node); @@ -558,4 +562,41 @@ class KeyboardShortcutHotkeyEntry {      _setArgumentInputValue(node, value) {          node.value = value;      } + +    async _updateArgumentInputValidity() { +        if (this._argumentInput === null) { return; } + +        let okay = true; +        const {action, argument} = this._data; +        const details = this._parent.getActionDetails(action); +        const {argument: argumentDetails} = typeof details !== 'undefined' ? details : {}; + +        if (typeof argumentDetails !== 'undefined') { +            const {template} = argumentDetails; +            switch (template) { +                case 'hotkey-argument-setting-path': +                    okay = await this._isHotkeyArgumentSettingPathValid(argument); +                    break; +            } +        } + +        this._argumentInput.dataset.invalid = `${!okay}`; +    } + +    async _isHotkeyArgumentSettingPathValid(path) { +        if (path.length === 0) { return true; } + +        const options = await this._parent.settingsController.getOptions(); +        const accessor = new ObjectPropertyAccessor(options); +        const pathArray = ObjectPropertyAccessor.getPathArray(path); +        try { +            const value = accessor.get(pathArray, pathArray.length); +            if (typeof value === 'boolean') { +                return true; +            } +        } catch (e) { +            // NOP +        } +        return false; +    }  } diff --git a/ext/popup.html b/ext/popup.html index 78e89997..36cff420 100644 --- a/ext/popup.html +++ b/ext/popup.html @@ -105,6 +105,7 @@  <script src="/js/display/display-notification.js"></script>  <script src="/js/display/display-profile-selection.js"></script>  <script src="/js/display/display-resizer.js"></script> +<script src="/js/display/option-toggle-hotkey-handler.js"></script>  <script src="/js/display/query-parser.js"></script>  <script src="/js/dom/document-focus-controller.js"></script>  <script src="/js/dom/document-util.js"></script> diff --git a/ext/search.html b/ext/search.html index 48abb8b7..4ef8860f 100644 --- a/ext/search.html +++ b/ext/search.html @@ -89,6 +89,7 @@  <script src="/js/display/display-generator.js"></script>  <script src="/js/display/display-history.js"></script>  <script src="/js/display/display-notification.js"></script> +<script src="/js/display/option-toggle-hotkey-handler.js"></script>  <script src="/js/display/query-parser.js"></script>  <script src="/js/display/search-display-controller.js"></script>  <script src="/js/dom/document-focus-controller.js"></script> diff --git a/ext/settings.html b/ext/settings.html index e9f27751..3de183db 100644 --- a/ext/settings.html +++ b/ext/settings.html @@ -3172,6 +3172,7 @@              <option value="playAudioFromSource">Play audio from source</option>              <option value="copyHostSelection">Copy host window selection</option>              <option value="scanSelectedText">Scan selected text</option> +            <option value="toggleOption">Toggle option</option>          </select>          <div class="hotkey-list-item-action-argument-container"></div>      </div> @@ -3221,9 +3222,14 @@      <input type="number" step="1" min="1" class="hotkey-argument-input">  </div></template> +<template id="hotkey-argument-setting-path-template"><div class="flex-row-nowrap"> +    <span class="hotkey-argument-label">Path:</span> +    <input type="text" class="hotkey-argument-input horizontal-flex-fill" spellcheck="false" autocomplete="off"> +</div></template> +  <template id="hotkey-argument-audio-source-template"><div class="flex-row-nowrap">      <span class="hotkey-argument-label">Source:</span> -    <select class="audio-source-select hotkey-argument-input"> +    <select class="audio-source-select hotkey-argument-input horizontal-flex-fill">          <option value="jpod101">JapanesePod101</option>          <option value="jpod101-alternate">JapanesePod101 (Alternate)</option>          <option value="jisho">Jisho.org</option> |