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 @@
+
Enable background clipboard text monitoring
Open the search page in a new window when the clipboard contains Japanese text.
@@ -1577,6 +1578,7 @@
+
Enable search page clipboard text monitoring
The query on the search page will be automatically updated with text in the clipboard.
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;