aboutsummaryrefslogtreecommitdiff
path: root/ext/mixed
diff options
context:
space:
mode:
Diffstat (limited to 'ext/mixed')
-rw-r--r--ext/mixed/css/display-dark.css7
-rw-r--r--ext/mixed/css/display-default.css7
-rw-r--r--ext/mixed/css/display.css96
-rw-r--r--ext/mixed/display-templates.html1
-rw-r--r--ext/mixed/js/display-generator.js85
-rw-r--r--ext/mixed/js/display.js6
-rw-r--r--ext/mixed/js/media-loader.js107
7 files changed, 304 insertions, 5 deletions
diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css
index e4549bbf..acfa2782 100644
--- a/ext/mixed/css/display-dark.css
+++ b/ext/mixed/css/display-dark.css
@@ -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;
+}
diff --git a/ext/mixed/css/display-default.css b/ext/mixed/css/display-default.css
index 7bcb1014..70f81eb6 100644
--- a/ext/mixed/css/display-default.css
+++ b/ext/mixed/css/display-default.css
@@ -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;
+}
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css
index d1a54064..ca1fa371 100644
--- a/ext/mixed/css/display.css
+++ b/ext/mixed/css/display.css
@@ -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
diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html
index 3baa8293..5ecf2240 100644
--- a/ext/mixed/display-templates.html
+++ b/ext/mixed/display-templates.html
@@ -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;">
diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js
index 0f991362..a2b2b139 100644
--- a/ext/mixed/js/display-generator.js
+++ b/ext/mixed/js/display-generator.js
@@ -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;
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index c2284ffe..9587ec3b 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -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);
diff --git a/ext/mixed/js/media-loader.js b/ext/mixed/js/media-loader.js
new file mode 100644
index 00000000..c89c4f12
--- /dev/null
+++ b/ext/mixed/js/media-loader.js
@@ -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;
+ }
+}