diff options
-rw-r--r-- | ext/bg/background.html | 3 | ||||
-rw-r--r-- | ext/bg/img/spinner.gif | bin | 0 -> 7358 bytes | |||
-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 | ||||
-rw-r--r-- | ext/bg/options.html | 152 | ||||
-rw-r--r-- | ext/fg/js/driver.js | 16 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 2 |
12 files changed, 506 insertions, 241 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html index c490df81..863d1ab4 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -3,6 +3,9 @@ <body> <script src="../lib/handlebars.min.js"></script> <script src="../lib/dexie.min.js"></script> + <script src="js/ankiweb.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/dictionary.js"></script> diff --git a/ext/bg/img/spinner.gif b/ext/bg/img/spinner.gif Binary files differnew file mode 100644 index 00000000..8ed30cb6 --- /dev/null +++ b/ext/bg/img/spinner.gif 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)); } } diff --git a/ext/bg/options.html b/ext/bg/options.html index f5bad2bd..b9be4c9d 100644 --- a/ext/bg/options.html +++ b/ext/bg/options.html @@ -6,7 +6,7 @@ <link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css"> <style> - .options-anki, .options-advanced { + #anki-spinner, #anki-general, #anki-error, #options-advanced { display: none; } @@ -16,15 +16,11 @@ border-right: 1px #ddd solid; padding: 10px; } - - .error-dlg { - display: none; - } </style> </head> <body> <div class="container"> - <div class="options-basic"> + <div> <h3>General Options</h3> <form class="form-horizontal"> @@ -47,14 +43,6 @@ <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <div class="checkbox"> - <label class="control-label"><input type="checkbox" id="enable-anki-connect"> Enable <a href="https://foosoft.net/projects/anki-connect">AnkiConnect</a></label> - </div> - </div> - </div> - - <div class="form-group"> - <div class="col-sm-offset-2 col-sm-10"> - <div class="checkbox"> <label class="control-label"><input type="checkbox" id="show-advanced-options"> Show advanced options</label> </div> </div> @@ -62,7 +50,7 @@ </form> </div> - <div class="options-basic"> + <div> <h3>Scanning Options</h3> <form class="form-horizontal"> @@ -94,19 +82,35 @@ </form> </div> - <div class="options-anki"> + <div> + <img src="img/spinner.gif" class="pull-right" id="anki-spinner" alt> + <h3>Anki Options</h3> - <div class="alert alert-danger error-dlg error-dlg-connection"> - <strong>Unable to Connect</strong><br> - Is the <a href="https://foosoft.net/projects/anki-connect">AnkiConnect</a> extension for <a href="http://ankisrs.net/">Anki</a> installed and running? This software is required for Anki-related features. + <div class="alert alert-danger" id="anki-error"> + <strong>Error:</strong> <span></span> </div> - <div class="alert alert-warning error-dlg error-dlg-version"> - <strong>Unsupported Version</strong><br> - The installed version of the <a href="https://foosoft.net/projects/anki-connect">AnkiConnect</a> extension for <a href="http://ankisrs.net/">Anki</a> is not compatible with this release; please update it. + + <div class="form-group"> + <label class="control-label" for="anki-method">Connection method</label> + <select class="form-control" id="anki-method"> + <option value="disabled">Disabled (no auto flashcard creation)</option> + <option value="ankiweb">AnkiWeb (requires an account on AnkiWeb)</option> + <option value="ankiconnect">AnkiConnect (requires the AnkiConnect plugin)</option> + </select> </div> - <form class="form-horizontal options-anki-controls"> + <form class="form-horizontal" id="anki-general"> + <div class="form-group anki-login"> + <label for="anki-username" class="control-label col-sm-2">Username</label> + <div class="col-sm-10"><input type="text" id="anki-username" class="form-control anki-credential"></div> + </div> + + <div class="form-group anki-login"> + <label for="anki-password" class="control-label col-sm-2">Password</label> + <div class="col-sm-10"><input type="password" id="anki-password" class="form-control anki-credential"></div> + </div> + <div class="form-group"> <label for="anki-card-tags" class="control-label col-sm-2">Card tags</label> <div class="col-sm-10"><input type="text" id="anki-card-tags" class="form-control"></div> @@ -117,70 +121,72 @@ <div class="col-sm-10"><input type="number" min="1" id="sentence-extent" class="form-control"></div> </div> - <ul class="nav nav-tabs col-sm-offset-2 col-sm-10"> - <li class="active"><a href="#term" data-toggle="tab">Terms</a></li> - <li><a href="#kanji" data-toggle="tab">Kanji</a></li> - </ul> - - <div class="tab-content col-sm-offset-2 col-sm-10"> - <div id="term" class="tab-pane fade in active"> - <div class="form-group"> - <label class="col-sm-2 control-label" for="anki-term-deck">Deck</label> - <div class="col-sm-10"> - <select class="form-control anki-deck" id="anki-term-deck"></select> + <div id="anki-format"> + <ul class="nav nav-tabs col-sm-offset-2 col-sm-10"> + <li class="active"><a href="#term" data-toggle="tab">Terms</a></li> + <li><a href="#kanji" data-toggle="tab">Kanji</a></li> + </ul> + + <div class="tab-content col-sm-offset-2 col-sm-10"> + <div id="term" class="tab-pane fade in active"> + <div class="form-group"> + <label class="col-sm-2 control-label" for="anki-term-deck">Deck</label> + <div class="col-sm-10"> + <select class="form-control anki-deck" id="anki-term-deck"></select> + </div> </div> - </div> - <div class="form-group"> - <label class="col-sm-2 control-label" for="anki-term-model">Model</label> - <div class="col-sm-10"> - <select class="form-control anki-model" id="anki-term-model"></select> + <div class="form-group"> + <label class="col-sm-2 control-label" for="anki-term-model">Model</label> + <div class="col-sm-10"> + <select class="form-control anki-model" id="anki-term-model"></select> + </div> </div> - </div> - <table class="table table-bordered anki-fields"> - <thead> - <tr> - <th>Field</th> - <th>Value</th> - </tr> - </thead> - <tbody> - </tbody> - </table> - </div> + <table class="table table-bordered anki-fields"> + <thead> + <tr> + <th>Field</th> + <th>Value</th> + </tr> + </thead> + <tbody> + </tbody> + </table> + </div> - <div id="kanji" class="tab-pane fade"> - <div class="form-group"> - <label class="col-sm-2 control-label" for="anki-kanji-deck">Deck</label> - <div class="col-sm-10"> - <select class="form-control anki-deck" id="anki-kanji-deck"></select> + <div id="kanji" class="tab-pane fade"> + <div class="form-group"> + <label class="col-sm-2 control-label" for="anki-kanji-deck">Deck</label> + <div class="col-sm-10"> + <select class="form-control anki-deck" id="anki-kanji-deck"></select> + </div> </div> - </div> - <div class="form-group"> - <label class="col-sm-2 control-label" for="anki-kanji-model">Model</label> - <div class="col-sm-10"> - <select class="form-control anki-model" id="anki-kanji-model"></select> + <div class="form-group"> + <label class="col-sm-2 control-label" for="anki-kanji-model">Model</label> + <div class="col-sm-10"> + <select class="form-control anki-model" id="anki-kanji-model"></select> + </div> </div> - </div> - <table class="table table-bordered anki-fields"> - <thead> - <tr> - <th>Field</th> - <th>Value</th> - </tr> - </thead> - <tbody> - </tbody> - </table> + <table class="table table-bordered anki-fields"> + <thead> + <tr> + <th>Field</th> + <th>Value</th> + </tr> + </thead> + <tbody> + </tbody> + </table> + </div> </div> </div> </form> </div> - <div style="text-align: right;"> + <div class="pull-right"> <small><a href="https://foosoft.net/projects/yomichan-chrome">Homepage</a> • <a href="legal.html">Legal</a></small> </div> </div> diff --git a/ext/fg/js/driver.js b/ext/fg/js/driver.js index 93f46622..1be69f11 100644 --- a/ext/fg/js/driver.js +++ b/ext/fg/js/driver.js @@ -37,7 +37,6 @@ class Driver { window.addEventListener('mousedown', this.onMouseDown.bind(this)); window.addEventListener('mousemove', this.onMouseMove.bind(this)); window.addEventListener('keydown', this.onKeyDown.bind(this)); - window.addEventListener('scroll', e => this.hidePopup()); window.addEventListener('resize', e => this.hidePopup()); getOptions().then(opts => { @@ -65,7 +64,7 @@ class Driver { if (this.enabled && this.lastMousePos !== null && e.keyCode === 16 /* shift */) { this.searchAt(this.lastMousePos, true); - } else if (e.keyCode === 27) { + } else if (e.keyCode === 27 /* esc */) { this.hidePopup(); } } @@ -77,13 +76,17 @@ class Driver { } onMouseMove(e) { + this.lastMousePos = {x: e.clientX, y: e.clientY}; this.popupTimerClear(); - this.lastMousePos = {x: e.clientX, y: e.clientY}; if (!this.enabled) { return; } + if (e.which === 1 /* lmb */) { + return; + } + if (this.options.holdShiftToScan && !e.shiftKey) { return; } @@ -98,11 +101,8 @@ class Driver { onMouseDown(e) { this.lastMousePos = {x: e.clientX, y: e.clientY}; - if (this.enabled && (e.shiftKey || !this.options.holdShiftToScan || e.which === 2 /* mmb */)) { - this.searchAt(this.lastMousePos, true); - } else { - this.hidePopup(); - } + this.popupTimerClear(); + this.hidePopup(); } onBgMessage({action, params}, sender, callback) { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 83da3fe1..2a2f7c54 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -66,6 +66,8 @@ class Popup { return; } + this.container.contentWindow.scrollTo(0, 0); + const doc = this.container.contentDocument; doc.open(); doc.write(content); |