From 00c5ae79833a641ccc5f7d31b6eea3e91db4cb71 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 31 Jul 2021 12:30:31 -0400 Subject: [PATCH] DictionaryImporterMediaLoader (#1860) * Rename param for consistency * Move media loading functionality into DictionaryImporterMediaLoader * Create test class for media loading * Remove unnecessary Blob/Image/URL functionality --- dev/database-vm.js | 69 +++---------------- dev/translator-vm.js | 5 +- dev/vm.js | 18 +---- .../dictionary-importer-media-loader.js | 57 +++++++++++++++ ext/js/language/dictionary-importer.js | 41 ++--------- .../settings/dictionary-import-controller.js | 4 +- ext/settings.html | 1 + ext/welcome.html | 1 + test/test-database.js | 14 ++-- 9 files changed, 91 insertions(+), 119 deletions(-) create mode 100644 ext/js/language/dictionary-importer-media-loader.js diff --git a/dev/database-vm.js b/dev/database-vm.js index 07d9bd5a..ebde5a2a 100644 --- a/dev/database-vm.js +++ b/dev/database-vm.js @@ -30,63 +30,6 @@ const chrome = { } }; -class Image { - constructor() { - this._src = ''; - this._loadCallbacks = []; - } - - get src() { - return this._src; - } - - set src(value) { - this._src = value; - this._delayTriggerLoad(); - } - - get naturalWidth() { - return 100; - } - - get naturalHeight() { - return 100; - } - - addEventListener(eventName, callback) { - if (eventName === 'load') { - this._loadCallbacks.push(callback); - } - } - - removeEventListener(eventName, callback) { - if (eventName === 'load') { - const index = this._loadCallbacks.indexOf(callback); - if (index >= 0) { - this._loadCallbacks.splice(index, 1); - } - } - } - - removeAttribute() { - // NOP - } - - async _delayTriggerLoad() { - await Promise.resolve(); - for (const callback of this._loadCallbacks) { - callback(); - } - } -} - -class Blob { - constructor(array, options) { - this._array = array; - this._options = options; - } -} - async function fetch(url2) { const extDir = path.join(__dirname, '..', 'ext'); let filePath; @@ -114,8 +57,6 @@ class DatabaseVM extends VM { constructor(globals={}) { super(Object.assign({ chrome, - Image, - Blob, fetch, indexedDB: global.indexedDB, IDBKeyRange: global.IDBKeyRange, @@ -127,6 +68,14 @@ class DatabaseVM extends VM { } } +class DatabaseVMDictionaryImporterMediaLoader { + async getImageResolution() { + // Placeholder values + return {width: 100, height: 100}; + } +} + module.exports = { - DatabaseVM + DatabaseVM, + DatabaseVMDictionaryImporterMediaLoader }; diff --git a/dev/translator-vm.js b/dev/translator-vm.js index 3c3886ba..d616afc5 100644 --- a/dev/translator-vm.js +++ b/dev/translator-vm.js @@ -18,7 +18,7 @@ const fs = require('fs'); const path = require('path'); const assert = require('assert'); -const {DatabaseVM} = require('./database-vm'); +const {DatabaseVM, DatabaseVMDictionaryImporterMediaLoader} = require('./database-vm'); const {createDictionaryArchive} = require('./util'); function clone(value) { @@ -75,7 +75,8 @@ class TranslatorVM extends DatabaseVM { const testDictionaryContent = await testDictionary.generateAsync({type: 'arraybuffer'}); // Setup database - const dictionaryImporter = new DictionaryImporter(); + const dictionaryImporterMediaLoader = new DatabaseVMDictionaryImporterMediaLoader(); + const dictionaryImporter = new DictionaryImporter(dictionaryImporterMediaLoader); const dictionaryDatabase = new DictionaryDatabase(); await dictionaryDatabase.prepare(); diff --git a/dev/vm.js b/dev/vm.js index 363cc115..9db87cdf 100644 --- a/dev/vm.js +++ b/dev/vm.js @@ -116,7 +116,7 @@ function deepStrictEqual(actual, expected) { } -function createURLClass(urlMap) { +function createURLClass() { const BaseURL = URL; const result = function URL(url) { const u = new BaseURL(url); @@ -133,23 +133,13 @@ function createURLClass(urlMap) { this.searchParams = u.searchParams; this.username = u.username; }; - result.createObjectURL = (object) => { - const id = crypto.randomBytes(16).toString('hex'); - const url = `blob:${id}`; - urlMap.set(url, object); - return url; - }; - result.revokeObjectURL = (url) => { - urlMap.delete(url); - }; return result; } class VM { constructor(context={}) { - this._urlMap = new Map(); - context.URL = createURLClass(this._urlMap); + context.URL = createURLClass(); context.crypto = { getRandomValues: (array) => { const buffer = crypto.randomBytes(array.byteLength); @@ -205,10 +195,6 @@ class VM { return single ? results[0] : results; } - - getUrlObject(url) { - return this._urlMap.get(url); - } } diff --git a/ext/js/language/dictionary-importer-media-loader.js b/ext/js/language/dictionary-importer-media-loader.js new file mode 100644 index 00000000..bbcc5476 --- /dev/null +++ b/ext/js/language/dictionary-importer-media-loader.js @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2021 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global + * MediaUtil + */ + +/** + * Class used for loading and validating media during the dictionary import process. + */ +class DictionaryImporterMediaLoader { + /** + * Attempts to load an image using a base64 encoded content and a media type + * and returns its resolution. + * @param mediaType The media type for the image content. + * @param content The binary content for the image, encoded in base64. + * @returns A Promise which resolves with {width, height} on success, + * otherwise an error is thrown. + */ + getImageResolution(mediaType, content) { + return new Promise((resolve, reject) => { + const image = new Image(); + const eventListeners = new EventListenerCollection(); + const cleanup = () => { + image.removeAttribute('src'); + URL.revokeObjectURL(url); + eventListeners.removeAllEventListeners(); + }; + eventListeners.addEventListener(image, 'load', () => { + const {naturalWidth: width, naturalHeight: height} = image; + cleanup(); + resolve({width, height}); + }, false); + eventListeners.addEventListener(image, 'error', () => { + cleanup(); + reject(new Error('Image failed to load')); + }, false); + const blob = MediaUtil.createBlobFromBase64Content(content, mediaType); + const url = URL.createObjectURL(blob); + image.src = url; + }); + } +} diff --git a/ext/js/language/dictionary-importer.js b/ext/js/language/dictionary-importer.js index 27b3c44e..b931c929 100644 --- a/ext/js/language/dictionary-importer.js +++ b/ext/js/language/dictionary-importer.js @@ -22,11 +22,12 @@ */ class DictionaryImporter { - constructor() { + constructor(mediaLoader) { + this._mediaLoader = mediaLoader; this._schemas = new Map(); } - async importDictionary(dictionaryDatabase, archiveSource, details, onProgress) { + async importDictionary(dictionaryDatabase, archiveContent, details, onProgress) { if (!dictionaryDatabase) { throw new Error('Invalid database'); } @@ -37,7 +38,7 @@ class DictionaryImporter { const hasOnProgress = (typeof onProgress === 'function'); // Read archive - const archive = await JSZip.loadAsync(archiveSource); + const archive = await JSZip.loadAsync(archiveContent); // Read and validate index const indexFileName = 'index.json'; @@ -469,7 +470,7 @@ class DictionaryImporter { let width; let height; try { - ({width, height} = await this._getImageResolution(mediaType, content)); + ({width, height} = await this._mediaLoader.getImageResolution(mediaType, content)); } catch (e) { throw createError('Could not load image'); } @@ -502,36 +503,4 @@ class DictionaryImporter { } return await response.json(); } - - /** - * Attempts to load an image using a base64 encoded content and a media type - * and returns its resolution. - * @param mediaType The media type for the image content. - * @param content The binary content for the image, encoded in base64. - * @returns A Promise which resolves with {width, height} on success, - * otherwise an error is thrown. - */ - _getImageResolution(mediaType, content) { - return new Promise((resolve, reject) => { - const image = new Image(); - const eventListeners = new EventListenerCollection(); - const cleanup = () => { - image.removeAttribute('src'); - URL.revokeObjectURL(url); - eventListeners.removeAllEventListeners(); - }; - eventListeners.addEventListener(image, 'load', () => { - const {naturalWidth: width, naturalHeight: height} = image; - cleanup(); - resolve({width, height}); - }, false); - eventListeners.addEventListener(image, 'error', () => { - cleanup(); - reject(new Error('Image failed to load')); - }, false); - const blob = MediaUtil.createBlobFromBase64Content(content, mediaType); - const url = URL.createObjectURL(blob); - image.src = url; - }); - } } diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js index db9a1a50..128e18cb 100644 --- a/ext/js/pages/settings/dictionary-import-controller.js +++ b/ext/js/pages/settings/dictionary-import-controller.js @@ -19,6 +19,7 @@ * DictionaryController * DictionaryDatabase * DictionaryImporter + * DictionaryImporterMediaLoader */ class DictionaryImportController { @@ -184,7 +185,8 @@ class DictionaryImportController { async _importDictionary(file, importDetails, onProgress) { const dictionaryDatabase = await this._getPreparedDictionaryDatabase(); try { - const dictionaryImporter = new DictionaryImporter(); + const dictionaryImporterMediaLoader = new DictionaryImporterMediaLoader(); + const dictionaryImporter = new DictionaryImporter(dictionaryImporterMediaLoader); const archiveContent = await this._readFile(file); const {result, errors} = await dictionaryImporter.importDictionary(dictionaryDatabase, archiveContent, importDetails, onProgress); yomichan.api.triggerDatabaseUpdated('dictionary', 'import'); diff --git a/ext/settings.html b/ext/settings.html index 9d2d9791..cd2376d6 100644 --- a/ext/settings.html +++ b/ext/settings.html @@ -3451,6 +3451,7 @@ + diff --git a/ext/welcome.html b/ext/welcome.html index 85022e2c..5c9f4469 100644 --- a/ext/welcome.html +++ b/ext/welcome.html @@ -409,6 +409,7 @@ + diff --git a/test/test-database.js b/test/test-database.js index 19ac49e2..ac7e825b 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -18,7 +18,7 @@ const path = require('path'); const assert = require('assert'); const {createDictionaryArchive, testMain} = require('../dev/util'); -const {DatabaseVM} = require('../dev/database-vm'); +const {DatabaseVM, DatabaseVMDictionaryImporterMediaLoader} = require('../dev/database-vm'); const vm = new DatabaseVM(); @@ -41,6 +41,12 @@ function createTestDictionaryArchive(dictionary, dictionaryName) { } +function createDictionaryImporter() { + const dictionaryImporterMediaLoader = new DatabaseVMDictionaryImporterMediaLoader(); + return new DictionaryImporter(dictionaryImporterMediaLoader); +} + + function countDictionaryDatabaseEntriesWithTerm(dictionaryDatabaseEntries, term) { return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.term === term ? 1 : 0)), 0); } @@ -125,7 +131,7 @@ async function testDatabase1() { ]; // Setup database - const dictionaryImporter = new DictionaryImporter(); + const dictionaryImporter = createDictionaryImporter(); const dictionaryDatabase = new DictionaryDatabase(); await dictionaryDatabase.prepare(); @@ -775,7 +781,7 @@ async function testDatabase2() { ]); // Setup database - const dictionaryImporter = new DictionaryImporter(); + const dictionaryImporter = createDictionaryImporter(); const dictionaryDatabase = new DictionaryDatabase(); // Error: not prepared @@ -817,7 +823,7 @@ async function testDatabase3() { ]; // Setup database - const dictionaryImporter = new DictionaryImporter(); + const dictionaryImporter = createDictionaryImporter(); const dictionaryDatabase = new DictionaryDatabase(); await dictionaryDatabase.prepare();