diff --git a/README.md b/README.md index b913fe25..d8d45292 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Flashcard fields can be configured with the following steps: Marker | Description -------|------------ `{audio}` | Audio sample of a native speaker's pronunciation in MP3 format (if available). + `{clipboard-image}` | An image which is stored in the system clipboard, if present. `{cloze-body}` | Raw, inflected term as it appeared before being reduced to dictionary form by Yomichan. `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. @@ -178,6 +179,7 @@ Flashcard fields can be configured with the following steps: Marker | Description -------|------------ `{character}` | Unicode glyph representing the current kanji. + `{clipboard-image}` | An image which is stored in the system clipboard, if present. `{cloze-body}` | Raw, inflected parent term as it appeared before being reduced to dictionary form by Yomichan. `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. diff --git a/ext/bg/data/anki-field-templates-upgrade-v4.handlebars b/ext/bg/data/anki-field-templates-upgrade-v4.handlebars new file mode 100644 index 00000000..a16b5b68 --- /dev/null +++ b/ext/bg/data/anki-field-templates-upgrade-v4.handlebars @@ -0,0 +1,5 @@ +{{#*inline "clipboard-image"}} + {{~#if definition.clipboardImageFileName~}} + + {{~/if~}} +{{/inline}} diff --git a/ext/bg/data/default-anki-field-templates.handlebars b/ext/bg/data/default-anki-field-templates.handlebars index b348042c..98f6897b 100644 --- a/ext/bg/data/default-anki-field-templates.handlebars +++ b/ext/bg/data/default-anki-field-templates.handlebars @@ -276,4 +276,10 @@ {{/inline}} {{! End Pitch Accents }} +{{#*inline "clipboard-image"}} + {{~#if definition.clipboardImageFileName~}} + + {{~/if~}} +{{/inline}} + {{~> (lookup . "marker") ~}} diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index 041e6dcd..d69a4fea 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -20,10 +20,11 @@ */ class AnkiNoteBuilder { - constructor({anki, audioSystem, renderTemplate}) { + constructor({anki, audioSystem, renderTemplate, getClipboardImage=null}) { this._anki = anki; this._audioSystem = audioSystem; this._renderTemplate = renderTemplate; + this._getClipboardImage = getClipboardImage; } async createNote(definition, mode, context, options, templates) { @@ -138,6 +139,31 @@ class AnkiNoteBuilder { definition.screenshotFileName = fileName; } + async injectClipboardImage(definition, fields) { + if (!this._containsMarker(fields, 'clipboard-image')) { return; } + + const reading = definition.reading; + const now = new Date(Date.now()); + + try { + const dataUrl = await this._getClipboardImage(); + if (dataUrl === null) { return; } + + const extension = this._getImageExtensionFromDataUrl(dataUrl); + if (extension === null) { return; } + + let fileName = `yomichan_clipboard_image_${reading}_${this._dateToString(now)}.${extension}`; + fileName = AnkiNoteBuilder.replaceInvalidFileNameCharacters(fileName); + const data = dataUrl.replace(/^data:[\w\W]*?,/, ''); + + await this._anki.storeMediaFile(fileName, data); + + definition.clipboardImageFileName = fileName; + } catch (e) { + // NOP + } + } + _createInjectedAudioFileName(definition) { const {reading, expression} = definition; if (!reading && !expression) { return null; } @@ -170,6 +196,16 @@ class AnkiNoteBuilder { return false; } + _getImageExtensionFromDataUrl(dataUrl) { + const match = /^data:([^;]*);/.exec(dataUrl); + if (match === null) { return null; } + switch (match[1].toLowerCase()) { + case 'image/png': return 'png'; + case 'image/jpeg': return 'jpeg'; + default: return null; + } + } + static replaceInvalidFileNameCharacters(fileName) { // eslint-disable-next-line no-control-regex return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-'); diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 047044df..e9f3fbec 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -60,7 +60,8 @@ class Backend { this._ankiNoteBuilder = new AnkiNoteBuilder({ anki: this._anki, audioSystem: this._audioSystem, - renderTemplate: this._renderTemplate.bind(this) + renderTemplate: this._renderTemplate.bind(this), + getClipboardImage: this._onApiClipboardImageGet.bind(this) }); this._templateRenderer = new TemplateRenderer(); @@ -444,21 +445,28 @@ class Backend { async _onApiDefinitionAdd({definition, mode, context, details, optionsContext}) { const options = this.getOptions(optionsContext); const templates = this._getTemplates(options); + const fields = ( + mode === 'kanji' ? + options.anki.kanji.fields : + options.anki.terms.fields + ); if (mode !== 'kanji') { const {customSourceUrl} = options.audio; await this._ankiNoteBuilder.injectAudio( definition, - options.anki.terms.fields, + fields, options.audio.sources, customSourceUrl ); } + await this._ankiNoteBuilder.injectClipboardImage(definition, fields); + if (details && details.screenshot) { await this._ankiNoteBuilder.injectScreenshot( definition, - options.anki.terms.fields, + fields, details.screenshot ); } diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index fc2403e1..9dc0c166 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -432,7 +432,7 @@ class OptionsUtil { update: this._updateVersion3.bind(this) }, { - async: false, + async: true, update: this._updateVersion4.bind(this) } ]; @@ -468,10 +468,11 @@ class OptionsUtil { return options; } - static _updateVersion4(options) { + static async _updateVersion4(options) { // Version 4 changes: // Options conditions converted to string representations. // Added usePopupWindow. + // Updated handlebars templates to include "clipboard-image" definition. for (const {conditionGroups} of options.profiles) { for (const {conditions} of conditionGroups) { for (const condition of conditions) { @@ -487,6 +488,7 @@ class OptionsUtil { for (const {options: profileOptions} of options.profiles) { profileOptions.general.usePopupWindow = false; } + await this._addFieldTemplatesToOptions(options, '/bg/data/anki-field-templates-upgrade-v4.handlebars'); return options; } } diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js index 4e004308..fb03ef14 100644 --- a/ext/bg/js/settings/anki-templates.js +++ b/ext/bg/js/settings/anki-templates.js @@ -143,7 +143,10 @@ class AnkiTemplatesController { }; let templates = options.anki.fieldTemplates; if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; } - const ankiNoteBuilder = new AnkiNoteBuilder({renderTemplate: api.templateRender.bind(api)}); + const ankiNoteBuilder = new AnkiNoteBuilder({ + renderTemplate: api.templateRender.bind(api), + getClipboardImage: api.clipboardGetImage.bind(api) + }); const data = ankiNoteBuilder.createNoteData(definition, mode, context, options); result = await ankiNoteBuilder.formatField(field, data, templates, exceptions); } diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js index ac4c5455..0965e633 100644 --- a/ext/bg/js/settings/anki.js +++ b/ext/bg/js/settings/anki.js @@ -44,6 +44,7 @@ class AnkiController { case 'terms': return [ 'audio', + 'clipboard-image', 'cloze-body', 'cloze-prefix', 'cloze-suffix', @@ -66,6 +67,7 @@ class AnkiController { case 'kanji': return [ 'character', + 'clipboard-image', 'cloze-body', 'cloze-prefix', 'cloze-suffix',