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/handlebars.js"></script>
|
||||||
<script src="/bg/js/japanese.js"></script>
|
<script src="/bg/js/japanese.js"></script>
|
||||||
<script src="/bg/js/json-schema.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/options.js"></script>
|
||||||
<script src="/bg/js/profile-conditions.js"></script>
|
<script src="/bg/js/profile-conditions.js"></script>
|
||||||
<script src="/bg/js/request.js"></script>
|
<script src="/bg/js/request.js"></script>
|
||||||
|
@ -31,8 +31,85 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Array of definitions for the term/expression.",
|
"description": "Array of definitions for the term/expression.",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"oneOf": [
|
||||||
"description": "Single definition for the term/expression."
|
{
|
||||||
|
"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 {
|
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, [
|
Database._upgrade(db, transaction, oldVersion, [
|
||||||
{
|
{
|
||||||
version: 2,
|
version: 2,
|
||||||
@ -90,6 +90,15 @@ class Database {
|
|||||||
indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse']
|
indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 6,
|
||||||
|
stores: {
|
||||||
|
media: {
|
||||||
|
primaryKey: {keyPath: 'id', autoIncrement: true},
|
||||||
|
indices: ['dictionary', 'path']
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
/* global
|
/* global
|
||||||
* JSZip
|
* JSZip
|
||||||
* JsonSchema
|
* JsonSchema
|
||||||
|
* mediaUtility
|
||||||
* requestJson
|
* 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
|
// Add dictionary
|
||||||
const summary = this._createSummary(dictionaryTitle, version, index, {prefixWildcardsSupported});
|
const summary = this._createSummary(dictionaryTitle, version, index, {prefixWildcardsSupported});
|
||||||
|
|
||||||
@ -188,6 +205,7 @@ class DictionaryImporter {
|
|||||||
await bulkAdd('kanji', kanjiList);
|
await bulkAdd('kanji', kanjiList);
|
||||||
await bulkAdd('kanjiMeta', kanjiMetaList);
|
await bulkAdd('kanjiMeta', kanjiMetaList);
|
||||||
await bulkAdd('tagMeta', tagList);
|
await bulkAdd('tagMeta', tagList);
|
||||||
|
await bulkAdd('media', media);
|
||||||
|
|
||||||
return {result: summary, errors};
|
return {result: summary, errors};
|
||||||
}
|
}
|
||||||
@ -275,4 +293,76 @@ class DictionaryImporter {
|
|||||||
|
|
||||||
return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank];
|
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