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": {
"alameda": "^1.4.0",
"blurhash": "^1.1.3",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"core-js": "^3.6.5",
"date-fns": "^2.14.0",

View file

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

View file

@ -503,94 +503,49 @@ import 'programStyles';
const primaryImageAspectRatio = item.PrimaryImageAspectRatio;
let forceName = false;
let imgUrl = null;
let imgTag = null;
let coverImage = false;
let uiAspect = null;
let imgType = null;
let itemId = null;
if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: 'Thumb',
maxWidth: width,
tag: item.ImageTags.Thumb
});
imgType = 'Thumb';
imgTag = item.ImageTags.Thumb;
} else if ((options.preferBanner || shape === 'banner') && item.ImageTags && item.ImageTags.Banner) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: 'Banner',
maxWidth: width,
tag: item.ImageTags.Banner
});
imgType = 'Banner';
imgTag = item.ImageTags.Banner;
} else if (options.preferDisc && item.ImageTags && item.ImageTags.Disc) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: 'Disc',
maxWidth: width,
tag: item.ImageTags.Disc
});
imgType = 'Disc';
imgTag = item.ImageTags.Disc;
} else if (options.preferLogo && item.ImageTags && item.ImageTags.Logo) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: 'Logo',
maxWidth: width,
tag: item.ImageTags.Logo
});
imgType = 'Logo';
imgTag = item.ImageTags.Logo;
} else if (options.preferLogo && item.ParentLogoImageTag && item.ParentLogoItemId) {
imgUrl = apiClient.getScaledImageUrl(item.ParentLogoItemId, {
type: 'Logo',
maxWidth: width,
tag: item.ParentLogoImageTag
});
imgType = 'Logo';
imgTag = item.ParentLogoImageTag;
itemId = item.ParentLogoItemId;
} else if (options.preferThumb && item.SeriesThumbImageTag && options.inheritThumb !== false) {
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, {
type: 'Thumb',
maxWidth: width,
tag: item.SeriesThumbImageTag
});
imgType = 'Thumb';
imgTag = item.SeriesThumbImageTag;
itemId = item.SeriesId;
} else if (options.preferThumb && item.ParentThumbItemId && options.inheritThumb !== false && item.MediaType !== 'Photo') {
imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, {
type: 'Thumb',
maxWidth: width,
tag: item.ParentThumbImageTag
});
imgType = 'Thumb';
imgTag = item.ParentThumbImageTag;
itemId = item.ParentThumbItemId;
} else if (options.preferThumb && item.BackdropImageTags && item.BackdropImageTags.length) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: 'Backdrop',
maxWidth: width,
tag: item.BackdropImageTags[0]
});
imgType = 'Backdrop';
imgTag = item.BackdropImageTags[0];
forceName = true;
} else if (options.preferThumb && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false && item.Type === 'Episode') {
imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, {
type: 'Backdrop',
maxWidth: width,
tag: item.ParentBackdropImageTags[0]
});
imgType = 'Backdrop';
imgTag = item.ParentBackdropImageTags[0];
itemId = item.ParentBackdropItemId;
} else if (item.ImageTags && item.ImageTags.Primary) {
imgType = 'Primary';
imgTag = item.ImageTags.Primary;
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) {
forceName = true;
}
@ -603,16 +558,11 @@ import 'programStyles';
}
} else if (item.PrimaryImageTag) {
imgType = 'Primary';
imgTag = item.PrimaryImageTag;
itemId = item.PrimaryImageItemId;
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) {
forceName = true;
}
@ -624,30 +574,19 @@ import 'programStyles';
}
}
} else if (item.ParentPrimaryImageTag) {
imgUrl = apiClient.getScaledImageUrl(item.ParentPrimaryImageItemId, {
type: 'Primary',
maxWidth: width,
tag: item.ParentPrimaryImageTag
});
imgType = 'Primary';
imgTag = item.ParentPrimaryImageTag;
itemId = item.ParentPrimaryImageItemId;
} else if (item.SeriesPrimaryImageTag) {
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, {
type: 'Primary',
maxWidth: width,
tag: item.SeriesPrimaryImageTag
});
imgType = 'Primary';
imgTag = item.SeriesPrimaryImageTag;
itemId = item.SeriesId;
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
imgType = 'Primary';
imgTag = item.AlbumPrimaryImageTag;
itemId = item.AlbumId;
height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null;
imgUrl = apiClient.getScaledImageUrl(item.AlbumId, {
type: 'Primary',
maxHeight: height,
maxWidth: width,
tag: item.AlbumPrimaryImageTag
});
if (primaryImageAspectRatio) {
uiAspect = getDesiredAspect(shape);
if (uiAspect) {
@ -655,57 +594,46 @@ import 'programStyles';
}
}
} else if (item.Type === 'Season' && item.ImageTags && item.ImageTags.Thumb) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: 'Thumb',
maxWidth: width,
tag: item.ImageTags.Thumb
});
imgType = 'Thumb';
imgTag = item.ImageTags.Thumb;
} else if (item.BackdropImageTags && item.BackdropImageTags.length) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: 'Backdrop',
maxWidth: width,
tag: item.BackdropImageTags[0]
});
imgType = 'Backdrop';
imgTag = item.BackdropImageTags[0];
} else if (item.ImageTags && item.ImageTags.Thumb) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: 'Thumb',
maxWidth: width,
tag: item.ImageTags.Thumb
});
imgType = 'Thumb';
imgTag = item.ImageTags.Thumb;
} else if (item.SeriesThumbImageTag && options.inheritThumb !== false) {
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, {
type: 'Thumb',
maxWidth: width,
tag: item.SeriesThumbImageTag
});
imgType = 'Thumb';
imgTag = item.SeriesThumbImageTag;
itemId = item.SeriesId;
} else if (item.ParentThumbItemId && options.inheritThumb !== false) {
imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, {
type: 'Thumb',
maxWidth: width,
tag: item.ParentThumbImageTag
});
imgType = 'Thumb';
imgTag = item.ParentThumbImageTag;
itemId = item.ParentThumbItemId;
} else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false) {
imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, {
type: 'Backdrop',
maxWidth: width,
tag: item.ParentBackdropImageTags[0]
});
imgType = 'Backdrop';
imgTag = item.ParentBackdropImageTags[0];
itemId = item.ParentBackdropItemId;
}
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 {
imgUrl: imgUrl,
blurhash: (blurHashes[imgType] || {})[imgTag],
forceName: forceName,
coverImage: coverImage
};
@ -1321,6 +1249,7 @@ import 'programStyles';
const imgInfo = getCardImageUrl(item, apiClient, options, shape);
const imgUrl = imgInfo.imgUrl;
const blurhash = imgInfo.blurhash;
const forceName = imgInfo.forceName;
@ -1445,15 +1374,20 @@ import 'programStyles';
cardContentClass += ' cardContent-shadow';
}
let blurhashAttrib = '';
if (blurhash && blurhash.length > 0) {
blurhashAttrib = 'data-blurhash="' + blurhash + '"';
}
if (layoutManager.tv) {
// 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>';
} else {
// 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>';
}

View file

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

View file

@ -143,12 +143,12 @@
<select is="emby-select" class="selectSoundEffects" label="${LabelSoundEffects}"></select>
</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}" />
<div class="fieldDescription">${LabelLibraryPageSizeHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldFadein">
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkFadein" />
<span>${EnableFastImageFadeIn}</span>
@ -156,7 +156,15 @@
<div class="fieldDescription checkboxFieldDescription">${EnableFastImageFadeInHelp}</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>
<input type="checkbox" is="emby-checkbox" id="chkDetailsBanner" />
<span>${EnableDetailsBanner}</span>

View file

@ -1,5 +1,6 @@
import * as lazyLoader from 'lazyLoader';
import * as userSettings from 'userSettings';
import * as blurhash from 'blurhash';
import 'css!./style';
/* eslint-disable indent */
@ -11,47 +12,111 @@ import 'css!./style';
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) {
if (!entry) {
throw new Error('entry cannot be null');
}
let target = entry.target;
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 {
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 (source) fillImageElement(entry.target, source);
if (source) fillImageElement(target, source);
} else if (!source) {
emptyImageElement(entry.target);
emptyImageElement(target);
}
}
function fillImageElement(elem, url) {
if (url === undefined) {
throw new Error('url cannot be undefined');
throw new TypeError('url cannot be undefined');
}
let preloaderImg = new Image();
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', () => {
if (elem.tagName !== 'IMG') {
elem.style.backgroundImage = "url('" + url + "')";
} else {
elem.setAttribute('src', url);
}
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');
}
elem.removeAttribute('data-src');
} else {
switchCanvas(elem);
}
});
}
@ -65,11 +130,14 @@ import 'css!./style';
url = elem.getAttribute('src');
elem.setAttribute('src', '');
}
elem.setAttribute('data-src', url);
elem.classList.remove('lazy-image-fadein-fast');
elem.classList.remove('lazy-image-fadein');
if (elem.classList.contains('non-blurhashable') || !userSettings.enableBlurhash()) {
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
elem.classList.add('lazy-hidden');
} else {
switchCanvas(elem);
}
}
export function lazyChildren(elem) {

View file

@ -1,13 +1,32 @@
.cardImageContainer.lazy {
opacity: 0;
}
.cardImageContainer.lazy.lazy-image-fadein {
.lazy-image-fadein {
opacity: 1;
transition: opacity 0.7s;
}
.cardImageContainer.lazy.lazy-image-fadein-fast {
.lazy-image-fadein-fast {
opacity: 1;
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) {
var apiClient = connectionManager.getApiClient(item.ServerId);
let itemId;
var options = {
maxWidth: width * 2,
@ -77,45 +78,45 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan
};
if (item.ImageTags && item.ImageTags.Primary) {
options.tag = item.ImageTags.Primary;
return apiClient.getScaledImageUrl(item.Id, options);
itemId = item.Id;
}
if (item.AlbumId && item.AlbumPrimaryImageTag) {
options.tag = item.AlbumPrimaryImageTag;
return apiClient.getScaledImageUrl(item.AlbumId, options);
itemId = item.AlbumId;
} else if (item.SeriesId && item.SeriesPrimaryImageTag) {
options.tag = item.SeriesPrimaryImageTag;
return apiClient.getScaledImageUrl(item.SeriesId, options);
itemId = item.SeriesId;
} else if (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) {
var apiClient = connectionManager.getApiClient(item.ServerId);
var options = {
maxWidth: width * 2,
type: 'Primary'
};
if (item.ChannelId && 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) {
@ -268,8 +269,10 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan
}
if (options.image !== false) {
var imgUrl = options.imageSource === 'channel' ? getChannelImageUrl(item, downloadWidth) : getImageUrl(item, downloadWidth);
var imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage';
let imgData = options.imageSource === 'channel' ? getChannelImageUrl(item, downloadWidth) : getImageUrl(item, downloadWidth);
let imgUrl = imgData.url;
let blurhash = imgData.blurhash;
let imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage';
if (isLargeStyle && layoutManager.tv) {
imageClass += ' listItemImage-large-tv';
@ -283,8 +286,13 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan
var imageAction = playOnImageClick ? 'resume' : action;
let blurhashAttrib = '';
if (blurhash && blurhash.length > 0) {
blurhashAttrib = 'data-blurhash="' + blurhash + '"';
}
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 {
html += '<div class="' + imageClass + '">';
}

View file

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

View file

@ -128,6 +128,15 @@ import events from 'events';
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) {
if (val !== undefined) {
return this.set('enableBackdrops', val.toString(), false);
@ -294,6 +303,7 @@ export default {
enableThemeSongs: enableThemeSongs,
enableThemeVideos: enableThemeVideos,
enableFastFadein: enableFastFadein,
enableBlurhash: enableBlurhash,
enableBackdrops: enableBackdrops,
language: language,
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.",
"LabelScheduledTaskLastRan": "Last ran {0}, taking {1}.",
"LabelScreensaver": "Screensaver:",
"EnableFastImageFadeIn": "Fast Image Fade Animations",
"EnableFastImageFadeInHelp": "Show posters and other images with a quicker fade animation when they finish loading.",
"EnableFastImageFadeIn": "Faster animations",
"EnableFastImageFadeInHelp": "Use faster animations and transitions",
"LabelSeasonNumber": "Season number:",
"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.",
@ -1548,5 +1548,7 @@
"EveryHour": "Every hour",
"EveryXHours": "Every {0} hours",
"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",
"HeaderNavigation": "Navegación",
"MessageConfirmAppExit": "¿Quieres salir?",
"EnableFastImageFadeInHelp": "Mostrar carteles y otras imágenes con difuminado rápido cuando termine la carga.",
"EnableFastImageFadeIn": "Difuminado rápido de imágenes",
"EnableFastImageFadeInHelp": "Las animaciones y transiciones durarán menos tiempo",
"EnableFastImageFadeIn": "Animaciones más rápidas",
"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.",
"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",
"TabDVR": "DVR",
"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",
"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.",

View file

@ -1836,6 +1836,11 @@ bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
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:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"