diff --git a/dev/data/structured-content-overrides.css b/dev/data/structured-content-overrides.css index c792cbb684..dca2b18396 100644 --- a/dev/data/structured-content-overrides.css +++ b/dev/data/structured-content-overrides.css @@ -33,13 +33,8 @@ line-height: initial; color: initial; } -.gloss-image-background { - background-color: currentColor; -} :root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, -:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background, -:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, -:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background { +:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image { /* remove-rule */ } .gloss-image-link-text { diff --git a/ext/css/structured-content.css b/ext/css/structured-content.css index 05bce018f7..f3ef41984e 100644 --- a/ext/css/structured-content.css +++ b/ext/css/structured-content.css @@ -67,62 +67,32 @@ text-align: center; padding: 0.25em; } -.gloss-image-background { - --image: none; - - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: var(--text-color); - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center center; - -webkit-mask-mode: alpha; - -webkit-mask-size: contain; - -webkit-mask-image: var(--image); - mask-repeat: no-repeat; - mask-position: center center; - mask-mode: alpha; - mask-size: contain; - mask-image: var(--image); -} .gloss-image { display: inline-block; vertical-align: top; object-fit: contain; border: none; outline: none; -} -.gloss-image-link[data-has-aspect-ratio=true] .gloss-image { - position: absolute; - left: 0; - top: 0; width: 100%; - height: 100%; } .gloss-image:not([src]) { display: none; } -.gloss-image-link[data-image-rendering=pixelated] .gloss-image, -.gloss-image-link[data-image-rendering=pixelated] .gloss-image-background { +.gloss-image-link[data-image-rendering=pixelated] .gloss-image { image-rendering: auto; image-rendering: -moz-crisp-edges; image-rendering: -webkit-optimize-contrast; image-rendering: pixelated; image-rendering: crisp-edges; } -.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, -.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background { +.gloss-image-link[data-image-rendering=crisp-edges] .gloss-image { image-rendering: auto; image-rendering: -moz-crisp-edges; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; } :root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, -:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background, -:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, -:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background { +:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image { image-rendering: auto; } .gloss-image-link[data-has-aspect-ratio=true] .gloss-image-sizer { @@ -131,26 +101,9 @@ vertical-align: top; font-size: 0; } -.gloss-image-link-text { - display: none; - line-height: var(--line-height); -} -.gloss-image-link-text::before { - content: '['; -} -.gloss-image-link-text::after { - content: ']'; -} -.gloss-image-description { - display: block; - white-space: pre-line; -} .gloss-image-link[data-appearance=monochrome] .gloss-image { - opacity: 0; -} -.gloss-image-link:not([data-appearance=monochrome]) .gloss-image-background { - display: none; + filter: grayscale(1); } .gloss-image-link[data-size-units=em] .gloss-image-container { @@ -189,14 +142,6 @@ :root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true]:focus .gloss-image-container { display: block; } -.gloss-image-link[data-collapsed=true] .gloss-image-link-text, -:root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true] .gloss-image-link-text { - display: inline; -} -.gloss-image-link[data-collapsed=true]~.gloss-image-description, -:root[data-glossary-layout-mode=compact] .gloss-image-description { - display: inline; -} /* Links */ diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 8fe408ee9a..2fb00708aa 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -172,6 +172,7 @@ export class Backend { ['getDictionaryInfo', this._onApiGetDictionaryInfo.bind(this)], ['purgeDatabase', this._onApiPurgeDatabase.bind(this)], ['getMedia', this._onApiGetMedia.bind(this)], + ['getMediaObjects', this._onApiGetMediaObjects.bind(this)], ['logGenericErrorBackend', this._onApiLogGenericErrorBackend.bind(this)], ['logIndicatorClear', this._onApiLogIndicatorClear.bind(this)], ['modifySettings', this._onApiModifySettings.bind(this)], @@ -795,6 +796,11 @@ export class Backend { return await this._getNormalizedDictionaryDatabaseMedia(targets); } + /** @type {import('api').ApiHandler<'getMediaObjects'>} */ + async _onApiGetMediaObjects({targets}) { + return await this._dictionaryDatabase.getMediaObjects(targets); + } + /** @type {import('api').ApiHandler<'logGenericErrorBackend'>} */ _onApiLogGenericErrorBackend({error, level, context}) { log.logGenericError(ExtensionError.deserialize(error), level, context); diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js index 7150cc2922..2f888fb598 100644 --- a/ext/js/background/offscreen-proxy.js +++ b/ext/js/background/offscreen-proxy.js @@ -28,6 +28,9 @@ import {base64ToArrayBuffer} from '../data/array-buffer-util.js'; * The background service workers doesn't have access a webpage to read the clipboard from, * so it must be done in the offscreen page. * + * - Provide access to `URL.createObjectURL` so that we can load media as blobs directly on the + * backend, which performs better than sending the media as base64 strings to the frontend. + * * - Provide a longer lifetime for the dictionary database. The background service worker can be * terminated by the web browser, which means that when it restarts, it has to go through its * initialization process again. This initialization process can take a non-trivial amount of @@ -173,6 +176,15 @@ export class DictionaryDatabaseProxy { const serializedMedia = /** @type {import('dictionary-database').Media[]} */ (await this._offscreen.sendMessagePromise({action: 'databaseGetMediaOffscreen', params: {targets}})); return serializedMedia.map((m) => ({...m, content: base64ToArrayBuffer(m.content)})); } + + /** + * @param {import('dictionary-database').MediaRequest[]} targets + * @returns {Promise} + */ + async getMediaObjects(targets) { + console.log('offscreen getMediaObjects', targets); + return await this._offscreen.sendMessagePromise({action: 'databaseGetMediaObjectsOffscreen', params: {targets}}); + } } export class TranslatorProxy { diff --git a/ext/js/background/offscreen.js b/ext/js/background/offscreen.js index 5e901005ef..a732d5a89d 100644 --- a/ext/js/background/offscreen.js +++ b/ext/js/background/offscreen.js @@ -53,6 +53,7 @@ export class Offscreen { ['getDictionaryInfoOffscreen', this._getDictionaryInfoHandler.bind(this)], ['databasePurgeOffscreen', this._purgeDatabaseHandler.bind(this)], ['databaseGetMediaOffscreen', this._getMediaHandler.bind(this)], + ['databaseGetMediaObjectsOffscreen', this._getMediaObjectsHandler.bind(this)], ['translatorPrepareOffscreen', this._prepareTranslatorHandler.bind(this)], ['findKanjiOffscreen', this._findKanjiHandler.bind(this)], ['findTermsOffscreen', this._findTermsHandler.bind(this)], @@ -110,6 +111,11 @@ export class Offscreen { return media.map((m) => ({...m, content: arrayBufferToBase64(m.content)})); } + /** @type {import('offscreen').ApiHandler<'databaseGetMediaObjectsOffscreen'>} */ + async _getMediaObjectsHandler({targets}) { + return await this._dictionaryDatabase.getMediaObjects(targets); + } + /** @type {import('offscreen').ApiHandler<'translatorPrepareOffscreen'>} */ _prepareTranslatorHandler() { this._translator.prepare(); diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 952b5bfa7a..c9e84903e6 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -254,6 +254,15 @@ export class API { return this._invoke('getMedia', {targets}); } + /** + * @param {import('api').ApiParam<'getMediaObjects', 'targets'>} targets + * @returns {Promise>} + */ + getMediaObjects(targets) { + console.log('getMediaObjects', targets); + return this._invoke('getMediaObjects', {targets}); + } + /** * @param {import('api').ApiParam<'logGenericErrorBackend', 'error'>} error * @param {import('api').ApiParam<'logGenericErrorBackend', 'level'>} level diff --git a/ext/js/dictionary/dictionary-database.js b/ext/js/dictionary/dictionary-database.js index e15d486c8d..039e72ec74 100644 --- a/ext/js/dictionary/dictionary-database.js +++ b/ext/js/dictionary/dictionary-database.js @@ -346,6 +346,21 @@ export class DictionaryDatabase { return this._findMultiBulk('media', ['path'], items, this._createOnlyQuery4, predicate, this._createMediaBind); } + /** + * This requires the ability to call URL.createObjectURL, which is not available in a service worker. Therefore Chrome must go through the Offscreen API as opposed to directly calling this. + * @param {import('dictionary-database').MediaRequest[]} items + * @returns {Promise} + */ + async getMediaObjects(items) { + /** @type {import('dictionary-database').FindPredicate} */ + const predicate = (row, item) => (row.dictionary === item.dictionary); + return (await this._findMultiBulk('media', ['path'], items, this._createOnlyQuery4, predicate, this._createMediaBind)).map((m) => { + const blob = new Blob([m.content], {type: m.mediaType}); + const url = URL.createObjectURL(blob); + return {...m, content: null, url}; + }); + } + /** * @returns {Promise} */ diff --git a/ext/js/display/display-content-manager.js b/ext/js/display/display-content-manager.js index 070ab81ab6..e491b8e05d 100644 --- a/ext/js/display/display-content-manager.js +++ b/ext/js/display/display-content-manager.js @@ -17,7 +17,6 @@ */ import {EventListenerCollection} from '../core/event-listener-collection.js'; -import {base64ToArrayBuffer} from '../data/array-buffer-util.js'; /** * The content manager which is used when generating HTML display content. @@ -32,47 +31,44 @@ export class DisplayContentManager { this._display = display; /** @type {import('core').TokenObject} */ this._token = {}; - /** @type {Map>>} */ + /** @type {Map>} */ this._mediaCache = new Map(); - /** @type {import('display-content-manager').LoadMediaDataInfo[]} */ - this._loadMediaData = []; /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); + /** @type {import('display-content-manager').LoadMediaRequest[]} */ + this._loadMediaRequests = []; + } + + /** @type {import('display-content-manager').LoadMediaRequest[]} */ + get loadMediaRequests() { + return this._loadMediaRequests; } /** - * Attempts to load the media file from a given dictionary. - * @param {string} path The path to the media file in the dictionary. - * @param {string} dictionary The name of the dictionary. - * @param {import('display-content-manager').OnLoadCallback} onLoad The callback that is executed if the media was loaded successfully. - * No assumptions should be made about the synchronicity of this callback. - * @param {import('display-content-manager').OnUnloadCallback} onUnload The callback that is executed when the media should be unloaded. + * Queues loading media file from a given dictionary. + * @param {string} path + * @param {string} dictionary + * @param {import('display-content-manager').OnLoadCallback} onLoad + * @param {import('display-content-manager').OnUnloadCallback} onUnload */ loadMedia(path, dictionary, onLoad, onUnload) { - void this._loadMedia(path, dictionary, onLoad, onUnload); + this._loadMediaRequests.push({path, dictionary, onLoad, onUnload}); } /** * Unloads all media that has been loaded. */ unloadAll() { - for (const {onUnload, loaded} of this._loadMediaData) { - if (typeof onUnload === 'function') { - onUnload(loaded); - } - } - this._loadMediaData = []; - - for (const map of this._mediaCache.values()) { - for (const result of map.values()) { - void this._revokeUrl(result); - } + for (const mediaObject of this._mediaCache.values()) { + URL.revokeObjectURL(mediaObject.url); } this._mediaCache.clear(); this._token = {}; this._eventListeners.removeAllEventListeners(); + + this._loadMediaRequests = []; } /** @@ -91,63 +87,56 @@ export class DisplayContentManager { } /** - * @param {string} path - * @param {string} dictionary - * @param {import('display-content-manager').OnLoadCallback} onLoad - * @param {import('display-content-manager').OnUnloadCallback} onUnload + * Execute media requests */ - async _loadMedia(path, dictionary, onLoad, onUnload) { - const token = this._token; - const media = await this._getMedia(path, dictionary); - if (token !== this._token || media === null) { return; } - - /** @type {import('display-content-manager').LoadMediaDataInfo} */ - const data = {onUnload, loaded: false}; - this._loadMediaData.push(data); - onLoad(media.url); - data.loaded = true; - } - - /** - * @param {string} path - * @param {string} dictionary - * @returns {Promise} - */ - _getMedia(path, dictionary) { - /** @type {Promise|undefined} */ - let promise; - let dictionaryCache = this._mediaCache.get(dictionary); - if (typeof dictionaryCache !== 'undefined') { - promise = dictionaryCache.get(path); - } else { - dictionaryCache = new Map(); - this._mediaCache.set(dictionary, dictionaryCache); + async executeMediaRequests() { + /** @type {Map} */ + const uncachedRequests = new Map(); + for (const request of this._loadMediaRequests) { + const cacheKey = this._cacheKey(request.path, request.dictionary); + const mediaObject = this._mediaCache.get(cacheKey); + if (typeof mediaObject !== 'undefined' && mediaObject !== null) { + await request.onLoad(mediaObject.url); + } else { + const cache = uncachedRequests.get(cacheKey); + if (typeof cache === 'undefined') { + uncachedRequests.set(cacheKey, [request]); + } else { + cache.push(request); + } + } } - if (typeof promise === 'undefined') { - promise = this._getMediaData(path, dictionary); - dictionaryCache.set(path, promise); + performance.mark('display-content-manager:executeMediaRequests:getMediaObjects:start'); + const mediaObjects = await this._display.application.api.getMediaObjects([...uncachedRequests.values()].map((r) => ({path: r[0].path, dictionary: r[0].dictionary}))); + performance.mark('display-content-manager:executeMediaRequests:getMediaObjects:end'); + performance.measure('display-content-manager:executeMediaRequests:getMediaObjects', 'display-content-manager:executeMediaRequests:getMediaObjects:start', 'display-content-manager:executeMediaRequests:getMediaObjects:end'); + const promises = []; + for (const mediaObject of mediaObjects) { + const cacheKey = this._cacheKey(mediaObject.path, mediaObject.dictionary); + this._mediaCache.set(cacheKey, mediaObject); + const requests = uncachedRequests.get(cacheKey); + if (typeof requests !== 'undefined') { + for (const request of requests) { + promises.push(request.onLoad(mediaObject.url)); + } + } } - - return promise; + performance.mark('display-content-manager:executeMediaRequests:runCallbacks:start'); + await Promise.allSettled(promises); + performance.mark('display-content-manager:executeMediaRequests:runCallbacks:end'); + performance.measure('display-content-manager:executeMediaRequests:runCallbacks', 'display-content-manager:executeMediaRequests:runCallbacks:start', 'display-content-manager:executeMediaRequests:runCallbacks:end'); + this._loadMediaRequests = []; } /** + * * @param {string} path * @param {string} dictionary - * @returns {Promise} + * @returns {import('display-content-manager').MediaCacheKey} */ - async _getMediaData(path, dictionary) { - const token = this._token; - const datas = await this._display.application.api.getMedia([{path, dictionary}]); - if (token === this._token && datas.length > 0) { - const data = datas[0]; - const buffer = base64ToArrayBuffer(data.content); - const blob = new Blob([buffer], {type: data.mediaType}); - const url = URL.createObjectURL(blob); - return {data, url}; - } - return null; + _cacheKey(path, dictionary) { + return /** @type {import('display-content-manager').MediaCacheKey} */ (path + ':::' + dictionary); } /** @@ -177,13 +166,4 @@ export class DisplayContentManager { content: null, }); } - - /** - * @param {Promise} data - */ - async _revokeUrl(data) { - const result = await data; - if (result === null) { return; } - URL.revokeObjectURL(result.url); - } } diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js index fc9369d55a..fb28410cfb 100644 --- a/ext/js/display/display-generator.js +++ b/ext/js/display/display-generator.js @@ -42,6 +42,13 @@ export class DisplayGenerator { this._language = 'ja'; } + /** @type {import('./display-content-manager.js').DisplayContentManager} */ + get contentManager() { return this._contentManager; } + + set contentManager(contentManager) { + this._contentManager = contentManager; + } + /** */ async prepare() { await this._templates.loadFromFiles(['/templates-display.html']); @@ -66,9 +73,9 @@ export class DisplayGenerator { /** * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {HTMLElement} + * @returns {Promise} */ - createTermEntry(dictionaryEntry) { + async createTermEntry(dictionaryEntry) { const node = this._instantiate('term-entry'); const headwordsContainer = this._querySelector(node, '.headword-list'); @@ -117,17 +124,19 @@ export class DisplayGenerator { } headwordsContainer.dataset.count = `${headwords.length}`; - this._appendMultiple(inflectionRuleChainsContainer, this._createInflectionRuleChain.bind(this), inflectionRuleChainCandidates); - this._appendMultiple(frequencyGroupListContainer, this._createFrequencyGroup.bind(this), groupedFrequencies, false); - this._appendMultiple(groupedPronunciationsContainer, this._createGroupedPronunciation.bind(this), groupedPronunciations); - this._appendMultiple(headwordTagsContainer, this._createTermTag.bind(this), termTags, headwords.length); + await Promise.all([ + this._appendMultiple(inflectionRuleChainsContainer, this._createInflectionRuleChain.bind(this), inflectionRuleChainCandidates), + this._appendMultiple(frequencyGroupListContainer, this._createFrequencyGroup.bind(this), groupedFrequencies, false), + this._appendMultiple(groupedPronunciationsContainer, this._createGroupedPronunciation.bind(this), groupedPronunciations), + this._appendMultiple(headwordTagsContainer, this._createTermTag.bind(this), termTags, headwords.length) + ]); for (const term of uniqueTerms) { - headwordTagsContainer.appendChild(this._createSearchTag(term)); + headwordTagsContainer.appendChild(await this._createSearchTag(term)); } for (const reading of uniqueReadings) { if (uniqueTerms.has(reading)) { continue; } - headwordTagsContainer.appendChild(this._createSearchTag(reading)); + headwordTagsContainer.appendChild(await this._createSearchTag(reading)); } // Add definitions @@ -145,7 +154,7 @@ export class DisplayGenerator { dictionaryTag.content = [dictionary]; } - const node2 = this._createTermDefinition(definition, dictionaryTag, headwords, uniqueTerms, uniqueReadings); + const node2 = await this._createTermDefinition(definition, dictionaryTag, headwords, uniqueTerms, uniqueReadings); node2.dataset.index = `${i}`; definitionsContainer.appendChild(node2); } @@ -156,9 +165,9 @@ export class DisplayGenerator { /** * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry - * @returns {HTMLElement} + * @returns {Promise} */ - createKanjiEntry(dictionaryEntry) { + async createKanjiEntry(dictionaryEntry) { const node = this._instantiate('kanji-entry'); const glyphContainer = this._querySelector(node, '.kanji-glyph'); @@ -180,16 +189,18 @@ export class DisplayGenerator { dictionaryTag.name = dictionaryEntry.dictionaryAlias; dictionaryTag.content = [dictionaryEntry.dictionary]; - this._appendMultiple(frequencyGroupListContainer, this._createFrequencyGroup.bind(this), groupedFrequencies, true); - this._appendMultiple(tagContainer, this._createTag.bind(this), [...dictionaryEntry.tags, dictionaryTag]); - this._appendMultiple(definitionsContainer, this._createKanjiDefinition.bind(this), dictionaryEntry.definitions); - this._appendMultiple(chineseReadingsContainer, this._createKanjiReading.bind(this), dictionaryEntry.onyomi); - this._appendMultiple(japaneseReadingsContainer, this._createKanjiReading.bind(this), dictionaryEntry.kunyomi); + await Promise.all([ + this._appendMultiple(frequencyGroupListContainer, this._createFrequencyGroup.bind(this), groupedFrequencies, true), + this._appendMultiple(tagContainer, this._createTag.bind(this), [...dictionaryEntry.tags, dictionaryTag]), + this._appendMultiple(definitionsContainer, this._createKanjiDefinition.bind(this), dictionaryEntry.definitions), + this._appendMultiple(chineseReadingsContainer, this._createKanjiReading.bind(this), dictionaryEntry.onyomi), + this._appendMultiple(japaneseReadingsContainer, this._createKanjiReading.bind(this), dictionaryEntry.kunyomi) + ]); - statisticsContainer.appendChild(this._createKanjiInfoTable(dictionaryEntry.stats.misc)); - classificationsContainer.appendChild(this._createKanjiInfoTable(dictionaryEntry.stats.class)); - codepointsContainer.appendChild(this._createKanjiInfoTable(dictionaryEntry.stats.code)); - dictionaryIndicesContainer.appendChild(this._createKanjiInfoTable(dictionaryEntry.stats.index)); + statisticsContainer.appendChild(await this._createKanjiInfoTable(dictionaryEntry.stats.misc)); + classificationsContainer.appendChild(await this._createKanjiInfoTable(dictionaryEntry.stats.class)); + codepointsContainer.appendChild(await this._createKanjiInfoTable(dictionaryEntry.stats.code)); + dictionaryIndicesContainer.appendChild(await this._createKanjiInfoTable(dictionaryEntry.stats.index)); return node; } @@ -367,9 +378,9 @@ export class DisplayGenerator { /** * @param {import('dictionary').InflectionRuleChainCandidate} inflectionRuleChain - * @returns {?HTMLElement} + * @returns {Promise} */ - _createInflectionRuleChain(inflectionRuleChain) { + async _createInflectionRuleChain(inflectionRuleChain) { const {source, inflectionRules} = inflectionRuleChain; if (!Array.isArray(inflectionRules) || inflectionRules.length === 0) { return null; } const fragment = this._instantiate('inflection-rule-chain'); @@ -378,7 +389,7 @@ export class DisplayGenerator { fragment.appendChild(sourceIcon); - this._appendMultiple(fragment, this._createTermInflection.bind(this), inflectionRules); + await this._appendMultiple(fragment, this._createTermInflection.bind(this), inflectionRules); return fragment; } @@ -405,9 +416,9 @@ export class DisplayGenerator { /** * @param {import('dictionary').InflectionRule} inflection - * @returns {DocumentFragment} + * @returns {Promise} */ - _createTermInflection(inflection) { + async _createTermInflection(inflection) { const {name, description} = inflection; const fragment = this._templates.instantiateFragment('inflection'); const node = this._querySelector(fragment, '.inflection'); @@ -423,9 +434,9 @@ export class DisplayGenerator { * @param {import('dictionary').TermHeadword[]} headwords * @param {Set} uniqueTerms * @param {Set} uniqueReadings - * @returns {HTMLElement} + * @returns {Promise} */ - _createTermDefinition(definition, dictionaryTag, headwords, uniqueTerms, uniqueReadings) { + async _createTermDefinition(definition, dictionaryTag, headwords, uniqueTerms, uniqueReadings) { const {dictionary, tags, headwordIndices, entries} = definition; const disambiguations = getDisambiguations(headwords, headwordIndices, uniqueTerms, uniqueReadings); @@ -437,19 +448,20 @@ export class DisplayGenerator { node.dataset.dictionary = dictionary; - this._appendMultiple(tagListContainer, this._createTag.bind(this), [...tags, dictionaryTag]); - this._appendMultiple(onlyListContainer, this._createTermDisambiguation.bind(this), disambiguations); - this._appendMultiple(entriesContainer, this._createTermDefinitionEntry.bind(this), entries, dictionary); - + await Promise.all([ + this._appendMultiple(tagListContainer, this._createTag.bind(this), [...tags, dictionaryTag]), + this._appendMultiple(onlyListContainer, this._createTermDisambiguation.bind(this), disambiguations), + this._appendMultiple(entriesContainer, this._createTermDefinitionEntry.bind(this), entries, dictionary) + ]); return node; } /** * @param {import('dictionary-data').TermGlossaryContent} entry * @param {string} dictionary - * @returns {?HTMLElement} + * @returns {Promise} */ - _createTermDefinitionEntry(entry, dictionary) { + async _createTermDefinitionEntry(entry, dictionary) { switch (typeof entry) { case 'string': return this._createTermDefinitionEntryText(entry); @@ -483,15 +495,15 @@ export class DisplayGenerator { /** * @param {import('dictionary-data').TermGlossaryImage} data * @param {string} dictionary - * @returns {HTMLElement} + * @returns {Promise} */ - _createTermDefinitionEntryImage(data, dictionary) { + async _createTermDefinitionEntryImage(data, dictionary) { const {description} = data; const node = this._instantiate('gloss-item'); const contentContainer = this._querySelector(node, '.gloss-content'); - const image = this._structuredContentGenerator.createDefinitionImage(data, dictionary); + const image = await this._structuredContentGenerator.createDefinitionImage(data, dictionary); contentContainer.appendChild(image); if (typeof description === 'string') { @@ -507,20 +519,20 @@ export class DisplayGenerator { /** * @param {import('structured-content').Content} content * @param {string} dictionary - * @returns {HTMLElement} + * @returns {Promise} */ - _createTermDefinitionEntryStructuredContent(content, dictionary) { + async _createTermDefinitionEntryStructuredContent(content, dictionary) { const node = this._instantiate('gloss-item'); const contentContainer = this._querySelector(node, '.gloss-content'); - this._structuredContentGenerator.appendStructuredContent(contentContainer, content, dictionary); + await this._structuredContentGenerator.appendStructuredContent(contentContainer, content, dictionary); return node; } /** * @param {string} disambiguation - * @returns {HTMLElement} + * @returns {Promise} */ - _createTermDisambiguation(disambiguation) { + async _createTermDisambiguation(disambiguation) { const node = this._instantiate('definition-disambiguation'); node.dataset.term = disambiguation; this._setTextContent(node, disambiguation, this._language); @@ -540,9 +552,9 @@ export class DisplayGenerator { /** * @param {string} text - * @returns {HTMLElement} + * @returns {Promise} */ - _createKanjiDefinition(text) { + async _createKanjiDefinition(text) { const node = this._instantiate('kanji-gloss-item'); const container = this._querySelector(node, '.kanji-gloss-content'); this._setMultilineTextContent(container, text); @@ -551,9 +563,9 @@ export class DisplayGenerator { /** * @param {string} reading - * @returns {HTMLElement} + * @returns {Promise} */ - _createKanjiReading(reading) { + async _createKanjiReading(reading) { const node = this._instantiate('kanji-reading'); this._setTextContent(node, reading, this._language); return node; @@ -561,13 +573,13 @@ export class DisplayGenerator { /** * @param {import('dictionary').KanjiStat[]} details - * @returns {HTMLElement} + * @returns {Promise} */ - _createKanjiInfoTable(details) { + async _createKanjiInfoTable(details) { const node = this._instantiate('kanji-info-table'); const container = this._querySelector(node, '.kanji-info-table-body'); - const count = this._appendMultiple(container, this._createKanjiInfoTableItem.bind(this), details); + const count = await this._appendMultiple(container, this._createKanjiInfoTableItem.bind(this), details); if (count === 0) { const n = this._createKanjiInfoTableItemEmpty(); container.appendChild(n); @@ -578,9 +590,9 @@ export class DisplayGenerator { /** * @param {import('dictionary').KanjiStat} details - * @returns {HTMLElement} + * @returns {Promise} */ - _createKanjiInfoTableItem(details) { + async _createKanjiInfoTableItem(details) { const {content, name, value} = details; const node = this._instantiate('kanji-info-table-item'); const nameNode = this._querySelector(node, '.kanji-info-table-item-header'); @@ -599,9 +611,9 @@ export class DisplayGenerator { /** * @param {import('dictionary').Tag} tag - * @returns {HTMLElement} + * @returns {Promise} */ - _createTag(tag) { + async _createTag(tag) { const {content, name, category, redundant} = tag; const node = this._instantiate('tag'); @@ -621,11 +633,11 @@ export class DisplayGenerator { /** * @param {import('dictionary-data-util').TagGroup} tagInfo * @param {number} totalHeadwordCount - * @returns {HTMLElement} + * @returns {Promise} */ - _createTermTag(tagInfo, totalHeadwordCount) { + async _createTermTag(tagInfo, totalHeadwordCount) { const {tag, headwordIndices} = tagInfo; - const node = this._createTag(tag); + const node = await this._createTag(tag); node.dataset.headwords = headwordIndices.join(' '); node.dataset.totalHeadwordCount = `${totalHeadwordCount}`; node.dataset.matchedHeadwordCount = `${headwordIndices.length}`; @@ -652,17 +664,17 @@ export class DisplayGenerator { /** * @param {string} text - * @returns {HTMLElement} + * @returns {Promise} */ - _createSearchTag(text) { + async _createSearchTag(text) { return this._createTag(this._createTagData(text, 'search')); } /** * @param {import('dictionary-data-util').DictionaryGroupedPronunciations} details - * @returns {HTMLElement} + * @returns {Promise} */ - _createGroupedPronunciation(details) { + async _createGroupedPronunciation(details) { const {dictionary, dictionaryAlias, pronunciations} = details; const node = this._instantiate('pronunciation-group'); @@ -671,7 +683,7 @@ export class DisplayGenerator { node.dataset.pronunciationsCount = `${pronunciations.length}`; const n1 = this._querySelector(node, '.pronunciation-group-tag-list'); - const tag = this._createTag(this._createTagData(dictionaryAlias, 'pronunciation-dictionary')); + const tag = await this._createTag(this._createTagData(dictionaryAlias, 'pronunciation-dictionary')); tag.dataset.details = dictionary; n1.appendChild(tag); @@ -685,16 +697,16 @@ export class DisplayGenerator { const n = this._querySelector(node, '.pronunciation-list'); n.dataset.hasTags = `${hasTags}`; - this._appendMultiple(n, this._createPronunciation.bind(this), pronunciations); + await this._appendMultiple(n, this._createPronunciation.bind(this), pronunciations); return node; } /** * @param {import('dictionary-data-util').GroupedPronunciation} details - * @returns {HTMLElement} + * @returns {Promise} */ - _createPronunciation(details) { + async _createPronunciation(details) { const {pronunciation} = details; switch (pronunciation.type) { case 'pitch-accent': @@ -708,9 +720,9 @@ export class DisplayGenerator { /** * @param {import('dictionary').PhoneticTranscription} pronunciation * @param {import('dictionary-data-util').GroupedPronunciation} details - * @returns {HTMLElement} + * @returns {Promise} */ - _createPronunciationPhoneticTranscription(pronunciation, details) { + async _createPronunciationPhoneticTranscription(pronunciation, details) { const {ipa, tags} = pronunciation; const {exclusiveTerms, exclusiveReadings} = details; @@ -720,7 +732,7 @@ export class DisplayGenerator { node.dataset.tagCount = `${tags.length}`; let n = this._querySelector(node, '.pronunciation-tag-list'); - this._appendMultiple(n, this._createTag.bind(this), tags); + await this._appendMultiple(n, this._createTag.bind(this), tags); n = this._querySelector(node, '.pronunciation-disambiguation-list'); this._createPronunciationDisambiguations(n, exclusiveTerms, exclusiveReadings); @@ -735,9 +747,9 @@ export class DisplayGenerator { /** * @param {import('dictionary').PitchAccent} pitchAccent * @param {import('dictionary-data-util').GroupedPronunciation} details - * @returns {HTMLElement} + * @returns {Promise} */ - _createPronunciationPitchAccent(pitchAccent, details) { + async _createPronunciationPitchAccent(pitchAccent, details) { const {position, nasalPositions, devoicePositions, tags} = pitchAccent; const {reading, exclusiveTerms, exclusiveReadings} = details; const morae = getKanaMorae(reading); @@ -751,7 +763,7 @@ export class DisplayGenerator { node.dataset.tagCount = `${tags.length}`; let n = this._querySelector(node, '.pronunciation-tag-list'); - this._appendMultiple(n, this._createTag.bind(this), tags); + await this._appendMultiple(n, this._createTag.bind(this), tags); n = this._querySelector(node, '.pronunciation-disambiguation-list'); this._createPronunciationDisambiguations(n, exclusiveTerms, exclusiveReadings); @@ -799,9 +811,9 @@ export class DisplayGenerator { /** * @param {import('dictionary-data-util').DictionaryFrequency|import('dictionary-data-util').DictionaryFrequency} details * @param {boolean} kanji - * @returns {HTMLElement} + * @returns {Promise} */ - _createFrequencyGroup(details, kanji) { + async _createFrequencyGroup(details, kanji) { const {dictionary, dictionaryAlias, frequencies} = details; const node = this._instantiate('frequency-group-item'); @@ -932,7 +944,7 @@ export class DisplayGenerator { _appendKanjiLinks(container, text) { let part = ''; for (const c of text) { - if (isCodePointKanji(/** @type {number} */ (c.codePointAt(0)))) { + if (isCodePointKanji(/** @type {number} */(c.codePointAt(0)))) { if (part.length > 0) { container.appendChild(document.createTextNode(part)); part = ''; @@ -953,17 +965,17 @@ export class DisplayGenerator { * @template [TItem=unknown] * @template [TExtraArg=void] * @param {HTMLElement} container - * @param {(item: TItem, arg: TExtraArg) => ?Node} createItem + * @param {(item: TItem, arg: TExtraArg) => Promise} createItem * @param {TItem[]} detailsArray * @param {TExtraArg} [arg] - * @returns {number} + * @returns {Promise} */ - _appendMultiple(container, createItem, detailsArray, arg) { + async _appendMultiple(container, createItem, detailsArray, arg) { let count = 0; const {ELEMENT_NODE} = Node; if (Array.isArray(detailsArray)) { for (const details of detailsArray) { - const item = createItem(details, /** @type {TExtraArg} */ (arg)); + const item = await createItem(details, /** @type {TExtraArg} */(arg)); if (item === null) { continue; } container.appendChild(item); if (item.nodeType === ELEMENT_NODE) { diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 3f6d893a8d..86eb2059a8 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -795,7 +795,6 @@ export class Display extends EventDispatcher { this._closePopups(); this._closeAllPopupMenus(); this._eventListeners.removeAllEventListeners(); - this._contentManager.unloadAll(); this._hideTagNotification(false); this._hideInflectionNotification(false); this._triggerContentClear(); @@ -1358,7 +1357,10 @@ export class Display extends EventDispatcher { let {dictionaryEntries} = content; if (!Array.isArray(dictionaryEntries)) { + performance.mark('display:findDictionaryEntries:start'); dictionaryEntries = hasEnabledDictionaries && lookup && query.length > 0 ? await this._findDictionaryEntries(type === 'kanji', query, wildcardsEnabled, optionsContext) : []; + performance.mark('display:findDictionaryEntries:end'); + performance.measure('display:findDictionaryEntries', 'display:findDictionaryEntries:start', 'display:findDictionaryEntries:end'); if (this._setContentToken !== token) { return; } content.dictionaryEntries = dictionaryEntries; changeHistory = true; @@ -1393,7 +1395,11 @@ export class Display extends EventDispatcher { this._dictionaryEntries = dictionaryEntries; + performance.mark('display:updateNavigationAuto:start'); this._updateNavigationAuto(); + performance.mark('display:updateNavigationAuto:end'); + performance.measure('display:updateNavigationAuto', 'display:updateNavigationAuto:start', 'display:updateNavigationAuto:end'); + this._setNoContentVisible(hasEnabledDictionaries && dictionaryEntries.length === 0 && lookup); this._setNoDictionariesVisible(!hasEnabledDictionaries); @@ -1403,7 +1409,8 @@ export class Display extends EventDispatcher { performance.mark('display:contentUpdate:start'); this._triggerContentUpdateStart(); - for (let i = 0, ii = dictionaryEntries.length; i < ii; ++i) { + let i = 0; + for (const dictionaryEntry of dictionaryEntries) { performance.mark('display:createEntry:start'); if (i > 0) { @@ -1411,17 +1418,21 @@ export class Display extends EventDispatcher { if (this._setContentToken !== token) { return; } } - const dictionaryEntry = dictionaryEntries[i]; const entry = ( dictionaryEntry.type === 'term' ? - this._displayGenerator.createTermEntry(dictionaryEntry) : - this._displayGenerator.createKanjiEntry(dictionaryEntry) + await this._displayGenerator.createTermEntry(dictionaryEntry) : + await this._displayGenerator.createKanjiEntry(dictionaryEntry) ); entry.dataset.index = `${i}`; this._dictionaryEntryNodes.push(entry); this._addEntryEventListeners(entry); this._triggerContentUpdateEntry(dictionaryEntry, entry, i); + performance.mark('display:waitMedia:start'); + await this._contentManager.executeMediaRequests(); + performance.mark('display:waitMedia:end'); + performance.measure('display:waitMedia', 'display:waitMedia:start', 'display:waitMedia:end'); container.appendChild(entry); + if (focusEntry === i) { this._focusEntry(i, 0, false); } @@ -1430,6 +1441,8 @@ export class Display extends EventDispatcher { performance.mark('display:createEntry:end'); performance.measure('display:createEntry', 'display:createEntry:start', 'display:createEntry:end'); + + ++i; } if (typeof scrollX === 'number' || typeof scrollY === 'number') { diff --git a/ext/js/display/structured-content-generator.js b/ext/js/display/structured-content-generator.js index 7074243a17..f51aeb0862 100644 --- a/ext/js/display/structured-content-generator.js +++ b/ext/js/display/structured-content-generator.js @@ -35,28 +35,28 @@ export class StructuredContentGenerator { * @param {import('structured-content').Content} content * @param {string} dictionary */ - appendStructuredContent(node, content, dictionary) { + async appendStructuredContent(node, content, dictionary) { node.classList.add('structured-content'); - this._appendStructuredContent(node, content, dictionary, null); + await this._appendStructuredContent(node, content, dictionary, null); } /** * @param {import('structured-content').Content} content * @param {string} dictionary - * @returns {HTMLElement} + * @returns {Promise} */ - createStructuredContent(content, dictionary) { + async createStructuredContent(content, dictionary) { const node = this._createElement('span', 'structured-content'); - this._appendStructuredContent(node, content, dictionary, null); + await this._appendStructuredContent(node, content, dictionary, null); return node; } /** * @param {import('structured-content').ImageElement|import('dictionary-data').TermGlossaryImage} data * @param {string} dictionary - * @returns {HTMLAnchorElement} + * @returns {Promise} */ - createDefinitionImage(data, dictionary) { + async createDefinitionImage(data, dictionary) { const { path, width = 100, @@ -81,13 +81,13 @@ export class StructuredContentGenerator { const hasPreferredHeight = (typeof preferredHeight === 'number'); const invAspectRatio = ( hasPreferredWidth && hasPreferredHeight ? - preferredHeight / preferredWidth : - height / width + preferredHeight / preferredWidth : + height / width ); const usedWidth = ( hasPreferredWidth ? - preferredWidth : - (hasPreferredHeight ? preferredHeight / invAspectRatio : width) + preferredWidth : + (hasPreferredHeight ? preferredHeight / invAspectRatio : width) ); const node = /** @type {HTMLAnchorElement} */ (this._createElement('a', 'gloss-image-link')); @@ -97,23 +97,6 @@ export class StructuredContentGenerator { const imageContainer = this._createElement('span', 'gloss-image-container'); node.appendChild(imageContainer); - const aspectRatioSizer = this._createElement('span', 'gloss-image-sizer'); - imageContainer.appendChild(aspectRatioSizer); - - const imageBackground = this._createElement('span', 'gloss-image-background'); - imageContainer.appendChild(imageBackground); - - const image = /** @type {HTMLImageElement} */ (this._createElement('img', 'gloss-image')); - image.alt = typeof alt === 'string' ? alt : ''; - imageContainer.appendChild(image); - - const overlay = this._createElement('span', 'gloss-image-container-overlay'); - imageContainer.appendChild(overlay); - - const linkText = this._createElement('span', 'gloss-image-link-text'); - linkText.textContent = 'Image'; - node.appendChild(linkText); - node.dataset.path = path; node.dataset.dictionary = dictionary; node.dataset.imageLoadState = 'not-loaded'; @@ -137,14 +120,12 @@ export class StructuredContentGenerator { imageContainer.title = title; } - aspectRatioSizer.style.paddingTop = `${invAspectRatio * 100}%`; - if (this._contentManager !== null) { this._contentManager.loadMedia( path, dictionary, - (url) => this._setImageData(node, image, imageBackground, url, false), - () => this._setImageData(node, image, imageBackground, null, true), + async (url) => await this._setImageData(node, imageContainer, alt, invAspectRatio, url, false), + async () => await this._setImageData(node, imageContainer, alt, invAspectRatio, null, true), ); } @@ -159,7 +140,7 @@ export class StructuredContentGenerator { * @param {string} dictionary * @param {?string} language */ - _appendStructuredContent(container, content, dictionary, language) { + async _appendStructuredContent(container, content, dictionary, language) { if (typeof content === 'string') { if (content.length > 0) { container.appendChild(this._createTextNode(content)); @@ -177,11 +158,11 @@ export class StructuredContentGenerator { } if (Array.isArray(content)) { for (const item of content) { - this._appendStructuredContent(container, item, dictionary, language); + await this._appendStructuredContent(container, item, dictionary, language); } return; } - const node = this._createStructuredContentGenericElement(content, dictionary, language); + const node = await this._createStructuredContentGenericElement(content, dictionary, language); if (node !== null) { container.appendChild(node); } @@ -226,22 +207,38 @@ export class StructuredContentGenerator { /** * @param {HTMLAnchorElement} node - * @param {HTMLImageElement} image - * @param {HTMLElement} imageBackground + * @param {HTMLElement} imageContainer + * @param {string|undefined} alt + * @param {number} invAspectRatio * @param {?string} url * @param {boolean} unloaded */ - _setImageData(node, image, imageBackground, url, unloaded) { + async _setImageData(node, imageContainer, alt, invAspectRatio, url, unloaded) { + const img = /** @type {HTMLImageElement} */ (this._createElement('img', 'gloss-image')); if (url !== null) { - image.src = url; - node.href = url; - node.dataset.imageLoadState = 'loaded'; - imageBackground.style.setProperty('--image', `url("${url}")`); + if (typeof alt === 'string') { + img.alt = alt; + } + img.src = url; + performance.mark('structured-content-generator:_setImageData:decode:[' + url + ']:start'); + await img.decode().then(() => { + performance.mark('structured-content-generator:_setImageData:decode:[' + url + ']:end'); + performance.measure('structured-content-generator:_setImageData:decode:[' + url + ']', 'structured-content-generator:_setImageData:decode:[' + url + ']:start', 'structured-content-generator:_setImageData:decode:[' + url + ']:end'); + node.dataset.imageLoadState = 'loaded'; + node.href = url; + performance.mark('structured-content-generator:_setImageData:appendChild:start'); + imageContainer.appendChild(img); + performance.mark('structured-content-generator:_setImageData:appendChild:end'); + performance.measure('structured-content-generator:_setImageData:appendChild', 'structured-content-generator:_setImageData:appendChild:start', 'structured-content-generator:_setImageData:appendChild:end'); + }).catch(() => { + node.dataset.imageLoadState = 'load-error'; + }); } else { - image.removeAttribute('src'); + if (node.children.length > 0) { + node.children[0].remove(); + } node.removeAttribute('href'); node.dataset.imageLoadState = unloaded ? 'unloaded' : 'load-error'; - imageBackground.style.removeProperty('--image'); } } @@ -249,9 +246,9 @@ export class StructuredContentGenerator { * @param {import('structured-content').Element} content * @param {string} dictionary * @param {?string} language - * @returns {?HTMLElement} + * @returns {Promise} */ - _createStructuredContentGenericElement(content, dictionary, language) { + async _createStructuredContentGenericElement(content, dictionary, language) { const {tag} = content; switch (tag) { case 'br': @@ -279,7 +276,7 @@ export class StructuredContentGenerator { case 'summary': return this._createStructuredContentElement(tag, content, dictionary, language, 'simple', true, true); case 'img': - return this.createDefinitionImage(content, dictionary); + return await this.createDefinitionImage(content, dictionary); case 'a': return this._createLinkElement(content, dictionary, language); } @@ -291,11 +288,11 @@ export class StructuredContentGenerator { * @param {import('structured-content').UnstyledElement} content * @param {string} dictionary * @param {?string} language - * @returns {HTMLElement} + * @returns {Promise} */ - _createStructuredContentTableElement(tag, content, dictionary, language) { + async _createStructuredContentTableElement(tag, content, dictionary, language) { const container = this._createElement('div', 'gloss-sc-table-container'); - const table = this._createStructuredContentElement(tag, content, dictionary, language, 'table', true, false); + const table = await this._createStructuredContentElement(tag, content, dictionary, language, 'table', true, false); container.appendChild(table); return container; } @@ -308,9 +305,9 @@ export class StructuredContentGenerator { * @param {'simple'|'table'|'table-cell'} type * @param {boolean} hasChildren * @param {boolean} hasStyle - * @returns {HTMLElement} + * @returns {Promise} */ - _createStructuredContentElement(tag, content, dictionary, language, type, hasChildren, hasStyle) { + async _createStructuredContentElement(tag, content, dictionary, language, type, hasChildren, hasStyle) { const node = this._createElement(tag, `gloss-sc-${tag}`); const {data, lang} = content; if (typeof data === 'object' && data !== null) { this._setElementDataset(node, data); } @@ -336,7 +333,7 @@ export class StructuredContentGenerator { if (typeof title === 'string') { node.title = title; } } if (hasChildren) { - this._appendStructuredContent(node, content.content, dictionary, language); + await this._appendStructuredContent(node, content.content, dictionary, language); } return node; } @@ -431,9 +428,9 @@ export class StructuredContentGenerator { * @param {import('structured-content').LinkElement} content * @param {string} dictionary * @param {?string} language - * @returns {HTMLAnchorElement} + * @returns {Promise} */ - _createLinkElement(content, dictionary, language) { + async _createLinkElement(content, dictionary, language) { let {href} = content; const internal = href.startsWith('?'); if (internal) { @@ -452,7 +449,7 @@ export class StructuredContentGenerator { language = lang; } - this._appendStructuredContent(text, content.content, dictionary, language); + await this._appendStructuredContent(text, content.content, dictionary, language); if (!internal) { const icon = this._createElement('span', 'gloss-link-external-icon icon'); diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index 21719da3d1..267b7722b4 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -76,6 +76,7 @@ export class Translator { * @returns {Promise<{dictionaryEntries: import('dictionary').TermDictionaryEntry[], originalTextLength: number}>} An object containing dictionary entries and the length of the original source text. */ async findTerms(mode, text, options) { + performance.mark('translator:findTerms:start'); const {enabledDictionaryMap, excludeDictionaryDefinitions, sortFrequencyDictionary, sortFrequencyDictionaryOrder, language} = options; const tagAggregator = new TranslatorTagAggregator(); let {dictionaryEntries, originalTextLength} = await this._findTermsInternal(text, options, tagAggregator); @@ -120,7 +121,8 @@ export class Translator { if (frequencies.length > 1) { this._sortTermDictionaryEntrySimpleData(frequencies); } if (pronunciations.length > 1) { this._sortTermDictionaryEntrySimpleData(pronunciations); } } - + performance.mark('translator:findTerms:end'); + performance.measure('translator:findTerms', 'translator:findTerms:start', 'translator:findTerms:end'); const withUserFacingInflections = this._addUserFacingInflections(language, dictionaryEntries); return {dictionaryEntries: withUserFacingInflections, originalTextLength}; diff --git a/ext/js/templates/anki-template-renderer.js b/ext/js/templates/anki-template-renderer.js index d526a1d13e..29ffca1fce 100644 --- a/ext/js/templates/anki-template-renderer.js +++ b/ext/js/templates/anki-template-renderer.js @@ -91,7 +91,7 @@ export class AnkiTemplateRenderer { ['join', this._join.bind(this)], ['concat', this._concat.bind(this)], ['pitchCategories', this._pitchCategories.bind(this)], - ['formatGlossary', this._formatGlossary.bind(this)], + ['formatGlossary', this._formatGlossary.bind(this)], // TODO: Does this work when it's async? ['hasMedia', this._hasMedia.bind(this)], ['getMedia', this._getMedia.bind(this)], ['pronunciation', this._pronunciation.bind(this)], @@ -669,14 +669,14 @@ export class AnkiTemplateRenderer { /** * @type {import('template-renderer').HelperFunction} */ - _formatGlossary(args, _context, options) { + async _formatGlossary(args, _context, options) { const [dictionary, content] = /** @type {[dictionary: string, content: import('dictionary-data').TermGlossaryContent]} */ (args); const data = this._getNoteDataFromOptions(options); if (typeof content === 'string') { return this._safeString(this._stringToMultiLineHtml(content)); } if (!(typeof content === 'object' && content !== null)) { return ''; } switch (content.type) { - case 'image': return this._formatGlossaryImage(content, dictionary, data); - case 'structured-content': return this._formatStructuredContent(content, dictionary, data); + case 'image': return await this._formatGlossaryImage(content, dictionary, data); + case 'structured-content': return await this._formatStructuredContent(content, dictionary, data); case 'text': return this._safeString(this._stringToMultiLineHtml(content.text)); } return ''; @@ -686,11 +686,11 @@ export class AnkiTemplateRenderer { * @param {import('dictionary-data').TermGlossaryImage} content * @param {string} dictionary * @param {import('anki-templates').NoteData} data - * @returns {string} + * @returns {Promise} */ - _formatGlossaryImage(content, dictionary, data) { + async _formatGlossaryImage(content, dictionary, data) { const structuredContentGenerator = this._createStructuredContentGenerator(data); - const node = structuredContentGenerator.createDefinitionImage(content, dictionary); + const node = await structuredContentGenerator.createDefinitionImage(content, dictionary); return this._getStructuredContentHtml(node); } @@ -698,11 +698,11 @@ export class AnkiTemplateRenderer { * @param {import('dictionary-data').TermGlossaryStructuredContent} content * @param {string} dictionary * @param {import('anki-templates').NoteData} data - * @returns {string} + * @returns {Promise} */ - _formatStructuredContent(content, dictionary, data) { + async _formatStructuredContent(content, dictionary, data) { const structuredContentGenerator = this._createStructuredContentGenerator(data); - const node = structuredContentGenerator.createStructuredContent(content.content, dictionary); + const node = await structuredContentGenerator.createStructuredContent(content.content, dictionary); return node !== null ? this._getStructuredContentHtml(node) : ''; } diff --git a/types/ext/api.d.ts b/types/ext/api.d.ts index 46be7938eb..6ea84bf52b 100644 --- a/types/ext/api.d.ts +++ b/types/ext/api.d.ts @@ -292,6 +292,12 @@ type ApiSurface = { }; return: DictionaryDatabase.MediaDataStringContent[]; }; + getMediaObjects: { + params: { + targets: GetMediaDetailsTarget[]; + }; + return: DictionaryDatabase.MediaObject[]; + }; logGenericErrorBackend: { params: { error: Core.SerializedError; diff --git a/types/ext/dictionary-database.d.ts b/types/ext/dictionary-database.d.ts index 84c6da625e..447c8e56c5 100644 --- a/types/ext/dictionary-database.d.ts +++ b/types/ext/dictionary-database.d.ts @@ -36,10 +36,13 @@ export type MediaDataArrayBufferContent = MediaDataBase; export type MediaDataStringContent = MediaDataBase; -type MediaType = ArrayBuffer | string; +type MediaType = ArrayBuffer | string | null; export type Media = {index: number} & MediaDataBase; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export type MediaObject<_T extends MediaType = ArrayBuffer> = {index: number} & MediaDataBase & {url: string}; + export type DatabaseTermEntry = { expression: string; reading: string; diff --git a/types/ext/display-content-manager.d.ts b/types/ext/display-content-manager.d.ts index a216fce9b7..7c216f8830 100644 --- a/types/ext/display-content-manager.d.ts +++ b/types/ext/display-content-manager.d.ts @@ -15,26 +15,32 @@ * along with this program. If not, see . */ -import type * as DictionaryDatabase from './dictionary-database'; - /** A callback used when a media file has been loaded. */ export type OnLoadCallback = ( /** The URL of the media that was loaded. */ url: string, -) => void; +) => Promise; /** A callback used when a media file should be unloaded. */ export type OnUnloadCallback = ( /** Whether or not the media was fully loaded. */ fullyLoaded: boolean, -) => void; - -export type CachedMediaDataLoaded = { - data: DictionaryDatabase.MediaDataStringContent; - url: string; -}; +) => Promise; export type LoadMediaDataInfo = { onUnload: OnUnloadCallback; loaded: boolean; }; + +export type MediaCacheKey = string & {readonly __tag: unique symbol}; + +export type LoadMediaRequest = { + /** The path to the media file in the dictionary. */ + path: string; + /** The name of the dictionary. */ + dictionary: string; + /** The callback that is executed if the media was loaded successfully. */ + onLoad: OnLoadCallback; + /** The callback that is executed when the media should be unloaded. */ + onUnload: OnUnloadCallback; +}; diff --git a/types/ext/offscreen.d.ts b/types/ext/offscreen.d.ts index 2f180e215c..4d978614f7 100644 --- a/types/ext/offscreen.d.ts +++ b/types/ext/offscreen.d.ts @@ -49,6 +49,12 @@ type ApiSurface = { }; return: DictionaryDatabase.Media[]; }; + databaseGetMediaObjectsOffscreen: { + params: { + targets: DictionaryDatabase.MediaRequest[]; + }; + return: DictionaryDatabase.MediaObject[]; + }; translatorPrepareOffscreen: { params: void; return: void;