summaryrefslogtreecommitdiff
path: root/ext/js/data/options-util.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/data/options-util.js')
-rw-r--r--ext/js/data/options-util.js739
1 files changed, 739 insertions, 0 deletions
diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js
new file mode 100644
index 00000000..1105dfed
--- /dev/null
+++ b/ext/js/data/options-util.js
@@ -0,0 +1,739 @@
+/*
+ * Copyright (C) 2016-2021 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 <https://www.gnu.org/licenses/>.
+ */
+
+/* global
+ * JsonSchemaValidator
+ * TemplatePatcher
+ */
+
+class OptionsUtil {
+ constructor() {
+ this._schemaValidator = new JsonSchemaValidator();
+ this._templatePatcher = null;
+ this._optionsSchema = null;
+ }
+
+ async prepare() {
+ this._optionsSchema = await this._fetchAsset('/data/schemas/options-schema.json', true);
+ }
+
+ async update(options) {
+ // Invalid options
+ if (!isObject(options)) {
+ options = {};
+ }
+
+ // Check for legacy options
+ let defaultProfileOptions = {};
+ if (!Array.isArray(options.profiles)) {
+ defaultProfileOptions = options;
+ options = {};
+ }
+
+ // Ensure profiles is an array
+ if (!Array.isArray(options.profiles)) {
+ options.profiles = [];
+ }
+
+ // Remove invalid profiles
+ const profiles = options.profiles;
+ for (let i = profiles.length - 1; i >= 0; --i) {
+ if (!isObject(profiles[i])) {
+ profiles.splice(i, 1);
+ }
+ }
+
+ // Require at least one profile
+ if (profiles.length === 0) {
+ profiles.push({
+ name: 'Default',
+ options: defaultProfileOptions,
+ conditionGroups: []
+ });
+ }
+
+ // 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;
+ }
+
+ // Version
+ if (typeof options.version !== 'number') {
+ options.version = 0;
+ }
+
+ // Generic updates
+ options = await this._applyUpdates(options, this._getVersionUpdates());
+
+ // Validation
+ options = this._schemaValidator.getValidValueOrDefault(this._optionsSchema, options);
+
+ // Result
+ return options;
+ }
+
+ async load() {
+ let options;
+ try {
+ const optionsStr = await new Promise((resolve, reject) => {
+ chrome.storage.local.get(['options'], (store) => {
+ const error = chrome.runtime.lastError;
+ if (error) {
+ reject(new Error(error.message));
+ } else {
+ resolve(store.options);
+ }
+ });
+ });
+ options = JSON.parse(optionsStr);
+ } catch (e) {
+ // NOP
+ }
+
+ if (typeof options !== 'undefined') {
+ options = await this.update(options);
+ } else {
+ options = this.getDefault();
+ }
+
+ return options;
+ }
+
+ save(options) {
+ return new Promise((resolve, reject) => {
+ chrome.storage.local.set({options: JSON.stringify(options)}, () => {
+ const error = chrome.runtime.lastError;
+ if (error) {
+ reject(new Error(error.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
+
+ getDefault() {
+ const optionsVersion = this._getVersionUpdates().length;
+ const options = this._schemaValidator.getValidValueOrDefault(this._optionsSchema);
+ options.version = optionsVersion;
+ return options;
+ }
+
+ createValidatingProxy(options) {
+ return this._schemaValidator.createProxy(options, this._optionsSchema);
+ }
+
+ validate(options) {
+ return this._schemaValidator.validate(options, this._optionsSchema);
+ }
+
+ // Legacy profile updating
+
+ _legacyProfileUpdateGetUpdates() {
+ return [
+ null,
+ null,
+ null,
+ null,
+ (options) => {
+ options.general.audioSource = options.general.audioPlayback ? 'jpod101' : 'disabled';
+ },
+ (options) => {
+ options.general.showGuide = false;
+ },
+ (options) => {
+ options.scanning.modifier = options.scanning.requireShift ? 'shift' : 'none';
+ },
+ (options) => {
+ options.general.resultOutputMode = options.general.groupResults ? 'group' : 'split';
+ options.anki.fieldTemplates = null;
+ },
+ (options) => {
+ if (this._getStringHashCode(options.anki.fieldTemplates) === 1285806040) {
+ options.anki.fieldTemplates = null;
+ }
+ },
+ (options) => {
+ if (this._getStringHashCode(options.anki.fieldTemplates) === -250091611) {
+ options.anki.fieldTemplates = null;
+ }
+ },
+ (options) => {
+ const oldAudioSource = options.general.audioSource;
+ const disabled = oldAudioSource === 'disabled';
+ options.audio.enabled = !disabled;
+ options.audio.volume = options.general.audioVolume;
+ options.audio.autoPlay = options.general.autoPlayAudio;
+ options.audio.sources = [disabled ? 'jpod101' : oldAudioSource];
+
+ delete options.general.audioSource;
+ delete options.general.audioVolume;
+ delete options.general.autoPlayAudio;
+ },
+ (options) => {
+ // Version 12 changes:
+ // The preferred default value of options.anki.fieldTemplates has been changed to null.
+ if (this._getStringHashCode(options.anki.fieldTemplates) === 1444379824) {
+ options.anki.fieldTemplates = null;
+ }
+ },
+ (options) => {
+ // Version 13 changes:
+ // Default anki field tempaltes updated to include {document-title}.
+ let fieldTemplates = options.anki.fieldTemplates;
+ if (typeof fieldTemplates === 'string') {
+ fieldTemplates += '\n\n{{#*inline "document-title"}}\n {{~context.document.title~}}\n{{/inline}}';
+ options.anki.fieldTemplates = fieldTemplates;
+ }
+ },
+ (options) => {
+ // Version 14 changes:
+ // Changed template for Anki audio and tags.
+ let fieldTemplates = options.anki.fieldTemplates;
+ if (typeof fieldTemplates !== 'string') { return; }
+
+ const replacements = [
+ [
+ '{{#*inline "audio"}}{{/inline}}',
+ '{{#*inline "audio"}}\n {{~#if definition.audioFileName~}}\n [sound:{{definition.audioFileName}}]\n {{~/if~}}\n{{/inline}}'
+ ],
+ [
+ '{{#*inline "tags"}}\n {{~#each definition.definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each~}}\n{{/inline}}',
+ '{{#*inline "tags"}}\n {{~#mergeTags definition group merge}}{{this}}{{/mergeTags~}}\n{{/inline}}'
+ ]
+ ];
+
+ for (const [pattern, replacement] of replacements) {
+ let replaced = false;
+ fieldTemplates = fieldTemplates.replace(new RegExp(escapeRegExp(pattern), 'g'), () => {
+ replaced = true;
+ return replacement;
+ });
+
+ if (!replaced) {
+ fieldTemplates += '\n\n' + replacement;
+ }
+ }
+
+ options.anki.fieldTemplates = fieldTemplates;
+ }
+ ];
+ }
+
+ _legacyProfileUpdateGetDefaults() {
+ return {
+ general: {
+ enable: true,
+ enableClipboardPopups: false,
+ resultOutputMode: 'group',
+ debugInfo: false,
+ maxResults: 32,
+ showAdvanced: false,
+ popupDisplayMode: 'default',
+ popupWidth: 400,
+ popupHeight: 250,
+ popupHorizontalOffset: 0,
+ popupVerticalOffset: 10,
+ popupHorizontalOffset2: 10,
+ popupVerticalOffset2: 0,
+ popupHorizontalTextPosition: 'below',
+ popupVerticalTextPosition: 'before',
+ popupScalingFactor: 1,
+ popupScaleRelativeToPageZoom: false,
+ popupScaleRelativeToVisualViewport: true,
+ showGuide: true,
+ compactTags: false,
+ compactGlossaries: false,
+ mainDictionary: '',
+ popupTheme: 'default',
+ popupOuterTheme: 'default',
+ customPopupCss: '',
+ customPopupOuterCss: '',
+ enableWanakana: true,
+ enableClipboardMonitor: false,
+ showPitchAccentDownstepNotation: true,
+ showPitchAccentPositionNotation: true,
+ showPitchAccentGraph: false,
+ showIframePopupsInRootFrame: false,
+ useSecurePopupFrameUrl: true,
+ usePopupShadowDom: true
+ },
+
+ audio: {
+ enabled: true,
+ sources: ['jpod101'],
+ volume: 100,
+ autoPlay: false,
+ customSourceUrl: '',
+ textToSpeechVoice: ''
+ },
+
+ scanning: {
+ middleMouse: true,
+ touchInputEnabled: true,
+ selectText: true,
+ alphanumeric: true,
+ autoHideResults: false,
+ delay: 20,
+ length: 10,
+ modifier: 'shift',
+ deepDomScan: false,
+ popupNestingMaxDepth: 0,
+ enablePopupSearch: false,
+ enableOnPopupExpressions: false,
+ enableOnSearchPage: true,
+ enableSearchTags: false,
+ layoutAwareScan: false
+ },
+
+ translation: {
+ convertHalfWidthCharacters: 'false',
+ convertNumericCharacters: 'false',
+ convertAlphabeticCharacters: 'false',
+ convertHiraganaToKatakana: 'false',
+ convertKatakanaToHiragana: 'variant',
+ collapseEmphaticSequences: 'false'
+ },
+
+ dictionaries: {},
+
+ parsing: {
+ enableScanningParser: true,
+ enableMecabParser: false,
+ selectedParser: null,
+ termSpacing: true,
+ readingMode: 'hiragana'
+ },
+
+ anki: {
+ enable: false,
+ server: 'http://127.0.0.1:8765',
+ tags: ['yomichan'],
+ sentenceExt: 200,
+ screenshot: {format: 'png', quality: 92},
+ terms: {deck: '', model: '', fields: {}},
+ kanji: {deck: '', model: '', fields: {}},
+ duplicateScope: 'collection',
+ fieldTemplates: null
+ }
+ };
+ }
+
+ _legacyProfileUpdateAssignDefaults(options) {
+ const defaults = this._legacyProfileUpdateGetDefaults();
+
+ const combine = (target, source) => {
+ for (const key in source) {
+ if (!Object.prototype.hasOwnProperty.call(target, key)) {
+ target[key] = source[key];
+ }
+ }
+ };
+
+ combine(options, defaults);
+ combine(options.general, defaults.general);
+ combine(options.scanning, defaults.scanning);
+ combine(options.anki, defaults.anki);
+ combine(options.anki.terms, defaults.anki.terms);
+ combine(options.anki.kanji, defaults.anki.kanji);
+
+ return options;
+ }
+
+ _legacyProfileUpdateUpdateVersion(options) {
+ const updates = this._legacyProfileUpdateGetUpdates();
+ this._legacyProfileUpdateAssignDefaults(options);
+
+ const targetVersion = updates.length;
+ const currentVersion = options.version;
+
+ if (typeof currentVersion === 'number' && Number.isFinite(currentVersion)) {
+ for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) {
+ const update = updates[i];
+ if (update !== null) {
+ update(options);
+ }
+ }
+ }
+
+ options.version = targetVersion;
+ return options;
+ }
+
+ // Private
+
+ async _applyAnkiFieldTemplatesPatch(options, modificationsUrl) {
+ let patch = null;
+ for (const {options: profileOptions} of options.profiles) {
+ const fieldTemplates = profileOptions.anki.fieldTemplates;
+ if (fieldTemplates === null) { continue; }
+
+ if (patch === null) {
+ const content = await this._fetchAsset(modificationsUrl);
+ if (this._templatePatcher === null) {
+ this._templatePatcher = new TemplatePatcher();
+ }
+ patch = this._templatePatcher.parsePatch(content);
+ }
+
+ profileOptions.anki.fieldTemplates = this._templatePatcher.applyPatch(fieldTemplates, patch);
+ }
+ }
+
+ async _fetchAsset(url, json=false) {
+ url = chrome.runtime.getURL(url);
+ const response = await fetch(url, {
+ method: 'GET',
+ mode: 'no-cors',
+ cache: 'default',
+ credentials: 'omit',
+ redirect: 'follow',
+ referrerPolicy: 'no-referrer'
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to fetch ${url}: ${response.status}`);
+ }
+ return await (json ? response.json() : response.text());
+ }
+
+ _getStringHashCode(string) {
+ let hashCode = 0;
+
+ if (typeof string !== 'string') { return hashCode; }
+
+ for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) {
+ hashCode = ((hashCode << 5) - hashCode) + charCode;
+ hashCode |= 0;
+ }
+
+ return hashCode;
+ }
+
+ async _applyUpdates(options, updates) {
+ const targetVersion = updates.length;
+ let currentVersion = options.version;
+
+ if (typeof currentVersion !== 'number' || !Number.isFinite(currentVersion)) {
+ currentVersion = 0;
+ }
+
+ for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) {
+ const {update, async} = updates[i];
+ const result = update(options);
+ options = (async ? await result : result);
+ }
+
+ options.version = targetVersion;
+ return options;
+ }
+
+ _getVersionUpdates() {
+ return [
+ {async: false, update: this._updateVersion1.bind(this)},
+ {async: false, update: this._updateVersion2.bind(this)},
+ {async: true, update: this._updateVersion3.bind(this)},
+ {async: true, update: this._updateVersion4.bind(this)},
+ {async: false, update: this._updateVersion5.bind(this)},
+ {async: true, update: this._updateVersion6.bind(this)},
+ {async: false, update: this._updateVersion7.bind(this)},
+ {async: true, update: this._updateVersion8.bind(this)}
+ ];
+ }
+
+ _updateVersion1(options) {
+ // Version 1 changes:
+ // Added options.global.database.prefixWildcardsSupported = false.
+ options.global = {
+ database: {
+ prefixWildcardsSupported: false
+ }
+ };
+ return options;
+ }
+
+ _updateVersion2(options) {
+ // Version 2 changes:
+ // Legacy profile update process moved into this upgrade function.
+ for (const profile of options.profiles) {
+ if (!Array.isArray(profile.conditionGroups)) {
+ profile.conditionGroups = [];
+ }
+ profile.options = this._legacyProfileUpdateUpdateVersion(profile.options);
+ }
+ return options;
+ }
+
+ async _updateVersion3(options) {
+ // Version 3 changes:
+ // Pitch accent Anki field templates added.
+ await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v2.handlebars');
+ return options;
+ }
+
+ async _updateVersion4(options) {
+ // Version 4 changes:
+ // Options conditions converted to string representations.
+ // Added usePopupWindow.
+ // Updated handlebars templates to include "clipboard-image" definition.
+ // Updated handlebars templates to include "clipboard-text" definition.
+ // Added hideDelay.
+ // Added inputs to profileOptions.scanning.
+ // Added pointerEventsEnabled to profileOptions.scanning.
+ // Added preventMiddleMouse to profileOptions.scanning.
+ for (const {conditionGroups} of options.profiles) {
+ for (const {conditions} of conditionGroups) {
+ for (const condition of conditions) {
+ const value = condition.value;
+ condition.value = (
+ Array.isArray(value) ?
+ value.join(', ') :
+ `${value}`
+ );
+ }
+ }
+ }
+ const createInputDefaultOptions = () => ({
+ showAdvanced: false,
+ searchTerms: true,
+ searchKanji: true,
+ scanOnTouchMove: true,
+ scanOnPenHover: true,
+ scanOnPenPress: true,
+ scanOnPenRelease: false,
+ preventTouchScrolling: true
+ });
+ for (const {options: profileOptions} of options.profiles) {
+ profileOptions.general.usePopupWindow = false;
+ profileOptions.scanning.hideDelay = 0;
+ profileOptions.scanning.pointerEventsEnabled = false;
+ profileOptions.scanning.preventMiddleMouse = {
+ onWebPages: false,
+ onPopupPages: false,
+ onSearchPages: false,
+ onSearchQuery: false
+ };
+
+ const {modifier, middleMouse} = profileOptions.scanning;
+ delete profileOptions.scanning.modifier;
+ delete profileOptions.scanning.middleMouse;
+ const scanningInputs = [];
+ let modifierInput = '';
+ switch (modifier) {
+ case 'alt':
+ case 'ctrl':
+ case 'shift':
+ case 'meta':
+ modifierInput = modifier;
+ break;
+ case 'none':
+ modifierInput = '';
+ break;
+ }
+ scanningInputs.push({
+ include: modifierInput,
+ exclude: 'mouse0',
+ types: {mouse: true, touch: false, pen: false},
+ options: createInputDefaultOptions()
+ });
+ if (middleMouse) {
+ scanningInputs.push({
+ include: 'mouse2',
+ exclude: '',
+ types: {mouse: true, touch: false, pen: false},
+ options: createInputDefaultOptions()
+ });
+ }
+ scanningInputs.push({
+ include: '',
+ exclude: '',
+ types: {mouse: false, touch: true, pen: true},
+ options: createInputDefaultOptions()
+ });
+ profileOptions.scanning.inputs = scanningInputs;
+ }
+ await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v4.handlebars');
+ return options;
+ }
+
+ _updateVersion5(options) {
+ // Version 5 changes:
+ // Removed legacy version number from profile options.
+ for (const profile of options.profiles) {
+ delete profile.options.version;
+ }
+ return options;
+ }
+
+ async _updateVersion6(options) {
+ // Version 6 changes:
+ // Updated handlebars templates to include "conjugation" definition.
+ // Added global option showPopupPreview.
+ // Added global option useSettingsV2.
+ // Added anki.checkForDuplicates.
+ // Added general.glossaryLayoutMode; removed general.compactGlossaries.
+ await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v6.handlebars');
+ options.global.showPopupPreview = false;
+ options.global.useSettingsV2 = false;
+ for (const profile of options.profiles) {
+ profile.options.anki.checkForDuplicates = true;
+ profile.options.general.glossaryLayoutMode = (profile.options.general.compactGlossaries ? 'compact' : 'default');
+ delete profile.options.general.compactGlossaries;
+ const fieldTemplates = profile.options.anki.fieldTemplates;
+ if (typeof fieldTemplates === 'string') {
+ profile.options.anki.fieldTemplates = this._updateVersion6AnkiTemplatesCompactTags(fieldTemplates);
+ }
+ }
+ return options;
+ }
+
+ _updateVersion6AnkiTemplatesCompactTags(templates) {
+ const rawPattern1 = '{{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}';
+ const pattern1 = new RegExp(`((\r?\n)?[ \t]*)${escapeRegExp(rawPattern1)}`, 'g');
+ const replacement1 = (
+ // eslint-disable-next-line indent
+`{{~#scope~}}
+ {{~#set "any" false}}{{/set~}}
+ {{~#if definitionTags~}}{{#each definitionTags~}}
+ {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}}
+ {{~#if (get "any")}}, {{else}}<i>({{/if~}}
+ {{name}}
+ {{~#set "any" true}}{{/set~}}
+ {{~/if~}}
+ {{~/each~}}
+ {{~#if (get "any")}})</i> {{/if~}}
+ {{~/if~}}
+{{~/scope~}}`
+ );
+ const simpleNewline = /\n/g;
+ templates = templates.replace(pattern1, (g0, space) => (space + replacement1.replace(simpleNewline, space)));
+ templates = templates.replace(/\bcompactGlossaries=((?:\.*\/)*)compactGlossaries\b/g, (g0, g1) => `${g0} data=${g1}.`);
+ return templates;
+ }
+
+ _updateVersion7(options) {
+ // Version 7 changes:
+ // Added general.maximumClipboardSearchLength.
+ // Added general.popupCurrentIndicatorMode.
+ // Added general.popupActionBarVisibility.
+ // Added general.popupActionBarLocation.
+ // Removed global option showPopupPreview.
+ delete options.global.showPopupPreview;
+ for (const profile of options.profiles) {
+ profile.options.general.maximumClipboardSearchLength = 1000;
+ profile.options.general.popupCurrentIndicatorMode = 'triangle';
+ profile.options.general.popupActionBarVisibility = 'auto';
+ profile.options.general.popupActionBarLocation = 'right';
+ }
+ return options;
+ }
+
+ async _updateVersion8(options) {
+ // Version 8 changes:
+ // Added translation.textReplacements.
+ // Moved anki.sentenceExt to sentenceParsing.scanExtent.
+ // Added sentenceParsing.enableTerminationCharacters.
+ // Added sentenceParsing.terminationCharacters.
+ // Changed general.popupActionBarLocation.
+ // Added inputs.hotkeys.
+ // Added anki.suspendNewCards.
+ // Added popupWindow.
+ // Updated handlebars templates to include "stroke-count" definition.
+ // Updated global.useSettingsV2 to be true (opt-out).
+ // Added audio.customSourceType.
+ // Moved general.enableClipboardPopups => clipboard.enableBackgroundMonitor.
+ // Moved general.enableClipboardMonitor => clipboard.enableSearchPageMonitor. Forced value to false due to a bug which caused its value to not be read.
+ // Moved general.maximumClipboardSearchLength => clipboard.maximumSearchLength.
+ // Added clipboard.autoSearchContent.
+ await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v8.handlebars');
+ options.global.useSettingsV2 = true;
+ for (const profile of options.profiles) {
+ profile.options.translation.textReplacements = {
+ searchOriginal: true,
+ groups: []
+ };
+ profile.options.sentenceParsing = {
+ scanExtent: profile.options.anki.sentenceExt,
+ enableTerminationCharacters: true,
+ terminationCharacters: [
+ {enabled: true, character1: '「', character2: '」', includeCharacterAtStart: false, includeCharacterAtEnd: false},
+ {enabled: true, character1: '『', character2: '』', includeCharacterAtStart: false, includeCharacterAtEnd: false},
+ {enabled: true, character1: '"', character2: '"', includeCharacterAtStart: false, includeCharacterAtEnd: false},
+ {enabled: true, character1: '\'', character2: '\'', includeCharacterAtStart: false, includeCharacterAtEnd: false},
+ {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
+ {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
+ {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
+ {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
+ {enabled: true, character1: '。', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
+ {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
+ {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
+ {enabled: true, character1: '…', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}
+ ]
+ };
+ delete profile.options.anki.sentenceExt;
+ profile.options.general.popupActionBarLocation = 'top';
+ profile.options.inputs = {
+ hotkeys: [
+ {action: 'close', key: 'Escape', modifiers: [], scopes: ['popup'], enabled: true},
+ {action: 'focusSearchBox', key: 'Escape', modifiers: [], scopes: ['search'], enabled: true},
+ {action: 'previousEntry3', key: 'PageUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'nextEntry3', key: 'PageDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'lastEntry', key: 'End', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'firstEntry', key: 'Home', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'previousEntry', key: 'ArrowUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'nextEntry', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'historyBackward', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'historyForward', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'addNoteKanji', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'addNoteTermKanji', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'addNoteTermKana', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'playAudio', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'viewNote', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
+ {action: 'copyHostSelection', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup'], enabled: true}
+ ]
+ };
+ profile.options.anki.suspendNewCards = false;
+ profile.options.popupWindow = {
+ width: profile.options.general.popupWidth,
+ height: profile.options.general.popupHeight,
+ left: 0,
+ top: 0,
+ useLeft: false,
+ useTop: false,
+ windowType: 'popup',
+ windowState: 'normal'
+ };
+ profile.options.audio.customSourceType = 'audio';
+ profile.options.clipboard = {
+ enableBackgroundMonitor: profile.options.general.enableClipboardPopups,
+ enableSearchPageMonitor: false,
+ autoSearchContent: true,
+ maximumSearchLength: profile.options.general.maximumClipboardSearchLength
+ };
+ delete profile.options.general.enableClipboardPopups;
+ delete profile.options.general.enableClipboardMonitor;
+ delete profile.options.general.maximumClipboardSearchLength;
+ }
+ return options;
+ }
+}