diff --git a/package-lock.json b/package-lock.json index 4e9bca4b62..fde7e301ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3082,7 +3082,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", - "dev": true + "dev": true, + "requires": {} }, "@webpack-cli/info": { "version": "1.4.0", @@ -3097,7 +3098,8 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz", "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", - "dev": true + "dev": true, + "requires": {} }, "@xmldom/xmldom": { "version": "0.7.5", @@ -3142,13 +3144,15 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true + "dev": true, + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "aggregate-error": { "version": "3.1.0", @@ -3205,7 +3209,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "alphanum-sort": { "version": "1.0.2", @@ -4648,7 +4653,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", - "dev": true + "dev": true, + "requires": {} }, "csso": { "version": "4.2.0", @@ -5801,7 +5807,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-rule-composer": { "version": "0.3.0", @@ -6025,7 +6032,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", "integrity": "sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==", - "dev": true + "dev": true, + "requires": {} }, "express": { "version": "4.17.2", @@ -7030,7 +7038,8 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true + "dev": true, + "requires": {} }, "idb": { "version": "6.1.2", @@ -7763,7 +7772,7 @@ }, "libass-wasm": { "version": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#f4625ac313b318bd5d2e0ae18679ff516370bae6", - "from": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4" + "from": "libass-wasm@git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4" }, "lie": { "version": "3.1.1", @@ -9058,25 +9067,29 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", - "dev": true + "dev": true, + "requires": {} }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", - "dev": true + "dev": true, + "requires": {} }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", - "dev": true + "dev": true, + "requires": {} }, "postcss-double-position-gradients": { "version": "3.0.4", @@ -9692,7 +9705,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -9748,7 +9762,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", - "dev": true + "dev": true, + "requires": {} }, "postcss-normalize-display-values": { "version": "5.0.1", @@ -10150,7 +10165,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.2.tgz", "integrity": "sha512-xfdkU128CkKKKVAwkyt0M8OdnelJ3MRcIRAPPQkRpoPeuzWY3RIeg7piRCpZ79MK7Q16diLXMMAD9dN5mauPlQ==", - "dev": true + "dev": true, + "requires": {} }, "postcss-selector-not": { "version": "5.0.0", @@ -10225,7 +10241,8 @@ "version": "0.36.2", "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", - "dev": true + "dev": true, + "requires": {} }, "postcss-unique-selectors": { "version": "5.0.2", @@ -11495,6 +11512,14 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -11542,14 +11567,6 @@ "define-properties": "^1.1.3" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "stringify-entities": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", @@ -11633,7 +11650,8 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "dev": true + "dev": true, + "requires": {} }, "style-search": { "version": "0.1.0", @@ -11845,7 +11863,8 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true + "dev": true, + "requires": {} }, "postcss-selector-parser": { "version": "6.0.8", @@ -13663,7 +13682,8 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-7.0.1.tgz", "integrity": "sha512-iLBFYz6VRYyLJEJsBJ8M3TCqNcckVzz4wFounSc5Oez35ogE/X+aoC5fFu103Ot7NyvjU3/xqIXn93Gp3kJk4g==", - "dev": true + "dev": true, + "requires": {} } } }, @@ -15340,15 +15360,6 @@ } } }, - "worker-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-5.0.1.tgz", - "integrity": "sha512-Pn7+19jIiANcGuTSGdy+vrzyF+SGH03A5wV8iu4jRTMAOfAC9bNeiHo4+l5tPS7F0uvICMBv+h8UCvL7lunxcA==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 8e91326891..173aefe54c 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,7 @@ "webpack-cli": "^4.9.1", "webpack-dev-server": "^4.7.2", "webpack-merge": "^5.8.0", - "workbox-webpack-plugin": "^6.2.4", - "worker-plugin": "^5.0.1" + "workbox-webpack-plugin": "^6.2.4" }, "dependencies": { "@fontsource/noto-sans": "^4.5.1", diff --git a/src/components/images/blurhash.worker.ts b/src/components/images/blurhash.worker.ts new file mode 100644 index 0000000000..a37d8aaf00 --- /dev/null +++ b/src/components/images/blurhash.worker.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-restricted-globals */ +import { decode } from 'blurhash'; + +self.onmessage = ({ data: { hash, width, height } }): void => { + try { + self.postMessage({ + pixels: decode(hash, width, height), + hsh: hash, + width: width, + height: height + }); + } catch { + throw new TypeError(`Blurhash ${hash} is not valid`); + } +}; +/* eslint-enable no-restricted-globals */ diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index ef89cd1d03..81b47da257 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -1,7 +1,21 @@ import * as lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver'; import * as userSettings from '../../scripts/settings/userSettings'; -import { decode, isBlurhashValid } from 'blurhash'; import './style.scss'; +// eslint-disable-next-line compat/compat +const worker = new Worker(new URL('./blurhash.worker.ts', import.meta.url)); +const targetDic = {}; +worker.addEventListener( + 'message', + ({ data: { pixels, hsh, width, height } }) => { + const elems = targetDic[hsh]; + if (elems && elems.length) { + for (const elem of elems) { + drawBlurhash(elem, pixels, width, height); + } + delete targetDic[hsh]; + } + } +); /* eslint-disable indent */ export function lazyImage(elem, source = elem.getAttribute('data-src')) { @@ -12,42 +26,45 @@ import './style.scss'; fillImageElement(elem, source); } - function itemBlurhashing(target, blurhashstr) { - if (isBlurhashValid(blurhashstr)) { - // Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 seems to be the sweet spot for us, + function drawBlurhash(target, pixels, width, height) { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + const imgData = ctx.createImageData(width, height); + + imgData.data.set(pixels); + ctx.putImageData(imgData, 0, 0); + + requestAnimationFrame(() => { + // This class is just an utility class, so users can customize the canvas using their own CSS. + canvas.classList.add('blurhash-canvas'); + + target.parentNode.insertBefore(canvas, target); + target.classList.add('blurhashed'); + target.removeAttribute('data-blurhash'); + }); + } + + function itemBlurhashing(target, hash) { + try { + // Although the default values recommended by Blurhash developers is 32x32, a size of 20x20 seems to be the sweet spot for us, // improving the performance and reducing the memory usage, while retaining almost full blur quality. // Lower values had more visible pixelation - const width = 18; - const height = 18; - let pixels; - try { - pixels = decode(blurhashstr, width, height); - } catch (err) { - console.error('Blurhash decode error: ', err); - target.classList.add('non-blurhashable'); - return; - } - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - const imgData = ctx.createImageData(width, height); + const width = 20; + const height = 20; + targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target); + targetDic[hash].push(target); - imgData.data.set(pixels); - ctx.putImageData(imgData, 0, 0); - - requestAnimationFrame(() => { - canvas.classList.add('blurhash-canvas'); - if (userSettings.enableFastFadein()) { - canvas.classList.add('lazy-blurhash-fadein-fast'); - } else { - canvas.classList.add('lazy-blurhash-fadein'); - } - - target.parentNode.insertBefore(canvas, target); - target.classList.add('blurhashed'); - target.removeAttribute('data-blurhash'); + worker.postMessage({ + hash, + width, + height }); + } catch (err) { + console.error(err); + target.classList.add('non-blurhashable'); + return; } } @@ -65,14 +82,25 @@ import './style.scss'; } if (entry.intersectionRatio > 0) { - if (source) fillImageElement(target, source); + if (source) { + fillImageElement(target, source); + } } else if (!source) { - requestAnimationFrame(() => { - emptyImageElement(target); - }); + emptyImageElement(target); } } + function onAnimationEnd(event) { + const elem = event.target; + requestAnimationFrame(() => { + const canvas = elem.previousSibling; + if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') { + canvas.classList.add('lazy-hidden'); + } + }); + elem.removeEventListener('animationend', onAnimationEnd); + } + function fillImageElement(elem, url) { if (url === undefined) { throw new TypeError('url cannot be undefined'); @@ -82,6 +110,7 @@ import './style.scss'; preloaderImg.src = url; elem.classList.add('lazy-hidden'); + elem.addEventListener('animationend', onAnimationEnd); preloaderImg.addEventListener('load', () => { requestAnimationFrame(() => { @@ -92,25 +121,22 @@ import './style.scss'; } elem.removeAttribute('data-src'); - elem.classList.remove('lazy-hidden'); if (userSettings.enableFastFadein()) { elem.classList.add('lazy-image-fadein-fast'); } else { elem.classList.add('lazy-image-fadein'); } - - const canvas = elem.previousSibling; - if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') { - canvas.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein'); - canvas.classList.add('lazy-hidden'); - } + elem.classList.remove('lazy-hidden'); }); }); } function emptyImageElement(elem) { - // block repeated call - requestAnimationFrame twice for one image - if (elem.getAttribute('data-src')) return; + elem.removeEventListener('animationend', onAnimationEnd); + const canvas = elem.previousSibling; + if (canvas && canvas.tagName === 'CANVAS') { + canvas.classList.remove('lazy-hidden'); + } let url; @@ -125,16 +151,6 @@ import './style.scss'; elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein'); elem.classList.add('lazy-hidden'); - - const canvas = elem.previousSibling; - if (canvas && canvas.tagName === 'CANVAS') { - canvas.classList.remove('lazy-hidden'); - if (userSettings.enableFastFadein()) { - canvas.classList.add('lazy-image-fadein-fast'); - } else { - canvas.classList.add('lazy-image-fadein'); - } - } } export function lazyChildren(elem) { diff --git a/src/components/images/style.scss b/src/components/images/style.scss index 7e8b01aff2..9fae14fd0f 100644 --- a/src/components/images/style.scss +++ b/src/components/images/style.scss @@ -1,17 +1,3 @@ -.lazy-image-fadein { - opacity: 1; - transition: opacity 0.5s; -} - -.lazy-image-fadein-fast { - opacity: 1; - transition: opacity 0.1s; -} - -.lazy-hidden { - opacity: 0; -} - @keyframes fadein { from { opacity: 0; @@ -22,12 +8,18 @@ } } -.lazy-blurhash-fadein-fast { +.lazy-image-fadein { + opacity: 1; + animation: fadein 0.5s; +} + +.lazy-image-fadein-fast { + opacity: 1; animation: fadein 0.1s; } -.lazy-blurhash-fadein { - animation: fadein 0.4s; +.lazy-hidden { + opacity: 0; } .blurhash-canvas { diff --git a/src/components/lazyLoader/lazyLoaderIntersectionObserver.js b/src/components/lazyLoader/lazyLoaderIntersectionObserver.js index 7751fe8bc2..3b78083cc2 100644 --- a/src/components/lazyLoader/lazyLoaderIntersectionObserver.js +++ b/src/components/lazyLoader/lazyLoaderIntersectionObserver.js @@ -13,7 +13,10 @@ callback(entry); }); }, - {rootMargin: '25%'}); + { + rootMargin: '50%', + threshold: 0 + }); this.observer = observer; }