aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ext/bg/background.html3
-rw-r--r--ext/bg/img/spinner.gifbin0 -> 7358 bytes
-rw-r--r--ext/bg/js/ankiconnect.js82
-rw-r--r--ext/bg/js/ankinull.js39
-rw-r--r--ext/bg/js/ankiweb.js148
-rw-r--r--ext/bg/js/options-form.js189
-rw-r--r--ext/bg/js/options.js5
-rw-r--r--ext/bg/js/util.js2
-rw-r--r--ext/bg/js/yomichan.js109
-rw-r--r--ext/bg/options.html152
-rw-r--r--ext/fg/js/driver.js16
-rw-r--r--ext/fg/js/popup.js2
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
new file mode 100644
index 00000000..8ed30cb6
--- /dev/null
+++ b/ext/bg/img/spinner.gif
Binary files differ
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> &bull; <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);