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
This commit is contained in:
toasted-nutbread 2021-01-31 11:55:11 -05:00 committed by GitHub
parent 855234a157
commit 08a87bd007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 143 additions and 28 deletions

View File

@ -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); 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; flex: 1 1 auto;
align-self: center; align-self: center;
position: relative;
} }
.settings-item-left:last-child { .settings-item-left:last-child {
padding-right: var(--settings-group-inner-horizontal-padding); 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 { .settings-item.settings-item-button:active .icon-button>.icon-button-inner>.icon {
background-color: var(--accent-color); 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 */ /* Settings item groups */

View File

@ -152,18 +152,14 @@ class AnkiController {
return await this._ankiConnect.getModelFieldNames(model); return await this._ankiConnect.getModelFieldNames(model);
} }
validateFieldPermissions(fieldValue) { getRequiredPermissions(fieldValue) {
let requireClipboard = false;
const markers = this._getFieldMarkers(fieldValue); const markers = this._getFieldMarkers(fieldValue);
for (const marker of markers) { for (const marker of markers) {
if (this._fieldMarkersRequiringClipboardPermission.has(marker)) { if (this._fieldMarkersRequiringClipboardPermission.has(marker)) {
requireClipboard = true; return ['clipboardRead'];
} }
} }
return [];
if (requireClipboard) {
this._requestClipboardReadPermission();
}
} }
containsAnyMarker(field) { containsAnyMarker(field) {
@ -338,10 +334,6 @@ class AnkiController {
this._ankiErrorMessageDetailsToggle.hidden = false; this._ankiErrorMessageDetailsToggle.hidden = false;
} }
async _requestClipboardReadPermission() {
return await this._settingsController.setPermissionsGranted(['clipboardRead'], true);
}
_getFieldMarkers(fieldValue) { _getFieldMarkers(fieldValue) {
const pattern = /\{([\w-]+)\}/g; const pattern = /\{([\w-]+)\}/g;
const markers = []; const markers = [];
@ -375,6 +367,7 @@ class AnkiCardController {
this._ankiCardModelSelect = null; this._ankiCardModelSelect = null;
this._ankiCardFieldsContainer = null; this._ankiCardFieldsContainer = null;
this._cleaned = false; this._cleaned = false;
this._fieldEntries = [];
} }
async prepare() { async prepare() {
@ -398,12 +391,14 @@ class AnkiCardController {
this._eventListeners.addEventListener(this._ankiCardDeckSelect, 'change', this._onCardDeckChange.bind(this), false); this._eventListeners.addEventListener(this._ankiCardDeckSelect, 'change', this._onCardDeckChange.bind(this), false);
this._eventListeners.addEventListener(this._ankiCardModelSelect, 'change', this._onCardModelChange.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(); await this.updateAnkiState();
} }
cleanup() { cleanup() {
this._cleaned = true; this._cleaned = true;
this._fieldEntries = [];
this._eventListeners.removeAllEventListeners(); this._eventListeners.removeAllEventListeners();
} }
@ -430,7 +425,7 @@ class AnkiCardController {
_onFieldChange(index, e) { _onFieldChange(index, e) {
const node = e.currentTarget; const node = e.currentTarget;
this._ankiController.validateFieldPermissions(node.value); this._validateFieldPermissions(node, index, true);
this._validateField(node, index); this._validateField(node, index);
} }
@ -439,6 +434,11 @@ class AnkiCardController {
this._validateField(node, index); this._validateField(node, index);
} }
_onFieldSettingChanged(index, e) {
const node = e.currentTarget;
this._validateFieldPermissions(node, index, false);
}
_onFieldMenuClose({currentTarget: button, detail: {action, item}}) { _onFieldMenuClose({currentTarget: button, detail: {action, item}}) {
switch (action) { switch (action) {
case 'setFieldMarker': case 'setFieldMarker':
@ -454,10 +454,11 @@ class AnkiCardController {
} }
_validateField(node, index) { _validateField(node, index) {
if (index === 0) { let valid = (node.dataset.hasPermissions !== 'false');
const containsAnyMarker = this._ankiController.containsAnyMarker(node.value); if (valid && index === 0 && !this._ankiController.containsAnyMarker(node.value)) {
node.dataset.invalid = `${!containsAnyMarker}`; valid = false;
} }
node.dataset.invalid = `${!valid}`;
} }
_setFieldMarker(element, marker) { _setFieldMarker(element, marker) {
@ -504,7 +505,7 @@ class AnkiCardController {
const markers = this._ankiController.getFieldMarkers(this._cardType); const markers = this._ankiController.getFieldMarkers(this._cardType);
const totalFragment = document.createDocumentFragment(); const totalFragment = document.createDocumentFragment();
const fieldMap = new Map(); this._fieldEntries = [];
let index = 0; let index = 0;
for (const [fieldName, fieldValue] of Object.entries(this._fields)) { for (const [fieldName, fieldValue] of Object.entries(this._fields)) {
const content = this._settingsController.instantiateTemplateFragment('anki-card-field'); const content = this._settingsController.instantiateTemplateFragment('anki-card-field');
@ -513,7 +514,6 @@ class AnkiCardController {
fieldNameContainerNode.dataset.index = `${index}`; fieldNameContainerNode.dataset.index = `${index}`;
const fieldNameNode = content.querySelector('.anki-card-field-name'); const fieldNameNode = content.querySelector('.anki-card-field-name');
fieldNameNode.textContent = fieldName; fieldNameNode.textContent = fieldName;
fieldMap.set(fieldName, {fieldNameContainerNode});
const valueContainer = content.querySelector('.anki-card-field-value-container'); const valueContainer = content.querySelector('.anki-card-field-value-container');
valueContainer.dataset.index = `${index}`; valueContainer.dataset.index = `${index}`;
@ -521,8 +521,11 @@ class AnkiCardController {
const inputField = content.querySelector('.anki-card-field-value'); const inputField = content.querySelector('.anki-card-field-value');
inputField.value = fieldValue; inputField.value = fieldValue;
inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields', fieldName]); 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, 'change', this._onFieldChange.bind(this, index), false);
this._fieldEventListeners.addEventListener(inputField, 'input', this._onFieldInput.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); this._validateField(inputField, index);
const markerList = content.querySelector('.anki-card-field-marker-list'); const markerList = content.querySelector('.anki-card-field-marker-list');
@ -545,6 +548,7 @@ class AnkiCardController {
} }
totalFragment.appendChild(content); totalFragment.appendChild(content);
this._fieldEntries.push({fieldName, inputField, fieldNameContainerNode});
++index; ++index;
} }
@ -557,10 +561,10 @@ class AnkiCardController {
} }
container.appendChild(totalFragment); container.appendChild(totalFragment);
this._validateFields(fieldMap); this._validateFields();
} }
async _validateFields(fieldMap) { async _validateFields() {
const token = {}; const token = {};
this._validateFieldsToken = token; this._validateFieldsToken = token;
@ -575,7 +579,7 @@ class AnkiCardController {
const fieldNamesSet = new Set(fieldNames); const fieldNamesSet = new Set(fieldNames);
let index = 0; 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.invalid = `${!fieldNamesSet.has(fieldName)}`;
fieldNameContainerNode.dataset.orderMatches = `${index < fieldNames.length && fieldName === fieldNames[index]}`; fieldNameContainerNode.dataset.orderMatches = `${index < fieldNames.length && fieldName === fieldNames[index]}`;
++index; ++index;
@ -638,4 +642,52 @@ class AnkiCardController {
this._setupFields(); 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);
}
}
} }

View File

@ -87,7 +87,7 @@ class BackupController {
const optionsFull = await this._settingsController.getOptionsFull(); const optionsFull = await this._settingsController.getOptionsFull();
const environment = await api.getEnvironmentInfo(); const environment = await api.getEnvironmentInfo();
const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates(); const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates();
const permissions = await this._getPermissions(); const permissions = await this._settingsController.getAllPermissions();
// Format options // Format options
for (const {options} of optionsFull.profiles) { for (const {options} of optionsFull.profiles) {
@ -167,10 +167,6 @@ class BackupController {
}); });
} }
_getPermissions() {
return new Promise((resolve) => chrome.permissions.getAll(resolve));
}
// Importing // Importing
async _settingsImportSetOptionsFull(optionsFull) { async _settingsImportSetOptionsFull(optionsFull) {

View File

@ -32,6 +32,7 @@ class ClipboardPopupsController {
toggle.addEventListener('change', this._onClipboardToggleChange.bind(this), false); toggle.addEventListener('change', this._onClipboardToggleChange.bind(this), false);
} }
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
this._settingsController.on('permissionsChanged', this._onPermissionsChanged.bind(this));
const options = await this._settingsController.getOptions(); const options = await this._settingsController.getOptions();
this._onOptionsChanged({options}); this._onOptionsChanged({options});
@ -51,17 +52,40 @@ class ClipboardPopupsController {
} }
toggle.checked = !!value; toggle.checked = !!value;
} }
this._updateValidity();
} }
async _onClipboardToggleChange(e) { async _onClipboardToggleChange(e) {
const checkbox = e.currentTarget; const toggle = e.currentTarget;
let value = checkbox.checked; let value = toggle.checked;
if (value) { if (value) {
toggle.checked = false;
value = await this._settingsController.setPermissionsGranted(['clipboardRead'], true); value = await this._settingsController.setPermissionsGranted(['clipboardRead'], true);
checkbox.checked = value; toggle.checked = value;
} }
this._setToggleValid(toggle, true);
await this._settingsController.setProfileSetting('clipboard.enableBackgroundMonitor', value); 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});
}
} }

View File

@ -46,6 +46,8 @@ class SettingsController extends EventDispatcher {
prepare() { prepare() {
yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this)); 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() { 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 // Private
_setProfileIndex(value) { _setProfileIndex(value) {
@ -220,4 +233,16 @@ class SettingsController extends EventDispatcher {
this._pageExitPreventionEventListeners.removeAllEventListeners(); 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});
}
} }

View File

@ -1568,6 +1568,7 @@
<div class="settings-group"> <div class="settings-group">
<div class="settings-item"><div class="settings-item-inner"> <div class="settings-item"><div class="settings-item-inner">
<div class="settings-item-left"> <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-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 class="settings-item-description">Open the search page in a new window when the clipboard contains Japanese text.</div>
</div> </div>
@ -1577,6 +1578,7 @@
</div></div> </div></div>
<div class="settings-item"><div class="settings-item-inner"> <div class="settings-item"><div class="settings-item-inner">
<div class="settings-item-left"> <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-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 class="settings-item-description">The query on the search page will be automatically updated with text in the clipboard.</div>
</div> </div>

View File

@ -794,6 +794,9 @@ button.input-suffix-button {
box-sizing: border-box; box-sizing: border-box;
padding: 0 0.5em; padding: 0 0.5em;
border-color: transparent; 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 { button.input-suffix-button.input-suffix-icon-button {
width: 2.125em; width: 2.125em;