diff options
author | Alex Yatskov <alex@foosoft.net> | 2019-12-03 18:30:22 -0800 |
---|---|---|
committer | Alex Yatskov <alex@foosoft.net> | 2019-12-03 18:30:22 -0800 |
commit | f9ea6206550ceee625ea93215a6e08d45a750086 (patch) | |
tree | 803fe11a788a631076b3fb11a98e50bb8b454396 /ext/bg | |
parent | 08ad2779678cd447bd747c2b155ef9b5135fdf5d (diff) | |
parent | 3975aabf4dc283d49ec46d0ed7ead982b9fa7441 (diff) |
Merge branch 'master' into testing
Diffstat (limited to 'ext/bg')
40 files changed, 3249 insertions, 1532 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html index 3ab68639..5a6970c3 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -14,14 +14,15 @@ <body> <div id="clipboard-paste-target" contenteditable="true"></div> - <script src="/mixed/lib/dexie.min.js"></script> <script src="/mixed/lib/handlebars.min.js"></script> <script src="/mixed/lib/jszip.min.js"></script> <script src="/mixed/lib/wanakana.min.js"></script> - <script src="/mixed/js/extension.js"></script> + <script src="/mixed/js/core.js"></script> + <script src="/mixed/js/dom.js"></script> <script src="/bg/js/anki.js"></script> + <script src="/bg/js/mecab.js"></script> <script src="/bg/js/api.js"></script> <script src="/bg/js/audio.js"></script> <script src="/bg/js/backend-api-forwarder.js"></script> diff --git a/ext/bg/context.html b/ext/bg/context.html index 7e08dddd..eda09a68 100644 --- a/ext/bg/context.html +++ b/ext/bg/context.html @@ -11,7 +11,6 @@ <link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128"> <link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap/css/bootstrap-theme.min.css"> - <link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap-toggle/bootstrap-toggle.min.css"> <style type="text/css"> body { padding: 10px; @@ -89,12 +88,73 @@ .link-group-label { vertical-align: middle; } + + + .toggle { + width: 60px; + height: 34px; + position: relative; + overflow: hidden; + } + .toggle-group { + position: absolute; + width: 200%; + left: 0; + top: 0; + bottom: 0; + user-select: none; + } + .toggle-group.toggle-group-animated { + transition: transform 0.35s; + } + .toggle-on, + .toggle-off { + position: absolute; + top: 0; + bottom: 0; + margin: 0; + border: 0; + border-radius: 0; + } + .toggle-on { + padding-right: 24px; + left: 0; + right: 50%; + } + .toggle-off { + padding-left: 24px; + left: 50%; + right: 0; + } + .toggle-handle { + position: relative; + margin: 0 auto; + padding-top: 0; + padding-bottom: 0; + height: 100%; + width: 0; + border-width: 0 1px; + } + + .toggle>input[type=checkbox] { + display: none; + } + .toggle>input[type=checkbox]:not(:checked)~.toggle-group { + transform: translateX(-50%); + } </style> </head> <body> <div id="mini"> <div> - <input type="checkbox" id="enable-search"> + <label class="btn btn-primary toggle"> + <input type="checkbox" id="enable-search" /> + <div class="toggle-group"> + <span class="btn btn-primary toggle-on">On</span> + <span class="btn btn-default active toggle-off">Off</span> + <span class="btn btn-default toggle-handle"></span> + </div> + </label> </div> <div class="btn-group"> <a title="Search (Alt + Insert) (Middle click to open in new tab)" class="btn btn-default btn-xs action-open-search"><span class="glyphicon glyphicon-search"></span></a> @@ -118,10 +178,8 @@ </a> </div> - <script src="/mixed/lib/jquery.min.js"></script> - <script src="/mixed/lib/bootstrap-toggle/bootstrap-toggle.min.js"></script> - - <script src="/mixed/js/extension.js"></script> + <script src="/mixed/js/core.js"></script> + <script src="/mixed/js/dom.js"></script> <script src="/bg/js/api.js"></script> <script src="/bg/js/options.js"></script> diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index b3d5b884..8adae47c 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -17,9 +17,16 @@ */ -#anki-spinner, #anki-general, #anki-error, -#dict-spinner, #dict-error, #dict-warning, #dict-purge, #dict-import-progress, -#debug, .options-advanced, .storage-hidden, #storage-spinner { +#anki-spinner, +#dict-spinner, #dict-import-progress, +.storage-hidden, #storage-spinner { + display: none; +} + +html:root:not([data-options-anki-enable=true]) #anki-general, +html:root:not([data-options-general-debug-info=true]) .debug, +html:root:not([data-options-general-show-advanced=true]) .options-advanced, +html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group { display: none; } @@ -30,12 +37,6 @@ padding: 10px; } -#field-templates { - font-family: monospace; - overflow-x: hidden; - white-space: pre; -} - .bottom-links { padding-bottom: 1em; } @@ -129,14 +130,24 @@ } #custom-popup-css, -#custom-popup-outer-css { +#custom-popup-outer-css, +#field-templates { width: 100%; min-height: 34px; + line-height: 18px; height: 96px; resize: vertical; font-family: 'Courier New', Courier, monospace; white-space: pre; } +#field-templates { + height: 240px; + border-bottom-left-radius: 0; +} +#field-templates-reset { + border-top-left-radius: 0; + border-top-right-radius: 0; +} .btn-inner-middle { vertical-align: middle; @@ -158,6 +169,24 @@ input[type=checkbox].storage-button-checkbox { height: 320px; } +.dict-delete-table { + display: table; + width: 100%; +} +.dict-delete-table>*:first-child { + display: table-cell; + vertical-align: middle; + padding-right: 1em; +} +.dict-delete-table>*:nth-child(n+2) { + display: table-cell; + width: 100%; + vertical-align: middle; +} +.dict-delete-table .progress { + margin: 0; +} + [data-show-for-browser], [data-show-for-operating-system] { display: none; diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js index 9f851f13..17b93620 100644 --- a/ext/bg/js/anki.js +++ b/ext/bg/js/anki.js @@ -74,7 +74,7 @@ class AnkiConnect { async findNoteIds(notes) { await this.checkVersion(); - const actions = notes.map(note => ({ + const actions = notes.map((note) => ({ action: 'findNotes', params: { query: `deck:"${AnkiConnect.escapeQuery(note.deckName)}" ${AnkiConnect.fieldsToQuery(note.fields)}` @@ -108,11 +108,11 @@ class AnkiConnect { */ class AnkiNull { - async addNote(note) { + async addNote() { return null; } - async canAddNotes(notes) { + async canAddNotes() { return []; } @@ -124,15 +124,15 @@ class AnkiNull { return []; } - async getModelFieldNames(modelName) { + async getModelFieldNames() { return []; } - async guiBrowse(query) { + async guiBrowse() { return []; } - async findNoteIds(notes) { + async findNoteIds() { return []; } } diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 3209cc31..b489b8d2 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -25,42 +25,36 @@ async function apiOptionsSet(changedOptions, optionsContext, source) { const options = await apiOptionsGet(optionsContext); function getValuePaths(obj) { - let valuePaths = []; - let nodes = [{ - obj, - path: [] - }]; + const valuePaths = []; + const nodes = [{obj, path: []}]; while (nodes.length > 0) { - let node = nodes.pop(); - Object.keys(node.obj).forEach((key) => { - let path = node.path.concat(key); - let value = node.obj[key]; - if (typeof value === 'object') { - nodes.unshift({ - obj: value, - path: path - }); + const node = nodes.pop(); + for (const key of Object.keys(node.obj)) { + const path = node.path.concat(key); + const obj = node.obj[key]; + if (obj !== null && typeof obj === 'object') { + nodes.unshift({obj, path}); } else { - valuePaths.push([value, path]); + valuePaths.push([obj, path]); } - }); + } } return valuePaths; } function modifyOption(path, value, options) { let pivot = options; - for (let pathKey of path.slice(0, -1)) { - if (!(pathKey in pivot)) { + for (const key of path.slice(0, -1)) { + if (!hasOwn(pivot, key)) { return false; } - pivot = pivot[pathKey]; + pivot = pivot[key]; } pivot[path[path.length - 1]] = value; return true; } - for (let [value, path] of getValuePaths(changedOptions)) { + for (const [value, path] of getValuePaths(changedOptions)) { modifyOption(path, value, options); } @@ -78,33 +72,83 @@ async function apiOptionsSave(source) { backend.onOptionsUpdated(source); } -async function apiTermsFind(text, optionsContext) { +async function apiTermsFind(text, details, optionsContext) { const options = await apiOptionsGet(optionsContext); - const translator = utilBackend().translator; + const [definitions, length] = await utilBackend().translator.findTerms(text, details, options); + definitions.splice(options.general.maxResults); + return {length, definitions}; +} - const searcher = { - 'merge': translator.findTermsMerged, - 'split': translator.findTermsSplit, - 'group': translator.findTermsGrouped - }[options.general.resultOutputMode].bind(translator); +async function apiTextParse(text, optionsContext) { + const options = await apiOptionsGet(optionsContext); + const translator = utilBackend().translator; - const {definitions, length} = await searcher( - text, - dictEnabledSet(options), - options.scanning.alphanumeric, - options - ); + const results = []; + while (text.length > 0) { + const term = []; + const [definitions, sourceLength] = await translator.findTermsInternal( + text.slice(0, options.scanning.length), + dictEnabledSet(options), + options.scanning.alphanumeric, + {} + ); + if (definitions.length > 0) { + dictTermsSort(definitions); + const {expression, reading} = definitions[0]; + const source = text.slice(0, sourceLength); + for (const {text, furigana} of jpDistributeFuriganaInflected(expression, reading, source)) { + const reading = jpConvertReading(text, furigana, options.parsing.readingMode); + term.push({text, reading}); + } + text = text.slice(source.length); + } else { + const reading = jpConvertReading(text[0], null, options.parsing.readingMode); + term.push({text: text[0], reading}); + text = text.slice(1); + } + results.push(term); + } + return results; +} - return { - length, - definitions: definitions.slice(0, options.general.maxResults) - }; +async function apiTextParseMecab(text, optionsContext) { + const options = await apiOptionsGet(optionsContext); + const mecab = utilBackend().mecab; + + const results = {}; + const rawResults = await mecab.parseText(text); + for (const mecabName in rawResults) { + const result = []; + for (const parsedLine of rawResults[mecabName]) { + for (const {expression, reading, source} of parsedLine) { + const term = []; + if (expression !== null && reading !== null) { + for (const {text, furigana} of jpDistributeFuriganaInflected( + expression, + jpKatakanaToHiragana(reading), + source + )) { + const reading = jpConvertReading(text, furigana, options.parsing.readingMode); + term.push({text, reading}); + } + } else { + const reading = jpConvertReading(source, null, options.parsing.readingMode); + term.push({text: source, reading}); + } + result.push(term); + } + result.push([{text: '\n'}]); + } + results[mecabName] = result; + } + return results; } async function apiKanjiFind(text, optionsContext) { const options = await apiOptionsGet(optionsContext); - const definitions = await utilBackend().translator.findKanji(text, dictEnabledSet(options)); - return definitions.slice(0, options.general.maxResults); + const definitions = await utilBackend().translator.findKanji(text, options); + definitions.splice(options.general.maxResults); + return definitions; } async function apiDefinitionAdd(definition, mode, context, optionsContext) { @@ -163,7 +207,7 @@ async function apiDefinitionsAddable(definitions, modes, optionsContext) { } if (cannotAdd.length > 0) { - const noteIdsArray = await anki.findNoteIds(cannotAdd.map(e => e[0])); + const noteIdsArray = await anki.findNoteIds(cannotAdd.map((e) => e[0])); for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) { const noteIds = noteIdsArray[i]; if (noteIds.length > 0) { @@ -192,7 +236,7 @@ async function apiTemplateRender(template, data, dynamic) { async function apiCommandExec(command, params) { const handlers = apiCommandExec.handlers; - if (handlers.hasOwnProperty(command)) { + if (hasOwn(handlers, command)) { const handler = handlers[command]; handler(params); } @@ -360,7 +404,9 @@ async function apiGetBrowser() { if (info.name === 'Fennec') { return 'firefox-mobile'; } - } catch (e) { } + } catch (e) { + // NOP + } return 'firefox'; } else { return 'chrome'; diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 3efcce46..dc0ba5eb 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -107,12 +107,12 @@ const audioUrlBuilders = { 'custom': async (definition, optionsContext) => { const options = await apiOptionsGet(optionsContext); const customSourceUrl = options.audio.customSourceUrl; - return customSourceUrl.replace(/\{([^\}]*)\}/g, (m0, m1) => (definition.hasOwnProperty(m1) ? `${definition[m1]}` : m0)); + return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0)); } }; async function audioGetUrl(definition, mode, optionsContext, download) { - if (audioUrlBuilders.hasOwnProperty(mode)) { + if (hasOwn(audioUrlBuilders, mode)) { const handler = audioUrlBuilders[mode]; try { return await handler(definition, optionsContext, download); @@ -128,12 +128,12 @@ function audioUrlNormalize(url, baseUrl, basePath) { if (url[0] === '/') { if (url.length >= 2 && url[1] === '/') { // Begins with "//" - url = baseUrl.substr(0, baseUrl.indexOf(':') + 1) + url; + url = baseUrl.substring(0, baseUrl.indexOf(':') + 1) + url; } else { // Begins with "/" url = baseUrl + url; } - } else if (!/^[a-z][a-z0-9\+\-\.]*:/i.test(url)) { + } else if (!/^[a-z][a-z0-9\-+.]*:/i.test(url)) { // No URI scheme => relative path url = baseUrl + basePath + url; } @@ -171,7 +171,7 @@ async function audioInject(definition, fields, sources, optionsContext) { try { let audioSourceDefinition = definition; - if (definition.hasOwnProperty('expressions')) { + if (hasOwn(definition, 'expressions')) { audioSourceDefinition = definition.expressions[0]; } diff --git a/ext/bg/js/backend-api-forwarder.js b/ext/bg/js/backend-api-forwarder.js index 979afd16..db4d30b9 100644 --- a/ext/bg/js/backend-api-forwarder.js +++ b/ext/bg/js/backend-api-forwarder.js @@ -37,8 +37,8 @@ class BackendApiForwarder { const forwardPort = chrome.tabs.connect(tabId, {name: 'frontend-api-receiver'}); - port.onMessage.addListener(message => forwardPort.postMessage(message)); - forwardPort.onMessage.addListener(message => port.postMessage(message)); + port.onMessage.addListener((message) => forwardPort.postMessage(message)); + forwardPort.onMessage.addListener((message) => port.postMessage(message)); port.onDisconnect.addListener(() => forwardPort.disconnect()); forwardPort.onDisconnect.addListener(() => port.disconnect()); } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 71393467..d9f9b586 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -21,6 +21,7 @@ class Backend { constructor() { this.translator = new Translator(); this.anki = new AnkiNull(); + this.mecab = new Mecab(); this.options = null; this.optionsContext = { depth: 0, @@ -59,7 +60,7 @@ class Backend { this.applyOptions(); const callback = () => this.checkLastError(chrome.runtime.lastError); - chrome.tabs.query({}, tabs => { + chrome.tabs.query({}, (tabs) => { for (const tab of tabs) { chrome.tabs.sendMessage(tab.id, {action: 'optionsUpdate', params: {source}}, callback); } @@ -72,12 +73,12 @@ class Backend { onMessage({action, params}, sender, callback) { const handlers = Backend.messageHandlers; - if (handlers.hasOwnProperty(action)) { + if (hasOwn(handlers, action)) { const handler = handlers[action]; const promise = handler(params, sender); promise.then( - result => callback({result}), - error => callback({error: errorToJson(error)}) + (result) => callback({result}), + (error) => callback({error: errorToJson(error)}) ); } @@ -97,6 +98,12 @@ class Backend { } this.anki = options.anki.enable ? new AnkiConnect(options.anki.server) : new AnkiNull(); + + if (options.parsing.enableMecabParser) { + this.mecab.startListener(); + } else { + this.mecab.stopListener(); + } } async getFullOptions() { @@ -170,7 +177,7 @@ class Backend { } } - checkLastError(e) { + checkLastError() { // NOP } } @@ -179,7 +186,9 @@ Backend.messageHandlers = { optionsGet: ({optionsContext}) => apiOptionsGet(optionsContext), optionsSet: ({changedOptions, optionsContext, source}) => apiOptionsSet(changedOptions, optionsContext, source), kanjiFind: ({text, optionsContext}) => apiKanjiFind(text, optionsContext), - termsFind: ({text, optionsContext}) => apiTermsFind(text, optionsContext), + termsFind: ({text, details, optionsContext}) => apiTermsFind(text, details, optionsContext), + textParse: ({text, optionsContext}) => apiTextParse(text, optionsContext), + textParseMecab: ({text, optionsContext}) => apiTextParseMecab(text, optionsContext), definitionAdd: ({definition, mode, context, optionsContext}) => apiDefinitionAdd(definition, mode, context, optionsContext), definitionsAddable: ({definitions, modes, optionsContext}) => apiDefinitionsAddable(definitions, modes, optionsContext), noteView: ({noteId}) => apiNoteView(noteId), diff --git a/ext/bg/js/conditions-ui.js b/ext/bg/js/conditions-ui.js index 43c6dc08..cc9db087 100644 --- a/ext/bg/js/conditions-ui.js +++ b/ext/bg/js/conditions-ui.js @@ -84,7 +84,7 @@ ConditionsUI.Container = class Container { createDefaultCondition(type) { let operator = ''; let value = ''; - if (this.conditionDescriptors.hasOwnProperty(type)) { + if (hasOwn(this.conditionDescriptors, type)) { const conditionDescriptor = this.conditionDescriptors[type]; operator = conditionDescriptor.defaultOperator; ({value} = this.getOperatorDefaultValue(type, operator)); @@ -96,15 +96,15 @@ ConditionsUI.Container = class Container { } getOperatorDefaultValue(type, operator) { - if (this.conditionDescriptors.hasOwnProperty(type)) { + if (hasOwn(this.conditionDescriptors, type)) { const conditionDescriptor = this.conditionDescriptors[type]; - if (conditionDescriptor.operators.hasOwnProperty(operator)) { + if (hasOwn(conditionDescriptor.operators, operator)) { const operatorDescriptor = conditionDescriptor.operators[operator]; - if (operatorDescriptor.hasOwnProperty('defaultValue')) { + if (hasOwn(operatorDescriptor, 'defaultValue')) { return {value: operatorDescriptor.defaultValue, fromOperator: true}; } } - if (conditionDescriptor.hasOwnProperty('defaultValue')) { + if (hasOwn(conditionDescriptor, 'defaultValue')) { return {value: conditionDescriptor.defaultValue, fromOperator: false}; } } @@ -219,7 +219,7 @@ ConditionsUI.Condition = class Condition { optionGroup.empty(); const type = this.condition.type; - if (conditionDescriptors.hasOwnProperty(type)) { + if (hasOwn(conditionDescriptors, type)) { const conditionDescriptor = conditionDescriptors[type]; const operators = conditionDescriptor.operators; for (const operatorName of Object.keys(operators)) { @@ -240,23 +240,23 @@ ConditionsUI.Condition = class Condition { }; const objects = []; - if (conditionDescriptors.hasOwnProperty(type)) { + if (hasOwn(conditionDescriptors, type)) { const conditionDescriptor = conditionDescriptors[type]; objects.push(conditionDescriptor); - if (conditionDescriptor.operators.hasOwnProperty(operator)) { + if (hasOwn(conditionDescriptor.operators, operator)) { const operatorDescriptor = conditionDescriptor.operators[operator]; objects.push(operatorDescriptor); } } for (const object of objects) { - if (object.hasOwnProperty('placeholder')) { + if (hasOwn(object, 'placeholder')) { props.placeholder = object.placeholder; } if (object.type === 'number') { props.type = 'number'; for (const prop of ['step', 'min', 'max']) { - if (object.hasOwnProperty(prop)) { + if (hasOwn(object, prop)) { props[prop] = object[prop]; } } diff --git a/ext/bg/js/conditions.js b/ext/bg/js/conditions.js index ed4b14f5..c0f0f301 100644 --- a/ext/bg/js/conditions.js +++ b/ext/bg/js/conditions.js @@ -18,14 +18,14 @@ function conditionsValidateOptionValue(object, value) { - if (object.hasOwnProperty('validate') && !object.validate(value)) { + if (hasOwn(object, 'validate') && !object.validate(value)) { throw new Error('Invalid value for condition'); } - if (object.hasOwnProperty('transform')) { + if (hasOwn(object, 'transform')) { value = object.transform(value); - if (object.hasOwnProperty('validateTransformed') && !object.validateTransformed(value)) { + if (hasOwn(object, 'validateTransformed') && !object.validateTransformed(value)) { throw new Error('Invalid value for condition'); } } @@ -34,12 +34,12 @@ function conditionsValidateOptionValue(object, value) { } function conditionsNormalizeOptionValue(descriptors, type, operator, optionValue) { - if (!descriptors.hasOwnProperty(type)) { + if (!hasOwn(descriptors, type)) { throw new Error('Invalid type'); } const conditionDescriptor = descriptors[type]; - if (!conditionDescriptor.operators.hasOwnProperty(operator)) { + if (!hasOwn(conditionDescriptor.operators, operator)) { throw new Error('Invalid operator'); } @@ -48,28 +48,28 @@ function conditionsNormalizeOptionValue(descriptors, type, operator, optionValue let transformedValue = conditionsValidateOptionValue(conditionDescriptor, optionValue); transformedValue = conditionsValidateOptionValue(operatorDescriptor, transformedValue); - if (operatorDescriptor.hasOwnProperty('transformReverse')) { + if (hasOwn(operatorDescriptor, 'transformReverse')) { transformedValue = operatorDescriptor.transformReverse(transformedValue); } return transformedValue; } function conditionsTestValueThrowing(descriptors, type, operator, optionValue, value) { - if (!descriptors.hasOwnProperty(type)) { + if (!hasOwn(descriptors, type)) { throw new Error('Invalid type'); } const conditionDescriptor = descriptors[type]; - if (!conditionDescriptor.operators.hasOwnProperty(operator)) { + if (!hasOwn(conditionDescriptor.operators, operator)) { throw new Error('Invalid operator'); } const operatorDescriptor = conditionDescriptor.operators[operator]; - if (operatorDescriptor.hasOwnProperty('transform')) { - if (operatorDescriptor.hasOwnProperty('transformCache')) { + if (hasOwn(operatorDescriptor, 'transform')) { + if (hasOwn(operatorDescriptor, 'transformCache')) { const key = `${optionValue}`; const transformCache = operatorDescriptor.transformCache; - if (transformCache.hasOwnProperty(key)) { + if (hasOwn(transformCache, key)) { optionValue = transformCache[key]; } else { optionValue = operatorDescriptor.transform(optionValue); @@ -93,23 +93,23 @@ function conditionsTestValue(descriptors, type, operator, optionValue, value) { function conditionsClearCaches(descriptors) { for (const type in descriptors) { - if (!descriptors.hasOwnProperty(type)) { + if (!hasOwn(descriptors, type)) { continue; } const conditionDescriptor = descriptors[type]; - if (conditionDescriptor.hasOwnProperty('transformCache')) { + if (hasOwn(conditionDescriptor, 'transformCache')) { conditionDescriptor.transformCache = {}; } const operatorDescriptors = conditionDescriptor.operators; for (const operator in operatorDescriptors) { - if (!operatorDescriptors.hasOwnProperty(operator)) { + if (!hasOwn(operatorDescriptors, operator)) { continue; } const operatorDescriptor = operatorDescriptors[operator]; - if (operatorDescriptor.hasOwnProperty('transformCache')) { + if (hasOwn(operatorDescriptor, 'transformCache')) { operatorDescriptor.transformCache = {}; } } diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js index 3fb27f0d..0b21f662 100644 --- a/ext/bg/js/context.js +++ b/ext/bg/js/context.js @@ -26,26 +26,28 @@ function showExtensionInfo() { } function setupButtonEvents(selector, command, url) { - const node = $(selector); - node.on('click', (e) => { - if (e.button !== 0) { return; } - apiCommandExec(command, {newTab: e.ctrlKey}); - e.preventDefault(); - }) - .on('auxclick', (e) => { - if (e.button !== 1) { return; } - apiCommandExec(command, {newTab: true}); - e.preventDefault(); - }); + const nodes = document.querySelectorAll(selector); + for (const node of nodes) { + node.addEventListener('click', (e) => { + if (e.button !== 0) { return; } + apiCommandExec(command, {newTab: e.ctrlKey}); + e.preventDefault(); + }, false); + node.addEventListener('auxclick', (e) => { + if (e.button !== 1) { return; } + apiCommandExec(command, {newTab: true}); + e.preventDefault(); + }, false); - if (typeof url === 'string') { - node.attr('href', url); - node.attr('target', '_blank'); - node.attr('rel', 'noopener'); + if (typeof url === 'string') { + node.href = url; + node.target = '_blank'; + node.rel = 'noopener'; + } } } -$(document).ready(utilAsync(() => { +window.addEventListener('DOMContentLoaded', () => { showExtensionInfo(); apiGetEnvironmentInfo().then(({browser}) => { @@ -63,14 +65,19 @@ $(document).ready(utilAsync(() => { depth: 0, url: window.location.href }; - apiOptionsGet(optionsContext).then(options => { - const toggle = $('#enable-search'); - toggle.prop('checked', options.general.enable).change(); - toggle.bootstrapToggle(); - toggle.change(() => apiCommandExec('toggle')); + apiOptionsGet(optionsContext).then((options) => { + const toggle = document.querySelector('#enable-search'); + toggle.checked = options.general.enable; + toggle.addEventListener('change', () => apiCommandExec('toggle'), false); + + const toggle2 = document.querySelector('#enable-search2'); + toggle2.checked = options.general.enable; + toggle2.addEventListener('change', () => apiCommandExec('toggle'), false); - const toggle2 = $('#enable-search2'); - toggle2.prop('checked', options.general.enable).change(); - toggle2.change(() => apiCommandExec('toggle')); + setTimeout(() => { + for (const n of document.querySelectorAll('.toggle-group')) { + n.classList.add('toggle-group-animated'); + } + }, 10); }); -})); +}); diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 9f477b24..a20d5f15 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -23,63 +23,137 @@ class Database { } async prepare() { - if (this.db) { + if (this.db !== null) { throw new Error('Database already initialized'); } - this.db = new Dexie('dict'); - this.db.version(2).stores({ - terms: '++id,dictionary,expression,reading', - kanji: '++,dictionary,character', - tagMeta: '++,dictionary', - dictionaries: '++,title,version' - }); - this.db.version(3).stores({ - termMeta: '++,dictionary,expression', - kanjiMeta: '++,dictionary,character', - tagMeta: '++,dictionary,name' - }); - this.db.version(4).stores({ - terms: '++id,dictionary,expression,reading,sequence' - }); - - await this.db.open(); + try { + this.db = await Database.open('dict', 4, (db, transaction, oldVersion) => { + Database.upgrade(db, transaction, oldVersion, [ + { + version: 2, + stores: { + terms: { + primaryKey: {keyPath: 'id', autoIncrement: true}, + indices: ['dictionary', 'expression', 'reading'] + }, + kanji: { + primaryKey: {autoIncrement: true}, + indices: ['dictionary', 'character'] + }, + tagMeta: { + primaryKey: {autoIncrement: true}, + indices: ['dictionary'] + }, + dictionaries: { + primaryKey: {autoIncrement: true}, + indices: ['title', 'version'] + } + } + }, + { + version: 3, + stores: { + termMeta: { + primaryKey: {autoIncrement: true}, + indices: ['dictionary', 'expression'] + }, + kanjiMeta: { + primaryKey: {autoIncrement: true}, + indices: ['dictionary', 'character'] + }, + tagMeta: { + primaryKey: {autoIncrement: true}, + indices: ['dictionary', 'name'] + } + } + }, + { + version: 4, + stores: { + terms: { + primaryKey: {keyPath: 'id', autoIncrement: true}, + indices: ['dictionary', 'expression', 'reading', 'sequence'] + } + } + } + ]); + }); + return true; + } catch (e) { + console.error(e); + return false; + } } async purge() { this.validate(); this.db.close(); - await this.db.delete(); + await Database.deleteDatabase(this.db.name); this.db = null; await this.prepare(); } - async findTermsBulk(termList, titles) { + async deleteDictionary(dictionaryName, onProgress, progressSettings) { + this.validate(); + + const targets = [ + ['dictionaries', 'title'], + ['kanji', 'dictionary'], + ['kanjiMeta', 'dictionary'], + ['terms', 'dictionary'], + ['termMeta', 'dictionary'], + ['tagMeta', 'dictionary'] + ]; + const promises = []; + const progressData = { + count: 0, + processed: 0, + storeCount: targets.length, + storesProcesed: 0 + }; + let progressRate = (typeof progressSettings === 'object' && progressSettings !== null ? progressSettings.rate : 0); + if (typeof progressRate !== 'number' || progressRate <= 0) { + progressRate = 1000; + } + + for (const [objectStoreName, index] of targets) { + const dbTransaction = this.db.transaction([objectStoreName], 'readwrite'); + const dbObjectStore = dbTransaction.objectStore(objectStoreName); + const dbIndex = dbObjectStore.index(index); + const only = IDBKeyRange.only(dictionaryName); + promises.push(Database.deleteValues(dbObjectStore, dbIndex, only, onProgress, progressData, progressRate)); + } + + await Promise.all(promises); + } + + async findTermsBulk(termList, titles, wildcard) { this.validate(); const promises = []; const visited = {}; const results = []; const processRow = (row, index) => { - if (titles.includes(row.dictionary) && !visited.hasOwnProperty(row.id)) { + if (titles.includes(row.dictionary) && !hasOwn(visited, row.id)) { visited[row.id] = true; results.push(Database.createTerm(row, index)); } }; - const db = this.db.backendDB(); - const dbTransaction = db.transaction(['terms'], 'readonly'); + const dbTransaction = this.db.transaction(['terms'], 'readonly'); const dbTerms = dbTransaction.objectStore('terms'); const dbIndex1 = dbTerms.index('expression'); const dbIndex2 = dbTerms.index('reading'); for (let i = 0; i < termList.length; ++i) { - const only = IDBKeyRange.only(termList[i]); + const term = termList[i]; + const query = wildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term); promises.push( - Database.getAll(dbIndex1, only, i, processRow), - Database.getAll(dbIndex2, only, i, processRow) + Database.getAll(dbIndex1, query, i, processRow), + Database.getAll(dbIndex2, query, i, processRow) ); } @@ -99,8 +173,7 @@ class Database { } }; - const db = this.db.backendDB(); - const dbTransaction = db.transaction(['terms'], 'readonly'); + const dbTransaction = this.db.transaction(['terms'], 'readonly'); const dbTerms = dbTransaction.objectStore('terms'); const dbIndex = dbTerms.index('expression'); @@ -125,8 +198,7 @@ class Database { } }; - const db = this.db.backendDB(); - const dbTransaction = db.transaction(['terms'], 'readonly'); + const dbTransaction = this.db.transaction(['terms'], 'readonly'); const dbTerms = dbTransaction.objectStore('terms'); const dbIndex = dbTerms.index('sequence'); @@ -163,8 +235,7 @@ class Database { } }; - const db = this.db.backendDB(); - const dbTransaction = db.transaction([tableName], 'readonly'); + const dbTransaction = this.db.transaction([tableName], 'readonly'); const dbTerms = dbTransaction.objectStore(tableName); const dbIndex = dbTerms.index(indexName); @@ -182,12 +253,11 @@ class Database { this.validate(); let result = null; - const db = this.db.backendDB(); - const dbTransaction = db.transaction(['tagMeta'], 'readonly'); + const dbTransaction = this.db.transaction(['tagMeta'], 'readonly'); const dbTerms = dbTransaction.objectStore('tagMeta'); const dbIndex = dbTerms.index('name'); const only = IDBKeyRange.only(name); - await Database.getAll(dbIndex, only, null, row => { + await Database.getAll(dbIndex, only, null, (row) => { if (title === row.dictionary) { result = row; } @@ -196,24 +266,76 @@ class Database { return result; } - async summarize() { + async getDictionaryInfo() { + this.validate(); + + const results = []; + const dbTransaction = this.db.transaction(['dictionaries'], 'readonly'); + const dbDictionaries = dbTransaction.objectStore('dictionaries'); + + await Database.getAll(dbDictionaries, null, null, (info) => results.push(info)); + + return results; + } + + async getDictionaryCounts(dictionaryNames, getTotal) { this.validate(); - return this.db.dictionaries.toArray(); + const objectStoreNames = [ + 'kanji', + 'kanjiMeta', + 'terms', + 'termMeta', + 'tagMeta' + ]; + const dbCountTransaction = this.db.transaction(objectStoreNames, 'readonly'); + + const targets = []; + for (const objectStoreName of objectStoreNames) { + targets.push([ + objectStoreName, + dbCountTransaction.objectStore(objectStoreName).index('dictionary') + ]); + } + + // Query is required for Edge, otherwise index.count throws an exception. + const query1 = IDBKeyRange.lowerBound('', false); + const totalPromise = getTotal ? Database.getCounts(targets, query1) : null; + + const counts = []; + const countPromises = []; + for (let i = 0; i < dictionaryNames.length; ++i) { + counts.push(null); + const index = i; + const query2 = IDBKeyRange.only(dictionaryNames[i]); + const countPromise = Database.getCounts(targets, query2).then((v) => counts[index] = v); + countPromises.push(countPromise); + } + await Promise.all(countPromises); + + const result = {counts}; + if (totalPromise !== null) { + result.total = await totalPromise; + } + return result; } async importDictionary(archive, progressCallback, exceptions) { this.validate(); const maxTransactionLength = 1000; - const bulkAdd = async (table, items, total, current) => { - if (items.length < maxTransactionLength) { + const bulkAdd = async (objectStoreName, items, total, current) => { + const db = this.db; + for (let i = 0; i < items.length; i += maxTransactionLength) { if (progressCallback) { - progressCallback(total, current); + progressCallback(total, current + i / items.length); } try { - await table.bulkAdd(items); + const count = Math.min(maxTransactionLength, items.length - i); + const transaction = db.transaction([objectStoreName], 'readwrite'); + const objectStore = transaction.objectStore(objectStoreName); + await Database.bulkAdd(objectStore, items, i, count); } catch (e) { if (exceptions) { exceptions.push(e); @@ -221,37 +343,27 @@ class Database { throw e; } } - } else { - for (let i = 0; i < items.length; i += maxTransactionLength) { - if (progressCallback) { - progressCallback(total, current + i / items.length); - } - - let count = Math.min(maxTransactionLength, items.length - i); - try { - await table.bulkAdd(items.slice(i, i + count)); - } catch (e) { - if (exceptions) { - exceptions.push(e); - } else { - throw e; - } - } - } } }; - const indexDataLoaded = async summary => { + const indexDataLoaded = async (summary) => { if (summary.version > 3) { throw new Error('Unsupported dictionary version'); } - const count = await this.db.dictionaries.where('title').equals(summary.title).count(); + const db = this.db; + const dbCountTransaction = db.transaction(['dictionaries'], 'readonly'); + const dbIndex = dbCountTransaction.objectStore('dictionaries').index('title'); + const only = IDBKeyRange.only(summary.title); + const count = await Database.getCount(dbIndex, only); + if (count > 0) { throw new Error('Dictionary is already imported'); } - await this.db.dictionaries.add(summary); + const transaction = db.transaction(['dictionaries'], 'readwrite'); + const objectStore = transaction.objectStore('dictionaries'); + await Database.bulkAdd(objectStore, [summary], 0, 1); }; const termDataLoaded = async (summary, entries, total, current) => { @@ -284,7 +396,7 @@ class Database { } } - await bulkAdd(this.db.terms, rows, total, current); + await bulkAdd('terms', rows, total, current); }; const termMetaDataLoaded = async (summary, entries, total, current) => { @@ -298,7 +410,7 @@ class Database { }); } - await bulkAdd(this.db.termMeta, rows, total, current); + await bulkAdd('termMeta', rows, total, current); }; const kanjiDataLoaded = async (summary, entries, total, current) => { @@ -328,7 +440,7 @@ class Database { } } - await bulkAdd(this.db.kanji, rows, total, current); + await bulkAdd('kanji', rows, total, current); }; const kanjiMetaDataLoaded = async (summary, entries, total, current) => { @@ -342,7 +454,7 @@ class Database { }); } - await bulkAdd(this.db.kanjiMeta, rows, total, current); + await bulkAdd('kanjiMeta', rows, total, current); }; const tagDataLoaded = async (summary, entries, total, current) => { @@ -360,7 +472,7 @@ class Database { rows.push(row); } - await bulkAdd(this.db.tagMeta, rows, total, current); + await bulkAdd('tagMeta', rows, total, current); }; return await Database.importDictionaryZip( @@ -410,13 +522,13 @@ class Database { await indexDataLoaded(summary); - const buildTermBankName = index => `term_bank_${index + 1}.json`; - const buildTermMetaBankName = index => `term_meta_bank_${index + 1}.json`; - const buildKanjiBankName = index => `kanji_bank_${index + 1}.json`; - const buildKanjiMetaBankName = index => `kanji_meta_bank_${index + 1}.json`; - const buildTagBankName = index => `tag_bank_${index + 1}.json`; + const buildTermBankName = (index) => `term_bank_${index + 1}.json`; + const buildTermMetaBankName = (index) => `term_meta_bank_${index + 1}.json`; + const buildKanjiBankName = (index) => `kanji_bank_${index + 1}.json`; + const buildKanjiMetaBankName = (index) => `kanji_meta_bank_${index + 1}.json`; + const buildTagBankName = (index) => `tag_bank_${index + 1}.json`; - const countBanks = namer => { + const countBanks = (namer) => { let count = 0; while (zip.files[namer(count)]) { ++count; @@ -539,4 +651,176 @@ class Database { }; }); } + + static getCounts(targets, query) { + const countPromises = []; + const counts = {}; + for (const [objectStoreName, index] of targets) { + const n = objectStoreName; + const countPromise = Database.getCount(index, query).then((count) => counts[n] = count); + countPromises.push(countPromise); + } + return Promise.all(countPromises).then(() => counts); + } + + static getCount(dbIndex, query) { + return new Promise((resolve, reject) => { + const request = dbIndex.count(query); + request.onerror = (e) => reject(e); + request.onsuccess = (e) => resolve(e.target.result); + }); + } + + static getAllKeys(dbIndex, query) { + const fn = typeof dbIndex.getAllKeys === 'function' ? Database.getAllKeysFast : Database.getAllKeysUsingCursor; + return fn(dbIndex, query); + } + + static getAllKeysFast(dbIndex, query) { + return new Promise((resolve, reject) => { + const request = dbIndex.getAllKeys(query); + request.onerror = (e) => reject(e); + request.onsuccess = (e) => resolve(e.target.result); + }); + } + + static getAllKeysUsingCursor(dbIndex, query) { + return new Promise((resolve, reject) => { + const primaryKeys = []; + const request = dbIndex.openKeyCursor(query, 'next'); + request.onerror = (e) => reject(e); + request.onsuccess = (e) => { + const cursor = e.target.result; + if (cursor) { + primaryKeys.push(cursor.primaryKey); + cursor.continue(); + } else { + resolve(primaryKeys); + } + }; + }); + } + + static async deleteValues(dbObjectStore, dbIndex, query, onProgress, progressData, progressRate) { + const hasProgress = (typeof onProgress === 'function'); + const count = await Database.getCount(dbIndex, query); + ++progressData.storesProcesed; + progressData.count += count; + if (hasProgress) { + onProgress(progressData); + } + + const onValueDeleted = ( + hasProgress ? + () => { + const p = ++progressData.processed; + if ((p % progressRate) === 0 || p === progressData.count) { + onProgress(progressData); + } + } : + () => {} + ); + + const promises = []; + const primaryKeys = await Database.getAllKeys(dbIndex, query); + for (const key of primaryKeys) { + const promise = Database.deleteValue(dbObjectStore, key).then(onValueDeleted); + promises.push(promise); + } + + await Promise.all(promises); + } + + static deleteValue(dbObjectStore, key) { + return new Promise((resolve, reject) => { + const request = dbObjectStore.delete(key); + request.onerror = (e) => reject(e); + request.onsuccess = () => resolve(); + }); + } + + static bulkAdd(objectStore, items, start, count) { + return new Promise((resolve, reject) => { + if (start + count > items.length) { + count = items.length - start; + } + + if (count <= 0) { + resolve(); + return; + } + + const end = start + count; + let completedCount = 0; + const onError = (e) => reject(e); + const onSuccess = () => { + if (++completedCount >= count) { + resolve(); + } + }; + + for (let i = start; i < end; ++i) { + const request = objectStore.add(items[i]); + request.onerror = onError; + request.onsuccess = onSuccess; + } + }); + } + + static open(name, version, onUpgradeNeeded) { + return new Promise((resolve, reject) => { + const request = window.indexedDB.open(name, version * 10); + + request.onupgradeneeded = (event) => { + try { + request.transaction.onerror = (e) => reject(e); + onUpgradeNeeded(request.result, request.transaction, event.oldVersion / 10, event.newVersion / 10); + } catch (e) { + reject(e); + } + }; + + request.onerror = (e) => reject(e); + request.onsuccess = () => resolve(request.result); + }); + } + + static upgrade(db, transaction, oldVersion, upgrades) { + for (const {version, stores} of upgrades) { + if (oldVersion >= version) { continue; } + + const objectStoreNames = Object.keys(stores); + for (const objectStoreName of objectStoreNames) { + const {primaryKey, indices} = stores[objectStoreName]; + + const objectStoreNames = transaction.objectStoreNames || db.objectStoreNames; + const objectStore = ( + Database.listContains(objectStoreNames, objectStoreName) ? + transaction.objectStore(objectStoreName) : + db.createObjectStore(objectStoreName, primaryKey) + ); + + for (const indexName of indices) { + if (Database.listContains(objectStore.indexNames, indexName)) { continue; } + + objectStore.createIndex(indexName, indexName, {}); + } + } + } + } + + static deleteDatabase(dbName) { + return new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(dbName); + request.onerror = (e) => reject(e); + request.onsuccess = () => resolve(); + }); + } + + static listContains(list, value) { + for (let i = 0, ii = list.length; i < ii; ++i) { + if (list[i] === value) { return true; } + } + return false; + } } diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js index ce4b2961..51f4723c 100644 --- a/ext/bg/js/deinflector.js +++ b/ext/bg/js/deinflector.js @@ -44,7 +44,7 @@ class Deinflector { results.push({ source, - term: term.slice(0, -kanaIn.length) + kanaOut, + term: term.substring(0, term.length - kanaIn.length) + kanaOut, rules: rulesOut, definitions: [], reasons: [reason, ...reasons] @@ -88,5 +88,5 @@ Deinflector.ruleTypes = { 'vs': 0b0000100, // Verb suru 'vk': 0b0001000, // Verb kuru 'adj-i': 0b0010000, // Adjective i - 'iru': 0b0100000, // Intermediate -iru endings for progressive or perfect tense + 'iru': 0b0100000 // Intermediate -iru endings for progressive or perfect tense }; diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index 191058c1..0b35e32e 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -55,39 +55,23 @@ function dictRowsSort(rows, options) { function dictTermsSort(definitions, dictionaries=null) { return definitions.sort((v1, v2) => { + let i; if (dictionaries !== null) { - const p1 = (dictionaries[v1.dictionary] || {}).priority || 0; - const p2 = (dictionaries[v2.dictionary] || {}).priority || 0; - if (p1 > p2) { - return -1; - } else if (p1 < p2) { - return 1; - } + i = ( + ((dictionaries[v2.dictionary] || {}).priority || 0) - + ((dictionaries[v1.dictionary] || {}).priority || 0) + ); + if (i !== 0) { return i; } } - const sl1 = v1.source.length; - const sl2 = v2.source.length; - if (sl1 > sl2) { - return -1; - } else if (sl1 < sl2) { - return 1; - } + i = v2.source.length - v1.source.length; + if (i !== 0) { return i; } - const rl1 = v1.reasons.length; - const rl2 = v2.reasons.length; - if (rl1 < rl2) { - return -1; - } else if (rl1 > rl2) { - return 1; - } + i = v2.reasons.length - v1.reasons.length; + if (i !== 0) { return i; } - const s1 = v1.score; - const s2 = v2.score; - if (s1 > s2) { - return -1; - } else if (s1 < s2) { - return 1; - } + i = v2.score - v1.score; + if (i !== 0) { return i; } return v2.expression.toString().localeCompare(v1.expression.toString()); }); @@ -97,7 +81,7 @@ function dictTermsUndupe(definitions) { const definitionGroups = {}; for (const definition of definitions) { const definitionExisting = definitionGroups[definition.id]; - if (!definitionGroups.hasOwnProperty(definition.id) || definition.expression.length > definitionExisting.expression.length) { + if (!hasOwn(definitionGroups, definition.id) || definition.expression.length > definitionExisting.expression.length) { definitionGroups[definition.id] = definition; } } @@ -115,8 +99,8 @@ function dictTermsCompressTags(definitions) { let lastPartOfSpeech = ''; for (const definition of definitions) { - const dictionary = JSON.stringify(definition.definitionTags.filter(tag => tag.category === 'dictionary').map(tag => tag.name).sort()); - const partOfSpeech = JSON.stringify(definition.definitionTags.filter(tag => tag.category === 'partOfSpeech').map(tag => tag.name).sort()); + const dictionary = JSON.stringify(definition.definitionTags.filter((tag) => tag.category === 'dictionary').map((tag) => tag.name).sort()); + const partOfSpeech = JSON.stringify(definition.definitionTags.filter((tag) => tag.category === 'partOfSpeech').map((tag) => tag.name).sort()); const filterOutCategories = []; @@ -133,7 +117,7 @@ function dictTermsCompressTags(definitions) { lastPartOfSpeech = partOfSpeech; } - definition.definitionTags = definition.definitionTags.filter(tag => !filterOutCategories.includes(tag.category)); + definition.definitionTags = definition.definitionTags.filter((tag) => !filterOutCategories.includes(tag.category)); } } @@ -147,7 +131,7 @@ function dictTermsGroup(definitions, dictionaries) { } const keyString = key.toString(); - if (groups.hasOwnProperty(keyString)) { + if (hasOwn(groups, keyString)) { groups[keyString].push(definition); } else { groups[keyString] = [definition]; @@ -247,7 +231,7 @@ function dictTermsMergeByGloss(result, definitions, appendTo, mergedIndices) { result.reading.add(definition.reading); for (const tag of definition.definitionTags) { - if (!definitionsByGloss[gloss].definitionTags.find(existingTag => existingTag.name === tag.name)) { + if (!definitionsByGloss[gloss].definitionTags.find((existingTag) => existingTag.name === tag.name)) { definitionsByGloss[gloss].definitionTags.push(tag); } } @@ -262,7 +246,7 @@ function dictTermsMergeByGloss(result, definitions, appendTo, mergedIndices) { } for (const tag of definition.termTags) { - if (!result.expressions.get(definition.expression).get(definition.reading).find(existingTag => existingTag.name === tag.name)) { + if (!result.expressions.get(definition.expression).get(definition.reading).find((existingTag) => existingTag.name === tag.name)) { result.expressions.get(definition.expression).get(definition.reading).push(tag); } } @@ -326,46 +310,52 @@ function dictFieldSplit(field) { return field.length === 0 ? [] : field.split(' '); } -async function dictFieldFormat(field, definition, mode, options) { - const markers = [ - 'audio', - 'character', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'dictionary', - 'expression', - 'furigana', - 'furigana-plain', - 'glossary', - 'glossary-brief', - 'kunyomi', - 'onyomi', - 'reading', - 'screenshot', - 'sentence', - 'tags', - 'url' - ]; - - for (const marker of markers) { - const data = { - marker, - definition, - group: options.general.resultOutputMode === 'group', - merge: options.general.resultOutputMode === 'merge', - modeTermKanji: mode === 'term-kanji', - modeTermKana: mode === 'term-kana', - modeKanji: mode === 'kanji', - compactGlossaries: options.general.compactGlossaries - }; - - const html = await apiTemplateRender(options.anki.fieldTemplates, data, true); - field = field.replace(`{${marker}}`, html); - } - - return field; +async function dictFieldFormat(field, definition, mode, options, exceptions) { + const data = { + marker: null, + definition, + group: options.general.resultOutputMode === 'group', + merge: options.general.resultOutputMode === 'merge', + modeTermKanji: mode === 'term-kanji', + modeTermKana: mode === 'term-kana', + modeKanji: mode === 'kanji', + compactGlossaries: options.general.compactGlossaries + }; + const markers = dictFieldFormat.markers; + const pattern = /\{([\w-]+)\}/g; + return await stringReplaceAsync(field, pattern, async (g0, marker) => { + if (!markers.has(marker)) { + return g0; + } + data.marker = marker; + try { + return await apiTemplateRender(options.anki.fieldTemplates, data, true); + } catch (e) { + if (exceptions) { exceptions.push(e); } + return `{${marker}-render-error}`; + } + }); } +dictFieldFormat.markers = new Set([ + 'audio', + 'character', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'dictionary', + 'expression', + 'furigana', + 'furigana-plain', + 'glossary', + 'glossary-brief', + 'kunyomi', + 'onyomi', + 'reading', + 'screenshot', + 'sentence', + 'tags', + 'url' +]); async function dictNoteFormat(definition, mode, options) { const note = {fields: {}, tags: options.anki.tags}; diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js index fba437da..8f43cf9a 100644 --- a/ext/bg/js/handlebars.js +++ b/ext/bg/js/handlebars.js @@ -79,6 +79,47 @@ function handlebarsSanitizeCssClass(options) { return options.fn(this).replace(/[^_a-z0-9\u00a0-\uffff]/ig, '_'); } +function handlebarsRegexReplace(...args) { + // Usage: + // {{#regexReplace regex string [flags]}}content{{/regexReplace}} + // regex: regular expression string + // string: string to replace + // flags: optional flags for regular expression + // e.g. "i" for case-insensitive, "g" for replace all + let value = args[args.length - 1].fn(this); + if (args.length >= 3) { + try { + const flags = args.length > 3 ? args[2] : 'g'; + const regex = new RegExp(args[0], flags); + value = value.replace(regex, args[1]); + } catch (e) { + return `${e}`; + } + } + return value; +} + +function handlebarsRegexMatch(...args) { + // Usage: + // {{#regexMatch regex [flags]}}content{{/regexMatch}} + // regex: regular expression string + // flags: optional flags for regular expression + // e.g. "i" for case-insensitive, "g" for match all + let value = args[args.length - 1].fn(this); + if (args.length >= 2) { + try { + const flags = args.length > 2 ? args[1] : ''; + const regex = new RegExp(args[0], flags); + const parts = []; + value.replace(regex, (g0) => parts.push(g0)); + value = parts.join(''); + } catch (e) { + return `${e}`; + } + } + return value; +} + function handlebarsRegisterHelpers() { if (Handlebars.partials !== Handlebars.templates) { Handlebars.partials = Handlebars.templates; @@ -88,6 +129,8 @@ function handlebarsRegisterHelpers() { Handlebars.registerHelper('kanjiLinks', handlebarsKanjiLinks); Handlebars.registerHelper('multiLine', handlebarsMultiLine); Handlebars.registerHelper('sanitizeCssClass', handlebarsSanitizeCssClass); + Handlebars.registerHelper('regexReplace', handlebarsRegexReplace); + Handlebars.registerHelper('regexMatch', handlebarsRegexMatch); } } diff --git a/ext/bg/js/mecab.js b/ext/bg/js/mecab.js new file mode 100644 index 00000000..62111f73 --- /dev/null +++ b/ext/bg/js/mecab.js @@ -0,0 +1,92 @@ +/* + * 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/>. + */ + + +class Mecab { + constructor() { + this.port = null; + this.listeners = {}; + this.sequence = 0; + } + + onError(error) { + logError(error, false); + } + + async checkVersion() { + try { + const {version} = await this.invoke('get_version', {}); + if (version !== Mecab.version) { + this.stopListener(); + throw new Error(`Unsupported MeCab native messenger version ${version}. Yomichan supports version ${Mecab.version}.`); + } + } catch (error) { + this.onError(error); + } + } + + async parseText(text) { + return await this.invoke('parse_text', {text}); + } + + startListener() { + if (this.port !== null) { return; } + this.port = chrome.runtime.connectNative('yomichan_mecab'); + this.port.onMessage.addListener(this.onNativeMessage.bind(this)); + this.checkVersion(); + } + + stopListener() { + if (this.port === null) { return; } + this.port.disconnect(); + this.port = null; + this.listeners = {}; + this.sequence = 0; + } + + onNativeMessage({sequence, data}) { + if (hasOwn(this.listeners, sequence)) { + const {callback, timer} = this.listeners[sequence]; + clearTimeout(timer); + callback(data); + delete this.listeners[sequence]; + } + } + + invoke(action, params) { + if (this.port === null) { + return Promise.resolve({}); + } + return new Promise((resolve, reject) => { + const sequence = this.sequence++; + + this.listeners[sequence] = { + callback: resolve, + timer: setTimeout(() => { + delete this.listeners[sequence]; + reject(new Error(`Mecab invoke timed out in ${Mecab.timeout} ms`)); + }, Mecab.timeout) + }; + + this.port.postMessage({action, params, sequence}); + }); + } +} + +Mecab.timeout = 5000; +Mecab.version = 1; diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index be1ccfbb..e53a8a13 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -311,6 +311,13 @@ function profileOptionsCreateDefaults() { dictionaries: {}, + parsing: { + enableScanningParser: true, + enableMecabParser: false, + selectedParser: null, + readingMode: 'hiragana' + }, + anki: { enable: false, server: 'http://127.0.0.1:8765', @@ -329,7 +336,7 @@ function profileOptionsSetDefaults(options) { const combine = (target, source) => { for (const key in source) { - if (!target.hasOwnProperty(key)) { + if (!hasOwn(target, key)) { target[key] = source[key]; } } @@ -382,7 +389,7 @@ function optionsUpdateVersion(options, defaultProfileOptions) { // Remove invalid const profiles = options.profiles; for (let i = profiles.length - 1; i >= 0; --i) { - if (!utilIsObject(profiles[i])) { + if (!isObject(profiles[i])) { profiles.splice(i, 1); } } @@ -422,7 +429,7 @@ function optionsUpdateVersion(options, defaultProfileOptions) { function optionsLoad() { return new Promise((resolve, reject) => { - chrome.storage.local.get(['options'], store => { + chrome.storage.local.get(['options'], (store) => { const error = chrome.runtime.lastError; if (error) { reject(new Error(error)); @@ -430,17 +437,17 @@ function optionsLoad() { resolve(store.options); } }); - }).then(optionsStr => { + }).then((optionsStr) => { if (typeof optionsStr === 'string') { const options = JSON.parse(optionsStr); - if (utilIsObject(options)) { + if (isObject(options)) { return options; } } return {}; }).catch(() => { return {}; - }).then(options => { + }).then((options) => { return ( Array.isArray(options.profiles) ? optionsUpdateVersion(options, {}) : diff --git a/ext/bg/js/page-exit-prevention.js b/ext/bg/js/page-exit-prevention.js new file mode 100644 index 00000000..aee4e3c2 --- /dev/null +++ b/ext/bg/js/page-exit-prevention.js @@ -0,0 +1,60 @@ +/* + * 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/>. + */ + + +class PageExitPrevention { + constructor() { + } + + start() { + PageExitPrevention._addInstance(this); + } + + end() { + PageExitPrevention._removeInstance(this); + } + + static _addInstance(instance) { + const size = PageExitPrevention._instances.size; + PageExitPrevention._instances.set(instance, true); + if (size === 0) { + window.addEventListener('beforeunload', PageExitPrevention._onBeforeUnload); + } + } + + static _removeInstance(instance) { + if ( + PageExitPrevention._instances.delete(instance) && + PageExitPrevention._instances.size === 0 + ) { + window.removeEventListener('beforeunload', PageExitPrevention._onBeforeUnload); + } + } + + static _onBeforeUnload(e) { + if (PageExitPrevention._instances.size === 0) { + return; + } + + e.preventDefault(); + e.returnValue = ''; + return ''; + } +} + +PageExitPrevention._instances = new Map(); diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js index 5daa904e..ebc6680a 100644 --- a/ext/bg/js/profile-conditions.js +++ b/ext/bg/js/profile-conditions.js @@ -17,6 +17,26 @@ */ +function _profileConditionTestDomain(urlDomain, domain) { + return ( + urlDomain.endsWith(domain) && + ( + domain.length === urlDomain.length || + urlDomain[urlDomain.length - domain.length - 1] === '.' + ) + ); +} + +function _profileConditionTestDomainList(url, domainList) { + const urlDomain = new URL(url).hostname.toLowerCase(); + for (const domain of domainList) { + if (_profileConditionTestDomain(urlDomain, domain)) { + return true; + } + } + return false; +} + const profileConditionsDescriptor = { popupLevel: { name: 'Popup Level', @@ -66,10 +86,10 @@ const profileConditionsDescriptor = { placeholder: 'Comma separated list of domains', defaultValue: 'example.com', transformCache: {}, - transform: (optionValue) => optionValue.split(/[,;\s]+/).map(v => v.trim().toLowerCase()).filter(v => v.length > 0), + transform: (optionValue) => optionValue.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0), transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '), validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0), - test: ({url}, transformedOptionValue) => (transformedOptionValue.indexOf(new URL(url).hostname.toLowerCase()) >= 0) + test: ({url}, transformedOptionValue) => _profileConditionTestDomainList(url, transformedOptionValue) }, matchRegExp: { name: 'Matches RegExp', diff --git a/ext/bg/js/request.js b/ext/bg/js/request.js index 3afc1506..7d73d49b 100644 --- a/ext/bg/js/request.js +++ b/ext/bg/js/request.js @@ -29,7 +29,7 @@ function requestJson(url, action, params) { } else { xhr.send(); } - }).then(responseText => { + }).then((responseText) => { try { return JSON.parse(responseText); } diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js index b21dac17..6ba8467e 100644 --- a/ext/bg/js/search-frontend.js +++ b/ext/bg/js/search-frontend.js @@ -25,7 +25,12 @@ async function searchFrontendSetup() { const options = await apiOptionsGet(optionsContext); if (!options.scanning.enableOnSearchPage) { return; } - window.frontendInitializationData = {depth: 1, proxy: false}; + const ignoreNodes = ['.scan-disable', '.scan-disable *']; + if (!options.scanning.enableOnPopupExpressions) { + ignoreNodes.push('.expression-scan-toggle', '.expression-scan-toggle *'); + } + + window.frontendInitializationData = {depth: 1, ignoreNodes, proxy: false}; const scriptSrcs = [ '/fg/js/frontend-api-receiver.js', diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js new file mode 100644 index 00000000..8dc2e30a --- /dev/null +++ b/ext/bg/js/search-query-parser.js @@ -0,0 +1,228 @@ +/* + * 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/>. + */ + + +class QueryParser { + constructor(search) { + this.search = search; + this.pendingLookup = false; + this.clickScanPrevent = false; + + this.parseResults = []; + this.selectedParser = null; + + this.queryParser = document.querySelector('#query-parser'); + this.queryParserSelect = document.querySelector('#query-parser-select'); + + this.queryParser.addEventListener('mousedown', (e) => this.onMouseDown(e)); + this.queryParser.addEventListener('mouseup', (e) => this.onMouseUp(e)); + } + + onError(error) { + logError(error, false); + } + + onMouseDown(e) { + if (DOM.isMouseButtonPressed(e, 'primary')) { + this.clickScanPrevent = false; + } + } + + onMouseUp(e) { + if ( + this.search.options.scanning.enablePopupSearch && + !this.clickScanPrevent && + DOM.isMouseButtonPressed(e, 'primary') + ) { + const selectText = this.search.options.scanning.selectText; + this.onTermLookup(e, {disableScroll: true, selectText}); + } + } + + onMouseMove(e) { + if (this.pendingLookup || DOM.isMouseButtonDown(e, 'primary')) { + return; + } + + const scanningOptions = this.search.options.scanning; + const scanningModifier = scanningOptions.modifier; + if (!( + Frontend.isScanningModifierPressed(scanningModifier, e) || + (scanningOptions.middleMouse && DOM.isMouseButtonDown(e, 'auxiliary')) + )) { + return; + } + + const selectText = this.search.options.scanning.selectText; + this.onTermLookup(e, {disableScroll: true, disableHistory: true, selectText}); + } + + onMouseLeave(e) { + this.clickScanPrevent = true; + clearTimeout(e.target.dataset.timer); + delete e.target.dataset.timer; + } + + onTermLookup(e, params) { + this.pendingLookup = true; + (async () => { + await this.search.onTermLookup(e, params); + this.pendingLookup = false; + })(); + } + + onParserChange(e) { + const selectedParser = e.target.value; + this.selectedParser = selectedParser; + apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext()); + this.renderParseResult(this.getParseResult()); + } + + refreshSelectedParser() { + if (this.parseResults.length > 0) { + if (this.selectedParser === null) { + this.selectedParser = this.search.options.parsing.selectedParser; + } + if (this.selectedParser === null || !this.getParseResult()) { + const selectedParser = this.parseResults[0].id; + this.selectedParser = selectedParser; + apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext()); + } + } + } + + getParseResult() { + return this.parseResults.find((r) => r.id === this.selectedParser); + } + + async setText(text) { + this.search.setSpinnerVisible(true); + + await this.setPreview(text); + + this.parseResults = await this.parseText(text); + this.refreshSelectedParser(); + + this.renderParserSelect(); + await this.renderParseResult(); + + this.search.setSpinnerVisible(false); + } + + async parseText(text) { + const results = []; + if (this.search.options.parsing.enableScanningParser) { + results.push({ + name: 'Scanning parser', + id: 'scan', + parsedText: await apiTextParse(text, this.search.getOptionsContext()) + }); + } + if (this.search.options.parsing.enableMecabParser) { + const mecabResults = await apiTextParseMecab(text, this.search.getOptionsContext()); + for (const mecabDictName in mecabResults) { + results.push({ + name: `MeCab: ${mecabDictName}`, + id: `mecab-${mecabDictName}`, + parsedText: mecabResults[mecabDictName] + }); + } + } + return results; + } + + async setPreview(text) { + const previewTerms = []; + while (text.length > 0) { + const tempText = text.slice(0, 2); + previewTerms.push([{text: Array.from(tempText)}]); + text = text.slice(2); + } + this.queryParser.innerHTML = await apiTemplateRender('query-parser.html', { + terms: previewTerms, + preview: true + }); + + for (const charElement of this.queryParser.querySelectorAll('.query-parser-char')) { + this.activateScanning(charElement); + } + } + + renderParserSelect() { + this.queryParserSelect.innerHTML = ''; + if (this.parseResults.length > 1) { + const select = document.createElement('select'); + select.classList.add('form-control'); + for (const parseResult of this.parseResults) { + const option = document.createElement('option'); + option.value = parseResult.id; + option.innerText = parseResult.name; + option.defaultSelected = this.selectedParser === parseResult.id; + select.appendChild(option); + } + select.addEventListener('change', this.onParserChange.bind(this)); + this.queryParserSelect.appendChild(select); + } + } + + async renderParseResult() { + const parseResult = this.getParseResult(); + if (!parseResult) { + this.queryParser.innerHTML = ''; + return; + } + + this.queryParser.innerHTML = await apiTemplateRender( + 'query-parser.html', + {terms: QueryParser.processParseResultForDisplay(parseResult.parsedText)} + ); + + for (const charElement of this.queryParser.querySelectorAll('.query-parser-char')) { + this.activateScanning(charElement); + } + } + + activateScanning(element) { + element.addEventListener('mousemove', (e) => { + clearTimeout(e.target.dataset.timer); + if (this.search.options.scanning.modifier === 'none') { + e.target.dataset.timer = setTimeout(() => { + this.onMouseMove(e); + delete e.target.dataset.timer; + }, this.search.options.scanning.delay); + } else { + this.onMouseMove(e); + } + }); + element.addEventListener('mouseleave', (e) => { + this.onMouseLeave(e); + }); + } + + static processParseResultForDisplay(result) { + return result.map((term) => { + return term.filter((part) => part.text.trim()).map((part) => { + return { + text: Array.from(part.text), + reading: part.reading, + raw: !part.reading || !part.reading.trim() + }; + }); + }); + } +} diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index dbfcb15d..00b7ca4b 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -32,6 +32,8 @@ class DisplaySearch extends Display { url: window.location.href }; + this.queryParser = new QueryParser(this); + this.search = document.querySelector('#search'); this.query = document.querySelector('#query'); this.intro = document.querySelector('#intro'); @@ -72,11 +74,11 @@ class DisplaySearch extends Display { const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || ''; if (e.target.checked) { window.wanakana.bind(this.query); - this.query.value = window.wanakana.toKana(query); + this.setQuery(window.wanakana.toKana(query)); apiOptionsSet({general: {enableWanakana: true}}, this.getOptionsContext()); } else { window.wanakana.unbind(this.query); - this.query.value = query; + this.setQuery(query); apiOptionsSet({general: {enableWanakana: false}}, this.getOptionsContext()); } this.onSearchQueryUpdated(this.query.value, false); @@ -86,9 +88,9 @@ class DisplaySearch extends Display { const query = DisplaySearch.getSearchQueryFromLocation(window.location.href); if (query !== null) { if (this.isWanakanaEnabled()) { - this.query.value = window.wanakana.toKana(query); + this.setQuery(window.wanakana.toKana(query)); } else { - this.query.value = query; + this.setQuery(query); } this.onSearchQueryUpdated(this.query.value, false); } @@ -159,18 +161,19 @@ class DisplaySearch extends Display { e.preventDefault(); const query = this.query.value; + this.queryParser.setText(query); const queryString = query.length > 0 ? `?query=${encodeURIComponent(query)}` : ''; window.history.pushState(null, '', `${window.location.pathname}${queryString}`); this.onSearchQueryUpdated(query, true); } - onPopState(e) { + onPopState() { const query = DisplaySearch.getSearchQueryFromLocation(window.location.href) || ''; if (this.query !== null) { if (this.isWanakanaEnabled()) { - this.query.value = window.wanakana.toKana(query); + this.setQuery(window.wanakana.toKana(query)); } else { - this.query.value = query; + this.setQuery(query); } } @@ -179,27 +182,14 @@ class DisplaySearch extends Display { onKeyDown(e) { const key = Display.getKeyFromEvent(e); + const ignoreKeys = DisplaySearch.onKeyDownIgnoreKeys; - let activeModifierMap = { + const activeModifierMap = { 'Control': e.ctrlKey, 'Meta': e.metaKey, 'ANY_MOD': true }; - const ignoreKeys = { - 'ANY_MOD': ['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End'] - .concat( - Array.from(Array(24).keys()) - .map(i => `F${i + 1}`) - ), - 'Control': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'], - 'Meta': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'], - 'OS': [], - 'Alt': [], - 'AltGraph': [], - 'Shift': [] - } - let preventFocus = false; for (const [modifier, keys] of Object.entries(ignoreKeys)) { const modifierActive = activeModifierMap[modifier]; @@ -216,19 +206,28 @@ class DisplaySearch extends Display { async onSearchQueryUpdated(query, animate) { try { + const details = {}; + const match = /[*\uff0a]+$/.exec(query); + if (match !== null) { + details.wildcard = true; + query = query.substring(0, query.length - match[0].length); + } + const valid = (query.length > 0); this.setIntroVisible(!valid, animate); this.updateSearchButton(); if (valid) { - const {definitions} = await apiTermsFind(query, this.optionsContext); + const {definitions} = await apiTermsFind(query, details, this.optionsContext); this.setContentTerms(definitions, { focus: false, - sentence: null, + disableHistory: true, + sentence: {text: query, offset: 0}, url: window.location.href }); } else { this.container.textContent = ''; } + window.parent.postMessage('popupClose', '*'); } catch (e) { this.onError(e); } @@ -236,7 +235,7 @@ class DisplaySearch extends Display { onRuntimeMessage({action, params}, sender, callback) { const handlers = DisplaySearch.runtimeMessageHandlers; - if (handlers.hasOwnProperty(action)) { + if (hasOwn(handlers, action)) { const handler = handlers[action]; const result = handler(this, params); callback(result); @@ -247,7 +246,7 @@ class DisplaySearch extends Display { initClipboardMonitor() { // ignore copy from search page - window.addEventListener('copy', (e) => { + window.addEventListener('copy', () => { this.clipboardPrevText = document.getSelection().toString().trim(); }); } @@ -261,11 +260,11 @@ class DisplaySearch extends Display { } else if (IS_FIREFOX === false) { curText = (await apiClipboardGet()).trim(); } - if (curText && (curText !== this.clipboardPrevText)) { + if (curText && (curText !== this.clipboardPrevText) && jpIsJapaneseText(curText)) { if (this.isWanakanaEnabled()) { - this.query.value = window.wanakana.toKana(curText); + this.setQuery(window.wanakana.toKana(curText)); } else { - this.query.value = curText; + this.setQuery(curText); } const queryString = curText.length > 0 ? `?query=${encodeURIComponent(curText)}` : ''; @@ -292,8 +291,9 @@ class DisplaySearch extends Display { return this.optionsContext; } - setCustomCss() { - // No custom CSS + setQuery(query) { + this.query.value = query; + this.queryParser.setText(query); } setIntroVisible(visible, animate) { @@ -325,7 +325,7 @@ class DisplaySearch extends Display { this.intro.style.transition = ''; this.intro.style.height = ''; const size = this.intro.getBoundingClientRect(); - this.intro.style.height = `0px`; + this.intro.style.height = '0px'; this.intro.style.transition = `height ${duration}s ease-in-out 0s`; window.getComputedStyle(this.intro).getPropertyValue('height'); // Commits height so next line can start animation this.intro.style.height = `${size.height}px`; @@ -357,7 +357,7 @@ class DisplaySearch extends Display { } static getSearchQueryFromLocation(url) { - let match = /^[^\?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url); + const match = /^[^?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url); return match !== null ? decodeURIComponent(match[1]) : null; } } @@ -368,4 +368,19 @@ DisplaySearch.runtimeMessageHandlers = { } }; +DisplaySearch.onKeyDownIgnoreKeys = { + 'ANY_MOD': [ + 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageDown', 'PageUp', 'Home', 'End', + 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', + 'F11', 'F12', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20', + 'F21', 'F22', 'F23', 'F24' + ], + 'Control': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'], + 'Meta': ['C', 'A', 'Z', 'Y', 'X', 'F', 'G'], + 'OS': [], + 'Alt': [], + 'AltGraph': [], + 'Shift': [] +}; + window.yomichan_search = DisplaySearch.create(); diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js deleted file mode 100644 index 05a0604a..00000000 --- a/ext/bg/js/settings.js +++ /dev/null @@ -1,954 +0,0 @@ -/* - * Copyright (C) 2016-2017 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/>. - */ - -async function getOptionsArray() { - const optionsFull = await apiOptionsGetFull(); - return optionsFull.profiles.map(profile => profile.options); -} - -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.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.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); - - 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); - options.anki.server = $('#interface-server').val(); - options.anki.screenshot.format = $('#screenshot-format').val(); - options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); - options.anki.fieldTemplates = $('#field-templates').val(); - - if (optionsAnkiEnableOld && !ankiErrorShown()) { - options.anki.terms.deck = $('#anki-terms-deck').val(); - options.anki.terms.model = $('#anki-terms-model').val(); - options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict($('#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($('#kanji .anki-field-value'))); - } - - options.general.mainDictionary = $('#dict-main').val(); - $('.dict-group').each((index, element) => { - const dictionary = $(element); - options.dictionaries[dictionary.data('title')] = utilBackgroundIsolate({ - priority: parseInt(dictionary.find('.dict-priority').val(), 10), - enabled: dictionary.find('.dict-enabled').prop('checked'), - allowSecondarySearches: dictionary.find('.dict-allow-secondary-searches').prop('checked') - }); - }); -} - -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-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); - $('#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); - - $('#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); - $('#screenshot-format').val(options.anki.screenshot.format); - $('#screenshot-quality').val(options.anki.screenshot.quality); - $('#field-templates').val(options.anki.fieldTemplates); - - try { - await dictionaryGroupsPopulate(options); - await formMainDictionaryOptionsPopulate(options); - } catch (e) { - dictionaryErrorsShow([e]); - } - - try { - await ankiDeckAndModelPopulate(options); - } catch (e) { - ankiErrorShow(e); - } - - formUpdateVisibility(options); -} - -function formSetupEventListeners() { - $('#dict-purge-link').click(utilAsync(onDictionaryPurge)); - $('#dict-file').change(utilAsync(onDictionaryImport)); - $('#dict-file-button').click(onDictionaryImportButtonClick); - - $('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset)); - $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(utilAsync(onFormOptionsChanged)); - $('.anki-model').change(utilAsync(onAnkiModelChanged)); -} - -function formUpdateVisibility(options) { - const general = $('#anki-general'); - if (options.anki.enable) { - general.show(); - } else { - general.hide(); - } - - const advanced = $('.options-advanced'); - if (options.general.showAdvanced) { - advanced.show(); - } else { - advanced.hide(); - } - - const mainGroup = $('#dict-main-group'); - if (options.general.resultOutputMode === 'merge') { - mainGroup.show(); - } else { - mainGroup.hide(); - } - - const debug = $('#debug'); - if (options.general.debugInfo) { - const temp = utilIsolate(options); - temp.anki.fieldTemplates = '...'; - const text = JSON.stringify(temp, null, 4); - debug.html(handlebarsEscape(text)); - debug.show(); - } else { - debug.hide(); - } -} - -async function formMainDictionaryOptionsPopulate(options) { - const select = $('#dict-main').empty(); - select.append($('<option class="text-muted" value="">Not selected</option>')); - - let mainDictionary = ''; - for (const dictRow of toIterable(await utilDatabaseSummarize())) { - if (dictRow.sequenced) { - select.append($(`<option value="${dictRow.title}">${dictRow.title}</option>`)); - if (dictRow.title === options.general.mainDictionary) { - mainDictionary = dictRow.title; - } - } - } - - select.val(mainDictionary); -} - -async function onFormOptionsChanged(e) { - if (!e.originalEvent && !e.isTrigger) { - return; - } - - const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - const optionsAnkiEnableOld = options.anki.enable; - const optionsAnkiServerOld = options.anki.server; - - await formRead(options); - await settingsSaveOptions(); - formUpdateVisibility(options); - - try { - const ankiUpdated = - options.anki.enable !== optionsAnkiEnableOld || - options.anki.server !== optionsAnkiServerOld; - - if (ankiUpdated) { - ankiSpinnerShow(true); - await ankiDeckAndModelPopulate(options); - ankiErrorShow(); - } - } catch (e) { - ankiErrorShow(e); - } finally { - ankiSpinnerShow(false); - } -} - -async function onReady() { - showExtensionInformation(); - - formSetupEventListeners(); - appearanceInitialize(); - await audioSettingsInitialize(); - await profileOptionsSetup(); - - storageInfoInitialize(); - - chrome.runtime.onMessage.addListener(onMessage); -} - -$(document).ready(utilAsync(onReady)); - - -/* - * Appearance - */ - -function appearanceInitialize() { - let previewVisible = false; - $('#settings-popup-preview-button').on('click', () => { - if (previewVisible) { return; } - showAppearancePreview(); - previewVisible = true; - }); -} - -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'; - - window.wanakana.bind(text[0]); - - text.on('input', () => { - const action = 'setText'; - const params = {text: text.val()}; - frame.contentWindow.postMessage({action, params}, '*'); - }); - customCss.on('input', () => { - const action = 'setCustomCss'; - const params = {css: customCss.val()}; - frame.contentWindow.postMessage({action, params}, '*'); - }); - customOuterCss.on('input', () => { - const action = 'setCustomOuterCss'; - const params = {css: customOuterCss.val()}; - frame.contentWindow.postMessage({action, params}, '*'); - }); - - container.append(frame); - buttonContainer.remove(); - settings.css('display', ''); -} - - -/* - * Audio - */ - -let audioSourceUI = null; - -async function audioSettingsInitialize() { - const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - audioSourceUI = new AudioSourceUI.Container(options.audio.sources, $('.audio-source-list'), $('.audio-source-add')); - audioSourceUI.save = () => apiOptionsSave(); - - textToSpeechInitialize(); -} - -function textToSpeechInitialize() { - if (typeof speechSynthesis === 'undefined') { return; } - - speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false); - updateTextToSpeechVoices(); - - $('#text-to-speech-voice-test').on('click', () => textToSpeechTest()); -} - -function updateTextToSpeechVoices() { - const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index})); - voices.sort(textToSpeechVoiceCompare); - if (voices.length > 0) { - $('#text-to-speech-voice-container').css('display', ''); - } - - const select = $('#text-to-speech-voice'); - select.empty(); - select.append($('<option>').val('').text('None')); - for (const {voice} of voices) { - select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`)); - } - - select.val(select.attr('data-value')); -} - -function 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; } - } - - const aIsDefault = a.voice.default; - const bIsDefault = b.voice.default; - if (aIsDefault) { - if (!bIsDefault) { return -1; } - } else { - if (bIsDefault) { return 1; } - } - - if (a.index < b.index) { return -1; } - if (a.index > b.index) { return 1; } - return 0; -} - -function textToSpeechTest() { - try { - const text = $('#text-to-speech-voice-test').attr('data-speech-text') || ''; - const voiceURI = $('#text-to-speech-voice').val(); - const voice = audioGetTextToSpeechVoice(voiceURI); - if (voice === null) { return; } - - const utterance = new SpeechSynthesisUtterance(text); - utterance.lang = 'ja-JP'; - utterance.voice = voice; - utterance.volume = 1.0; - - speechSynthesis.speak(utterance); - } catch (e) { - // NOP - } -} - - -/* - * Remote options updates - */ - -function settingsGetSource() { - return new Promise((resolve) => { - chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`)); - }); -} - -async function settingsSaveOptions() { - const source = await settingsGetSource(); - await apiOptionsSave(source); -} - -async function onOptionsUpdate({source}) { - const thisSource = await settingsGetSource(); - if (source === thisSource) { return; } - - const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - await formWrite(options); -} - -function onMessage({action, params}, sender, callback) { - switch (action) { - case 'optionsUpdate': - onOptionsUpdate(params); - break; - case 'getUrl': - callback({url: window.location.href}); - break; - } -} - - -/* - * Dictionary - */ - -function dictionaryErrorToString(error) { - if (error.toString) { - error = error.toString(); - } else { - error = `${error}`; - } - - for (const [match, subst] of dictionaryErrorToString.overrides) { - if (error.includes(match)) { - error = subst; - break; - } - } - - 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 = $('#dict-error'); - dialog.show().text(''); - - if (errors !== null && errors.length > 0) { - const uniqueErrors = {}; - for (let e of errors) { - e = dictionaryErrorToString(e); - uniqueErrors[e] = uniqueErrors.hasOwnProperty(e) ? uniqueErrors[e] + 1 : 1; - } - - for (const e in uniqueErrors) { - const count = uniqueErrors[e]; - 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.append($(div)); - } - - dialog.show(); - } else { - dialog.hide(); - } -} - -function dictionarySpinnerShow(show) { - const spinner = $('#dict-spinner'); - if (show) { - spinner.show(); - } else { - spinner.hide(); - } -} - -function dictionaryGroupsSort() { - const dictGroups = $('#dict-groups'); - const dictGroupChildren = dictGroups.children('.dict-group').sort((ca, cb) => { - const pa = parseInt($(ca).find('.dict-priority').val(), 10); - const pb = parseInt($(cb).find('.dict-priority').val(), 10); - if (pa < pb) { - return 1; - } else if (pa > pb) { - return -1; - } else { - return 0; - } - }); - - dictGroups.append(dictGroupChildren); -} - -async function dictionaryGroupsPopulate(options) { - const dictGroups = $('#dict-groups').empty(); - const dictWarning = $('#dict-warning').hide(); - - const dictRows = toIterable(await utilDatabaseSummarize()); - if (dictRows.length === 0) { - dictWarning.show(); - } - - for (const dictRow of toIterable(dictRowsSort(dictRows, options))) { - const dictOptions = options.dictionaries[dictRow.title] || { - enabled: false, - priority: 0, - allowSecondarySearches: false - }; - - const dictHtml = await apiTemplateRender('dictionary.html', { - enabled: dictOptions.enabled, - priority: dictOptions.priority, - allowSecondarySearches: dictOptions.allowSecondarySearches, - title: dictRow.title, - version: dictRow.version, - revision: dictRow.revision, - outdated: dictRow.version < 3 - }); - - dictGroups.append($(dictHtml)); - } - - formUpdateVisibility(options); - - $('.dict-enabled, .dict-priority, .dict-allow-secondary-searches').change(e => { - dictionaryGroupsSort(); - onFormOptionsChanged(e); - }); -} - -async function onDictionaryPurge(e) { - e.preventDefault(); - - const dictControls = $('#dict-importer, #dict-groups, #dict-main-group').hide(); - const dictProgress = $('#dict-purge').show(); - - try { - dictionaryErrorsShow(null); - dictionarySpinnerShow(true); - - await utilDatabasePurge(); - for (const options of toIterable(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) { - dictionaryErrorsShow([e]); - } finally { - dictionarySpinnerShow(false); - - dictControls.show(); - dictProgress.hide(); - - if (storageEstimate.mostRecent !== null) { - storageUpdateStats(); - } - } -} - -function onDictionaryImportButtonClick() { - const dictFile = document.querySelector('#dict-file'); - dictFile.click(); -} - -async function onDictionaryImport(e) { - const dictFile = $('#dict-file'); - const dictControls = $('#dict-importer').hide(); - const dictProgress = $('#dict-import-progress').show(); - - try { - dictionaryErrorsShow(null); - 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(); - } - }; - setProgress(0.0); - - const exceptions = []; - const summary = await utilDatabaseImport(e.target.files[0], updateProgress, exceptions); - for (const options of toIterable(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); - } - - const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - await dictionaryGroupsPopulate(options); - await formMainDictionaryOptionsPopulate(options); - } catch (e) { - dictionaryErrorsShow([e]); - } finally { - dictionarySpinnerShow(false); - - dictFile.val(''); - dictControls.show(); - dictProgress.hide(); - } -} - - -/* - * Anki - */ - -function ankiSpinnerShow(show) { - const spinner = $('#anki-spinner'); - if (show) { - spinner.show(); - } else { - spinner.hide(); - } -} - -function ankiErrorShow(error) { - const dialog = $('#anki-error'); - if (error) { - dialog.show().text(error); - } - else { - dialog.hide(); - } -} - -function ankiErrorShown() { - return $('#anki-error').is(':visible'); -} - -function ankiFieldsToDict(selection) { - const result = {}; - selection.each((index, element) => { - result[$(element).data('field')] = $(element).val(); - }); - - return result; -} - -async function ankiDeckAndModelPopulate(options) { - const ankiFormat = $('#anki-format').hide(); - - const deckNames = await utilAnkiGetDeckNames(); - const ankiDeck = $('.anki-deck'); - ankiDeck.find('option').remove(); - deckNames.sort().forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); - - const modelNames = await utilAnkiGetModelNames(); - const ankiModel = $('.anki-model'); - ankiModel.find('option').remove(); - modelNames.sort().forEach(name => ankiModel.append($('<option/>', {value: name, text: name}))); - - $('#anki-terms-deck').val(options.anki.terms.deck); - await ankiFieldsPopulate($('#anki-terms-model').val(options.anki.terms.model), options); - - $('#anki-kanji-deck').val(options.anki.kanji.deck); - await ankiFieldsPopulate($('#anki-kanji-model').val(options.anki.kanji.model), options); - - ankiFormat.show(); -} - -async function ankiFieldsPopulate(element, options) { - const modelName = element.val(); - if (!modelName) { - return; - } - - const tab = element.closest('.tab-pane'); - const tabId = tab.attr('id'); - const container = tab.find('tbody').empty(); - - const markers = { - 'terms': [ - 'audio', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'dictionary', - 'expression', - 'furigana', - 'furigana-plain', - 'glossary', - 'glossary-brief', - 'reading', - 'screenshot', - 'sentence', - 'tags', - 'url' - ], - 'kanji': [ - 'character', - 'dictionary', - 'glossary', - 'kunyomi', - 'onyomi', - 'screenshot', - 'sentence', - 'tags', - 'url' - ] - }[tabId] || {}; - - for (const name of await utilAnkiGetModelFieldNames(modelName)) { - const value = options.anki[tabId].fields[name] || ''; - const html = Handlebars.templates['model.html']({name, markers, value}); - container.append($(html)); - } - - tab.find('.anki-field-value').change(utilAsync(onFormOptionsChanged)); - tab.find('.marker-link').click(onAnkiMarkerClicked); -} - -function onAnkiMarkerClicked(e) { - e.preventDefault(); - const link = e.target; - $(link).closest('.input-group').find('.anki-field-value').val(`{${link.text}}`).trigger('change'); -} - -async function onAnkiModelChanged(e) { - try { - if (!e.originalEvent) { - return; - } - - const element = $(this); - const tab = element.closest('.tab-pane'); - const tabId = tab.attr('id'); - - const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - await formRead(options); - options.anki[tabId].fields = utilBackgroundIsolate({}); - await settingsSaveOptions(); - - ankiSpinnerShow(true); - await ankiFieldsPopulate(element, options); - ankiErrorShow(); - } catch (e) { - ankiErrorShow(e); - } finally { - ankiSpinnerShow(false); - } -} - -async function onAnkiFieldTemplatesReset(e) { - try { - e.preventDefault(); - const optionsContext = getOptionsContext(); - const options = await apiOptionsGet(optionsContext); - const fieldTemplates = profileOptionsGetDefaultFieldTemplates(); - options.anki.fieldTemplates = fieldTemplates; - $('#field-templates').val(fieldTemplates); - await settingsSaveOptions(); - } catch (e) { - ankiErrorShow(e); - } -} - - -/* - * Storage - */ - -function storageBytesToLabeledString(size) { - const base = 1000; - const labels = [' bytes', 'KB', 'MB', 'GB']; - let labelIndex = 0; - while (size >= base) { - size /= base; - ++labelIndex; - } - 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) { } - return null; -} -storageEstimate.mostRecent = null; - -async function isStoragePeristent() { - try { - return await navigator.storage.persisted(); - } catch (e) { } - return false; -} - -async function storageInfoInitialize() { - storagePersistInitialize(); - const {browser, platform} = await apiGetEnvironmentInfo(); - 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); - } - 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); - - storageSpinnerShow(false); -} - -function storageSpinnerShow(show) { - const spinner = $('#storage-spinner'); - if (show) { - spinner.show(); - } else { - spinner.hide(); - } -} - -async function storagePersistInitialize() { - if (!(navigator.storage && navigator.storage.persist)) { - // Not supported - return; - } - - 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; - } - let result = false; - try { - result = await navigator.storage.persist(); - } catch (e) { - // NOP - } - - if (result) { - persisted = true; - checkbox.checked = true; - storageShowInfo(); - } else { - $('.storage-persist-fail-warning').removeClass('storage-hidden'); - } - }, false); -} - - -/* - * Information - */ - -function showExtensionInformation() { - const node = document.getElementById('extension-info'); - if (node === null) { return; } - - const manifest = chrome.runtime.getManifest(); - node.textContent = `${manifest.name} v${manifest.version}`; -} diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js new file mode 100644 index 00000000..9cdfc134 --- /dev/null +++ b/ext/bg/js/settings/anki-templates.js @@ -0,0 +1,109 @@ +/* + * 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/>. + */ + + +function onAnkiFieldTemplatesReset(e) { + e.preventDefault(); + $('#field-template-reset-modal').modal('show'); +} + +function onAnkiFieldTemplatesResetConfirm(e) { + e.preventDefault(); + + $('#field-template-reset-modal').modal('hide'); + + const element = document.querySelector('#field-templates'); + element.value = profileOptionsGetDefaultFieldTemplates(); + element.dispatchEvent(new Event('change')); +} + +function ankiTemplatesInitialize() { + const markers = new Set(ankiGetFieldMarkers('terms').concat(ankiGetFieldMarkers('kanji'))); + const fragment = ankiGetFieldMarkersHtml(markers); + + const list = document.querySelector('#field-templates-list'); + list.appendChild(fragment); + for (const node of list.querySelectorAll('.marker-link')) { + node.addEventListener('click', onAnkiTemplateMarkerClicked, false); + } + + $('#field-templates').on('change', (e) => onAnkiTemplatesValidateCompile(e)); + $('#field-template-render').on('click', (e) => onAnkiTemplateRender(e)); + $('#field-templates-reset').on('click', (e) => onAnkiFieldTemplatesReset(e)); + $('#field-templates-reset-confirm').on('click', (e) => onAnkiFieldTemplatesResetConfirm(e)); +} + +const ankiTemplatesValidateGetDefinition = (() => { + let cachedValue = null; + let cachedText = null; + + return async (text, optionsContext) => { + if (cachedText !== text) { + const {definitions} = await apiTermsFind(text, {}, optionsContext); + if (definitions.length === 0) { return null; } + + cachedValue = definitions[0]; + cachedText = text; + } + 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 { + const optionsContext = getOptionsContext(); + const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext); + if (definition !== null) { + const options = await apiOptionsGet(optionsContext); + result = await dictFieldFormat(field, definition, mode, options, exceptions); + } + } catch (e) { + exceptions.push(e); + } + + 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); + } +} + +function onAnkiTemplatesValidateCompile() { + const infoNode = document.querySelector('#field-template-compile-result'); + ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true); +} + +function onAnkiTemplateMarkerClicked(e) { + e.preventDefault(); + document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`; +} + +function onAnkiTemplateRender(e) { + e.preventDefault(); + + 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); +} diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js new file mode 100644 index 00000000..e1aabbaf --- /dev/null +++ b/ext/bg/js/settings/anki.js @@ -0,0 +1,247 @@ +/* + * 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/>. + */ + + +// Private + +let _ankiDataPopulated = false; + + +function _ankiSpinnerShow(show) { + const spinner = $('#anki-spinner'); + if (show) { + spinner.show(); + } else { + spinner.hide(); + } +} + +function _ankiSetError(error) { + const node = document.querySelector('#anki-error'); + if (!node) { return; } + if (error) { + node.hidden = false; + node.textContent = `${error}`; + } + else { + node.hidden = true; + node.textContent = ''; + } +} + +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); + } + 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([utilAnkiGetDeckNames(), utilAnkiGetModelNames()]); + 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); + } + + 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; + } +} + +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)); + + return content; +} + +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); + } + + container.textContent = ''; + container.appendChild(fragment); + + for (const node of container.querySelectorAll('.anki-field-value')) { + node.addEventListener('change', (e) => onFormOptionsChanged(e), false); + } + for (const node of container.querySelectorAll('.marker-link')) { + node.addEventListener('click', (e) => _onAnkiMarkerClicked(e), false); + } +} + +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 utilAnkiGetModelFieldNames(modelName); + _ankiSetError(null); + } catch (error) { + _ankiSetError(error); + return; + } finally { + _ankiSpinnerShow(false); + } + + const tabId = node.dataset.ankiCardType; + if (tabId !== 'terms' && tabId !== 'kanji') { return; } + + const fields = {}; + for (const name of fieldNames) { + fields[name] = ''; + } + + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + options.anki[tabId].fields = utilBackgroundIsolate(fields); + await settingsSaveOptions(); + + await _ankiFieldsPopulate(tabId, options); +} + + +// Public + +function ankiErrorShown() { + const node = document.querySelector('#anki-error'); + return node && !node.hidden; +} + +function ankiFieldsToDict(elements) { + const result = {}; + for (const element of elements) { + result[element.dataset.field] = element.value; + } + 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); + } + return fragment; +} + +function ankiGetFieldMarkers(type) { + switch (type) { + case 'terms': + return [ + 'audio', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'dictionary', + 'expression', + 'furigana', + 'furigana-plain', + 'glossary', + 'glossary-brief', + 'reading', + 'screenshot', + 'sentence', + 'tags', + 'url' + ]; + case 'kanji': + return [ + 'character', + 'dictionary', + 'glossary', + 'kunyomi', + 'onyomi', + 'screenshot', + 'sentence', + 'tags', + 'url' + ]; + default: + return []; + } +} + + +function ankiInitialize() { + for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { + node.addEventListener('change', (e) => _onAnkiModelChanged(e), false); + } +} + +async function onAnkiOptionsChanged(options) { + 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/audio.js b/ext/bg/js/settings/audio.js new file mode 100644 index 00000000..f63551ed --- /dev/null +++ b/ext/bg/js/settings/audio.js @@ -0,0 +1,102 @@ +/* + * 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 audioSourceUI = null; + +async function audioSettingsInitialize() { + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + audioSourceUI = new AudioSourceUI.Container(options.audio.sources, $('.audio-source-list'), $('.audio-source-add')); + audioSourceUI.save = () => settingsSaveOptions(); + + textToSpeechInitialize(); +} + +function textToSpeechInitialize() { + if (typeof speechSynthesis === 'undefined') { return; } + + speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false); + updateTextToSpeechVoices(); + + $('#text-to-speech-voice-test').on('click', () => textToSpeechTest()); +} + +function updateTextToSpeechVoices() { + const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index})); + voices.sort(textToSpeechVoiceCompare); + if (voices.length > 0) { + $('#text-to-speech-voice-container').css('display', ''); + } + + const select = $('#text-to-speech-voice'); + select.empty(); + select.append($('<option>').val('').text('None')); + for (const {voice} of voices) { + select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`)); + } + + select.val(select.attr('data-value')); +} + +function 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; } + } + + const aIsDefault = a.voice.default; + const bIsDefault = b.voice.default; + if (aIsDefault) { + if (!bIsDefault) { return -1; } + } else { + if (bIsDefault) { return 1; } + } + + if (a.index < b.index) { return -1; } + if (a.index > b.index) { return 1; } + return 0; +} + +function textToSpeechTest() { + try { + const text = $('#text-to-speech-voice-test').attr('data-speech-text') || ''; + const voiceURI = $('#text-to-speech-voice').val(); + const voice = audioGetTextToSpeechVoice(voiceURI); + if (voice === null) { return; } + + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = 'ja-JP'; + utterance.voice = voice; + utterance.volume = 1.0; + + speechSynthesis.speak(utterance); + } catch (e) { + // NOP + } +} diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js new file mode 100644 index 00000000..065a8abc --- /dev/null +++ b/ext/bg/js/settings/dictionaries.js @@ -0,0 +1,618 @@ +/* + * 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 dictionaryUI = null; + + +class SettingsDictionaryListUI { + constructor(container, template, extraContainer, extraTemplate) { + this.container = container; + this.template = template; + this.extraContainer = extraContainer; + this.extraTemplate = extraTemplate; + this.optionsDictionaries = null; + this.dictionaries = null; + this.dictionaryEntries = []; + this.extra = null; + + document.querySelector('#dict-delete-confirm').addEventListener('click', (e) => this.onDictionaryConfirmDelete(e), false); + } + + setOptionsDictionaries(optionsDictionaries) { + this.optionsDictionaries = optionsDictionaries; + if (this.dictionaries !== null) { + this.setDictionaries(this.dictionaries); + } + } + + setDictionaries(dictionaries) { + for (const dictionaryEntry of this.dictionaryEntries) { + dictionaryEntry.cleanup(); + } + + this.dictionaryEntries = []; + this.dictionaries = toIterable(dictionaries); + + if (this.optionsDictionaries === null) { + return; + } + + let changed = false; + for (const dictionaryInfo of this.dictionaries) { + if (this.createEntry(dictionaryInfo)) { + changed = true; + } + } + + this.updateDictionaryOrder(); + + const titles = this.dictionaryEntries.map((e) => e.dictionaryInfo.title); + const removeKeys = Object.keys(this.optionsDictionaries).filter((key) => titles.indexOf(key) < 0); + if (removeKeys.length > 0) { + for (const key of toIterable(removeKeys)) { + delete this.optionsDictionaries[key]; + } + changed = true; + } + + if (changed) { + this.save(); + } + } + + createEntry(dictionaryInfo) { + const title = dictionaryInfo.title; + let changed = false; + let optionsDictionary; + const optionsDictionaries = this.optionsDictionaries; + if (hasOwn(optionsDictionaries, title)) { + optionsDictionary = optionsDictionaries[title]; + } else { + optionsDictionary = SettingsDictionaryListUI.createDictionaryOptions(); + optionsDictionaries[title] = optionsDictionary; + changed = true; + } + + const content = document.importNode(this.template.content, true).firstChild; + + this.dictionaryEntries.push(new SettingsDictionaryEntryUI(this, dictionaryInfo, content, optionsDictionary)); + + return changed; + } + + static createDictionaryOptions() { + return utilBackgroundIsolate({ + priority: 0, + enabled: false, + allowSecondarySearches: false + }); + } + + createExtra(totalCounts, remainders, totalRemainder) { + const content = document.importNode(this.extraTemplate.content, true).firstChild; + this.extraContainer.appendChild(content); + return new SettingsDictionaryExtraUI(this, totalCounts, remainders, totalRemainder, content); + } + + setCounts(dictionaryCounts, totalCounts) { + const remainders = Object.assign({}, totalCounts); + const keys = Object.keys(remainders); + + for (let i = 0, ii = Math.min(this.dictionaryEntries.length, dictionaryCounts.length); i < ii; ++i) { + const counts = dictionaryCounts[i]; + this.dictionaryEntries[i].setCounts(counts); + + for (const key of keys) { + remainders[key] -= counts[key]; + } + } + + let totalRemainder = 0; + for (const key of keys) { + totalRemainder += remainders[key]; + } + + if (this.extra !== null) { + this.extra.cleanup(); + this.extra = null; + } + + if (totalRemainder > 0) { + this.extra = this.createExtra(totalCounts, remainders, totalRemainder); + } + } + + updateDictionaryOrder() { + const sortInfo = this.dictionaryEntries.map((e, i) => [e, i]); + sortInfo.sort((a, b) => { + const i = b[0].optionsDictionary.priority - a[0].optionsDictionary.priority; + return (i !== 0 ? i : a[1] - b[1]); + }); + + for (const [e] of sortInfo) { + this.container.appendChild(e.content); + } + } + + save() { + // Overwrite + } + + onDictionaryConfirmDelete(e) { + e.preventDefault(); + const n = document.querySelector('#dict-delete-modal'); + const title = n.dataset.dict; + delete n.dataset.dict; + $(n).modal('hide'); + + const index = this.dictionaryEntries.findIndex((e) => e.dictionaryInfo.title === title); + if (index >= 0) { + this.dictionaryEntries[index].deleteDictionary(); + } + } +} + +class SettingsDictionaryEntryUI { + constructor(parent, dictionaryInfo, content, optionsDictionary) { + this.parent = parent; + this.dictionaryInfo = dictionaryInfo; + this.optionsDictionary = optionsDictionary; + this.counts = null; + this.eventListeners = []; + this.isDeleting = false; + + this.content = content; + this.enabledCheckbox = this.content.querySelector('.dict-enabled'); + this.allowSecondarySearchesCheckbox = this.content.querySelector('.dict-allow-secondary-searches'); + this.priorityInput = this.content.querySelector('.dict-priority'); + this.deleteButton = this.content.querySelector('.dict-delete-button'); + + if (this.dictionaryInfo.version < 3) { + this.content.querySelector('.dict-outdated').hidden = false; + } + + this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title; + this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`; + + this.applyValues(); + + this.addEventListener(this.enabledCheckbox, 'change', (e) => this.onEnabledChanged(e), false); + this.addEventListener(this.allowSecondarySearchesCheckbox, 'change', (e) => this.onAllowSecondarySearchesChanged(e), false); + this.addEventListener(this.priorityInput, 'change', (e) => this.onPriorityChanged(e), false); + this.addEventListener(this.deleteButton, 'click', (e) => this.onDeleteButtonClicked(e), false); + } + + cleanup() { + if (this.content !== null) { + if (this.content.parentNode !== null) { + this.content.parentNode.removeChild(this.content); + } + this.content = null; + } + this.dictionaryInfo = null; + this.clearEventListeners(); + } + + setCounts(counts) { + this.counts = counts; + const node = this.content.querySelector('.dict-counts'); + node.textContent = JSON.stringify({ + info: this.dictionaryInfo, + counts + }, null, 4); + node.removeAttribute('hidden'); + } + + save() { + this.parent.save(); + } + + addEventListener(node, type, listener, options) { + node.addEventListener(type, listener, options); + this.eventListeners.push([node, type, listener, options]); + } + + clearEventListeners() { + for (const [node, type, listener, options] of this.eventListeners) { + node.removeEventListener(type, listener, options); + } + this.eventListeners = []; + } + + applyValues() { + this.enabledCheckbox.checked = this.optionsDictionary.enabled; + this.allowSecondarySearchesCheckbox.checked = this.optionsDictionary.allowSecondarySearches; + this.priorityInput.value = `${this.optionsDictionary.priority}`; + } + + async deleteDictionary() { + if (this.isDeleting) { + return; + } + + const progress = this.content.querySelector('.progress'); + progress.hidden = false; + const progressBar = this.content.querySelector('.progress-bar'); + this.isDeleting = true; + + const prevention = new PageExitPrevention(); + try { + prevention.start(); + + const onProgress = ({processed, count, storeCount, storesProcesed}) => { + let percent = 0.0; + if (count > 0 && storesProcesed > 0) { + percent = (processed / count) * (storesProcesed / storeCount) * 100.0; + } + progressBar.style.width = `${percent}%`; + }; + + await utilDatabaseDeleteDictionary(this.dictionaryInfo.title, onProgress, {rate: 1000}); + } catch (e) { + dictionaryErrorsShow([e]); + } finally { + prevention.end(); + this.isDeleting = false; + progress.hidden = true; + + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + onDatabaseUpdated(options); + } + } + + onEnabledChanged(e) { + this.optionsDictionary.enabled = !!e.target.checked; + this.save(); + } + + onAllowSecondarySearchesChanged(e) { + this.optionsDictionary.allowSecondarySearches = !!e.target.checked; + this.save(); + } + + onPriorityChanged(e) { + let value = Number.parseFloat(e.target.value); + if (Number.isNaN(value)) { + value = this.optionsDictionary.priority; + } else { + this.optionsDictionary.priority = value; + this.save(); + } + + e.target.value = `${value}`; + + this.parent.updateDictionaryOrder(); + } + + onDeleteButtonClicked(e) { + e.preventDefault(); + + if (this.isDeleting) { + return; + } + + const title = this.dictionaryInfo.title; + const n = document.querySelector('#dict-delete-modal'); + n.dataset.dict = title; + document.querySelector('#dict-remove-modal-dict-name').textContent = title; + $(n).modal('show'); + } +} + +class SettingsDictionaryExtraUI { + constructor(parent, totalCounts, remainders, totalRemainder, content) { + this.parent = parent; + this.content = content; + + this.content.querySelector('.dict-total-count').textContent = `${totalRemainder} item${totalRemainder !== 1 ? 's' : ''}`; + + const node = this.content.querySelector('.dict-counts'); + node.textContent = JSON.stringify({ + counts: totalCounts, + remainders: remainders + }, null, 4); + node.removeAttribute('hidden'); + } + + cleanup() { + if (this.content !== null) { + if (this.content.parentNode !== null) { + this.content.parentNode.removeChild(this.content); + } + this.content = null; + } + } +} + + +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', (e) => onDictionaryPurgeButtonClick(e), false); + document.querySelector('#dict-purge-confirm').addEventListener('click', (e) => onDictionaryPurge(e), false); + document.querySelector('#dict-file-button').addEventListener('click', (e) => onDictionaryImportButtonClick(e), false); + document.querySelector('#dict-file').addEventListener('change', (e) => onDictionaryImport(e), false); + document.querySelector('#dict-main').addEventListener('change', (e) => onDictionaryMainChanged(e), false); + + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + onDictionaryOptionsChanged(options); + onDatabaseUpdated(options); +} + +async function onDictionaryOptionsChanged(options) { + if (dictionaryUI === null) { return; } + dictionaryUI.setOptionsDictionaries(options.dictionaries); +} + +async function onDatabaseUpdated(options) { + try { + const dictionaries = await utilDatabaseGetDictionaryInfo(); + dictionaryUI.setDictionaries(dictionaries); + + document.querySelector('#dict-warning').hidden = (dictionaries.length > 0); + + updateMainDictionarySelect(options, dictionaries); + + const {counts, total} = await utilDatabaseGetDictionaryCounts(dictionaries.map((v) => v.title), true); + dictionaryUI.setCounts(counts, total); + } catch (e) { + dictionaryErrorsShow([e]); + } +} + +async function updateMainDictionarySelect(options, 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); + + let value = ''; + const currentValue = options.general.mainDictionary; + for (const {title, sequenced} of toIterable(dictionaries)) { + if (!sequenced) { continue; } + + option = document.createElement('option'); + option.value = title; + option.textContent = title; + select.appendChild(option); + + if (title === currentValue) { + value = title; + } + } + + select.value = value; + + if (options.general.mainDictionary !== value) { + options.general.mainDictionary = value; + settingsSaveOptions(); + } +} + +async function onDictionaryMainChanged(e) { + const value = e.target.value; + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + options.general.mainDictionary = value; + settingsSaveOptions(); +} + + +function dictionaryErrorToString(error) { + if (error.toString) { + error = error.toString(); + } else { + error = `${error}`; + } + + for (const [match, subst] of dictionaryErrorToString.overrides) { + if (error.includes(match)) { + error = subst; + break; + } + } + + 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 = {}; + for (let e of errors) { + console.error(e); + e = dictionaryErrorToString(e); + uniqueErrors[e] = hasOwn(uniqueErrors, e) ? uniqueErrors[e] + 1 : 1; + } + + for (const e in uniqueErrors) { + const count = uniqueErrors[e]; + 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); + } + + dialog.hidden = false; + } else { + dialog.hidden = true; + } +} + + +function dictionarySpinnerShow(show) { + const spinner = $('#dict-spinner'); + if (show) { + spinner.show(); + } else { + spinner.hide(); + } +} + +function onDictionaryImportButtonClick() { + const dictFile = document.querySelector('#dict-file'); + dictFile.click(); +} + +function onDictionaryPurgeButtonClick(e) { + e.preventDefault(); + $('#dict-purge-modal').modal('show'); +} + +async function onDictionaryPurge(e) { + e.preventDefault(); + + $('#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 prevention = new PageExitPrevention(); + + try { + prevention.start(); + dictionaryErrorsShow(null); + dictionarySpinnerShow(true); + + await utilDatabasePurge(); + for (const options of toIterable(await getOptionsArray())) { + options.dictionaries = utilBackgroundIsolate({}); + options.general.mainDictionary = ''; + } + await settingsSaveOptions(); + + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + onDatabaseUpdated(options); + } catch (err) { + dictionaryErrorsShow([err]); + } finally { + prevention.end(); + + dictionarySpinnerShow(false); + + dictControls.show(); + dictProgress.hidden = true; + + if (storageEstimate.mostRecent !== null) { + storageUpdateStats(); + } + } +} + +async function onDictionaryImport(e) { + 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(); + + try { + prevention.start(); + dictionaryErrorsShow(null); + 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 exceptions = []; + const files = [...e.target.files]; + + 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 summary = await utilDatabaseImport(files[i], updateProgress, exceptions); + for (const options of toIterable(await getOptionsArray())) { + const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions(); + dictionaryOptions.enabled = true; + options.dictionaries[summary.title] = dictionaryOptions; + 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); + } + + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + onDatabaseUpdated(options); + } + } catch (err) { + dictionaryErrorsShow([err]); + } finally { + prevention.end(); + dictionarySpinnerShow(false); + + dictImportInfo.hidden = false; + dictImportInfo.textContent = ''; + dictFile.val(''); + dictControls.show(); + dictProgress.hide(); + } +} diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js new file mode 100644 index 00000000..7456e7a4 --- /dev/null +++ b/ext/bg/js/settings/main.js @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2016-2017 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/>. + */ + +async function getOptionsArray() { + const optionsFull = await apiOptionsGetFull(); + return optionsFull.profiles.map((profile) => profile.options); +} + +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.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.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.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked'); + options.parsing.enableMecabParser = $('#parsing-mecab-enable').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); + options.anki.server = $('#interface-server').val(); + options.anki.screenshot.format = $('#screenshot-format').val(); + options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10); + options.anki.fieldTemplates = $('#field-templates').val(); + + if (optionsAnkiEnableOld && !ankiErrorShown()) { + options.anki.terms.deck = $('#anki-terms-deck').val(); + 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); + $('#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-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); + $('#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); + + $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser); + $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser); + $('#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); + $('#screenshot-format').val(options.anki.screenshot.format); + $('#screenshot-quality').val(options.anki.screenshot.quality); + $('#field-templates').val(options.anki.fieldTemplates); + + onAnkiTemplatesValidateCompile(); + await onAnkiOptionsChanged(options); + await onDictionaryOptionsChanged(options); + + formUpdateVisibility(options); +} + +function formSetupEventListeners() { + $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change((e) => onFormOptionsChanged(e)); +} + +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}`; + + if (options.general.debugInfo) { + const temp = utilIsolate(options); + temp.anki.fieldTemplates = '...'; + const text = JSON.stringify(temp, null, 4); + $('#debug').text(text); + } +} + +async function onFormOptionsChanged() { + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + + await formRead(options); + await settingsSaveOptions(); + formUpdateVisibility(options); + + await onAnkiOptionsChanged(options); +} + + +function settingsGetSource() { + return new Promise((resolve) => { + chrome.tabs.getCurrent((tab) => resolve(`settings${tab ? tab.id : ''}`)); + }); +} + +async function settingsSaveOptions() { + const source = await settingsGetSource(); + await apiOptionsSave(source); +} + +async function onOptionsUpdate({source}) { + const thisSource = await settingsGetSource(); + if (source === thisSource) { return; } + + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + await formWrite(options); +} + +function onMessage({action, params}, sender, callback) { + switch (action) { + case 'optionsUpdate': + onOptionsUpdate(params); + break; + case 'getUrl': + callback({url: window.location.href}); + break; + } +} + + +function showExtensionInformation() { + const node = document.getElementById('extension-info'); + if (node === null) { return; } + + const manifest = chrome.runtime.getManifest(); + node.textContent = `${manifest.name} v${manifest.version}`; +} + + +async function onReady() { + showExtensionInformation(); + + formSetupEventListeners(); + appearanceInitialize(); + await audioSettingsInitialize(); + await profileOptionsSetup(); + await dictSettingsInitialize(); + ankiInitialize(); + ankiTemplatesInitialize(); + + storageInfoInitialize(); + + chrome.runtime.onMessage.addListener(onMessage); +} + +$(document).ready(() => onReady()); diff --git a/ext/bg/js/settings-popup-preview.js b/ext/bg/js/settings/popup-preview-frame.js index 7d641c46..49409968 100644 --- a/ext/bg/js/settings-popup-preview.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -106,7 +106,7 @@ class SettingsPopupPreview { onMessage(e) { const {action, params} = e.data; const handlers = SettingsPopupPreview.messageHandlers; - if (handlers.hasOwnProperty(action)) { + if (hasOwn(handlers, action)) { const handler = handlers[action]; handler(this, params); } diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js new file mode 100644 index 00000000..d8579eb1 --- /dev/null +++ b/ext/bg/js/settings/popup-preview.js @@ -0,0 +1,62 @@ +/* + * 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/>. + */ + + +function appearanceInitialize() { + let previewVisible = false; + $('#settings-popup-preview-button').on('click', () => { + if (previewVisible) { return; } + showAppearancePreview(); + previewVisible = true; + }); +} + +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'; + + window.wanakana.bind(text[0]); + + text.on('input', () => { + const action = 'setText'; + const params = {text: text.val()}; + frame.contentWindow.postMessage({action, params}, '*'); + }); + customCss.on('input', () => { + const action = 'setCustomCss'; + const params = {css: customCss.val()}; + frame.contentWindow.postMessage({action, params}, '*'); + }); + customOuterCss.on('input', () => { + const action = 'setCustomOuterCss'; + const params = {css: customOuterCss.val()}; + frame.contentWindow.postMessage({action, params}, '*'); + }); + + container.append(frame); + buttonContainer.remove(); + settings.css('display', ''); +} diff --git a/ext/bg/js/settings-profiles.js b/ext/bg/js/settings/profiles.js index ededc998..8c218e97 100644 --- a/ext/bg/js/settings-profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -35,16 +35,16 @@ async function profileOptionsSetup() { } 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-target').change((e) => onTargetProfileChanged(e)); + $('#profile-name').change((e) => onProfileNameChanged(e)); + $('#profile-add').click((e) => onProfileAdd(e)); + $('#profile-remove').click((e) => onProfileRemove(e)); + $('#profile-remove-confirm').click((e) => onProfileRemoveConfirm(e)); + $('#profile-copy').click((e) => onProfileCopy(e)); + $('#profile-copy-confirm').click((e) => onProfileCopyConfirm(e)); $('#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)); + $('.profile-form').find('input, select, textarea').not('.profile-form-manual').change((e) => onProfileOptionsChanged(e)); } function tryGetIntegerValue(selector, min, max) { @@ -95,7 +95,7 @@ async function profileFormWrite(optionsFull) { $('#profile-add-condition-group') ); profileConditionsContainer.save = () => { - apiOptionsSave(); + settingsSaveOptions(); conditionsClearCaches(profileConditionsDescriptor); }; profileConditionsContainer.isolate = utilBackgroundIsolate; @@ -147,7 +147,7 @@ function profileOptionsCreateCopyName(name, profiles, maxUniqueAttempts) { let i = 0; while (true) { const newName = `${prefix}${space}${index}${suffix}`; - if (i++ >= maxUniqueAttempts || profiles.findIndex(profile => profile.name === newName) < 0) { + if (i++ >= maxUniqueAttempts || profiles.findIndex((profile) => profile.name === newName) < 0) { return newName; } if (typeof index !== 'number') { @@ -166,7 +166,7 @@ async function onProfileOptionsChanged(e) { const optionsFull = await apiOptionsGetFull(); await profileFormRead(optionsFull); - await apiOptionsSave(); + await settingsSaveOptions(); } async function onTargetProfileChanged() { @@ -188,7 +188,7 @@ async function onProfileAdd() { optionsFull.profiles.push(profile); currentProfileIndex = optionsFull.profiles.length - 1; await profileOptionsUpdateTarget(optionsFull); - await apiOptionsSave(); + await settingsSaveOptions(); } async function onProfileRemove(e) { @@ -226,7 +226,7 @@ async function onProfileRemoveConfirm() { } await profileOptionsUpdateTarget(optionsFull); - await apiOptionsSave(); + await settingsSaveOptions(); } function onProfileNameChanged() { diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js new file mode 100644 index 00000000..51ca6855 --- /dev/null +++ b/ext/bg/js/settings/storage.js @@ -0,0 +1,138 @@ +/* + * 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/>. + */ + + +function storageBytesToLabeledString(size) { + const base = 1000; + const labels = [' bytes', 'KB', 'MB', 'GB']; + let labelIndex = 0; + while (size >= base) { + size /= base; + ++labelIndex; + } + 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 + } + 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 apiGetEnvironmentInfo(); + 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); + } + 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); + + storageSpinnerShow(false); +} + +function storageSpinnerShow(show) { + const spinner = $('#storage-spinner'); + if (show) { + spinner.show(); + } else { + spinner.hide(); + } +} + +async function storagePersistInitialize() { + if (!(navigator.storage && navigator.storage.persist)) { + // Not supported + return; + } + + 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; + } + let result = false; + try { + result = await navigator.storage.persist(); + } catch (e) { + // NOP + } + + if (result) { + persisted = true; + checkbox.checked = true; + storageShowInfo(); + } else { + $('.storage-persist-fail-warning').removeClass('storage-hidden'); + } + }, false); +} diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js index 59516d97..9320477f 100644 --- a/ext/bg/js/templates.js +++ b/ext/bg/js/templates.js @@ -1,32 +1,5 @@ (function() { var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; -templates['dictionary.html'] = template({"1":function(container,depth0,helpers,partials,data) { - return " <p class=\"text-warning\">This dictionary is outdated and may not support new extension features; please import the latest version.</p>\n"; -},"3":function(container,depth0,helpers,partials,data) { - return "checked"; -},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { - var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; - - return "<div class=\"dict-group well well-sm\" data-title=\"" - + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) - + "\">\n <h4><span class=\"text-muted glyphicon glyphicon-book\"></span> " - + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) - + " <small>rev." - + alias4(((helper = (helper = helpers.revision || (depth0 != null ? depth0.revision : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"revision","hash":{},"data":data}) : helper))) - + "</small></h4>\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.outdated : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + "\n <div class=\"checkbox\">\n <label><input type=\"checkbox\" class=\"dict-enabled\" " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enabled : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + "> Enable search</label>\n </div>\n <div class=\"checkbox options-advanced\">\n <label><input type=\"checkbox\" class=\"dict-allow-secondary-searches\" " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.allowSecondarySearches : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + "> Allow secondary searches</label>\n </div>\n <div class=\"form-group options-advanced\">\n <label for=\"dict-" - + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) - + "\">Result priority</label>\n <input type=\"number\" value=\"" - + alias4(((helper = (helper = helpers.priority || (depth0 != null ? depth0.priority : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"priority","hash":{},"data":data}) : helper))) - + "\" id=\"dict-" - + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) - + "\" class=\"form-control dict-priority\">\n </div>\n</div>\n"; -},"useData":true}); templates['kanji.html'] = template({"1":function(container,depth0,helpers,partials,data) { var stack1; @@ -60,19 +33,18 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia return "<div class=\"entry\" data-type=\"kanji\">\n <div class=\"actions\">\n" + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + " <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n <div class=\"glyph\">" + + " <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n <div class=\"glyph expression-scan-toggle\">" + container.escapeExpression(((helper = (helper = helpers.character || (depth0 != null ? depth0.character : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"character","hash":{},"data":data}) : helper))) + "</div>\n\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"if","hash":{},"fn":container.program(18, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"if","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n <table class=\"table table-condensed glyph-data\">\n <tr>\n <th>Glossary</th>\n <th>Readings</th>\n <th>Statistics</th>\n </tr>\n <tr>\n <td class=\"glossary\">\n" - + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(21, data, 0),"inverse":container.program(24, data, 0),"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(19, data, 0),"inverse":container.program(22, data, 0),"data":data})) != null ? stack1 : "") + " </td>\n <td class=\"reading\">\n " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"if","hash":{},"fn":container.program(24, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n " - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"if","hash":{},"fn":container.program(29, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.kunyomi : depth0),{"name":"if","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n </td>\n <td>" + ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1.misc : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") + "</td>\n </tr>\n <tr>\n <th colspan=\"3\">Classifications</th>\n </tr>\n <tr>\n <td colspan=\"3\">" @@ -82,19 +54,17 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia + "</td>\n </tr>\n <tr>\n <th colspan=\"3\">Dictionary Indices</th>\n </tr>\n <tr>\n <td colspan=\"3\">" + ((stack1 = container.invokePartial(partials.table,depth0,{"name":"table","hash":{"data":((stack1 = (depth0 != null ? depth0.stats : depth0)) != null ? stack1.index : stack1)},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") + "</td>\n </tr>\n </table>\n\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(31, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(29, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div>\n"; },"11":function(container,depth0,helpers,partials,data) { return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add Kanji (Alt + K)\" alt></a>\n"; },"13":function(container,depth0,helpers,partials,data) { - return " <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n"; -},"15":function(container,depth0,helpers,partials,data) { var stack1; return " <div>\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"16":function(container,depth0,helpers,partials,data) { +},"14":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; return " <span class=\"label label-default tag-frequency\">" @@ -102,13 +72,13 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia + ":" + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper))) + "</span>\n"; -},"18":function(container,depth0,helpers,partials,data) { +},"16":function(container,depth0,helpers,partials,data) { var stack1; return " <div>\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(19, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(17, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"19":function(container,depth0,helpers,partials,data) { +},"17":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; return " <span class=\"label label-default tag-" @@ -118,68 +88,81 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia + "\">" + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper))) + "</span>\n"; -},"21":function(container,depth0,helpers,partials,data) { +},"19":function(container,depth0,helpers,partials,data) { var stack1; return " <ol>" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(22, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(20, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</ol>\n"; -},"22":function(container,depth0,helpers,partials,data) { +},"20":function(container,depth0,helpers,partials,data) { return "<li><span class=\"glossary-item\">" + container.escapeExpression(container.lambda(depth0, depth0)) + "</span></li>"; -},"24":function(container,depth0,helpers,partials,data) { +},"22":function(container,depth0,helpers,partials,data) { var stack1; return " <span class=\"glossary-item\">" + container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)) + "</span>\n"; -},"26":function(container,depth0,helpers,partials,data) { +},"24":function(container,depth0,helpers,partials,data) { var stack1; return "<dl>" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(25, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</dl>"; -},"27":function(container,depth0,helpers,partials,data) { +},"25":function(container,depth0,helpers,partials,data) { return "<dd>" + container.escapeExpression(container.lambda(depth0, depth0)) + "</dd>"; -},"29":function(container,depth0,helpers,partials,data) { +},"27":function(container,depth0,helpers,partials,data) { var stack1; return "<dl>" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(27, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.kunyomi : depth0),{"name":"each","hash":{},"fn":container.program(25, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</dl>"; -},"31":function(container,depth0,helpers,partials,data) { +},"29":function(container,depth0,helpers,partials,data) { var stack1, helper, options, buffer = " <pre>"; - stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); + stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(30, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</pre>\n"; -},"32":function(container,depth0,helpers,partials,data) { +},"30":function(container,depth0,helpers,partials,data) { var stack1; return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : ""); -},"34":function(container,depth0,helpers,partials,data,blockParams,depths) { - var stack1; +},"32":function(container,depth0,helpers,partials,data,blockParams,depths) { + var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"35":function(container,depth0,helpers,partials,data,blockParams,depths) { + return "<div class=\"term-navigation\">\n <a href=\"#\" " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(33, data, 0, blockParams, depths),"inverse":container.program(35, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + + "><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n <a href=\"#\" " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.next : depth0),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.program(39, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + + "><img src=\"/mixed/img/source-term.svg\" style=\"transform: scaleX(-1);\" title=\"Next term (Alt + F)\" alt></a>\n</div>\n" + + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(41, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"33":function(container,depth0,helpers,partials,data) { + return "class=\"source-term\""; +},"35":function(container,depth0,helpers,partials,data) { + return "class=\"source-term term-button-fade\""; +},"37":function(container,depth0,helpers,partials,data) { + return "class=\"next-term\""; +},"39":function(container,depth0,helpers,partials,data) { + return "class=\"next-term term-button-fade\""; +},"41":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(36, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(42, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n" - + ((stack1 = container.invokePartial(partials.kanji,depth0,{"name":"kanji","hash":{"root":(depths[1] != null ? depths[1].root : depths[1]),"source":(depths[1] != null ? depths[1].source : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"36":function(container,depth0,helpers,partials,data) { + + ((stack1 = container.invokePartial(partials.kanji,depth0,{"name":"kanji","hash":{"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"42":function(container,depth0,helpers,partials,data) { return "<hr>"; -},"38":function(container,depth0,helpers,partials,data) { +},"44":function(container,depth0,helpers,partials,data) { return "<p class=\"note\">No results found</p>\n"; },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; return "\n\n" - + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.program(38, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); + + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.program(44, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); },"main_d": function(fn, props, container, depth0, data, blockParams, depths) { var decorators = container.decorators; @@ -190,23 +173,58 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia } ,"useDecorators":true,"usePartial":true,"useData":true,"useDepths":true}); -templates['model.html'] = template({"1":function(container,depth0,helpers,partials,data) { - return " <li><a class=\"marker-link\" href=\"#\">" +templates['query-parser.html'] = template({"1":function(container,depth0,helpers,partials,data) { + var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); + + return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.preview : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(4, data, 0),"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(alias1,depth0,{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "</span>"; +},"2":function(container,depth0,helpers,partials,data) { + return "<span class=\"query-parser-term-preview\">"; +},"4":function(container,depth0,helpers,partials,data) { + return "<span class=\"query-parser-term\">"; +},"6":function(container,depth0,helpers,partials,data) { + var stack1; + + return ((stack1 = container.invokePartial(partials.part,depth0,{"name":"part","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"8":function(container,depth0,helpers,partials,data) { + var stack1; + + return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.raw : depth0),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.program(12, data, 0),"data":data})) != null ? stack1 : ""); +},"9":function(container,depth0,helpers,partials,data) { + var stack1; + + return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"10":function(container,depth0,helpers,partials,data) { + return "<span class=\"query-parser-char\">" + container.escapeExpression(container.lambda(depth0, depth0)) - + "</a></li>\n"; -},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { - var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; + + "</span>"; +},"12":function(container,depth0,helpers,partials,data) { + var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - return "<tr>\n <td class=\"col-sm-2\">" - + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper))) - + "</td>\n <td class=\"col-sm-10\">\n <div class=\"input-group\">\n <input type=\"text\" class=\"anki-field-value form-control\" data-field=\"" - + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper))) - + "\" value=\"" - + alias4(((helper = (helper = helpers.value || (depth0 != null ? depth0.value : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"value","hash":{},"data":data}) : helper))) - + "\">\n <div class=\"input-group-btn\">\n <button type=\"button\" class=\"btn btn-default dropdown-toggle\" data-toggle=\"dropdown\">\n <span class=\"caret\"></span>\n </button>\n <ul class=\"dropdown-menu dropdown-menu-right\">\n" - + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.markers : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + " </ul>\n </div>\n </div>\n </td>\n</tr>\n"; -},"useData":true}); + return "<ruby>" + + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + "<rt>" + + container.escapeExpression(((helper = (helper = helpers.reading || (depth0 != null ? depth0.reading : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"reading","hash":{},"data":data}) : helper))) + + "</rt></ruby>"; +},"14":function(container,depth0,helpers,partials,data,blockParams,depths) { + var stack1; + + return ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"preview":(depths[1] != null ? depths[1].preview : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { + var stack1; + + return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.terms : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"main_d": function(fn, props, container, depth0, data, blockParams, depths) { + + var decorators = container.decorators; + + fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn; + fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(8, data, 0, blockParams, depths),"inverse":container.noop,"args":["part"],"data":data}) || fn; + return fn; + } + +,"useDecorators":true,"usePartial":true,"useData":true,"useDepths":true}); templates['terms.html'] = template({"1":function(container,depth0,helpers,partials,data) { var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer = "<div class=\"dict-"; @@ -298,17 +316,16 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia return "<div class=\"entry\" data-type=\"term\">\n <div class=\"actions\">\n" + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(25, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"unless","hash":{},"fn":container.program(27, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(30, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " <img src=\"/mixed/img/entry-current.svg\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n </div>\n\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.program(47, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(30, data, 0, blockParams, depths),"inverse":container.program(45, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + "\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(50, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"if","hash":{},"fn":container.program(48, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(54, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(52, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n <div class=\"glossary\">\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(63, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(55, data, 0, blockParams, depths),"inverse":container.program(61, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + " </div>\n\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(66, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.debug : depth0),{"name":"if","hash":{},"fn":container.program(64, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div>\n"; },"25":function(container,depth0,helpers,partials,data) { return " <a href=\"#\" class=\"action-view-note pending disabled\"><img src=\"/mixed/img/view-note.svg\" title=\"View added note (Alt + V)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.svg\" title=\"Add expression (Alt + E)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.svg\" title=\"Add reading (Alt + R)\" alt></a>\n"; @@ -318,47 +335,45 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(28, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); },"28":function(container,depth0,helpers,partials,data) { return " <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio (Alt + P)\" alt></a>\n"; -},"30":function(container,depth0,helpers,partials,data) { - return " <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n"; -},"32":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"30":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(33, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"33":function(container,depth0,helpers,partials,data,blockParams,depths) { + return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.expressions : depth0),{"name":"each","hash":{},"fn":container.program(31, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"31":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", buffer = - "<div class=\"expression\"><span class=\"expression-" + "<div class=\"expression expression-scan-toggle\"><span class=\"expression-" + container.escapeExpression(((helper = (helper = helpers.termFrequency || (depth0 != null ? depth0.termFrequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"termFrequency","hash":{},"data":data}) : helper))) + "\">"; - stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0, blockParams, depths),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper)); + stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : alias2),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0, blockParams, depths),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper)); if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</span><div class=\"peek-wrapper\">" - + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(39, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(42, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depths[1] != null ? depths[1].playback : depths[1]),{"name":"if","hash":{},"fn":container.program(35, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(37, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.frequencies : depth0),{"name":"if","hash":{},"fn":container.program(40, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div><span class=\"" - + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(45, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(43, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\">、</span></div>"; -},"34":function(container,depth0,helpers,partials,data) { +},"32":function(container,depth0,helpers,partials,data) { var stack1, helper, options; - stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); + stack1 = ((helper = (helper = helpers.furigana || (depth0 != null ? depth0.furigana : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"furigana","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); if (!helpers.furigana) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { return stack1; } else { return ''; } -},"35":function(container,depth0,helpers,partials,data) { +},"33":function(container,depth0,helpers,partials,data) { var stack1; return ((stack1 = container.lambda(depth0, depth0)) != null ? stack1 : ""); -},"37":function(container,depth0,helpers,partials,data) { +},"35":function(container,depth0,helpers,partials,data) { return "<a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.svg\" title=\"Play audio\" alt></a>"; -},"39":function(container,depth0,helpers,partials,data) { +},"37":function(container,depth0,helpers,partials,data) { var stack1; return "<div class=\"tags\">" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(40, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(38, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div>"; -},"40":function(container,depth0,helpers,partials,data) { +},"38":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; return " <span class=\"label label-default tag-" @@ -368,13 +383,13 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia + "\">" + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper))) + "</span>\n"; -},"42":function(container,depth0,helpers,partials,data) { +},"40":function(container,depth0,helpers,partials,data) { var stack1; return "<div class=\"frequencies\">" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(43, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(41, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "</div>"; -},"43":function(container,depth0,helpers,partials,data) { +},"41":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; return " <span class=\"label label-default tag-frequency\">" @@ -382,45 +397,45 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia + ":" + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper))) + "</span>\n"; -},"45":function(container,depth0,helpers,partials,data) { +},"43":function(container,depth0,helpers,partials,data) { return "invisible"; -},"47":function(container,depth0,helpers,partials,data) { +},"45":function(container,depth0,helpers,partials,data) { var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), buffer = - " <div class=\"expression\">"; - stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(34, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper)); + " <div class=\"expression expression-scan-toggle\">"; + stack1 = ((helper = (helper = helpers.kanjiLinks || (depth0 != null ? depth0.kanjiLinks : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"kanjiLinks","hash":{},"fn":container.program(32, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(alias1,options) : helper)); if (!helpers.kanjiLinks) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</div>\n" - + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(48, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"48":function(container,depth0,helpers,partials,data) { + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.termTags : depth0),{"name":"if","hash":{},"fn":container.program(46, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"46":function(container,depth0,helpers,partials,data) { var stack1; return " <div style=\"display: inline-block;\">\n" + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.termTags : depth0),{"name":"each","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"50":function(container,depth0,helpers,partials,data) { +},"48":function(container,depth0,helpers,partials,data) { var stack1; return " <div class=\"reasons\">\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(51, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(49, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"51":function(container,depth0,helpers,partials,data) { +},"49":function(container,depth0,helpers,partials,data) { var stack1; return " <span class=\"reasons\">" + container.escapeExpression(container.lambda(depth0, depth0)) + "</span> " - + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(52, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.last),{"name":"unless","hash":{},"fn":container.program(50, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n"; -},"52":function(container,depth0,helpers,partials,data) { +},"50":function(container,depth0,helpers,partials,data) { return "«"; -},"54":function(container,depth0,helpers,partials,data) { +},"52":function(container,depth0,helpers,partials,data) { var stack1; return " <div>\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(55, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.frequencies : depth0),{"name":"each","hash":{},"fn":container.program(53, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </div>\n"; -},"55":function(container,depth0,helpers,partials,data) { +},"53":function(container,depth0,helpers,partials,data) { var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; return " <span class=\"label label-default tag-frequency\">" @@ -428,61 +443,74 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia + ":" + alias4(((helper = (helper = helpers.frequency || (depth0 != null ? depth0.frequency : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"frequency","hash":{},"data":data}) : helper))) + "</span>\n"; -},"57":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"55":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(58, data, 0, blockParams, depths),"inverse":container.program(61, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); -},"58":function(container,depth0,helpers,partials,data,blockParams,depths) { + return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(56, data, 0, blockParams, depths),"inverse":container.program(59, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +},"56":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; return " <ol>\n" - + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(59, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + " </ol>\n"; -},"59":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"57":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; return " <li>" + ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") + "</li>\n"; -},"61":function(container,depth0,helpers,partials,data) { +},"59":function(container,depth0,helpers,partials,data) { var stack1; return ((stack1 = container.invokePartial(partials.definition,((stack1 = (depth0 != null ? depth0.definitions : depth0)) != null ? stack1["0"] : stack1),{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"63":function(container,depth0,helpers,partials,data,blockParams,depths) { +},"61":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; - return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(57, data, 0, blockParams, depths),"inverse":container.program(64, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); -},"64":function(container,depth0,helpers,partials,data) { + return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.merged : depth0),{"name":"if","hash":{},"fn":container.program(55, data, 0, blockParams, depths),"inverse":container.program(62, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); +},"62":function(container,depth0,helpers,partials,data) { var stack1; return ((stack1 = container.invokePartial(partials.definition,depth0,{"name":"definition","hash":{"compactGlossaries":(depth0 != null ? depth0.compactGlossaries : depth0)},"data":data,"indent":" ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") + " "; -},"66":function(container,depth0,helpers,partials,data) { +},"64":function(container,depth0,helpers,partials,data) { var stack1, helper, options, buffer = " <pre>"; - stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(35, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); + stack1 = ((helper = (helper = helpers.dumpObject || (depth0 != null ? depth0.dumpObject : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"dumpObject","hash":{},"fn":container.program(33, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),options) : helper)); if (!helpers.dumpObject) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)} if (stack1 != null) { buffer += stack1; } return buffer + "</pre>\n"; -},"68":function(container,depth0,helpers,partials,data,blockParams,depths) { - var stack1; - - return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(69, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); -},"69":function(container,depth0,helpers,partials,data,blockParams,depths) { - var stack1; +},"66":function(container,depth0,helpers,partials,data,blockParams,depths) { + var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {}); - return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(70, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + return "<div class=\"term-navigation\">\n <a href=\"#\" " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(67, data, 0, blockParams, depths),"inverse":container.program(69, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + + "><img src=\"/mixed/img/source-term.svg\" title=\"Source term (Alt + B)\" alt></a>\n <a href=\"#\" " + + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.next : depth0),{"name":"if","hash":{},"fn":container.program(71, data, 0, blockParams, depths),"inverse":container.program(73, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "") + + "><img src=\"/mixed/img/source-term.svg\" style=\"transform: scaleX(-1);\" title=\"Next term (Alt + F)\" alt></a>\n</div>\n" + + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(75, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"67":function(container,depth0,helpers,partials,data) { + return "class=\"source-term\""; +},"69":function(container,depth0,helpers,partials,data) { + return "class=\"source-term term-button-fade\""; +},"71":function(container,depth0,helpers,partials,data) { + return "class=\"next-term\""; +},"73":function(container,depth0,helpers,partials,data) { + return "class=\"next-term term-button-fade\""; +},"75":function(container,depth0,helpers,partials,data,blockParams,depths) { + var stack1; + + return ((stack1 = helpers.unless.call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.first),{"name":"unless","hash":{},"fn":container.program(76, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") + "\n" - + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"source":(depths[1] != null ? depths[1].source : depths[1]),"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1]),"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"merged":(depths[1] != null ? depths[1].merged : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); -},"70":function(container,depth0,helpers,partials,data) { + + ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"compactGlossaries":(depths[1] != null ? depths[1].compactGlossaries : depths[1]),"playback":(depths[1] != null ? depths[1].playback : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1]),"merged":(depths[1] != null ? depths[1].merged : depths[1]),"grouped":(depths[1] != null ? depths[1].grouped : depths[1]),"debug":(depths[1] != null ? depths[1].debug : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"76":function(container,depth0,helpers,partials,data) { return "<hr>"; -},"72":function(container,depth0,helpers,partials,data) { +},"78":function(container,depth0,helpers,partials,data) { return "<p class=\"note\">No results found.</p>\n"; },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) { var stack1; return "\n\n" - + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(68, data, 0, blockParams, depths),"inverse":container.program(72, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); + + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(66, data, 0, blockParams, depths),"inverse":container.program(78, data, 0, blockParams, depths),"data":data})) != null ? stack1 : ""); },"main_d": function(fn, props, container, depth0, data, blockParams, depths) { var decorators = container.decorators; diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 9d90136b..202014c9 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -42,27 +42,16 @@ class Translator { await this.database.purge(); } - async findTermsGrouped(text, dictionaries, alphanumeric, options) { - const titles = Object.keys(dictionaries); - const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); - - const definitionsGrouped = dictTermsGroup(definitions, dictionaries); - await this.buildTermFrequencies(definitionsGrouped, titles); - - if (options.general.compactTags) { - for (const definition of definitionsGrouped) { - dictTermsCompressTags(definition.definitions); - } - } - - return {length, definitions: definitionsGrouped}; + async deleteDictionary(dictionaryName) { + this.tagCache = {}; + await this.database.deleteDictionary(dictionaryName); } async getSequencedDefinitions(definitions, mainDictionary) { const definitionsBySequence = dictTermsMergeBySequence(definitions, mainDictionary); const defaultDefinitions = definitionsBySequence['-1']; - const sequenceList = Object.keys(definitionsBySequence).map(v => Number(v)).filter(v => v >= 0); + const sequenceList = Object.keys(definitionsBySequence).map((v) => Number(v)).filter((v) => v >= 0); const sequencedDefinitions = sequenceList.map((key) => ({ definitions: definitionsBySequence[key], rawDefinitions: [] @@ -135,7 +124,7 @@ class Translator { for (const expression of result.expressions.keys()) { for (const reading of result.expressions.get(expression).keys()) { const termTags = result.expressions.get(expression).get(reading); - const score = termTags.map(tag => tag.score).reduce((p, v) => p + v, 0); + const score = termTags.map((tag) => tag.score).reduce((p, v) => p + v, 0); expressions.push({ expression: expression, reading: reading, @@ -152,10 +141,41 @@ class Translator { return result; } - async findTermsMerged(text, dictionaries, alphanumeric, options) { - const secondarySearchTitles = Object.keys(options.dictionaries).filter(dict => options.dictionaries[dict].allowSecondarySearches); + async findTerms(text, details, options) { + switch (options.general.resultOutputMode) { + case 'group': + return await this.findTermsGrouped(text, details, options); + case 'merge': + return await this.findTermsMerged(text, details, options); + case 'split': + return await this.findTermsSplit(text, details, options); + default: + return [[], 0]; + } + } + + async findTermsGrouped(text, details, options) { + const dictionaries = dictEnabledSet(options); + const titles = Object.keys(dictionaries); + const [definitions, length] = await this.findTermsInternal(text, dictionaries, options.scanning.alphanumeric, details); + + const definitionsGrouped = dictTermsGroup(definitions, dictionaries); + await this.buildTermFrequencies(definitionsGrouped, titles); + + if (options.general.compactTags) { + for (const definition of definitionsGrouped) { + dictTermsCompressTags(definition.definitions); + } + } + + return [definitionsGrouped, length]; + } + + async findTermsMerged(text, details, options) { + const dictionaries = dictEnabledSet(options); + const secondarySearchTitles = Object.keys(options.dictionaries).filter((dict) => options.dictionaries[dict].allowSecondarySearches); const titles = Object.keys(dictionaries); - const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); + const [definitions, length] = await this.findTermsInternal(text, dictionaries, options.scanning.alphanumeric, details); const {sequencedDefinitions, defaultDefinitions} = await this.getSequencedDefinitions(definitions, options.general.mainDictionary); const definitionsMerged = []; const mergedByTermIndices = new Set(); @@ -186,29 +206,33 @@ class Translator { } } - return {length, definitions: dictTermsSort(definitionsMerged)}; + return [dictTermsSort(definitionsMerged), length]; } - async findTermsSplit(text, dictionaries, alphanumeric) { + async findTermsSplit(text, details, options) { + const dictionaries = dictEnabledSet(options); const titles = Object.keys(dictionaries); - const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); + const [definitions, length] = await this.findTermsInternal(text, dictionaries, options.scanning.alphanumeric, details); await this.buildTermFrequencies(definitions, titles); - return {length, definitions}; + return [definitions, length]; } - async findTerms(text, dictionaries, alphanumeric) { + async findTermsInternal(text, dictionaries, alphanumeric, details) { if (!alphanumeric && text.length > 0) { const c = text[0]; if (!jpIsKana(c) && !jpIsKanji(c)) { - return {length: 0, definitions: []}; + return [[], 0]; } } - const textHiragana = jpKatakanaToHiragana(text); const titles = Object.keys(dictionaries); - const deinflections = await this.findTermDeinflections(text, textHiragana, titles); + const deinflections = ( + details.wildcard ? + await this.findTermWildcard(text, titles) : + await this.findTermDeinflections(text, titles) + ); let definitions = []; for (const deinflection of deinflections) { @@ -241,10 +265,26 @@ class Translator { length = Math.max(length, definition.source.length); } - return {length, definitions}; + return [definitions, length]; } - async findTermDeinflections(text, text2, titles) { + async findTermWildcard(text, titles) { + const definitions = await this.database.findTermsBulk([text], titles, true); + if (definitions.length === 0) { + return []; + } + + return [{ + source: text, + term: text, + rules: 0, + definitions, + reasons: [] + }]; + } + + async findTermDeinflections(text, titles) { + const text2 = jpKatakanaToHiragana(text); const deinflections = (text === text2 ? this.getDeinflections(text) : this.getDeinflections2(text, text2)); if (deinflections.length === 0) { @@ -257,7 +297,7 @@ class Translator { for (const deinflection of deinflections) { const term = deinflection.term; let deinflectionArray; - if (uniqueDeinflectionsMap.hasOwnProperty(term)) { + if (hasOwn(uniqueDeinflectionsMap, term)) { deinflectionArray = uniqueDeinflectionsMap[term]; } else { deinflectionArray = []; @@ -268,7 +308,7 @@ class Translator { deinflectionArray.push(deinflection); } - const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles); + const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles, false); for (const definition of definitions) { const definitionRules = Deinflector.rulesToRuleFlags(definition.rules); @@ -280,41 +320,42 @@ class Translator { } } - return deinflections.filter(e => e.definitions.length > 0); + return deinflections.filter((e) => e.definitions.length > 0); } getDeinflections(text) { const deinflections = []; for (let i = text.length; i > 0; --i) { - const textSlice = text.slice(0, i); - deinflections.push(...this.deinflector.deinflect(textSlice)); + const textSubstring = text.substring(0, i); + deinflections.push(...this.deinflector.deinflect(textSubstring)); } return deinflections; } - getDeinflections2(text, text2) { + getDeinflections2(text1, text2) { const deinflections = []; - for (let i = text.length; i > 0; --i) { - const textSlice = text.slice(0, i); - const text2Slice = text2.slice(0, i); - deinflections.push(...this.deinflector.deinflect(textSlice)); - if (textSlice !== text2Slice) { - deinflections.push(...this.deinflector.deinflect(text2Slice)); + for (let i = text1.length; i > 0; --i) { + const text1Substring = text1.substring(0, i); + const text2Substring = text2.substring(0, i); + deinflections.push(...this.deinflector.deinflect(text1Substring)); + if (text1Substring !== text2Substring) { + deinflections.push(...this.deinflector.deinflect(text2Substring)); } } return deinflections; } - async findKanji(text, dictionaries) { + async findKanji(text, options) { + const dictionaries = dictEnabledSet(options); const titles = Object.keys(dictionaries); const kanjiUnique = {}; const kanjiList = []; for (const c of text) { - if (!kanjiUnique.hasOwnProperty(c)) { + if (!hasOwn(kanjiUnique, c)) { kanjiList.push(c); kanjiUnique[c] = true; } @@ -376,7 +417,7 @@ class Translator { const expression = term.expression; term.frequencies = []; - if (termsUniqueMap.hasOwnProperty(expression)) { + if (hasOwn(termsUniqueMap, expression)) { termsUniqueMap[expression].push(term); } else { const termList = [term]; @@ -423,7 +464,7 @@ class Translator { const category = meta.category; const group = ( - stats.hasOwnProperty(category) ? + hasOwn(stats, category) ? stats[category] : (stats[category] = []) ); @@ -443,7 +484,7 @@ class Translator { async getTagMetaList(names, title) { const tagMetaList = []; const cache = ( - this.tagCache.hasOwnProperty(title) ? + hasOwn(this.tagCache, title) ? this.tagCache[title] : (this.tagCache[title] = {}) ); @@ -451,7 +492,7 @@ class Translator { for (const name of names) { const base = Translator.getNameBase(name); - if (cache.hasOwnProperty(base)) { + if (hasOwn(cache, base)) { tagMetaList.push(cache[base]); } else { const tagMeta = await this.database.findTagForTitle(base, title); @@ -475,6 +516,6 @@ class Translator { static getNameBase(name) { const pos = name.indexOf(':'); - return (pos >= 0 ? name.substr(0, pos) : name); + return (pos >= 0 ? name.substring(0, pos) : name); } } diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 1ca0833b..3dd5fd55 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -16,12 +16,6 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -function utilAsync(func) { - return function(...args) { - func.apply(this, args); - }; -} - function utilIsolate(data) { return JSON.parse(JSON.stringify(data)); } @@ -47,13 +41,13 @@ function utilSetEqual(setA, setB) { function utilSetIntersection(setA, setB) { return new Set( - [...setA].filter(value => setB.has(value)) + [...setA].filter((value) => setB.has(value)) ); } function utilSetDifference(setA, setB) { return new Set( - [...setA].filter(value => !setB.has(value)) + [...setA].filter((value) => !setB.has(value)) ); } @@ -80,8 +74,12 @@ function utilAnkiGetDeckNames() { return utilBackend().anki.getDeckNames(); } -function utilDatabaseSummarize() { - return utilBackend().translator.database.summarize(); +function utilDatabaseGetDictionaryInfo() { + return utilBackend().translator.database.getDictionaryInfo(); +} + +function utilDatabaseGetDictionaryCounts(dictionaryNames, getTotal) { + return utilBackend().translator.database.getDictionaryCounts(dictionaryNames, getTotal); } function utilAnkiGetModelFieldNames(modelName) { @@ -92,6 +90,10 @@ function utilDatabasePurge() { return utilBackend().translator.purgeDatabase(); } +function utilDatabaseDeleteDictionary(dictionaryName, onProgress) { + return utilBackend().translator.database.deleteDictionary(dictionaryName, onProgress); +} + async function utilDatabaseImport(data, progress, exceptions) { // Edge cannot read data on the background page due to the File object // being created from a different window. Read on the same page instead. @@ -109,7 +111,3 @@ function utilReadFile(file) { reader.readAsBinaryString(file); }); } - -function utilIsObject(value) { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} diff --git a/ext/bg/legal.html b/ext/bg/legal.html index 30927da6..082239d7 100644 --- a/ext/bg/legal.html +++ b/ext/bg/legal.html @@ -41,12 +41,10 @@ and are used in conformance with the Group's <a href="https://www.edrdg.org/edrd <h3>Third-Party Software Licenses</h3> <ul> <li><a href="https://github.com/twbs/bootstrap/blob/v3.3.7/LICENSE" target="_blank" rel="noopener">Bootstrap v3.3.7</a></li> - <li><a href="https://github.com/minhur/bootstrap-toggle/blob/2.2.0/LICENSE" target="_blank" rel="noopener">Bootstrap Toggle v2.2.0</a></li> - <li><a href="https://github.com/dfahlander/Dexie.js/blob/v2.0.0-beta.10/LICENSE" target="_blank" rel="noopener">Dexie v2.0.0-beta.10</a></li> <li><a href="https://github.com/wycats/handlebars.js/blob/v4.0.6/LICENSE" target="_blank" rel="noopener">Handlebars v4.0.6</a></li> <li><a href="https://github.com/jquery/jquery/blob/3.2.1/LICENSE.txt" target="_blank" rel="noopener">jQuery v3.2.1</a></li> <li><a href="https://github.com/Stuk/jszip/blob/v3.1.3/LICENSE.markdown" target="_blank" rel="noopener">JSZip v3.1.3</a></li> - <li><a href="https://github.com/WaniKani/WanaKana/blob/2.2.3/LICENSE" target="_blank" rel="noopener">WanaKana v2.2.3</a></li> + <li><a href="https://github.com/WaniKani/WanaKana/blob/4.0.2/LICENSE" target="_blank" rel="noopener">WanaKana v4.0.2</a></li> </ul> </div> </div> diff --git a/ext/bg/search.html b/ext/bg/search.html index 91140b95..fef30456 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -25,18 +25,14 @@ <p style="margin-bottom: 0;">Search your installed dictionaries by entering a Japanese expression into the field below.</p> </div> - <div class="input-group" style="padding-top: 10px; font-size: 20px; user-select: none;"> + <div class="input-group" style="padding-top: 20px;"> <span title="Enable kana input method" class="input-group-text"> - <label> - あ - <input type="checkbox" id="wanakana-enable" /> - </label> + <input type="checkbox" id="wanakana-enable" class="icon-checkbox" /> + <label for="wanakana-enable" class="scan-disable">あ</label> </span> <span title="Enable clipboard monitor" class="input-group-text"> - <label> - <span class="glyphicon glyphicon-paste"></span> - <input type="checkbox" id="clipboard-monitor-enable" /> - </label> + <input type="checkbox" id="clipboard-monitor-enable" class="icon-checkbox" /> + <label for="clipboard-monitor-enable"><span class="glyphicon glyphicon-paste"></span></label> </span> </div> @@ -51,13 +47,21 @@ <img src="/mixed/img/spinner.gif"> </div> + <div class="scan-disable"> + <div id="query-parser-select" class="input-group"></div> + <div id="query-parser"></div> + </div> + + <hr> + <div id="content"></div> </div> <script src="/mixed/lib/handlebars.min.js"></script> <script src="/mixed/lib/wanakana.min.js"></script> - <script src="/mixed/js/extension.js"></script> + <script src="/mixed/js/core.js"></script> + <script src="/mixed/js/dom.js"></script> <script src="/bg/js/dictionary.js"></script> <script src="/bg/js/handlebars.js"></script> @@ -67,10 +71,12 @@ <script src="/fg/js/source.js"></script> <script src="/fg/js/util.js"></script> <script src="/mixed/js/audio.js"></script> + <script src="/mixed/js/display-context.js"></script> <script src="/mixed/js/display.js"></script> <script src="/mixed/js/japanese.js"></script> <script src="/mixed/js/scroll.js"></script> + <script src="/bg/js/search-query-parser.js"></script> <script src="/bg/js/search.js"></script> <script src="/bg/js/search-frontend.js"></script> </body> diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index d27a9a33..339467d4 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -117,7 +117,9 @@ </div> </div></div></div> - <script src="/mixed/js/extension.js"></script> + <script src="/mixed/js/core.js"></script> + <script src="/mixed/js/dom.js"></script> + <script src="/fg/js/api.js"></script> <script src="/fg/js/document.js"></script> <script src="/fg/js/frontend-api-receiver.js"></script> @@ -126,6 +128,6 @@ <script src="/fg/js/util.js"></script> <script src="/fg/js/popup-proxy-host.js"></script> <script src="/fg/js/frontend.js"></script> - <script src="/bg/js/settings-popup-preview.js"></script> + <script src="/bg/js/settings/popup-preview-frame.js"></script> </body> </html> diff --git a/ext/bg/settings.html b/ext/bg/settings.html index a3b75576..3c5494b8 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -272,7 +272,7 @@ <div class="form-group ignore-form-changes" style="display: none;" id="settings-popup-preview-settings"> <label for="settings-popup-preview-text">Popup preview text</label> - <input type="text" id="settings-popup-preview-text" class="form-control" value="読め"> + <input type="text" id="settings-popup-preview-text" class="form-control" value="読め" placeholder="Preview text"> </div> <div class="form-group ignore-form-changes"> @@ -401,7 +401,7 @@ </div> <div class="checkbox"> - <label><input type="checkbox" id="enable-scanning-of-popup-expressions"> Enable scanning of popup expressions</label> + <label><input type="checkbox" id="enable-scanning-of-popup-expressions"> Enable scanning of expressions in search results</label> </div> <div class="form-group"> @@ -410,18 +410,52 @@ </div> </div> - <div> + <div id="text-parsing"> + <h3>Text Parsing Options</h3> + + <p class="help-block"> + Yomichan can attempt to parse entire sentences or longer text blocks on the search page, + adding furigana above words and a small space between words. + </p> + + <p class="help-block"> + Two types of parsers are supported. The first one, enabled by default, works using the built-in + scanning functionality by automatically advancing in the sentence after a matching word. + </p> + + <p class="help-block"> + The second type is an external program called <a href="https://en.wikipedia.org/wiki/MeCab" target="_blank" rel="noopener">MeCab</a> + that uses its own dictionaries and a special parsing algorithm. To get it working, you must first + install it and <a href="https://github.com/siikamiika/yomichan-mecab-installer" target="_blank" rel="noopener">a native messaging component</a> + that acts as a bridge between the program and Yomichan. + </p> + + <div class="checkbox"> + <label><input type="checkbox" id="parsing-scan-enable"> Enable text parsing using installed dictionaries</label> + </div> + + <div class="checkbox"> + <label><input type="checkbox" id="parsing-mecab-enable"> Enable text parsing using MeCab</label> + </div> + + <div class="form-group"> + <label for="parsing-reading-mode">Reading mode</label> + <select class="form-control" id="parsing-reading-mode"> + <option value="hiragana">ひらがな</option> + <option value="katakana">カタカナ</option> + <option value="romaji">Romaji</option> + </select> + </div> + </div> + + <div class="ignore-form-changes"> <div> <img src="/mixed/img/spinner.gif" class="pull-right" id="dict-spinner" alt> <h3>Dictionaries</h3> </div> <p class="help-block"> - Yomichan can import and use a variety of dictionary formats. Unneeded dictionaries can be disabled, - or you can simply <a href="#" id="dict-purge-link">purge the database</a> to delete everything. - </p> - <p class="help-block"> - Deleting individual dictionaries is not currently feasible due to limitations of browser database technology. + Yomichan can import and use a variety of dictionary formats. Unneeded dictionaries can be disabled. </p> <div class="form-group" id="dict-main-group"> @@ -429,14 +463,16 @@ <select class="form-control" id="dict-main"></select> </div> - <div class="text-danger" id="dict-purge">Dictionary data is being purged, please be patient...</div> - <div class="alert alert-warning" id="dict-warning">No dictionaries have been installed</div> - <div class="alert alert-danger" id="dict-error"></div> + <div class="text-danger" id="dict-purge" hidden>Dictionary data is being purged, please be patient...</div> + <div class="alert alert-warning" id="dict-warning" hidden>No dictionaries have been installed</div> + <div class="alert alert-danger" id="dict-error" hidden></div> <div id="dict-groups"></div> + <div id="dict-groups-extra"></div> <div id="dict-import-progress"> Dictionary data is being imported, please be patient... + <span id="dict-import-info" hidden></span> <div class="progress"> <div class="progress-bar progress-bar-striped" style="width: 0%"></div> </div> @@ -448,9 +484,85 @@ <a href="https://foosoft.net/projects/yomichan" target="_blank" rel="noopener">download free dictionaries</a> for use with this extension and to learn about importing proprietary EPWING dictionaries. </p> - <button class="btn btn-primary" id="dict-file-button">Import Dictionary</button> - <div hidden><input type="file" id="dict-file"></div> + <div> + <button class="btn btn-primary" id="dict-file-button">Import Dictionary</button> + <button class="btn btn-danger" id="dict-purge-button">Purge Database</button> + </div> + <div hidden><input type="file" id="dict-file" accept=".zip,application/zip" multiple></div> + </div> + + <div class="modal fade" tabindex="-1" role="dialog" id="dict-purge-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 database purge</h4> + </div> + <div class="modal-body"> + Are you sure you want to delete all data in the database? + </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="dict-purge-confirm">Purge Database</button> + </div> + </div> + </div> + </div> + + <div class="modal fade" tabindex="-1" role="dialog" id="dict-delete-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 dictionary deletion</h4> + </div> + <div class="modal-body"> + Are you sure you want to delete the dictionary <em id="dict-remove-modal-dict-name"></em>? + This operation may take some time and the responsiveness of this browser tab may be reduced. + </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="dict-delete-confirm">Delete Dictionary</button> + </div> + </div> + </div> </div> + + <template id="dict-template"><div class="dict-group well well-sm"> + <h4><span class="text-muted glyphicon glyphicon-book"></span> <span class="dict-title"></span> <small class="dict-revision"></small></h4> + <p class="text-warning dict-outdated" hidden>This dictionary is outdated and may not support new extension features; please import the latest version.</p> + + <div class="checkbox"> + <label><input type="checkbox" class="dict-enabled"> Enable search</label> + </div> + <div class="checkbox options-advanced"> + <label><input type="checkbox" class="dict-allow-secondary-searches"> Allow secondary searches</label> + </div> + <div class="form-group options-advanced"> + <label class="dict-result-priority-label">Result priority</label> + <input type="number" class="form-control dict-priority"> + </div> + <div class="dict-delete-table"> + <div> + <button class="btn btn-default dict-delete-button">Delete Dictionary</button> + </div> + <div> + <div class="progress" hidden> + <div class="progress-bar progress-bar-striped" style="width: 0%"></div> + </div> + </div> + </div> + <pre class="debug dict-counts" hidden></pre> + </div></template> + + <template id="dict-extra-template"><div class="well well-sm"> + <h4><span class="text-muted glyphicon glyphicon-alert"></span> <span class="dict-title">Unassociated Data</span> <small class="dict-total-count"></small></h4> + <p class="text-warning"> + The database contains extra data which is not associated with any installed dictionary. + Purging the database can fix this issue. + </p> + <pre class="debug dict-counts" hidden></pre> + </div></template> </div> <div id="storage-info"> @@ -538,7 +650,7 @@ </div> </div> - <div class="alert alert-danger" id="anki-error"></div> + <div class="alert alert-danger" id="anki-error" hidden></div> <div class="form-group"> <label for="card-tags">Card tags <span class="label-light">(comma or space separated)</span></label> @@ -582,16 +694,16 @@ </ul> <div class="tab-content"> - <div id="terms" class="tab-pane fade in active"> + <div id="terms" class="tab-pane fade in active" data-anki-card-type="terms"> <div class="row"> <div class="form-group col-xs-6"> <label for="anki-terms-deck">Deck</label> - <select class="form-control anki-deck" id="anki-terms-deck"></select> + <select class="form-control anki-deck" id="anki-terms-deck" data-anki-card-type="terms"></select> </div> <div class="form-group col-xs-6"> <label for="anki-terms-model">Model</label> - <select class="form-control anki-model" id="anki-terms-model"></select> + <select class="form-control anki-model" id="anki-terms-model" data-anki-card-type="terms"></select> </div> </div> @@ -601,16 +713,16 @@ </table> </div> - <div id="kanji" class="tab-pane fade"> + <div id="kanji" class="tab-pane fade" data-anki-card-type="kanji"> <div class="row"> <div class="form-group col-xs-6"> <label for="anki-kanji-deck">Deck</label> - <select class="form-control anki-deck" id="anki-kanji-deck"></select> + <select class="form-control anki-deck" id="anki-kanji-deck" data-anki-card-type="kanji"></select> </div> <div class="form-group col-xs-6"> <label for="anki-kanji-model">Model</label> - <select class="form-control anki-model" id="anki-kanji-model"></select> + <select class="form-control anki-model" id="anki-kanji-model" data-anki-card-type="kanji"></select> </div> </div> @@ -625,11 +737,78 @@ <p class="help-block"> Fields are formatted using the <a href="https://handlebarsjs.com/" target="_blank" rel="noopener">Handlebars.js</a> template rendering engine. Advanced users can modify these templates for ultimate control of what information gets included in - their Anki cards. If you encounter problems with your changes you can always <a href="#" id="field-templates-reset">reset to default</a> - template settings. + their Anki cards. If you encounter problems with your changes, you can always reset to the default template settings. </p> <textarea autocomplete="off" spellcheck="false" wrap="soft" class="form-control" rows="10" id="field-templates"></textarea> + <div> + <button class="btn btn-danger" id="field-templates-reset">Reset Templates</button> + </div> + <p></p> + <pre id="field-template-compile-result" hidden></pre> + + <p>Templates can be tested using the inputs below.</p> + + <div class="form-group"> + <div class="row"> + <div class="col-xs-6"> + <label for="field-templates-preview-text">Preview text</label> + <input type="text" id="field-templates-preview-text" class="form-control" value="読め" placeholder="Preview text"> + </div> + <div class="col-xs-6"> + <label for="field-template-render-text">Test field</label> + <div class="input-group"> + <div class="input-group-btn"> + <button class="btn btn-default" id="field-template-render" title="Test"><span class="glyphicon glyphicon-play"></span></button> + </div> + <input type="text" class="form-control" id="field-template-render-text" value="{expression}" placeholder="{marker}"> + <div class="input-group-btn"> + <button class="btn btn-default dropdown-toggle" id="field-templates-dropdown" data-toggle="dropdown"><span class="caret"></span></button> + <ul class="dropdown-menu dropdown-menu-right" id="field-templates-list"></ul> + </div> + </div> + </div> + </div> + </div> + + <p></p> + <pre id="field-template-render-result" hidden></pre> + </div> + + <div class="modal fade" tabindex="-1" role="dialog" id="field-template-reset-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 template reset</h4> + </div> + <div class="modal-body"> + Are you sure you want to reset the field templates to the default value? + Any changes you made will be lost. + </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="field-templates-reset-confirm">Reset Templates</button> + </div> + </div> + </div> </div> + + <template id="anki-field-template"><tr> + <td class="col-sm-2 anki-field-name"></td> + <td class="col-sm-10"> + <div class="input-group"> + <input type="text" class="anki-field-value form-control" data-field="" value=""> + <div class="input-group-btn"> + <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"> + <span class="caret"></span> + </button> + <ul class="dropdown-menu dropdown-menu-right anki-field-marker-list"></ul> + </div> + </div> + </td> + </tr></template> + + <template id="anki-field-marker-template"><li><a class="marker-link" href="#"></a></li></template> </div> </div> </div> @@ -673,7 +852,7 @@ </p> </div> - <pre id="debug"></pre> + <pre id="debug" class="debug"></pre> <div class="pull-right bottom-links"> <small><span id="extension-info"></span> • <a href="search.html">Search</a> • <a href="https://foosoft.net/projects/yomichan/" target="_blank" rel="noopener">Homepage</a> • <a href="legal.html">Legal</a></small> @@ -685,7 +864,9 @@ <script src="/mixed/lib/handlebars.min.js"></script> <script src="/mixed/lib/wanakana.min.js"></script> - <script src="/mixed/js/extension.js"></script> + <script src="/mixed/js/core.js"></script> + <script src="/mixed/js/dom.js"></script> + <script src="/mixed/js/japanese.js"></script> <script src="/bg/js/anki.js"></script> <script src="/bg/js/api.js"></script> @@ -695,12 +876,20 @@ <script src="/bg/js/dictionary.js"></script> <script src="/bg/js/handlebars.js"></script> <script src="/bg/js/options.js"></script> + <script src="/bg/js/page-exit-prevention.js"></script> <script src="/bg/js/profile-conditions.js"></script> <script src="/bg/js/templates.js"></script> <script src="/bg/js/util.js"></script> <script src="/mixed/js/audio.js"></script> - <script src="/bg/js/settings-profiles.js"></script> - <script src="/bg/js/settings.js"></script> + <script src="/bg/js/settings/anki.js"></script> + <script src="/bg/js/settings/anki-templates.js"></script> + <script src="/bg/js/settings/audio.js"></script> + <script src="/bg/js/settings/dictionaries.js"></script> + <script src="/bg/js/settings/popup-preview.js"></script> + <script src="/bg/js/settings/profiles.js"></script> + <script src="/bg/js/settings/storage.js"></script> + + <script src="/bg/js/settings/main.js"></script> </body> </html> |