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.gifBinary files differ new 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); |