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:
parent
855234a157
commit
08a87bd007
@ -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 */
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user