aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-01-31 11:55:11 -0500
committerGitHub <noreply@github.com>2021-01-31 11:55:11 -0500
commit08a87bd0075cec19d037573261cca012dacaefb9 (patch)
tree83a2e4732ebd5428a95a06267153979aa48f3bc6 /ext
parent855234a15708d28895145dfad0073a3f07248750 (diff)
Settings permissions info display (#1338)
* Add getAllPermissions function * Add permissionsChanged event * Update ClipboardPopupsController to show permissions validation info * Add invalid indicator * Display invalid indicator when permissions are not valid * Fix border color transition not being necessary on input-suffix-button
Diffstat (limited to 'ext')
-rw-r--r--ext/bg/css/settings2.css13
-rw-r--r--ext/bg/js/settings/anki-controller.js92
-rw-r--r--ext/bg/js/settings/backup-controller.js6
-rw-r--r--ext/bg/js/settings/clipboard-popups-controller.js30
-rw-r--r--ext/bg/js/settings/settings-controller.js25
-rw-r--r--ext/bg/settings2.html2
-rw-r--r--ext/mixed/css/material.css3
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;