diff --git a/ext/bg/background.html b/ext/bg/background.html index 93e4b5c7..2f3fd441 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -24,7 +24,6 @@ - @@ -36,7 +35,6 @@ - diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 3d1b1544..b9be9d0a 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -17,7 +17,6 @@ /* global * AnkiConnect - * AnkiNoteBuilder * AudioSystem * AudioUriBuilder * ClipboardMonitor @@ -29,7 +28,6 @@ * OptionsUtil * ProfileConditions * RequestBuilder - * TemplateRenderer * Translator * jp */ @@ -57,10 +55,6 @@ class Backend { requestBuilder: this._requestBuilder, useCache: false }); - this._ankiNoteBuilder = new AnkiNoteBuilder({ - renderTemplate: this._renderTemplate.bind(this) - }); - this._templateRenderer = new TemplateRenderer(); this._clipboardPasteTarget = null; this._clipboardPasteTargetInitialized = false; @@ -94,10 +88,7 @@ class Backend { ['addAnkiNote', {async: true, contentScript: true, handler: this._onApiAddAnkiNote.bind(this)}], ['getAnkiNoteInfo', {async: true, contentScript: true, handler: this._onApiGetAnkiNoteInfo.bind(this)}], ['injectAnkiNoteMedia', {async: true, contentScript: true, handler: this._onApiInjectAnkiNoteMedia.bind(this)}], - ['definitionAdd', {async: true, contentScript: true, handler: this._onApiDefinitionAdd.bind(this)}], - ['definitionsAddable', {async: true, contentScript: true, handler: this._onApiDefinitionsAddable.bind(this)}], ['noteView', {async: true, contentScript: true, handler: this._onApiNoteView.bind(this)}], - ['templateRender', {async: true, contentScript: true, handler: this._onApiTemplateRender.bind(this)}], ['commandExec', {async: false, contentScript: true, handler: this._onApiCommandExec.bind(this)}], ['audioGetUri', {async: true, contentScript: true, handler: this._onApiAudioGetUri.bind(this)}], ['screenshotGet', {async: true, contentScript: true, handler: this._onApiScreenshotGet.bind(this)}], @@ -473,7 +464,11 @@ class Backend { return results; } - async _onApiInjectAnkiNoteMedia({expression, reading, timestamp, audioDetails, screenshotDetails, clipboardImage}) { + async _onApiInjectAnkiNoteMedia({expression, reading, timestamp, audioDetails, screenshotDetails, clipboardImage}, sender) { + if (isObject(screenshotDetails)) { + const {id: tabId, windowId} = (sender && sender.tab ? sender.tab : {}); + screenshotDetails = Object.assign({}, screenshotDetails, {tabId, windowId}); + } return await this._injectAnkNoteMedia( this._anki, expression, @@ -485,45 +480,10 @@ class Backend { ); } - async _onApiDefinitionAdd({definition, mode, context, ownerFrameId, optionsContext}, sender) { - const options = this.getOptions(optionsContext); - const templates = this._getTemplates(options); - const {id: tabId, windowId} = (sender && sender.tab ? sender.tab : {}); - const note = await this._createNote(definition, mode, context, options, templates, true, {windowId, tabId, ownerFrameId}); - return await this._onApiAddAnkiNote({note}); - } - - async _onApiDefinitionsAddable({definitions, modes, context, optionsContext}) { - const options = this.getOptions(optionsContext); - const templates = this._getTemplates(options); - - const modeCount = modes.length; - const {duplicateScope} = options.anki; - const notePromises = []; - for (const definition of definitions) { - for (const mode of modes) { - const notePromise = this._createNote(definition, mode, context, options, templates, false, null); - notePromises.push(notePromise); - } - } - const notes = await Promise.all(notePromises); - - const infos = await this._onApiGetAnkiNoteInfo({notes, duplicateScope}); - const results = []; - for (let i = 0, ii = infos.length; i < ii; i += modeCount) { - results.push(infos.slice(i, i + modeCount)); - } - return results; - } - async _onApiNoteView({noteId}) { return await this._anki.guiBrowseNote(noteId); } - async _onApiTemplateRender({template, data, marker}) { - return this._renderTemplate(template, data, marker); - } - _onApiCommandExec({command, params}) { return this._runCommand(command, params); } @@ -1381,10 +1341,6 @@ class Backend { return false; } - async _renderTemplate(template, data, marker) { - return await this._templateRenderer.render(template, data, marker); - } - _getTemplates(options) { const templates = options.anki.fieldTemplates; return typeof templates === 'string' ? templates : this._defaultAnkiFieldTemplates; @@ -1595,52 +1551,6 @@ class Backend { }); } - async _createNote(definition, mode, context, options, templates, injectMedia, screenshotTarget) { - const { - general: {resultOutputMode, compactGlossaries}, - anki: {tags, duplicateScope, kanji, terms, screenshot: {format, quality}}, - audio: {sources, customSourceUrl} - } = options; - const modeOptions = (mode === 'kanji') ? kanji : terms; - const {windowId, tabId, ownerFrameId} = (isObject(screenshotTarget) ? screenshotTarget : {}); - - if (injectMedia) { - const fields = modeOptions.fields; - const timestamp = Date.now(); - const definitionExpressions = definition.expressions; - const {expression, reading} = Array.isArray(definitionExpressions) ? definitionExpressions[0] : definition; - const audioDetails = (mode !== 'kanji' && this._ankiNoteBuilder.containsMarker(fields, 'audio') ? {sources, customSourceUrl} : null); - const screenshotDetails = (this._ankiNoteBuilder.containsMarker(fields, 'screenshot') ? {windowId, tabId, ownerFrameId, format, quality} : null); - const clipboardImage = (this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image')); - const {screenshotFileName, clipboardImageFileName, audioFileName} = await this._onApiInjectAnkiNoteMedia({ - expression, - reading, - timestamp, - audioDetails, - screenshotDetails, - clipboardImage - }); - if (screenshotFileName !== null) { definition.screenshotFileName = screenshotFileName; } - if (clipboardImageFileName !== null) { definition.clipboardImageFileName = clipboardImageFileName; } - if (audioFileName !== null) { definition.audioFileName = audioFileName; } - } - - return await this._ankiNoteBuilder.createNote({ - definition, - mode, - context, - templates, - tags, - duplicateScope, - resultOutputMode, - compactGlossaries, - modeOptions, - audioDetails: {sources, customSourceUrl}, - screenshotDetails: {windowId, tabId, ownerFrameId, format, quality}, - clipboardImage: true - }); - } - async _getScreenshot(windowId, tabId, ownerFrameId, format, quality) { if (typeof windowId !== 'number') { throw new Error('Invalid window ID'); diff --git a/ext/bg/js/settings/anki-templates-controller.js b/ext/bg/js/settings/anki-templates-controller.js index c05301df..c53e4553 100644 --- a/ext/bg/js/settings/anki-templates-controller.js +++ b/ext/bg/js/settings/anki-templates-controller.js @@ -17,6 +17,7 @@ /* global * AnkiNoteBuilder + * TemplateRenderer * api */ @@ -27,6 +28,7 @@ class AnkiTemplatesController { this._cachedDefinitionValue = null; this._cachedDefinitionText = null; this._defaultFieldTemplates = null; + this._templateRenderer = new TemplateRenderer(); } async prepare() { @@ -144,7 +146,7 @@ class AnkiTemplatesController { let templates = options.anki.fieldTemplates; if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } const ankiNoteBuilder = new AnkiNoteBuilder({ - renderTemplate: api.templateRender.bind(api) + renderTemplate: this._renderTemplate.bind(this) }); const {general: {resultOutputMode, compactGlossaries}} = options; const note = await ankiNoteBuilder.createNote({ @@ -176,4 +178,8 @@ class AnkiTemplatesController { input.classList.toggle('is-invalid', hasException); } } + + async _renderTemplate(template, data, marker) { + return await this._templateRenderer.render(template, data, marker); + } } diff --git a/ext/bg/search.html b/ext/bg/search.html index f18bed0d..ae117fd1 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -91,6 +91,9 @@ + + + diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 5ee23785..5e6af7f7 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1183,6 +1183,7 @@ + diff --git a/ext/fg/float.html b/ext/fg/float.html index 725b7d8b..98e849f0 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -44,6 +44,8 @@ + + @@ -66,6 +68,9 @@ + + + diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 4a1b0c11..1e7625da 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -89,22 +89,10 @@ const api = (() => { return this._invoke('injectAnkiNoteMedia', {expression, reading, timestamp, audioDetails, screenshotDetails, clipboardImage}); } - definitionAdd(definition, mode, context, ownerFrameId, optionsContext) { - return this._invoke('definitionAdd', {definition, mode, context, ownerFrameId, optionsContext}); - } - - definitionsAddable(definitions, modes, context, optionsContext) { - return this._invoke('definitionsAddable', {definitions, modes, context, optionsContext}); - } - noteView(noteId) { return this._invoke('noteView', {noteId}); } - templateRender(template, data, marker) { - return this._invoke('templateRender', {data, template, marker}); - } - audioGetUri(source, expression, reading, details) { return this._invoke('audioGetUri', {source, expression, reading, details}); } diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 7dd5920a..4c42a48d 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -16,6 +16,7 @@ */ /* global + * AnkiNoteBuilder * AudioSystem * DisplayGenerator * DisplayHistory @@ -24,6 +25,7 @@ * MediaLoader * PopupFactory * QueryParser + * TemplateRenderer * WindowScroll * api * dynamicLoader @@ -83,6 +85,12 @@ class Display extends EventDispatcher { }); this._mode = null; this._ownerFrameId = null; + this._defaultAnkiFieldTemplates = null; + this._defaultAnkiFieldTemplatesPromise = null; + this._templateRenderer = new TemplateRenderer(); + this._ankiNoteBuilder = new AnkiNoteBuilder({ + renderTemplate: this._renderTemplate.bind(this) + }); this.registerActions([ ['close', () => { this.onEscape(); }], @@ -891,7 +899,8 @@ class Display extends EventDispatcher { const modes = isTerms ? ['term-kanji', 'term-kana'] : ['kanji']; let states; try { - states = await this._getDefinitionsAddable(definitions, modes); + const noteContext = await this._getNoteContext(); + states = await this._areDefinitionsAddable(definitions, modes, noteContext); } catch (e) { return; } @@ -1054,10 +1063,8 @@ class Display extends EventDispatcher { try { this.setSpinnerVisible(true); - const ownerFrameId = this._ownerFrameId; - const optionsContext = this.getOptionsContext(); const noteContext = await this._getNoteContext(); - const noteId = await api.definitionAdd(definition, mode, noteContext, ownerFrameId, optionsContext); + const noteId = await this._addDefinition(definition, mode, noteContext); if (noteId) { const index = this._definitions.indexOf(definition); const adderButton = this._adderButtonFind(index, mode); @@ -1196,15 +1203,6 @@ class Display extends EventDispatcher { return container !== null ? container.querySelector('.action-play-audio>img') : null; } - async _getDefinitionsAddable(definitions, modes) { - try { - const noteContext = await this._getNoteContext(); - return await api.definitionsAddable(definitions, modes, noteContext, this.getOptionsContext()); - } catch (e) { - return []; - } - } - _indexOf(nodeList, node) { for (let i = 0, ii = nodeList.length; i < ii; ++i) { if (nodeList[i] === node) { @@ -1315,4 +1313,108 @@ class Display extends EventDispatcher { this._mode = mode; this.trigger('modeChange', {mode}); } + + async _getTemplates(options) { + let templates = options.anki.fieldTemplates; + if (typeof templates === 'string') { return templates; } + + templates = this._defaultAnkiFieldTemplates; + if (typeof templates === 'string') { return templates; } + + return await this._getDefaultTemplatesPromise(); + } + + _getDefaultTemplatesPromise() { + if (this._defaultAnkiFieldTemplatesPromise === null) { + this._defaultAnkiFieldTemplatesPromise = this._getDefaultTemplates(); + this._defaultAnkiFieldTemplatesPromise.then( + () => { this._defaultAnkiFieldTemplatesPromise = null; }, + () => {} // NOP + ); + } + return this._defaultAnkiFieldTemplatesPromise; + } + + async _getDefaultTemplates() { + const value = await api.getDefaultAnkiFieldTemplates(); + this._defaultAnkiFieldTemplates = value; + return value; + } + + async _renderTemplate(template, data, marker) { + return await this._templateRenderer.render(template, data, marker); + } + + async _addDefinition(definition, mode, context) { + const options = this._options; + const templates = await this._getTemplates(options); + const note = await this._createNote(definition, mode, context, options, templates, true); + return await api.addAnkiNote(note); + } + + async _areDefinitionsAddable(definitions, modes, context) { + const options = this._options; + const templates = await this._getTemplates(options); + + const modeCount = modes.length; + const {duplicateScope} = options.anki; + const notePromises = []; + for (const definition of definitions) { + for (const mode of modes) { + const notePromise = this._createNote(definition, mode, context, options, templates, false); + notePromises.push(notePromise); + } + } + const notes = await Promise.all(notePromises); + + const infos = await api.getAnkiNoteInfo(notes, duplicateScope); + const results = []; + for (let i = 0, ii = infos.length; i < ii; i += modeCount) { + results.push(infos.slice(i, i + modeCount)); + } + return results; + } + + async _createNote(definition, mode, context, options, templates, injectMedia) { + const { + general: {resultOutputMode, compactGlossaries}, + anki: {tags, duplicateScope, kanji, terms, screenshot: {format, quality}}, + audio: {sources, customSourceUrl} + } = options; + const modeOptions = (mode === 'kanji') ? kanji : terms; + + if (injectMedia) { + const timestamp = Date.now(); + const ownerFrameId = this._ownerFrameId; + const {fields} = modeOptions; + const definitionExpressions = definition.expressions; + const {expression, reading} = Array.isArray(definitionExpressions) ? definitionExpressions[0] : definition; + const audioDetails = (mode !== 'kanji' && this._ankiNoteBuilder.containsMarker(fields, 'audio') ? {sources, customSourceUrl} : null); + const screenshotDetails = (this._ankiNoteBuilder.containsMarker(fields, 'screenshot') ? {ownerFrameId, format, quality} : null); + const clipboardImage = (this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image')); + const {screenshotFileName, clipboardImageFileName, audioFileName} = await api.injectAnkiNoteMedia( + expression, + reading, + timestamp, + audioDetails, + screenshotDetails, + clipboardImage + ); + if (screenshotFileName !== null) { definition.screenshotFileName = screenshotFileName; } + if (clipboardImageFileName !== null) { definition.clipboardImageFileName = clipboardImageFileName; } + if (audioFileName !== null) { definition.audioFileName = audioFileName; } + } + + return await this._ankiNoteBuilder.createNote({ + definition, + mode, + context, + templates, + tags, + duplicateScope, + resultOutputMode, + compactGlossaries, + modeOptions + }); + } }