Anki note clipboard marker (#780)
* Update fields reference * Add support for adding clipboard images to anki notes * Add handlebars templates * Add markers * Add markers to readme
This commit is contained in:
parent
36fc5abae5
commit
f7093b4c1a
@ -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}`.
|
||||
|
5
ext/bg/data/anki-field-templates-upgrade-v4.handlebars
Normal file
5
ext/bg/data/anki-field-templates-upgrade-v4.handlebars
Normal file
@ -0,0 +1,5 @@
|
||||
{{#*inline "clipboard-image"}}
|
||||
{{~#if definition.clipboardImageFileName~}}
|
||||
<img src="{{definition.clipboardImageFileName}}" />
|
||||
{{~/if~}}
|
||||
{{/inline}}
|
@ -276,4 +276,10 @@
|
||||
{{/inline}}
|
||||
{{! End Pitch Accents }}
|
||||
|
||||
{{#*inline "clipboard-image"}}
|
||||
{{~#if definition.clipboardImageFileName~}}
|
||||
<img src="{{definition.clipboardImageFileName}}" />
|
||||
{{~/if~}}
|
||||
{{/inline}}
|
||||
|
||||
{{~> (lookup . "marker") ~}}
|
||||
|
@ -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, '-');
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user