From c61a87b152b91bdebe01eefdbc3fa00670a3071d Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 24 May 2020 13:30:40 -0400 Subject: API refactor (#532) * Convert api.js into a class instance * Use new api.* functions * Fix missing binds * Group functions with progress callbacks together * Change style * Fix API override not working --- ext/bg/js/settings/main.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'ext/bg/js/settings/main.js') diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 61395b1c..94f7f8f5 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -21,9 +21,7 @@ * ankiInitialize * ankiTemplatesInitialize * ankiTemplatesUpdateValue - * apiForwardLogsToBackend - * apiGetEnvironmentInfo - * apiOptionsSave + * api * appearanceInitialize * audioSettingsInitialize * backupInitialize @@ -265,7 +263,7 @@ function settingsGetSource() { async function settingsSaveOptions() { const source = await settingsGetSource(); - await apiOptionsSave(source); + await api.optionsSave(source); } async function onOptionsUpdated({source}) { @@ -290,7 +288,7 @@ async function settingsPopulateModifierKeys() { const scanModifierKeySelect = document.querySelector('#scan-modifier-key'); scanModifierKeySelect.textContent = ''; - const environment = await apiGetEnvironmentInfo(); + const environment = await api.getEnvironmentInfo(); const modifierKeys = [ {value: 'none', name: 'None'}, ...environment.modifiers.keys @@ -305,7 +303,7 @@ async function settingsPopulateModifierKeys() { async function onReady() { - apiForwardLogsToBackend(); + api.forwardLogsToBackend(); await yomichan.prepare(); showExtensionInformation(); -- cgit v1.2.3 From 3089bb7908e42e9101241476f700033df82e685d Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 24 May 2020 13:38:48 -0400 Subject: Settings refactor (#541) * Remove debug info * Trigger onOptionsUpdated instead of formWrite when profile changes * Update how Anki field changes are observed * Update how general.enableClipboardPopups setting is changed * Change where ankiTemplatesUpdateValue occurs * Change where onDictionaryOptionsChanged occurs * Remove unused global declarations * Remove stray data attribute --- ext/bg/js/settings/anki.js | 38 ++++++++++++++++------ ext/bg/js/settings/main.js | 74 +++++++++++++++++------------------------- ext/bg/js/settings/profiles.js | 8 ++--- ext/bg/settings.html | 6 ++-- 4 files changed, 62 insertions(+), 64 deletions(-) (limited to 'ext/bg/js/settings/main.js') diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index ba83f994..8de97554 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -19,16 +19,12 @@ * api * getOptionsContext * getOptionsMutable - * onFormOptionsChanged * settingsSaveOptions * utilBackgroundIsolate */ // Private -let _ankiDataPopulated = false; - - function _ankiSpinnerShow(show) { const spinner = $('#anki-spinner'); if (show) { @@ -158,7 +154,7 @@ async function _ankiFieldsPopulate(tabId, options) { container.appendChild(fragment); for (const node of container.querySelectorAll('.anki-field-value')) { - node.addEventListener('change', onFormOptionsChanged, false); + node.addEventListener('change', _onAnkiFieldsChanged, false); } for (const node of container.querySelectorAll('.marker-link')) { node.addEventListener('click', _onAnkiMarkerClicked, false); @@ -203,6 +199,23 @@ async function _onAnkiModelChanged(e) { await _ankiFieldsPopulate(tabId, options); } +async function _onAnkiFieldsChanged() { + const optionsContext = getOptionsContext(); + const options = await getOptionsMutable(optionsContext); + + options.anki.terms.deck = $('#anki-terms-deck').val(); + options.anki.terms.model = $('#anki-terms-model').val(); + options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value'))); + options.anki.kanji.deck = $('#anki-kanji-deck').val(); + options.anki.kanji.model = $('#anki-kanji-model').val(); + options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value'))); + + await settingsSaveOptions(); + + await onAnkiOptionsChanged(options); +} + + // Public @@ -272,20 +285,25 @@ function ankiGetFieldMarkers(type) { function ankiInitialize() { + $('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(_onAnkiFieldsChanged); + for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { node.addEventListener('change', _onAnkiModelChanged, false); } + + onAnkiOptionsChanged(); } -async function onAnkiOptionsChanged(options) { +async function onAnkiOptionsChanged(options=null) { + if (options === null) { + const optionsContext = getOptionsContext(); + options = await getOptionsMutable(optionsContext); + } + if (!options.anki.enable) { - _ankiDataPopulated = false; return; } - if (_ankiDataPopulated) { return; } - await _ankiDeckAndModelPopulate(options); - _ankiDataPopulated = true; await Promise.all([_ankiFieldsPopulate('terms', options), _ankiFieldsPopulate('kanji', options)]); } diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 94f7f8f5..60b9e008 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -16,8 +16,6 @@ */ /* global - * ankiErrorShown - * ankiFieldsToDict * ankiInitialize * ankiTemplatesInitialize * ankiTemplatesUpdateValue @@ -33,7 +31,6 @@ * storageInfoInitialize * utilBackend * utilBackgroundIsolate - * utilIsolate */ function getOptionsMutable(optionsContext) { @@ -48,22 +45,6 @@ function getOptionsFullMutable() { async function formRead(options) { options.general.enable = $('#enable').prop('checked'); - const enableClipboardPopups = $('#enable-clipboard-popups').prop('checked'); - if (enableClipboardPopups) { - options.general.enableClipboardPopups = await new Promise((resolve, _reject) => { - chrome.permissions.request( - {permissions: ['clipboardRead']}, - (granted) => { - if (!granted) { - $('#enable-clipboard-popups').prop('checked', false); - } - resolve(granted); - } - ); - }); - } else { - options.general.enableClipboardPopups = false; - } options.general.showGuide = $('#show-usage-guide').prop('checked'); options.general.compactTags = $('#compact-tags').prop('checked'); options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); @@ -125,7 +106,6 @@ async function formRead(options) { options.parsing.termSpacing = $('#parsing-term-spacing').prop('checked'); options.parsing.readingMode = $('#parsing-reading-mode').val(); - const optionsAnkiEnableOld = options.anki.enable; options.anki.enable = $('#anki-enable').prop('checked'); options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/)); options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); @@ -133,20 +113,10 @@ async function formRead(options) { options.anki.duplicateScope = $('#duplicate-scope').val(); options.anki.screenshot.format = $('#screenshot-format').val(); options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); - - if (optionsAnkiEnableOld && !ankiErrorShown()) { - options.anki.terms.deck = $('#anki-terms-deck').val(); - options.anki.terms.model = $('#anki-terms-model').val(); - options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value'))); - options.anki.kanji.deck = $('#anki-kanji-deck').val(); - options.anki.kanji.model = $('#anki-kanji-model').val(); - options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value'))); - } } async function formWrite(options) { $('#enable').prop('checked', options.general.enable); - $('#enable-clipboard-popups').prop('checked', options.general.enableClipboardPopups); $('#show-usage-guide').prop('checked', options.general.showGuide); $('#compact-tags').prop('checked', options.general.compactTags); $('#compact-glossaries').prop('checked', options.general.compactGlossaries); @@ -216,14 +186,11 @@ async function formWrite(options) { $('#screenshot-format').val(options.anki.screenshot.format); $('#screenshot-quality').val(options.anki.screenshot.quality); - await ankiTemplatesUpdateValue(); - await onAnkiOptionsChanged(options); - await onDictionaryOptionsChanged(); - formUpdateVisibility(options); } function formSetupEventListeners() { + document.querySelector('#enable-clipboard-popups').addEventListener('change', onEnableClipboardPopupsChanged, false); $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(onFormOptionsChanged); } @@ -232,15 +199,6 @@ function formUpdateVisibility(options) { document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`; document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`; document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`; - - if (options.general.debugInfo) { - const temp = utilIsolate(options); - if (typeof temp.anki.fieldTemplates === 'string') { - temp.anki.fieldTemplates = '...'; - } - const text = JSON.stringify(temp, null, 4); - $('#debug').text(text); - } } async function onFormOptionsChanged() { @@ -250,8 +208,30 @@ async function onFormOptionsChanged() { await formRead(options); await settingsSaveOptions(); formUpdateVisibility(options); +} - await onAnkiOptionsChanged(options); +async function onEnableClipboardPopupsChanged(e) { + const optionsContext = getOptionsContext(); + const options = await getOptionsMutable(optionsContext); + + const enableClipboardPopups = e.target.checked; + if (enableClipboardPopups) { + options.general.enableClipboardPopups = await new Promise((resolve) => { + chrome.permissions.request( + {permissions: ['clipboardRead']}, + (granted) => { + if (!granted) { + $('#enable-clipboard-popups').prop('checked', false); + } + resolve(granted); + } + ); + }); + } else { + options.general.enableClipboardPopups = false; + } + + await settingsSaveOptions(); } @@ -272,6 +252,12 @@ async function onOptionsUpdated({source}) { const optionsContext = getOptionsContext(); const options = await getOptionsMutable(optionsContext); + + document.querySelector('#enable-clipboard-popups').checked = options.general.enableClipboardPopups; + ankiTemplatesUpdateValue(); + onDictionaryOptionsChanged(); + onAnkiOptionsChanged(); + await formWrite(options); } diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index e32d5525..59f7fbb1 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -19,9 +19,8 @@ * ConditionsUI * api * conditionsClearCaches - * formWrite * getOptionsFullMutable - * getOptionsMutable + * onOptionsUpdated * profileConditionsDescriptor * profileConditionsDescriptorPromise * settingsSaveOptions @@ -131,10 +130,7 @@ function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndi async function profileOptionsUpdateTarget(optionsFull) { await profileFormWrite(optionsFull); - - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - await formWrite(options); + await onOptionsUpdated({source: null}); } function profileOptionsCreateCopyName(name, profiles, maxUniqueAttempts) { diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 3ce91f12..7964ab90 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -138,7 +138,7 @@ -
+
@@ -854,7 +854,7 @@
  • Kanji
  • -
    +
    @@ -1111,8 +1111,6 @@

    -
    
    -
                 
    -- 
    cgit v1.2.3
    
    
    From 13f57cccba5a29ff9e270a3fc2b2d7fee6e46b51 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Sun, 24 May 2020 13:56:46 -0400
    Subject: Settings backup refactor (#551)
    
    * Update backup.js to be a class
    
    * Move utilReadFileArrayBuffer
    ---
     ext/bg/js/settings/backup.js | 569 ++++++++++++++++++++++---------------------
     ext/bg/js/settings/main.js   |   4 +-
     ext/bg/js/util.js            |   9 -
     3 files changed, 292 insertions(+), 290 deletions(-)
    
    (limited to 'ext/bg/js/settings/main.js')
    
    diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js
    index 5eb55502..4e104e6f 100644
    --- a/ext/bg/js/settings/backup.js
    +++ b/ext/bg/js/settings/backup.js
    @@ -22,355 +22,366 @@
      * utilBackend
      * utilBackgroundIsolate
      * utilIsolate
    - * utilReadFileArrayBuffer
      */
     
    -// 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('');
    -}
    +class SettingsBackup {
    +    constructor() {
    +        this._settingsExportToken = null;
    +        this._settingsExportRevoke = null;
    +        this._currentVersion = 0;
    +    }
     
    -async function _getSettingsExportData(date) {
    -    const optionsFull = await api.optionsGetFull();
    -    const environment = await api.getEnvironmentInfo();
    -    const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates();
    +    prepare() {
    +        document.querySelector('#settings-export').addEventListener('click', this._onSettingsExportClick.bind(this), false);
    +        document.querySelector('#settings-import').addEventListener('click', this._onSettingsImportClick.bind(this), false);
    +        document.querySelector('#settings-import-file').addEventListener('change', this._onSettingsImportFileChange.bind(this), false);
    +        document.querySelector('#settings-reset').addEventListener('click', this._onSettingsResetClick.bind(this), false);
    +        document.querySelector('#settings-reset-modal-confirm').addEventListener('click', this._onSettingsResetConfirmClick.bind(this), false);
    +    }
     
    -    // Format options
    -    for (const {options} of optionsFull.profiles) {
    -        if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) {
    -            delete options.anki.fieldTemplates; // Default
    -        }
    +    // Private
    +
    +    _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('');
         }
     
    -    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;
    -}
    +    async _getSettingsExportData(date) {
    +        const optionsFull = await api.optionsGetFull();
    +        const environment = await api.getEnvironmentInfo();
    +        const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates();
     
    -function _saveBlob(blob, fileName) {
    -    if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') {
    -        if (navigator.msSaveBlob(blob)) {
    -            return;
    +        // Format options
    +        for (const {options} of optionsFull.profiles) {
    +            if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) {
    +                delete options.anki.fieldTemplates; // Default
    +            }
             }
    -    }
     
    -    const blobUrl = URL.createObjectURL(blob);
    +        const data = {
    +            version: this._currentVersion,
    +            date: this._getSettingsExportDateString(date, '-', ' ', ':', 6),
    +            url: chrome.runtime.getURL('/'),
    +            manifest: chrome.runtime.getManifest(),
    +            environment,
    +            userAgent: navigator.userAgent,
    +            options: optionsFull
    +        };
     
    -    const a = document.createElement('a');
    -    a.href = blobUrl;
    -    a.download = fileName;
    -    a.rel = 'noopener';
    -    a.target = '_blank';
    +        return data;
    +    }
     
    -    const revoke = () => {
    -        URL.revokeObjectURL(blobUrl);
    -        a.href = '';
    -        _settingsExportRevoke = null;
    -    };
    -    _settingsExportRevoke = revoke;
    +    _saveBlob(blob, fileName) {
    +        if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') {
    +            if (navigator.msSaveBlob(blob)) {
    +                return;
    +            }
    +        }
     
    -    a.dispatchEvent(new MouseEvent('click'));
    -    setTimeout(revoke, 60000);
    -}
    +        const blobUrl = URL.createObjectURL(blob);
     
    -async function _onSettingsExportClick() {
    -    if (_settingsExportRevoke !== null) {
    -        _settingsExportRevoke();
    -        _settingsExportRevoke = null;
    -    }
    +        const a = document.createElement('a');
    +        a.href = blobUrl;
    +        a.download = fileName;
    +        a.rel = 'noopener';
    +        a.target = '_blank';
     
    -    const date = new Date(Date.now());
    +        const revoke = () => {
    +            URL.revokeObjectURL(blobUrl);
    +            a.href = '';
    +            this._settingsExportRevoke = null;
    +        };
    +        this._settingsExportRevoke = revoke;
     
    -    const token = {};
    -    _settingsExportToken = token;
    -    const data = await _getSettingsExportData(date);
    -    if (_settingsExportToken !== token) {
    -        // A new export has been started
    -        return;
    +        a.dispatchEvent(new MouseEvent('click'));
    +        setTimeout(revoke, 60000);
         }
    -    _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 _onSettingsExportClick() {
    +        if (this._settingsExportRevoke !== null) {
    +            this._settingsExportRevoke();
    +            this._settingsExportRevoke = null;
    +        }
     
    -async function _settingsImportSetOptionsFull(optionsFull) {
    -    return utilIsolate(utilBackend().setFullOptions(
    -        utilBackgroundIsolate(optionsFull)
    -    ));
    -}
    +        const date = new Date(Date.now());
     
    -function _showSettingsImportError(error) {
    -    yomichan.logError(error);
    -    document.querySelector('#settings-import-error-modal-message').textContent = `${error}`;
    -    $('#settings-import-error-modal').modal('show');
    -}
    +        const token = {};
    +        this._settingsExportToken = token;
    +        const data = await this._getSettingsExportData(date);
    +        if (this._settingsExportToken !== token) {
    +            // A new export has been started
    +            return;
    +        }
    +        this._settingsExportToken = null;
     
    -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};
    +        const fileName = `yomichan-settings-${this._getSettingsExportDateString(date, '-', '-', '-', 6)}.json`;
    +        const blob = new Blob([JSON.stringify(data, null, 4)], {type: 'application/json'});
    +        this._saveBlob(blob, fileName);
         }
     
    -    // Set message
    -    const fragment = document.createDocumentFragment();
    -    for (const warning of warnings) {
    -        const node = document.createElement('li');
    -        node.textContent = `${warning}`;
    -        fragment.appendChild(node);
    +    _readFileArrayBuffer(file) {
    +        return new Promise((resolve, reject) => {
    +            const reader = new FileReader();
    +            reader.onload = () => resolve(reader.result);
    +            reader.onerror = () => reject(reader.error);
    +            reader.readAsArrayBuffer(file);
    +        });
         }
    -    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;
    +    // Importing
     
    -            modalNode.off('hide.bs.modal', onModalHide);
    -            for (const button of buttons) {
    -                button.removeEventListener('click', onButtonClick, false);
    -            }
    +    async _settingsImportSetOptionsFull(optionsFull) {
    +        return utilIsolate(utilBackend().setFullOptions(
    +            utilBackgroundIsolate(optionsFull)
    +        ));
    +    }
     
    -            resolve(result);
    -        };
    +    _showSettingsImportError(error) {
    +        yomichan.logError(error);
    +        document.querySelector('#settings-import-error-modal-message').textContent = `${error}`;
    +        $('#settings-import-error-modal').modal('show');
    +    }
     
    -        // Hook events
    -        modalNode.on('hide.bs.modal', onModalHide);
    -        for (const button of buttons) {
    -            button.addEventListener('click', onButtonClick, false);
    +    async _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};
             }
    -    });
    -}
     
    -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;
    +        // Set message
    +        const fragment = document.createDocumentFragment();
    +        for (const warning of warnings) {
    +            const node = document.createElement('li');
    +            node.textContent = `${warning}`;
    +            fragment.appendChild(node);
             }
    -    } catch (e) {
    -        // NOP
    -    }
    -    return false;
    -}
    +        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);
    +                }
     
    -function _settingsImportSanitizeProfileOptions(options, dryRun) {
    -    const warnings = [];
    +                resolve(result);
    +            };
     
    -    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;
    +            // Hook events
    +            modalNode.on('hide.bs.modal', onModalHide);
    +            for (const button of buttons) {
    +                button.addEventListener('click', onButtonClick, false);
                 }
    -        }
    +        });
         }
     
    -    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;
    +    _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;
         }
     
    -    return warnings;
    -}
    -
    -function _settingsImportSanitizeOptions(optionsFull, dryRun) {
    -    const warnings = new Set();
    +    _settingsImportSanitizeProfileOptions(options, dryRun) {
    +        const warnings = [];
     
    -    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);
    +        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 && !this._isLocalhostUrl(server)) {
    +                warnings.push('anki.server uses a non-localhost URL');
    +                if (!dryRun) {
    +                    delete anki.server;
    +                }
                 }
             }
    -    }
     
    -    return warnings;
    -}
    +        const audio = options.audio;
    +        if (isObject(audio)) {
    +            const customSourceUrl = audio.customSourceUrl;
    +            if (typeof customSourceUrl === 'string' && customSourceUrl.length > 0 && !this._isLocalhostUrl(customSourceUrl)) {
    +                warnings.push('audio.customSourceUrl uses a non-localhost URL');
    +                if (!dryRun) {
    +                    delete audio.customSourceUrl;
    +                }
    +            }
    +        }
     
    -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));
    +        return warnings;
         }
    -}
     
    -async function _importSettingsFile(file) {
    -    const dataString = _utf8Decode(await utilReadFileArrayBuffer(file));
    -    const data = JSON.parse(dataString);
    +    _settingsImportSanitizeOptions(optionsFull, dryRun) {
    +        const warnings = new Set();
     
    -    // Type check
    -    if (!isObject(data)) {
    -        throw new Error(`Invalid data type: ${typeof data}`);
    -    }
    +        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; }
     
    -    // Version check
    -    const version = data.version;
    -    if (!(
    -        typeof version === 'number' &&
    -        Number.isFinite(version) &&
    -        version === Math.floor(version)
    -    )) {
    -        throw new Error(`Invalid version: ${version}`);
    -    }
    +                const warnings2 = this._settingsImportSanitizeProfileOptions(options, dryRun);
    +                for (const warning of warnings2) {
    +                    warnings.add(warning);
    +                }
    +            }
    +        }
     
    -    if (!(
    -        version >= 0 &&
    -        version <= SETTINGS_EXPORT_CURRENT_VERSION
    -    )) {
    -        throw new Error(`Unsupported version: ${version}`);
    +        return warnings;
         }
     
    -    // Verify options exists
    -    let optionsFull = data.options;
    -    if (!isObject(optionsFull)) {
    -        throw new Error(`Invalid options type: ${typeof optionsFull}`);
    +    _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));
    +        }
         }
     
    -    // Upgrade options
    -    optionsFull = optionsUpdateVersion(optionsFull, {});
    +    async _importSettingsFile(file) {
    +        const dataString = this._utf8Decode(await this._readFileArrayBuffer(file));
    +        const data = JSON.parse(dataString);
     
    -    // Check for warnings
    -    const sanitizationWarnings = _settingsImportSanitizeOptions(optionsFull, true);
    +        // Type check
    +        if (!isObject(data)) {
    +            throw new Error(`Invalid data type: ${typeof data}`);
    +        }
     
    -    // Show sanitization warnings
    -    if (sanitizationWarnings.size > 0) {
    -        const {result, sanitize} = await _showSettingsImportWarnings(sanitizationWarnings);
    -        if (!result) { return; }
    +        // Version check
    +        const version = data.version;
    +        if (!(
    +            typeof version === 'number' &&
    +            Number.isFinite(version) &&
    +            version === Math.floor(version)
    +        )) {
    +            throw new Error(`Invalid version: ${version}`);
    +        }
     
    -        if (sanitize !== false) {
    -            _settingsImportSanitizeOptions(optionsFull, false);
    +        if (!(
    +            version >= 0 &&
    +            version <= this._currentVersion
    +        )) {
    +            throw new Error(`Unsupported version: ${version}`);
             }
    -    }
     
    -    // Assign options
    -    await _settingsImportSetOptionsFull(optionsFull);
    +        // Verify options exists
    +        let optionsFull = data.options;
    +        if (!isObject(optionsFull)) {
    +            throw new Error(`Invalid options type: ${typeof optionsFull}`);
    +        }
     
    -    // Reload settings page
    -    window.location.reload();
    -}
    +        // Upgrade options
    +        optionsFull = optionsUpdateVersion(optionsFull, {});
     
    -function _onSettingsImportClick() {
    -    document.querySelector('#settings-import-file').click();
    -}
    +        // Check for warnings
    +        const sanitizationWarnings = this._settingsImportSanitizeOptions(optionsFull, true);
     
    -function _onSettingsImportFileChange(e) {
    -    const files = e.target.files;
    -    if (files.length === 0) { return; }
    +        // Show sanitization warnings
    +        if (sanitizationWarnings.size > 0) {
    +            const {result, sanitize} = await this._showSettingsImportWarnings(sanitizationWarnings);
    +            if (!result) { return; }
     
    -    const file = files[0];
    -    e.target.value = null;
    -    _importSettingsFile(file).catch(_showSettingsImportError);
    -}
    +            if (sanitize !== false) {
    +                this._settingsImportSanitizeOptions(optionsFull, false);
    +            }
    +        }
     
    +        // Assign options
    +        await this._settingsImportSetOptionsFull(optionsFull);
     
    -// Resetting
    +        // Reload settings page
    +        window.location.reload();
    +    }
     
    -function _onSettingsResetClick() {
    -    $('#settings-reset-modal').modal('show');
    -}
    +    _onSettingsImportClick() {
    +        document.querySelector('#settings-import-file').click();
    +    }
     
    -async function _onSettingsResetConfirmClick() {
    -    $('#settings-reset-modal').modal('hide');
    +    async _onSettingsImportFileChange(e) {
    +        const files = e.target.files;
    +        if (files.length === 0) { return; }
     
    -    // Get default options
    -    const optionsFull = optionsGetDefault();
    +        const file = files[0];
    +        e.target.value = null;
    +        try {
    +            await this._importSettingsFile(file);
    +        } catch (error) {
    +            this._showSettingsImportError(error);
    +        }
    +    }
     
    -    // Assign options
    -    await _settingsImportSetOptionsFull(optionsFull);
    +    // Resetting
     
    -    // Reload settings page
    -    window.location.reload();
    -}
    +    _onSettingsResetClick() {
    +        $('#settings-reset-modal').modal('show');
    +    }
     
    +    async _onSettingsResetConfirmClick() {
    +        $('#settings-reset-modal').modal('hide');
     
    -// Setup
    +        // Get default options
    +        const optionsFull = optionsGetDefault();
     
    -function backupInitialize() {
    -    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);
    +        // Assign options
    +        await this._settingsImportSetOptionsFull(optionsFull);
    +
    +        // Reload settings page
    +        window.location.reload();
    +    }
     }
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index 60b9e008..f96167af 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -16,13 +16,13 @@
      */
     
     /* global
    + * SettingsBackup
      * ankiInitialize
      * ankiTemplatesInitialize
      * ankiTemplatesUpdateValue
      * api
      * appearanceInitialize
      * audioSettingsInitialize
    - * backupInitialize
      * dictSettingsInitialize
      * getOptionsContext
      * onAnkiOptionsChanged
    @@ -302,7 +302,7 @@ async function onReady() {
         await dictSettingsInitialize();
         ankiInitialize();
         ankiTemplatesInitialize();
    -    backupInitialize();
    +    new SettingsBackup().prepare();
     
         storageInfoInitialize();
     
    diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
    index 8f86e47a..edc19c6e 100644
    --- a/ext/bg/js/util.js
    +++ b/ext/bg/js/util.js
    @@ -65,12 +65,3 @@ function utilBackend() {
         }
         return backend;
     }
    -
    -function utilReadFileArrayBuffer(file) {
    -    return new Promise((resolve, reject) => {
    -        const reader = new FileReader();
    -        reader.onload = () => resolve(reader.result);
    -        reader.onerror = () => reject(reader.error);
    -        reader.readAsArrayBuffer(file);
    -    });
    -}
    -- 
    cgit v1.2.3
    
    
    From 8537c8f386b7c04f21e62a6b82b179ec9a123ce1 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 19:45:54 -0400
    Subject: Create class to abstract access, mutation, and events for settings
     (#565)
    
    ---
     ext/bg/js/settings/main.js                |  4 ++
     ext/bg/js/settings/settings-controller.js | 83 +++++++++++++++++++++++++++++++
     ext/bg/settings.html                      |  1 +
     3 files changed, 88 insertions(+)
     create mode 100644 ext/bg/js/settings/settings-controller.js
    
    (limited to 'ext/bg/js/settings/main.js')
    
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index f96167af..0cb1734e 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -17,6 +17,7 @@
     
     /* global
      * SettingsBackup
    + * SettingsController
      * ankiInitialize
      * ankiTemplatesInitialize
      * ankiTemplatesUpdateValue
    @@ -292,6 +293,9 @@ async function onReady() {
         api.forwardLogsToBackend();
         await yomichan.prepare();
     
    +    const settingsController = new SettingsController();
    +    settingsController.prepare();
    +
         showExtensionInformation();
     
         await settingsPopulateModifierKeys();
    diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js
    new file mode 100644
    index 00000000..61230226
    --- /dev/null
    +++ b/ext/bg/js/settings/settings-controller.js
    @@ -0,0 +1,83 @@
    +/*
    + * Copyright (C) 2020  Yomichan Authors
    + *
    + * 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 .
    + */
    +
    +/* global
    + * api
    + * utilBackend
    + * utilBackgroundIsolate
    + */
    +
    +class SettingsController extends EventDispatcher {
    +    constructor(profileIndex=0) {
    +        super();
    +        this._profileIndex = profileIndex;
    +        this._source = yomichan.generateId(16);
    +    }
    +
    +    get profileIndex() {
    +        return this._profileIndex;
    +    }
    +
    +    set profileIndex(value) {
    +        if (this._profileIndex === value) { return; }
    +        this._profileIndex = value;
    +        this.trigger('optionsContextChanged');
    +        this._onOptionsUpdatedInternal();
    +    }
    +
    +    prepare() {
    +        yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this));
    +    }
    +
    +    async save() {
    +        await api.optionsSave(this._source);
    +    }
    +
    +    async getOptions() {
    +        const optionsContext = this.getOptionsContext();
    +        return await api.optionsGet(optionsContext);
    +    }
    +
    +    async getOptionsFull() {
    +        return await api.optionsGetFull();
    +    }
    +
    +    async getOptionsMutable() {
    +        const optionsContext = this.getOptionsContext();
    +        return utilBackend().getOptions(utilBackgroundIsolate(optionsContext));
    +    }
    +
    +    async getOptionsFullMutable() {
    +        return utilBackend().getFullOptions();
    +    }
    +
    +    getOptionsContext() {
    +        return {index: this._profileIndex};
    +    }
    +
    +    // Private
    +
    +    _onOptionsUpdated({source}) {
    +        if (source === this._source) { return; }
    +        this._onOptionsUpdatedInternal();
    +    }
    +
    +    async _onOptionsUpdatedInternal() {
    +        const options = await this.getOptions();
    +        this.trigger('optionsChanged', {options});
    +    }
    +}
    diff --git a/ext/bg/settings.html b/ext/bg/settings.html
    index 7964ab90..c8f5b15c 100644
    --- a/ext/bg/settings.html
    +++ b/ext/bg/settings.html
    @@ -1147,6 +1147,7 @@
             
             
             
    +        
             
     
             
    -- 
    cgit v1.2.3
    
    
    From fde0072118128ea698e15472c9b61b17b4827c8a Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 19:47:18 -0400
    Subject: Change profile.js into a class (#566)
    
    * Update how settings profile is stored and accessed
    
    * Convert profiles.js into a class
    
    * Rename members of ProfileController
    ---
     ext/bg/js/settings/main.js     |  20 +-
     ext/bg/js/settings/profiles.js | 422 +++++++++++++++++++++--------------------
     2 files changed, 233 insertions(+), 209 deletions(-)
    
    (limited to 'ext/bg/js/settings/main.js')
    
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index 0cb1734e..64611ad5 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -16,6 +16,7 @@
      */
     
     /* global
    + * ProfileController
      * SettingsBackup
      * SettingsController
      * ankiInitialize
    @@ -25,15 +26,28 @@
      * appearanceInitialize
      * audioSettingsInitialize
      * dictSettingsInitialize
    - * getOptionsContext
      * onAnkiOptionsChanged
      * onDictionaryOptionsChanged
    - * profileOptionsSetup
      * storageInfoInitialize
      * utilBackend
      * utilBackgroundIsolate
      */
     
    +let profileIndex = 0;
    +
    +function getOptionsContext() {
    +    return {index: getProfileIndex()};
    +}
    +
    +function getProfileIndex() {
    +    return profileIndex;
    +}
    +
    +function setProfileIndex(value) {
    +    profileIndex = value;
    +}
    +
    +
     function getOptionsMutable(optionsContext) {
         return utilBackend().getOptions(
             utilBackgroundIsolate(optionsContext)
    @@ -302,7 +316,7 @@ async function onReady() {
         formSetupEventListeners();
         appearanceInitialize();
         await audioSettingsInitialize();
    -    await profileOptionsSetup();
    +    await (new ProfileController()).prepare();
         await dictSettingsInitialize();
         ankiInitialize();
         ankiTemplatesInitialize();
    diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js
    index 59f7fbb1..e2c558e9 100644
    --- a/ext/bg/js/settings/profiles.js
    +++ b/ext/bg/js/settings/profiles.js
    @@ -20,281 +20,291 @@
      * api
      * conditionsClearCaches
      * getOptionsFullMutable
    + * getProfileIndex
      * onOptionsUpdated
      * profileConditionsDescriptor
      * profileConditionsDescriptorPromise
    + * setProfileIndex
      * settingsSaveOptions
      * utilBackgroundIsolate
      */
     
    -let currentProfileIndex = 0;
    -let profileConditionsContainer = null;
    -
    -function getOptionsContext() {
    -    return {
    -        index: currentProfileIndex
    -    };
    -}
    +class ProfileController {
    +    constructor() {
    +        this._conditionsContainer = null;
    +    }
     
    +    async prepare() {
    +        const optionsFull = await getOptionsFullMutable();
    +        setProfileIndex(optionsFull.profileCurrent);
     
    -async function profileOptionsSetup() {
    -    const optionsFull = await getOptionsFullMutable();
    -    currentProfileIndex = optionsFull.profileCurrent;
    +        this._setupEventListeners();
    +        await this._updateTarget(optionsFull);
    +    }
     
    -    profileOptionsSetupEventListeners();
    -    await profileOptionsUpdateTarget(optionsFull);
    -}
    +    // Private
    +
    +    _setupEventListeners() {
    +        $('#profile-target').change(this._onTargetProfileChanged.bind(this));
    +        $('#profile-name').change(this._onNameChanged.bind(this));
    +        $('#profile-add').click(this._onAdd.bind(this));
    +        $('#profile-remove').click(this._onRemove.bind(this));
    +        $('#profile-remove-confirm').click(this._onRemoveConfirm.bind(this));
    +        $('#profile-copy').click(this._onCopy.bind(this));
    +        $('#profile-copy-confirm').click(this._onCopyConfirm.bind(this));
    +        $('#profile-move-up').click(() => this._onMove(-1));
    +        $('#profile-move-down').click(() => this._onMove(1));
    +        $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(this._onInputChanged.bind(this));
    +    }
     
    -function profileOptionsSetupEventListeners() {
    -    $('#profile-target').change(onTargetProfileChanged);
    -    $('#profile-name').change(onProfileNameChanged);
    -    $('#profile-add').click(onProfileAdd);
    -    $('#profile-remove').click(onProfileRemove);
    -    $('#profile-remove-confirm').click(onProfileRemoveConfirm);
    -    $('#profile-copy').click(onProfileCopy);
    -    $('#profile-copy-confirm').click(onProfileCopyConfirm);
    -    $('#profile-move-up').click(() => onProfileMove(-1));
    -    $('#profile-move-down').click(() => onProfileMove(1));
    -    $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(onProfileOptionsChanged);
    -}
    +    _tryGetIntegerValue(selector, min, max) {
    +        const value = parseInt($(selector).val(), 10);
    +        return (
    +            typeof value === 'number' &&
    +            Number.isFinite(value) &&
    +            Math.floor(value) === value &&
    +            value >= min &&
    +            value < max
    +        ) ? value : null;
    +    }
     
    -function tryGetIntegerValue(selector, min, max) {
    -    const value = parseInt($(selector).val(), 10);
    -    return (
    -        typeof value === 'number' &&
    -        Number.isFinite(value) &&
    -        Math.floor(value) === value &&
    -        value >= min &&
    -        value < max
    -    ) ? value : null;
    -}
    +    async _formRead(optionsFull) {
    +        const currentProfileIndex = getProfileIndex();
    +        const profile = optionsFull.profiles[currentProfileIndex];
     
    -async function profileFormRead(optionsFull) {
    -    const profile = optionsFull.profiles[currentProfileIndex];
    +        // Current profile
    +        const index = this._tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length);
    +        if (index !== null) {
    +            optionsFull.profileCurrent = index;
    +        }
     
    -    // Current profile
    -    const index = tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length);
    -    if (index !== null) {
    -        optionsFull.profileCurrent = index;
    +        // Profile name
    +        profile.name = $('#profile-name').val();
         }
     
    -    // Profile name
    -    profile.name = $('#profile-name').val();
    -}
    +    async _formWrite(optionsFull) {
    +        const currentProfileIndex = getProfileIndex();
    +        const profile = optionsFull.profiles[currentProfileIndex];
     
    -async function profileFormWrite(optionsFull) {
    -    const profile = optionsFull.profiles[currentProfileIndex];
    +        this._populateSelect($('#profile-active'), optionsFull.profiles, optionsFull.profileCurrent, null);
    +        this._populateSelect($('#profile-target'), optionsFull.profiles, currentProfileIndex, null);
    +        $('#profile-remove').prop('disabled', optionsFull.profiles.length <= 1);
    +        $('#profile-copy').prop('disabled', optionsFull.profiles.length <= 1);
    +        $('#profile-move-up').prop('disabled', currentProfileIndex <= 0);
    +        $('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1);
     
    -    profileOptionsPopulateSelect($('#profile-active'), optionsFull.profiles, optionsFull.profileCurrent, null);
    -    profileOptionsPopulateSelect($('#profile-target'), optionsFull.profiles, currentProfileIndex, null);
    -    $('#profile-remove').prop('disabled', optionsFull.profiles.length <= 1);
    -    $('#profile-copy').prop('disabled', optionsFull.profiles.length <= 1);
    -    $('#profile-move-up').prop('disabled', currentProfileIndex <= 0);
    -    $('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1);
    +        $('#profile-name').val(profile.name);
     
    -    $('#profile-name').val(profile.name);
    +        if (this._conditionsContainer !== null) {
    +            this._conditionsContainer.cleanup();
    +        }
     
    -    if (profileConditionsContainer !== null) {
    -        profileConditionsContainer.cleanup();
    +        await profileConditionsDescriptorPromise;
    +        this._conditionsContainer = new ConditionsUI.Container(
    +            profileConditionsDescriptor,
    +            'popupLevel',
    +            profile.conditionGroups,
    +            $('#profile-condition-groups'),
    +            $('#profile-add-condition-group')
    +        );
    +        this._conditionsContainer.save = () => {
    +            settingsSaveOptions();
    +            conditionsClearCaches(profileConditionsDescriptor);
    +        };
    +        this._conditionsContainer.isolate = utilBackgroundIsolate;
         }
     
    -    await profileConditionsDescriptorPromise;
    -    profileConditionsContainer = new ConditionsUI.Container(
    -        profileConditionsDescriptor,
    -        'popupLevel',
    -        profile.conditionGroups,
    -        $('#profile-condition-groups'),
    -        $('#profile-add-condition-group')
    -    );
    -    profileConditionsContainer.save = () => {
    -        settingsSaveOptions();
    -        conditionsClearCaches(profileConditionsDescriptor);
    -    };
    -    profileConditionsContainer.isolate = utilBackgroundIsolate;
    -}
    -
    -function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) {
    -    select.empty();
    +    _populateSelect(select, profiles, currentValue, ignoreIndices) {
    +        select.empty();
     
     
    -    for (let i = 0; i < profiles.length; ++i) {
    -        if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) {
    -            continue;
    +        for (let i = 0; i < profiles.length; ++i) {
    +            if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) {
    +                continue;
    +            }
    +            const profile = profiles[i];
    +            select.append($(``));
             }
    -        const profile = profiles[i];
    -        select.append($(``));
    -    }
     
    -    select.val(`${currentValue}`);
    -}
    +        select.val(`${currentValue}`);
    +    }
     
    -async function profileOptionsUpdateTarget(optionsFull) {
    -    await profileFormWrite(optionsFull);
    -    await onOptionsUpdated({source: null});
    -}
    +    async _updateTarget(optionsFull) {
    +        await this._formWrite(optionsFull);
    +        await onOptionsUpdated({source: null});
    +    }
     
    -function profileOptionsCreateCopyName(name, profiles, maxUniqueAttempts) {
    -    let space, index, prefix, suffix;
    -    const match = /^([\w\W]*\(Copy)((\s+)(\d+))?(\)\s*)$/.exec(name);
    -    if (match === null) {
    -        prefix = `${name} (Copy`;
    -        space = '';
    -        index = '';
    -        suffix = ')';
    -    } else {
    -        prefix = match[1];
    -        suffix = match[5];
    -        if (typeof match[2] === 'string') {
    -            space = match[3];
    -            index = parseInt(match[4], 10) + 1;
    +    _createCopyName(name, profiles, maxUniqueAttempts) {
    +        let space, index, prefix, suffix;
    +        const match = /^([\w\W]*\(Copy)((\s+)(\d+))?(\)\s*)$/.exec(name);
    +        if (match === null) {
    +            prefix = `${name} (Copy`;
    +            space = '';
    +            index = '';
    +            suffix = ')';
             } else {
    -            space = ' ';
    -            index = 2;
    +            prefix = match[1];
    +            suffix = match[5];
    +            if (typeof match[2] === 'string') {
    +                space = match[3];
    +                index = parseInt(match[4], 10) + 1;
    +            } else {
    +                space = ' ';
    +                index = 2;
    +            }
             }
    -    }
     
    -    let i = 0;
    -    while (true) {
    -        const newName = `${prefix}${space}${index}${suffix}`;
    -        if (i++ >= maxUniqueAttempts || profiles.findIndex((profile) => profile.name === newName) < 0) {
    -            return newName;
    -        }
    -        if (typeof index !== 'number') {
    -            index = 2;
    -            space = ' ';
    -        } else {
    -            ++index;
    +        let i = 0;
    +        while (true) {
    +            const newName = `${prefix}${space}${index}${suffix}`;
    +            if (i++ >= maxUniqueAttempts || profiles.findIndex((profile) => profile.name === newName) < 0) {
    +                return newName;
    +            }
    +            if (typeof index !== 'number') {
    +                index = 2;
    +                space = ' ';
    +            } else {
    +                ++index;
    +            }
             }
         }
    -}
     
    -async function onProfileOptionsChanged(e) {
    -    if (!e.originalEvent && !e.isTrigger) {
    -        return;
    +    async _onInputChanged(e) {
    +        if (!e.originalEvent && !e.isTrigger) {
    +            return;
    +        }
    +
    +        const optionsFull = await getOptionsFullMutable();
    +        await this._formRead(optionsFull);
    +        await settingsSaveOptions();
         }
     
    -    const optionsFull = await getOptionsFullMutable();
    -    await profileFormRead(optionsFull);
    -    await settingsSaveOptions();
    -}
    +    async _onTargetProfileChanged() {
    +        const optionsFull = await getOptionsFullMutable();
    +        const currentProfileIndex = getProfileIndex();
    +        const index = this._tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length);
    +        if (index === null || currentProfileIndex === index) {
    +            return;
    +        }
     
    -async function onTargetProfileChanged() {
    -    const optionsFull = await getOptionsFullMutable();
    -    const index = tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length);
    -    if (index === null || currentProfileIndex === index) {
    -        return;
    +        setProfileIndex(index);
    +
    +        await this._updateTarget(optionsFull);
    +
    +        yomichan.trigger('modifyingProfileChange');
         }
     
    -    currentProfileIndex = index;
    +    async _onAdd() {
    +        const optionsFull = await getOptionsFullMutable();
    +        const currentProfileIndex = getProfileIndex();
    +        const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]);
    +        profile.name = this._createCopyName(profile.name, optionsFull.profiles, 100);
    +        optionsFull.profiles.push(profile);
     
    -    await profileOptionsUpdateTarget(optionsFull);
    +        setProfileIndex(optionsFull.profiles.length - 1);
     
    -    yomichan.trigger('modifyingProfileChange');
    -}
    +        await this._updateTarget(optionsFull);
    +        await settingsSaveOptions();
     
    -async function onProfileAdd() {
    -    const optionsFull = await getOptionsFullMutable();
    -    const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]);
    -    profile.name = profileOptionsCreateCopyName(profile.name, optionsFull.profiles, 100);
    -    optionsFull.profiles.push(profile);
    +        yomichan.trigger('modifyingProfileChange');
    +    }
     
    -    currentProfileIndex = optionsFull.profiles.length - 1;
    +    async _onRemove(e) {
    +        if (e.shiftKey) {
    +            return await this._onRemoveConfirm();
    +        }
     
    -    await profileOptionsUpdateTarget(optionsFull);
    -    await settingsSaveOptions();
    +        const optionsFull = await api.optionsGetFull();
    +        if (optionsFull.profiles.length <= 1) {
    +            return;
    +        }
     
    -    yomichan.trigger('modifyingProfileChange');
    -}
    +        const currentProfileIndex = getProfileIndex();
    +        const profile = optionsFull.profiles[currentProfileIndex];
     
    -async function onProfileRemove(e) {
    -    if (e.shiftKey) {
    -        return await onProfileRemoveConfirm();
    +        $('#profile-remove-modal-profile-name').text(profile.name);
    +        $('#profile-remove-modal').modal('show');
         }
     
    -    const optionsFull = await api.optionsGetFull();
    -    if (optionsFull.profiles.length <= 1) {
    -        return;
    -    }
    +    async _onRemoveConfirm() {
    +        $('#profile-remove-modal').modal('hide');
     
    -    const profile = optionsFull.profiles[currentProfileIndex];
    +        const optionsFull = await getOptionsFullMutable();
    +        if (optionsFull.profiles.length <= 1) {
    +            return;
    +        }
     
    -    $('#profile-remove-modal-profile-name').text(profile.name);
    -    $('#profile-remove-modal').modal('show');
    -}
    +        const currentProfileIndex = getProfileIndex();
    +        optionsFull.profiles.splice(currentProfileIndex, 1);
     
    -async function onProfileRemoveConfirm() {
    -    $('#profile-remove-modal').modal('hide');
    +        if (currentProfileIndex >= optionsFull.profiles.length) {
    +            setProfileIndex(optionsFull.profiles.length - 1);
    +        }
     
    -    const optionsFull = await getOptionsFullMutable();
    -    if (optionsFull.profiles.length <= 1) {
    -        return;
    -    }
    +        if (optionsFull.profileCurrent >= optionsFull.profiles.length) {
    +            optionsFull.profileCurrent = optionsFull.profiles.length - 1;
    +        }
     
    -    optionsFull.profiles.splice(currentProfileIndex, 1);
    +        await this._updateTarget(optionsFull);
    +        await settingsSaveOptions();
     
    -    if (currentProfileIndex >= optionsFull.profiles.length) {
    -        --currentProfileIndex;
    +        yomichan.trigger('modifyingProfileChange');
         }
     
    -    if (optionsFull.profileCurrent >= optionsFull.profiles.length) {
    -        optionsFull.profileCurrent = optionsFull.profiles.length - 1;
    +    _onNameChanged() {
    +        const currentProfileIndex = getProfileIndex();
    +        $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value);
         }
     
    -    await profileOptionsUpdateTarget(optionsFull);
    -    await settingsSaveOptions();
    +    async _onMove(offset) {
    +        const optionsFull = await getOptionsFullMutable();
    +        const currentProfileIndex = getProfileIndex();
    +        const index = currentProfileIndex + offset;
    +        if (index < 0 || index >= optionsFull.profiles.length) {
    +            return;
    +        }
     
    -    yomichan.trigger('modifyingProfileChange');
    -}
    +        const profile = optionsFull.profiles[currentProfileIndex];
    +        optionsFull.profiles.splice(currentProfileIndex, 1);
    +        optionsFull.profiles.splice(index, 0, profile);
     
    -function onProfileNameChanged() {
    -    $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value);
    -}
    +        if (optionsFull.profileCurrent === currentProfileIndex) {
    +            optionsFull.profileCurrent = index;
    +        }
     
    -async function onProfileMove(offset) {
    -    const optionsFull = await getOptionsFullMutable();
    -    const index = currentProfileIndex + offset;
    -    if (index < 0 || index >= optionsFull.profiles.length) {
    -        return;
    -    }
    +        setProfileIndex(index);
     
    -    const profile = optionsFull.profiles[currentProfileIndex];
    -    optionsFull.profiles.splice(currentProfileIndex, 1);
    -    optionsFull.profiles.splice(index, 0, profile);
    +        await this._updateTarget(optionsFull);
    +        await settingsSaveOptions();
     
    -    if (optionsFull.profileCurrent === currentProfileIndex) {
    -        optionsFull.profileCurrent = index;
    +        yomichan.trigger('modifyingProfileChange');
         }
     
    -    currentProfileIndex = index;
    -
    -    await profileOptionsUpdateTarget(optionsFull);
    -    await settingsSaveOptions();
    -
    -    yomichan.trigger('modifyingProfileChange');
    -}
    +    async _onCopy() {
    +        const optionsFull = await api.optionsGetFull();
    +        if (optionsFull.profiles.length <= 1) {
    +            return;
    +        }
     
    -async function onProfileCopy() {
    -    const optionsFull = await api.optionsGetFull();
    -    if (optionsFull.profiles.length <= 1) {
    -        return;
    +        const currentProfileIndex = getProfileIndex();
    +        this._populateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]);
    +        $('#profile-copy-modal').modal('show');
         }
     
    -    profileOptionsPopulateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]);
    -    $('#profile-copy-modal').modal('show');
    -}
    -
    -async function onProfileCopyConfirm() {
    -    $('#profile-copy-modal').modal('hide');
    +    async _onCopyConfirm() {
    +        $('#profile-copy-modal').modal('hide');
     
    -    const optionsFull = await getOptionsFullMutable();
    -    const index = tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length);
    -    if (index === null || index === currentProfileIndex) {
    -        return;
    -    }
    +        const optionsFull = await getOptionsFullMutable();
    +        const index = this._tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length);
    +        const currentProfileIndex = getProfileIndex();
    +        if (index === null || index === currentProfileIndex) {
    +            return;
    +        }
     
    -    const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options);
    -    optionsFull.profiles[currentProfileIndex].options = profileOptions;
    +        const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options);
    +        optionsFull.profiles[currentProfileIndex].options = profileOptions;
     
    -    await profileOptionsUpdateTarget(optionsFull);
    -    await settingsSaveOptions();
    +        await this._updateTarget(optionsFull);
    +        await settingsSaveOptions();
    +    }
     }
    -- 
    cgit v1.2.3
    
    
    From 5f9889fd26f38396aa6ffaa5c669081b02467393 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 19:52:51 -0400
    Subject: Anki settings controllers (#567)
    
    * Convert anki-templates.js to a class
    
    * Convert anki.js to a class
    ---
     ext/bg/js/settings/anki-templates.js | 216 ++++++++--------
     ext/bg/js/settings/anki.js           | 464 +++++++++++++++++------------------
     ext/bg/js/settings/main.js           |  22 +-
     3 files changed, 354 insertions(+), 348 deletions(-)
    
    (limited to 'ext/bg/js/settings/main.js')
    
    diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js
    index 0dadb433..dd128ab8 100644
    --- a/ext/bg/js/settings/anki-templates.js
    +++ b/ext/bg/js/settings/anki-templates.js
    @@ -17,141 +17,147 @@
     
     /* global
      * AnkiNoteBuilder
    - * ankiGetFieldMarkers
    - * ankiGetFieldMarkersHtml
      * api
      * getOptionsContext
      * getOptionsMutable
      * settingsSaveOptions
      */
     
    -function onAnkiFieldTemplatesReset(e) {
    -    e.preventDefault();
    -    $('#field-template-reset-modal').modal('show');
    -}
    -
    -async function onAnkiFieldTemplatesResetConfirm(e) {
    -    e.preventDefault();
    +class AnkiTemplatesController {
    +    constructor(ankiController) {
    +        this._ankiController = ankiController;
    +        this._cachedDefinitionValue = null;
    +        this._cachedDefinitionText = null;
    +    }
     
    -    $('#field-template-reset-modal').modal('hide');
    +    prepare() {
    +        const markers = new Set([
    +            ...this._ankiController.getFieldMarkers('terms'),
    +            ...this._ankiController.getFieldMarkers('kanji')
    +        ]);
    +        const fragment = this._ankiController.getFieldMarkersHtml(markers);
    +
    +        const list = document.querySelector('#field-templates-list');
    +        list.appendChild(fragment);
    +        for (const node of list.querySelectorAll('.marker-link')) {
    +            node.addEventListener('click', this._onMarkerClicked.bind(this), false);
    +        }
     
    -    const value = await api.getDefaultAnkiFieldTemplates();
    +        $('#field-templates').on('change', this._onChanged.bind(this));
    +        $('#field-template-render').on('click', this._onRender.bind(this));
    +        $('#field-templates-reset').on('click', this._onReset.bind(this));
    +        $('#field-templates-reset-confirm').on('click', this._onResetConfirm.bind(this));
     
    -    const element = document.querySelector('#field-templates');
    -    element.value = value;
    -    element.dispatchEvent(new Event('change'));
    -}
    +        this.updateValue();
    +    }
     
    -function ankiTemplatesInitialize() {
    -    const markers = new Set(ankiGetFieldMarkers('terms').concat(ankiGetFieldMarkers('kanji')));
    -    const fragment = ankiGetFieldMarkersHtml(markers);
    +    async updateValue() {
    +        const optionsContext = getOptionsContext();
    +        const options = await api.optionsGet(optionsContext);
    +        let templates = options.anki.fieldTemplates;
    +        if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); }
    +        $('#field-templates').val(templates);
     
    -    const list = document.querySelector('#field-templates-list');
    -    list.appendChild(fragment);
    -    for (const node of list.querySelectorAll('.marker-link')) {
    -        node.addEventListener('click', onAnkiTemplateMarkerClicked, false);
    +        this._onValidateCompile();
         }
     
    -    $('#field-templates').on('change', onAnkiFieldTemplatesChanged);
    -    $('#field-template-render').on('click', onAnkiTemplateRender);
    -    $('#field-templates-reset').on('click', onAnkiFieldTemplatesReset);
    -    $('#field-templates-reset-confirm').on('click', onAnkiFieldTemplatesResetConfirm);
    +    // Private
     
    -    ankiTemplatesUpdateValue();
    -}
    +    _onReset(e) {
    +        e.preventDefault();
    +        $('#field-template-reset-modal').modal('show');
    +    }
     
    -async function ankiTemplatesUpdateValue() {
    -    const optionsContext = getOptionsContext();
    -    const options = await api.optionsGet(optionsContext);
    -    let templates = options.anki.fieldTemplates;
    -    if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); }
    -    $('#field-templates').val(templates);
    +    async _onResetConfirm(e) {
    +        e.preventDefault();
     
    -    onAnkiTemplatesValidateCompile();
    -}
    +        $('#field-template-reset-modal').modal('hide');
     
    -const ankiTemplatesValidateGetDefinition = (() => {
    -    let cachedValue = null;
    -    let cachedText = null;
    +        const value = await api.getDefaultAnkiFieldTemplates();
     
    -    return async (text, optionsContext) => {
    -        if (cachedText !== text) {
    -            const {definitions} = await api.termsFind(text, {}, optionsContext);
    -            if (definitions.length === 0) { return null; }
    +        const element = document.querySelector('#field-templates');
    +        element.value = value;
    +        element.dispatchEvent(new Event('change'));
    +    }
     
    -            cachedValue = definitions[0];
    -            cachedText = text;
    +    async _onChanged(e) {
    +        // Get value
    +        let templates = e.currentTarget.value;
    +        if (templates === await api.getDefaultAnkiFieldTemplates()) {
    +            // Default
    +            templates = null;
             }
    -        return cachedValue;
    -    };
    -})();
    -
    -async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, invalidateInput) {
    -    const text = document.querySelector('#field-templates-preview-text').value || '';
    -    const exceptions = [];
    -    let result = `No definition found for ${text}`;
    -    try {
    +
    +        // Overwrite
             const optionsContext = getOptionsContext();
    -        const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext);
    -        if (definition !== null) {
    -            const options = await api.optionsGet(optionsContext);
    -            const context = {
    -                document: {
    -                    title: document.title
    -                }
    -            };
    -            let templates = options.anki.fieldTemplates;
    -            if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); }
    -            const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: api.templateRender.bind(api)});
    -            result = await ankiNoteBuilder.formatField(field, definition, mode, context, options, templates, exceptions);
    -        }
    -    } catch (e) {
    -        exceptions.push(e);
    +        const options = await getOptionsMutable(optionsContext);
    +        options.anki.fieldTemplates = templates;
    +        await settingsSaveOptions();
    +
    +        // Compile
    +        this._onValidateCompile();
         }
     
    -    const hasException = exceptions.length > 0;
    -    infoNode.hidden = !(showSuccessResult || hasException);
    -    infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : '');
    -    infoNode.classList.toggle('text-danger', hasException);
    -    if (invalidateInput) {
    -        const input = document.querySelector('#field-templates');
    -        input.classList.toggle('is-invalid', hasException);
    +    _onValidateCompile() {
    +        const infoNode = document.querySelector('#field-template-compile-result');
    +        this._validate(infoNode, '{expression}', 'term-kanji', false, true);
         }
    -}
     
    -async function onAnkiFieldTemplatesChanged(e) {
    -    // Get value
    -    let templates = e.currentTarget.value;
    -    if (templates === await api.getDefaultAnkiFieldTemplates()) {
    -        // Default
    -        templates = null;
    +    _onMarkerClicked(e) {
    +        e.preventDefault();
    +        document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`;
         }
     
    -    // Overwrite
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    -    options.anki.fieldTemplates = templates;
    -    await settingsSaveOptions();
    +    _onRender(e) {
    +        e.preventDefault();
     
    -    // Compile
    -    onAnkiTemplatesValidateCompile();
    -}
    +        const field = document.querySelector('#field-template-render-text').value;
    +        const infoNode = document.querySelector('#field-template-render-result');
    +        infoNode.hidden = true;
    +        this._validate(infoNode, field, 'term-kanji', true, false);
    +    }
     
    -function onAnkiTemplatesValidateCompile() {
    -    const infoNode = document.querySelector('#field-template-compile-result');
    -    ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true);
    -}
    +    async _getDefinition(text, optionsContext) {
    +        if (this._cachedDefinitionText !== text) {
    +            const {definitions} = await api.termsFind(text, {}, optionsContext);
    +            if (definitions.length === 0) { return null; }
     
    -function onAnkiTemplateMarkerClicked(e) {
    -    e.preventDefault();
    -    document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`;
    -}
    +            this._cachedDefinitionValue = definitions[0];
    +            this._cachedDefinitionText = text;
    +        }
    +        return this._cachedDefinitionValue;
    +    }
     
    -function onAnkiTemplateRender(e) {
    -    e.preventDefault();
    +    async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) {
    +        const text = document.querySelector('#field-templates-preview-text').value || '';
    +        const exceptions = [];
    +        let result = `No definition found for ${text}`;
    +        try {
    +            const optionsContext = getOptionsContext();
    +            const definition = await this._getDefinition(text, optionsContext);
    +            if (definition !== null) {
    +                const options = await api.optionsGet(optionsContext);
    +                const context = {
    +                    document: {
    +                        title: document.title
    +                    }
    +                };
    +                let templates = options.anki.fieldTemplates;
    +                if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); }
    +                const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: api.templateRender.bind(api)});
    +                result = await ankiNoteBuilder.formatField(field, definition, mode, context, options, templates, exceptions);
    +            }
    +        } catch (e) {
    +            exceptions.push(e);
    +        }
     
    -    const field = document.querySelector('#field-template-render-text').value;
    -    const infoNode = document.querySelector('#field-template-render-result');
    -    infoNode.hidden = true;
    -    ankiTemplatesValidate(infoNode, field, 'term-kanji', true, false);
    +        const hasException = exceptions.length > 0;
    +        infoNode.hidden = !(showSuccessResult || hasException);
    +        infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : '');
    +        infoNode.classList.toggle('text-danger', hasException);
    +        if (invalidateInput) {
    +            const input = document.querySelector('#field-templates');
    +            input.classList.toggle('is-invalid', hasException);
    +        }
    +    }
     }
    diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js
    index 8de97554..d110ef39 100644
    --- a/ext/bg/js/settings/anki.js
    +++ b/ext/bg/js/settings/anki.js
    @@ -23,287 +23,281 @@
      * utilBackgroundIsolate
      */
     
    -// Private
    -
    -function _ankiSpinnerShow(show) {
    -    const spinner = $('#anki-spinner');
    -    if (show) {
    -        spinner.show();
    -    } else {
    -        spinner.hide();
    -    }
    -}
    +class AnkiController {
    +    prepare() {
    +        $('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(this._onFieldsChanged.bind(this));
     
    -function _ankiSetError(error) {
    -    const node = document.querySelector('#anki-error');
    -    const node2 = document.querySelector('#anki-invalid-response-error');
    -    if (error) {
    -        const errorString = `${error}`;
    -        if (node !== null) {
    -            node.hidden = false;
    -            node.textContent = errorString;
    -            _ankiSetErrorData(node, error);
    +        for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
    +            node.addEventListener('change', this._onModelChanged.bind(this), false);
             }
     
    -        if (node2 !== null) {
    -            node2.hidden = (errorString.indexOf('Invalid response') < 0);
    -        }
    -    } else {
    -        if (node !== null) {
    -            node.hidden = true;
    -            node.textContent = '';
    +        this.optionsChanged();
    +    }
    +
    +    async optionsChanged(options=null) {
    +        if (options === null) {
    +            const optionsContext = getOptionsContext();
    +            options = await getOptionsMutable(optionsContext);
             }
     
    -        if (node2 !== null) {
    -            node2.hidden = true;
    +        if (!options.anki.enable) {
    +            return;
             }
    -    }
    -}
     
    -function _ankiSetErrorData(node, error) {
    -    const data = error.data;
    -    let message = '';
    -    if (typeof data !== 'undefined') {
    -        message += `${JSON.stringify(data, null, 4)}\n\n`;
    +        await this._deckAndModelPopulate(options);
    +        await Promise.all([
    +            this._fieldsPopulate('terms', options),
    +            this._fieldsPopulate('kanji', options)
    +        ]);
         }
    -    message += `${error.stack}`.trimRight();
     
    -    const button = document.createElement('a');
    -    button.className = 'error-data-show-button';
    -
    -    const content = document.createElement('div');
    -    content.className = 'error-data-container';
    -    content.textContent = message;
    -    content.hidden = true;
    +    getFieldMarkers(type) {
    +        switch (type) {
    +            case 'terms':
    +                return [
    +                    'audio',
    +                    'cloze-body',
    +                    'cloze-prefix',
    +                    'cloze-suffix',
    +                    'dictionary',
    +                    'document-title',
    +                    'expression',
    +                    'furigana',
    +                    'furigana-plain',
    +                    'glossary',
    +                    'glossary-brief',
    +                    'reading',
    +                    'screenshot',
    +                    'sentence',
    +                    'tags',
    +                    'url'
    +                ];
    +            case 'kanji':
    +                return [
    +                    'character',
    +                    'dictionary',
    +                    'document-title',
    +                    'glossary',
    +                    'kunyomi',
    +                    'onyomi',
    +                    'screenshot',
    +                    'sentence',
    +                    'tags',
    +                    'url'
    +                ];
    +            default:
    +                return [];
    +        }
    +    }
     
    -    button.addEventListener('click', () => content.hidden = !content.hidden, false);
    +    getFieldMarkersHtml(markers) {
    +        const template = document.querySelector('#anki-field-marker-template').content;
    +        const fragment = document.createDocumentFragment();
    +        for (const marker of markers) {
    +            const markerNode = document.importNode(template, true).firstChild;
    +            markerNode.querySelector('.marker-link').textContent = marker;
    +            fragment.appendChild(markerNode);
    +        }
    +        return fragment;
    +    }
     
    -    node.appendChild(button);
    -    node.appendChild(content);
    -}
    +    // Private
     
    -function _ankiSetDropdownOptions(dropdown, optionValues) {
    -    const fragment = document.createDocumentFragment();
    -    for (const optionValue of optionValues) {
    -        const option = document.createElement('option');
    -        option.value = optionValue;
    -        option.textContent = optionValue;
    -        fragment.appendChild(option);
    +    _fieldsToDict(elements) {
    +        const result = {};
    +        for (const element of elements) {
    +            result[element.dataset.field] = element.value;
    +        }
    +        return result;
         }
    -    dropdown.textContent = '';
    -    dropdown.appendChild(fragment);
    -}
     
    -async function _ankiDeckAndModelPopulate(options) {
    -    const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'};
    -    const kanjiDeck = {value: options.anki.kanji.deck, selector: '#anki-kanji-deck'};
    -    const termsModel = {value: options.anki.terms.model, selector: '#anki-terms-model'};
    -    const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'};
    -    try {
    -        _ankiSpinnerShow(true);
    -        const [deckNames, modelNames] = await Promise.all([api.getAnkiDeckNames(), api.getAnkiModelNames()]);
    -        deckNames.sort();
    -        modelNames.sort();
    -        termsDeck.values = deckNames;
    -        kanjiDeck.values = deckNames;
    -        termsModel.values = modelNames;
    -        kanjiModel.values = modelNames;
    -        _ankiSetError(null);
    -    } catch (error) {
    -        _ankiSetError(error);
    -    } finally {
    -        _ankiSpinnerShow(false);
    +    _spinnerShow(show) {
    +        const spinner = $('#anki-spinner');
    +        if (show) {
    +            spinner.show();
    +        } else {
    +            spinner.hide();
    +        }
         }
     
    -    for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
    -        const node = document.querySelector(selector);
    -        _ankiSetDropdownOptions(node, Array.isArray(values) ? values : [value]);
    -        node.value = value;
    +    _setError(error) {
    +        const node = document.querySelector('#anki-error');
    +        const node2 = document.querySelector('#anki-invalid-response-error');
    +        if (error) {
    +            const errorString = `${error}`;
    +            if (node !== null) {
    +                node.hidden = false;
    +                node.textContent = errorString;
    +                this._setErrorData(node, error);
    +            }
    +
    +            if (node2 !== null) {
    +                node2.hidden = (errorString.indexOf('Invalid response') < 0);
    +            }
    +        } else {
    +            if (node !== null) {
    +                node.hidden = true;
    +                node.textContent = '';
    +            }
    +
    +            if (node2 !== null) {
    +                node2.hidden = true;
    +            }
    +        }
         }
    -}
    -
    -function _ankiCreateFieldTemplate(name, value, markers) {
    -    const template = document.querySelector('#anki-field-template').content;
    -    const content = document.importNode(template, true).firstChild;
    -
    -    content.querySelector('.anki-field-name').textContent = name;
    -
    -    const field = content.querySelector('.anki-field-value');
    -    field.dataset.field = name;
    -    field.value = value;
     
    -    content.querySelector('.anki-field-marker-list').appendChild(ankiGetFieldMarkersHtml(markers));
    +    _setErrorData(node, error) {
    +        const data = error.data;
    +        let message = '';
    +        if (typeof data !== 'undefined') {
    +            message += `${JSON.stringify(data, null, 4)}\n\n`;
    +        }
    +        message += `${error.stack}`.trimRight();
     
    -    return content;
    -}
    +        const button = document.createElement('a');
    +        button.className = 'error-data-show-button';
     
    -async function _ankiFieldsPopulate(tabId, options) {
    -    const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`);
    -    const container = tab.querySelector('tbody');
    -    const markers = ankiGetFieldMarkers(tabId);
    -
    -    const fragment = document.createDocumentFragment();
    -    const fields = options.anki[tabId].fields;
    -    for (const name of Object.keys(fields)) {
    -        const value = fields[name];
    -        const html = _ankiCreateFieldTemplate(name, value, markers);
    -        fragment.appendChild(html);
    -    }
    +        const content = document.createElement('div');
    +        content.className = 'error-data-container';
    +        content.textContent = message;
    +        content.hidden = true;
     
    -    container.textContent = '';
    -    container.appendChild(fragment);
    +        button.addEventListener('click', () => content.hidden = !content.hidden, false);
     
    -    for (const node of container.querySelectorAll('.anki-field-value')) {
    -        node.addEventListener('change', _onAnkiFieldsChanged, false);
    -    }
    -    for (const node of container.querySelectorAll('.marker-link')) {
    -        node.addEventListener('click', _onAnkiMarkerClicked, false);
    +        node.appendChild(button);
    +        node.appendChild(content);
         }
    -}
    -
    -function _onAnkiMarkerClicked(e) {
    -    e.preventDefault();
    -    const link = e.currentTarget;
    -    const input = $(link).closest('.input-group').find('.anki-field-value')[0];
    -    input.value = `{${link.textContent}}`;
    -    input.dispatchEvent(new Event('change'));
    -}
     
    -async function _onAnkiModelChanged(e) {
    -    const node = e.currentTarget;
    -    let fieldNames;
    -    try {
    -        const modelName = node.value;
    -        fieldNames = await api.getAnkiModelFieldNames(modelName);
    -        _ankiSetError(null);
    -    } catch (error) {
    -        _ankiSetError(error);
    -        return;
    -    } finally {
    -        _ankiSpinnerShow(false);
    +    _setDropdownOptions(dropdown, optionValues) {
    +        const fragment = document.createDocumentFragment();
    +        for (const optionValue of optionValues) {
    +            const option = document.createElement('option');
    +            option.value = optionValue;
    +            option.textContent = optionValue;
    +            fragment.appendChild(option);
    +        }
    +        dropdown.textContent = '';
    +        dropdown.appendChild(fragment);
         }
     
    -    const tabId = node.dataset.ankiCardType;
    -    if (tabId !== 'terms' && tabId !== 'kanji') { return; }
    +    async _deckAndModelPopulate(options) {
    +        const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'};
    +        const kanjiDeck = {value: options.anki.kanji.deck, selector: '#anki-kanji-deck'};
    +        const termsModel = {value: options.anki.terms.model, selector: '#anki-terms-model'};
    +        const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'};
    +        try {
    +            this._spinnerShow(true);
    +            const [deckNames, modelNames] = await Promise.all([api.getAnkiDeckNames(), api.getAnkiModelNames()]);
    +            deckNames.sort();
    +            modelNames.sort();
    +            termsDeck.values = deckNames;
    +            kanjiDeck.values = deckNames;
    +            termsModel.values = modelNames;
    +            kanjiModel.values = modelNames;
    +            this._setError(null);
    +        } catch (error) {
    +            this._setError(error);
    +        } finally {
    +            this._spinnerShow(false);
    +        }
     
    -    const fields = {};
    -    for (const name of fieldNames) {
    -        fields[name] = '';
    +        for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
    +            const node = document.querySelector(selector);
    +            this._setDropdownOptions(node, Array.isArray(values) ? values : [value]);
    +            node.value = value;
    +        }
         }
     
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    -    options.anki[tabId].fields = utilBackgroundIsolate(fields);
    -    await settingsSaveOptions();
    -
    -    await _ankiFieldsPopulate(tabId, options);
    -}
    -
    -async function _onAnkiFieldsChanged() {
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    -
    -    options.anki.terms.deck = $('#anki-terms-deck').val();
    -    options.anki.terms.model = $('#anki-terms-model').val();
    -    options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value')));
    -    options.anki.kanji.deck = $('#anki-kanji-deck').val();
    -    options.anki.kanji.model = $('#anki-kanji-model').val();
    -    options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value')));
    +    _createFieldTemplate(name, value, markers) {
    +        const template = document.querySelector('#anki-field-template').content;
    +        const content = document.importNode(template, true).firstChild;
     
    -    await settingsSaveOptions();
    +        content.querySelector('.anki-field-name').textContent = name;
     
    -    await onAnkiOptionsChanged(options);
    -}
    +        const field = content.querySelector('.anki-field-value');
    +        field.dataset.field = name;
    +        field.value = value;
     
    +        content.querySelector('.anki-field-marker-list').appendChild(this.getFieldMarkersHtml(markers));
     
    +        return content;
    +    }
     
    -// Public
    +    async _fieldsPopulate(tabId, options) {
    +        const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`);
    +        const container = tab.querySelector('tbody');
    +        const markers = this.getFieldMarkers(tabId);
    +
    +        const fragment = document.createDocumentFragment();
    +        const fields = options.anki[tabId].fields;
    +        for (const name of Object.keys(fields)) {
    +            const value = fields[name];
    +            const html = this._createFieldTemplate(name, value, markers);
    +            fragment.appendChild(html);
    +        }
     
    -function ankiErrorShown() {
    -    const node = document.querySelector('#anki-error');
    -    return node && !node.hidden;
    -}
    +        container.textContent = '';
    +        container.appendChild(fragment);
     
    -function ankiFieldsToDict(elements) {
    -    const result = {};
    -    for (const element of elements) {
    -        result[element.dataset.field] = element.value;
    +        for (const node of container.querySelectorAll('.anki-field-value')) {
    +            node.addEventListener('change', this._onFieldsChanged.bind(this), false);
    +        }
    +        for (const node of container.querySelectorAll('.marker-link')) {
    +            node.addEventListener('click', this._onMarkerClicked.bind(this), false);
    +        }
         }
    -    return result;
    -}
     
    -
    -function ankiGetFieldMarkersHtml(markers) {
    -    const template = document.querySelector('#anki-field-marker-template').content;
    -    const fragment = document.createDocumentFragment();
    -    for (const marker of markers) {
    -        const markerNode = document.importNode(template, true).firstChild;
    -        markerNode.querySelector('.marker-link').textContent = marker;
    -        fragment.appendChild(markerNode);
    +    _onMarkerClicked(e) {
    +        e.preventDefault();
    +        const link = e.currentTarget;
    +        const input = $(link).closest('.input-group').find('.anki-field-value')[0];
    +        input.value = `{${link.textContent}}`;
    +        input.dispatchEvent(new Event('change'));
         }
    -    return fragment;
    -}
     
    -function ankiGetFieldMarkers(type) {
    -    switch (type) {
    -        case 'terms':
    -            return [
    -                'audio',
    -                'cloze-body',
    -                'cloze-prefix',
    -                'cloze-suffix',
    -                'dictionary',
    -                'document-title',
    -                'expression',
    -                'furigana',
    -                'furigana-plain',
    -                'glossary',
    -                'glossary-brief',
    -                'reading',
    -                'screenshot',
    -                'sentence',
    -                'tags',
    -                'url'
    -            ];
    -        case 'kanji':
    -            return [
    -                'character',
    -                'dictionary',
    -                'document-title',
    -                'glossary',
    -                'kunyomi',
    -                'onyomi',
    -                'screenshot',
    -                'sentence',
    -                'tags',
    -                'url'
    -            ];
    -        default:
    -            return [];
    -    }
    -}
    +    async _onModelChanged(e) {
    +        const node = e.currentTarget;
    +        let fieldNames;
    +        try {
    +            const modelName = node.value;
    +            fieldNames = await api.getAnkiModelFieldNames(modelName);
    +            this._setError(null);
    +        } catch (error) {
    +            this._setError(error);
    +            return;
    +        } finally {
    +            this._spinnerShow(false);
    +        }
     
    +        const tabId = node.dataset.ankiCardType;
    +        if (tabId !== 'terms' && tabId !== 'kanji') { return; }
     
    -function ankiInitialize() {
    -    $('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(_onAnkiFieldsChanged);
    +        const fields = {};
    +        for (const name of fieldNames) {
    +            fields[name] = '';
    +        }
     
    -    for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
    -        node.addEventListener('change', _onAnkiModelChanged, false);
    -    }
    +        const optionsContext = getOptionsContext();
    +        const options = await getOptionsMutable(optionsContext);
    +        options.anki[tabId].fields = utilBackgroundIsolate(fields);
    +        await settingsSaveOptions();
     
    -    onAnkiOptionsChanged();
    -}
    +        await this._fieldsPopulate(tabId, options);
    +    }
     
    -async function onAnkiOptionsChanged(options=null) {
    -    if (options === null) {
    +    async _onFieldsChanged() {
             const optionsContext = getOptionsContext();
    -        options = await getOptionsMutable(optionsContext);
    -    }
    +        const options = await getOptionsMutable(optionsContext);
     
    -    if (!options.anki.enable) {
    -        return;
    -    }
    +        options.anki.terms.deck = $('#anki-terms-deck').val();
    +        options.anki.terms.model = $('#anki-terms-model').val();
    +        options.anki.terms.fields = utilBackgroundIsolate(this._fieldsToDict(document.querySelectorAll('#terms .anki-field-value')));
    +        options.anki.kanji.deck = $('#anki-kanji-deck').val();
    +        options.anki.kanji.model = $('#anki-kanji-model').val();
    +        options.anki.kanji.fields = utilBackgroundIsolate(this._fieldsToDict(document.querySelectorAll('#kanji .anki-field-value')));
    +
    +        await settingsSaveOptions();
     
    -    await _ankiDeckAndModelPopulate(options);
    -    await Promise.all([_ankiFieldsPopulate('terms', options), _ankiFieldsPopulate('kanji', options)]);
    +        await this.optionsChanged(options);
    +    }
     }
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index 64611ad5..872f8f05 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -16,17 +16,15 @@
      */
     
     /* global
    + * AnkiController
    + * AnkiTemplatesController
      * ProfileController
      * SettingsBackup
      * SettingsController
    - * ankiInitialize
    - * ankiTemplatesInitialize
    - * ankiTemplatesUpdateValue
      * api
      * appearanceInitialize
      * audioSettingsInitialize
      * dictSettingsInitialize
    - * onAnkiOptionsChanged
      * onDictionaryOptionsChanged
      * storageInfoInitialize
      * utilBackend
    @@ -269,9 +267,13 @@ async function onOptionsUpdated({source}) {
         const options = await getOptionsMutable(optionsContext);
     
         document.querySelector('#enable-clipboard-popups').checked = options.general.enableClipboardPopups;
    -    ankiTemplatesUpdateValue();
    +    if (ankiTemplatesController !== null) {
    +        ankiTemplatesController.updateValue();
    +    }
         onDictionaryOptionsChanged();
    -    onAnkiOptionsChanged();
    +    if (ankiController !== null) {
    +        ankiController.optionsChanged();
    +    }
     
         await formWrite(options);
     }
    @@ -302,6 +304,8 @@ async function settingsPopulateModifierKeys() {
         }
     }
     
    +let ankiController = null;
    +let ankiTemplatesController = null;
     
     async function onReady() {
         api.forwardLogsToBackend();
    @@ -318,8 +322,10 @@ async function onReady() {
         await audioSettingsInitialize();
         await (new ProfileController()).prepare();
         await dictSettingsInitialize();
    -    ankiInitialize();
    -    ankiTemplatesInitialize();
    +    ankiController = new AnkiController();
    +    ankiController.prepare();
    +    ankiTemplatesController = new AnkiTemplatesController(ankiController);
    +    ankiTemplatesController.prepare();
         new SettingsBackup().prepare();
     
         storageInfoInitialize();
    -- 
    cgit v1.2.3
    
    
    From c62f980f37007743bed004ff43a82a8d7664dac6 Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 19:56:38 -0400
    Subject: Audio controller (#569)
    
    * Convert audio.js into a class
    
    * Move audio-ui.js classes into audio.js
    
    * Rename fields
    
    * Merge classes
    
    * Remove audio-ui.js
    ---
     ext/bg/js/settings/audio-ui.js | 139 ----------------------
     ext/bg/js/settings/audio.js    | 255 ++++++++++++++++++++++++++++-------------
     ext/bg/js/settings/main.js     |   4 +-
     ext/bg/settings.html           |   1 -
     4 files changed, 178 insertions(+), 221 deletions(-)
     delete mode 100644 ext/bg/js/settings/audio-ui.js
    
    (limited to 'ext/bg/js/settings/main.js')
    
    diff --git a/ext/bg/js/settings/audio-ui.js b/ext/bg/js/settings/audio-ui.js
    deleted file mode 100644
    index 73c64227..00000000
    --- a/ext/bg/js/settings/audio-ui.js
    +++ /dev/null
    @@ -1,139 +0,0 @@
    -/*
    - * Copyright (C) 2019-2020  Yomichan Authors
    - *
    - * 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 .
    - */
    -
    -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.bind(this);
    -        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.bind(this);
    -        this._removeClickListener = this.onRemoveClicked.bind(this);
    -
    -        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 ac2d82f3..5c1cb131 100644
    --- a/ext/bg/js/settings/audio.js
    +++ b/ext/bg/js/settings/audio.js
    @@ -16,110 +16,207 @@
      */
     
     /* global
    - * AudioSourceUI
      * AudioSystem
      * getOptionsContext
      * getOptionsMutable
      * settingsSaveOptions
      */
     
    -let audioSourceUI = null;
    -let audioSystem = null;
    -
    -async function audioSettingsInitialize() {
    -    audioSystem = new AudioSystem({
    -        audioUriBuilder: null,
    -        useCache: true
    -    });
    -
    -    const optionsContext = getOptionsContext();
    -    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();
    -}
    +class AudioController {
    +    constructor() {
    +        this._audioSystem = null;
    +        this._settingsAudioSources = null;
    +        this._audioSourceContainer = null;
    +        this._audioSourceAddButton = null;
    +        this._audioSourceEntries = [];
    +    }
     
    -function textToSpeechInitialize() {
    -    if (typeof speechSynthesis === 'undefined') { return; }
    +    async prepare() {
    +        this._audioSystem = new AudioSystem({
    +            audioUriBuilder: null,
    +            useCache: true
    +        });
     
    -    speechSynthesis.addEventListener('voiceschanged', updateTextToSpeechVoices, false);
    -    updateTextToSpeechVoices();
    +        const optionsContext = getOptionsContext();
    +        const options = await getOptionsMutable(optionsContext);
     
    -    document.querySelector('#text-to-speech-voice').addEventListener('change', onTextToSpeechVoiceChange, false);
    -    document.querySelector('#text-to-speech-voice-test').addEventListener('click', textToSpeechTest, false);
    -}
    +        this._settingsAudioSources = options.audio.sources;
    +        this._audioSourceContainer = document.querySelector('.audio-source-list');
    +        this._audioSourceAddButton = document.querySelector('.audio-source-add');
    +        this._audioSourceContainer.textContent = '';
    +
    +        this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false);
    +
    +        for (const audioSource of toIterable(this._settingsAudioSources)) {
    +            this._createAudioSourceEntry(audioSource);
    +        }
     
    -function updateTextToSpeechVoices() {
    -    const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index}));
    -    voices.sort(textToSpeechVoiceCompare);
    +        this._prepareTextToSpeech();
    +    }
    +
    +    // Private
     
    -    document.querySelector('#text-to-speech-voice-container').hidden = (voices.length === 0);
    +    async _save() {
    +        await settingsSaveOptions();
    +    }
     
    -    const fragment = document.createDocumentFragment();
    +    _prepareTextToSpeech() {
    +        if (typeof speechSynthesis === 'undefined') { return; }
     
    -    let option = document.createElement('option');
    -    option.value = '';
    -    option.textContent = 'None';
    -    fragment.appendChild(option);
    +        speechSynthesis.addEventListener('voiceschanged', this._updateTextToSpeechVoices.bind(this), false);
    +        this._updateTextToSpeechVoices();
    +
    +        document.querySelector('#text-to-speech-voice').addEventListener('change', this._onTextToSpeechVoiceChange.bind(this), false);
    +        document.querySelector('#text-to-speech-voice-test').addEventListener('click', this._testTextToSpeech.bind(this), false);
    +    }
     
    -    for (const {voice} of voices) {
    -        option = document.createElement('option');
    -        option.value = voice.voiceURI;
    -        option.textContent = `${voice.name} (${voice.lang})`;
    +    _updateTextToSpeechVoices() {
    +        const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index}));
    +        voices.sort(this._textToSpeechVoiceCompare.bind(this));
    +
    +        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) {
    +            option = document.createElement('option');
    +            option.value = voice.voiceURI;
    +            option.textContent = `${voice.name} (${voice.lang})`;
    +            fragment.appendChild(option);
    +        }
    +
    +        const select = document.querySelector('#text-to-speech-voice');
    +        select.textContent = '';
    +        select.appendChild(fragment);
    +        select.value = select.dataset.value;
         }
     
    -    const select = document.querySelector('#text-to-speech-voice');
    -    select.textContent = '';
    -    select.appendChild(fragment);
    -    select.value = select.dataset.value;
    -}
    +    _textToSpeechVoiceCompare(a, b) {
    +        const aIsJapanese = this._languageTagIsJapanese(a.voice.lang);
    +        const bIsJapanese = this._languageTagIsJapanese(b.voice.lang);
    +        if (aIsJapanese) {
    +            if (!bIsJapanese) { return -1; }
    +        } else {
    +            if (bIsJapanese) { return 1; }
    +        }
    +
    +        const aIsDefault = a.voice.default;
    +        const bIsDefault = b.voice.default;
    +        if (aIsDefault) {
    +            if (!bIsDefault) { return -1; }
    +        } else {
    +            if (bIsDefault) { return 1; }
    +        }
    +
    +        return a.index - b.index;
    +    }
     
    -function languageTagIsJapanese(languageTag) {
    -    return (
    -        languageTag.startsWith('ja-') ||
    -        languageTag.startsWith('jpn-')
    -    );
    -}
    +    _languageTagIsJapanese(languageTag) {
    +        return (
    +            languageTag.startsWith('ja-') ||
    +            languageTag.startsWith('jpn-')
    +        );
    +    }
     
    -function textToSpeechVoiceCompare(a, b) {
    -    const aIsJapanese = languageTagIsJapanese(a.voice.lang);
    -    const bIsJapanese = languageTagIsJapanese(b.voice.lang);
    -    if (aIsJapanese) {
    -        if (!bIsJapanese) { return -1; }
    -    } else {
    -        if (bIsJapanese) { return 1; }
    +    _testTextToSpeech() {
    +        try {
    +            const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || '';
    +            const voiceUri = document.querySelector('#text-to-speech-voice').value;
    +
    +            const audio = this._audioSystem.createTextToSpeechAudio(text, voiceUri);
    +            audio.volume = 1.0;
    +            audio.play();
    +        } catch (e) {
    +            // NOP
    +        }
         }
     
    -    const aIsDefault = a.voice.default;
    -    const bIsDefault = b.voice.default;
    -    if (aIsDefault) {
    -        if (!bIsDefault) { return -1; }
    -    } else {
    -        if (bIsDefault) { return 1; }
    +    _instantiateTemplate(templateSelector) {
    +        const template = document.querySelector(templateSelector);
    +        const content = document.importNode(template.content, true);
    +        return content.firstChild;
         }
     
    -    return a.index - b.index;
    -}
    +    _getUnusedAudioSource() {
    +        const audioSourcesAvailable = [
    +            'jpod101',
    +            'jpod101-alternate',
    +            'jisho',
    +            'custom'
    +        ];
    +        for (const source of audioSourcesAvailable) {
    +            if (this._settingsAudioSources.indexOf(source) < 0) {
    +                return source;
    +            }
    +        }
    +        return audioSourcesAvailable[0];
    +    }
    +
    +    _createAudioSourceEntry(value) {
    +        const eventListeners = new EventListenerCollection();
    +        const container = this._instantiateTemplate('#audio-source-template');
    +        const select = container.querySelector('.audio-source-select');
    +        const removeButton = container.querySelector('.audio-source-remove');
    +
    +        select.value = value;
     
    -function textToSpeechTest() {
    -    try {
    -        const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || '';
    -        const voiceUri = document.querySelector('#text-to-speech-voice').value;
    +        const entry = {
    +            container,
    +            eventListeners
    +        };
     
    -        const audio = audioSystem.createTextToSpeechAudio(text, voiceUri);
    -        audio.volume = 1.0;
    -        audio.play();
    -    } catch (e) {
    -        // NOP
    +        eventListeners.addEventListener(select, 'change', this._onAudioSourceSelectChange.bind(this, entry), false);
    +        eventListeners.addEventListener(removeButton, 'click', this._onAudioSourceRemoveClicked.bind(this, entry), false);
    +
    +        this._audioSourceContainer.appendChild(container);
    +        this._audioSourceEntries.push(entry);
    +    }
    +
    +    _removeAudioSourceEntry(entry) {
    +        const index = this._audioSourceEntries.indexOf(entry);
    +        if (index < 0) { return; }
    +
    +        const {container, eventListeners} = entry;
    +        if (container.parentNode !== null) {
    +            container.parentNode.removeChild(container);
    +        }
    +        eventListeners.removeAllEventListeners();
    +
    +        this._audioSourceEntries.splice(index, 1);
    +        this._settingsAudioSources.splice(index, 1);
    +
    +        for (let i = index, ii = this._audioSourceEntries.length; i < ii; ++i) {
    +            this._audioSourceEntries[i].index = i;
    +        }
    +    }
    +
    +    _onTextToSpeechVoiceChange(e) {
    +        e.currentTarget.dataset.value = e.currentTarget.value;
    +    }
    +
    +    _onAddAudioSource() {
    +        const audioSource = this._getUnusedAudioSource();
    +        this._settingsAudioSources.push(audioSource);
    +        this._createAudioSourceEntry(audioSource);
    +        this._save();
         }
    -}
     
    -function onTextToSpeechVoiceChange(e) {
    -    e.currentTarget.dataset.value = e.currentTarget.value;
    +    _onAudioSourceSelectChange(entry, event) {
    +        const index = this._audioSourceEntries.indexOf(entry);
    +        if (index < 0) { return; }
    +
    +        const value = event.currentTarget.value;
    +        this._settingsAudioSources[index] = value;
    +        this._save();
    +    }
    +
    +    _onAudioSourceRemoveClicked(entry) {
    +        this._removeAudioSourceEntry(entry);
    +        this._save();
    +    }
     }
    diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
    index 872f8f05..dddaef6c 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -18,12 +18,12 @@
     /* global
      * AnkiController
      * AnkiTemplatesController
    + * AudioController
      * ProfileController
      * SettingsBackup
      * SettingsController
      * api
      * appearanceInitialize
    - * audioSettingsInitialize
      * dictSettingsInitialize
      * onDictionaryOptionsChanged
      * storageInfoInitialize
    @@ -319,7 +319,7 @@ async function onReady() {
         await settingsPopulateModifierKeys();
         formSetupEventListeners();
         appearanceInitialize();
    -    await audioSettingsInitialize();
    +    new AudioController().prepare();
         await (new ProfileController()).prepare();
         await dictSettingsInitialize();
         ankiController = new AnkiController();
    diff --git a/ext/bg/settings.html b/ext/bg/settings.html
    index c8f5b15c..5c7fde41 100644
    --- a/ext/bg/settings.html
    +++ b/ext/bg/settings.html
    @@ -1141,7 +1141,6 @@
             
             
             
    -        
             
             
             
    -- 
    cgit v1.2.3
    
    
    From 418e8a57bf7ea1def3e7b83270742d466e98e8cf Mon Sep 17 00:00:00 2001
    From: toasted-nutbread 
    Date: Fri, 29 May 2020 20:25:22 -0400
    Subject: Convert dictionaries.js and storage.js to classes (#570)
    
    * Convert dictionaries.js to a class
    
    * Remove storage spinner
    
    * Convert storage.js to a class
    
    * Move dataset assignments into main.js
    ---
     ext/bg/css/settings.css            |   2 +-
     ext/bg/js/settings/dictionaries.js | 544 ++++++++++++++++++-------------------
     ext/bg/js/settings/main.js         |  25 +-
     ext/bg/js/settings/storage.js      | 199 +++++++-------
     ext/bg/settings.html               |   1 -
     5 files changed, 384 insertions(+), 387 deletions(-)
    
    (limited to 'ext/bg/js/settings/main.js')
    
    diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css
    index f55082e7..eb11d77e 100644
    --- a/ext/bg/css/settings.css
    +++ b/ext/bg/css/settings.css
    @@ -18,7 +18,7 @@
     
     #anki-spinner,
     #dict-spinner, #dict-import-progress,
    -.storage-hidden, #storage-spinner {
    +.storage-hidden {
         display: none;
     }
     
    diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js
    index 4d307f0f..dd6dd1c1 100644
    --- a/ext/bg/js/settings/dictionaries.js
    +++ b/ext/bg/js/settings/dictionaries.js
    @@ -22,14 +22,9 @@
      * getOptionsFullMutable
      * getOptionsMutable
      * settingsSaveOptions
    - * storageEstimate
    - * storageUpdateStats
      * utilBackgroundIsolate
      */
     
    -let dictionaryUI = null;
    -
    -
     class SettingsDictionaryListUI {
         constructor(container, template, extraContainer, extraTemplate) {
             this.container = container;
    @@ -308,13 +303,13 @@ class SettingsDictionaryEntryUI {
     
                 await api.deleteDictionary(this.dictionaryInfo.title, onProgress);
             } catch (e) {
    -            dictionaryErrorsShow([e]);
    +            this.dictionaryErrorsShow([e]);
             } finally {
                 prevention.end();
                 this.isDeleting = false;
                 progress.hidden = true;
     
    -            onDatabaseUpdated();
    +            this.onDatabaseUpdated();
             }
         }
     
    @@ -388,340 +383,341 @@ class SettingsDictionaryExtraUI {
         }
     }
     
    +class DictionaryController {
    +    constructor(storageController) {
    +        this._storageController = storageController;
    +        this._dictionaryUI = null;
    +        this._dictionaryErrorToStringOverrides = [
    +            [
    +                'A mutation operation was attempted on a database that did not allow mutations.',
    +                'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
    +            ],
    +            [
    +                'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
    +                'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
    +            ],
    +            [
    +                'BulkError',
    +                'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
    +            ]
    +        ];
    +    }
     
    -async function dictSettingsInitialize() {
    -    dictionaryUI = new SettingsDictionaryListUI(
    -        document.querySelector('#dict-groups'),
    -        document.querySelector('#dict-template'),
    -        document.querySelector('#dict-groups-extra'),
    -        document.querySelector('#dict-extra-template')
    -    );
    -    dictionaryUI.save = settingsSaveOptions;
    -
    -    document.querySelector('#dict-purge-button').addEventListener('click', onDictionaryPurgeButtonClick, false);
    -    document.querySelector('#dict-purge-confirm').addEventListener('click', onDictionaryPurge, false);
    -    document.querySelector('#dict-file-button').addEventListener('click', onDictionaryImportButtonClick, false);
    -    document.querySelector('#dict-file').addEventListener('change', onDictionaryImport, false);
    -    document.querySelector('#dict-main').addEventListener('change', onDictionaryMainChanged, false);
    -    document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', onDatabaseEnablePrefixWildcardSearchesChanged, false);
    -
    -    await onDictionaryOptionsChanged();
    -    await onDatabaseUpdated();
    -}
    -
    -async function onDictionaryOptionsChanged() {
    -    if (dictionaryUI === null) { return; }
    -
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    +    async prepare() {
    +        this._dictionaryUI = new SettingsDictionaryListUI(
    +            document.querySelector('#dict-groups'),
    +            document.querySelector('#dict-template'),
    +            document.querySelector('#dict-groups-extra'),
    +            document.querySelector('#dict-extra-template')
    +        );
    +        this._dictionaryUI.save = settingsSaveOptions;
     
    -    dictionaryUI.setOptionsDictionaries(options.dictionaries);
    +        document.querySelector('#dict-purge-button').addEventListener('click', this._onPurgeButtonClick.bind(this), false);
    +        document.querySelector('#dict-purge-confirm').addEventListener('click', this._onPurgeConfirmButtonClick.bind(this), false);
    +        document.querySelector('#dict-file-button').addEventListener('click', this._onImportButtonClick.bind(this), false);
    +        document.querySelector('#dict-file').addEventListener('change', this._onImportFileChange.bind(this), false);
    +        document.querySelector('#dict-main').addEventListener('change', this._onDictionaryMainChanged.bind(this), false);
    +        document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', this._onDatabaseEnablePrefixWildcardSearchesChanged.bind(this), false);
     
    -    const optionsFull = await api.optionsGetFull();
    -    document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
    +        await this.optionsChanged();
    +        await this._onDatabaseUpdated();
    +    }
     
    -    await updateMainDictionarySelectValue();
    -}
    +    async optionsChanged() {
    +        if (this._dictionaryUI === null) { return; }
     
    -async function onDatabaseUpdated() {
    -    try {
    -        const dictionaries = await api.getDictionaryInfo();
    -        dictionaryUI.setDictionaries(dictionaries);
    +        const optionsContext = getOptionsContext();
    +        const options = await getOptionsMutable(optionsContext);
     
    -        document.querySelector('#dict-warning').hidden = (dictionaries.length > 0);
    +        this._dictionaryUI.setOptionsDictionaries(options.dictionaries);
     
    -        updateMainDictionarySelectOptions(dictionaries);
    -        await updateMainDictionarySelectValue();
    +        const optionsFull = await api.optionsGetFull();
    +        document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
     
    -        const {counts, total} = await api.getDictionaryCounts(dictionaries.map((v) => v.title), true);
    -        dictionaryUI.setCounts(counts, total);
    -    } catch (e) {
    -        dictionaryErrorsShow([e]);
    +        await this._updateMainDictionarySelectValue();
         }
    -}
    -
    -function updateMainDictionarySelectOptions(dictionaries) {
    -    const select = document.querySelector('#dict-main');
    -    select.textContent = ''; // Empty
     
    -    let option = document.createElement('option');
    -    option.className = 'text-muted';
    -    option.value = '';
    -    option.textContent = 'Not selected';
    -    select.appendChild(option);
    +    // Private
     
    -    for (const {title, sequenced} of toIterable(dictionaries)) {
    -        if (!sequenced) { continue; }
    +    _updateMainDictionarySelectOptions(dictionaries) {
    +        const select = document.querySelector('#dict-main');
    +        select.textContent = ''; // Empty
     
    -        option = document.createElement('option');
    -        option.value = title;
    -        option.textContent = title;
    +        let option = document.createElement('option');
    +        option.className = 'text-muted';
    +        option.value = '';
    +        option.textContent = 'Not selected';
             select.appendChild(option);
    -    }
    -}
    -
    -async function updateMainDictionarySelectValue() {
    -    const optionsContext = getOptionsContext();
    -    const options = await api.optionsGet(optionsContext);
     
    -    const value = options.general.mainDictionary;
    +        for (const {title, sequenced} of toIterable(dictionaries)) {
    +            if (!sequenced) { continue; }
     
    -    const select = document.querySelector('#dict-main');
    -    let selectValue = null;
    -    for (const child of select.children) {
    -        if (child.nodeName.toUpperCase() === 'OPTION' && child.value === value) {
    -            selectValue = value;
    -            break;
    +            option = document.createElement('option');
    +            option.value = title;
    +            option.textContent = title;
    +            select.appendChild(option);
             }
         }
     
    -    let missingNodeOption = select.querySelector('option[data-not-installed=true]');
    -    if (selectValue === null) {
    -        if (missingNodeOption === null) {
    -            missingNodeOption = document.createElement('option');
    -            missingNodeOption.className = 'text-muted';
    -            missingNodeOption.value = value;
    -            missingNodeOption.textContent = `${value} (Not installed)`;
    -            missingNodeOption.dataset.notInstalled = 'true';
    -            select.appendChild(missingNodeOption);
    +    async _updateMainDictionarySelectValue() {
    +        const optionsContext = getOptionsContext();
    +        const options = await api.optionsGet(optionsContext);
    +
    +        const value = options.general.mainDictionary;
    +
    +        const select = document.querySelector('#dict-main');
    +        let selectValue = null;
    +        for (const child of select.children) {
    +            if (child.nodeName.toUpperCase() === 'OPTION' && child.value === value) {
    +                selectValue = value;
    +                break;
    +            }
             }
    -    } else {
    -        if (missingNodeOption !== null) {
    -            missingNodeOption.parentNode.removeChild(missingNodeOption);
    +
    +        let missingNodeOption = select.querySelector('option[data-not-installed=true]');
    +        if (selectValue === null) {
    +            if (missingNodeOption === null) {
    +                missingNodeOption = document.createElement('option');
    +                missingNodeOption.className = 'text-muted';
    +                missingNodeOption.value = value;
    +                missingNodeOption.textContent = `${value} (Not installed)`;
    +                missingNodeOption.dataset.notInstalled = 'true';
    +                select.appendChild(missingNodeOption);
    +            }
    +        } else {
    +            if (missingNodeOption !== null) {
    +                missingNodeOption.parentNode.removeChild(missingNodeOption);
    +            }
             }
    +
    +        select.value = value;
         }
     
    -    select.value = value;
    -}
    +    _dictionaryErrorToString(error) {
    +        if (error.toString) {
    +            error = error.toString();
    +        } else {
    +            error = `${error}`;
    +        }
     
    -async function onDictionaryMainChanged(e) {
    -    const select = e.target;
    -    const value = select.value;
    +        for (const [match, subst] of this._dictionaryErrorToStringOverrides) {
    +            if (error.includes(match)) {
    +                error = subst;
    +                break;
    +            }
    +        }
     
    -    const missingNodeOption = select.querySelector('option[data-not-installed=true]');
    -    if (missingNodeOption !== null && missingNodeOption.value !== value) {
    -        missingNodeOption.parentNode.removeChild(missingNodeOption);
    +        return error;
         }
     
    -    const optionsContext = getOptionsContext();
    -    const options = await getOptionsMutable(optionsContext);
    -    options.general.mainDictionary = value;
    -    await settingsSaveOptions();
    -}
    +    _dictionaryErrorsShow(errors) {
    +        const dialog = document.querySelector('#dict-error');
    +        dialog.textContent = '';
     
    +        if (errors !== null && errors.length > 0) {
    +            const uniqueErrors = new Map();
    +            for (let e of errors) {
    +                yomichan.logError(e);
    +                e = this._dictionaryErrorToString(e);
    +                let count = uniqueErrors.get(e);
    +                if (typeof count === 'undefined') {
    +                    count = 0;
    +                }
    +                uniqueErrors.set(e, count + 1);
    +            }
     
    -function dictionaryErrorToString(error) {
    -    if (error.toString) {
    -        error = error.toString();
    -    } else {
    -        error = `${error}`;
    -    }
    +            for (const [e, count] of uniqueErrors.entries()) {
    +                const div = document.createElement('p');
    +                if (count > 1) {
    +                    div.textContent = `${e} `;
    +                    const em = document.createElement('em');
    +                    em.textContent = `(${count})`;
    +                    div.appendChild(em);
    +                } else {
    +                    div.textContent = `${e}`;
    +                }
    +                dialog.appendChild(div);
    +            }
     
    -    for (const [match, subst] of dictionaryErrorToString.overrides) {
    -        if (error.includes(match)) {
    -            error = subst;
    -            break;
    +            dialog.hidden = false;
    +        } else {
    +            dialog.hidden = true;
             }
         }
     
    -    return error;
    -}
    -dictionaryErrorToString.overrides = [
    -    [
    -        'A mutation operation was attempted on a database that did not allow mutations.',
    -        'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
    -    ],
    -    [
    -        'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
    -        'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
    -    ],
    -    [
    -        'BulkError',
    -        'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
    -    ]
    -];
    -
    -function dictionaryErrorsShow(errors) {
    -    const dialog = document.querySelector('#dict-error');
    -    dialog.textContent = '';
    -
    -    if (errors !== null && errors.length > 0) {
    -        const uniqueErrors = new Map();
    -        for (let e of errors) {
    -            yomichan.logError(e);
    -            e = dictionaryErrorToString(e);
    -            let count = uniqueErrors.get(e);
    -            if (typeof count === 'undefined') {
    -                count = 0;
    -            }
    -            uniqueErrors.set(e, count + 1);
    -        }
    -
    -        for (const [e, count] of uniqueErrors.entries()) {
    -            const div = document.createElement('p');
    -            if (count > 1) {
    -                div.textContent = `${e} `;
    -                const em = document.createElement('em');
    -                em.textContent = `(${count})`;
    -                div.appendChild(em);
    -            } else {
    -                div.textContent = `${e}`;
    -            }
    -            dialog.appendChild(div);
    +    _dictionarySpinnerShow(show) {
    +        const spinner = $('#dict-spinner');
    +        if (show) {
    +            spinner.show();
    +        } else {
    +            spinner.hide();
             }
    +    }
     
    -        dialog.hidden = false;
    -    } else {
    -        dialog.hidden = true;
    +    _dictReadFile(file) {
    +        return new Promise((resolve, reject) => {
    +            const reader = new FileReader();
    +            reader.onload = () => resolve(reader.result);
    +            reader.onerror = () => reject(reader.error);
    +            reader.readAsBinaryString(file);
    +        });
    +    }
    +
    +    async _onDatabaseUpdated() {
    +        try {
    +            const dictionaries = await api.getDictionaryInfo();
    +            this._dictionaryUI.setDictionaries(dictionaries);
    +
    +            document.querySelector('#dict-warning').hidden = (dictionaries.length > 0);
    +
    +            this._updateMainDictionarySelectOptions(dictionaries);
    +            await this._updateMainDictionarySelectValue();
    +
    +            const {counts, total} = await api.getDictionaryCounts(dictionaries.map((v) => v.title), true);
    +            this._dictionaryUI.setCounts(counts, total);
    +        } catch (e) {
    +            this._dictionaryErrorsShow([e]);
    +        }
         }
    -}
     
    +    async _onDictionaryMainChanged(e) {
    +        const select = e.target;
    +        const value = select.value;
    +
    +        const missingNodeOption = select.querySelector('option[data-not-installed=true]');
    +        if (missingNodeOption !== null && missingNodeOption.value !== value) {
    +            missingNodeOption.parentNode.removeChild(missingNodeOption);
    +        }
     
    -function dictionarySpinnerShow(show) {
    -    const spinner = $('#dict-spinner');
    -    if (show) {
    -        spinner.show();
    -    } else {
    -        spinner.hide();
    +        const optionsContext = getOptionsContext();
    +        const options = await getOptionsMutable(optionsContext);
    +        options.general.mainDictionary = value;
    +        await settingsSaveOptions();
         }
    -}
     
    -function onDictionaryImportButtonClick() {
    -    const dictFile = document.querySelector('#dict-file');
    -    dictFile.click();
    -}
    +    _onImportButtonClick() {
    +        const dictFile = document.querySelector('#dict-file');
    +        dictFile.click();
    +    }
     
    -function onDictionaryPurgeButtonClick(e) {
    -    e.preventDefault();
    -    $('#dict-purge-modal').modal('show');
    -}
    +    _onPurgeButtonClick(e) {
    +        e.preventDefault();
    +        $('#dict-purge-modal').modal('show');
    +    }
     
    -async function onDictionaryPurge(e) {
    -    e.preventDefault();
    +    async _onPurgeConfirmButtonClick(e) {
    +        e.preventDefault();
     
    -    $('#dict-purge-modal').modal('hide');
    +        $('#dict-purge-modal').modal('hide');
     
    -    const dictControls = $('#dict-importer, #dict-groups, #dict-groups-extra, #dict-main-group').hide();
    -    const dictProgress = document.querySelector('#dict-purge');
    -    dictProgress.hidden = false;
    +        const dictControls = $('#dict-importer, #dict-groups, #dict-groups-extra, #dict-main-group').hide();
    +        const dictProgress = document.querySelector('#dict-purge');
    +        dictProgress.hidden = false;
     
    -    const prevention = new PageExitPrevention();
    +        const prevention = new PageExitPrevention();
     
    -    try {
    -        prevention.start();
    -        dictionaryErrorsShow(null);
    -        dictionarySpinnerShow(true);
    +        try {
    +            prevention.start();
    +            this._dictionaryErrorsShow(null);
    +            this._dictionarySpinnerShow(true);
     
    -        await api.purgeDatabase();
    -        for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
    -            options.dictionaries = utilBackgroundIsolate({});
    -            options.general.mainDictionary = '';
    -        }
    -        await settingsSaveOptions();
    +            await api.purgeDatabase();
    +            for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
    +                options.dictionaries = utilBackgroundIsolate({});
    +                options.general.mainDictionary = '';
    +            }
    +            await settingsSaveOptions();
     
    -        onDatabaseUpdated();
    -    } catch (err) {
    -        dictionaryErrorsShow([err]);
    -    } finally {
    -        prevention.end();
    +            this._onDatabaseUpdated();
    +        } catch (err) {
    +            this._dictionaryErrorsShow([err]);
    +        } finally {
    +            prevention.end();
     
    -        dictionarySpinnerShow(false);
    +            this._dictionarySpinnerShow(false);
     
    -        dictControls.show();
    -        dictProgress.hidden = true;
    +            dictControls.show();
    +            dictProgress.hidden = true;
     
    -        if (storageEstimate.mostRecent !== null) {
    -            storageUpdateStats();
    +            this._storageController.updateStats();
             }
         }
    -}
     
    -async function onDictionaryImport(e) {
    -    const files = [...e.target.files];
    -    e.target.value = null;
    +    async _onImportFileChange(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();
    -    const dictImportInfo = document.querySelector('#dict-import-info');
    +        const dictFile = $('#dict-file');
    +        const dictControls = $('#dict-importer').hide();
    +        const dictProgress = $('#dict-import-progress').show();
    +        const dictImportInfo = document.querySelector('#dict-import-info');
     
    -    const prevention = new PageExitPrevention();
    +        const prevention = new PageExitPrevention();
     
    -    try {
    -        prevention.start();
    -        dictionaryErrorsShow(null);
    -        dictionarySpinnerShow(true);
    +        try {
    +            prevention.start();
    +            this._dictionaryErrorsShow(null);
    +            this._dictionarySpinnerShow(true);
     
    -        const setProgress = (percent) => dictProgress.find('.progress-bar').css('width', `${percent}%`);
    -        const updateProgress = (total, current) => {
    -            setProgress(current / total * 100.0);
    -            if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) {
    -                storageUpdateStats();
    -            }
    -        };
    +            const setProgress = (percent) => dictProgress.find('.progress-bar').css('width', `${percent}%`);
    +            const updateProgress = (total, current) => {
    +                setProgress(current / total * 100.0);
    +                this._storageController.updateStats();
    +            };
     
    -        const optionsFull = await api.optionsGetFull();
    +            const optionsFull = await api.optionsGetFull();
     
    -        const importDetails = {
    -            prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported
    -        };
    +            const importDetails = {
    +                prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported
    +            };
     
    -        for (let i = 0, ii = files.length; i < ii; ++i) {
    -            setProgress(0.0);
    -            if (ii > 1) {
    -                dictImportInfo.hidden = false;
    -                dictImportInfo.textContent = `(${i + 1} of ${ii})`;
    -            }
    +            for (let i = 0, ii = files.length; i < ii; ++i) {
    +                setProgress(0.0);
    +                if (ii > 1) {
    +                    dictImportInfo.hidden = false;
    +                    dictImportInfo.textContent = `(${i + 1} of ${ii})`;
    +                }
     
    -            const archiveContent = await dictReadFile(files[i]);
    -            const {result, errors} = await api.importDictionaryArchive(archiveContent, importDetails, updateProgress);
    -            for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
    -                const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
    -                dictionaryOptions.enabled = true;
    -                options.dictionaries[result.title] = dictionaryOptions;
    -                if (result.sequenced && options.general.mainDictionary === '') {
    -                    options.general.mainDictionary = result.title;
    +                const archiveContent = await this._dictReadFile(files[i]);
    +                const {result, errors} = await api.importDictionaryArchive(archiveContent, importDetails, updateProgress);
    +                for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
    +                    const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
    +                    dictionaryOptions.enabled = true;
    +                    options.dictionaries[result.title] = dictionaryOptions;
    +                    if (result.sequenced && options.general.mainDictionary === '') {
    +                        options.general.mainDictionary = result.title;
    +                    }
                     }
    -            }
     
    -            await settingsSaveOptions();
    +                await settingsSaveOptions();
    +
    +                if (errors.length > 0) {
    +                    const errors2 = errors.map((error) => jsonToError(error));
    +                    errors2.push(`Dictionary may not have been imported properly: ${errors2.length} error${errors2.length === 1 ? '' : 's'} reported.`);
    +                    this._dictionaryErrorsShow(errors2);
    +                }
     
    -            if (errors.length > 0) {
    -                const errors2 = errors.map((error) => jsonToError(error));
    -                errors2.push(`Dictionary may not have been imported properly: ${errors2.length} error${errors2.length === 1 ? '' : 's'} reported.`);
    -                dictionaryErrorsShow(errors2);
    +                this._onDatabaseUpdated();
                 }
    +        } catch (err) {
    +            this._dictionaryErrorsShow([err]);
    +        } finally {
    +            prevention.end();
    +            this._dictionarySpinnerShow(false);
     
    -            onDatabaseUpdated();
    +            dictImportInfo.hidden = false;
    +            dictImportInfo.textContent = '';
    +            dictFile.val('');
    +            dictControls.show();
    +            dictProgress.hide();
             }
    -    } catch (err) {
    -        dictionaryErrorsShow([err]);
    -    } finally {
    -        prevention.end();
    -        dictionarySpinnerShow(false);
    -
    -        dictImportInfo.hidden = false;
    -        dictImportInfo.textContent = '';
    -        dictFile.val('');
    -        dictControls.show();
    -        dictProgress.hide();
         }
    -}
    -
    -function dictReadFile(file) {
    -    return new Promise((resolve, reject) => {
    -        const reader = new FileReader();
    -        reader.onload = () => resolve(reader.result);
    -        reader.onerror = () => reject(reader.error);
    -        reader.readAsBinaryString(file);
    -    });
    -}
     
    -
    -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();
    +    async _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 dddaef6c..1d387749 100644
    --- a/ext/bg/js/settings/main.js
    +++ b/ext/bg/js/settings/main.js
    @@ -19,14 +19,13 @@
      * AnkiController
      * AnkiTemplatesController
      * AudioController
    + * DictionaryController
      * ProfileController
      * SettingsBackup
      * SettingsController
    + * StorageController
      * api
      * appearanceInitialize
    - * dictSettingsInitialize
    - * onDictionaryOptionsChanged
    - * storageInfoInitialize
      * utilBackend
      * utilBackgroundIsolate
      */
    @@ -270,7 +269,9 @@ async function onOptionsUpdated({source}) {
         if (ankiTemplatesController !== null) {
             ankiTemplatesController.updateValue();
         }
    -    onDictionaryOptionsChanged();
    +    if (dictionaryController !== null) {
    +        dictionaryController.optionsChanged();
    +    }
         if (ankiController !== null) {
             ankiController.optionsChanged();
         }
    @@ -304,8 +305,15 @@ async function settingsPopulateModifierKeys() {
         }
     }
     
    +async function setupEnvironmentInfo() {
    +    const {browser, platform} = await api.getEnvironmentInfo();
    +    document.documentElement.dataset.browser = browser;
    +    document.documentElement.dataset.operatingSystem = platform.os;
    +}
    +
     let ankiController = null;
     let ankiTemplatesController = null;
    +let dictionaryController = null;
     
     async function onReady() {
         api.forwardLogsToBackend();
    @@ -314,22 +322,25 @@ async function onReady() {
         const settingsController = new SettingsController();
         settingsController.prepare();
     
    +    setupEnvironmentInfo();
         showExtensionInformation();
     
    +    const storageController = new StorageController();
    +    storageController.prepare();
    +
         await settingsPopulateModifierKeys();
         formSetupEventListeners();
         appearanceInitialize();
         new AudioController().prepare();
         await (new ProfileController()).prepare();
    -    await dictSettingsInitialize();
    +    dictionaryController = new DictionaryController(storageController);
    +    dictionaryController.prepare();
         ankiController = new AnkiController();
         ankiController.prepare();
         ankiTemplatesController = new AnkiTemplatesController(ankiController);
         ankiTemplatesController.prepare();
         new SettingsBackup().prepare();
     
    -    storageInfoInitialize();
    -
         yomichan.on('optionsUpdated', onOptionsUpdated);
     }
     
    diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js
    index 73c93fa1..24c6d7ef 100644
    --- a/ext/bg/js/settings/storage.js
    +++ b/ext/bg/js/settings/storage.js
    @@ -15,126 +15,117 @@
      * along with this program.  If not, see .
      */
     
    -/* global
    - * api
    - */
    -
    -function storageBytesToLabeledString(size) {
    -    const base = 1000;
    -    const labels = [' bytes', 'KB', 'MB', 'GB'];
    -    let labelIndex = 0;
    -    while (size >= base) {
    -        size /= base;
    -        ++labelIndex;
    +class StorageController {
    +    constructor() {
    +        this._mostRecentStorageEstimate = null;
    +        this._storageEstimateFailed = false;
    +        this._isUpdating = false;
         }
    -    const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
    -    return `${label}${labels[labelIndex]}`;
    -}
     
    -async function storageEstimate() {
    -    try {
    -        return (storageEstimate.mostRecent = await navigator.storage.estimate());
    -    } catch (e) {
    -        // NOP
    +    prepare() {
    +        this._preparePersistentStorage();
    +        this.updateStats();
    +        document.querySelector('#storage-refresh').addEventListener('click', this.updateStats.bind(this), false);
         }
    -    return null;
    -}
    -storageEstimate.mostRecent = null;
    -
    -async function isStoragePeristent() {
    -    try {
    -        return await navigator.storage.persisted();
    -    } catch (e) {
    -        // NOP
    -    }
    -    return false;
    -}
    -
    -async function storageInfoInitialize() {
    -    storagePersistInitialize();
    -    const {browser, platform} = await api.getEnvironmentInfo();
    -    document.documentElement.dataset.browser = browser;
    -    document.documentElement.dataset.operatingSystem = platform.os;
    -
    -    await storageShowInfo();
     
    -    document.querySelector('#storage-refresh').addEventListener('click', storageShowInfo, false);
    -}
    -
    -async function storageUpdateStats() {
    -    storageUpdateStats.isUpdating = true;
    -
    -    const estimate = await storageEstimate();
    -    const valid = (estimate !== null);
    -
    -    if (valid) {
    -        // Firefox reports usage as 0 when persistent storage is enabled.
    -        const finite = (estimate.usage > 0 || !(await isStoragePeristent()));
    -        if (finite) {
    -            document.querySelector('#storage-usage').textContent = storageBytesToLabeledString(estimate.usage);
    -            document.querySelector('#storage-quota').textContent = storageBytesToLabeledString(estimate.quota);
    +    async updateStats() {
    +        try {
    +            this._isUpdating = true;
    +
    +            const estimate = await this._storageEstimate();
    +            const valid = (estimate !== null);
    +
    +            if (valid) {
    +                // Firefox reports usage as 0 when persistent storage is enabled.
    +                const finite = (estimate.usage > 0 || !(await this._isStoragePeristent()));
    +                if (finite) {
    +                    document.querySelector('#storage-usage').textContent = this._bytesToLabeledString(estimate.usage);
    +                    document.querySelector('#storage-quota').textContent = this._bytesToLabeledString(estimate.quota);
    +                }
    +                document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite);
    +                document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite);
    +            }
    +
    +            document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid);
    +            document.querySelector('#storage-error').classList.toggle('storage-hidden', valid);
    +
    +            return valid;
    +        } finally {
    +            this._isUpdating = false;
             }
    -        document.querySelector('#storage-use-finite').classList.toggle('storage-hidden', !finite);
    -        document.querySelector('#storage-use-infinite').classList.toggle('storage-hidden', finite);
         }
     
    -    storageUpdateStats.isUpdating = false;
    -    return valid;
    -}
    -storageUpdateStats.isUpdating = false;
    -
    -async function storageShowInfo() {
    -    storageSpinnerShow(true);
    -
    -    const valid = await storageUpdateStats();
    -    document.querySelector('#storage-use').classList.toggle('storage-hidden', !valid);
    -    document.querySelector('#storage-error').classList.toggle('storage-hidden', valid);
    +    // Private
     
    -    storageSpinnerShow(false);
    -}
    +    async _preparePersistentStorage() {
    +        if (!(navigator.storage && navigator.storage.persist)) {
    +            // Not supported
    +            return;
    +        }
     
    -function storageSpinnerShow(show) {
    -    const spinner = $('#storage-spinner');
    -    if (show) {
    -        spinner.show();
    -    } else {
    -        spinner.hide();
    +        const info = document.querySelector('#storage-persist-info');
    +        const button = document.querySelector('#storage-persist-button');
    +        const checkbox = document.querySelector('#storage-persist-button-checkbox');
    +
    +        info.classList.remove('storage-hidden');
    +        button.classList.remove('storage-hidden');
    +
    +        let persisted = await this._isStoragePeristent();
    +        checkbox.checked = persisted;
    +
    +        button.addEventListener('click', async () => {
    +            if (persisted) {
    +                return;
    +            }
    +            let result = false;
    +            try {
    +                result = await navigator.storage.persist();
    +            } catch (e) {
    +                // NOP
    +            }
    +
    +            if (result) {
    +                persisted = true;
    +                checkbox.checked = true;
    +                this.updateStats();
    +            } else {
    +                document.querySelector('.storage-persist-fail-warning').classList.remove('storage-hidden');
    +            }
    +        }, false);
         }
    -}
     
    -async function storagePersistInitialize() {
    -    if (!(navigator.storage && navigator.storage.persist)) {
    -        // Not supported
    -        return;
    +    async _storageEstimate() {
    +        if (this._storageEstimateFailed && this._mostRecentStorageEstimate === null) {
    +            return null;
    +        }
    +        try {
    +            const value = await navigator.storage.estimate();
    +            this._mostRecentStorageEstimate = value;
    +            return value;
    +        } catch (e) {
    +            this._storageEstimateFailed = true;
    +        }
    +        return null;
         }
     
    -    const info = document.querySelector('#storage-persist-info');
    -    const button = document.querySelector('#storage-persist-button');
    -    const checkbox = document.querySelector('#storage-persist-button-checkbox');
    -
    -    info.classList.remove('storage-hidden');
    -    button.classList.remove('storage-hidden');
    -
    -    let persisted = await isStoragePeristent();
    -    checkbox.checked = persisted;
    -
    -    button.addEventListener('click', async () => {
    -        if (persisted) {
    -            return;
    +    _bytesToLabeledString(size) {
    +        const base = 1000;
    +        const labels = [' bytes', 'KB', 'MB', 'GB'];
    +        let labelIndex = 0;
    +        while (size >= base) {
    +            size /= base;
    +            ++labelIndex;
             }
    -        let result = false;
    +        const label = labelIndex === 0 ? `${size}` : size.toFixed(1);
    +        return `${label}${labels[labelIndex]}`;
    +    }
    +
    +    async _isStoragePeristent() {
             try {
    -            result = await navigator.storage.persist();
    +            return await navigator.storage.persisted();
             } catch (e) {
                 // NOP
             }
    -
    -        if (result) {
    -            persisted = true;
    -            checkbox.checked = true;
    -            storageShowInfo();
    -        } else {
    -            $('.storage-persist-fail-warning').removeClass('storage-hidden');
    -        }
    -    }, false);
    +        return false;
    +    }
     }
    diff --git a/ext/bg/settings.html b/ext/bg/settings.html
    index 5c7fde41..4856b0b4 100644
    --- a/ext/bg/settings.html
    +++ b/ext/bg/settings.html
    @@ -711,7 +711,6 @@
     
                 
    -

    Storage

    -- cgit v1.2.3 From 9624566d2af853141ea5c6c781a019eeea212066 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 29 May 2020 20:28:12 -0400 Subject: Convert popup-preview.js to a class (#571) * Convert popup-preview.js to a class * Don't invoke 'prepare' --- ext/bg/js/settings/main.js | 4 +- ext/bg/js/settings/popup-preview.js | 138 ++++++++++++++++++++---------------- 2 files changed, 80 insertions(+), 62 deletions(-) (limited to 'ext/bg/js/settings/main.js') diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 1d387749..b84824e6 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -20,12 +20,12 @@ * AnkiTemplatesController * AudioController * DictionaryController + * PopupPreviewController * ProfileController * SettingsBackup * SettingsController * StorageController * api - * appearanceInitialize * utilBackend * utilBackgroundIsolate */ @@ -330,7 +330,7 @@ async function onReady() { await settingsPopulateModifierKeys(); formSetupEventListeners(); - appearanceInitialize(); + new PopupPreviewController().prepare(); new AudioController().prepare(); await (new ProfileController()).prepare(); dictionaryController = new DictionaryController(storageController); diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js index fdc3dd94..d5519959 100644 --- a/ext/bg/js/settings/popup-preview.js +++ b/ext/bg/js/settings/popup-preview.js @@ -20,65 +20,83 @@ * wanakana */ -function appearanceInitialize() { - let previewVisible = false; - $('#settings-popup-preview-button').on('click', () => { - if (previewVisible) { return; } - showAppearancePreview(); - previewVisible = true; - }); -} +class PopupPreviewController { + constructor() { + this._previewVisible = false; + this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); + this._frame = null; + this._previewTextInput = null; + } + + prepare() { + document.querySelector('#settings-popup-preview-button').addEventListener('click', this._onShowPopupPreviewButtonClick.bind(this), false); + } + + // Private + + _onShowPopupPreviewButtonClick() { + if (this._previewVisible) { return; } + this._showAppearancePreview(); + this._previewVisible = true; + } + + _showAppearancePreview() { + const container = document.querySelector('#settings-popup-preview-container'); + const buttonContainer = document.querySelector('#settings-popup-preview-button-container'); + const settings = document.querySelector('#settings-popup-preview-settings'); + const text = document.querySelector('#settings-popup-preview-text'); + const customCss = document.querySelector('#custom-popup-css'); + const customOuterCss = document.querySelector('#custom-popup-outer-css'); + const frame = document.createElement('iframe'); + + this._previewTextInput = text; + this._frame = frame; + + wanakana.bind(text); + + frame.addEventListener('load', this._onFrameLoad.bind(this), false); + text.addEventListener('input', this._onTextChange.bind(this), false); + customCss.addEventListener('input', this._onCustomCssChange.bind(this), false); + customOuterCss.addEventListener('input', this._onCustomOuterCssChange.bind(this), false); + yomichan.on('modifyingProfileChange', this._onOptionsContextChange.bind(this)); + + frame.src = '/bg/settings-popup-preview.html'; + frame.id = 'settings-popup-preview-frame'; + + container.appendChild(frame); + if (buttonContainer.parentNode !== null) { + buttonContainer.parentNode.removeChild(buttonContainer); + } + settings.style.display = ''; + } + + _onFrameLoad() { + this._onOptionsContextChange(); + this._setText(this._previewTextInput.value); + } + + _onTextChange(e) { + this._setText(e.currentTarget.value); + } + + _onCustomCssChange(e) { + this._invoke('setCustomCss', {css: e.currentTarget.value}); + } + + _onCustomOuterCssChange(e) { + this._invoke('setCustomOuterCss', {css: e.currentTarget.value}); + } + + _onOptionsContextChange() { + this._invoke('updateOptionsContext', {optionsContext: getOptionsContext()}); + } + + _setText(text) { + this._invoke('setText', {text}); + } -function showAppearancePreview() { - const container = $('#settings-popup-preview-container'); - const buttonContainer = $('#settings-popup-preview-button-container'); - const settings = $('#settings-popup-preview-settings'); - const text = $('#settings-popup-preview-text'); - const customCss = $('#custom-popup-css'); - const customOuterCss = $('#custom-popup-outer-css'); - - const frame = document.createElement('iframe'); - frame.src = '/bg/settings-popup-preview.html'; - frame.id = 'settings-popup-preview-frame'; - - wanakana.bind(text[0]); - - const targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); - - text.on('input', () => { - const action = 'setText'; - const params = {text: text.val()}; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }); - customCss.on('input', () => { - const action = 'setCustomCss'; - const params = {css: customCss.val()}; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }); - customOuterCss.on('input', () => { - const action = 'setCustomOuterCss'; - const params = {css: customOuterCss.val()}; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }); - - const updateOptionsContext = () => { - const action = 'updateOptionsContext'; - const params = { - optionsContext: getOptionsContext() - }; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }; - yomichan.on('modifyingProfileChange', updateOptionsContext); - - frame.addEventListener('load', () => { - const action = 'prepare'; - const params = { - optionsContext: getOptionsContext() - }; - frame.contentWindow.postMessage({action, params}, targetOrigin); - }); - - container.append(frame); - buttonContainer.remove(); - settings.css('display', ''); + _invoke(action, params) { + if (this._frame === null || this._frame.contentWindow === null) { return; } + this._frame.contentWindow.postMessage({action, params}, this._targetOrigin); + } } -- cgit v1.2.3 From 18f376358c4c8eec1e46fe1d9396861a42559918 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 29 May 2020 20:33:40 -0400 Subject: Generic settings controller + clipboard popups controller (#573) * Create GenericSettingController * Create ClipboardPopupsController --- ext/bg/js/settings/clipboard-popups-controller.js | 52 ++++++ ext/bg/js/settings/generic-setting-controller.js | 197 +++++++++++++++++++++ ext/bg/js/settings/main.js | 202 +--------------------- ext/bg/settings.html | 2 + 4 files changed, 260 insertions(+), 193 deletions(-) create mode 100644 ext/bg/js/settings/clipboard-popups-controller.js create mode 100644 ext/bg/js/settings/generic-setting-controller.js (limited to 'ext/bg/js/settings/main.js') diff --git a/ext/bg/js/settings/clipboard-popups-controller.js b/ext/bg/js/settings/clipboard-popups-controller.js new file mode 100644 index 00000000..cb9e857f --- /dev/null +++ b/ext/bg/js/settings/clipboard-popups-controller.js @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * 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 . + */ + +/* globals + * getOptionsContext + * getOptionsMutable + * settingsSaveOptions + */ + +class ClipboardPopupsController { + prepare() { + document.querySelector('#enable-clipboard-popups').addEventListener('change', this._onEnableClipboardPopupsChanged.bind(this), false); + } + + async _onEnableClipboardPopupsChanged(e) { + const optionsContext = getOptionsContext(); + const options = await getOptionsMutable(optionsContext); + + const enableClipboardPopups = e.target.checked; + if (enableClipboardPopups) { + options.general.enableClipboardPopups = await new Promise((resolve) => { + chrome.permissions.request( + {permissions: ['clipboardRead']}, + (granted) => { + if (!granted) { + $('#enable-clipboard-popups').prop('checked', false); + } + resolve(granted); + } + ); + }); + } else { + options.general.enableClipboardPopups = false; + } + + await settingsSaveOptions(); + } +} diff --git a/ext/bg/js/settings/generic-setting-controller.js b/ext/bg/js/settings/generic-setting-controller.js new file mode 100644 index 00000000..4a20bf65 --- /dev/null +++ b/ext/bg/js/settings/generic-setting-controller.js @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * 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 . + */ + +/* globals + * getOptionsContext + * getOptionsMutable + * settingsSaveOptions + * utilBackgroundIsolate + */ + +class GenericSettingController { + prepare() { + $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(this._onFormOptionsChanged.bind(this)); + } + + optionsChanged(options) { + this._formWrite(options); + } + + // Private + + async _formWrite(options) { + $('#enable').prop('checked', options.general.enable); + $('#show-usage-guide').prop('checked', options.general.showGuide); + $('#compact-tags').prop('checked', options.general.compactTags); + $('#compact-glossaries').prop('checked', options.general.compactGlossaries); + $('#result-output-mode').val(options.general.resultOutputMode); + $('#show-debug-info').prop('checked', options.general.debugInfo); + $('#show-advanced-options').prop('checked', options.general.showAdvanced); + $('#max-displayed-results').val(options.general.maxResults); + $('#popup-display-mode').val(options.general.popupDisplayMode); + $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition); + $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition); + $('#popup-width').val(options.general.popupWidth); + $('#popup-height').val(options.general.popupHeight); + $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset); + $('#popup-vertical-offset').val(options.general.popupVerticalOffset); + $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2); + $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2); + $('#popup-scaling-factor').val(options.general.popupScalingFactor); + $('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom); + $('#popup-scale-relative-to-visual-viewport').prop('checked', options.general.popupScaleRelativeToVisualViewport); + $('#show-pitch-accent-downstep-notation').prop('checked', options.general.showPitchAccentDownstepNotation); + $('#show-pitch-accent-position-notation').prop('checked', options.general.showPitchAccentPositionNotation); + $('#show-pitch-accent-graph').prop('checked', options.general.showPitchAccentGraph); + $('#show-iframe-popups-in-root-frame').prop('checked', options.general.showIframePopupsInRootFrame); + $('#popup-theme').val(options.general.popupTheme); + $('#popup-outer-theme').val(options.general.popupOuterTheme); + $('#custom-popup-css').val(options.general.customPopupCss); + $('#custom-popup-outer-css').val(options.general.customPopupOuterCss); + + $('#audio-playback-enabled').prop('checked', options.audio.enabled); + $('#auto-play-audio').prop('checked', options.audio.autoPlay); + $('#audio-playback-volume').val(options.audio.volume); + $('#audio-custom-source').val(options.audio.customSourceUrl); + $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice); + + $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); + $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); + $('#select-matched-text').prop('checked', options.scanning.selectText); + $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); + $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); + $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan); + $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch); + $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions); + $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage); + $('#enable-search-tags').prop('checked', options.scanning.enableSearchTags); + $('#scan-delay').val(options.scanning.delay); + $('#scan-length').val(options.scanning.length); + $('#scan-modifier-key').val(options.scanning.modifier); + $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth); + + $('#translation-convert-half-width-characters').val(options.translation.convertHalfWidthCharacters); + $('#translation-convert-numeric-characters').val(options.translation.convertNumericCharacters); + $('#translation-convert-alphabetic-characters').val(options.translation.convertAlphabeticCharacters); + $('#translation-convert-hiragana-to-katakana').val(options.translation.convertHiraganaToKatakana); + $('#translation-convert-katakana-to-hiragana').val(options.translation.convertKatakanaToHiragana); + $('#translation-collapse-emphatic-sequences').val(options.translation.collapseEmphaticSequences); + + $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser); + $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser); + $('#parsing-term-spacing').prop('checked', options.parsing.termSpacing); + $('#parsing-reading-mode').val(options.parsing.readingMode); + + $('#anki-enable').prop('checked', options.anki.enable); + $('#card-tags').val(options.anki.tags.join(' ')); + $('#sentence-detection-extent').val(options.anki.sentenceExt); + $('#interface-server').val(options.anki.server); + $('#duplicate-scope').val(options.anki.duplicateScope); + $('#screenshot-format').val(options.anki.screenshot.format); + $('#screenshot-quality').val(options.anki.screenshot.quality); + + this._formUpdateVisibility(options); + } + + async _formRead(options) { + options.general.enable = $('#enable').prop('checked'); + options.general.showGuide = $('#show-usage-guide').prop('checked'); + options.general.compactTags = $('#compact-tags').prop('checked'); + options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); + options.general.resultOutputMode = $('#result-output-mode').val(); + options.general.debugInfo = $('#show-debug-info').prop('checked'); + options.general.showAdvanced = $('#show-advanced-options').prop('checked'); + options.general.maxResults = parseInt($('#max-displayed-results').val(), 10); + options.general.popupDisplayMode = $('#popup-display-mode').val(); + options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val(); + options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val(); + options.general.popupWidth = parseInt($('#popup-width').val(), 10); + options.general.popupHeight = parseInt($('#popup-height').val(), 10); + options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0); + options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); + options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0); + options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10); + options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val()); + options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked'); + options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').prop('checked'); + options.general.showPitchAccentDownstepNotation = $('#show-pitch-accent-downstep-notation').prop('checked'); + options.general.showPitchAccentPositionNotation = $('#show-pitch-accent-position-notation').prop('checked'); + options.general.showPitchAccentGraph = $('#show-pitch-accent-graph').prop('checked'); + options.general.showIframePopupsInRootFrame = $('#show-iframe-popups-in-root-frame').prop('checked'); + options.general.popupTheme = $('#popup-theme').val(); + options.general.popupOuterTheme = $('#popup-outer-theme').val(); + options.general.customPopupCss = $('#custom-popup-css').val(); + options.general.customPopupOuterCss = $('#custom-popup-outer-css').val(); + + options.audio.enabled = $('#audio-playback-enabled').prop('checked'); + options.audio.autoPlay = $('#auto-play-audio').prop('checked'); + options.audio.volume = parseFloat($('#audio-playback-volume').val()); + options.audio.customSourceUrl = $('#audio-custom-source').val(); + options.audio.textToSpeechVoice = $('#text-to-speech-voice').val(); + + options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); + options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked'); + options.scanning.selectText = $('#select-matched-text').prop('checked'); + options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); + options.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); + options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked'); + options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked'); + options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked'); + options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked'); + options.scanning.enableSearchTags = $('#enable-search-tags').prop('checked'); + options.scanning.delay = parseInt($('#scan-delay').val(), 10); + options.scanning.length = parseInt($('#scan-length').val(), 10); + options.scanning.modifier = $('#scan-modifier-key').val(); + options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10); + + options.translation.convertHalfWidthCharacters = $('#translation-convert-half-width-characters').val(); + options.translation.convertNumericCharacters = $('#translation-convert-numeric-characters').val(); + options.translation.convertAlphabeticCharacters = $('#translation-convert-alphabetic-characters').val(); + options.translation.convertHiraganaToKatakana = $('#translation-convert-hiragana-to-katakana').val(); + options.translation.convertKatakanaToHiragana = $('#translation-convert-katakana-to-hiragana').val(); + options.translation.collapseEmphaticSequences = $('#translation-collapse-emphatic-sequences').val(); + + options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked'); + options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked'); + options.parsing.termSpacing = $('#parsing-term-spacing').prop('checked'); + options.parsing.readingMode = $('#parsing-reading-mode').val(); + + options.anki.enable = $('#anki-enable').prop('checked'); + options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/)); + options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); + options.anki.server = $('#interface-server').val(); + options.anki.duplicateScope = $('#duplicate-scope').val(); + options.anki.screenshot.format = $('#screenshot-format').val(); + options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); + } + + async _onFormOptionsChanged() { + const optionsContext = getOptionsContext(); + const options = await getOptionsMutable(optionsContext); + + await this._formRead(options); + await settingsSaveOptions(); + this._formUpdateVisibility(options); + } + + _formUpdateVisibility(options) { + document.documentElement.dataset.optionsAnkiEnable = `${!!options.anki.enable}`; + document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`; + document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`; + document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`; + } +} diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index b84824e6..d6f55bde 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -19,7 +19,9 @@ * AnkiController * AnkiTemplatesController * AudioController + * ClipboardPopupsController * DictionaryController + * GenericSettingController * PopupPreviewController * ProfileController * SettingsBackup @@ -55,197 +57,6 @@ function getOptionsFullMutable() { return utilBackend().getFullOptions(); } -async function formRead(options) { - options.general.enable = $('#enable').prop('checked'); - options.general.showGuide = $('#show-usage-guide').prop('checked'); - options.general.compactTags = $('#compact-tags').prop('checked'); - options.general.compactGlossaries = $('#compact-glossaries').prop('checked'); - options.general.resultOutputMode = $('#result-output-mode').val(); - options.general.debugInfo = $('#show-debug-info').prop('checked'); - options.general.showAdvanced = $('#show-advanced-options').prop('checked'); - options.general.maxResults = parseInt($('#max-displayed-results').val(), 10); - options.general.popupDisplayMode = $('#popup-display-mode').val(); - options.general.popupHorizontalTextPosition = $('#popup-horizontal-text-position').val(); - options.general.popupVerticalTextPosition = $('#popup-vertical-text-position').val(); - options.general.popupWidth = parseInt($('#popup-width').val(), 10); - options.general.popupHeight = parseInt($('#popup-height').val(), 10); - options.general.popupHorizontalOffset = parseInt($('#popup-horizontal-offset').val(), 0); - options.general.popupVerticalOffset = parseInt($('#popup-vertical-offset').val(), 10); - options.general.popupHorizontalOffset2 = parseInt($('#popup-horizontal-offset2').val(), 0); - options.general.popupVerticalOffset2 = parseInt($('#popup-vertical-offset2').val(), 10); - options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val()); - options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked'); - options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').prop('checked'); - options.general.showPitchAccentDownstepNotation = $('#show-pitch-accent-downstep-notation').prop('checked'); - options.general.showPitchAccentPositionNotation = $('#show-pitch-accent-position-notation').prop('checked'); - options.general.showPitchAccentGraph = $('#show-pitch-accent-graph').prop('checked'); - options.general.showIframePopupsInRootFrame = $('#show-iframe-popups-in-root-frame').prop('checked'); - options.general.popupTheme = $('#popup-theme').val(); - options.general.popupOuterTheme = $('#popup-outer-theme').val(); - options.general.customPopupCss = $('#custom-popup-css').val(); - options.general.customPopupOuterCss = $('#custom-popup-outer-css').val(); - - options.audio.enabled = $('#audio-playback-enabled').prop('checked'); - options.audio.autoPlay = $('#auto-play-audio').prop('checked'); - options.audio.volume = parseFloat($('#audio-playback-volume').val()); - options.audio.customSourceUrl = $('#audio-custom-source').val(); - options.audio.textToSpeechVoice = $('#text-to-speech-voice').val(); - - options.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); - options.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked'); - options.scanning.selectText = $('#select-matched-text').prop('checked'); - options.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); - options.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); - options.scanning.deepDomScan = $('#deep-dom-scan').prop('checked'); - options.scanning.enablePopupSearch = $('#enable-search-within-first-popup').prop('checked'); - options.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked'); - options.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked'); - options.scanning.enableSearchTags = $('#enable-search-tags').prop('checked'); - options.scanning.delay = parseInt($('#scan-delay').val(), 10); - options.scanning.length = parseInt($('#scan-length').val(), 10); - options.scanning.modifier = $('#scan-modifier-key').val(); - options.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10); - - options.translation.convertHalfWidthCharacters = $('#translation-convert-half-width-characters').val(); - options.translation.convertNumericCharacters = $('#translation-convert-numeric-characters').val(); - options.translation.convertAlphabeticCharacters = $('#translation-convert-alphabetic-characters').val(); - options.translation.convertHiraganaToKatakana = $('#translation-convert-hiragana-to-katakana').val(); - options.translation.convertKatakanaToHiragana = $('#translation-convert-katakana-to-hiragana').val(); - options.translation.collapseEmphaticSequences = $('#translation-collapse-emphatic-sequences').val(); - - options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked'); - options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked'); - options.parsing.termSpacing = $('#parsing-term-spacing').prop('checked'); - options.parsing.readingMode = $('#parsing-reading-mode').val(); - - options.anki.enable = $('#anki-enable').prop('checked'); - options.anki.tags = utilBackgroundIsolate($('#card-tags').val().split(/[,; ]+/)); - options.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); - options.anki.server = $('#interface-server').val(); - options.anki.duplicateScope = $('#duplicate-scope').val(); - options.anki.screenshot.format = $('#screenshot-format').val(); - options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); -} - -async function formWrite(options) { - $('#enable').prop('checked', options.general.enable); - $('#show-usage-guide').prop('checked', options.general.showGuide); - $('#compact-tags').prop('checked', options.general.compactTags); - $('#compact-glossaries').prop('checked', options.general.compactGlossaries); - $('#result-output-mode').val(options.general.resultOutputMode); - $('#show-debug-info').prop('checked', options.general.debugInfo); - $('#show-advanced-options').prop('checked', options.general.showAdvanced); - $('#max-displayed-results').val(options.general.maxResults); - $('#popup-display-mode').val(options.general.popupDisplayMode); - $('#popup-horizontal-text-position').val(options.general.popupHorizontalTextPosition); - $('#popup-vertical-text-position').val(options.general.popupVerticalTextPosition); - $('#popup-width').val(options.general.popupWidth); - $('#popup-height').val(options.general.popupHeight); - $('#popup-horizontal-offset').val(options.general.popupHorizontalOffset); - $('#popup-vertical-offset').val(options.general.popupVerticalOffset); - $('#popup-horizontal-offset2').val(options.general.popupHorizontalOffset2); - $('#popup-vertical-offset2').val(options.general.popupVerticalOffset2); - $('#popup-scaling-factor').val(options.general.popupScalingFactor); - $('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom); - $('#popup-scale-relative-to-visual-viewport').prop('checked', options.general.popupScaleRelativeToVisualViewport); - $('#show-pitch-accent-downstep-notation').prop('checked', options.general.showPitchAccentDownstepNotation); - $('#show-pitch-accent-position-notation').prop('checked', options.general.showPitchAccentPositionNotation); - $('#show-pitch-accent-graph').prop('checked', options.general.showPitchAccentGraph); - $('#show-iframe-popups-in-root-frame').prop('checked', options.general.showIframePopupsInRootFrame); - $('#popup-theme').val(options.general.popupTheme); - $('#popup-outer-theme').val(options.general.popupOuterTheme); - $('#custom-popup-css').val(options.general.customPopupCss); - $('#custom-popup-outer-css').val(options.general.customPopupOuterCss); - - $('#audio-playback-enabled').prop('checked', options.audio.enabled); - $('#auto-play-audio').prop('checked', options.audio.autoPlay); - $('#audio-playback-volume').val(options.audio.volume); - $('#audio-custom-source').val(options.audio.customSourceUrl); - $('#text-to-speech-voice').val(options.audio.textToSpeechVoice).attr('data-value', options.audio.textToSpeechVoice); - - $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); - $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); - $('#select-matched-text').prop('checked', options.scanning.selectText); - $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); - $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); - $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan); - $('#enable-search-within-first-popup').prop('checked', options.scanning.enablePopupSearch); - $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions); - $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage); - $('#enable-search-tags').prop('checked', options.scanning.enableSearchTags); - $('#scan-delay').val(options.scanning.delay); - $('#scan-length').val(options.scanning.length); - $('#scan-modifier-key').val(options.scanning.modifier); - $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth); - - $('#translation-convert-half-width-characters').val(options.translation.convertHalfWidthCharacters); - $('#translation-convert-numeric-characters').val(options.translation.convertNumericCharacters); - $('#translation-convert-alphabetic-characters').val(options.translation.convertAlphabeticCharacters); - $('#translation-convert-hiragana-to-katakana').val(options.translation.convertHiraganaToKatakana); - $('#translation-convert-katakana-to-hiragana').val(options.translation.convertKatakanaToHiragana); - $('#translation-collapse-emphatic-sequences').val(options.translation.collapseEmphaticSequences); - - $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser); - $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser); - $('#parsing-term-spacing').prop('checked', options.parsing.termSpacing); - $('#parsing-reading-mode').val(options.parsing.readingMode); - - $('#anki-enable').prop('checked', options.anki.enable); - $('#card-tags').val(options.anki.tags.join(' ')); - $('#sentence-detection-extent').val(options.anki.sentenceExt); - $('#interface-server').val(options.anki.server); - $('#duplicate-scope').val(options.anki.duplicateScope); - $('#screenshot-format').val(options.anki.screenshot.format); - $('#screenshot-quality').val(options.anki.screenshot.quality); - - formUpdateVisibility(options); -} - -function formSetupEventListeners() { - document.querySelector('#enable-clipboard-popups').addEventListener('change', onEnableClipboardPopupsChanged, false); - $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(onFormOptionsChanged); -} - -function formUpdateVisibility(options) { - document.documentElement.dataset.optionsAnkiEnable = `${!!options.anki.enable}`; - document.documentElement.dataset.optionsGeneralDebugInfo = `${!!options.general.debugInfo}`; - document.documentElement.dataset.optionsGeneralShowAdvanced = `${!!options.general.showAdvanced}`; - document.documentElement.dataset.optionsGeneralResultOutputMode = `${options.general.resultOutputMode}`; -} - -async function onFormOptionsChanged() { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - await formRead(options); - await settingsSaveOptions(); - formUpdateVisibility(options); -} - -async function onEnableClipboardPopupsChanged(e) { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - const enableClipboardPopups = e.target.checked; - if (enableClipboardPopups) { - options.general.enableClipboardPopups = await new Promise((resolve) => { - chrome.permissions.request( - {permissions: ['clipboardRead']}, - (granted) => { - if (!granted) { - $('#enable-clipboard-popups').prop('checked', false); - } - resolve(granted); - } - ); - }); - } else { - options.general.enableClipboardPopups = false; - } - - await settingsSaveOptions(); -} - function settingsGetSource() { return new Promise((resolve) => { @@ -276,7 +87,9 @@ async function onOptionsUpdated({source}) { ankiController.optionsChanged(); } - await formWrite(options); + if (genericSettingController !== null) { + genericSettingController.optionsChanged(options); + } } @@ -314,6 +127,7 @@ async function setupEnvironmentInfo() { let ankiController = null; let ankiTemplatesController = null; let dictionaryController = null; +let genericSettingController = null; async function onReady() { api.forwardLogsToBackend(); @@ -329,7 +143,9 @@ async function onReady() { storageController.prepare(); await settingsPopulateModifierKeys(); - formSetupEventListeners(); + genericSettingController = new GenericSettingController(); + genericSettingController.prepare(); + new ClipboardPopupsController().prepare(); new PopupPreviewController().prepare(); new AudioController().prepare(); await (new ProfileController()).prepare(); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 4856b0b4..bab62519 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1141,8 +1141,10 @@ + + -- cgit v1.2.3 From 63a3e56367b95f7ea64a5701d17179de60ed8718 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 30 May 2020 09:33:13 -0400 Subject: Use SettingsController (#576) * Use settingsController internally in settings/main.js * Replace modifyingProfileChange with SettingsController.optionsContextChanged * Update ClipboardPopupsController to use SettingsController * Store reference to checkbox * Use this._settingsController for everything * Change where current profile is initially assigned from * Remove some unnecessary async calls * Move setup calls * Update AnkiTemplatesController to use SettingsController * Cache default field templates * Update AnkiController to use SettingsController * Update AudioController to use SettingsController * Update SettingsBackup to use SettingsController * Update DictionaryController to use SettingsController * Update GenericSettingController to use SettingsController * Update ProfileController to use SettingsController * Remove unused * Remove unused * Replace some uses of api.options* functions * Fix missing awaits * Fix invalid function --- ext/bg/js/settings/anki-templates.js | 35 ++++---- ext/bg/js/settings/anki.js | 53 +++++------ ext/bg/js/settings/audio.js | 33 ++++--- ext/bg/js/settings/backup.js | 12 +-- ext/bg/js/settings/clipboard-popups-controller.js | 34 ++++--- ext/bg/js/settings/dictionaries.js | 54 ++++++----- ext/bg/js/settings/dom-settings-binder.js | 1 - ext/bg/js/settings/generic-setting-controller.js | 28 +++--- ext/bg/js/settings/main.js | 97 +++----------------- ext/bg/js/settings/popup-preview.js | 9 +- ext/bg/js/settings/profiles.js | 105 +++++++++------------- ext/bg/js/settings/settings-controller.js | 5 ++ 12 files changed, 192 insertions(+), 274 deletions(-) (limited to 'ext/bg/js/settings/main.js') diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index dd128ab8..4ceff835 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -18,19 +18,20 @@ /* global * AnkiNoteBuilder * api - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions */ class AnkiTemplatesController { - constructor(ankiController) { + constructor(settingsController, ankiController) { + this._settingsController = settingsController; this._ankiController = ankiController; this._cachedDefinitionValue = null; this._cachedDefinitionText = null; + this._defaultFieldTemplates = null; } - prepare() { + async prepare() { + this._defaultFieldTemplates = await api.getDefaultAnkiFieldTemplates(); + const markers = new Set([ ...this._ankiController.getFieldMarkers('terms'), ...this._ankiController.getFieldMarkers('kanji') @@ -48,21 +49,22 @@ class AnkiTemplatesController { $('#field-templates-reset').on('click', this._onReset.bind(this)); $('#field-templates-reset-confirm').on('click', this._onResetConfirm.bind(this)); - this.updateValue(); + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); } - async updateValue() { - const optionsContext = getOptionsContext(); - const options = await api.optionsGet(optionsContext); + // Private + + _onOptionsChanged({options}) { let templates = options.anki.fieldTemplates; - if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); } + if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } $('#field-templates').val(templates); this._onValidateCompile(); } - // Private - _onReset(e) { e.preventDefault(); $('#field-template-reset-modal').modal('show'); @@ -89,10 +91,9 @@ class AnkiTemplatesController { } // Overwrite - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + const options = await this._settingsController.getOptionsMutable(); options.anki.fieldTemplates = templates; - await settingsSaveOptions(); + await this._settingsController.save(); // Compile this._onValidateCompile(); @@ -133,10 +134,10 @@ class AnkiTemplatesController { const exceptions = []; let result = `No definition found for ${text}`; try { - const optionsContext = getOptionsContext(); + const optionsContext = this._settingsController.getOptionsContext(); const definition = await this._getDefinition(text, optionsContext); if (definition !== null) { - const options = await api.optionsGet(optionsContext); + const options = await this._settingsController.getOptions(); const context = { document: { title: document.title diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index d110ef39..d099239d 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -17,38 +17,25 @@ /* global * api - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions * utilBackgroundIsolate */ class AnkiController { - prepare() { + constructor(settingsController) { + this._settingsController = settingsController; + } + + async prepare() { $('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(this._onFieldsChanged.bind(this)); for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { node.addEventListener('change', this._onModelChanged.bind(this), false); } - this.optionsChanged(); - } + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); - async optionsChanged(options=null) { - if (options === null) { - const optionsContext = getOptionsContext(); - options = await getOptionsMutable(optionsContext); - } - - if (!options.anki.enable) { - return; - } - - await this._deckAndModelPopulate(options); - await Promise.all([ - this._fieldsPopulate('terms', options), - this._fieldsPopulate('kanji', options) - ]); + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); } getFieldMarkers(type) { @@ -103,6 +90,18 @@ class AnkiController { // Private + async _onOptionsChanged({options}) { + if (!options.anki.enable) { + return; + } + + await this._deckAndModelPopulate(options); + await Promise.all([ + this._fieldsPopulate('terms', options), + this._fieldsPopulate('kanji', options) + ]); + } + _fieldsToDict(elements) { const result = {}; for (const element of elements) { @@ -277,17 +276,15 @@ class AnkiController { fields[name] = ''; } - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + const options = await this._settingsController.getOptionsMutable(); options.anki[tabId].fields = utilBackgroundIsolate(fields); - await settingsSaveOptions(); + await this._settingsController.save(); await this._fieldsPopulate(tabId, options); } async _onFieldsChanged() { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + const options = await this._settingsController.getOptionsMutable(); options.anki.terms.deck = $('#anki-terms-deck').val(); options.anki.terms.model = $('#anki-terms-model').val(); @@ -296,8 +293,6 @@ class AnkiController { options.anki.kanji.model = $('#anki-kanji-model').val(); options.anki.kanji.fields = utilBackgroundIsolate(this._fieldsToDict(document.querySelectorAll('#kanji .anki-field-value'))); - await settingsSaveOptions(); - - await this.optionsChanged(options); + await this._settingsController.save(); } } diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js index 5c1cb131..1a41a498 100644 --- a/ext/bg/js/settings/audio.js +++ b/ext/bg/js/settings/audio.js @@ -17,13 +17,11 @@ /* global * AudioSystem - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions */ class AudioController { - constructor() { + constructor(settingsController) { + this._settingsController = settingsController; this._audioSystem = null; this._settingsAudioSources = null; this._audioSourceContainer = null; @@ -37,27 +35,36 @@ class AudioController { useCache: true }); - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - this._settingsAudioSources = options.audio.sources; this._audioSourceContainer = document.querySelector('.audio-source-list'); this._audioSourceAddButton = document.querySelector('.audio-source-add'); this._audioSourceContainer.textContent = ''; this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false); - for (const audioSource of toIterable(this._settingsAudioSources)) { - this._createAudioSourceEntry(audioSource); - } - this._prepareTextToSpeech(); + + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + this._onOptionsChanged(); } // Private + async _onOptionsChanged() { + const options = await this._settingsController.getOptionsMutable(); + + for (const entry of [...this._audioSourceEntries]) { + this._removeAudioSourceEntry(entry); + } + + this._settingsAudioSources = options.audio.sources; + for (const audioSource of toIterable(this._settingsAudioSources)) { + this._createAudioSourceEntry(audioSource); + } + } + async _save() { - await settingsSaveOptions(); + await this._settingsController.save(); } _prepareTextToSpeech() { diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js index 4e104e6f..e93e15bf 100644 --- a/ext/bg/js/settings/backup.js +++ b/ext/bg/js/settings/backup.js @@ -19,13 +19,11 @@ * api * optionsGetDefault * optionsUpdateVersion - * utilBackend - * utilBackgroundIsolate - * utilIsolate */ class SettingsBackup { - constructor() { + constructor(settingsController) { + this._settingsController = settingsController; this._settingsExportToken = null; this._settingsExportRevoke = null; this._currentVersion = 0; @@ -59,7 +57,7 @@ class SettingsBackup { } async _getSettingsExportData(date) { - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFull(); const environment = await api.getEnvironmentInfo(); const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates(); @@ -143,9 +141,7 @@ class SettingsBackup { // Importing async _settingsImportSetOptionsFull(optionsFull) { - return utilIsolate(utilBackend().setFullOptions( - utilBackgroundIsolate(optionsFull) - )); + await this._settingsController.setOptionsFull(optionsFull); } _showSettingsImportError(error) { diff --git a/ext/bg/js/settings/clipboard-popups-controller.js b/ext/bg/js/settings/clipboard-popups-controller.js index cb9e857f..77fae305 100644 --- a/ext/bg/js/settings/clipboard-popups-controller.js +++ b/ext/bg/js/settings/clipboard-popups-controller.js @@ -15,29 +15,37 @@ * along with this program. If not, see . */ -/* globals - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions - */ - class ClipboardPopupsController { - prepare() { - document.querySelector('#enable-clipboard-popups').addEventListener('change', this._onEnableClipboardPopupsChanged.bind(this), false); + constructor(settingsController) { + this._settingsController = settingsController; + this._checkbox = document.querySelector('#enable-clipboard-popups'); } - async _onEnableClipboardPopupsChanged(e) { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + async prepare() { + this._checkbox.addEventListener('change', this._onEnableClipboardPopupsChanged.bind(this), false); + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); + } + // Private + + _onOptionsChanged({options}) { + this._checkbox.checked = options.general.enableClipboardPopups; + } + + async _onEnableClipboardPopupsChanged(e) { const enableClipboardPopups = e.target.checked; + const options = await this._settingsController.getOptionsMutable(); + if (enableClipboardPopups) { options.general.enableClipboardPopups = await new Promise((resolve) => { chrome.permissions.request( {permissions: ['clipboardRead']}, (granted) => { if (!granted) { - $('#enable-clipboard-popups').prop('checked', false); + this._checkbox.checked = false; } resolve(granted); } @@ -47,6 +55,6 @@ class ClipboardPopupsController { options.general.enableClipboardPopups = false; } - await settingsSaveOptions(); + await this._settingsController.save(); } } diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index dd6dd1c1..94a71233 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -18,15 +18,12 @@ /* global * PageExitPrevention * api - * getOptionsContext - * getOptionsFullMutable - * getOptionsMutable - * settingsSaveOptions * utilBackgroundIsolate */ -class SettingsDictionaryListUI { +class SettingsDictionaryListUI extends EventDispatcher { constructor(container, template, extraContainer, extraTemplate) { + super(); this.container = container; this.template = template; this.extraContainer = extraContainer; @@ -309,7 +306,7 @@ class SettingsDictionaryEntryUI { this.isDeleting = false; progress.hidden = true; - this.onDatabaseUpdated(); + this.parent.trigger('databaseUpdated'); } } @@ -384,7 +381,8 @@ class SettingsDictionaryExtraUI { } class DictionaryController { - constructor(storageController) { + constructor(settingsController, storageController) { + this._settingsController = settingsController; this._storageController = storageController; this._dictionaryUI = null; this._dictionaryErrorToStringOverrides = [ @@ -410,7 +408,8 @@ class DictionaryController { document.querySelector('#dict-groups-extra'), document.querySelector('#dict-extra-template') ); - this._dictionaryUI.save = settingsSaveOptions; + this._dictionaryUI.save = () => this._settingsController.save(); + this._dictionaryUI.on('databaseUpdated', this._onDatabaseUpdated.bind(this)); document.querySelector('#dict-purge-button').addEventListener('click', this._onPurgeButtonClick.bind(this), false); document.querySelector('#dict-purge-confirm').addEventListener('click', this._onPurgeConfirmButtonClick.bind(this), false); @@ -419,26 +418,25 @@ class DictionaryController { document.querySelector('#dict-main').addEventListener('change', this._onDictionaryMainChanged.bind(this), false); document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', this._onDatabaseEnablePrefixWildcardSearchesChanged.bind(this), false); - await this.optionsChanged(); + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + await this._onOptionsChanged(); await this._onDatabaseUpdated(); } - async optionsChanged() { - if (this._dictionaryUI === null) { return; } + // Private - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + async _onOptionsChanged() { + const options = await this._settingsController.getOptionsMutable(); this._dictionaryUI.setOptionsDictionaries(options.dictionaries); - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFull(); document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported; await this._updateMainDictionarySelectValue(); } - // Private - _updateMainDictionarySelectOptions(dictionaries) { const select = document.querySelector('#dict-main'); select.textContent = ''; // Empty @@ -460,8 +458,7 @@ class DictionaryController { } async _updateMainDictionarySelectValue() { - const optionsContext = getOptionsContext(); - const options = await api.optionsGet(optionsContext); + const options = await this._settingsController.getOptions(); const value = options.general.mainDictionary; @@ -589,10 +586,9 @@ class DictionaryController { missingNodeOption.parentNode.removeChild(missingNodeOption); } - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); + const options = await this._settingsController.getOptionsMutable(); options.general.mainDictionary = value; - await settingsSaveOptions(); + await this._settingsController.save(); } _onImportButtonClick() { @@ -622,11 +618,12 @@ class DictionaryController { this._dictionarySpinnerShow(true); await api.purgeDatabase(); - for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) { + const optionsFull = await this._settingsController.getOptionsFullMutable(); + for (const {options} of toIterable(optionsFull.profiles)) { options.dictionaries = utilBackgroundIsolate({}); options.general.mainDictionary = ''; } - await settingsSaveOptions(); + await this._settingsController.save(); this._onDatabaseUpdated(); } catch (err) { @@ -665,7 +662,7 @@ class DictionaryController { this._storageController.updateStats(); }; - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFull(); const importDetails = { prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported @@ -680,7 +677,8 @@ class DictionaryController { const archiveContent = await this._dictReadFile(files[i]); const {result, errors} = await api.importDictionaryArchive(archiveContent, importDetails, updateProgress); - for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) { + const optionsFull2 = await this._settingsController.getOptionsFullMutable(); + for (const {options} of toIterable(optionsFull2.profiles)) { const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions(); dictionaryOptions.enabled = true; options.dictionaries[result.title] = dictionaryOptions; @@ -689,7 +687,7 @@ class DictionaryController { } } - await settingsSaveOptions(); + await this._settingsController.save(); if (errors.length > 0) { const errors2 = errors.map((error) => jsonToError(error)); @@ -714,10 +712,10 @@ class DictionaryController { } async _onDatabaseEnablePrefixWildcardSearchesChanged(e) { - const optionsFull = await getOptionsFullMutable(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); const v = !!e.target.checked; if (optionsFull.global.database.prefixWildcardsSupported === v) { return; } optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked; - await settingsSaveOptions(); + await this._settingsController.save(); } } diff --git a/ext/bg/js/settings/dom-settings-binder.js b/ext/bg/js/settings/dom-settings-binder.js index 4b63859f..07da4f37 100644 --- a/ext/bg/js/settings/dom-settings-binder.js +++ b/ext/bg/js/settings/dom-settings-binder.js @@ -18,7 +18,6 @@ /* global * DOMDataBinder * api - * getOptionsContext */ class DOMSettingsBinder { diff --git a/ext/bg/js/settings/generic-setting-controller.js b/ext/bg/js/settings/generic-setting-controller.js index 4a20bf65..d7d40c5d 100644 --- a/ext/bg/js/settings/generic-setting-controller.js +++ b/ext/bg/js/settings/generic-setting-controller.js @@ -16,24 +16,26 @@ */ /* globals - * getOptionsContext - * getOptionsMutable - * settingsSaveOptions * utilBackgroundIsolate */ class GenericSettingController { - prepare() { - $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(this._onFormOptionsChanged.bind(this)); + constructor(settingsController) { + this._settingsController = settingsController; } - optionsChanged(options) { - this._formWrite(options); + async prepare() { + $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(this._onFormOptionsChanged.bind(this)); + + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + const options = await this._settingsController.getOptions(); + this._onOptionsChanged({options}); } // Private - async _formWrite(options) { + _onOptionsChanged({options}) { $('#enable').prop('checked', options.general.enable); $('#show-usage-guide').prop('checked', options.general.showGuide); $('#compact-tags').prop('checked', options.general.compactTags); @@ -107,7 +109,7 @@ class GenericSettingController { this._formUpdateVisibility(options); } - async _formRead(options) { + _formRead(options) { options.general.enable = $('#enable').prop('checked'); options.general.showGuide = $('#show-usage-guide').prop('checked'); options.general.compactTags = $('#compact-tags').prop('checked'); @@ -180,12 +182,10 @@ class GenericSettingController { } async _onFormOptionsChanged() { - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - await this._formRead(options); - await settingsSaveOptions(); + const options = await this._settingsController.getOptionsMutable(); + this._formRead(options); this._formUpdateVisibility(options); + await this._settingsController.save(); } _formUpdateVisibility(options) { diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index d6f55bde..cf74c0fc 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -28,71 +28,8 @@ * SettingsController * StorageController * api - * utilBackend - * utilBackgroundIsolate */ -let profileIndex = 0; - -function getOptionsContext() { - return {index: getProfileIndex()}; -} - -function getProfileIndex() { - return profileIndex; -} - -function setProfileIndex(value) { - profileIndex = value; -} - - -function getOptionsMutable(optionsContext) { - return utilBackend().getOptions( - utilBackgroundIsolate(optionsContext) - ); -} - -function getOptionsFullMutable() { - return utilBackend().getFullOptions(); -} - - -function settingsGetSource() { - return new Promise((resolve) => { - chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`)); - }); -} - -async function settingsSaveOptions() { - const source = await settingsGetSource(); - await api.optionsSave(source); -} - -async function onOptionsUpdated({source}) { - const thisSource = await settingsGetSource(); - if (source === thisSource) { return; } - - const optionsContext = getOptionsContext(); - const options = await getOptionsMutable(optionsContext); - - document.querySelector('#enable-clipboard-popups').checked = options.general.enableClipboardPopups; - if (ankiTemplatesController !== null) { - ankiTemplatesController.updateValue(); - } - if (dictionaryController !== null) { - dictionaryController.optionsChanged(); - } - if (ankiController !== null) { - ankiController.optionsChanged(); - } - - if (genericSettingController !== null) { - genericSettingController.optionsChanged(options); - } -} - - function showExtensionInformation() { const node = document.getElementById('extension-info'); if (node === null) { return; } @@ -124,40 +61,34 @@ async function setupEnvironmentInfo() { document.documentElement.dataset.operatingSystem = platform.os; } -let ankiController = null; -let ankiTemplatesController = null; -let dictionaryController = null; -let genericSettingController = null; async function onReady() { api.forwardLogsToBackend(); await yomichan.prepare(); - const settingsController = new SettingsController(); - settingsController.prepare(); - setupEnvironmentInfo(); showExtensionInformation(); + settingsPopulateModifierKeys(); + + const optionsFull = await api.optionsGetFull(); + const settingsController = new SettingsController(optionsFull.profileCurrent); + settingsController.prepare(); const storageController = new StorageController(); storageController.prepare(); - await settingsPopulateModifierKeys(); - genericSettingController = new GenericSettingController(); + const genericSettingController = new GenericSettingController(settingsController); genericSettingController.prepare(); - new ClipboardPopupsController().prepare(); - new PopupPreviewController().prepare(); - new AudioController().prepare(); - await (new ProfileController()).prepare(); - dictionaryController = new DictionaryController(storageController); + new ClipboardPopupsController(settingsController).prepare(); + new PopupPreviewController(settingsController).prepare(); + new AudioController(settingsController).prepare(); + new ProfileController(settingsController).prepare(); + const dictionaryController = new DictionaryController(settingsController, storageController); dictionaryController.prepare(); - ankiController = new AnkiController(); + const ankiController = new AnkiController(settingsController); ankiController.prepare(); - ankiTemplatesController = new AnkiTemplatesController(ankiController); - ankiTemplatesController.prepare(); - new SettingsBackup().prepare(); - - yomichan.on('optionsUpdated', onOptionsUpdated); + new AnkiTemplatesController(settingsController, ankiController).prepare(); + new SettingsBackup(settingsController).prepare(); } $(document).ready(() => onReady()); diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js index d5519959..d4145b76 100644 --- a/ext/bg/js/settings/popup-preview.js +++ b/ext/bg/js/settings/popup-preview.js @@ -16,12 +16,12 @@ */ /* global - * getOptionsContext * wanakana */ class PopupPreviewController { - constructor() { + constructor(settingsController) { + this._settingsController = settingsController; this._previewVisible = false; this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); this._frame = null; @@ -58,7 +58,7 @@ class PopupPreviewController { text.addEventListener('input', this._onTextChange.bind(this), false); customCss.addEventListener('input', this._onCustomCssChange.bind(this), false); customOuterCss.addEventListener('input', this._onCustomOuterCssChange.bind(this), false); - yomichan.on('modifyingProfileChange', this._onOptionsContextChange.bind(this)); + this._settingsController.on('optionsContextChanged', this._onOptionsContextChange.bind(this)); frame.src = '/bg/settings-popup-preview.html'; frame.id = 'settings-popup-preview-frame'; @@ -88,7 +88,8 @@ class PopupPreviewController { } _onOptionsContextChange() { - this._invoke('updateOptionsContext', {optionsContext: getOptionsContext()}); + const optionsContext = this._settingsController.getOptionsContext(); + this._invoke('updateOptionsContext', {optionsContext}); } _setText(text) { diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index e2c558e9..2449ab44 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -17,34 +17,19 @@ /* global * ConditionsUI - * api * conditionsClearCaches - * getOptionsFullMutable - * getProfileIndex - * onOptionsUpdated * profileConditionsDescriptor * profileConditionsDescriptorPromise - * setProfileIndex - * settingsSaveOptions * utilBackgroundIsolate */ class ProfileController { - constructor() { + constructor(settingsController) { + this._settingsController = settingsController; this._conditionsContainer = null; } async prepare() { - const optionsFull = await getOptionsFullMutable(); - setProfileIndex(optionsFull.profileCurrent); - - this._setupEventListeners(); - await this._updateTarget(optionsFull); - } - - // Private - - _setupEventListeners() { $('#profile-target').change(this._onTargetProfileChanged.bind(this)); $('#profile-name').change(this._onNameChanged.bind(this)); $('#profile-add').click(this._onAdd.bind(this)); @@ -55,6 +40,17 @@ class ProfileController { $('#profile-move-up').click(() => this._onMove(-1)); $('#profile-move-down').click(() => this._onMove(1)); $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(this._onInputChanged.bind(this)); + + this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); + + this._onOptionsChanged(); + } + + // Private + + async _onOptionsChanged() { + const optionsFull = await this._settingsController.getOptionsFullMutable(); + await this._formWrite(optionsFull); } _tryGetIntegerValue(selector, min, max) { @@ -69,7 +65,7 @@ class ProfileController { } async _formRead(optionsFull) { - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; const profile = optionsFull.profiles[currentProfileIndex]; // Current profile @@ -83,7 +79,7 @@ class ProfileController { } async _formWrite(optionsFull) { - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; const profile = optionsFull.profiles[currentProfileIndex]; this._populateSelect($('#profile-active'), optionsFull.profiles, optionsFull.profileCurrent, null); @@ -108,7 +104,7 @@ class ProfileController { $('#profile-add-condition-group') ); this._conditionsContainer.save = () => { - settingsSaveOptions(); + this._settingsController.save(); conditionsClearCaches(profileConditionsDescriptor); }; this._conditionsContainer.isolate = utilBackgroundIsolate; @@ -129,11 +125,6 @@ class ProfileController { select.val(`${currentValue}`); } - async _updateTarget(optionsFull) { - await this._formWrite(optionsFull); - await onOptionsUpdated({source: null}); - } - _createCopyName(name, profiles, maxUniqueAttempts) { let space, index, prefix, suffix; const match = /^([\w\W]*\(Copy)((\s+)(\d+))?(\)\s*)$/.exec(name); @@ -174,39 +165,32 @@ class ProfileController { return; } - const optionsFull = await getOptionsFullMutable(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); await this._formRead(optionsFull); - await settingsSaveOptions(); + await this._settingsController.save(); } async _onTargetProfileChanged() { - const optionsFull = await getOptionsFullMutable(); - const currentProfileIndex = getProfileIndex(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); + const currentProfileIndex = this._settingsController.profileIndex; const index = this._tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length); if (index === null || currentProfileIndex === index) { return; } - setProfileIndex(index); - - await this._updateTarget(optionsFull); - - yomichan.trigger('modifyingProfileChange'); + this._settingsController.profileIndex = index; } async _onAdd() { - const optionsFull = await getOptionsFullMutable(); - const currentProfileIndex = getProfileIndex(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); + const currentProfileIndex = this._settingsController.profileIndex; const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]); profile.name = this._createCopyName(profile.name, optionsFull.profiles, 100); optionsFull.profiles.push(profile); - setProfileIndex(optionsFull.profiles.length - 1); - - await this._updateTarget(optionsFull); - await settingsSaveOptions(); + this._settingsController.profileIndex = optionsFull.profiles.length - 1; - yomichan.trigger('modifyingProfileChange'); + await this._settingsController.save(); } async _onRemove(e) { @@ -214,12 +198,12 @@ class ProfileController { return await this._onRemoveConfirm(); } - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFull(); if (optionsFull.profiles.length <= 1) { return; } - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; const profile = optionsFull.profiles[currentProfileIndex]; $('#profile-remove-modal-profile-name').text(profile.name); @@ -229,36 +213,33 @@ class ProfileController { async _onRemoveConfirm() { $('#profile-remove-modal').modal('hide'); - const optionsFull = await getOptionsFullMutable(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); if (optionsFull.profiles.length <= 1) { return; } - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; optionsFull.profiles.splice(currentProfileIndex, 1); if (currentProfileIndex >= optionsFull.profiles.length) { - setProfileIndex(optionsFull.profiles.length - 1); + this._settingsController.profileIndex = optionsFull.profiles.length - 1; } if (optionsFull.profileCurrent >= optionsFull.profiles.length) { optionsFull.profileCurrent = optionsFull.profiles.length - 1; } - await this._updateTarget(optionsFull); - await settingsSaveOptions(); - - yomichan.trigger('modifyingProfileChange'); + await this._settingsController.save(); } _onNameChanged() { - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value); } async _onMove(offset) { - const optionsFull = await getOptionsFullMutable(); - const currentProfileIndex = getProfileIndex(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); + const currentProfileIndex = this._settingsController.profileIndex; const index = currentProfileIndex + offset; if (index < 0 || index >= optionsFull.profiles.length) { return; @@ -272,21 +253,18 @@ class ProfileController { optionsFull.profileCurrent = index; } - setProfileIndex(index); - - await this._updateTarget(optionsFull); - await settingsSaveOptions(); + this._settingsController.profileIndex = index; - yomichan.trigger('modifyingProfileChange'); + await this._settingsController.save(); } async _onCopy() { - const optionsFull = await api.optionsGetFull(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); if (optionsFull.profiles.length <= 1) { return; } - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; this._populateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]); $('#profile-copy-modal').modal('show'); } @@ -294,9 +272,9 @@ class ProfileController { async _onCopyConfirm() { $('#profile-copy-modal').modal('hide'); - const optionsFull = await getOptionsFullMutable(); + const optionsFull = await this._settingsController.getOptionsFullMutable(); const index = this._tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length); - const currentProfileIndex = getProfileIndex(); + const currentProfileIndex = this._settingsController.profileIndex; if (index === null || index === currentProfileIndex) { return; } @@ -304,7 +282,6 @@ class ProfileController { const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options); optionsFull.profiles[currentProfileIndex].options = profileOptions; - await this._updateTarget(optionsFull); - await settingsSaveOptions(); + await this._settingsController.save(); } } diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js index 61230226..9f903f48 100644 --- a/ext/bg/js/settings/settings-controller.js +++ b/ext/bg/js/settings/settings-controller.js @@ -65,6 +65,11 @@ class SettingsController extends EventDispatcher { return utilBackend().getFullOptions(); } + async setOptionsFull(optionsFull) { + utilBackend().setFullOptions(utilBackgroundIsolate(optionsFull)); + await this.save(); + } + getOptionsContext() { return {index: this._profileIndex}; } -- cgit v1.2.3 From 789da0206b0a452605b49e9f72c4b294088b8046 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 30 May 2020 09:50:33 -0400 Subject: Organize settings/main.js (#577) --- ext/bg/js/settings/main.js | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) (limited to 'ext/bg/js/settings/main.js') diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index cf74c0fc..e22c5e53 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -62,7 +62,7 @@ async function setupEnvironmentInfo() { } -async function onReady() { +(async () => { api.forwardLogsToBackend(); await yomichan.prepare(); @@ -71,6 +71,7 @@ async function onReady() { settingsPopulateModifierKeys(); const optionsFull = await api.optionsGetFull(); + const settingsController = new SettingsController(optionsFull.profileCurrent); settingsController.prepare(); @@ -79,16 +80,28 @@ async function onReady() { const genericSettingController = new GenericSettingController(settingsController); genericSettingController.prepare(); - new ClipboardPopupsController(settingsController).prepare(); - new PopupPreviewController(settingsController).prepare(); - new AudioController(settingsController).prepare(); - new ProfileController(settingsController).prepare(); + + const clipboardPopupsController = new ClipboardPopupsController(settingsController); + clipboardPopupsController.prepare(); + + const popupPreviewController = new PopupPreviewController(settingsController); + popupPreviewController.prepare(); + + const audioController = new AudioController(settingsController); + audioController.prepare(); + + const profileController = new ProfileController(settingsController); + profileController.prepare(); + const dictionaryController = new DictionaryController(settingsController, storageController); dictionaryController.prepare(); + const ankiController = new AnkiController(settingsController); ankiController.prepare(); - new AnkiTemplatesController(settingsController, ankiController).prepare(); - new SettingsBackup(settingsController).prepare(); -} -$(document).ready(() => onReady()); + const ankiTemplatesController = new AnkiTemplatesController(settingsController, ankiController); + ankiTemplatesController.prepare(); + + const settingsBackup = new SettingsBackup(settingsController); + settingsBackup.prepare(); +})(); -- cgit v1.2.3