diff options
Diffstat (limited to 'ext')
| -rw-r--r-- | ext/bg/js/api.js | 6 | ||||
| -rw-r--r-- | ext/bg/js/backend.js | 10 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 91 | ||||
| -rw-r--r-- | ext/bg/js/settings-profiles.js | 263 | ||||
| -rw-r--r-- | ext/bg/js/settings.js | 47 | ||||
| -rw-r--r-- | ext/bg/settings.html | 72 | 
6 files changed, 449 insertions, 40 deletions
| diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 81772d08..f32b984f 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -21,9 +21,13 @@ function apiOptionsGet(optionsContext) {      return utilBackend().getOptions(optionsContext);  } +function apiOptionsGetFull() { +    return utilBackend().getFullOptions(); +} +  async function apiOptionsSave(source) {      const backend = utilBackend(); -    const options = await backend.getFullOptions(); +    const options = await apiOptionsGetFull();      await optionsSave(options);      backend.onOptionsUpdated(source);  } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 9a300d62..3839da39 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -165,7 +165,15 @@ class Backend {      }      getOptionsSync(optionsContext) { -        return this.options; +        return this.getProfileSync(optionsContext).options; +    } + +    getProfileSync(optionsContext) { +        const profiles = this.options.profiles; +        if (typeof optionsContext.index === 'number') { +            return profiles[optionsContext.index]; +        } +        return this.options.profiles[this.options.profileCurrent];      }      setExtensionBadgeBackgroundColor(color) { diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 5f04ec31..3dce5221 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -17,7 +17,11 @@   */ -function optionsApplyUpdates(options, updates) { +/* + * Generic options functions + */ + +function optionsGenericApplyUpdates(options, updates) {      const targetVersion = updates.length;      const currentVersion = options.version;      if (typeof currentVersion === 'number' && Number.isFinite(currentVersion)) { @@ -33,7 +37,12 @@ function optionsApplyUpdates(options, updates) {      return options;  } -const optionsVersionUpdates = [ + +/* + * Per-profile options + */ + +const profileOptionsVersionUpdates = [      null,      null,      null, @@ -48,7 +57,7 @@ const optionsVersionUpdates = [          options.scanning.modifier = options.scanning.requireShift ? 'shift' : 'none';      },      (options) => { -        const fieldTemplatesDefault = optionsFieldTemplates(); +        const fieldTemplatesDefault = profileOptionsGetDefaultFieldTemplates();          options.general.resultOutputMode = options.general.groupResults ? 'group' : 'split';          options.anki.fieldTemplates = (              (utilStringHashCode(options.anki.fieldTemplates) !== -805327496) ? @@ -58,17 +67,17 @@ const optionsVersionUpdates = [      },      (options) => {          if (utilStringHashCode(options.anki.fieldTemplates) === 1285806040) { -            options.anki.fieldTemplates = optionsFieldTemplates(); +            options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates();          }      },      (options) => {          if (utilStringHashCode(options.anki.fieldTemplates) === -250091611) { -            options.anki.fieldTemplates = optionsFieldTemplates(); +            options.anki.fieldTemplates = profileOptionsGetDefaultFieldTemplates();          }      }  ]; -function optionsFieldTemplates() { +function profileOptionsGetDefaultFieldTemplates() {      return `  {{#*inline "glossary-single"}}      {{~#unless brief~}} @@ -234,7 +243,7 @@ function optionsFieldTemplates() {  `.trim();  } -function optionsCreateDefaults() { +function profileOptionsCreateDefaults() {      return {          general: {              enable: true, @@ -286,13 +295,13 @@ function optionsCreateDefaults() {              screenshot: {format: 'png', quality: 92},              terms: {deck: '', model: '', fields: {}},              kanji: {deck: '', model: '', fields: {}}, -            fieldTemplates: optionsFieldTemplates() +            fieldTemplates: profileOptionsGetDefaultFieldTemplates()          }      };  } -function optionsSetDefaults(options) { -    const defaults = optionsCreateDefaults(); +function profileOptionsSetDefaults(options) { +    const defaults = profileOptionsCreateDefaults();      const combine = (target, source) => {          for (const key in source) { @@ -312,9 +321,59 @@ function optionsSetDefaults(options) {      return options;  } -function optionsVersion(options) { -    optionsSetDefaults(options); -    return optionsApplyUpdates(options, optionsVersionUpdates); +function profileOptionsUpdateVersion(options) { +    profileOptionsSetDefaults(options); +    return optionsGenericApplyUpdates(options, profileOptionsVersionUpdates); +} + + +/* + * Global options + */ + +const optionsVersionUpdates = []; + +function optionsUpdateVersion(options, defaultProfileOptions) { +    // Ensure profiles is an array +    if (!Array.isArray(options.profiles)) { +        options.profiles = []; +    } + +    // Remove invalid +    const profiles = options.profiles; +    for (let i = profiles.length - 1; i >= 0; --i) { +        if (!utilIsObject(profiles[i])) { +            profiles.splice(i, 1); +        } +    } + +    // Require at least one profile +    if (profiles.length === 0) { +        profiles.push({ +            name: 'Default', +            options: defaultProfileOptions +        }); +    } + +    // Ensure profileCurrent is valid +    const profileCurrent = options.profileCurrent; +    if (!( +        typeof profileCurrent === 'number' && +        Number.isFinite(profileCurrent) && +        Math.floor(profileCurrent) === profileCurrent && +        profileCurrent >= 0 && +        profileCurrent < profiles.length +    )) { +        options.profileCurrent = 0; +    } + +    // Update profile options +    for (const profile of profiles) { +        profile.options = profileOptionsUpdateVersion(profile.options); +    } + +    // Generic updates +    return optionsGenericApplyUpdates(options, optionsVersionUpdates);  }  function optionsLoad() { @@ -338,7 +397,11 @@ function optionsLoad() {      }).catch(() => {          return {};      }).then(options => { -        return optionsVersion(options); +        return ( +            Array.isArray(options.profiles) ? +            optionsUpdateVersion(options, {}) : +            optionsUpdateVersion({}, options) +        );      });  } diff --git a/ext/bg/js/settings-profiles.js b/ext/bg/js/settings-profiles.js new file mode 100644 index 00000000..624562c6 --- /dev/null +++ b/ext/bg/js/settings-profiles.js @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +let currentProfileIndex = 0; + +function getOptionsContext() { +    return { +        index: currentProfileIndex +    }; +} + + +async function profileOptionsSetup() { +    const optionsFull = await apiOptionsGetFull(); +    currentProfileIndex = optionsFull.profileCurrent; + +    profileOptionsSetupEventListeners(); +    await profileOptionsUpdateTarget(optionsFull); +} + +function profileOptionsSetupEventListeners() { +    $('#profile-target').change(utilAsync(onTargetProfileChanged)); +    $('#profile-name').change(onProfileNameChanged); +    $('#profile-add').click(utilAsync(onProfileAdd)); +    $('#profile-remove').click(utilAsync(onProfileRemove)); +    $('#profile-remove-confirm').click(utilAsync(onProfileRemoveConfirm)); +    $('#profile-copy').click(utilAsync(onProfileCopy)); +    $('#profile-copy-confirm').click(utilAsync(onProfileCopyConfirm)); +    $('#profile-move-up').click(() => onProfileMove(-1)); +    $('#profile-move-down').click(() => onProfileMove(1)); +    $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change(utilAsync(onProfileOptionsChanged)); +} + +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 function profileFormRead(optionsFull) { +    const profile = optionsFull.profiles[currentProfileIndex]; + +    // Current profile +    const index = tryGetIntegerValue('#profile-active', 0, optionsFull.profiles.length); +    if (index !== null) { +        optionsFull.profileCurrent = index; +    } + +    // Profile name +    profile.name = $('#profile-name').val(); +} + +async function profileFormWrite(optionsFull) { +    const profile = optionsFull.profiles[currentProfileIndex]; + +    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); +} + +function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) { +    select.empty(); + + +    for (let i = 0; i < profiles.length; ++i) { +        if (ignoreIndices !== null && ignoreIndices.indexOf(i) >= 0) { +            continue; +        } +        const profile = profiles[i]; +        select.append($(`<option value="${i}">${profile.name}</option>`)); +    } + +    select.val(`${currentValue}`); +} + +async function profileOptionsUpdateTarget(optionsFull) { +    profileFormWrite(optionsFull); + +    const optionsContext = getOptionsContext(); +    const options = await apiOptionsGet(optionsContext); +    await formWrite(options); +} + +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; +        } 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; +        } +    } +} + +async function onProfileOptionsChanged(e) { +    if (!e.originalEvent && !e.isTrigger) { +        return; +    } + +    const optionsFull = await apiOptionsGetFull(); +    await profileFormRead(optionsFull); +    await apiOptionsSave(); +} + +async function onTargetProfileChanged() { +    const optionsFull = await apiOptionsGetFull(); +    const index = tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length); +    if (index === null || currentProfileIndex === index) { +        return; +    } + +    currentProfileIndex = index; + +    await profileOptionsUpdateTarget(optionsFull); +} + +async function onProfileAdd() { +    const optionsFull = await apiOptionsGetFull(); +    const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]); +    profile.name = profileOptionsCreateCopyName(profile.name, optionsFull.profiles, 100); +    optionsFull.profiles.push(profile); +    currentProfileIndex = optionsFull.profiles.length - 1; +    await profileOptionsUpdateTarget(optionsFull); +    await apiOptionsSave(); +} + +async function onProfileRemove(e) { +    if (e.shiftKey) { +        return await onProfileRemoveConfirm(); +    } + +    const optionsFull = await apiOptionsGetFull(); +    if (optionsFull.profiles.length <= 1) { +        return; +    } + +    const profile = optionsFull.profiles[currentProfileIndex]; + +    $('#profile-remove-modal-profile-name').text(profile.name); +    $('#profile-remove-modal').modal('show'); +} + +async function onProfileRemoveConfirm() { +    $('#profile-remove-modal').modal('hide'); + +    const optionsFull = await apiOptionsGetFull(); +    if (optionsFull.profiles.length <= 1) { +        return; +    } + +    optionsFull.profiles.splice(currentProfileIndex, 1); + +    if (currentProfileIndex >= optionsFull.profiles.length) { +        --currentProfileIndex; +    } + +    if (optionsFull.profileCurrent >= optionsFull.profiles.length) { +        optionsFull.profileCurrent = optionsFull.profiles.length - 1; +    } + +    await profileOptionsUpdateTarget(optionsFull); +    await apiOptionsSave(); +} + +function onProfileNameChanged() { +    $('#profile-active, #profile-target').find(`[value="${currentProfileIndex}"]`).text(this.value); +} + +async function onProfileMove(offset) { +    const optionsFull = await apiOptionsGetFull(); +    const index = currentProfileIndex + offset; +    if (index < 0 || index >= optionsFull.profiles.length) { +        return; +    } + +    const profile = optionsFull.profiles[currentProfileIndex]; +    optionsFull.profiles.splice(currentProfileIndex, 1); +    optionsFull.profiles.splice(index, 0, profile); + +    if (optionsFull.profileCurrent === currentProfileIndex) { +        optionsFull.profileCurrent = index; +    } + +    currentProfileIndex = index; + +    await profileOptionsUpdateTarget(optionsFull); +    await settingsSaveOptions(); +} + +async function onProfileCopy() { +    const optionsFull = await apiOptionsGetFull(); +    if (optionsFull.profiles.length <= 1) { +        return; +    } + +    profileOptionsPopulateSelect($('#profile-copy-source'), optionsFull.profiles, currentProfileIndex === 0 ? 1 : 0, [currentProfileIndex]); +    $('#profile-copy-modal').modal('show'); +} + +async function onProfileCopyConfirm() { +    $('#profile-copy-modal').modal('hide'); + +    const optionsFull = await apiOptionsGetFull(); +    const index = tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length); +    if (index === null || index === currentProfileIndex) { +        return; +    } + +    const profileOptions = utilBackgroundIsolate(optionsFull.profiles[index].options); +    optionsFull.profiles[currentProfileIndex].options = profileOptions; + +    await profileOptionsUpdateTarget(optionsFull); +    await settingsSaveOptions(); +} diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 3d581ba5..cb3ddd4e 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -16,10 +16,9 @@   * along with this program.  If not, see <http://www.gnu.org/licenses/>.   */ -function getOptionsContext() { -    return { -        depth: 0 -    }; +async function getOptionsArray() { +    const optionsFull = await apiOptionsGetFull(); +    return optionsFull.profiles.map(profile => profile.options);  }  async function formRead(options) { @@ -239,11 +238,8 @@ async function onFormOptionsChanged(e) {  }  async function onReady() { -    const optionsContext = getOptionsContext(); -    const options = await apiOptionsGet(optionsContext); -      formSetupEventListeners(); -    await formWrite(options); +    await profileOptionsSetup();      storageInfoInitialize(); @@ -424,12 +420,14 @@ async function onDictionaryPurge(e) {          dictionarySpinnerShow(true);          await utilDatabasePurge(); -        const optionsContext = getOptionsContext(); -        const options = await apiOptionsGet(optionsContext); -        options.dictionaries = utilBackgroundIsolate({}); -        options.general.mainDictionary = ''; +        for (const options of await getOptionsArray()) { +            options.dictionaries = utilBackgroundIsolate({}); +            options.general.mainDictionary = ''; +        }          await settingsSaveOptions(); +        const optionsContext = getOptionsContext(); +        const options = await apiOptionsGet(optionsContext);          await dictionaryGroupsPopulate(options);          await formMainDictionaryOptionsPopulate(options);      } catch (e) { @@ -466,24 +464,25 @@ async function onDictionaryImport(e) {          const exceptions = [];          const summary = await utilDatabaseImport(e.target.files[0], updateProgress, exceptions); -        const optionsContext = getOptionsContext(); -        const options = await apiOptionsGet(optionsContext); -        options.dictionaries[summary.title] = utilBackgroundIsolate({ -            enabled: true, -            priority: 0, -            allowSecondarySearches: false -        }); -        if (summary.sequenced && options.general.mainDictionary === '') { -            options.general.mainDictionary = summary.title; +        for (const options of await getOptionsArray()) { +            options.dictionaries[summary.title] = utilBackgroundIsolate({ +                enabled: true, +                priority: 0, +                allowSecondarySearches: false +            }); +            if (summary.sequenced && options.general.mainDictionary === '') { +                options.general.mainDictionary = summary.title; +            }          } +        await settingsSaveOptions();          if (exceptions.length > 0) {              exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`);              dictionaryErrorsShow(exceptions);          } -        await settingsSaveOptions(); - +        const optionsContext = getOptionsContext(); +        const options = await apiOptionsGet(optionsContext);          await dictionaryGroupsPopulate(options);          await formMainDictionaryOptionsPopulate(options);      } catch (e) { @@ -643,7 +642,7 @@ async function onAnkiFieldTemplatesReset(e) {          e.preventDefault();          const optionsContext = getOptionsContext();          const options = await apiOptionsGet(optionsContext); -        const fieldTemplates = optionsFieldTemplates(); +        const fieldTemplates = profileOptionsGetDefaultFieldTemplates();          options.anki.fieldTemplates = fieldTemplates;          $('#field-templates').val(fieldTemplates);          await settingsSaveOptions(); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 7df47980..c0489894 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -67,6 +67,77 @@      </head>      <body>          <div class="container-fluid"> +            <div class="profile-form"> +                <h3>Profiles</h3> + +                <p class="help-block"> +                    Profiles allow you to create multiple configurations and quickly switch between them. +                </p> + +                <div class="form-group"> +                    <label for="profile-active">Active profile</label> +                    <select class="form-control" id="profile-active"></select> +                </div> + +                <div class="form-group"> +                    <label for="profile-target">Modifying profile</label> +                    <div class="input-group"> +                        <div class="input-group-btn"> +                            <button class="btn btn-default" id="profile-add" title="Add"><span class="glyphicon glyphicon-plus"></span></button> +                            <button class="btn btn-default" id="profile-move-up" title="Move up"><span class="glyphicon glyphicon-arrow-up"></span></button> +                            <button class="btn btn-default" id="profile-move-down" title="Move down"><span class="glyphicon glyphicon-arrow-down"></span></button> +                            <button class="btn btn-default" id="profile-copy" title="Copy"><span class="glyphicon glyphicon-copy"></span></button> +                        </div> +                        <select class="form-control profile-form-manual" id="profile-target"></select> +                        <div class="input-group-btn"> +                            <button class="btn btn-danger" id="profile-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button> +                        </div> +                    </div> +                </div> + +                <div class="form-group"> +                    <label for="profile-name">Profile name</label> +                    <input type="text" id="profile-name" class="form-control"> +                </div> + +                <div class="modal fade" tabindex="-1" role="dialog" id="profile-copy-modal"> +                    <div class="modal-dialog modal-dialog-centered"> +                        <div class="modal-content"> +                            <div class="modal-header"> +                                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> +                                <h4 class="modal-title">Copy Profile</h4> +                            </div> +                            <div class="modal-body"> +                                <p>Select which profile to copy options from:</p> +                                <select class="form-control" id="profile-copy-source"></select> +                            </div> +                            <div class="modal-footer"> +                                <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> +                                <button type="button" class="btn btn-primary" id="profile-copy-confirm">Copy Profile</button> +                            </div> +                        </div> +                    </div> +                </div> + +                <div class="modal fade" tabindex="-1" role="dialog" id="profile-remove-modal"> +                    <div class="modal-dialog modal-dialog-centered"> +                        <div class="modal-content"> +                            <div class="modal-header"> +                                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> +                                <h4 class="modal-title">Confirm profile removal</h4> +                            </div> +                            <div class="modal-body"> +                                Are you sure you want to delete the profile <em id="profile-remove-modal-profile-name"></em>? +                            </div> +                            <div class="modal-footer"> +                                <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> +                                <button type="button" class="btn btn-danger" id="profile-remove-confirm">Remove Profile</button> +                            </div> +                        </div> +                    </div> +                </div> +            </div> +              <div>                  <h3>General Options</h3> @@ -498,6 +569,7 @@          <script src="/bg/js/templates.js"></script>          <script src="/bg/js/util.js"></script> +        <script src="/bg/js/settings-profiles.js"></script>          <script src="/bg/js/settings.js"></script>      </body>  </html> |