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
|
Marker | Description
|
||||||
-------|------------
|
-------|------------
|
||||||
`{audio}` | Audio sample of a native speaker's pronunciation in MP3 format (if available).
|
`{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-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-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}`.
|
`{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
|
Marker | Description
|
||||||
-------|------------
|
-------|------------
|
||||||
`{character}` | Unicode glyph representing the current kanji.
|
`{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-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-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}`.
|
`{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}}
|
{{/inline}}
|
||||||
{{! End Pitch Accents }}
|
{{! End Pitch Accents }}
|
||||||
|
|
||||||
|
{{#*inline "clipboard-image"}}
|
||||||
|
{{~#if definition.clipboardImageFileName~}}
|
||||||
|
<img src="{{definition.clipboardImageFileName}}" />
|
||||||
|
{{~/if~}}
|
||||||
|
{{/inline}}
|
||||||
|
|
||||||
{{~> (lookup . "marker") ~}}
|
{{~> (lookup . "marker") ~}}
|
||||||
|
@ -20,10 +20,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class AnkiNoteBuilder {
|
class AnkiNoteBuilder {
|
||||||
constructor({anki, audioSystem, renderTemplate}) {
|
constructor({anki, audioSystem, renderTemplate, getClipboardImage=null}) {
|
||||||
this._anki = anki;
|
this._anki = anki;
|
||||||
this._audioSystem = audioSystem;
|
this._audioSystem = audioSystem;
|
||||||
this._renderTemplate = renderTemplate;
|
this._renderTemplate = renderTemplate;
|
||||||
|
this._getClipboardImage = getClipboardImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNote(definition, mode, context, options, templates) {
|
async createNote(definition, mode, context, options, templates) {
|
||||||
@ -138,6 +139,31 @@ class AnkiNoteBuilder {
|
|||||||
definition.screenshotFileName = fileName;
|
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) {
|
_createInjectedAudioFileName(definition) {
|
||||||
const {reading, expression} = definition;
|
const {reading, expression} = definition;
|
||||||
if (!reading && !expression) { return null; }
|
if (!reading && !expression) { return null; }
|
||||||
@ -170,6 +196,16 @@ class AnkiNoteBuilder {
|
|||||||
return false;
|
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) {
|
static replaceInvalidFileNameCharacters(fileName) {
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-');
|
return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-');
|
||||||
|
@ -60,7 +60,8 @@ class Backend {
|
|||||||
this._ankiNoteBuilder = new AnkiNoteBuilder({
|
this._ankiNoteBuilder = new AnkiNoteBuilder({
|
||||||
anki: this._anki,
|
anki: this._anki,
|
||||||
audioSystem: this._audioSystem,
|
audioSystem: this._audioSystem,
|
||||||
renderTemplate: this._renderTemplate.bind(this)
|
renderTemplate: this._renderTemplate.bind(this),
|
||||||
|
getClipboardImage: this._onApiClipboardImageGet.bind(this)
|
||||||
});
|
});
|
||||||
this._templateRenderer = new TemplateRenderer();
|
this._templateRenderer = new TemplateRenderer();
|
||||||
|
|
||||||
@ -444,21 +445,28 @@ class Backend {
|
|||||||
async _onApiDefinitionAdd({definition, mode, context, details, optionsContext}) {
|
async _onApiDefinitionAdd({definition, mode, context, details, optionsContext}) {
|
||||||
const options = this.getOptions(optionsContext);
|
const options = this.getOptions(optionsContext);
|
||||||
const templates = this._getTemplates(options);
|
const templates = this._getTemplates(options);
|
||||||
|
const fields = (
|
||||||
|
mode === 'kanji' ?
|
||||||
|
options.anki.kanji.fields :
|
||||||
|
options.anki.terms.fields
|
||||||
|
);
|
||||||
|
|
||||||
if (mode !== 'kanji') {
|
if (mode !== 'kanji') {
|
||||||
const {customSourceUrl} = options.audio;
|
const {customSourceUrl} = options.audio;
|
||||||
await this._ankiNoteBuilder.injectAudio(
|
await this._ankiNoteBuilder.injectAudio(
|
||||||
definition,
|
definition,
|
||||||
options.anki.terms.fields,
|
fields,
|
||||||
options.audio.sources,
|
options.audio.sources,
|
||||||
customSourceUrl
|
customSourceUrl
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this._ankiNoteBuilder.injectClipboardImage(definition, fields);
|
||||||
|
|
||||||
if (details && details.screenshot) {
|
if (details && details.screenshot) {
|
||||||
await this._ankiNoteBuilder.injectScreenshot(
|
await this._ankiNoteBuilder.injectScreenshot(
|
||||||
definition,
|
definition,
|
||||||
options.anki.terms.fields,
|
fields,
|
||||||
details.screenshot
|
details.screenshot
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -432,7 +432,7 @@ class OptionsUtil {
|
|||||||
update: this._updateVersion3.bind(this)
|
update: this._updateVersion3.bind(this)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
async: false,
|
async: true,
|
||||||
update: this._updateVersion4.bind(this)
|
update: this._updateVersion4.bind(this)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -468,10 +468,11 @@ class OptionsUtil {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
static _updateVersion4(options) {
|
static async _updateVersion4(options) {
|
||||||
// Version 4 changes:
|
// Version 4 changes:
|
||||||
// Options conditions converted to string representations.
|
// Options conditions converted to string representations.
|
||||||
// Added usePopupWindow.
|
// Added usePopupWindow.
|
||||||
|
// Updated handlebars templates to include "clipboard-image" definition.
|
||||||
for (const {conditionGroups} of options.profiles) {
|
for (const {conditionGroups} of options.profiles) {
|
||||||
for (const {conditions} of conditionGroups) {
|
for (const {conditions} of conditionGroups) {
|
||||||
for (const condition of conditions) {
|
for (const condition of conditions) {
|
||||||
@ -487,6 +488,7 @@ class OptionsUtil {
|
|||||||
for (const {options: profileOptions} of options.profiles) {
|
for (const {options: profileOptions} of options.profiles) {
|
||||||
profileOptions.general.usePopupWindow = false;
|
profileOptions.general.usePopupWindow = false;
|
||||||
}
|
}
|
||||||
|
await this._addFieldTemplatesToOptions(options, '/bg/data/anki-field-templates-upgrade-v4.handlebars');
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,10 @@ class AnkiTemplatesController {
|
|||||||
};
|
};
|
||||||
let templates = options.anki.fieldTemplates;
|
let templates = options.anki.fieldTemplates;
|
||||||
if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; }
|
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);
|
const data = ankiNoteBuilder.createNoteData(definition, mode, context, options);
|
||||||
result = await ankiNoteBuilder.formatField(field, data, templates, exceptions);
|
result = await ankiNoteBuilder.formatField(field, data, templates, exceptions);
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ class AnkiController {
|
|||||||
case 'terms':
|
case 'terms':
|
||||||
return [
|
return [
|
||||||
'audio',
|
'audio',
|
||||||
|
'clipboard-image',
|
||||||
'cloze-body',
|
'cloze-body',
|
||||||
'cloze-prefix',
|
'cloze-prefix',
|
||||||
'cloze-suffix',
|
'cloze-suffix',
|
||||||
@ -66,6 +67,7 @@ class AnkiController {
|
|||||||
case 'kanji':
|
case 'kanji':
|
||||||
return [
|
return [
|
||||||
'character',
|
'character',
|
||||||
|
'clipboard-image',
|
||||||
'cloze-body',
|
'cloze-body',
|
||||||
'cloze-prefix',
|
'cloze-prefix',
|
||||||
'cloze-suffix',
|
'cloze-suffix',
|
||||||
|
Loading…
Reference in New Issue
Block a user