diff options
Diffstat (limited to 'ext/bg/js/settings')
-rw-r--r-- | ext/bg/js/settings/anki-templates.js | 40 | ||||
-rw-r--r-- | ext/bg/js/settings/anki.js | 6 | ||||
-rw-r--r-- | ext/bg/js/settings/audio-ui.js | 141 | ||||
-rw-r--r-- | ext/bg/js/settings/audio.js | 52 | ||||
-rw-r--r-- | ext/bg/js/settings/backup.js | 370 | ||||
-rw-r--r-- | ext/bg/js/settings/conditions-ui.js | 326 | ||||
-rw-r--r-- | ext/bg/js/settings/dictionaries.js | 57 | ||||
-rw-r--r-- | ext/bg/js/settings/main.js | 40 | ||||
-rw-r--r-- | ext/bg/js/settings/popup-preview-frame.js | 47 | ||||
-rw-r--r-- | ext/bg/js/settings/popup-preview.js | 4 | ||||
-rw-r--r-- | ext/bg/js/settings/profiles.js | 20 | ||||
-rw-r--r-- | ext/bg/js/settings/storage.js | 4 |
12 files changed, 1005 insertions, 102 deletions
diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index 9cdfc134..5e74358f 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ @@ -42,10 +42,22 @@ function ankiTemplatesInitialize() { node.addEventListener('click', onAnkiTemplateMarkerClicked, false); } - $('#field-templates').on('change', (e) => onAnkiTemplatesValidateCompile(e)); + $('#field-templates').on('change', (e) => onAnkiFieldTemplatesChanged(e)); $('#field-template-render').on('click', (e) => onAnkiTemplateRender(e)); $('#field-templates-reset').on('click', (e) => onAnkiFieldTemplatesReset(e)); $('#field-templates-reset-confirm').on('click', (e) => onAnkiFieldTemplatesResetConfirm(e)); + + ankiTemplatesUpdateValue(); +} + +async function ankiTemplatesUpdateValue() { + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + let templates = options.anki.fieldTemplates; + if (typeof templates !== 'string') { templates = profileOptionsGetDefaultFieldTemplates(); } + $('#field-templates').val(templates); + + onAnkiTemplatesValidateCompile(); } const ankiTemplatesValidateGetDefinition = (() => { @@ -73,7 +85,9 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext); if (definition !== null) { const options = await apiOptionsGet(optionsContext); - result = await dictFieldFormat(field, definition, mode, options, exceptions); + let templates = options.anki.fieldTemplates; + if (typeof templates !== 'string') { templates = profileOptionsGetDefaultFieldTemplates(); } + result = await dictFieldFormat(field, definition, mode, options, templates, exceptions); } } catch (e) { exceptions.push(e); @@ -89,6 +103,24 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i } } +async function onAnkiFieldTemplatesChanged(e) { + // Get value + let templates = e.currentTarget.value; + if (templates === profileOptionsGetDefaultFieldTemplates()) { + // Default + templates = null; + } + + // Overwrite + const optionsContext = getOptionsContext(); + const options = await getOptionsMutable(optionsContext); + options.anki.fieldTemplates = templates; + await settingsSaveOptions(); + + // Compile + onAnkiTemplatesValidateCompile(); +} + function onAnkiTemplatesValidateCompile() { const infoNode = document.querySelector('#field-template-compile-result'); ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true); diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index e1aabbaf..5f7989b8 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ @@ -154,7 +154,7 @@ async function _onAnkiModelChanged(e) { } const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await getOptionsMutable(optionsContext); options.anki[tabId].fields = utilBackgroundIsolate(fields); await settingsSaveOptions(); diff --git a/ext/bg/js/settings/audio-ui.js b/ext/bg/js/settings/audio-ui.js new file mode 100644 index 00000000..711c2291 --- /dev/null +++ b/ext/bg/js/settings/audio-ui.js @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + + +class AudioSourceUI { + static instantiateTemplate(templateSelector) { + const template = document.querySelector(templateSelector); + const content = document.importNode(template.content, true); + return content.firstChild; + } +} + +AudioSourceUI.Container = class Container { + constructor(audioSources, container, addButton) { + this.audioSources = audioSources; + this.container = container; + this.addButton = addButton; + this.children = []; + + this.container.textContent = ''; + + for (const audioSource of toIterable(audioSources)) { + this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length)); + } + + this._clickListener = () => this.onAddAudioSource(); + this.addButton.addEventListener('click', this._clickListener, false); + } + + cleanup() { + for (const child of this.children) { + child.cleanup(); + } + + this.addButton.removeEventListener('click', this._clickListener, false); + this.container.textContent = ''; + this._clickListener = null; + } + + save() { + // Override + } + + remove(child) { + const index = this.children.indexOf(child); + if (index < 0) { + return; + } + + child.cleanup(); + this.children.splice(index, 1); + this.audioSources.splice(index, 1); + + for (let i = index; i < this.children.length; ++i) { + this.children[i].index = i; + } + } + + onAddAudioSource() { + const audioSource = this.getUnusedAudioSource(); + this.audioSources.push(audioSource); + this.save(); + this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length)); + } + + getUnusedAudioSource() { + const audioSourcesAvailable = [ + 'jpod101', + 'jpod101-alternate', + 'jisho', + 'custom' + ]; + for (const source of audioSourcesAvailable) { + if (this.audioSources.indexOf(source) < 0) { + return source; + } + } + return audioSourcesAvailable[0]; + } +}; + +AudioSourceUI.AudioSource = class AudioSource { + constructor(parent, audioSource, index) { + this.parent = parent; + this.audioSource = audioSource; + this.index = index; + + this.container = AudioSourceUI.instantiateTemplate('#audio-source-template'); + this.select = this.container.querySelector('.audio-source-select'); + this.removeButton = this.container.querySelector('.audio-source-remove'); + + this.select.value = audioSource; + + this._selectChangeListener = () => this.onSelectChanged(); + this._removeClickListener = () => this.onRemoveClicked(); + + this.select.addEventListener('change', this._selectChangeListener, false); + this.removeButton.addEventListener('click', this._removeClickListener, false); + + parent.container.appendChild(this.container); + } + + cleanup() { + this.select.removeEventListener('change', this._selectChangeListener, false); + this.removeButton.removeEventListener('click', this._removeClickListener, false); + + if (this.container.parentNode !== null) { + this.container.parentNode.removeChild(this.container); + } + } + + save() { + this.parent.save(); + } + + onSelectChanged() { + this.audioSource = this.select.value; + this.parent.audioSources[this.index] = this.audioSource; + this.save(); + } + + onRemoveClicked() { + this.parent.remove(this); + this.save(); + } +}; diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js index f63551ed..cff3f521 100644 --- a/ext/bg/js/settings/audio.js +++ b/ext/bg/js/settings/audio.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ @@ -21,8 +21,12 @@ let audioSourceUI = null; async function audioSettingsInitialize() { const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - audioSourceUI = new AudioSourceUI.Container(options.audio.sources, $('.audio-source-list'), $('.audio-source-add')); + const options = await getOptionsMutable(optionsContext); + audioSourceUI = new AudioSourceUI.Container( + options.audio.sources, + document.querySelector('.audio-source-list'), + document.querySelector('.audio-source-add') + ); audioSourceUI.save = () => settingsSaveOptions(); textToSpeechInitialize(); @@ -34,24 +38,34 @@ function textToSpeechInitialize() { speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false); updateTextToSpeechVoices(); - $('#text-to-speech-voice-test').on('click', () => textToSpeechTest()); + document.querySelector('#text-to-speech-voice').addEventListener('change', (e) => onTextToSpeechVoiceChange(e), false); + document.querySelector('#text-to-speech-voice-test').addEventListener('click', () => textToSpeechTest(), false); } function updateTextToSpeechVoices() { const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index})); voices.sort(textToSpeechVoiceCompare); - if (voices.length > 0) { - $('#text-to-speech-voice-container').css('display', ''); - } - const select = $('#text-to-speech-voice'); - select.empty(); - select.append($('<option>').val('').text('None')); + document.querySelector('#text-to-speech-voice-container').hidden = (voices.length === 0); + + const fragment = document.createDocumentFragment(); + + let option = document.createElement('option'); + option.value = ''; + option.textContent = 'None'; + fragment.appendChild(option); + for (const {voice} of voices) { - select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`)); + option = document.createElement('option'); + option.value = voice.voiceURI; + option.textContent = `${voice.name} (${voice.lang})`; + fragment.appendChild(option); } - select.val(select.attr('data-value')); + const select = document.querySelector('#text-to-speech-voice'); + select.textContent = ''; + select.appendChild(fragment); + select.value = select.dataset.value; } function languageTagIsJapanese(languageTag) { @@ -78,15 +92,13 @@ function textToSpeechVoiceCompare(a, b) { if (bIsDefault) { return 1; } } - if (a.index < b.index) { return -1; } - if (a.index > b.index) { return 1; } - return 0; + return a.index - b.index; } function textToSpeechTest() { try { - const text = $('#text-to-speech-voice-test').attr('data-speech-text') || ''; - const voiceURI = $('#text-to-speech-voice').val(); + const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || ''; + const voiceURI = document.querySelector('#text-to-speech-voice').value; const voice = audioGetTextToSpeechVoice(voiceURI); if (voice === null) { return; } @@ -100,3 +112,7 @@ function textToSpeechTest() { // NOP } } + +function onTextToSpeechVoiceChange(e) { + e.currentTarget.dataset.value = e.currentTarget.value; +} diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js new file mode 100644 index 00000000..becdc568 --- /dev/null +++ b/ext/bg/js/settings/backup.js @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + + +// Exporting + +let _settingsExportToken = null; +let _settingsExportRevoke = null; +const SETTINGS_EXPORT_CURRENT_VERSION = 0; + +function _getSettingsExportDateString(date, dateSeparator, dateTimeSeparator, timeSeparator, resolution) { + const values = [ + date.getUTCFullYear().toString(), + dateSeparator, + (date.getUTCMonth() + 1).toString().padStart(2, '0'), + dateSeparator, + date.getUTCDate().toString().padStart(2, '0'), + dateTimeSeparator, + date.getUTCHours().toString().padStart(2, '0'), + timeSeparator, + date.getUTCMinutes().toString().padStart(2, '0'), + timeSeparator, + date.getUTCSeconds().toString().padStart(2, '0') + ]; + return values.slice(0, resolution * 2 - 1).join(''); +} + +async function _getSettingsExportData(date) { + const optionsFull = await apiOptionsGetFull(); + const environment = await apiGetEnvironmentInfo(); + + const fieldTemplatesDefault = profileOptionsGetDefaultFieldTemplates(); + + // Format options + for (const {options} of optionsFull.profiles) { + if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) { + delete options.anki.fieldTemplates; // Default + } + } + + const data = { + version: SETTINGS_EXPORT_CURRENT_VERSION, + date: _getSettingsExportDateString(date, '-', ' ', ':', 6), + url: chrome.runtime.getURL('/'), + manifest: chrome.runtime.getManifest(), + environment, + userAgent: navigator.userAgent, + options: optionsFull + }; + + return data; +} + +function _saveBlob(blob, fileName) { + if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') { + if (navigator.msSaveBlob(blob)) { + return; + } + } + + const blobUrl = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = blobUrl; + a.download = fileName; + a.rel = 'noopener'; + a.target = '_blank'; + + const revoke = () => { + URL.revokeObjectURL(blobUrl); + a.href = ''; + _settingsExportRevoke = null; + }; + _settingsExportRevoke = revoke; + + a.dispatchEvent(new MouseEvent('click')); + setTimeout(revoke, 60000); +} + +async function _onSettingsExportClick() { + if (_settingsExportRevoke !== null) { + _settingsExportRevoke(); + _settingsExportRevoke = null; + } + + const date = new Date(Date.now()); + + const token = {}; + _settingsExportToken = token; + const data = await _getSettingsExportData(date); + if (_settingsExportToken !== token) { + // A new export has been started + return; + } + _settingsExportToken = null; + + const fileName = `yomichan-settings-${_getSettingsExportDateString(date, '-', '-', '-', 6)}.json`; + const blob = new Blob([JSON.stringify(data, null, 4)], {type: 'application/json'}); + _saveBlob(blob, fileName); +} + + +// Importing + +async function _settingsImportSetOptionsFull(optionsFull) { + return utilIsolate(await utilBackend().setFullOptions( + utilBackgroundIsolate(optionsFull) + )); +} + +function _showSettingsImportError(error) { + logError(error); + document.querySelector('#settings-import-error-modal-message').textContent = `${error}`; + $('#settings-import-error-modal').modal('show'); +} + +async function _showSettingsImportWarnings(warnings) { + const modalNode = $('#settings-import-warning-modal'); + const buttons = document.querySelectorAll('.settings-import-warning-modal-import-button'); + const messageContainer = document.querySelector('#settings-import-warning-modal-message'); + if (modalNode.length === 0 || buttons.length === 0 || messageContainer === null) { + return {result: false}; + } + + // Set message + const fragment = document.createDocumentFragment(); + for (const warning of warnings) { + const node = document.createElement('li'); + node.textContent = `${warning}`; + fragment.appendChild(node); + } + messageContainer.textContent = ''; + messageContainer.appendChild(fragment); + + // Show modal + modalNode.modal('show'); + + // Wait for modal to close + return new Promise((resolve) => { + const onButtonClick = (e) => { + e.preventDefault(); + complete({ + result: true, + sanitize: e.currentTarget.dataset.importSanitize === 'true' + }); + modalNode.modal('hide'); + + }; + const onModalHide = () => { + complete({result: false}); + }; + + let completed = false; + const complete = (result) => { + if (completed) { return; } + completed = true; + + modalNode.off('hide.bs.modal', onModalHide); + for (const button of buttons) { + button.removeEventListener('click', onButtonClick, false); + } + + resolve(result); + }; + + // Hook events + modalNode.on('hide.bs.modal', onModalHide); + for (const button of buttons) { + button.addEventListener('click', onButtonClick, false); + } + }); +} + +function _isLocalhostUrl(urlString) { + try { + const url = new URL(urlString); + switch (url.hostname.toLowerCase()) { + case 'localhost': + case '127.0.0.1': + case '[::1]': + switch (url.protocol.toLowerCase()) { + case 'http:': + case 'https:': + return true; + } + break; + } + } catch (e) { + // NOP + } + return false; +} + +function _settingsImportSanitizeProfileOptions(options, dryRun) { + const warnings = []; + + const anki = options.anki; + if (isObject(anki)) { + const fieldTemplates = anki.fieldTemplates; + if (typeof fieldTemplates === 'string') { + warnings.push('anki.fieldTemplates contains a non-default value'); + if (!dryRun) { + delete anki.fieldTemplates; + } + } + const server = anki.server; + if (typeof server === 'string' && server.length > 0 && !_isLocalhostUrl(server)) { + warnings.push('anki.server uses a non-localhost URL'); + if (!dryRun) { + delete anki.server; + } + } + } + + const audio = options.audio; + if (isObject(audio)) { + const customSourceUrl = audio.customSourceUrl; + if (typeof customSourceUrl === 'string' && customSourceUrl.length > 0 && !_isLocalhostUrl(customSourceUrl)) { + warnings.push('audio.customSourceUrl uses a non-localhost URL'); + if (!dryRun) { + delete audio.customSourceUrl; + } + } + } + + return warnings; +} + +function _settingsImportSanitizeOptions(optionsFull, dryRun) { + const warnings = new Set(); + + const profiles = optionsFull.profiles; + if (Array.isArray(profiles)) { + for (const profile of profiles) { + if (!isObject(profile)) { continue; } + const options = profile.options; + if (!isObject(options)) { continue; } + + const warnings2 = _settingsImportSanitizeProfileOptions(options, dryRun); + for (const warning of warnings2) { + warnings.add(warning); + } + } + } + + return warnings; +} + +function _utf8Decode(arrayBuffer) { + try { + return new TextDecoder('utf-8').decode(arrayBuffer); + } catch (e) { + const binaryString = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)); + return decodeURIComponent(escape(binaryString)); + } +} + +async function _importSettingsFile(file) { + const dataString = _utf8Decode(await utilReadFileArrayBuffer(file)); + const data = JSON.parse(dataString); + + // Type check + if (!isObject(data)) { + throw new Error(`Invalid data type: ${typeof data}`); + } + + // Version check + const version = data.version; + if (!( + typeof version === 'number' && + Number.isFinite(version) && + version === Math.floor(version) + )) { + throw new Error(`Invalid version: ${version}`); + } + + if (!( + version >= 0 && + version <= SETTINGS_EXPORT_CURRENT_VERSION + )) { + throw new Error(`Unsupported version: ${version}`); + } + + // Verify options exists + let optionsFull = data.options; + if (!isObject(optionsFull)) { + throw new Error(`Invalid options type: ${typeof optionsFull}`); + } + + // Upgrade options + optionsFull = optionsUpdateVersion(optionsFull, {}); + + // Check for warnings + const sanitizationWarnings = _settingsImportSanitizeOptions(optionsFull, true); + + // Show sanitization warnings + if (sanitizationWarnings.size > 0) { + const {result, sanitize} = await _showSettingsImportWarnings(sanitizationWarnings); + if (!result) { return; } + + if (sanitize !== false) { + _settingsImportSanitizeOptions(optionsFull, false); + } + } + + // Assign options + await _settingsImportSetOptionsFull(optionsFull); + + // Reload settings page + window.location.reload(); +} + +function _onSettingsImportClick() { + document.querySelector('#settings-import-file').click(); +} + +function _onSettingsImportFileChange(e) { + const files = e.target.files; + if (files.length === 0) { return; } + + const file = files[0]; + e.target.value = null; + _importSettingsFile(file).catch(_showSettingsImportError); +} + + +// Resetting + +function _onSettingsResetClick() { + $('#settings-reset-modal').modal('show'); +} + +async function _onSettingsResetConfirmClick() { + $('#settings-reset-modal').modal('hide'); + + // Get default options + const optionsFull = optionsGetDefault(); + + // Assign options + await _settingsImportSetOptionsFull(optionsFull); + + // Reload settings page + window.location.reload(); +} + + +// Setup + +window.addEventListener('DOMContentLoaded', () => { + document.querySelector('#settings-export').addEventListener('click', _onSettingsExportClick, false); + document.querySelector('#settings-import').addEventListener('click', _onSettingsImportClick, false); + document.querySelector('#settings-import-file').addEventListener('change', _onSettingsImportFileChange, false); + document.querySelector('#settings-reset').addEventListener('click', _onSettingsResetClick, false); + document.querySelector('#settings-reset-modal-confirm').addEventListener('click', _onSettingsResetConfirmClick, false); +}, false); diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js new file mode 100644 index 00000000..4d041451 --- /dev/null +++ b/ext/bg/js/settings/conditions-ui.js @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + + +class ConditionsUI { + static instantiateTemplate(templateSelector) { + const template = document.querySelector(templateSelector); + const content = document.importNode(template.content, true); + return $(content.firstChild); + } +} + +ConditionsUI.Container = class Container { + constructor(conditionDescriptors, conditionNameDefault, conditionGroups, container, addButton) { + this.children = []; + this.conditionDescriptors = conditionDescriptors; + this.conditionNameDefault = conditionNameDefault; + this.conditionGroups = conditionGroups; + this.container = container; + this.addButton = addButton; + + this.container.empty(); + + for (const conditionGroup of toIterable(conditionGroups)) { + this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup)); + } + + this.addButton.on('click', () => this.onAddConditionGroup()); + } + + cleanup() { + for (const child of this.children) { + child.cleanup(); + } + + this.addButton.off('click'); + this.container.empty(); + } + + save() { + // Override + } + + isolate(object) { + // Override + return object; + } + + remove(child) { + const index = this.children.indexOf(child); + if (index < 0) { + return; + } + + child.cleanup(); + this.children.splice(index, 1); + this.conditionGroups.splice(index, 1); + } + + onAddConditionGroup() { + const conditionGroup = this.isolate({ + conditions: [this.createDefaultCondition(this.conditionNameDefault)] + }); + this.conditionGroups.push(conditionGroup); + this.save(); + this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup)); + } + + createDefaultCondition(type) { + let operator = ''; + let value = ''; + if (hasOwn(this.conditionDescriptors, type)) { + const conditionDescriptor = this.conditionDescriptors[type]; + operator = conditionDescriptor.defaultOperator; + ({value} = this.getOperatorDefaultValue(type, operator)); + if (typeof value === 'undefined') { + value = ''; + } + } + return {type, operator, value}; + } + + getOperatorDefaultValue(type, operator) { + if (hasOwn(this.conditionDescriptors, type)) { + const conditionDescriptor = this.conditionDescriptors[type]; + if (hasOwn(conditionDescriptor.operators, operator)) { + const operatorDescriptor = conditionDescriptor.operators[operator]; + if (hasOwn(operatorDescriptor, 'defaultValue')) { + return {value: operatorDescriptor.defaultValue, fromOperator: true}; + } + } + if (hasOwn(conditionDescriptor, 'defaultValue')) { + return {value: conditionDescriptor.defaultValue, fromOperator: false}; + } + } + return {fromOperator: false}; + } +}; + +ConditionsUI.ConditionGroup = class ConditionGroup { + constructor(parent, conditionGroup) { + this.parent = parent; + this.children = []; + this.conditionGroup = conditionGroup; + this.container = $('<div>').addClass('condition-group').appendTo(parent.container); + this.options = ConditionsUI.instantiateTemplate('#condition-group-options-template').appendTo(parent.container); + this.separator = ConditionsUI.instantiateTemplate('#condition-group-separator-template').appendTo(parent.container); + this.addButton = this.options.find('.condition-add'); + + for (const condition of toIterable(conditionGroup.conditions)) { + this.children.push(new ConditionsUI.Condition(this, condition)); + } + + this.addButton.on('click', () => this.onAddCondition()); + } + + cleanup() { + for (const child of this.children) { + child.cleanup(); + } + + this.addButton.off('click'); + this.container.remove(); + this.options.remove(); + this.separator.remove(); + } + + save() { + this.parent.save(); + } + + isolate(object) { + return this.parent.isolate(object); + } + + remove(child) { + const index = this.children.indexOf(child); + if (index < 0) { + return; + } + + child.cleanup(); + this.children.splice(index, 1); + this.conditionGroup.conditions.splice(index, 1); + + if (this.children.length === 0) { + this.parent.remove(this, false); + } + } + + onAddCondition() { + const condition = this.isolate(this.parent.createDefaultCondition(this.parent.conditionNameDefault)); + this.conditionGroup.conditions.push(condition); + this.children.push(new ConditionsUI.Condition(this, condition)); + } +}; + +ConditionsUI.Condition = class Condition { + constructor(parent, condition) { + this.parent = parent; + this.condition = condition; + this.container = ConditionsUI.instantiateTemplate('#condition-template').appendTo(parent.container); + this.input = this.container.find('input'); + this.typeSelect = this.container.find('.condition-type'); + this.operatorSelect = this.container.find('.condition-operator'); + this.removeButton = this.container.find('.condition-remove'); + + this.updateTypes(); + this.updateOperators(); + this.updateInput(); + + this.input.on('change', () => this.onInputChanged()); + this.typeSelect.on('change', () => this.onConditionTypeChanged()); + this.operatorSelect.on('change', () => this.onConditionOperatorChanged()); + this.removeButton.on('click', () => this.onRemoveClicked()); + } + + cleanup() { + this.input.off('change'); + this.typeSelect.off('change'); + this.operatorSelect.off('change'); + this.removeButton.off('click'); + this.container.remove(); + } + + save() { + this.parent.save(); + } + + updateTypes() { + const conditionDescriptors = this.parent.parent.conditionDescriptors; + const optionGroup = this.typeSelect.find('optgroup'); + optionGroup.empty(); + for (const type of Object.keys(conditionDescriptors)) { + const conditionDescriptor = conditionDescriptors[type]; + $('<option>').val(type).text(conditionDescriptor.name).appendTo(optionGroup); + } + this.typeSelect.val(this.condition.type); + } + + updateOperators() { + const conditionDescriptors = this.parent.parent.conditionDescriptors; + const optionGroup = this.operatorSelect.find('optgroup'); + optionGroup.empty(); + + const type = this.condition.type; + if (hasOwn(conditionDescriptors, type)) { + const conditionDescriptor = conditionDescriptors[type]; + const operators = conditionDescriptor.operators; + for (const operatorName of Object.keys(operators)) { + const operatorDescriptor = operators[operatorName]; + $('<option>').val(operatorName).text(operatorDescriptor.name).appendTo(optionGroup); + } + } + + this.operatorSelect.val(this.condition.operator); + } + + updateInput() { + const conditionDescriptors = this.parent.parent.conditionDescriptors; + const {type, operator} = this.condition; + const props = { + placeholder: '', + type: 'text' + }; + + const objects = []; + if (hasOwn(conditionDescriptors, type)) { + const conditionDescriptor = conditionDescriptors[type]; + objects.push(conditionDescriptor); + if (hasOwn(conditionDescriptor.operators, operator)) { + const operatorDescriptor = conditionDescriptor.operators[operator]; + objects.push(operatorDescriptor); + } + } + + for (const object of objects) { + if (hasOwn(object, 'placeholder')) { + props.placeholder = object.placeholder; + } + if (object.type === 'number') { + props.type = 'number'; + for (const prop of ['step', 'min', 'max']) { + if (hasOwn(object, prop)) { + props[prop] = object[prop]; + } + } + } + } + + for (const prop in props) { + this.input.prop(prop, props[prop]); + } + + const {valid} = this.validateValue(this.condition.value); + this.input.toggleClass('is-invalid', !valid); + this.input.val(this.condition.value); + } + + validateValue(value) { + const conditionDescriptors = this.parent.parent.conditionDescriptors; + let valid = true; + try { + value = conditionsNormalizeOptionValue( + conditionDescriptors, + this.condition.type, + this.condition.operator, + value + ); + } catch (e) { + valid = false; + } + return {valid, value}; + } + + onInputChanged() { + const {valid, value} = this.validateValue(this.input.val()); + this.input.toggleClass('is-invalid', !valid); + this.input.val(value); + this.condition.value = value; + this.save(); + } + + onConditionTypeChanged() { + const type = this.typeSelect.val(); + const {operator, value} = this.parent.parent.createDefaultCondition(type); + this.condition.type = type; + this.condition.operator = operator; + this.condition.value = value; + this.save(); + this.updateOperators(); + this.updateInput(); + } + + onConditionOperatorChanged() { + const type = this.condition.type; + const operator = this.operatorSelect.val(); + const {value, fromOperator} = this.parent.parent.getOperatorDefaultValue(type, operator); + this.condition.operator = operator; + if (fromOperator) { + this.condition.value = value; + } + this.save(); + this.updateInput(); + } + + onRemoveClicked() { + this.parent.remove(this); + this.save(); + } +}; diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index 065a8abc..ed171ae9 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ @@ -189,6 +189,7 @@ class SettingsDictionaryEntryUI { this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title; this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`; + this.content.querySelector('.dict-prefix-wildcard-searches-supported').checked = !!this.dictionaryInfo.prefixWildcardsSupported; this.applyValues(); @@ -272,7 +273,7 @@ class SettingsDictionaryEntryUI { progress.hidden = true; const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await getOptionsMutable(optionsContext); onDatabaseUpdated(options); } } @@ -356,9 +357,10 @@ async function dictSettingsInitialize() { document.querySelector('#dict-file-button').addEventListener('click', (e) => onDictionaryImportButtonClick(e), false); document.querySelector('#dict-file').addEventListener('change', (e) => onDictionaryImport(e), false); document.querySelector('#dict-main').addEventListener('change', (e) => onDictionaryMainChanged(e), false); + document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', (e) => onDatabaseEnablePrefixWildcardSearchesChanged(e), false); const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await getOptionsMutable(optionsContext); onDictionaryOptionsChanged(options); onDatabaseUpdated(options); } @@ -366,6 +368,9 @@ async function dictSettingsInitialize() { async function onDictionaryOptionsChanged(options) { if (dictionaryUI === null) { return; } dictionaryUI.setOptionsDictionaries(options.dictionaries); + + const optionsFull = await apiOptionsGetFull(); + document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported; } async function onDatabaseUpdated(options) { @@ -420,7 +425,7 @@ async function updateMainDictionarySelect(options, dictionaries) { async function onDictionaryMainChanged(e) { const value = e.target.value; const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await getOptionsMutable(optionsContext); options.general.mainDictionary = value; settingsSaveOptions(); } @@ -526,14 +531,14 @@ async function onDictionaryPurge(e) { dictionarySpinnerShow(true); await utilDatabasePurge(); - for (const options of toIterable(await getOptionsArray())) { + for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) { options.dictionaries = utilBackgroundIsolate({}); options.general.mainDictionary = ''; } await settingsSaveOptions(); const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await getOptionsMutable(optionsContext); onDatabaseUpdated(options); } catch (err) { dictionaryErrorsShow([err]); @@ -552,6 +557,9 @@ async function onDictionaryPurge(e) { } async function onDictionaryImport(e) { + const files = [...e.target.files]; + e.target.value = null; + const dictFile = $('#dict-file'); const dictControls = $('#dict-importer').hide(); const dictProgress = $('#dict-import-progress').show(); @@ -572,8 +580,11 @@ async function onDictionaryImport(e) { } }; - const exceptions = []; - const files = [...e.target.files]; + const optionsFull = await apiOptionsGetFull(); + + const importDetails = { + prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported + }; for (let i = 0, ii = files.length; i < ii; ++i) { setProgress(0.0); @@ -582,25 +593,26 @@ async function onDictionaryImport(e) { dictImportInfo.textContent = `(${i + 1} of ${ii})`; } - const summary = await utilDatabaseImport(files[i], updateProgress, exceptions); - for (const options of toIterable(await getOptionsArray())) { + const {result, errors} = await utilDatabaseImport(files[i], updateProgress, importDetails); + for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) { const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions(); dictionaryOptions.enabled = true; - options.dictionaries[summary.title] = dictionaryOptions; - if (summary.sequenced && options.general.mainDictionary === '') { - options.general.mainDictionary = summary.title; + options.dictionaries[result.title] = dictionaryOptions; + if (result.sequenced && options.general.mainDictionary === '') { + options.general.mainDictionary = result.title; } } await settingsSaveOptions(); - if (exceptions.length > 0) { - exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`); - dictionaryErrorsShow(exceptions); + if (errors.length > 0) { + errors.push(...errors); + errors.push(`Dictionary may not have been imported properly: ${errors.length} error${errors.length === 1 ? '' : 's'} reported.`); + dictionaryErrorsShow(errors); } const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await getOptionsMutable(optionsContext); onDatabaseUpdated(options); } } catch (err) { @@ -616,3 +628,12 @@ async function onDictionaryImport(e) { dictProgress.hide(); } } + + +async function onDatabaseEnablePrefixWildcardSearchesChanged(e) { + const optionsFull = await getOptionsFullMutable(); + const v = !!e.target.checked; + if (optionsFull.global.database.prefixWildcardsSupported === v) { return; } + optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked; + await settingsSaveOptions(); +} diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 7456e7a4..56828a15 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,12 +13,17 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -async function getOptionsArray() { - const optionsFull = await apiOptionsGetFull(); - return optionsFull.profiles.map((profile) => profile.options); +function getOptionsMutable(optionsContext) { + return utilBackend().getOptions( + utilBackgroundIsolate(optionsContext) + ); +} + +function getOptionsFullMutable() { + return utilBackend().getFullOptions(); } async function formRead(options) { @@ -75,7 +80,6 @@ async function formRead(options) { options.anki.server = $('#interface-server').val(); options.anki.screenshot.format = $('#screenshot-format').val(); options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); - options.anki.fieldTemplates = $('#field-templates').val(); if (optionsAnkiEnableOld && !ankiErrorShown()) { options.anki.terms.deck = $('#anki-terms-deck').val(); @@ -140,9 +144,8 @@ async function formWrite(options) { $('#interface-server').val(options.anki.server); $('#screenshot-format').val(options.anki.screenshot.format); $('#screenshot-quality').val(options.anki.screenshot.quality); - $('#field-templates').val(options.anki.fieldTemplates); - onAnkiTemplatesValidateCompile(); + await ankiTemplatesUpdateValue(); await onAnkiOptionsChanged(options); await onDictionaryOptionsChanged(options); @@ -161,7 +164,9 @@ function formUpdateVisibility(options) { if (options.general.debugInfo) { const temp = utilIsolate(options); - temp.anki.fieldTemplates = '...'; + if (typeof temp.anki.fieldTemplates === 'string') { + temp.anki.fieldTemplates = '...'; + } const text = JSON.stringify(temp, null, 4); $('#debug').text(text); } @@ -169,7 +174,7 @@ function formUpdateVisibility(options) { async function onFormOptionsChanged() { const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await getOptionsMutable(optionsContext); await formRead(options); await settingsSaveOptions(); @@ -195,21 +200,10 @@ async function onOptionsUpdate({source}) { if (source === thisSource) { return; } const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await getOptionsMutable(optionsContext); await formWrite(options); } -function onMessage({action, params}, sender, callback) { - switch (action) { - case 'optionsUpdate': - onOptionsUpdate(params); - break; - case 'getUrl': - callback({url: window.location.href}); - break; - } -} - function showExtensionInformation() { const node = document.getElementById('extension-info'); @@ -233,7 +227,7 @@ async function onReady() { storageInfoInitialize(); - chrome.runtime.onMessage.addListener(onMessage); + yomichan.on('optionsUpdate', onOptionsUpdate); } $(document).ready(() => onReady()); diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index 49409968..2b727cbd 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ @@ -24,6 +24,7 @@ class SettingsPopupPreview { this.popupInjectOuterStylesheetOld = Popup.injectOuterStylesheet; this.popupShown = false; this.themeChangeTimeout = null; + this.textSource = null; } static create() { @@ -46,16 +47,18 @@ class SettingsPopupPreview { window.apiOptionsGet = (...args) => this.apiOptionsGet(...args); // Overwrite frontend - this.frontend = Frontend.create(); - window.yomichan_frontend = this.frontend; + const popupHost = new PopupProxyHost(); + await popupHost.prepare(); + + const popup = popupHost.createPopup(null, 0); + popup.setChildrenSupported(false); + + this.frontend = new Frontend(popup); this.frontend.setEnabled = function () {}; this.frontend.searchClear = function () {}; - this.frontend.popup.childrenSupported = false; - this.frontend.popup.interactive = false; - - await this.frontend.isPrepared(); + await this.frontend.prepare(); // Overwrite popup Popup.injectOuterStylesheet = (...args) => this.popupInjectOuterStylesheet(...args); @@ -95,7 +98,7 @@ class SettingsPopupPreview { onWindowResize() { if (this.frontend === null) { return; } - const textSource = this.frontend.textSourceLast; + const textSource = this.textSource; if (textSource === null) { return; } const elementRect = textSource.getRect(); @@ -105,11 +108,10 @@ class SettingsPopupPreview { onMessage(e) { const {action, params} = e.data; - const handlers = SettingsPopupPreview.messageHandlers; - if (hasOwn(handlers, action)) { - const handler = handlers[action]; - handler(this, params); - } + const handler = SettingsPopupPreview._messageHandlers.get(action); + if (typeof handler !== 'function') { return; } + + handler(this, params); } onThemeDarkCheckboxChanged(node) { @@ -160,13 +162,14 @@ class SettingsPopupPreview { const source = new TextSourceRange(range, range.toString(), null); try { - await this.frontend.searchSource(source, 'script'); + await this.frontend.onSearchSource(source, 'script'); } finally { source.cleanup(); } - await this.frontend.lastShowPromise; + this.textSource = source; + await this.frontend.showContentCompleted(); - if (this.frontend.popup.isVisible()) { + if (this.frontend.popup.isVisibleSync()) { this.popupShown = true; } @@ -174,11 +177,11 @@ class SettingsPopupPreview { } } -SettingsPopupPreview.messageHandlers = { - setText: (self, {text}) => self.setText(text), - setCustomCss: (self, {css}) => self.setCustomCss(css), - setCustomOuterCss: (self, {css}) => self.setCustomOuterCss(css) -}; +SettingsPopupPreview._messageHandlers = new Map([ + ['setText', (self, {text}) => self.setText(text)], + ['setCustomCss', (self, {css}) => self.setCustomCss(css)], + ['setCustomOuterCss', (self, {css}) => self.setCustomOuterCss(css)] +]); SettingsPopupPreview.instance = SettingsPopupPreview.create(); diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js index d8579eb1..0d20471e 100644 --- a/ext/bg/js/settings/popup-preview.js +++ b/ext/bg/js/settings/popup-preview.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index 8c218e97..c4e68b53 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ let currentProfileIndex = 0; @@ -27,7 +27,7 @@ function getOptionsContext() { async function profileOptionsSetup() { - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await getOptionsFullMutable(); currentProfileIndex = optionsFull.profileCurrent; profileOptionsSetupEventListeners(); @@ -120,7 +120,7 @@ async function profileOptionsUpdateTarget(optionsFull) { profileFormWrite(optionsFull); const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); + const options = await getOptionsMutable(optionsContext); await formWrite(options); } @@ -164,13 +164,13 @@ async function onProfileOptionsChanged(e) { return; } - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await getOptionsFullMutable(); await profileFormRead(optionsFull); await settingsSaveOptions(); } async function onTargetProfileChanged() { - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await getOptionsFullMutable(); const index = tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length); if (index === null || currentProfileIndex === index) { return; @@ -182,7 +182,7 @@ async function onTargetProfileChanged() { } async function onProfileAdd() { - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await getOptionsFullMutable(); const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]); profile.name = profileOptionsCreateCopyName(profile.name, optionsFull.profiles, 100); optionsFull.profiles.push(profile); @@ -210,7 +210,7 @@ async function onProfileRemove(e) { async function onProfileRemoveConfirm() { $('#profile-remove-modal').modal('hide'); - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await getOptionsFullMutable(); if (optionsFull.profiles.length <= 1) { return; } @@ -234,7 +234,7 @@ function onProfileNameChanged() { } async function onProfileMove(offset) { - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await getOptionsFullMutable(); const index = currentProfileIndex + offset; if (index < 0 || index >= optionsFull.profiles.length) { return; @@ -267,7 +267,7 @@ async function onProfileCopy() { async function onProfileCopyConfirm() { $('#profile-copy-modal').modal('hide'); - const optionsFull = await apiOptionsGetFull(); + const optionsFull = await getOptionsFullMutable(); const index = tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length); if (index === null || index === currentProfileIndex) { return; diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js index 51ca6855..6c10f665 100644 --- a/ext/bg/js/settings/storage.js +++ b/ext/bg/js/settings/storage.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ |