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:
toasted-nutbread 2020-09-09 16:57:35 -04:00 committed by GitHub
parent c0a6849f98
commit acb7ad32f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 59 additions and 54 deletions

View File

@ -20,8 +20,7 @@
*/
class AnkiNoteBuilder {
constructor({anki, audioSystem, renderTemplate, getClipboardImage=null, getScreenshot=null}) {
this._anki = anki;
constructor({audioSystem, renderTemplate, getClipboardImage=null, getScreenshot=null}) {
this._audioSystem = audioSystem;
this._renderTemplate = renderTemplate;
this._getClipboardImage = getClipboardImage;
@ -29,6 +28,7 @@ class AnkiNoteBuilder {
}
async createNote({
anki=null,
definition,
mode,
context,
@ -38,8 +38,15 @@ class AnkiNoteBuilder {
resultOutputMode='split',
compactGlossaries=false,
modeOptions: {fields, deck, model},
audioDetails=null,
screenshotDetails=null,
clipboardImage=false,
errors=null
}) {
if (anki !== null) {
await this._injectMedia(anki, definition, fields, mode, audioDetails, screenshotDetails, clipboardImage);
}
const fieldEntries = Object.entries(fields);
const noteFields = {};
const note = {
@ -50,10 +57,10 @@ class AnkiNoteBuilder {
options: {duplicateScope}
};
const data = this.createNoteData(definition, mode, context, resultOutputMode, compactGlossaries);
const data = this._createNoteData(definition, mode, context, resultOutputMode, compactGlossaries);
const formattedFieldValuePromises = [];
for (const [, fieldValue] of fieldEntries) {
const formattedFieldValuePromise = this.formatField(fieldValue, data, templates, errors);
const formattedFieldValuePromise = this._formatField(fieldValue, data, templates, errors);
formattedFieldValuePromises.push(formattedFieldValuePromise);
}
@ -67,7 +74,9 @@ class AnkiNoteBuilder {
return note;
}
createNoteData(definition, mode, context, resultOutputMode, compactGlossaries) {
// Private
_createNoteData(definition, mode, context, resultOutputMode, compactGlossaries) {
const pitches = DictionaryDataUtil.getPitchAccentInfos(definition);
const pitchCount = pitches.reduce((i, v) => i + v.pitches.length, 0);
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;
return await AnkiNoteBuilder.stringReplaceAsync(field, pattern, async (g0, marker) => {
return await this._stringReplaceAsync(field, pattern, async (g0, marker) => {
try {
return await this._renderTemplate(templates, data, marker);
} 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; }
try {
const {sources, customSourceUrl} = details;
const expressions = definition.expressions;
const audioSourceDefinition = Array.isArray(expressions) ? expressions[0] : definition;
let fileName = this._createInjectedAudioFileName(audioSourceDefinition);
if (fileName === null) { return; }
fileName = AnkiNoteBuilder.replaceInvalidFileNameCharacters(fileName);
fileName = this._replaceInvalidFileNameCharacters(fileName);
const {audio} = await this._audioSystem.getDefinitionAudio(
audioSourceDefinition,
@ -119,8 +141,8 @@ class AnkiNoteBuilder {
}
);
const data = AnkiNoteBuilder.arrayBufferToBase64(audio);
await this._anki.storeMediaFile(fileName, data);
const data = this._arrayBufferToBase64(audio);
await anki.storeMediaFile(fileName, data);
definition.audioFileName = fileName;
} 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; }
const reading = definition.reading;
const now = new Date(Date.now());
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 {mediaType, data} = this._getDataUrlInfo(dataUrl);
@ -143,9 +165,9 @@ class AnkiNoteBuilder {
if (extension === null) { return; }
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;
} catch (e) {
@ -153,7 +175,7 @@ class AnkiNoteBuilder {
}
}
async injectClipboardImage(definition, fields) {
async _injectClipboardImage(anki, definition, fields) {
if (!this._containsMarker(fields, 'clipboard-image')) { return; }
const reading = definition.reading;
@ -168,9 +190,9 @@ class AnkiNoteBuilder {
if (extension === null) { return; }
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;
} catch (e) {
@ -233,16 +255,16 @@ class AnkiNoteBuilder {
}
}
static replaceInvalidFileNameCharacters(fileName) {
_replaceInvalidFileNameCharacters(fileName) {
// eslint-disable-next-line no-control-regex
return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-');
}
static arrayBufferToBase64(arrayBuffer) {
_arrayBufferToBase64(arrayBuffer) {
return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
}
static stringReplaceAsync(str, regex, replacer) {
_stringReplaceAsync(str, regex, replacer) {
let match;
let index = 0;
const parts = [];

View File

@ -58,7 +58,6 @@ class Backend {
useCache: false
});
this._ankiNoteBuilder = new AnkiNoteBuilder({
anki: this._anki,
audioSystem: this._audioSystem,
renderTemplate: this._renderTemplate.bind(this),
getClipboardImage: this._onApiClipboardImageGet.bind(this),
@ -446,33 +445,8 @@ class Backend {
async _onApiDefinitionAdd({definition, mode, context, ownerFrameId, optionsContext}, sender) {
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,
fields,
options.audio.sources,
customSourceUrl
);
}
await this._ankiNoteBuilder.injectClipboardImage(definition, fields);
const {id: tabId, windowId} = (sender && sender.tab ? sender.tab : {});
const {format, quality} = options.anki.screenshot;
await this._ankiNoteBuilder.injectScreenshot(
definition,
fields,
{windowId, tabId, ownerFrameId, format, quality}
);
const note = await this._createNote(definition, mode, context, options, templates);
const note = await this._createNote(definition, mode, context, options, templates, true, {windowId, tabId, ownerFrameId});
return this._anki.addNote(note);
}
@ -485,7 +459,7 @@ class Backend {
const notePromises = [];
for (const definition of definitions) {
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);
}
}
@ -1611,11 +1585,17 @@ class Backend {
});
}
async _createNote(definition, mode, context, options, templates) {
const {general: {resultOutputMode, compactGlossaries}, anki: ankiOptions} = options;
const {tags, duplicateScope} = ankiOptions;
const modeOptions = (mode === 'kanji') ? ankiOptions.kanji : ankiOptions.terms;
async _createNote(definition, mode, context, options, templates, injectMedia, screenshotTarget) {
const {
general: {resultOutputMode, compactGlossaries},
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({
anki: injectMedia ? this._anki : null,
definition,
mode,
context,
@ -1624,7 +1604,10 @@ class Backend {
duplicateScope,
resultOutputMode,
compactGlossaries,
modeOptions
modeOptions,
audioDetails: {sources, customSourceUrl},
screenshotDetails: {windowId, tabId, ownerFrameId, format, quality},
clipboardImage: true
});
}