Add support for displaying images

This commit is contained in:
toasted-nutbread 2020-04-11 14:23:49 -04:00
parent fd6ea0e404
commit ac603d54a3
9 changed files with 306 additions and 5 deletions

View File

@ -85,6 +85,7 @@
<script src="/mixed/js/display-context.js"></script>
<script src="/mixed/js/display.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/text-scanner.js"></script>
<script src="/mixed/js/template-handler.js"></script>

View File

@ -51,6 +51,7 @@
<script src="/mixed/js/display-context.js"></script>
<script src="/mixed/js/display.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/template-handler.js"></script>

View File

@ -94,3 +94,10 @@ h2 { border-bottom-color: #2f2f2f; }
#term-pitch-accent-graph-dot-downstep>circle:last-of-type {
fill: #ffffff;
}
.term-glossary-image-container {
background-color: #2f2f2f;
}
.term-glossary-image-container-overlay {
color: #888888;
}

View File

@ -94,3 +94,10 @@ h2 { border-bottom-color: #eeeeee; }
#term-pitch-accent-graph-dot-downstep>circle:last-of-type {
fill: #000000;
}
.term-glossary-image-container {
background-color: #eeeeee;
}
.term-glossary-image-container-overlay {
color: #777777;
}

View File

@ -611,6 +611,102 @@ button.action-button {
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

View File

@ -35,6 +35,7 @@
</li></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-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-pitch-accent-static-template"><svg xmlns="http://www.w3.org/2000/svg" style="display: none;">

View File

@ -22,7 +22,8 @@
*/
class DisplayGenerator {
constructor() {
constructor({mediaLoader}) {
this._mediaLoader = mediaLoader;
this._templateHandler = null;
this._termPitchAccentStaticTemplateIsSetup = false;
}
@ -176,16 +177,30 @@ class DisplayGenerator {
const onlyListContainer = node.querySelector('.term-definition-disambiguation-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(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;
}
_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 container = node.querySelector('.term-glossary');
if (container !== null) {
@ -194,6 +209,68 @@ class DisplayGenerator {
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) {
const node = this._templateHandler.instantiate('term-definition-disambiguation');
node.dataset.term = disambiguation;

View File

@ -20,6 +20,7 @@
* DOM
* DisplayContext
* DisplayGenerator
* MediaLoader
* WindowScroll
* apiAudioGetUri
* apiBroadcastTab
@ -62,7 +63,8 @@ class Display {
this.clickScanPrevent = false;
this.setContentToken = null;
this.displayGenerator = new DisplayGenerator();
this.mediaLoader = new MediaLoader();
this.displayGenerator = new DisplayGenerator({mediaLoader: this.mediaLoader});
this.windowScroll = new WindowScroll();
this._onKeyDownHandlers = new Map([
@ -479,6 +481,8 @@ class Display {
const token = {}; // Unique identifier token
this.setContentToken = token;
try {
this.mediaLoader.unloadAll();
switch (type) {
case 'terms':
await this.setContentTerms(details.definitions, details.context, token);

View 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;
}
}