Add support for displaying images
This commit is contained in:
parent
fd6ea0e404
commit
ac603d54a3
@ -85,6 +85,7 @@
|
|||||||
<script src="/mixed/js/display-context.js"></script>
|
<script src="/mixed/js/display-context.js"></script>
|
||||||
<script src="/mixed/js/display.js"></script>
|
<script src="/mixed/js/display.js"></script>
|
||||||
<script src="/mixed/js/display-generator.js"></script>
|
<script src="/mixed/js/display-generator.js"></script>
|
||||||
|
<script src="/mixed/js/media-loader.js"></script>
|
||||||
<script src="/mixed/js/scroll.js"></script>
|
<script src="/mixed/js/scroll.js"></script>
|
||||||
<script src="/mixed/js/text-scanner.js"></script>
|
<script src="/mixed/js/text-scanner.js"></script>
|
||||||
<script src="/mixed/js/template-handler.js"></script>
|
<script src="/mixed/js/template-handler.js"></script>
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
<script src="/mixed/js/display-context.js"></script>
|
<script src="/mixed/js/display-context.js"></script>
|
||||||
<script src="/mixed/js/display.js"></script>
|
<script src="/mixed/js/display.js"></script>
|
||||||
<script src="/mixed/js/display-generator.js"></script>
|
<script src="/mixed/js/display-generator.js"></script>
|
||||||
|
<script src="/mixed/js/media-loader.js"></script>
|
||||||
<script src="/mixed/js/scroll.js"></script>
|
<script src="/mixed/js/scroll.js"></script>
|
||||||
<script src="/mixed/js/template-handler.js"></script>
|
<script src="/mixed/js/template-handler.js"></script>
|
||||||
|
|
||||||
|
@ -94,3 +94,10 @@ h2 { border-bottom-color: #2f2f2f; }
|
|||||||
#term-pitch-accent-graph-dot-downstep>circle:last-of-type {
|
#term-pitch-accent-graph-dot-downstep>circle:last-of-type {
|
||||||
fill: #ffffff;
|
fill: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.term-glossary-image-container {
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
}
|
||||||
|
.term-glossary-image-container-overlay {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
@ -94,3 +94,10 @@ h2 { border-bottom-color: #eeeeee; }
|
|||||||
#term-pitch-accent-graph-dot-downstep>circle:last-of-type {
|
#term-pitch-accent-graph-dot-downstep>circle:last-of-type {
|
||||||
fill: #000000;
|
fill: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.term-glossary-image-container {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
}
|
||||||
|
.term-glossary-image-container-overlay {
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
@ -611,6 +611,102 @@ button.action-button {
|
|||||||
stroke-width: 5;
|
stroke-width: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.term-glossary-image-container {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 0;
|
||||||
|
font-size: 0.07142857em; /* 14px => 1px */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-image-link {
|
||||||
|
cursor: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-image-link[href]:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-image-container-overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 14em; /* 1px => 14px; */
|
||||||
|
line-height: 1.42857143; /* 14px => 20px */
|
||||||
|
display: table;
|
||||||
|
table-layout: fixed;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-item[data-has-image=true][data-image-load-state=load-error] .term-glossary-image-container-overlay:after {
|
||||||
|
content: "Image failed to load";
|
||||||
|
display: table-cell;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-image {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: top;
|
||||||
|
object-fit: contain;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-image:not([src]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-image[data-pixelated=true] {
|
||||||
|
image-rendering: auto;
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-image-aspect-ratio-sizer {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-image-link-text:before {
|
||||||
|
content: "[";
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-glossary-image-link-text:after {
|
||||||
|
content: "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-compact-glossaries=true] .term-glossary-image-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-compact-glossaries=true]) .term-glossary-image-link-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not([data-compact-glossaries=true]) .term-glossary-image-description {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Kanji
|
* Kanji
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
</li></template>
|
</li></template>
|
||||||
<template id="term-definition-disambiguation-template"><span class="term-definition-disambiguation"></span></template>
|
<template id="term-definition-disambiguation-template"><span class="term-definition-disambiguation"></span></template>
|
||||||
<template id="term-glossary-item-template"><li class="term-glossary-item"><span class="term-glossary-separator"> </span><span class="term-glossary"></span></li></template>
|
<template id="term-glossary-item-template"><li class="term-glossary-item"><span class="term-glossary-separator"> </span><span class="term-glossary"></span></li></template>
|
||||||
|
<template id="term-glossary-item-image-template"><li class="term-glossary-item" data-has-image="true"><span class="term-glossary-separator"> </span><span class="term-glossary"><a class="term-glossary-image-link" target="_blank" rel="noreferrer noopener"><span class="term-glossary-image-container"><span class="term-glossary-image-aspect-ratio-sizer"></span><img class="term-glossary-image" alt="" /><span class="term-glossary-image-container-overlay"></span></span><span class="term-glossary-image-link-text">Image</span> </a><span class="term-glossary-image-description"></span></span></li></template>
|
||||||
<template id="term-reason-template"><span class="term-reason"></span><span class="term-reason-separator"> </span></template>
|
<template id="term-reason-template"><span class="term-reason"></span><span class="term-reason-separator"> </span></template>
|
||||||
|
|
||||||
<template id="term-pitch-accent-static-template"><svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
<template id="term-pitch-accent-static-template"><svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||||
|
@ -22,7 +22,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class DisplayGenerator {
|
class DisplayGenerator {
|
||||||
constructor() {
|
constructor({mediaLoader}) {
|
||||||
|
this._mediaLoader = mediaLoader;
|
||||||
this._templateHandler = null;
|
this._templateHandler = null;
|
||||||
this._termPitchAccentStaticTemplateIsSetup = false;
|
this._termPitchAccentStaticTemplateIsSetup = false;
|
||||||
}
|
}
|
||||||
@ -176,16 +177,30 @@ class DisplayGenerator {
|
|||||||
const onlyListContainer = node.querySelector('.term-definition-disambiguation-list');
|
const onlyListContainer = node.querySelector('.term-definition-disambiguation-list');
|
||||||
const glossaryContainer = node.querySelector('.term-glossary-list');
|
const glossaryContainer = node.querySelector('.term-glossary-list');
|
||||||
|
|
||||||
node.dataset.dictionary = details.dictionary;
|
const dictionary = details.dictionary;
|
||||||
|
node.dataset.dictionary = dictionary;
|
||||||
|
|
||||||
this._appendMultiple(tagListContainer, this._createTag.bind(this), details.definitionTags);
|
this._appendMultiple(tagListContainer, this._createTag.bind(this), details.definitionTags);
|
||||||
this._appendMultiple(onlyListContainer, this._createTermDisambiguation.bind(this), details.only);
|
this._appendMultiple(onlyListContainer, this._createTermDisambiguation.bind(this), details.only);
|
||||||
this._appendMultiple(glossaryContainer, this._createTermGlossaryItem.bind(this), details.glossary);
|
this._appendMultiple(glossaryContainer, this._createTermGlossaryItem.bind(this), details.glossary, dictionary);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createTermGlossaryItem(glossary) {
|
_createTermGlossaryItem(glossary, dictionary) {
|
||||||
|
if (typeof glossary === 'string') {
|
||||||
|
return this._createTermGlossaryItemText(glossary);
|
||||||
|
} else if (typeof glossary === 'object' && glossary !== null) {
|
||||||
|
switch (glossary.type) {
|
||||||
|
case 'image':
|
||||||
|
return this._createTermGlossaryItemImage(glossary, dictionary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createTermGlossaryItemText(glossary) {
|
||||||
const node = this._templateHandler.instantiate('term-glossary-item');
|
const node = this._templateHandler.instantiate('term-glossary-item');
|
||||||
const container = node.querySelector('.term-glossary');
|
const container = node.querySelector('.term-glossary');
|
||||||
if (container !== null) {
|
if (container !== null) {
|
||||||
@ -194,6 +209,68 @@ class DisplayGenerator {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_createTermGlossaryItemImage(data, dictionary) {
|
||||||
|
const {path, width, height, preferredWidth, preferredHeight, title, description, pixelated} = data;
|
||||||
|
|
||||||
|
const usedWidth = (
|
||||||
|
typeof preferredWidth === 'number' ?
|
||||||
|
preferredWidth :
|
||||||
|
width
|
||||||
|
);
|
||||||
|
const aspectRatio = (
|
||||||
|
typeof preferredWidth === 'number' &&
|
||||||
|
typeof preferredHeight === 'number' ?
|
||||||
|
preferredWidth / preferredHeight :
|
||||||
|
width / height
|
||||||
|
);
|
||||||
|
|
||||||
|
const node = this._templateHandler.instantiate('term-glossary-item-image');
|
||||||
|
node.dataset.path = path;
|
||||||
|
node.dataset.dictionary = dictionary;
|
||||||
|
node.dataset.imageLoadState = 'not-loaded';
|
||||||
|
|
||||||
|
const imageContainer = node.querySelector('.term-glossary-image-container');
|
||||||
|
imageContainer.style.width = `${usedWidth}em`;
|
||||||
|
if (typeof title === 'string') {
|
||||||
|
imageContainer.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aspectRatioSizer = node.querySelector('.term-glossary-image-aspect-ratio-sizer');
|
||||||
|
aspectRatioSizer.style.paddingTop = `${aspectRatio * 100.0}%`;
|
||||||
|
|
||||||
|
const image = node.querySelector('img.term-glossary-image');
|
||||||
|
const imageLink = node.querySelector('.term-glossary-image-link');
|
||||||
|
image.dataset.pixelated = `${pixelated === true}`;
|
||||||
|
|
||||||
|
if (this._mediaLoader !== null) {
|
||||||
|
this._mediaLoader.loadMedia(
|
||||||
|
path,
|
||||||
|
dictionary,
|
||||||
|
(url) => this._setImageData(node, image, imageLink, url, false),
|
||||||
|
() => this._setImageData(node, image, imageLink, null, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof description === 'string') {
|
||||||
|
const container = node.querySelector('.term-glossary-image-description');
|
||||||
|
this._appendMultilineText(container, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setImageData(container, image, imageLink, url, unloaded) {
|
||||||
|
if (url !== null) {
|
||||||
|
image.src = url;
|
||||||
|
imageLink.href = url;
|
||||||
|
container.dataset.imageLoadState = 'loaded';
|
||||||
|
} else {
|
||||||
|
image.removeAttribute('src');
|
||||||
|
imageLink.removeAttribute('href');
|
||||||
|
container.dataset.imageLoadState = unloaded ? 'unloaded' : 'load-error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_createTermDisambiguation(disambiguation) {
|
_createTermDisambiguation(disambiguation) {
|
||||||
const node = this._templateHandler.instantiate('term-definition-disambiguation');
|
const node = this._templateHandler.instantiate('term-definition-disambiguation');
|
||||||
node.dataset.term = disambiguation;
|
node.dataset.term = disambiguation;
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
* DOM
|
* DOM
|
||||||
* DisplayContext
|
* DisplayContext
|
||||||
* DisplayGenerator
|
* DisplayGenerator
|
||||||
|
* MediaLoader
|
||||||
* WindowScroll
|
* WindowScroll
|
||||||
* apiAudioGetUri
|
* apiAudioGetUri
|
||||||
* apiBroadcastTab
|
* apiBroadcastTab
|
||||||
@ -62,7 +63,8 @@ class Display {
|
|||||||
this.clickScanPrevent = false;
|
this.clickScanPrevent = false;
|
||||||
this.setContentToken = null;
|
this.setContentToken = null;
|
||||||
|
|
||||||
this.displayGenerator = new DisplayGenerator();
|
this.mediaLoader = new MediaLoader();
|
||||||
|
this.displayGenerator = new DisplayGenerator({mediaLoader: this.mediaLoader});
|
||||||
this.windowScroll = new WindowScroll();
|
this.windowScroll = new WindowScroll();
|
||||||
|
|
||||||
this._onKeyDownHandlers = new Map([
|
this._onKeyDownHandlers = new Map([
|
||||||
@ -479,6 +481,8 @@ class Display {
|
|||||||
const token = {}; // Unique identifier token
|
const token = {}; // Unique identifier token
|
||||||
this.setContentToken = token;
|
this.setContentToken = token;
|
||||||
try {
|
try {
|
||||||
|
this.mediaLoader.unloadAll();
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'terms':
|
case 'terms':
|
||||||
await this.setContentTerms(details.definitions, details.context, token);
|
await this.setContentTerms(details.definitions, details.context, token);
|
||||||
|
107
ext/mixed/js/media-loader.js
Normal file
107
ext/mixed/js/media-loader.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* global
|
||||||
|
* apiGetMedia
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MediaLoader {
|
||||||
|
constructor() {
|
||||||
|
this._token = {};
|
||||||
|
this._mediaCache = new Map();
|
||||||
|
this._loadMediaData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMedia(path, dictionaryName, onLoad, onUnload) {
|
||||||
|
const token = this.token;
|
||||||
|
const data = {onUnload, loaded: false};
|
||||||
|
|
||||||
|
this._loadMediaData.push(data);
|
||||||
|
|
||||||
|
const media = await this.getMedia(path, dictionaryName);
|
||||||
|
if (token !== this.token) { return; }
|
||||||
|
|
||||||
|
onLoad(media.url);
|
||||||
|
data.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {url} of map.values()) {
|
||||||
|
if (url !== null) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._mediaCache.clear();
|
||||||
|
|
||||||
|
this._token = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMedia(path, dictionaryName) {
|
||||||
|
let cachedData;
|
||||||
|
let dictionaryCache = this._mediaCache.get(dictionaryName);
|
||||||
|
if (typeof dictionaryCache !== 'undefined') {
|
||||||
|
cachedData = dictionaryCache.get(path);
|
||||||
|
} else {
|
||||||
|
dictionaryCache = new Map();
|
||||||
|
this._mediaCache.set(dictionaryName, dictionaryCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof cachedData === 'undefined') {
|
||||||
|
cachedData = {
|
||||||
|
promise: null,
|
||||||
|
data: null,
|
||||||
|
url: null
|
||||||
|
};
|
||||||
|
dictionaryCache.set(path, cachedData);
|
||||||
|
cachedData.promise = this._getMediaData(path, dictionaryName, cachedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedData.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getMediaData(path, dictionaryName, cachedData) {
|
||||||
|
const token = this._token;
|
||||||
|
const data = (await apiGetMedia([{path, dictionaryName}]))[0];
|
||||||
|
if (token === this._token && data !== null) {
|
||||||
|
const sourceArrayBuffer = this._base64ToArrayBuffer(data.source);
|
||||||
|
const blob = new Blob([sourceArrayBuffer], {type: data.mediaType});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
cachedData.data = data;
|
||||||
|
cachedData.url = url;
|
||||||
|
}
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
_base64ToArrayBuffer(source) {
|
||||||
|
const binarySource = window.atob(source);
|
||||||
|
const length = binarySource.length;
|
||||||
|
const array = new Uint8Array(length);
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
array[i] = binarySource.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return array.buffer;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user