Anki media injection move (#793)
* Update AnkiNoteBuilder to not store a reference to an AniConnect instance * Use more consistent details format * Organize options assignment * Move media injection * Inject images before injecting audio * Make functions private * Make static functions private
This commit is contained in:
parent
c0a6849f98
commit
acb7ad32f3
@ -20,8 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class AnkiNoteBuilder {
|
class AnkiNoteBuilder {
|
||||||
constructor({anki, audioSystem, renderTemplate, getClipboardImage=null, getScreenshot=null}) {
|
constructor({audioSystem, renderTemplate, getClipboardImage=null, getScreenshot=null}) {
|
||||||
this._anki = anki;
|
|
||||||
this._audioSystem = audioSystem;
|
this._audioSystem = audioSystem;
|
||||||
this._renderTemplate = renderTemplate;
|
this._renderTemplate = renderTemplate;
|
||||||
this._getClipboardImage = getClipboardImage;
|
this._getClipboardImage = getClipboardImage;
|
||||||
@ -29,6 +28,7 @@ class AnkiNoteBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNote({
|
async createNote({
|
||||||
|
anki=null,
|
||||||
definition,
|
definition,
|
||||||
mode,
|
mode,
|
||||||
context,
|
context,
|
||||||
@ -38,8 +38,15 @@ class AnkiNoteBuilder {
|
|||||||
resultOutputMode='split',
|
resultOutputMode='split',
|
||||||
compactGlossaries=false,
|
compactGlossaries=false,
|
||||||
modeOptions: {fields, deck, model},
|
modeOptions: {fields, deck, model},
|
||||||
|
audioDetails=null,
|
||||||
|
screenshotDetails=null,
|
||||||
|
clipboardImage=false,
|
||||||
errors=null
|
errors=null
|
||||||
}) {
|
}) {
|
||||||
|
if (anki !== null) {
|
||||||
|
await this._injectMedia(anki, definition, fields, mode, audioDetails, screenshotDetails, clipboardImage);
|
||||||
|
}
|
||||||
|
|
||||||
const fieldEntries = Object.entries(fields);
|
const fieldEntries = Object.entries(fields);
|
||||||
const noteFields = {};
|
const noteFields = {};
|
||||||
const note = {
|
const note = {
|
||||||
@ -50,10 +57,10 @@ class AnkiNoteBuilder {
|
|||||||
options: {duplicateScope}
|
options: {duplicateScope}
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = this.createNoteData(definition, mode, context, resultOutputMode, compactGlossaries);
|
const data = this._createNoteData(definition, mode, context, resultOutputMode, compactGlossaries);
|
||||||
const formattedFieldValuePromises = [];
|
const formattedFieldValuePromises = [];
|
||||||
for (const [, fieldValue] of fieldEntries) {
|
for (const [, fieldValue] of fieldEntries) {
|
||||||
const formattedFieldValuePromise = this.formatField(fieldValue, data, templates, errors);
|
const formattedFieldValuePromise = this._formatField(fieldValue, data, templates, errors);
|
||||||
formattedFieldValuePromises.push(formattedFieldValuePromise);
|
formattedFieldValuePromises.push(formattedFieldValuePromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +74,9 @@ class AnkiNoteBuilder {
|
|||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
createNoteData(definition, mode, context, resultOutputMode, compactGlossaries) {
|
// Private
|
||||||
|
|
||||||
|
_createNoteData(definition, mode, context, resultOutputMode, compactGlossaries) {
|
||||||
const pitches = DictionaryDataUtil.getPitchAccentInfos(definition);
|
const pitches = DictionaryDataUtil.getPitchAccentInfos(definition);
|
||||||
const pitchCount = pitches.reduce((i, v) => i + v.pitches.length, 0);
|
const pitchCount = pitches.reduce((i, v) => i + v.pitches.length, 0);
|
||||||
return {
|
return {
|
||||||
@ -85,9 +94,9 @@ class AnkiNoteBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async formatField(field, data, templates, errors=null) {
|
async _formatField(field, data, templates, errors=null) {
|
||||||
const pattern = /\{([\w-]+)\}/g;
|
const pattern = /\{([\w-]+)\}/g;
|
||||||
return await AnkiNoteBuilder.stringReplaceAsync(field, pattern, async (g0, marker) => {
|
return await this._stringReplaceAsync(field, pattern, async (g0, marker) => {
|
||||||
try {
|
try {
|
||||||
return await this._renderTemplate(templates, data, marker);
|
return await this._renderTemplate(templates, data, marker);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -97,16 +106,29 @@ class AnkiNoteBuilder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async injectAudio(definition, fields, sources, customSourceUrl) {
|
async _injectMedia(anki, definition, fields, mode, audioDetails, screenshotDetails, clipboardImage) {
|
||||||
|
if (screenshotDetails !== null) {
|
||||||
|
await this._injectScreenshot(anki, definition, fields, screenshotDetails);
|
||||||
|
}
|
||||||
|
if (clipboardImage) {
|
||||||
|
await this._injectClipboardImage(anki, definition, fields);
|
||||||
|
}
|
||||||
|
if (mode !== 'kanji' && audioDetails !== null) {
|
||||||
|
await this._injectAudio(anki, definition, fields, audioDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _injectAudio(anki, definition, fields, details) {
|
||||||
if (!this._containsMarker(fields, 'audio')) { return; }
|
if (!this._containsMarker(fields, 'audio')) { return; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const {sources, customSourceUrl} = details;
|
||||||
const expressions = definition.expressions;
|
const expressions = definition.expressions;
|
||||||
const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition;
|
const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition;
|
||||||
|
|
||||||
let fileName = this._createInjectedAudioFileName(audioSourceDefinition);
|
let fileName = this._createInjectedAudioFileName(audioSourceDefinition);
|
||||||
if (fileName === null) { return; }
|
if (fileName === null) { return; }
|
||||||
fileName = AnkiNoteBuilder.replaceInvalidFileNameCharacters(fileName);
|
fileName = this._replaceInvalidFileNameCharacters(fileName);
|
||||||
|
|
||||||
const {audio} = await this._audioSystem.getDefinitionAudio(
|
const {audio} = await this._audioSystem.getDefinitionAudio(
|
||||||
audioSourceDefinition,
|
audioSourceDefinition,
|
||||||
@ -119,8 +141,8 @@ class AnkiNoteBuilder {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = AnkiNoteBuilder.arrayBufferToBase64(audio);
|
const data = this._arrayBufferToBase64(audio);
|
||||||
await this._anki.storeMediaFile(fileName, data);
|
await anki.storeMediaFile(fileName, data);
|
||||||
|
|
||||||
definition.audioFileName = fileName;
|
definition.audioFileName = fileName;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -128,14 +150,14 @@ class AnkiNoteBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async injectScreenshot(definition, fields, screenshot) {
|
async _injectScreenshot(anki, definition, fields, details) {
|
||||||
if (!this._containsMarker(fields, 'screenshot')) { return; }
|
if (!this._containsMarker(fields, 'screenshot')) { return; }
|
||||||
|
|
||||||
const reading = definition.reading;
|
const reading = definition.reading;
|
||||||
const now = new Date(Date.now());
|
const now = new Date(Date.now());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {windowId, tabId, ownerFrameId, format, quality} = screenshot;
|
const {windowId, tabId, ownerFrameId, format, quality} = details;
|
||||||
const dataUrl = await this._getScreenshot(windowId, tabId, ownerFrameId, format, quality);
|
const dataUrl = await this._getScreenshot(windowId, tabId, ownerFrameId, format, quality);
|
||||||
|
|
||||||
const {mediaType, data} = this._getDataUrlInfo(dataUrl);
|
const {mediaType, data} = this._getDataUrlInfo(dataUrl);
|
||||||
@ -143,9 +165,9 @@ class AnkiNoteBuilder {
|
|||||||
if (extension === null) { return; }
|
if (extension === null) { return; }
|
||||||
|
|
||||||
let fileName = `yomichan_browser_screenshot_${reading}_${this._dateToString(now)}.${extension}`;
|
let fileName = `yomichan_browser_screenshot_${reading}_${this._dateToString(now)}.${extension}`;
|
||||||
fileName = AnkiNoteBuilder.replaceInvalidFileNameCharacters(fileName);
|
fileName = this._replaceInvalidFileNameCharacters(fileName);
|
||||||
|
|
||||||
await this._anki.storeMediaFile(fileName, data);
|
await anki.storeMediaFile(fileName, data);
|
||||||
|
|
||||||
definition.screenshotFileName = fileName;
|
definition.screenshotFileName = fileName;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -153,7 +175,7 @@ class AnkiNoteBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async injectClipboardImage(definition, fields) {
|
async _injectClipboardImage(anki, definition, fields) {
|
||||||
if (!this._containsMarker(fields, 'clipboard-image')) { return; }
|
if (!this._containsMarker(fields, 'clipboard-image')) { return; }
|
||||||
|
|
||||||
const reading = definition.reading;
|
const reading = definition.reading;
|
||||||
@ -168,9 +190,9 @@ class AnkiNoteBuilder {
|
|||||||
if (extension === null) { return; }
|
if (extension === null) { return; }
|
||||||
|
|
||||||
let fileName = `yomichan_clipboard_image_${reading}_${this._dateToString(now)}.${extension}`;
|
let fileName = `yomichan_clipboard_image_${reading}_${this._dateToString(now)}.${extension}`;
|
||||||
fileName = AnkiNoteBuilder.replaceInvalidFileNameCharacters(fileName);
|
fileName = this._replaceInvalidFileNameCharacters(fileName);
|
||||||
|
|
||||||
await this._anki.storeMediaFile(fileName, data);
|
await anki.storeMediaFile(fileName, data);
|
||||||
|
|
||||||
definition.clipboardImageFileName = fileName;
|
definition.clipboardImageFileName = fileName;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -233,16 +255,16 @@ class AnkiNoteBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static replaceInvalidFileNameCharacters(fileName) {
|
_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, '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
static arrayBufferToBase64(arrayBuffer) {
|
_arrayBufferToBase64(arrayBuffer) {
|
||||||
return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||||
}
|
}
|
||||||
|
|
||||||
static stringReplaceAsync(str, regex, replacer) {
|
_stringReplaceAsync(str, regex, replacer) {
|
||||||
let match;
|
let match;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
@ -58,7 +58,6 @@ class Backend {
|
|||||||
useCache: false
|
useCache: false
|
||||||
});
|
});
|
||||||
this._ankiNoteBuilder = new AnkiNoteBuilder({
|
this._ankiNoteBuilder = new AnkiNoteBuilder({
|
||||||
anki: this._anki,
|
|
||||||
audioSystem: this._audioSystem,
|
audioSystem: this._audioSystem,
|
||||||
renderTemplate: this._renderTemplate.bind(this),
|
renderTemplate: this._renderTemplate.bind(this),
|
||||||
getClipboardImage: this._onApiClipboardImageGet.bind(this),
|
getClipboardImage: this._onApiClipboardImageGet.bind(this),
|
||||||
@ -446,33 +445,8 @@ class Backend {
|
|||||||
async _onApiDefinitionAdd({definition, mode, context, ownerFrameId, optionsContext}, sender) {
|
async _onApiDefinitionAdd({definition, mode, context, ownerFrameId, optionsContext}, sender) {
|
||||||
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') {
|
|
||||||
const {customSourceUrl} = options.audio;
|
|
||||||
await this._ankiNoteBuilder.injectAudio(
|
|
||||||
definition,
|
|
||||||
fields,
|
|
||||||
options.audio.sources,
|
|
||||||
customSourceUrl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._ankiNoteBuilder.injectClipboardImage(definition, fields);
|
|
||||||
|
|
||||||
const {id: tabId, windowId} = (sender && sender.tab ? sender.tab : {});
|
const {id: tabId, windowId} = (sender && sender.tab ? sender.tab : {});
|
||||||
const {format, quality} = options.anki.screenshot;
|
const note = await this._createNote(definition, mode, context, options, templates, true, {windowId, tabId, ownerFrameId});
|
||||||
await this._ankiNoteBuilder.injectScreenshot(
|
|
||||||
definition,
|
|
||||||
fields,
|
|
||||||
{windowId, tabId, ownerFrameId, format, quality}
|
|
||||||
);
|
|
||||||
|
|
||||||
const note = await this._createNote(definition, mode, context, options, templates);
|
|
||||||
return this._anki.addNote(note);
|
return this._anki.addNote(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,7 +459,7 @@ class Backend {
|
|||||||
const notePromises = [];
|
const notePromises = [];
|
||||||
for (const definition of definitions) {
|
for (const definition of definitions) {
|
||||||
for (const mode of modes) {
|
for (const mode of modes) {
|
||||||
const notePromise = this._createNote(definition, mode, context, options, templates);
|
const notePromise = this._createNote(definition, mode, context, options, templates, false, null);
|
||||||
notePromises.push(notePromise);
|
notePromises.push(notePromise);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1611,11 +1585,17 @@ class Backend {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _createNote(definition, mode, context, options, templates) {
|
async _createNote(definition, mode, context, options, templates, injectMedia, screenshotTarget) {
|
||||||
const {general: {resultOutputMode, compactGlossaries}, anki: ankiOptions} = options;
|
const {
|
||||||
const {tags, duplicateScope} = ankiOptions;
|
general: {resultOutputMode, compactGlossaries},
|
||||||
const modeOptions = (mode === 'kanji') ? ankiOptions.kanji : ankiOptions.terms;
|
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 : {});
|
||||||
|
|
||||||
return await this._ankiNoteBuilder.createNote({
|
return await this._ankiNoteBuilder.createNote({
|
||||||
|
anki: injectMedia ? this._anki : null,
|
||||||
definition,
|
definition,
|
||||||
mode,
|
mode,
|
||||||
context,
|
context,
|
||||||
@ -1624,7 +1604,10 @@ class Backend {
|
|||||||
duplicateScope,
|
duplicateScope,
|
||||||
resultOutputMode,
|
resultOutputMode,
|
||||||
compactGlossaries,
|
compactGlossaries,
|
||||||
modeOptions
|
modeOptions,
|
||||||
|
audioDetails: {sources, customSourceUrl},
|
||||||
|
screenshotDetails: {windowId, tabId, ownerFrameId, format, quality},
|
||||||
|
clipboardImage: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user