1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge pull request #1286 from ferferga/blurhash

Implement blurhash (follow-up of #987 but from scratch)
This commit is contained in:
Anthony Lavado 2020-06-04 14:57:48 -04:00 committed by GitHub
commit 205d80e0ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 258 additions and 192 deletions

View file

@ -54,6 +54,7 @@
}, },
"dependencies": { "dependencies": {
"alameda": "^1.4.0", "alameda": "^1.4.0",
"blurhash": "^1.1.3",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"date-fns": "^2.14.0", "date-fns": "^2.14.0",

View file

@ -16,6 +16,12 @@ _define('fetch', function() {
return fetch; return fetch;
}); });
// Blurhash
var blurhash = require('blurhash');
_define('blurhash', function() {
return blurhash;
});
// query-string // query-string
var query = require('query-string'); var query = require('query-string');
_define('queryString', function() { _define('queryString', function() {

View file

@ -503,94 +503,49 @@ import 'programStyles';
const primaryImageAspectRatio = item.PrimaryImageAspectRatio; const primaryImageAspectRatio = item.PrimaryImageAspectRatio;
let forceName = false; let forceName = false;
let imgUrl = null; let imgUrl = null;
let imgTag = null;
let coverImage = false; let coverImage = false;
let uiAspect = null; let uiAspect = null;
let imgType = null;
let itemId = null;
if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) { if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) {
imgType = 'Thumb';
imgUrl = apiClient.getScaledImageUrl(item.Id, { imgTag = item.ImageTags.Thumb;
type: 'Thumb',
maxWidth: width,
tag: item.ImageTags.Thumb
});
} else if ((options.preferBanner || shape === 'banner') && item.ImageTags && item.ImageTags.Banner) { } else if ((options.preferBanner || shape === 'banner') && item.ImageTags && item.ImageTags.Banner) {
imgType = 'Banner';
imgUrl = apiClient.getScaledImageUrl(item.Id, { imgTag = item.ImageTags.Banner;
type: 'Banner',
maxWidth: width,
tag: item.ImageTags.Banner
});
} else if (options.preferDisc && item.ImageTags && item.ImageTags.Disc) { } else if (options.preferDisc && item.ImageTags && item.ImageTags.Disc) {
imgType = 'Disc';
imgUrl = apiClient.getScaledImageUrl(item.Id, { imgTag = item.ImageTags.Disc;
type: 'Disc',
maxWidth: width,
tag: item.ImageTags.Disc
});
} else if (options.preferLogo && item.ImageTags && item.ImageTags.Logo) { } else if (options.preferLogo && item.ImageTags && item.ImageTags.Logo) {
imgType = 'Logo';
imgUrl = apiClient.getScaledImageUrl(item.Id, { imgTag = item.ImageTags.Logo;
type: 'Logo',
maxWidth: width,
tag: item.ImageTags.Logo
});
} else if (options.preferLogo && item.ParentLogoImageTag && item.ParentLogoItemId) { } else if (options.preferLogo && item.ParentLogoImageTag && item.ParentLogoItemId) {
imgType = 'Logo';
imgUrl = apiClient.getScaledImageUrl(item.ParentLogoItemId, { imgTag = item.ParentLogoImageTag;
type: 'Logo', itemId = item.ParentLogoItemId;
maxWidth: width,
tag: item.ParentLogoImageTag
});
} else if (options.preferThumb && item.SeriesThumbImageTag && options.inheritThumb !== false) { } else if (options.preferThumb && item.SeriesThumbImageTag && options.inheritThumb !== false) {
imgType = 'Thumb';
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, { imgTag = item.SeriesThumbImageTag;
type: 'Thumb', itemId = item.SeriesId;
maxWidth: width,
tag: item.SeriesThumbImageTag
});
} else if (options.preferThumb && item.ParentThumbItemId && options.inheritThumb !== false && item.MediaType !== 'Photo') { } else if (options.preferThumb && item.ParentThumbItemId && options.inheritThumb !== false && item.MediaType !== 'Photo') {
imgType = 'Thumb';
imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, { imgTag = item.ParentThumbImageTag;
type: 'Thumb', itemId = item.ParentThumbItemId;
maxWidth: width,
tag: item.ParentThumbImageTag
});
} else if (options.preferThumb && item.BackdropImageTags && item.BackdropImageTags.length) { } else if (options.preferThumb && item.BackdropImageTags && item.BackdropImageTags.length) {
imgType = 'Backdrop';
imgUrl = apiClient.getScaledImageUrl(item.Id, { imgTag = item.BackdropImageTags[0];
type: 'Backdrop',
maxWidth: width,
tag: item.BackdropImageTags[0]
});
forceName = true; forceName = true;
} else if (options.preferThumb && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false && item.Type === 'Episode') { } else if (options.preferThumb && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false && item.Type === 'Episode') {
imgType = 'Backdrop';
imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { imgTag = item.ParentBackdropImageTags[0];
type: 'Backdrop', itemId = item.ParentBackdropItemId;
maxWidth: width,
tag: item.ParentBackdropImageTags[0]
});
} else if (item.ImageTags && item.ImageTags.Primary) { } else if (item.ImageTags && item.ImageTags.Primary) {
imgType = 'Primary';
imgTag = item.ImageTags.Primary;
height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null; height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null;
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: 'Primary',
maxHeight: height,
maxWidth: width,
tag: item.ImageTags.Primary
});
if (options.preferThumb && options.showTitle !== false) { if (options.preferThumb && options.showTitle !== false) {
forceName = true; forceName = true;
} }
@ -603,16 +558,11 @@ import 'programStyles';
} }
} else if (item.PrimaryImageTag) { } else if (item.PrimaryImageTag) {
imgType = 'Primary';
imgTag = item.PrimaryImageTag;
itemId = item.PrimaryImageItemId;
height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null; height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null;
imgUrl = apiClient.getScaledImageUrl(item.PrimaryImageItemId || item.Id || item.ItemId, {
type: 'Primary',
maxHeight: height,
maxWidth: width,
tag: item.PrimaryImageTag
});
if (options.preferThumb && options.showTitle !== false) { if (options.preferThumb && options.showTitle !== false) {
forceName = true; forceName = true;
} }
@ -624,30 +574,19 @@ import 'programStyles';
} }
} }
} else if (item.ParentPrimaryImageTag) { } else if (item.ParentPrimaryImageTag) {
imgType = 'Primary';
imgUrl = apiClient.getScaledImageUrl(item.ParentPrimaryImageItemId, { imgTag = item.ParentPrimaryImageTag;
type: 'Primary', itemId = item.ParentPrimaryImageItemId;
maxWidth: width,
tag: item.ParentPrimaryImageTag
});
} else if (item.SeriesPrimaryImageTag) { } else if (item.SeriesPrimaryImageTag) {
imgType = 'Primary';
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, { imgTag = item.SeriesPrimaryImageTag;
type: 'Primary', itemId = item.SeriesId;
maxWidth: width,
tag: item.SeriesPrimaryImageTag
});
} else if (item.AlbumId && item.AlbumPrimaryImageTag) { } else if (item.AlbumId && item.AlbumPrimaryImageTag) {
imgType = 'Primary';
imgTag = item.AlbumPrimaryImageTag;
itemId = item.AlbumId;
height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null; height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null;
imgUrl = apiClient.getScaledImageUrl(item.AlbumId, {
type: 'Primary',
maxHeight: height,
maxWidth: width,
tag: item.AlbumPrimaryImageTag
});
if (primaryImageAspectRatio) { if (primaryImageAspectRatio) {
uiAspect = getDesiredAspect(shape); uiAspect = getDesiredAspect(shape);
if (uiAspect) { if (uiAspect) {
@ -655,57 +594,46 @@ import 'programStyles';
} }
} }
} else if (item.Type === 'Season' && item.ImageTags && item.ImageTags.Thumb) { } else if (item.Type === 'Season' && item.ImageTags && item.ImageTags.Thumb) {
imgType = 'Thumb';
imgUrl = apiClient.getScaledImageUrl(item.Id, { imgTag = item.ImageTags.Thumb;
type: 'Thumb',
maxWidth: width,
tag: item.ImageTags.Thumb
});
} else if (item.BackdropImageTags && item.BackdropImageTags.length) { } else if (item.BackdropImageTags && item.BackdropImageTags.length) {
imgType = 'Backdrop';
imgUrl = apiClient.getScaledImageUrl(item.Id, { imgTag = item.BackdropImageTags[0];
type: 'Backdrop',
maxWidth: width,
tag: item.BackdropImageTags[0]
});
} else if (item.ImageTags && item.ImageTags.Thumb) { } else if (item.ImageTags && item.ImageTags.Thumb) {
imgType = 'Thumb';
imgUrl = apiClient.getScaledImageUrl(item.Id, { imgTag = item.ImageTags.Thumb;
type: 'Thumb',
maxWidth: width,
tag: item.ImageTags.Thumb
});
} else if (item.SeriesThumbImageTag && options.inheritThumb !== false) { } else if (item.SeriesThumbImageTag && options.inheritThumb !== false) {
imgType = 'Thumb';
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, { imgTag = item.SeriesThumbImageTag;
type: 'Thumb', itemId = item.SeriesId;
maxWidth: width,
tag: item.SeriesThumbImageTag
});
} else if (item.ParentThumbItemId && options.inheritThumb !== false) { } else if (item.ParentThumbItemId && options.inheritThumb !== false) {
imgType = 'Thumb';
imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, { imgTag = item.ParentThumbImageTag;
type: 'Thumb', itemId = item.ParentThumbItemId;
maxWidth: width,
tag: item.ParentThumbImageTag
});
} else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false) { } else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false) {
imgType = 'Backdrop';
imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { imgTag = item.ParentBackdropImageTags[0];
type: 'Backdrop', itemId = item.ParentBackdropItemId;
maxWidth: width,
tag: item.ParentBackdropImageTags[0]
});
} }
if (!itemId) {
itemId = item.Id;
}
if (imgTag && imgType) {
imgUrl = apiClient.getScaledImageUrl(itemId, {
type: imgType,
maxHeight: height,
maxWidth: width,
tag: imgTag
});
}
let blurHashes = options.imageBlurhashes || item.ImageBlurHashes || {};
return { return {
imgUrl: imgUrl, imgUrl: imgUrl,
blurhash: (blurHashes[imgType] || {})[imgTag],
forceName: forceName, forceName: forceName,
coverImage: coverImage coverImage: coverImage
}; };
@ -1321,6 +1249,7 @@ import 'programStyles';
const imgInfo = getCardImageUrl(item, apiClient, options, shape); const imgInfo = getCardImageUrl(item, apiClient, options, shape);
const imgUrl = imgInfo.imgUrl; const imgUrl = imgInfo.imgUrl;
const blurhash = imgInfo.blurhash;
const forceName = imgInfo.forceName; const forceName = imgInfo.forceName;
@ -1445,15 +1374,20 @@ import 'programStyles';
cardContentClass += ' cardContent-shadow'; cardContentClass += ' cardContent-shadow';
} }
let blurhashAttrib = '';
if (blurhash && blurhash.length > 0) {
blurhashAttrib = 'data-blurhash="' + blurhash + '"';
}
if (layoutManager.tv) { if (layoutManager.tv) {
// Don't use the IMG tag with safari because it puts a white border around it // Don't use the IMG tag with safari because it puts a white border around it
cardImageContainerOpen = imgUrl ? ('<div class="' + cardImageContainerClass + ' ' + cardContentClass + ' lazy" data-src="' + imgUrl + '">') : ('<div class="' + cardImageContainerClass + ' ' + cardContentClass + '">'); cardImageContainerOpen = imgUrl ? ('<div class="' + cardImageContainerClass + ' ' + cardContentClass + ' lazy" data-src="' + imgUrl + '" ' + blurhashAttrib + '>') : ('<div class="' + cardImageContainerClass + ' ' + cardContentClass + '">');
cardImageContainerClose = '</div>'; cardImageContainerClose = '</div>';
} else { } else {
// Don't use the IMG tag with safari because it puts a white border around it // Don't use the IMG tag with safari because it puts a white border around it
cardImageContainerOpen = imgUrl ? ('<button data-action="' + action + '" class="cardContent-button ' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction lazy" data-src="' + imgUrl + '">') : ('<button data-action="' + action + '" class="cardContent-button ' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction">'); cardImageContainerOpen = imgUrl ? ('<button data-action="' + action + '" class="cardContent-button ' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction lazy" data-src="' + imgUrl + '" ' + blurhashAttrib + '>') : ('<button data-action="' + action + '" class="cardContent-button ' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction">');
cardImageContainerClose = '</button>'; cardImageContainerClose = '</button>';
} }

View file

@ -181,6 +181,7 @@ define(['require', 'browser', 'layoutManager', 'appSettings', 'pluginManager', '
context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs(); context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs();
context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos(); context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos();
context.querySelector('#chkFadein').checked = userSettings.enableFastFadein(); context.querySelector('#chkFadein').checked = userSettings.enableFastFadein();
context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash();
context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops(); context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops();
context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner(); context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner();
@ -223,6 +224,7 @@ define(['require', 'browser', 'layoutManager', 'appSettings', 'pluginManager', '
userSettingsInstance.skin(context.querySelector('.selectSkin').value); userSettingsInstance.skin(context.querySelector('.selectSkin').value);
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked); userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked); userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked);
userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked); userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked);

View file

@ -143,12 +143,12 @@
<select is="emby-select" class="selectSoundEffects" label="${LabelSoundEffects}"></select> <select is="emby-select" class="selectSoundEffects" label="${LabelSoundEffects}"></select>
</div> </div>
<div class="inputContainer inputContainer-withDescription fldFadein"> <div class="inputContainer inputContainer-withDescription">
<input is="emby-input" type="number" id="txtLibraryPageSize" pattern="[0-9]*" required="required" min="0" max="1000" step="1" label="${LabelLibraryPageSize}" /> <input is="emby-input" type="number" id="txtLibraryPageSize" pattern="[0-9]*" required="required" min="0" max="1000" step="1" label="${LabelLibraryPageSize}" />
<div class="fieldDescription">${LabelLibraryPageSizeHelp}</div> <div class="fieldDescription">${LabelLibraryPageSizeHelp}</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription fldFadein"> <div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input type="checkbox" is="emby-checkbox" id="chkFadein" /> <input type="checkbox" is="emby-checkbox" id="chkFadein" />
<span>${EnableFastImageFadeIn}</span> <span>${EnableFastImageFadeIn}</span>
@ -156,7 +156,15 @@
<div class="fieldDescription checkboxFieldDescription">${EnableFastImageFadeInHelp}</div> <div class="fieldDescription checkboxFieldDescription">${EnableFastImageFadeInHelp}</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription fldDetailsBanner"> <div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkBlurhash" />
<span>${EnableBlurhash}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${EnableBlurhashHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label> <label>
<input type="checkbox" is="emby-checkbox" id="chkDetailsBanner" /> <input type="checkbox" is="emby-checkbox" id="chkDetailsBanner" />
<span>${EnableDetailsBanner}</span> <span>${EnableDetailsBanner}</span>

View file

@ -1,5 +1,6 @@
import * as lazyLoader from 'lazyLoader'; import * as lazyLoader from 'lazyLoader';
import * as userSettings from 'userSettings'; import * as userSettings from 'userSettings';
import * as blurhash from 'blurhash';
import 'css!./style'; import 'css!./style';
/* eslint-disable indent */ /* eslint-disable indent */
@ -11,47 +12,111 @@ import 'css!./style';
fillImageElement(elem, source); fillImageElement(elem, source);
} }
async function itemBlurhashing(target, blurhashstr) {
if (blurhash.isBlurhashValid(blurhashstr)) {
// Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 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
let width = 18;
let height = 18;
let pixels;
try {
pixels = blurhash.decode(blurhashstr, width, height);
} catch (err) {
console.error('Blurhash decode error: ', err);
target.classList.add('non-blurhashable');
return;
}
let canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext('2d');
let imgData = ctx.createImageData(width, height);
imgData.data.set(pixels);
ctx.putImageData(imgData, 0, 0);
let child = target.appendChild(canvas);
child.classList.add('blurhash-canvas');
child.style.opacity = 1;
if (userSettings.enableFastFadein()) {
child.classList.add('lazy-blurhash-fadein-fast');
} else {
child.classList.add('lazy-blurhash-fadein');
}
target.classList.add('blurhashed');
target.removeAttribute('data-blurhash');
}
}
function switchCanvas(elem) {
let child = elem.getElementsByClassName('blurhash-canvas')[0];
if (child) {
child.style.opacity = elem.getAttribute('data-src') ? 1 : 0;
}
}
export function fillImage(entry) { export function fillImage(entry) {
if (!entry) { if (!entry) {
throw new Error('entry cannot be null'); throw new Error('entry cannot be null');
} }
let target = entry.target;
var source = undefined; var source = undefined;
if (entry.target) {
source = entry.target.getAttribute('data-src'); if (target) {
source = target.getAttribute('data-src');
var blurhashstr = target.getAttribute('data-blurhash');
} else { } else {
source = entry; source = entry;
} }
if (userSettings.enableBlurhash()) {
if (!target.classList.contains('blurhashed', 'non-blurhashable') && blurhashstr) {
itemBlurhashing(target, blurhashstr);
} else if (!blurhashstr && !target.classList.contains('blurhashed')) {
target.classList.add('non-blurhashable');
}
}
if (entry.intersectionRatio > 0) { if (entry.intersectionRatio > 0) {
if (source) fillImageElement(entry.target, source); if (source) fillImageElement(target, source);
} else if (!source) { } else if (!source) {
emptyImageElement(entry.target); emptyImageElement(target);
} }
} }
function fillImageElement(elem, url) { function fillImageElement(elem, url) {
if (url === undefined) { if (url === undefined) {
throw new Error('url cannot be undefined'); throw new TypeError('url cannot be undefined');
} }
let preloaderImg = new Image(); let preloaderImg = new Image();
preloaderImg.src = url; preloaderImg.src = url;
// This is necessary here, so changing blurhash settings without reloading the page works
if (!userSettings.enableBlurhash() || elem.classList.contains('non-blurhashable')) {
elem.classList.add('lazy-hidden');
}
preloaderImg.addEventListener('load', () => { preloaderImg.addEventListener('load', () => {
if (elem.tagName !== 'IMG') { if (elem.tagName !== 'IMG') {
elem.style.backgroundImage = "url('" + url + "')"; elem.style.backgroundImage = "url('" + url + "')";
} else { } else {
elem.setAttribute('src', url); elem.setAttribute('src', url);
} }
if (userSettings.enableFastFadein()) {
elem.classList.add('lazy-image-fadein-fast');
} else {
elem.classList.add('lazy-image-fadein');
}
elem.removeAttribute('data-src'); elem.removeAttribute('data-src');
if (elem.classList.contains('non-blurhashable') || !userSettings.enableBlurhash()) {
elem.classList.remove('lazy-hidden');
if (userSettings.enableFastFadein()) {
elem.classList.add('lazy-image-fadein-fast');
} else {
elem.classList.add('lazy-image-fadein');
}
} else {
switchCanvas(elem);
}
}); });
} }
@ -65,11 +130,14 @@ import 'css!./style';
url = elem.getAttribute('src'); url = elem.getAttribute('src');
elem.setAttribute('src', ''); elem.setAttribute('src', '');
} }
elem.setAttribute('data-src', url); elem.setAttribute('data-src', url);
elem.classList.remove('lazy-image-fadein-fast'); if (elem.classList.contains('non-blurhashable') || !userSettings.enableBlurhash()) {
elem.classList.remove('lazy-image-fadein'); elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
elem.classList.add('lazy-hidden');
} else {
switchCanvas(elem);
}
} }
export function lazyChildren(elem) { export function lazyChildren(elem) {

View file

@ -1,13 +1,32 @@
.cardImageContainer.lazy { .lazy-image-fadein {
opacity: 0;
}
.cardImageContainer.lazy.lazy-image-fadein {
opacity: 1; opacity: 1;
transition: opacity 0.7s; transition: opacity 0.7s;
} }
.cardImageContainer.lazy.lazy-image-fadein-fast { .lazy-image-fadein-fast {
opacity: 1; opacity: 1;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.lazy-hidden {
opacity: 0;
}
.lazy-blurhash-fadein-fast {
transition: opacity 0.2s;
}
.lazy-blurhash-fadein {
transition: opacity 0.7s;
}
.blurhash-canvas {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
}

View file

@ -70,6 +70,7 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan
function getImageUrl(item, width) { function getImageUrl(item, width) {
var apiClient = connectionManager.getApiClient(item.ServerId); var apiClient = connectionManager.getApiClient(item.ServerId);
let itemId;
var options = { var options = {
maxWidth: width * 2, maxWidth: width * 2,
@ -77,45 +78,45 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan
}; };
if (item.ImageTags && item.ImageTags.Primary) { if (item.ImageTags && item.ImageTags.Primary) {
options.tag = item.ImageTags.Primary; options.tag = item.ImageTags.Primary;
return apiClient.getScaledImageUrl(item.Id, options); itemId = item.Id;
} }
if (item.AlbumId && item.AlbumPrimaryImageTag) { if (item.AlbumId && item.AlbumPrimaryImageTag) {
options.tag = item.AlbumPrimaryImageTag; options.tag = item.AlbumPrimaryImageTag;
return apiClient.getScaledImageUrl(item.AlbumId, options); itemId = item.AlbumId;
} else if (item.SeriesId && item.SeriesPrimaryImageTag) { } else if (item.SeriesId && item.SeriesPrimaryImageTag) {
options.tag = item.SeriesPrimaryImageTag; options.tag = item.SeriesPrimaryImageTag;
return apiClient.getScaledImageUrl(item.SeriesId, options); itemId = item.SeriesId;
} else if (item.ParentPrimaryImageTag) { } else if (item.ParentPrimaryImageTag) {
options.tag = item.ParentPrimaryImageTag; options.tag = item.ParentPrimaryImageTag;
return apiClient.getScaledImageUrl(item.ParentPrimaryImageItemId, options); itemId = item.ParentPrimaryImageItemId;
} }
let blurHashes = item.ImageBlurHashes || {};
let blurhashstr = (blurHashes[options.type] || {})[options.tag];
return null; if (itemId) {
return { url: apiClient.getScaledImageUrl(itemId, options), blurhash: blurhashstr };
}
} }
function getChannelImageUrl(item, width) { function getChannelImageUrl(item, width) {
var apiClient = connectionManager.getApiClient(item.ServerId); var apiClient = connectionManager.getApiClient(item.ServerId);
var options = { var options = {
maxWidth: width * 2, maxWidth: width * 2,
type: 'Primary' type: 'Primary'
}; };
if (item.ChannelId && item.ChannelPrimaryImageTag) { if (item.ChannelId && item.ChannelPrimaryImageTag) {
options.tag = item.ChannelPrimaryImageTag; options.tag = item.ChannelPrimaryImageTag;
return apiClient.getScaledImageUrl(item.ChannelId, options);
} }
let blurHashes = item.ImageBlurHashes || {};
let blurhashstr = (blurHashes[options.type])[options.tag];
return null; if (item.ChannelId) {
return { url: apiClient.getScaledImageUrl(item.ChannelId, options), blurhash: blurhashstr };
}
} }
function getTextLinesHtml(textlines, isLargeStyle) { function getTextLinesHtml(textlines, isLargeStyle) {
@ -268,8 +269,10 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan
} }
if (options.image !== false) { if (options.image !== false) {
var imgUrl = options.imageSource === 'channel' ? getChannelImageUrl(item, downloadWidth) : getImageUrl(item, downloadWidth); let imgData = options.imageSource === 'channel' ? getChannelImageUrl(item, downloadWidth) : getImageUrl(item, downloadWidth);
var imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage'; let imgUrl = imgData.url;
let blurhash = imgData.blurhash;
let imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage';
if (isLargeStyle && layoutManager.tv) { if (isLargeStyle && layoutManager.tv) {
imageClass += ' listItemImage-large-tv'; imageClass += ' listItemImage-large-tv';
@ -283,8 +286,13 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan
var imageAction = playOnImageClick ? 'resume' : action; var imageAction = playOnImageClick ? 'resume' : action;
let blurhashAttrib = '';
if (blurhash && blurhash.length > 0) {
blurhashAttrib = 'data-blurhash="' + blurhash + '"';
}
if (imgUrl) { if (imgUrl) {
html += '<div data-action="' + imageAction + '" class="' + imageClass + ' lazy" data-src="' + imgUrl + '" item-icon>'; html += '<div data-action="' + imageAction + '" class="' + imageClass + ' lazy" data-src="' + imgUrl + '" ' + blurhashAttrib + ' item-icon>';
} else { } else {
html += '<div class="' + imageClass + '">'; html += '<div class="' + imageClass + '">';
} }

View file

@ -1891,7 +1891,8 @@ define(['loading', 'appRouter', 'layoutManager', 'connectionManager', 'userSetti
itemsContainer: castContent, itemsContainer: castContent,
coverImage: true, coverImage: true,
serverId: item.ServerId, serverId: item.ServerId,
shape: 'overflowPortrait' shape: 'overflowPortrait',
imageBlurhashes: item.ImageBlurHashes
}); });
}); });
} }

View file

@ -128,6 +128,15 @@ import events from 'events';
return val !== 'false'; return val !== 'false';
} }
export function enableBlurhash(val) {
if (val !== undefined) {
return this.set('blurhash', val.toString(), false);
}
val = this.get('blurhash', false);
return val !== 'false';
}
export function enableBackdrops(val) { export function enableBackdrops(val) {
if (val !== undefined) { if (val !== undefined) {
return this.set('enableBackdrops', val.toString(), false); return this.set('enableBackdrops', val.toString(), false);
@ -294,6 +303,7 @@ export default {
enableThemeSongs: enableThemeSongs, enableThemeSongs: enableThemeSongs,
enableThemeVideos: enableThemeVideos, enableThemeVideos: enableThemeVideos,
enableFastFadein: enableFastFadein, enableFastFadein: enableFastFadein,
enableBlurhash: enableBlurhash,
enableBackdrops: enableBackdrops, enableBackdrops: enableBackdrops,
language: language, language: language,
dateTimeLocale: dateTimeLocale, dateTimeLocale: dateTimeLocale,

View file

@ -826,8 +826,8 @@
"LabelSaveLocalMetadataHelp": "Saving artwork into media folders will put them in a place where they can be easily edited.", "LabelSaveLocalMetadataHelp": "Saving artwork into media folders will put them in a place where they can be easily edited.",
"LabelScheduledTaskLastRan": "Last ran {0}, taking {1}.", "LabelScheduledTaskLastRan": "Last ran {0}, taking {1}.",
"LabelScreensaver": "Screensaver:", "LabelScreensaver": "Screensaver:",
"EnableFastImageFadeIn": "Fast Image Fade Animations", "EnableFastImageFadeIn": "Faster animations",
"EnableFastImageFadeInHelp": "Show posters and other images with a quicker fade animation when they finish loading.", "EnableFastImageFadeInHelp": "Use faster animations and transitions",
"LabelSeasonNumber": "Season number:", "LabelSeasonNumber": "Season number:",
"LabelSelectFolderGroups": "Automatically group content from the following folders into views such as Movies, Music and TV:", "LabelSelectFolderGroups": "Automatically group content from the following folders into views such as Movies, Music and TV:",
"LabelSelectFolderGroupsHelp": "Folders that are unchecked will be displayed by themselves in their own view.", "LabelSelectFolderGroupsHelp": "Folders that are unchecked will be displayed by themselves in their own view.",
@ -1548,5 +1548,7 @@
"EveryHour": "Every hour", "EveryHour": "Every hour",
"EveryXHours": "Every {0} hours", "EveryXHours": "Every {0} hours",
"OnApplicationStartup": "On application startup", "OnApplicationStartup": "On application startup",
"UnsupportedPlayback": "Jellyfin cannot decrypt content protected by DRM but all content will be attempted regardless, including protected titles. Some files may appear completely black due to encryption or other unsupported features, such as interactive titles." "UnsupportedPlayback": "Jellyfin cannot decrypt content protected by DRM but all content will be attempted regardless, including protected titles. Some files may appear completely black due to encryption or other unsupported features, such as interactive titles.",
"EnableBlurhash": "Enable blurred placeholders for images",
"EnableBlurhashHelp": "Images that are still being loaded will be displayed with a blurred placeholder"
} }

View file

@ -1458,8 +1458,8 @@
"ButtonSplit": "Dividir", "ButtonSplit": "Dividir",
"HeaderNavigation": "Navegación", "HeaderNavigation": "Navegación",
"MessageConfirmAppExit": "¿Quieres salir?", "MessageConfirmAppExit": "¿Quieres salir?",
"EnableFastImageFadeInHelp": "Mostrar carteles y otras imágenes con difuminado rápido cuando termine la carga.", "EnableFastImageFadeInHelp": "Las animaciones y transiciones durarán menos tiempo",
"EnableFastImageFadeIn": "Difuminado rápido de imágenes", "EnableFastImageFadeIn": "Animaciones más rápidas",
"CopyStreamURLError": "Ha habido un error copiando la dirección.", "CopyStreamURLError": "Ha habido un error copiando la dirección.",
"AllowFfmpegThrottlingHelp": "Cuando una transcodificación o un remux se adelanta lo suficiente desde la posición de reproducción actual, pause el proceso para que consuma menos recursos. Esto es más útil cuando se reproduce de forma linear, sin saltar de posición de reproducción a menudo. Desactívelo si experimenta problemas de reproducción.", "AllowFfmpegThrottlingHelp": "Cuando una transcodificación o un remux se adelanta lo suficiente desde la posición de reproducción actual, pause el proceso para que consuma menos recursos. Esto es más útil cuando se reproduce de forma linear, sin saltar de posición de reproducción a menudo. Desactívelo si experimenta problemas de reproducción.",
"PlaybackErrorNoCompatibleStream": "Este contenido no es compatible con este dispositivo y no se puede reproducir: No se puede obtener del servidor en un formato compatible.", "PlaybackErrorNoCompatibleStream": "Este contenido no es compatible con este dispositivo y no se puede reproducir: No se puede obtener del servidor en un formato compatible.",
@ -1524,6 +1524,8 @@
"LabelEnableHttps": "Activar HTTPS", "LabelEnableHttps": "Activar HTTPS",
"TabDVR": "DVR", "TabDVR": "DVR",
"SaveChanges": "Guardar cambios", "SaveChanges": "Guardar cambios",
"EnableBlurhash": "Mostrar una representación de las imágenes mientras cargan",
"EnableBlurhashHelp": "Aparecerá una representación de los colores de las imágenes antes de que terminen de cargar",
"HeaderDVR": "DVR", "HeaderDVR": "DVR",
"SyncPlayAccessHelp": "Selecciona los permisos de este usuario para utilizar SyncPlay. SyncPlay te permite sincroniza la reproducción entre varios dispositivos.", "SyncPlayAccessHelp": "Selecciona los permisos de este usuario para utilizar SyncPlay. SyncPlay te permite sincroniza la reproducción entre varios dispositivos.",
"MessageSyncPlayErrorMedia": "¡No se pudo activar SyncPlay! Error de medio.", "MessageSyncPlayErrorMedia": "¡No se pudo activar SyncPlay! Error de medio.",

View file

@ -1836,6 +1836,11 @@ bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
blurhash@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8" version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"