diff options
Diffstat (limited to 'ext/bg/js')
-rw-r--r-- | ext/bg/js/ankiconnect.js | 82 | ||||
-rw-r--r-- | ext/bg/js/ankinull.js | 39 | ||||
-rw-r--r-- | ext/bg/js/ankiweb.js | 148 | ||||
-rw-r--r-- | ext/bg/js/options-form.js | 189 | ||||
-rw-r--r-- | ext/bg/js/options.js | 5 | ||||
-rw-r--r-- | ext/bg/js/util.js | 2 | ||||
-rw-r--r-- | ext/bg/js/yomichan.js | 109 |
7 files changed, 414 insertions, 160 deletions
diff --git a/ext/bg/js/ankiconnect.js b/ext/bg/js/ankiconnect.js new file mode 100644 index 00000000..d17f3268 --- /dev/null +++ b/ext/bg/js/ankiconnect.js @@ -0,0 +1,82 @@ +/* + * 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/>. + */ + +class AnkiConnect { + constructor() { + this.asyncPools = {}; + this.localVersion = 1; + this.remoteVersion = null; + } + + addNote(note) { + return this.checkVersion().then(() => this.ankiInvoke('addNote', {note}, null)); + } + + canAddNotes(notes) { + return this.checkVersion().then(() => this.ankiInvoke('canAddNotes', {notes}, 'notes')); + } + + getDeckNames() { + return this.checkVersion().then(() => this.ankiInvoke('deckNames', {}, null)); + } + + getModelNames() { + return this.checkVersion().then(() => this.ankiInvoke('modelNames', {}, null)); + } + + getModelFieldNames(modelName) { + return this.checkVersion().then(() => this.ankiInvoke('modelFieldNames', {modelName}, null)); + } + + checkVersion() { + if (this.localVersion === this.remoteVersion) { + return Promise.resolve(true); + } + + return this.ankiInvoke('version', {}, null).then(version => { + this.remoteVersion = version; + if (this.remoteVersion !== this.localVersion) { + return Promise.reject('extension and plugin version mismatch'); + } + }); + } + + ankiInvoke(action, params, pool) { + return new Promise((resolve, reject) => { + if (pool !== null && this.asyncPools.hasOwnProperty(pool)) { + this.asyncPools[pool].abort(); + } + + const xhr = new XMLHttpRequest(); + xhr.addEventListener('loadend', () => { + if (pool !== null) { + delete this.asyncPools[pool]; + } + + if (xhr.responseText) { + resolve(JSON.parse(xhr.responseText)); + } else { + reject('unable to connect to plugin'); + } + }); + + xhr.open('POST', 'http://127.0.0.1:8765'); + xhr.send(JSON.stringify({action, params})); + }); + } +} diff --git a/ext/bg/js/ankinull.js b/ext/bg/js/ankinull.js new file mode 100644 index 00000000..0d0ed903 --- /dev/null +++ b/ext/bg/js/ankinull.js @@ -0,0 +1,39 @@ +/* + * 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/>. + */ + +class AnkiNull { + addNote(note) { + return Promise.reject('unsupported action'); + } + + canAddNotes(notes) { + return Promise.resolve([]); + } + + getDeckNames() { + return Promise.resolve([]); + } + + getModelNames() { + return Promise.resolve([]); + } + + getModelFieldNames(modelName) { + return Promise.resolve([]); + } +} diff --git a/ext/bg/js/ankiweb.js b/ext/bg/js/ankiweb.js new file mode 100644 index 00000000..1393f668 --- /dev/null +++ b/ext/bg/js/ankiweb.js @@ -0,0 +1,148 @@ +/* + * 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/>. + */ + +class AnkiWeb { + constructor(username, password) { + this.username = username; + this.password = password; + this.noteInfo = null; + } + + addNote(note) { + return this.retrieve().then(info => { + const model = info.models.find(m => m.name === note.modelName); + if (!model) { + return Promise.reject('invalid model'); + } + + const fields = []; + for (const field of model.fields) { + fields.push(note.fields[field]); + } + + const data = { + data: JSON.stringify([fields, note.tags.join(' ')]), + mid: model.id, + deck: note.deckName + }; + + return AnkiWeb.loadAccountPage('https://ankiweb.net/edit/save', data, this.username, this.password); + }).then(response => response !== '0'); + } + + canAddNotes(notes) { + return Promise.resolve(new Array(notes.length).fill(true)); + } + + getDeckNames() { + return this.retrieve().then(info => info.deckNames); + } + + getModelNames() { + return this.retrieve().then(info => info.models.map(m => m.name)); + } + + getModelFieldNames(modelName) { + return this.retrieve().then(info => { + const model = info.models.find(m => m.name === modelName); + return model ? model.fields : []; + }); + } + + retrieve() { + if (this.noteInfo !== null) { + return Promise.resolve(this.noteInfo); + } + + return AnkiWeb.scrape(this.username, this.password).then(({deckNames, models}) => { + this.noteInfo = {deckNames, models}; + return this.noteInfo; + }); + } + + static scrape(username, password) { + return AnkiWeb.loadAccountPage('https://ankiweb.net/edit/', null, username, password).then(response => { + const modelsMatch = /editor\.models = (.*}]);/.exec(response); + if (modelsMatch === null) { + return Promise.reject('failed to scrape model data'); + } + + const decksMatch = /editor\.decks = (.*}});/.exec(response); + if (decksMatch === null) { + return Promise.reject('failed to scrape deck data'); + } + + const modelsJson = JSON.parse(modelsMatch[1]); + const decksJson = JSON.parse(decksMatch[1]); + + const deckNames = Object.keys(decksJson).map(d => decksJson[d].name); + const models = []; + for (const modelJson of modelsJson) { + models.push({ + name: modelJson.name, + id: modelJson.id, + fields: modelJson.flds.map(f => f.name) + }); + } + + return {deckNames, models}; + }); + } + + static login(username, password) { + if (username.length === 0 || password.length === 0) { + return Promise.reject('unspecified login credentials'); + } + + const data = {username, password, submitted: 1}; + return AnkiWeb.loadPage('https://ankiweb.net/account/login', data).then(response => { + if (!response.includes('class="mitem"')) { + return Promise.reject('failed to authenticate'); + } + }); + } + + static loadAccountPage(url, data, username, password) { + return AnkiWeb.loadPage(url, data).then(response => { + if (response.includes('name="password"')) { + return AnkiWeb.login(username, password).then(() => AnkiWeb.loadPage(url, data)); + } else { + return response; + } + }); + } + + static loadPage(url, data) { + return new Promise((resolve, reject) => { + if (data) { + const params = []; + for (const key in data) { + params.push(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`); + } + + url += '?' + params.join('&'); + } + + const xhr = new XMLHttpRequest(); + xhr.addEventListener('error', () => reject('failed to execute request')); + xhr.addEventListener('load', () => resolve(xhr.responseText)); + xhr.open('GET', url); + xhr.send(); + }); + } +} diff --git a/ext/bg/js/options-form.js b/ext/bg/js/options-form.js index 6508a9eb..170b1e8c 100644 --- a/ext/bg/js/options-form.js +++ b/ext/bg/js/options-form.js @@ -21,6 +21,10 @@ function yomichan() { return chrome.extension.getBackgroundPage().yomichan; } +function anki() { + return yomichan().anki; +} + function fieldsToDict(selection) { const result = {}; selection.each((index, element) => { @@ -44,30 +48,22 @@ function modelIdToMarkers(id) { }[id]; } -function getBasicOptions() { +function getFormValues() { return loadOptions().then(optsOld => { const optsNew = $.extend({}, optsOld); optsNew.activateOnStartup = $('#activate-on-startup').prop('checked'); - optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked'); optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked'); - optsNew.enableAnkiConnect = $('#enable-anki-connect').prop('checked'); + optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked'); + optsNew.holdShiftToScan = $('#hold-shift-to-scan').prop('checked'); optsNew.selectMatchedText = $('#select-matched-text').prop('checked'); optsNew.scanDelay = parseInt($('#scan-delay').val(), 10); optsNew.scanLength = parseInt($('#scan-length').val(), 10); - return { - optsNew: sanitizeOptions(optsNew), - optsOld: sanitizeOptions(optsOld) - }; - }); -} - -function getAnkiOptions() { - return loadOptions().then(optsOld => { - const optsNew = $.extend({}, optsOld); - + optsNew.ankiMethod = $('#anki-method').val(); + optsNew.ankiUsername = $('#anki-username').val(); + optsNew.ankiPassword = $('#anki-password').val(); optsNew.ankiCardTags = $('#anki-card-tags').val().split(/[,; ]+/); optsNew.sentenceExtent = parseInt($('#sentence-extent').val(), 10); optsNew.ankiTermDeck = $('#anki-term-deck').val(); @@ -84,62 +80,76 @@ function getAnkiOptions() { }); } +function updateVisibility(opts) { + switch (opts.ankiMethod) { + case 'ankiweb': + $('#anki-general').show(); + $('.anki-login').show(); + break; + case 'ankiconnect': + $('#anki-general').show(); + $('.anki-login').hide(); + break; + default: + $('#anki-general').hide(); + break; + } + + if (opts.showAdvancedOptions) { + $('.options-advanced').show(); + } else { + $('.options-advanced').hide(); + } +} + function populateAnkiDeckAndModel(opts) { - const yomi = yomichan(); + const ankiSpinner = $('#anki-spinner'); + ankiSpinner.show(); + + const ankiFormat = $('#anki-format'); + ankiFormat.hide(); const ankiDeck = $('.anki-deck'); ankiDeck.find('option').remove(); - yomi.api_getDeckNames({callback: names => { - if (names !== null) { - names.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); - } - - $('#anki-term-deck').val(opts.ankiTermDeck); - $('#anki-kanji-deck').val(opts.ankiKanjiDeck); - }}); const ankiModel = $('.anki-model'); ankiModel.find('option').remove(); - yomi.api_getModelNames({callback: names => { - if (names !== null) { - names.forEach(name => ankiModel.append($('<option/>', {value: name, text: name}))); - } - - populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts); - populateAnkiFields($('#anki-kanji-model').val(opts.ankiKanjiModel), opts); - }}); -} -function updateAnkiStatus() { - $('.error-dlg').hide(); - - yomichan().api_getVersion({callback: version => { - if (version === null) { - $('.error-dlg-connection').show(); - $('.options-anki-controls').hide(); - } else if (version !== yomichan().getApiVersion()) { - $('.error-dlg-version').show(); - $('.options-anki-controls').hide(); - } else { - $('.options-anki-controls').show(); - } - }}); + return anki().getDeckNames().then(names => { + names.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); + $('#anki-term-deck').val(opts.ankiTermDeck); + $('#anki-kanji-deck').val(opts.ankiKanjiDeck); + }).then(() => { + return anki().getModelNames(); + }).then(names => { + names.forEach(name => ankiModel.append($('<option/>', {value: name, text: name}))); + return populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts); + }).then(() => { + return populateAnkiFields($('#anki-kanji-model').val(opts.ankiKanjiModel), opts); + }).then(() => { + $('#anki-error').hide(); + ankiFormat.show(); + }).catch(error => { + $('#anki-error').show().find('span').text(error); + }).then(() => { + ankiSpinner.hide(); + }); } function populateAnkiFields(element, opts) { + const table = element.closest('.tab-pane').find('.anki-fields'); + table.find('tbody').remove(); + const modelName = element.val(); if (modelName === null) { - return; + return Promise.resolve(); } const modelId = element.attr('id'); const optKey = modelIdToFieldOptKey(modelId); const markers = modelIdToMarkers(modelId); - yomichan().api_getModelFieldNames({modelName, callback: names => { - const table = element.closest('.tab-pane').find('.anki-fields'); - table.find('tbody').remove(); - + return anki().getModelFieldNames(modelName).then(names => { const tbody = $('<tbody>'); names.forEach(name => { const button = $('<button>', {type: 'button', class: 'btn btn-default dropdown-toggle'}); @@ -160,7 +170,11 @@ function populateAnkiFields(element, opts) { groupBtn.append(markerItems); const group = $('<div>', {class: 'input-group'}); - group.append($('<input>', {type: 'text', class: 'anki-field-value form-control', value: opts[optKey][name] || ''}).data('field', name).change(onOptionsAnkiChanged)); + group.append($('<input>', { + type: 'text', + class: 'anki-field-value form-control', + value: opts[optKey][name] || '' + }).data('field', name).change(onOptionsChanged)); group.append(groupBtn); const row = $('<tr>'); @@ -171,80 +185,73 @@ function populateAnkiFields(element, opts) { }); table.append(tbody); - }}); + }); } -function onOptionsBasicChanged(e) { +function onOptionsChanged(e) { if (!e.originalEvent && !e.isTrigger) { return; } - getBasicOptions().then(({optsNew, optsOld}) => { + getFormValues().then(({optsNew, optsOld}) => { saveOptions(optsNew).then(() => { yomichan().setOptions(optsNew); - if (!optsOld.enableAnkiConnect && optsNew.enableAnkiConnect) { - updateAnkiStatus(); - populateAnkiDeckAndModel(optsNew); - $('.options-anki').show(); - } else if (optsOld.enableAnkiConnect && !optsNew.enableAnkiConnect) { - $('.options-anki').hide(); - } + updateVisibility(optsNew); + + const invalidated = + optsNew.ankiMethod !== optsOld.ankiMethod || + optsNew.ankiUsername !== optsOld.ankiUsername || + optsNew.ankiPassword !== optsOld.ankiPassword; - if (optsNew.showAdvancedOptions) { - $('.options-advanced').show(); - } else { - $('.options-advanced').hide(); + if (invalidated) { + populateAnkiDeckAndModel(optsNew); } }); }); } -function onOptionsAnkiChanged(e) { - if (!e.originalEvent && !e.isTrigger) { +function onAnkiModelChanged(e) { + if (!e.originalEvent) { return; } - getAnkiOptions().then(({optsNew, optsOld}) => { - saveOptions(optsNew).then(() => yomichan().setOptions(optsNew)); - }); -} + getFormValues().then(({optsNew, optsOld}) => { + optsNew[modelIdToFieldOptKey($(this).id)] = {}; -function onAnkiModelChanged(e) { - if (e.originalEvent) { - getAnkiOptions().then(({optsNew, optsOld}) => { - optsNew[modelIdToFieldOptKey($(this).id)] = {}; - populateAnkiFields($(this), optsNew); + const ankiSpinner = $('#anki-spinner'); + ankiSpinner.show(); + + populateAnkiFields($(this), optsNew).then(() => { saveOptions(optsNew).then(() => yomichan().setOptions(optsNew)); + }).catch(error => { + $('#anki-error').show().find('span').text(error); + }).then(() => { + $('#anki-error').hide(); + ankiSpinner.hide(); }); - } + }); } $(document).ready(() => { loadOptions().then(opts => { $('#activate-on-startup').prop('checked', opts.activateOnStartup); $('#enable-audio-playback').prop('checked', opts.enableAudioPlayback); - $('#enable-anki-connect').prop('checked', opts.enableAnkiConnect); $('#show-advanced-options').prop('checked', opts.showAdvancedOptions); $('#hold-shift-to-scan').prop('checked', opts.holdShiftToScan); $('#select-matched-text').prop('checked', opts.selectMatchedText); $('#scan-delay').val(opts.scanDelay); $('#scan-length').val(opts.scanLength); + + $('#anki-method').val(opts.ankiMethod); + $('#anki-username').val(opts.ankiUsername); + $('#anki-password').val(opts.ankiPassword); $('#anki-card-tags').val(opts.ankiCardTags.join(' ')); $('#sentence-extent').val(opts.sentenceExtent); - $('.options-basic input').change(onOptionsBasicChanged); - $('.options-anki input').change(onOptionsAnkiChanged); - $('.anki-deck').change(onOptionsAnkiChanged); + $('input, select').not('.anki-model').change(onOptionsChanged); $('.anki-model').change(onAnkiModelChanged); - if (opts.showAdvancedOptions) { - $('.options-advanced').show(); - } - - if (opts.enableAnkiConnect) { - updateAnkiStatus(); - populateAnkiDeckAndModel(opts); - $('.options-anki').show(); - } + populateAnkiDeckAndModel(opts); + updateVisibility(opts); }); }); diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index e9ad74a3..45af2ff1 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -21,12 +21,15 @@ function sanitizeOptions(options) { const defaults = { activateOnStartup: true, enableAudioPlayback: true, - enableAnkiConnect: false, showAdvancedOptions: false, selectMatchedText: true, holdShiftToScan: true, scanDelay: 15, scanLength: 20, + + ankiMethod: 'disabled', + ankiUsername: '', + ankiPassword: '', ankiCardTags: ['yomichan'], sentenceExtent: 200, diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 95d1b43e..1e033eef 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -34,7 +34,7 @@ function loadJson(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.addEventListener('load', () => resolve(JSON.parse(xhr.responseText))); - xhr.open('GET', chrome.extension.getURL(url), true); + xhr.open('GET', chrome.extension.getURL(url)); xhr.send(); }); } diff --git a/ext/bg/js/yomichan.js b/ext/bg/js/yomichan.js index 46a240a3..04f29f42 100644 --- a/ext/bg/js/yomichan.js +++ b/ext/bg/js/yomichan.js @@ -23,10 +23,9 @@ class Yomichan { Handlebars.registerHelper('kanjiLinks', kanjiLinks); this.translator = new Translator(); + this.anki = new AnkiNull(); this.options = null; this.importTabId = null; - this.asyncPools = {}; - this.ankiConnectVer = 0; this.setState('disabled'); chrome.runtime.onMessage.addListener(this.onMessage.bind(this)); @@ -101,11 +100,20 @@ class Yomichan { setOptions(options) { this.options = options; - this.tabInvokeAll('setOptions', this.options); - } - getApiVersion() { - return 1; + switch (options.ankiMethod) { + case 'ankiweb': + this.anki = new AnkiWeb(options.ankiUsername, options.ankiPassword); + break; + case 'ankiconnect': + this.anki = new AnkiConnect(); + break; + default: + this.anki = new AnkiNull(); + break; + } + + this.tabInvokeAll('setOptions', this.options); } tabInvokeAll(action, params) { @@ -120,49 +128,12 @@ class Yomichan { chrome.tabs.sendMessage(tabId, {action, params}, () => null); } - ankiInvokeSafe(action, params, pool, callback) { - if (this.ankiConnectVer === this.getApiVersion()) { - this.ankiInvoke(action, params, pool, callback); - } else { - this.api_getVersion({callback: version => { - if (version === this.getApiVersion()) { - this.ankiConnectVer = version; - this.ankiInvoke(action, params, pool, callback); - } else { - callback(null); - } - }}); - } - } - - ankiInvoke(action, params, pool, callback) { - if (this.options.enableAnkiConnect) { - if (pool !== null && this.asyncPools.hasOwnProperty(pool)) { - this.asyncPools[pool].abort(); - } - - const xhr = new XMLHttpRequest(); - xhr.addEventListener('loadend', () => { - if (pool !== null) { - delete this.asyncPools[pool]; - } - - const resp = xhr.responseText; - callback(resp ? JSON.parse(resp) : null); - }); - - xhr.open('POST', 'http://127.0.0.1:8765'); - xhr.send(JSON.stringify({action, params})); - } else { - callback(null); - } - } - formatField(field, definition, mode) { const markers = [ 'audio', 'character', 'expression', + 'expression-furigana', 'glossary', 'glossary-list', 'kunyomi', @@ -184,6 +155,13 @@ class Yomichan { value = definition.reading; } break; + case 'expression-furigana': + if (mode === 'term_kana' && definition.reading) { + value = definition.reading; + } else { + value = `<ruby>${definition.expression}<rt>${definition.reading}</rt></ruby>`; + } + break; case 'reading': if (mode === 'term_kana') { value = null; @@ -257,12 +235,24 @@ class Yomichan { } api_getOptions({callback}) { - loadOptions().then(opts => callback(opts)); + loadOptions().then(opts => callback(opts)).catch(() => callback(null)); + } + + api_findKanji({text, callback}) { + this.translator.findKanji(text).then(result => callback(result)).catch(() => callback(null)); + } + + api_findTerm({text, callback}) { + this.translator.findTerm(text).then(result => callback(result)).catch(() => callback(null)); + } + + api_renderText({template, data, callback}) { + callback(Handlebars.templates[template](data)); } api_addDefinition({definition, mode, callback}) { const note = this.formatNote(definition, mode); - this.ankiInvokeSafe('addNote', {note}, null, callback); + this.anki.addNote(note).then(callback).catch(() => callback(null)); } api_canAddDefinitions({definitions, modes, callback}) { @@ -273,9 +263,8 @@ class Yomichan { } } - this.ankiInvokeSafe('canAddNotes', {notes}, 'notes', results => { + this.anki.canAddNotes(notes).then(results => { const states = []; - if (results !== null) { for (let resultBase = 0; resultBase < results.length; resultBase += modes.length) { const state = {}; @@ -288,35 +277,21 @@ class Yomichan { } callback(states); + }).catch(() => { + callback(null); }); } - api_findKanji({text, callback}) { - this.translator.findKanji(text).then(result => callback(result)); - } - - api_findTerm({text, callback}) { - this.translator.findTerm(text).then(result => callback(result)); - } - api_getDeckNames({callback}) { - this.ankiInvokeSafe('deckNames', {}, null, callback); + this.anki.getDeckNames().then(callback).catch(() => callback(null)); } api_getModelNames({callback}) { - this.ankiInvokeSafe('modelNames', {}, null, callback); + this.anki.getModelNames().then(callback).catch(() => callback(null)); } api_getModelFieldNames({modelName, callback}) { - this.ankiInvokeSafe('modelFieldNames', {modelName}, null, callback); - } - - api_getVersion({callback}) { - this.ankiInvoke('version', {}, null, callback); - } - - api_renderText({template, data, callback}) { - callback(Handlebars.templates[template](data)); + this.anki.getModelFieldNames(modelName).then(callback).catch(() => callback(null)); } } |