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 { 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 = [];

View File

@ -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
}); });
} }