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',