Add support for importing and storing media files
This commit is contained in:
parent
3b2663ba09
commit
8106f4744b
@ -36,6 +36,7 @@
|
||||
<script src="/bg/js/handlebars.js"></script>
|
||||
<script src="/bg/js/japanese.js"></script>
|
||||
<script src="/bg/js/json-schema.js"></script>
|
||||
<script src="/bg/js/media-utility.js"></script>
|
||||
<script src="/bg/js/options.js"></script>
|
||||
<script src="/bg/js/profile-conditions.js"></script>
|
||||
<script src="/bg/js/request.js"></script>
|
||||
|
@ -31,8 +31,85 @@
|
||||
"type": "array",
|
||||
"description": "Array of definitions for the term/expression.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Single definition for the term/expression."
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Single definition for the term/expression."
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Single detailed definition for the term/expression.",
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of the data for this definition.",
|
||||
"enum": ["text", "image"]
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"required": [
|
||||
"type",
|
||||
"text"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["text"]
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Single definition for the term/expression."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"type",
|
||||
"path"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["image"]
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the image file in the archive."
|
||||
},
|
||||
"width": {
|
||||
"type": "integer",
|
||||
"description": "Preferred width of the image.",
|
||||
"minimum": 1
|
||||
},
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"description": "Preferred width of the image.",
|
||||
"minimum": 1
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Hover text for the image."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the image."
|
||||
},
|
||||
"pixelated": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the image should appear pixelated at sizes larger than the image's native resolution.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -33,7 +33,7 @@ class Database {
|
||||
}
|
||||
|
||||
try {
|
||||
this.db = await Database._open('dict', 5, (db, transaction, oldVersion) => {
|
||||
this.db = await Database._open('dict', 6, (db, transaction, oldVersion) => {
|
||||
Database._upgrade(db, transaction, oldVersion, [
|
||||
{
|
||||
version: 2,
|
||||
@ -90,6 +90,15 @@ class Database {
|
||||
indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
version: 6,
|
||||
stores: {
|
||||
media: {
|
||||
primaryKey: {keyPath: 'id', autoIncrement: true},
|
||||
indices: ['dictionary', 'path']
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
@ -18,6 +18,7 @@
|
||||
/* global
|
||||
* JSZip
|
||||
* JsonSchema
|
||||
* mediaUtility
|
||||
* requestJson
|
||||
*/
|
||||
|
||||
@ -148,6 +149,22 @@ class DictionaryImporter {
|
||||
}
|
||||
}
|
||||
|
||||
// Extended data support
|
||||
const extendedDataContext = {
|
||||
archive,
|
||||
media: new Map()
|
||||
};
|
||||
for (const entry of termList) {
|
||||
const glossaryList = entry.glossary;
|
||||
for (let i = 0, ii = glossaryList.length; i < ii; ++i) {
|
||||
const glossary = glossaryList[i];
|
||||
if (typeof glossary !== 'object' || glossary === null) { continue; }
|
||||
glossaryList[i] = await this._formatDictionaryTermGlossaryObject(glossary, extendedDataContext, entry);
|
||||
}
|
||||
}
|
||||
|
||||
const media = [...extendedDataContext.media.values()];
|
||||
|
||||
// Add dictionary
|
||||
const summary = this._createSummary(dictionaryTitle, version, index, {prefixWildcardsSupported});
|
||||
|
||||
@ -188,6 +205,7 @@ class DictionaryImporter {
|
||||
await bulkAdd('kanji', kanjiList);
|
||||
await bulkAdd('kanjiMeta', kanjiMetaList);
|
||||
await bulkAdd('tagMeta', tagList);
|
||||
await bulkAdd('media', media);
|
||||
|
||||
return {result: summary, errors};
|
||||
}
|
||||
@ -275,4 +293,76 @@ class DictionaryImporter {
|
||||
|
||||
return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank];
|
||||
}
|
||||
|
||||
async _formatDictionaryTermGlossaryObject(data, context, entry) {
|
||||
switch (data.type) {
|
||||
case 'text':
|
||||
return data.text;
|
||||
case 'image':
|
||||
return await this._formatDictionaryTermGlossaryImage(data, context, entry);
|
||||
default:
|
||||
throw new Error(`Unhandled data type: ${data.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async _formatDictionaryTermGlossaryImage(data, context, entry) {
|
||||
const dictionary = entry.dictionary;
|
||||
const {path, width: preferredWidth, height: preferredHeight, title, description, pixelated} = data;
|
||||
if (context.media.has(path)) {
|
||||
// Already exists
|
||||
return data;
|
||||
}
|
||||
|
||||
let errorSource = entry.expression;
|
||||
if (entry.reading.length > 0) {
|
||||
errorSource += ` (${entry.reading});`;
|
||||
}
|
||||
|
||||
const file = context.archive.file(path);
|
||||
if (file === null) {
|
||||
throw new Error(`Could not find image at path ${JSON.stringify(path)} for ${errorSource}`);
|
||||
}
|
||||
|
||||
const source = await file.async('base64');
|
||||
const mediaType = mediaUtility.getImageMediaTypeFromFileName(path);
|
||||
if (mediaType === null) {
|
||||
throw new Error(`Could not determine media type for image at path ${JSON.stringify(path)} for ${errorSource}`);
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await mediaUtility.loadImage(mediaType, source);
|
||||
} catch (e) {
|
||||
throw new Error(`Could not load image at path ${JSON.stringify(path)} for ${errorSource}`);
|
||||
}
|
||||
|
||||
const width = image.naturalWidth;
|
||||
const height = image.naturalHeight;
|
||||
|
||||
// Create image data
|
||||
const mediaData = {
|
||||
dictionary,
|
||||
path,
|
||||
mediaType,
|
||||
width,
|
||||
height,
|
||||
source
|
||||
};
|
||||
context.media.set(path, mediaData);
|
||||
|
||||
// Create new data
|
||||
const newData = {
|
||||
type: 'image',
|
||||
path,
|
||||
width,
|
||||
height
|
||||
};
|
||||
if (typeof preferredWidth === 'number') { newData.preferredWidth = preferredWidth; }
|
||||
if (typeof preferredHeight === 'number') { newData.preferredHeight = preferredHeight; }
|
||||
if (typeof title === 'string') { newData.title = title; }
|
||||
if (typeof description === 'string') { newData.description = description; }
|
||||
if (typeof pixelated === 'boolean') { newData.pixelated = pixelated; }
|
||||
|
||||
return newData;
|
||||
}
|
||||
}
|
||||
|
75
ext/bg/js/media-utility.js
Normal file
75
ext/bg/js/media-utility.js
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const mediaUtility = (() => {
|
||||
function getFileNameExtension(fileName) {
|
||||
const match = /\.[^.]*$/.exec(fileName);
|
||||
return match !== null ? match[0] : '';
|
||||
}
|
||||
|
||||
function getImageMediaTypeFromFileName(fileName) {
|
||||
switch (getFileNameExtension(fileName).toLowerCase()) {
|
||||
case '.apng':
|
||||
return 'image/apng';
|
||||
case '.bmp':
|
||||
return 'image/bmp';
|
||||
case '.gif':
|
||||
return 'image/gif';
|
||||
case '.ico':
|
||||
case '.cur':
|
||||
return 'image/x-icon';
|
||||
case '.jpg':
|
||||
case '.jpeg':
|
||||
case '.jfif':
|
||||
case '.pjpeg':
|
||||
case '.pjp':
|
||||
return 'image/jpeg';
|
||||
case '.png':
|
||||
return 'image/png';
|
||||
case '.svg':
|
||||
return 'image/svg+xml';
|
||||
case '.tif':
|
||||
case '.tiff':
|
||||
return 'image/tiff';
|
||||
case '.webp':
|
||||
return 'image/webp';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadImage(mediaType, base64Source) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
const eventListeners = new EventListenerCollection();
|
||||
eventListeners.addEventListener(image, 'load', () => {
|
||||
eventListeners.removeAllEventListeners();
|
||||
resolve(image);
|
||||
}, false);
|
||||
eventListeners.addEventListener(image, 'error', () => {
|
||||
eventListeners.removeAllEventListeners();
|
||||
reject(new Error('Image failed to load'));
|
||||
}, false);
|
||||
image.src = `data:${mediaType};base64,${base64Source}`;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
getImageMediaTypeFromFileName,
|
||||
loadImage
|
||||
};
|
||||
})();
|
Loading…
Reference in New Issue
Block a user