diff options
| -rw-r--r-- | ext/bg/css/settings2.css | 13 | ||||
| -rw-r--r-- | ext/bg/js/settings/anki-controller.js | 92 | ||||
| -rw-r--r-- | ext/bg/js/settings/backup-controller.js | 6 | ||||
| -rw-r--r-- | ext/bg/js/settings/clipboard-popups-controller.js | 30 | ||||
| -rw-r--r-- | ext/bg/js/settings/settings-controller.js | 25 | ||||
| -rw-r--r-- | ext/bg/settings2.html | 2 | ||||
| -rw-r--r-- | ext/mixed/css/material.css | 3 | 
7 files changed, 143 insertions, 28 deletions
| diff --git a/ext/bg/css/settings2.css b/ext/bg/css/settings2.css index c0751d47..53278951 100644 --- a/ext/bg/css/settings2.css +++ b/ext/bg/css/settings2.css @@ -539,6 +539,7 @@ a.heading-link-light {      padding: var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding-half) var(--settings-group-inner-vertical-padding) var(--settings-group-inner-horizontal-padding);      flex: 1 1 auto;      align-self: center; +    position: relative;  }  .settings-item-left:last-child {      padding-right: var(--settings-group-inner-horizontal-padding); @@ -626,6 +627,18 @@ a.settings-item.settings-item-button {  .settings-item.settings-item-button:active .icon-button>.icon-button-inner>.icon {      background-color: var(--accent-color);  } +.settings-item-invalid-indicator { +    display: none; +    position: absolute; +    left: 0; +    top: 0; +    bottom: 0; +    width: 0.5em; +    background-color: var(--danger-color); +} +.settings-item[data-invalid=true] .settings-item-invalid-indicator { +    display: block; +}  /* Settings item groups */ diff --git a/ext/bg/js/settings/anki-controller.js b/ext/bg/js/settings/anki-controller.js index 26abebeb..a594fc8b 100644 --- a/ext/bg/js/settings/anki-controller.js +++ b/ext/bg/js/settings/anki-controller.js @@ -152,18 +152,14 @@ class AnkiController {          return await this._ankiConnect.getModelFieldNames(model);      } -    validateFieldPermissions(fieldValue) { -        let requireClipboard = false; +    getRequiredPermissions(fieldValue) {          const markers = this._getFieldMarkers(fieldValue);          for (const marker of markers) {              if (this._fieldMarkersRequiringClipboardPermission.has(marker)) { -                requireClipboard = true; +                return ['clipboardRead'];              }          } - -        if (requireClipboard) { -            this._requestClipboardReadPermission(); -        } +        return [];      }      containsAnyMarker(field) { @@ -338,10 +334,6 @@ class AnkiController {          this._ankiErrorMessageDetailsToggle.hidden = false;      } -    async _requestClipboardReadPermission() { -        return await this._settingsController.setPermissionsGranted(['clipboardRead'], true); -    } -      _getFieldMarkers(fieldValue) {          const pattern = /\{([\w-]+)\}/g;          const markers = []; @@ -375,6 +367,7 @@ class AnkiCardController {          this._ankiCardModelSelect = null;          this._ankiCardFieldsContainer = null;          this._cleaned = false; +        this._fieldEntries = [];      }      async prepare() { @@ -398,12 +391,14 @@ class AnkiCardController {          this._eventListeners.addEventListener(this._ankiCardDeckSelect, 'change', this._onCardDeckChange.bind(this), false);          this._eventListeners.addEventListener(this._ankiCardModelSelect, 'change', this._onCardModelChange.bind(this), false); +        this._eventListeners.on(this._settingsController, 'permissionsChanged', this._onPermissionsChanged.bind(this));          await this.updateAnkiState();      }      cleanup() {          this._cleaned = true; +        this._fieldEntries = [];          this._eventListeners.removeAllEventListeners();      } @@ -430,7 +425,7 @@ class AnkiCardController {      _onFieldChange(index, e) {          const node = e.currentTarget; -        this._ankiController.validateFieldPermissions(node.value); +        this._validateFieldPermissions(node, index, true);          this._validateField(node, index);      } @@ -439,6 +434,11 @@ class AnkiCardController {          this._validateField(node, index);      } +    _onFieldSettingChanged(index, e) { +        const node = e.currentTarget; +        this._validateFieldPermissions(node, index, false); +    } +      _onFieldMenuClose({currentTarget: button, detail: {action, item}}) {          switch (action) {              case 'setFieldMarker': @@ -454,10 +454,11 @@ class AnkiCardController {      }      _validateField(node, index) { -        if (index === 0) { -            const containsAnyMarker = this._ankiController.containsAnyMarker(node.value); -            node.dataset.invalid = `${!containsAnyMarker}`; +        let valid = (node.dataset.hasPermissions !== 'false'); +        if (valid && index === 0 && !this._ankiController.containsAnyMarker(node.value)) { +            valid = false;          } +        node.dataset.invalid = `${!valid}`;      }      _setFieldMarker(element, marker) { @@ -504,7 +505,7 @@ class AnkiCardController {          const markers = this._ankiController.getFieldMarkers(this._cardType);          const totalFragment = document.createDocumentFragment(); -        const fieldMap = new Map(); +        this._fieldEntries = [];          let index = 0;          for (const [fieldName, fieldValue] of Object.entries(this._fields)) {              const content = this._settingsController.instantiateTemplateFragment('anki-card-field'); @@ -513,7 +514,6 @@ class AnkiCardController {              fieldNameContainerNode.dataset.index = `${index}`;              const fieldNameNode = content.querySelector('.anki-card-field-name');              fieldNameNode.textContent = fieldName; -            fieldMap.set(fieldName, {fieldNameContainerNode});              const valueContainer = content.querySelector('.anki-card-field-value-container');              valueContainer.dataset.index = `${index}`; @@ -521,8 +521,11 @@ class AnkiCardController {              const inputField = content.querySelector('.anki-card-field-value');              inputField.value = fieldValue;              inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields', fieldName]); +            this._validateFieldPermissions(inputField, index, false); +              this._fieldEventListeners.addEventListener(inputField, 'change', this._onFieldChange.bind(this, index), false);              this._fieldEventListeners.addEventListener(inputField, 'input', this._onFieldInput.bind(this, index), false); +            this._fieldEventListeners.addEventListener(inputField, 'settingChanged', this._onFieldSettingChanged.bind(this, index), false);              this._validateField(inputField, index);              const markerList = content.querySelector('.anki-card-field-marker-list'); @@ -545,6 +548,7 @@ class AnkiCardController {              }              totalFragment.appendChild(content); +            this._fieldEntries.push({fieldName, inputField, fieldNameContainerNode});              ++index;          } @@ -557,10 +561,10 @@ class AnkiCardController {          }          container.appendChild(totalFragment); -        this._validateFields(fieldMap); +        this._validateFields();      } -    async _validateFields(fieldMap) { +    async _validateFields() {          const token = {};          this._validateFieldsToken = token; @@ -575,7 +579,7 @@ class AnkiCardController {          const fieldNamesSet = new Set(fieldNames);          let index = 0; -        for (const [fieldName, {fieldNameContainerNode}] of fieldMap.entries()) { +        for (const {fieldName, fieldNameContainerNode} of this._fieldEntries) {              fieldNameContainerNode.dataset.invalid = `${!fieldNamesSet.has(fieldName)}`;              fieldNameContainerNode.dataset.orderMatches = `${index < fieldNames.length && fieldName === fieldNames[index]}`;              ++index; @@ -638,4 +642,52 @@ class AnkiCardController {          this._setupFields();      } + +    async _requestPermissions(permissions) { +        try { +            await this._settingsController.setPermissionsGranted(permissions, true); +        } catch (e) { +            yomichan.logError(e); +        } +    } + +    async _validateFieldPermissions(node, index, request) { +        const fieldValue = node.value; +        const permissions = this._ankiController.getRequiredPermissions(fieldValue); +        if (permissions.length > 0) { +            node.dataset.requiredPermission = permissions.join(' '); +            const hasPermissions = await ( +                request ? +                this._settingsController.setPermissionsGranted(permissions, true) : +                this._settingsController.hasPermissions(permissions) +            ); +            node.dataset.hasPermissions = `${hasPermissions}`; +        } else { +            delete node.dataset.requiredPermission; +            delete node.dataset.hasPermissions; +        } + +        this._validateField(node, index); +    } + +    _onPermissionsChanged({permissions: {permissions}}) { +        const permissionsSet = new Set(permissions); +        for (let i = 0, ii = this._fieldEntries.length; i < ii; ++i) { +            const {inputField} = this._fieldEntries[i]; +            let {requiredPermission} = inputField.dataset; +            if (typeof requiredPermission !== 'string') { continue; } +            requiredPermission = (requiredPermission.length === 0 ? [] : requiredPermission.split(' ')); + +            let hasPermissions = true; +            for (const permission of requiredPermission) { +                if (!permissionsSet.has(permission)) { +                    hasPermissions = false; +                    break; +                } +            } + +            inputField.dataset.hasPermissions = `${hasPermissions}`; +            this._validateField(inputField, i); +        } +    }  } diff --git a/ext/bg/js/settings/backup-controller.js b/ext/bg/js/settings/backup-controller.js index f97a45c5..34817ee9 100644 --- a/ext/bg/js/settings/backup-controller.js +++ b/ext/bg/js/settings/backup-controller.js @@ -87,7 +87,7 @@ class BackupController {          const optionsFull = await this._settingsController.getOptionsFull();          const environment = await api.getEnvironmentInfo();          const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates(); -        const permissions = await this._getPermissions(); +        const permissions = await this._settingsController.getAllPermissions();          // Format options          for (const {options} of optionsFull.profiles) { @@ -167,10 +167,6 @@ class BackupController {          });      } -    _getPermissions() { -        return new Promise((resolve) => chrome.permissions.getAll(resolve)); -    } -      // Importing      async _settingsImportSetOptionsFull(optionsFull) { diff --git a/ext/bg/js/settings/clipboard-popups-controller.js b/ext/bg/js/settings/clipboard-popups-controller.js index ec1d20ec..4737b0b7 100644 --- a/ext/bg/js/settings/clipboard-popups-controller.js +++ b/ext/bg/js/settings/clipboard-popups-controller.js @@ -32,6 +32,7 @@ class ClipboardPopupsController {              toggle.addEventListener('change', this._onClipboardToggleChange.bind(this), false);          }          this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); +        this._settingsController.on('permissionsChanged', this._onPermissionsChanged.bind(this));          const options = await this._settingsController.getOptions();          this._onOptionsChanged({options}); @@ -51,17 +52,40 @@ class ClipboardPopupsController {              }              toggle.checked = !!value;          } +        this._updateValidity();      }      async _onClipboardToggleChange(e) { -        const checkbox = e.currentTarget; -        let value = checkbox.checked; +        const toggle = e.currentTarget; +        let value = toggle.checked;          if (value) { +            toggle.checked = false;              value = await this._settingsController.setPermissionsGranted(['clipboardRead'], true); -            checkbox.checked = value; +            toggle.checked = value;          } +        this._setToggleValid(toggle, true); +          await this._settingsController.setProfileSetting('clipboard.enableBackgroundMonitor', value);      } + +    _onPermissionsChanged({permissions: {permissions}}) { +        const permissionsSet = new Set(permissions); +        for (const toggle of this._toggles) { +            const valid = !toggle.checked || permissionsSet.has('clipboardRead'); +            this._setToggleValid(toggle, valid); +        } +    } + +    _setToggleValid(toggle, valid) { +        const relative = toggle.closest('.settings-item'); +        if (relative === null) { return; } +        relative.dataset.invalid = `${!valid}`; +    } + +    async _updateValidity() { +        const permissions = await this._settingsController.getAllPermissions(); +        this._onPermissionsChanged({permissions}); +    }  } diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js index e59ab7db..a3885ef6 100644 --- a/ext/bg/js/settings/settings-controller.js +++ b/ext/bg/js/settings/settings-controller.js @@ -46,6 +46,8 @@ class SettingsController extends EventDispatcher {      prepare() {          yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this)); +        chrome.permissions.onAdded.addListener(this._onPermissionsChanged.bind(this)); +        chrome.permissions.onRemoved.addListener(this._onPermissionsChanged.bind(this));      }      async refresh() { @@ -165,6 +167,17 @@ class SettingsController extends EventDispatcher {          );      } +    getAllPermissions() { +        return new Promise((resolve, reject) => chrome.permissions.getAll((result) => { +            const e = chrome.runtime.lastError; +            if (e) { +                reject(new Error(e.message)); +            } else { +                resolve(result); +            } +        })); +    } +      // Private      _setProfileIndex(value) { @@ -220,4 +233,16 @@ class SettingsController extends EventDispatcher {              this._pageExitPreventionEventListeners.removeAllEventListeners();          }      } + +    _onPermissionsChanged() { +        this._triggerPermissionsChanged(); +    } + +    async _triggerPermissionsChanged() { +        const event = 'permissionsChanged'; +        if (!this.hasListeners(event)) { return; } + +        const permissions = await this.getAllPermissions(); +        this.trigger(event, {permissions}); +    }  } diff --git a/ext/bg/settings2.html b/ext/bg/settings2.html index c8dee2c4..9c366ecd 100644 --- a/ext/bg/settings2.html +++ b/ext/bg/settings2.html @@ -1568,6 +1568,7 @@      <div class="settings-group">          <div class="settings-item"><div class="settings-item-inner">              <div class="settings-item-left"> +                <div class="settings-item-invalid-indicator"></div>                  <div class="settings-item-label">Enable background clipboard text monitoring</div>                  <div class="settings-item-description">Open the search page in a new window when the clipboard contains Japanese text.</div>              </div> @@ -1577,6 +1578,7 @@          </div></div>          <div class="settings-item"><div class="settings-item-inner">              <div class="settings-item-left"> +                <div class="settings-item-invalid-indicator"></div>                  <div class="settings-item-label">Enable search page clipboard text monitoring</div>                  <div class="settings-item-description">The query on the search page will be automatically updated with text in the clipboard.</div>              </div> diff --git a/ext/mixed/css/material.css b/ext/mixed/css/material.css index bbc4fb83..6dba7206 100644 --- a/ext/mixed/css/material.css +++ b/ext/mixed/css/material.css @@ -794,6 +794,9 @@ button.input-suffix-button {      box-sizing: border-box;      padding: 0 0.5em;      border-color: transparent; +    transition: +        background-color var(--animation-duration) ease-in, +        box-shadow var(--animation-duration) ease-in;  }  button.input-suffix-button.input-suffix-icon-button {      width: 2.125em; |