diff options
| -rw-r--r-- | ext/bg/js/anki-note-builder.js | 168 | ||||
| -rw-r--r-- | ext/bg/js/backend.js | 163 | ||||
| -rw-r--r-- | ext/bg/js/settings/anki-templates-controller.js | 3 | 
3 files changed, 170 insertions, 164 deletions
| diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js index a558db92..141259ce 100644 --- a/ext/bg/js/anki-note-builder.js +++ b/ext/bg/js/anki-note-builder.js @@ -20,11 +20,8 @@   */  class AnkiNoteBuilder { -    constructor({renderTemplate, getDefinitionAudio=null, getClipboardImage=null, getScreenshot=null}) { +    constructor({renderTemplate}) {          this._renderTemplate = renderTemplate; -        this._getDefinitionAudio = getDefinitionAudio; -        this._getClipboardImage = getClipboardImage; -        this._getScreenshot = getScreenshot;      }      async createNote({ @@ -74,6 +71,16 @@ class AnkiNoteBuilder {          return note;      } +    containsMarker(fields, marker) { +        marker = `{${marker}}`; +        for (const fieldValue of Object.values(fields)) { +            if (fieldValue.includes(marker)) { +                return true; +            } +        } +        return false; +    } +      // Private      _createNoteData(definition, mode, context, resultOutputMode, compactGlossaries) { @@ -106,159 +113,6 @@ class AnkiNoteBuilder {          });      } -    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 {expression, reading} = Array.isArray(expressions) ? expressions[0] : definition; - -            let fileName = this._createInjectedAudioFileName(expression, reading); -            if (fileName === null) { return; } -            fileName = this._replaceInvalidFileNameCharacters(fileName); - -            const {audio: data} = await this._getDefinitionAudio( -                sources, -                expression, -                reading, -                { -                    textToSpeechVoice: null, -                    customSourceUrl, -                    binary: true, -                    disableCache: true -                } -            ); - -            await anki.storeMediaFile(fileName, data); - -            definition.audioFileName = fileName; -        } catch (e) { -            // NOP -        } -    } - -    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} = details; -            const dataUrl = await this._getScreenshot(windowId, tabId, ownerFrameId, format, quality); - -            const {mediaType, data} = this._getDataUrlInfo(dataUrl); -            const extension = this._getImageExtensionFromMediaType(mediaType); -            if (extension === null) { return; } - -            let fileName = `yomichan_browser_screenshot_${reading}_${this._dateToString(now)}.${extension}`; -            fileName = this._replaceInvalidFileNameCharacters(fileName); - -            await anki.storeMediaFile(fileName, data); - -            definition.screenshotFileName = fileName; -        } catch (e) { -            // NOP -        } -    } - -    async _injectClipboardImage(anki, 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 {mediaType, data} = this._getDataUrlInfo(dataUrl); -            const extension = this._getImageExtensionFromMediaType(mediaType); -            if (extension === null) { return; } - -            let fileName = `yomichan_clipboard_image_${reading}_${this._dateToString(now)}.${extension}`; -            fileName = this._replaceInvalidFileNameCharacters(fileName); - -            await anki.storeMediaFile(fileName, data); - -            definition.clipboardImageFileName = fileName; -        } catch (e) { -            // NOP -        } -    } - -    _createInjectedAudioFileName(expression, reading) { -        if (!reading && !expression) { return null; } - -        let fileName = 'yomichan'; -        if (reading) { fileName += `_${reading}`; } -        if (expression) { fileName += `_${expression}`; } -        fileName += '.mp3'; -        fileName = fileName.replace(/\]/g, ''); -        return fileName; -    } - -    _dateToString(date) { -        const year = date.getUTCFullYear(); -        const month = date.getUTCMonth().toString().padStart(2, '0'); -        const day = date.getUTCDate().toString().padStart(2, '0'); -        const hours = date.getUTCHours().toString().padStart(2, '0'); -        const minutes = date.getUTCMinutes().toString().padStart(2, '0'); -        const seconds = date.getUTCSeconds().toString().padStart(2, '0'); -        return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; -    } - -    _containsMarker(fields, marker) { -        marker = `{${marker}}`; -        for (const fieldValue of Object.values(fields)) { -            if (fieldValue.includes(marker)) { -                return true; -            } -        } -        return false; -    } - -    _getDataUrlInfo(dataUrl) { -        const match = /^data:([^,]*?)(;base64)?,/.exec(dataUrl); -        if (match === null) { -            throw new Error('Invalid data URL'); -        } - -        let mediaType = match[1]; -        if (mediaType.length === 0) { mediaType = 'text/plain'; } - -        let data = dataUrl.substring(match[0].length); -        if (typeof match[2] === 'undefined') { data = btoa(data); } - -        return {mediaType, data}; -    } - -    _getImageExtensionFromMediaType(mediaType) { -        switch (mediaType.toLowerCase()) { -            case 'image/png': return 'png'; -            case 'image/jpeg': return 'jpeg'; -            default: return null; -        } -    } - -    _replaceInvalidFileNameCharacters(fileName) { -        // eslint-disable-next-line no-control-regex -        return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-'); -    } -      _stringReplaceAsync(str, regex, replacer) {          let match;          let index = 0; diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 37a5c77e..47295a27 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -58,10 +58,7 @@ class Backend {              useCache: false          });          this._ankiNoteBuilder = new AnkiNoteBuilder({ -            renderTemplate: this._renderTemplate.bind(this), -            getDefinitionAudio: this._getDefinitionAudio.bind(this), -            getClipboardImage: this._onApiClipboardImageGet.bind(this), -            getScreenshot: this._getScreenshot.bind(this) +            renderTemplate: this._renderTemplate.bind(this)          });          this._templateRenderer = new TemplateRenderer(); @@ -1584,8 +1581,29 @@ class Backend {          const modeOptions = (mode === 'kanji') ? kanji : terms;          const {windowId, tabId, ownerFrameId} = (isObject(screenshotTarget) ? screenshotTarget : {}); +        if (injectMedia) { +            const fields = modeOptions.fields; +            const timestamp = Date.now(); +            const definitionExpressions = definition.expressions; +            const {expression, reading} = Array.isArray(definitionExpressions) ? definitionExpressions[0] : definition; +            const audioDetails = (mode !== 'kanji' && this._ankiNoteBuilder.containsMarker(fields, 'audio') ? {sources, customSourceUrl} : null); +            const screenshotDetails = (this._ankiNoteBuilder.containsMarker(fields, 'screenshot') ? {windowId, tabId, ownerFrameId, format, quality} : null); +            const clipboardImage = (this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image')); +            const {screenshotFileName, clipboardImageFileName, audioFileName} = await this._injectAnkNoteMedia( +                this._anki, +                expression, +                reading, +                timestamp, +                audioDetails, +                screenshotDetails, +                clipboardImage +            ); +            if (screenshotFileName !== null) { definition.screenshotFileName = screenshotFileName; } +            if (clipboardImageFileName !== null) { definition.clipboardImageFileName = clipboardImageFileName; } +            if (audioFileName !== null) { definition.audioFileName = audioFileName; } +        } +          return await this._ankiNoteBuilder.createNote({ -            anki: injectMedia ? this._anki : null,              definition,              mode,              context, @@ -1640,4 +1658,139 @@ class Backend {      async _getDefinitionAudio(sources, expression, reading, details) {          return await this._audioSystem.getDefinitionAudio(sources, expression, reading, details);      } + +    async _injectAnkNoteMedia(ankiConnect, expression, reading, timestamp, audioDetails, screenshotDetails, clipboardImage) { +        const screenshotFileName = ( +            screenshotDetails !== null ? +            await this._injectAnkNoteScreenshot(ankiConnect, expression, reading, timestamp, screenshotDetails) : +            null +        ); +        const clipboardImageFileName = ( +            clipboardImage ? +            await this._injectAnkNoteClipboardImage(ankiConnect, expression, reading, timestamp) : +            null +        ); +        const audioFileName = ( +            audioDetails !== null ? +            await this._injectAnkNoteAudio(ankiConnect, expression, reading, timestamp, audioDetails) : +            null +        ); +        return {screenshotFileName, clipboardImageFileName, audioFileName}; +    } + +    async _injectAnkNoteAudio(ankiConnect, expression, reading, timestamp, details) { +        try { +            if (!reading && !expression) { +                throw new Error('Invalid reading and expression'); +            } + +            let fileName = 'yomichan'; +            if (reading) { fileName += `_${reading}`; } +            if (expression) { fileName += `_${expression}`; } +            fileName += '.mp3'; +            fileName = fileName.replace(/\]/g, ''); +            fileName = this._replaceInvalidFileNameCharacters(fileName); + +            const {sources, customSourceUrl} = details; +            const {audio: data} = await this._getDefinitionAudio( +                sources, +                expression, +                reading, +                { +                    textToSpeechVoice: null, +                    customSourceUrl, +                    binary: true, +                    disableCache: true +                } +            ); + +            await ankiConnect.storeMediaFile(fileName, data); + +            return fileName; +        } catch (e) { +            return null; +        } +    } + +    async _injectAnkNoteScreenshot(ankiConnect, expression, reading, timestamp, details) { +        try { +            const now = new Date(timestamp); + +            const {windowId, tabId, ownerFrameId, format, quality} = details; +            const dataUrl = await this._getScreenshot(windowId, tabId, ownerFrameId, format, quality); + +            const {mediaType, data} = this._getDataUrlInfo(dataUrl); +            const extension = this._getImageExtensionFromMediaType(mediaType); + +            let fileName = `yomichan_browser_screenshot_${reading}_${this._ankNoteDateToString(now)}.${extension}`; +            fileName = this._replaceInvalidFileNameCharacters(fileName); + +            await ankiConnect.storeMediaFile(fileName, data); + +            return fileName; +        } catch (e) { +            return null; +        } +    } + +    async _injectAnkNoteClipboardImage(ankiConnect, expression, reading, timestamp) { +        try { +            const now = new Date(timestamp); + +            const dataUrl = await this._onApiClipboardImageGet(); +            if (dataUrl === null) { +                throw new Error('No clipboard image'); +            } + +            const {mediaType, data} = this._getDataUrlInfo(dataUrl); +            const extension = this._getImageExtensionFromMediaType(mediaType); + +            let fileName = `yomichan_clipboard_image_${reading}_${this._ankNoteDateToString(now)}.${extension}`; +            fileName = this._replaceInvalidFileNameCharacters(fileName); + +            await ankiConnect.storeMediaFile(fileName, data); + +            return fileName; +        } catch (e) { +            return null; +        } +    } + +    _replaceInvalidFileNameCharacters(fileName) { +        // eslint-disable-next-line no-control-regex +        return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-'); +    } + +    _ankNoteDateToString(date) { +        const year = date.getUTCFullYear(); +        const month = date.getUTCMonth().toString().padStart(2, '0'); +        const day = date.getUTCDate().toString().padStart(2, '0'); +        const hours = date.getUTCHours().toString().padStart(2, '0'); +        const minutes = date.getUTCMinutes().toString().padStart(2, '0'); +        const seconds = date.getUTCSeconds().toString().padStart(2, '0'); +        return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; +    } + +    _getDataUrlInfo(dataUrl) { +        const match = /^data:([^,]*?)(;base64)?,/.exec(dataUrl); +        if (match === null) { +            throw new Error('Invalid data URL'); +        } + +        let mediaType = match[1]; +        if (mediaType.length === 0) { mediaType = 'text/plain'; } + +        let data = dataUrl.substring(match[0].length); +        if (typeof match[2] === 'undefined') { data = btoa(data); } + +        return {mediaType, data}; +    } + +    _getImageExtensionFromMediaType(mediaType) { +        switch (mediaType.toLowerCase()) { +            case 'image/png': return 'png'; +            case 'image/jpeg': return 'jpeg'; +            default: throw new Error('Unknown image media type'); +        } +    }  } diff --git a/ext/bg/js/settings/anki-templates-controller.js b/ext/bg/js/settings/anki-templates-controller.js index 87f13100..c05301df 100644 --- a/ext/bg/js/settings/anki-templates-controller.js +++ b/ext/bg/js/settings/anki-templates-controller.js @@ -144,8 +144,7 @@ class AnkiTemplatesController {                  let templates = options.anki.fieldTemplates;                  if (typeof templates !== 'string') { templates = this._defaultFieldTemplates; }                  const ankiNoteBuilder = new AnkiNoteBuilder({ -                    renderTemplate: api.templateRender.bind(api), -                    getClipboardImage: api.clipboardGetImage.bind(api) +                    renderTemplate: api.templateRender.bind(api)                  });                  const {general: {resultOutputMode, compactGlossaries}} = options;                  const note = await ankiNoteBuilder.createNote({ |