diff options
-rw-r--r-- | ext/bg/background.html | 29 | ||||
-rw-r--r-- | ext/bg/js/anki-connect.js (renamed from ext/bg/js/ankiconnect.js) | 1 | ||||
-rw-r--r-- | ext/bg/js/anki-null.js (renamed from ext/bg/js/ankinull.js) | 1 | ||||
-rw-r--r-- | ext/bg/js/database.js | 14 | ||||
-rw-r--r-- | ext/bg/js/gecko.js | 1 | ||||
-rw-r--r-- | ext/bg/js/options-form.js | 366 | ||||
-rw-r--r-- | ext/bg/js/options.js | 451 | ||||
-rw-r--r-- | ext/bg/js/popup.js | 3 | ||||
-rw-r--r-- | ext/bg/js/translator.js | 24 | ||||
-rw-r--r-- | ext/bg/js/util.js | 268 | ||||
-rw-r--r-- | ext/bg/js/yomichan.js | 8 | ||||
-rw-r--r-- | ext/bg/options.html | 1 |
12 files changed, 591 insertions, 576 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html index 625917ef..eecadf1e 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -1,18 +1,17 @@ <!DOCTYPE html> <html lang="en"> -<body> - <script src="../lib/handlebars.min.js"></script> - <script src="../lib/dexie.min.js"></script> - <script src="../lib/wanakana.min.js"></script> - <script src="js/gecko.js"></script> - <script src="js/ankiconnect.js"></script> - <script src="js/ankinull.js"></script> - <script src="js/templates.js"></script> - <script src="js/util.js"></script> - <script src="js/database.js"></script> - <script src="js/deinflector.js"></script> - <script src="js/translator.js"></script> - <script src="js/options.js"></script> - <script src="js/yomichan.js"></script> -</body> + <body> + <script src="../lib/handlebars.min.js"></script> + <script src="../lib/dexie.min.js"></script> + <script src="../lib/wanakana.min.js"></script> + <script src="js/templates.js"></script> + <script src="js/gecko.js"></script> + <script src="js/util.js"></script> + <script src="js/anki-connect.js"></script> + <script src="js/anki-null.js"></script> + <script src="js/database.js"></script> + <script src="js/deinflector.js"></script> + <script src="js/translator.js"></script> + <script src="js/yomichan.js"></script> + </body> </html> diff --git a/ext/bg/js/ankiconnect.js b/ext/bg/js/anki-connect.js index 3a6e3690..9759c8f5 100644 --- a/ext/bg/js/ankiconnect.js +++ b/ext/bg/js/anki-connect.js @@ -16,6 +16,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ + class AnkiConnect { constructor(server) { this.server = server; diff --git a/ext/bg/js/ankinull.js b/ext/bg/js/anki-null.js index 0d0ed903..99dc2f30 100644 --- a/ext/bg/js/ankinull.js +++ b/ext/bg/js/anki-null.js @@ -16,6 +16,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ + class AnkiNull { addNote(note) { return Promise.reject('unsupported action'); diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 31573065..51f639d9 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -76,8 +76,8 @@ class Database { results.push({ expression: row.expression, reading: row.reading, - tags: splitField(row.tags), - rules: splitField(row.rules), + tags: dictFieldSplit(row.tags), + rules: dictFieldSplit(row.rules), glossary: row.glossary, score: row.score, dictionary: row.dictionary, @@ -105,9 +105,9 @@ class Database { if (dictionaries.includes(row.dictionary)) { results.push({ character: row.character, - onyomi: splitField(row.onyomi), - kunyomi: splitField(row.kunyomi), - tags: splitField(row.tags), + onyomi: dictFieldSplit(row.onyomi), + kunyomi: dictFieldSplit(row.kunyomi), + tags: dictFieldSplit(row.tags), glossary: row.meanings, dictionary: row.dictionary }); @@ -172,7 +172,7 @@ class Database { const rows = []; for (const tag in tagMeta || {}) { const meta = tagMeta[tag]; - const row = sanitizeTag({ + const row = dictTagSanitize({ name: tag, category: meta.category, notes: meta.notes, @@ -229,6 +229,6 @@ class Database { }); }; - return importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded).then(() => summary); + return jsonLoadDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded).then(() => summary); } } diff --git a/ext/bg/js/gecko.js b/ext/bg/js/gecko.js index f195fffc..3df07f0b 100644 --- a/ext/bg/js/gecko.js +++ b/ext/bg/js/gecko.js @@ -20,6 +20,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + if (!chrome.storage.sync) { // https://bugzilla.mozilla.org/show_bug.cgi?id=1220494 chrome.storage.sync = chrome.storage.local; diff --git a/ext/bg/js/options-form.js b/ext/bg/js/options-form.js deleted file mode 100644 index c53c0c5e..00000000 --- a/ext/bg/js/options-form.js +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright (C) 2016 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/>. - */ - -// -// General -// - -function getFormData() { - return optionsLoad().then(optionsOld => { - const optionsNew = $.extend(true, {}, optionsOld); - - optionsNew.general.audioPlayback = $('#audio-playback-buttons').prop('checked'); - optionsNew.general.groupResults = $('#group-terms-results').prop('checked'); - optionsNew.general.softKatakana = $('#soft-katakana-search').prop('checked'); - optionsNew.general.showAdvanced = $('#show-advanced-options').prop('checked'); - optionsNew.general.maxResults = parseInt($('#max-displayed-results').val(), 10); - - optionsNew.scanning.requireShift = $('#hold-shift-to-scan').prop('checked'); - optionsNew.scanning.selectText = $('#select-matched-text').prop('checked'); - optionsNew.scanning.imposter = $('#search-form-text-fields').prop('checked'); - optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10); - optionsNew.scanning.length = parseInt($('#scan-length').val(), 10); - - optionsNew.anki.enable = $('#anki-enable').prop('checked'); - optionsNew.anki.tags = $('#card-tags').val().split(/[,; ]+/); - optionsNew.anki.htmlCards = $('#generate-html-cards').prop('checked'); - optionsNew.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); - optionsNew.anki.server = $('#interface-server').val(); - if (optionsOld.anki.enable) { - optionsNew.anki.terms.deck = $('#anki-terms-deck').val(); - optionsNew.anki.terms.model = $('#anki-terms-model').val(); - optionsNew.anki.terms.fields = ankiFieldsToDict($('#terms .anki-field-value')); - optionsNew.anki.kanji.deck = $('#anki-kanji-deck').val(); - optionsNew.anki.kanji.model = $('#anki-kanji-model').val(); - optionsNew.anki.kanji.fields = ankiFieldsToDict($('#kanji .anki-field-value')); - } - - $('.dict-group').each((index, element) => { - const dictionary = $(element); - const title = dictionary.data('title'); - const priority = parseInt(dictionary.find('.dict-priority').val(), 10); - const enabled = dictionary.find('.dict-enabled').prop('checked'); - optionsNew.dictionaries[title] = {priority, enabled}; - }); - - return {optionsNew, optionsOld}; - }); -} - -function updateVisibility(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(); - } -} - -$(document).ready(() => { - Handlebars.partials = Handlebars.templates; - - optionsLoad().then(options => { - $('#audio-playback-buttons').prop('checked', options.general.audioPlayback); - $('#group-terms-results').prop('checked', options.general.groupResults); - $('#soft-katakana-search').prop('checked', options.general.softKatakana); - $('#show-advanced-options').prop('checked', options.general.showAdvanced); - $('#max-displayed-results').val(options.general.maxResults); - - $('#hold-shift-to-scan').prop('checked', options.scanning.requireShift); - $('#select-matched-text').prop('checked', options.scanning.selectText); - $('#search-form-text-fields').prop('checked', options.scanning.imposter); - $('#scan-delay').val(options.scanning.delay); - $('#scan-length').val(options.scanning.length); - - $('#dict-purge').click(onDictionaryPurge); - $('#dict-importer a').click(onDictionarySetUrl); - $('#dict-import').click(onDictionaryImport); - $('#dict-url').on('input', onDictionaryUpdateUrl); - - $('#anki-enable').prop('checked', options.anki.enable); - $('#card-tags').val(options.anki.tags.join(' ')); - $('#generate-html-cards').prop('checked', options.anki.htmlCards); - $('#sentence-detection-extent').val(options.anki.sentenceExt); - $('#interface-server').val(options.anki.server); - $('input, select').not('.anki-model').change(onOptionsChanged); - $('.anki-model').change(onAnkiModelChanged); - - populateDictionaries(options); - populateAnkiDeckAndModel(options); - updateVisibility(options); - }); -}); - -// -// Dictionary -// - -function showDictionaryError(error) { - const dialog = $('#dict-error'); - if (error) { - dialog.show().find('span').text(error); - } else { - dialog.hide(); - } -} - -function showDictionarySpinner(show) { - const spinner = $('#dict-spinner'); - if (show) { - spinner.show(); - } else { - spinner.hide(); - } -} - -function populateDictionaries(options) { - showDictionaryError(null); - showDictionarySpinner(true); - - const dictGroups = $('#dict-groups').empty(); - const dictWarning = $('#dict-warning').hide(); - - let dictCount = 0; - return getDatabase().getDictionaries().then(rows => { - rows.forEach(row => { - const dictOptions = options.dictionaries[row.title] || {enableTerms: false, enableKanji: false, priority: 0}; - const html = Handlebars.templates['dictionary.html']({ - title: row.title, - version: row.version, - revision: row.revision, - priority: dictOptions.priority, - enabled: dictOptions.enabled - }); - - dictGroups.append($(html)); - ++dictCount; - }); - - updateVisibility(options); - - $('.dict-enabled, .dict-priority').change(onOptionsChanged); - }).catch(showDictionaryError).then(() => { - showDictionarySpinner(false); - if (dictCount === 0) { - dictWarning.show(); - } - }); -} - -function onDictionaryPurge(e) { - e.preventDefault(); - - showDictionaryError(null); - showDictionarySpinner(true); - - const dictControls = $('#dict-importer, #dict-groups').hide(); - const dictProgress = $('#dict-purge-progress').show(); - - return getDatabase().purge().catch(showDictionaryError).then(() => { - showDictionarySpinner(false); - dictControls.show(); - dictProgress.hide(); - return optionsLoad(); - }).then(options => { - options.dictionaries = {}; - return optionsSave(options).then(() => { - populateDictionaries(options); - getYomichan().setOptions(options); - }); - }); -} - -function onDictionaryImport() { - showDictionaryError(null); - showDictionarySpinner(true); - - const dictUrl = $('#dict-url'); - const dictImporter = $('#dict-importer').hide(); - const dictProgress = $('#dict-import-progress').show(); - const setProgress = percent => dictProgress.find('.progress-bar').css('width', `${percent}%`); - - setProgress(0.0); - - optionsLoad().then(options => { - getDatabase().importDictionary(dictUrl.val(), (total, current) => setProgress(current / total * 100.0)).then(summary => { - options.dictionaries[summary.title] = {enabled: true, priority: 0}; - return optionsSave(options).then(() => getYomichan().setOptions(options)); - }).then(() => populateDictionaries(options)).catch(showDictionaryError).then(() => { - showDictionarySpinner(false); - dictProgress.hide(); - dictImporter.show(); - dictUrl.val(''); - dictUrl.trigger('input'); - }); - }); -} - -function onDictionarySetUrl(e) { - e.preventDefault(); - - const dictUrl = $('#dict-url'); - const url = $(this).data('url'); - if (url.includes('/')) { - dictUrl.val(url); - } else { - dictUrl.val(chrome.extension.getURL(`bg/lang/data/${url}/index.json`)); - } - - dictUrl.trigger('input'); -} - -function onDictionaryUpdateUrl() { - $('#dict-import').prop('disabled', $(this).val().length === 0); -} - -// -// Anki -// - -function showAnkiSpinner(show) { - const spinner = $('#anki-spinner'); - if (show) { - spinner.show(); - } else { - spinner.hide(); - } -} - -function showAnkiError(error) { - const dialog = $('#anki-error'); - if (error) { - dialog.show().find('span').text(error); - } - else { - dialog.hide(); - } -} - -function ankiFieldsToDict(selection) { - const result = {}; - selection.each((index, element) => { - result[$(element).data('field')] = $(element).val(); - }); - - return result; -} - -function populateAnkiDeckAndModel(options) { - showAnkiError(null); - showAnkiSpinner(true); - - const ankiFormat = $('#anki-format').hide(); - return Promise.all([getAnki().getDeckNames(), getAnki().getModelNames()]).then(([deckNames, modelNames]) => { - const ankiDeck = $('.anki-deck'); - ankiDeck.find('option').remove(); - deckNames.sort().forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); - - $('#anki-terms-deck').val(options.anki.terms.deck); - $('#anki-kanji-deck').val(options.anki.kanji.deck); - - const ankiModel = $('.anki-model'); - ankiModel.find('option').remove(); - modelNames.sort().forEach(name => ankiModel.append($('<option/>', {value: name, text: name}))); - - return Promise.all([ - populateAnkiFields($('#anki-terms-model').val(options.anki.terms.model), options), - populateAnkiFields($('#anki-kanji-model').val(options.anki.kanji.model), options) - ]); - }).then(() => ankiFormat.show()).catch(showAnkiError).then(() => showAnkiSpinner(false)); -} - -function populateAnkiFields(element, options) { - const tab = element.closest('.tab-pane'); - const tabId = tab.attr('id'); - const container = tab.find('tbody').empty(); - - const modelName = element.val(); - if (modelName === null) { - return Promise.resolve(); - } - - const markers = { - 'terms': ['audio', 'dictionary', 'expression', 'furigana', 'glossary', 'reading', 'sentence', 'tags', 'url'], - 'kanji': ['character', 'dictionary', 'glossary', 'kunyomi', 'onyomi', 'sentence', 'tags', 'url'] - }[tabId] || {}; - - return getAnki().getModelFieldNames(modelName).then(names => { - names.forEach(name => { - 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(onOptionsChanged); - tab.find('.marker-link').click(e => { - e.preventDefault(); - const link = e.target; - $(link).closest('.input-group').find('.anki-field-value').val(`{${link.text}}`).trigger('change'); - }); - }); -} - -function onAnkiModelChanged(e) { - if (!e.originalEvent) { - return; - } - - showAnkiError(null); - showAnkiSpinner(true); - - const element = $(this); - getFormData().then(({optionsNew, optionsOld}) => { - const tab = element.closest('.tab-pane'); - const tabId = tab.attr('id'); - - optionsNew.anki[tabId].fields = {}; - populateAnkiFields(element, optionsNew).then(() => { - optionsSave(optionsNew).then(() => getYomichan().setOptions(optionsNew)); - }).catch(showAnkiError).then(() => showAnkiSpinner(false)); - }); -} - -function onOptionsChanged(e) { - if (!e.originalEvent && !e.isTrigger) { - return; - } - - getFormData().then(({optionsNew, optionsOld}) => { - return optionsSave(optionsNew).then(() => { - getYomichan().setOptions(optionsNew); - updateVisibility(optionsNew); - - const ankiUpdated = - optionsNew.anki.enable !== optionsOld.anki.enable || - optionsNew.anki.server !== optionsOld.anki.server; - - if (ankiUpdated) { - showAnkiError(null); - showAnkiSpinner(true); - return populateAnkiDeckAndModel(optionsNew); - } - }); - }).catch(showAnkiError).then(() => showAnkiSpinner(false)); -} diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 198deb6d..565ccd41 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -17,143 +17,352 @@ */ -function optionsSetDefaults(options) { - const defaults = { - general: { - enable: true, - audioPlayback: true, - groupResults: true, - softKatakana: true, - maxResults: 32, - showAdvanced: false - }, - - scanning: { - requireShift: true, - selectText: true, - imposter: true, - delay: 15, - length: 10 - }, - - dictionaries: {}, - - anki: { - enable: false, - server: 'http://127.0.0.1:8765', - tags: ['yomichan'], - htmlCards: true, - sentenceExt: 200, - terms: {deck: '', model: '', fields: {}}, - kanji: {deck: '', model: '', fields: {}} - } - }; +/* + * General + */ - const combine = (target, source) => { - for (const key in source) { - if (!target.hasOwnProperty(key)) { - target[key] = source[key]; - } +function getFormData() { + return optionsLoad().then(optionsOld => { + const optionsNew = $.extend(true, {}, optionsOld); + + optionsNew.general.audioPlayback = $('#audio-playback-buttons').prop('checked'); + optionsNew.general.groupResults = $('#group-terms-results').prop('checked'); + optionsNew.general.softKatakana = $('#soft-katakana-search').prop('checked'); + optionsNew.general.showAdvanced = $('#show-advanced-options').prop('checked'); + optionsNew.general.maxResults = parseInt($('#max-displayed-results').val(), 10); + + optionsNew.scanning.requireShift = $('#hold-shift-to-scan').prop('checked'); + optionsNew.scanning.selectText = $('#select-matched-text').prop('checked'); + optionsNew.scanning.imposter = $('#search-form-text-fields').prop('checked'); + optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10); + optionsNew.scanning.length = parseInt($('#scan-length').val(), 10); + + optionsNew.anki.enable = $('#anki-enable').prop('checked'); + optionsNew.anki.tags = $('#card-tags').val().split(/[,; ]+/); + optionsNew.anki.htmlCards = $('#generate-html-cards').prop('checked'); + optionsNew.anki.sentenceExt = parseInt($('#sentence-detection-extent').val(), 10); + optionsNew.anki.server = $('#interface-server').val(); + if (optionsOld.anki.enable) { + optionsNew.anki.terms.deck = $('#anki-terms-deck').val(); + optionsNew.anki.terms.model = $('#anki-terms-model').val(); + optionsNew.anki.terms.fields = ankiFieldsToDict($('#terms .anki-field-value')); + optionsNew.anki.kanji.deck = $('#anki-kanji-deck').val(); + optionsNew.anki.kanji.model = $('#anki-kanji-model').val(); + optionsNew.anki.kanji.fields = ankiFieldsToDict($('#kanji .anki-field-value')); } - }; - - combine(options, defaults); - combine(options.general, defaults.general); - combine(options.scanning, defaults.scanning); - combine(options.anki, defaults.anki); - combine(options.anki.terms, defaults.anki.terms); - combine(options.anki.kanji, defaults.anki.kanji); - - return options; -} - - -function optionsVersion(options) { - const fixups = [ - () => { - const copy = (targetDict, targetKey, sourceDict, sourceKey) => { - targetDict[targetKey] = sourceDict.hasOwnProperty(sourceKey) ? sourceDict[sourceKey] : targetDict[targetKey]; - }; - - copy(options.general, 'autoStart', options, 'activateOnStartup'); - copy(options.general, 'audioPlayback', options, 'enableAudioPlayback'); - copy(options.general, 'softKatakana', options, 'enableSoftKatakanaSearch'); - copy(options.general, 'groupResults', options, 'groupTermResults'); - copy(options.general, 'showAdvanced', options, 'showAdvancedOptions'); - - copy(options.scanning, 'requireShift', options, 'holdShiftToScan'); - copy(options.scanning, 'selectText', options, 'selectMatchedText'); - copy(options.scanning, 'delay', options, 'scanDelay'); - copy(options.scanning, 'length', options, 'scanLength'); - - options.anki.enable = options.ankiMethod === 'ankiconnect'; - - copy(options.anki, 'tags', options, 'ankiCardTags'); - copy(options.anki, 'sentenceExt', options, 'sentenceExtent'); - copy(options.anki.terms, 'deck', options, 'ankiTermDeck'); - copy(options.anki.terms, 'model', options, 'ankiTermModel'); - copy(options.anki.terms, 'fields', options, 'ankiTermFields'); - copy(options.anki.kanji, 'deck', options, 'ankiKanjiDeck'); - copy(options.anki.kanji, 'model', options, 'ankiKanjiModel'); - copy(options.anki.kanji, 'fields', options, 'ankiKanjiFields'); - - for (const title in options.dictionaries) { - const dictionary = options.dictionaries[title]; - dictionary.enabled = dictionary.enableTerms || dictionary.enableKanji; - dictionary.priority = 0; - } - }, - () => { - const fixupFields = fields => { - const fixups = { - '{expression-furigana}': '{furigana}', - '{glossary-list}': '{glossary}' - }; - - for (const name in fields) { - for (const fixup in fixups) { - fields[name] = fields[name].replace(fixup, fixups[fixup]); - } - } - }; - - fixupFields(options.anki.terms.fields); - fixupFields(options.anki.kanji.fields); + + $('.dict-group').each((index, element) => { + const dictionary = $(element); + const title = dictionary.data('title'); + const priority = parseInt(dictionary.find('.dict-priority').val(), 10); + const enabled = dictionary.find('.dict-enabled').prop('checked'); + optionsNew.dictionaries[title] = {priority, enabled}; + }); + + return {optionsNew, optionsOld}; + }); +} + +function updateVisibility(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(); + } +} + +$(document).ready(() => { + Handlebars.partials = Handlebars.templates; + + optionsLoad().then(options => { + $('#audio-playback-buttons').prop('checked', options.general.audioPlayback); + $('#group-terms-results').prop('checked', options.general.groupResults); + $('#soft-katakana-search').prop('checked', options.general.softKatakana); + $('#show-advanced-options').prop('checked', options.general.showAdvanced); + $('#max-displayed-results').val(options.general.maxResults); + + $('#hold-shift-to-scan').prop('checked', options.scanning.requireShift); + $('#select-matched-text').prop('checked', options.scanning.selectText); + $('#search-form-text-fields').prop('checked', options.scanning.imposter); + $('#scan-delay').val(options.scanning.delay); + $('#scan-length').val(options.scanning.length); + + $('#dict-purge').click(onDictionaryPurge); + $('#dict-importer a').click(onDictionarySetUrl); + $('#dict-import').click(onDictionaryImport); + $('#dict-url').on('input', onDictionaryUpdateUrl); + + $('#anki-enable').prop('checked', options.anki.enable); + $('#card-tags').val(options.anki.tags.join(' ')); + $('#generate-html-cards').prop('checked', options.anki.htmlCards); + $('#sentence-detection-extent').val(options.anki.sentenceExt); + $('#interface-server').val(options.anki.server); + $('input, select').not('.anki-model').change(onOptionsChanged); + $('.anki-model').change(onAnkiModelChanged); + + populateDictionaries(options); + populateAnkiDeckAndModel(options); + updateVisibility(options); + }); +}); + + +/* + * Dictionary + */ + +function showDictionaryError(error) { + const dialog = $('#dict-error'); + if (error) { + dialog.show().find('span').text(error); + } else { + dialog.hide(); + } +} + +function showDictionarySpinner(show) { + const spinner = $('#dict-spinner'); + if (show) { + spinner.show(); + } else { + spinner.hide(); + } +} + +function populateDictionaries(options) { + showDictionaryError(null); + showDictionarySpinner(true); + + const dictGroups = $('#dict-groups').empty(); + const dictWarning = $('#dict-warning').hide(); + + let dictCount = 0; + return instDb().getDictionaries().then(rows => { + rows.forEach(row => { + const dictOptions = options.dictionaries[row.title] || {enableTerms: false, enableKanji: false, priority: 0}; + const html = Handlebars.templates['dictionary.html']({ + title: row.title, + version: row.version, + revision: row.revision, + priority: dictOptions.priority, + enabled: dictOptions.enabled + }); + + dictGroups.append($(html)); + ++dictCount; + }); + + updateVisibility(options); + + $('.dict-enabled, .dict-priority').change(onOptionsChanged); + }).catch(showDictionaryError).then(() => { + showDictionarySpinner(false); + if (dictCount === 0) { + dictWarning.show(); } - ]; + }); +} + +function onDictionaryPurge(e) { + e.preventDefault(); + + showDictionaryError(null); + showDictionarySpinner(true); - optionsSetDefaults(options); - if (!options.hasOwnProperty('version')) { - options.version = fixups.length; + const dictControls = $('#dict-importer, #dict-groups').hide(); + const dictProgress = $('#dict-purge-progress').show(); + + return instDb().purge().catch(showDictionaryError).then(() => { + showDictionarySpinner(false); + dictControls.show(); + dictProgress.hide(); + return optionsLoad(); + }).then(options => { + options.dictionaries = {}; + return optionsSave(options).then(() => { + populateDictionaries(options); + instYomi().setOptions(options); + }); + }); +} + +function onDictionaryImport() { + showDictionaryError(null); + showDictionarySpinner(true); + + const dictUrl = $('#dict-url'); + const dictImporter = $('#dict-importer').hide(); + const dictProgress = $('#dict-import-progress').show(); + const setProgress = percent => dictProgress.find('.progress-bar').css('width', `${percent}%`); + + setProgress(0.0); + + optionsLoad().then(options => { + instDb().importDictionary(dictUrl.val(), (total, current) => setProgress(current / total * 100.0)).then(summary => { + options.dictionaries[summary.title] = {enabled: true, priority: 0}; + return optionsSave(options).then(() => instYomi().setOptions(options)); + }).then(() => populateDictionaries(options)).catch(showDictionaryError).then(() => { + showDictionarySpinner(false); + dictProgress.hide(); + dictImporter.show(); + dictUrl.val(''); + dictUrl.trigger('input'); + }); + }); +} + +function onDictionarySetUrl(e) { + e.preventDefault(); + + const dictUrl = $('#dict-url'); + const url = $(this).data('url'); + if (url.includes('/')) { + dictUrl.val(url); + } else { + dictUrl.val(chrome.extension.getURL(`bg/lang/data/${url}/index.json`)); } - while (options.version < fixups.length) { - fixups[options.version++](); + dictUrl.trigger('input'); +} + +function onDictionaryUpdateUrl() { + $('#dict-import').prop('disabled', $(this).val().length === 0); +} + +/* + * Anki + */ + +function showAnkiSpinner(show) { + const spinner = $('#anki-spinner'); + if (show) { + spinner.show(); + } else { + spinner.hide(); + } +} + +function showAnkiError(error) { + const dialog = $('#anki-error'); + if (error) { + dialog.show().find('span').text(error); + } + else { + dialog.hide(); } +} + +function ankiFieldsToDict(selection) { + const result = {}; + selection.each((index, element) => { + result[$(element).data('field')] = $(element).val(); + }); - return options; + return result; } -function optionsLoad() { - return new Promise((resolve, reject) => { - chrome.storage.sync.get(null, options => resolve(optionsVersion(options))); +function populateAnkiDeckAndModel(options) { + showAnkiError(null); + showAnkiSpinner(true); + + const ankiFormat = $('#anki-format').hide(); + return Promise.all([instAnki().getDeckNames(), instAnki().getModelNames()]).then(([deckNames, modelNames]) => { + const ankiDeck = $('.anki-deck'); + ankiDeck.find('option').remove(); + deckNames.sort().forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); + + $('#anki-terms-deck').val(options.anki.terms.deck); + $('#anki-kanji-deck').val(options.anki.kanji.deck); + + const ankiModel = $('.anki-model'); + ankiModel.find('option').remove(); + modelNames.sort().forEach(name => ankiModel.append($('<option/>', {value: name, text: name}))); + + return Promise.all([ + populateAnkiFields($('#anki-terms-model').val(options.anki.terms.model), options), + populateAnkiFields($('#anki-kanji-model').val(options.anki.kanji.model), options) + ]); + }).then(() => ankiFormat.show()).catch(showAnkiError).then(() => showAnkiSpinner(false)); +} + +function populateAnkiFields(element, options) { + const tab = element.closest('.tab-pane'); + const tabId = tab.attr('id'); + const container = tab.find('tbody').empty(); + + const modelName = element.val(); + if (modelName === null) { + return Promise.resolve(); + } + + const markers = { + 'terms': ['audio', 'dictionary', 'expression', 'furigana', 'glossary', 'reading', 'sentence', 'tags', 'url'], + 'kanji': ['character', 'dictionary', 'glossary', 'kunyomi', 'onyomi', 'sentence', 'tags', 'url'] + }[tabId] || {}; + + return instAnki().getModelFieldNames(modelName).then(names => { + names.forEach(name => { + 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(onOptionsChanged); + tab.find('.marker-link').click(e => { + e.preventDefault(); + const link = e.target; + $(link).closest('.input-group').find('.anki-field-value').val(`{${link.text}}`).trigger('change'); + }); }); } -function optionsSave(options) { - return new Promise((resolve, reject) => { - chrome.storage.sync.set(options, resolve); +function onAnkiModelChanged(e) { + if (!e.originalEvent) { + return; + } + + showAnkiError(null); + showAnkiSpinner(true); + + const element = $(this); + getFormData().then(({optionsNew, optionsOld}) => { + const tab = element.closest('.tab-pane'); + const tabId = tab.attr('id'); + + optionsNew.anki[tabId].fields = {}; + populateAnkiFields(element, optionsNew).then(() => { + optionsSave(optionsNew).then(() => instYomi().setOptions(optionsNew)); + }).catch(showAnkiError).then(() => showAnkiSpinner(false)); }); } -function optionsEnabledDicts(options) { - const dictionaries = {}; - for (const title in options.dictionaries) { - const dictionary = options.dictionaries[title]; - if (dictionary.enabled) { - dictionaries[title] = dictionary; - } +function onOptionsChanged(e) { + if (!e.originalEvent && !e.isTrigger) { + return; } - return dictionaries; + getFormData().then(({optionsNew, optionsOld}) => { + return optionsSave(optionsNew).then(() => { + instYomi().setOptions(optionsNew); + updateVisibility(optionsNew); + + const ankiUpdated = + optionsNew.anki.enable !== optionsOld.anki.enable || + optionsNew.anki.server !== optionsOld.anki.server; + + if (ankiUpdated) { + showAnkiError(null); + showAnkiSpinner(true); + return populateAnkiDeckAndModel(optionsNew); + } + }); + }).catch(showAnkiError).then(() => showAnkiSpinner(false)); } diff --git a/ext/bg/js/popup.js b/ext/bg/js/popup.js index 4f6942f0..0d1fb04e 100644 --- a/ext/bg/js/popup.js +++ b/ext/bg/js/popup.js @@ -16,6 +16,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ + $(document).ready(() => { $('#open-search').click(() => window.open(chrome.extension.getURL('bg/search.html'))); $('#open-options').click(() => chrome.runtime.openOptionsPage()); @@ -27,7 +28,7 @@ $(document).ready(() => { toggle.bootstrapToggle(); toggle.change(() => { options.general.enable = toggle.prop('checked'); - optionsSave(options).then(() => getYomichan().setOptions(options)); + optionsSave(options).then(() => instYomi().setOptions(options)); }); }); }); diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index e7c2aac2..967b8bfe 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -31,7 +31,7 @@ class Translator { } const promises = [ - loadJsonInt('bg/lang/deinflect.json'), + jsonLoadInt('bg/lang/deinflect.json'), this.database.prepare() ]; @@ -56,8 +56,8 @@ class Translator { let definitions = []; for (const deinflection of deinflections) { for (const definition of deinflection.definitions) { - const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta)); - tags.push(buildDictTag(definition.dictionary)); + const tags = definition.tags.map(tag => dictTagBuild(tag, definition.tagMeta)); + tags.push(dictTagBuildSource(definition.dictionary)); definitions.push({ source: deinflection.source, reasons: deinflection.reasons, @@ -67,13 +67,13 @@ class Translator { expression: definition.expression, reading: definition.reading, glossary: definition.glossary, - tags: sortTags(tags) + tags: dictTagsSort(tags) }); } } - definitions = undupeTermDefs(definitions); - definitions = sortTermDefs(definitions, dictionaries); + definitions = dictTermsUndupe(definitions); + definitions = dictTermsSort(definitions, dictionaries); let length = 0; for (const definition of definitions) { @@ -86,7 +86,7 @@ class Translator { findTermsGrouped(text, dictionaries, softKatakana) { return this.findTerms(text, dictionaries, softKatakana).then(({length, definitions}) => { - return {length, definitions: groupTermDefs(definitions, dictionaries)}; + return {length, definitions: dictTermsGroup(definitions, dictionaries)}; }); } @@ -105,9 +105,9 @@ class Translator { return Promise.all(promises).then(defSets => { const definitions = defSets.reduce((a, b) => a.concat(b), []); for (const definition of definitions) { - const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta)); - tags.push(buildDictTag(definition.dictionary)); - definition.tags = sortTags(tags); + const tags = definition.tags.map(tag => dictTagBuild(tag, definition.tagMeta)); + tags.push(dictTagBuildSource(definition.dictionary)); + definition.tags = dictTagsSort(tags); } return definitions; @@ -140,8 +140,8 @@ class Translator { processKanji(definitions) { for (const definition of definitions) { - const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta)); - definition.tags = sortTags(tags); + const tags = definition.tags.map(tag => dictTagBuild(tag, definition.tagMeta)); + definition.tags = dictTagsSort(tags); } return definitions; diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 852dfac7..7a8f15ef 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -17,27 +17,9 @@ */ -function helperKanjiLinks(options) { - const isKanji = c => { - const code = c.charCodeAt(0); - return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0; - }; - - let result = ''; - for (const c of options.fn(this)) { - if (isKanji(c)) { - result += `<a href="#" class="kanji-link">${c}</a>`; - } else { - result += c; - } - } - - return result; -} - -function helperMultiLine(options) { - return options.fn(this).split('\n').join('<br>'); -} +/* + * Promise + */ function promiseCallback(promise, callback) { return promise.then(result => { @@ -50,19 +32,175 @@ function promiseCallback(promise, callback) { }); } -function getYomichan() { + +/* + * Instance + */ + +function instYomi() { return chrome.extension.getBackgroundPage().yomichan; } -function getDatabase() { - return getYomichan().translator.database; +function instDb() { + return instYomi().translator.database; } -function getAnki() { - return getYomichan().anki; +function instAnki() { + return instYomi().anki; } -function sortTermDefs(definitions, dictionaries=null) { + + +/* + * Options + */ + +function optionsSetDefaults(options) { + const defaults = { + general: { + enable: true, + audioPlayback: true, + groupResults: true, + softKatakana: true, + maxResults: 32, + showAdvanced: false + }, + + scanning: { + requireShift: true, + selectText: true, + imposter: true, + delay: 15, + length: 10 + }, + + dictionaries: {}, + + anki: { + enable: false, + server: 'http://127.0.0.1:8765', + tags: ['yomichan'], + htmlCards: true, + sentenceExt: 200, + terms: {deck: '', model: '', fields: {}}, + kanji: {deck: '', model: '', fields: {}} + } + }; + + const combine = (target, source) => { + for (const key in source) { + if (!target.hasOwnProperty(key)) { + target[key] = source[key]; + } + } + }; + + combine(options, defaults); + combine(options.general, defaults.general); + combine(options.scanning, defaults.scanning); + combine(options.anki, defaults.anki); + combine(options.anki.terms, defaults.anki.terms); + combine(options.anki.kanji, defaults.anki.kanji); + + return options; +} + +function optionsVersion(options) { + const fixups = [ + () => { + const copy = (targetDict, targetKey, sourceDict, sourceKey) => { + targetDict[targetKey] = sourceDict.hasOwnProperty(sourceKey) ? sourceDict[sourceKey] : targetDict[targetKey]; + }; + + copy(options.general, 'autoStart', options, 'activateOnStartup'); + copy(options.general, 'audioPlayback', options, 'enableAudioPlayback'); + copy(options.general, 'softKatakana', options, 'enableSoftKatakanaSearch'); + copy(options.general, 'groupResults', options, 'groupTermResults'); + copy(options.general, 'showAdvanced', options, 'showAdvancedOptions'); + + copy(options.scanning, 'requireShift', options, 'holdShiftToScan'); + copy(options.scanning, 'selectText', options, 'selectMatchedText'); + copy(options.scanning, 'delay', options, 'scanDelay'); + copy(options.scanning, 'length', options, 'scanLength'); + + options.anki.enable = options.ankiMethod === 'ankiconnect'; + + copy(options.anki, 'tags', options, 'ankiCardTags'); + copy(options.anki, 'sentenceExt', options, 'sentenceExtent'); + copy(options.anki.terms, 'deck', options, 'ankiTermDeck'); + copy(options.anki.terms, 'model', options, 'ankiTermModel'); + copy(options.anki.terms, 'fields', options, 'ankiTermFields'); + copy(options.anki.kanji, 'deck', options, 'ankiKanjiDeck'); + copy(options.anki.kanji, 'model', options, 'ankiKanjiModel'); + copy(options.anki.kanji, 'fields', options, 'ankiKanjiFields'); + + for (const title in options.dictionaries) { + const dictionary = options.dictionaries[title]; + dictionary.enabled = dictionary.enableTerms || dictionary.enableKanji; + dictionary.priority = 0; + } + }, + () => { + const fixupFields = fields => { + const fixups = { + '{expression-furigana}': '{furigana}', + '{glossary-list}': '{glossary}' + }; + + for (const name in fields) { + for (const fixup in fixups) { + fields[name] = fields[name].replace(fixup, fixups[fixup]); + } + } + }; + + fixupFields(options.anki.terms.fields); + fixupFields(options.anki.kanji.fields); + } + ]; + + optionsSetDefaults(options); + if (!options.hasOwnProperty('version')) { + options.version = fixups.length; + } + + while (options.version < fixups.length) { + fixups[options.version++](); + } + + return options; +} + +function optionsLoad() { + return new Promise((resolve, reject) => { + chrome.storage.sync.get(null, options => resolve(optionsVersion(options))); + }); +} + +function optionsSave(options) { + return new Promise((resolve, reject) => { + chrome.storage.sync.set(options, resolve); + }); +} + + +/* + * Dictionary + */ + +function dictEnabled(options) { + const dictionaries = {}; + for (const title in options.dictionaries) { + const dictionary = options.dictionaries[title]; + if (dictionary.enabled) { + dictionaries[title] = dictionary; + } + } + + return dictionaries; +} + +function dictTermsSort(definitions, dictionaries=null) { return definitions.sort((v1, v2) => { const sl1 = v1.source.length; const sl2 = v2.source.length; @@ -102,7 +240,7 @@ function sortTermDefs(definitions, dictionaries=null) { }); } -function undupeTermDefs(definitions) { +function dictTermsUndupe(definitions) { const definitionGroups = {}; for (const definition of definitions) { const definitionExisting = definitionGroups[definition.id]; @@ -119,7 +257,7 @@ function undupeTermDefs(definitions) { return definitionsUnique; } -function groupTermDefs(definitions, dictionaries) { +function dictTermsGroup(definitions, dictionaries) { const groups = {}; for (const definition of definitions) { const key = [definition.source, definition.expression].concat(definition.reasons); @@ -139,7 +277,7 @@ function groupTermDefs(definitions, dictionaries) { for (const key in groups) { const groupDefs = groups[key]; const firstDef = groupDefs[0]; - sortTermDefs(groupDefs, dictionaries); + dictTermsSort(groupDefs, dictionaries); results.push({ definitions: groupDefs, expression: firstDef.expression, @@ -150,24 +288,24 @@ function groupTermDefs(definitions, dictionaries) { }); } - return sortTermDefs(results); + return dictTermsSort(results); } -function buildDictTag(name) { - return sanitizeTag({name, category: 'dictionary', order: 100}); +function dictTagBuildSource(name) { + return dictTagSanitize({name, category: 'dictionary', order: 100}); } -function buildTag(name, meta) { +function dictTagBuild(name, meta) { const tag = {name}; const symbol = name.split(':')[0]; for (const prop in meta[symbol] || {}) { tag[prop] = meta[symbol][prop]; } - return sanitizeTag(tag); + return dictTagSanitize(tag); } -function sanitizeTag(tag) { +function dictTagSanitize(tag) { tag.name = tag.name || 'untitled'; tag.category = tag.category || 'default'; tag.notes = tag.notes || ''; @@ -175,11 +313,7 @@ function sanitizeTag(tag) { return tag; } -function splitField(field) { - return field.length === 0 ? [] : field.split(' '); -} - -function sortTags(tags) { +function dictTagsSort(tags) { return tags.sort((v1, v2) => { const order1 = v1.order; const order2 = v2.order; @@ -201,7 +335,11 @@ function sortTags(tags) { }); } -function formatField(field, definition, mode, options) { +function dictFieldSplit(field) { + return field.length === 0 ? [] : field.split(' '); +} + +function dictFieldFormat(field, definition, mode, options) { const markers = [ 'audio', 'character', @@ -237,7 +375,12 @@ function formatField(field, definition, mode, options) { return field; } -function loadJson(url) { + +/* + * Json + */ + +function jsonLoad(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.overrideMimeType('application/json'); @@ -255,13 +398,13 @@ function loadJson(url) { }); } -function loadJsonInt(url) { - return loadJson(chrome.extension.getURL(url)); +function jsonLoadInt(url) { + return jsonLoad(chrome.extension.getURL(url)); } -function importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded) { +function jsonLoadDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded) { const indexDir = indexUrl.slice(0, indexUrl.lastIndexOf('/')); - return loadJson(indexUrl).then(index => { + return jsonLoad(indexUrl).then(index => { if (!index.title || !index.version || !index.revision) { return Promise.reject('unrecognized dictionary format'); } @@ -285,7 +428,7 @@ function importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded) { for (let i = 1; i <= index.termBanks; ++i) { const bankUrl = `${indexDir}/term_bank_${i}.json`; - loaders.push(() => loadJson(bankUrl).then(entries => termsLoaded( + loaders.push(() => jsonLoad(bankUrl).then(entries => termsLoaded( index.title, entries, banksTotal, @@ -295,7 +438,7 @@ function importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded) { for (let i = 1; i <= index.kanjiBanks; ++i) { const bankUrl = `${indexDir}/kanji_bank_${i}.json`; - loaders.push(() => loadJson(bankUrl).then(entries => kanjiLoaded( + loaders.push(() => jsonLoad(bankUrl).then(entries => kanjiLoaded( index.title, entries, banksTotal, @@ -311,3 +454,30 @@ function importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded) { return chain; }); } + + +/* + * Helpers + */ + +function helperKanjiLinks(options) { + const isKanji = c => { + const code = c.charCodeAt(0); + return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0; + }; + + let result = ''; + for (const c of options.fn(this)) { + if (isKanji(c)) { + result += `<a href="#" class="kanji-link">${c}</a>`; + } else { + result += c; + } + } + + return result; +} + +function helperMultiLine(options) { + return options.fn(this).split('\n').join('<br>'); +} diff --git a/ext/bg/js/yomichan.js b/ext/bg/js/yomichan.js index a544d0ad..abf6c10c 100644 --- a/ext/bg/js/yomichan.js +++ b/ext/bg/js/yomichan.js @@ -115,7 +115,7 @@ class Yomichan { } for (const name in fields) { - note.fields[name] = formatField( + note.fields[name] = dictFieldFormat( fields[name], definition, mode, @@ -132,7 +132,7 @@ class Yomichan { api_findKanji({text, callback}) { promiseCallback( - this.translator.findKanji(text, optionsEnabledDicts(this.options)).then(definitions => { + this.translator.findKanji(text, dictEnabled(this.options)).then(definitions => { return definitions.slice(0, this.options.general.maxResults); }), callback @@ -141,7 +141,7 @@ class Yomichan { api_findTerms({text, callback}) { promiseCallback( - this.translator.findTerms(text, optionsEnabledDicts(this.options), this.options.general.softKatakana).then(({definitions, length}) => { + this.translator.findTerms(text, dictEnabled(this.options), this.options.general.softKatakana).then(({definitions, length}) => { return {length, definitions: definitions.slice(0, this.options.general.maxResults)}; }), callback @@ -150,7 +150,7 @@ class Yomichan { api_findTermsGrouped({text, callback}) { promiseCallback( - this.translator.findTermsGrouped(text, optionsEnabledDicts(this.options), this.options.general.softKatakana).then(({definitions, length}) => { + this.translator.findTermsGrouped(text, dictEnabled(this.options), this.options.general.softKatakana).then(({definitions, length}) => { return {length, definitions: definitions.slice(0, this.options.general.maxResults)}; }), callback diff --git a/ext/bg/options.html b/ext/bg/options.html index 8bf773b0..7739c540 100644 --- a/ext/bg/options.html +++ b/ext/bg/options.html @@ -240,6 +240,5 @@ <script src="js/gecko.js"></script> <script src="js/util.js"></script> <script src="js/options.js"></script> - <script src="js/options-form.js"></script> </body> </html> |