mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Blurhash implementation (from scratch)
This commit is contained in:
parent
2efdc94146
commit
8ef7a7a054
4 changed files with 139 additions and 18 deletions
|
@ -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() {
|
||||||
|
|
|
@ -505,6 +505,9 @@ import 'programStyles';
|
||||||
let imgUrl = null;
|
let imgUrl = null;
|
||||||
let coverImage = false;
|
let coverImage = false;
|
||||||
let uiAspect = null;
|
let uiAspect = null;
|
||||||
|
let blurhash;
|
||||||
|
let blurhashimg = item.ImageBlurHashes;
|
||||||
|
let imgtag;
|
||||||
|
|
||||||
if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) {
|
if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) {
|
||||||
|
|
||||||
|
@ -513,6 +516,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ImageTags.Thumb
|
tag: item.ImageTags.Thumb
|
||||||
});
|
});
|
||||||
|
imgtag = 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) {
|
||||||
|
|
||||||
|
@ -521,6 +525,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ImageTags.Banner
|
tag: item.ImageTags.Banner
|
||||||
});
|
});
|
||||||
|
imgtag = item.ImageTags.Banner;
|
||||||
|
|
||||||
} else if (options.preferDisc && item.ImageTags && item.ImageTags.Disc) {
|
} else if (options.preferDisc && item.ImageTags && item.ImageTags.Disc) {
|
||||||
|
|
||||||
|
@ -529,6 +534,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ImageTags.Disc
|
tag: item.ImageTags.Disc
|
||||||
});
|
});
|
||||||
|
imgtag = item.ImageTags.Disc;
|
||||||
|
|
||||||
} else if (options.preferLogo && item.ImageTags && item.ImageTags.Logo) {
|
} else if (options.preferLogo && item.ImageTags && item.ImageTags.Logo) {
|
||||||
|
|
||||||
|
@ -537,6 +543,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ImageTags.Logo
|
tag: item.ImageTags.Logo
|
||||||
});
|
});
|
||||||
|
imgtag = item.ImageTags.Logo;
|
||||||
|
|
||||||
} else if (options.preferLogo && item.ParentLogoImageTag && item.ParentLogoItemId) {
|
} else if (options.preferLogo && item.ParentLogoImageTag && item.ParentLogoItemId) {
|
||||||
|
|
||||||
|
@ -545,6 +552,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ParentLogoImageTag
|
tag: item.ParentLogoImageTag
|
||||||
});
|
});
|
||||||
|
imgtag = item.ParentLogoImageTag;
|
||||||
|
|
||||||
} else if (options.preferThumb && item.SeriesThumbImageTag && options.inheritThumb !== false) {
|
} else if (options.preferThumb && item.SeriesThumbImageTag && options.inheritThumb !== false) {
|
||||||
|
|
||||||
|
@ -553,6 +561,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.SeriesThumbImageTag
|
tag: item.SeriesThumbImageTag
|
||||||
});
|
});
|
||||||
|
imgtag = 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') {
|
||||||
|
|
||||||
|
@ -561,6 +570,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ParentThumbImageTag
|
tag: item.ParentThumbImageTag
|
||||||
});
|
});
|
||||||
|
imgtag = item.ParentThumbImageTag;
|
||||||
|
|
||||||
} else if (options.preferThumb && item.BackdropImageTags && item.BackdropImageTags.length) {
|
} else if (options.preferThumb && item.BackdropImageTags && item.BackdropImageTags.length) {
|
||||||
|
|
||||||
|
@ -569,6 +579,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.BackdropImageTags[0]
|
tag: item.BackdropImageTags[0]
|
||||||
});
|
});
|
||||||
|
imgtag = item.BackdropImageTags[0];
|
||||||
|
|
||||||
forceName = true;
|
forceName = true;
|
||||||
|
|
||||||
|
@ -579,6 +590,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ParentBackdropImageTags[0]
|
tag: item.ParentBackdropImageTags[0]
|
||||||
});
|
});
|
||||||
|
imgtag = item.ParentBackdropImageTags[0];
|
||||||
|
|
||||||
} else if (item.ImageTags && item.ImageTags.Primary) {
|
} else if (item.ImageTags && item.ImageTags.Primary) {
|
||||||
|
|
||||||
|
@ -590,6 +602,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ImageTags.Primary
|
tag: item.ImageTags.Primary
|
||||||
});
|
});
|
||||||
|
imgtag = item.ImageTags.Primary;
|
||||||
|
|
||||||
if (options.preferThumb && options.showTitle !== false) {
|
if (options.preferThumb && options.showTitle !== false) {
|
||||||
forceName = true;
|
forceName = true;
|
||||||
|
@ -612,6 +625,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.PrimaryImageTag
|
tag: item.PrimaryImageTag
|
||||||
});
|
});
|
||||||
|
imgtag = item.PrimaryImageTag;
|
||||||
|
|
||||||
if (options.preferThumb && options.showTitle !== false) {
|
if (options.preferThumb && options.showTitle !== false) {
|
||||||
forceName = true;
|
forceName = true;
|
||||||
|
@ -630,6 +644,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ParentPrimaryImageTag
|
tag: item.ParentPrimaryImageTag
|
||||||
});
|
});
|
||||||
|
imgtag = item.ParentPrimaryImageTag;
|
||||||
} else if (item.SeriesPrimaryImageTag) {
|
} else if (item.SeriesPrimaryImageTag) {
|
||||||
|
|
||||||
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, {
|
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, {
|
||||||
|
@ -637,6 +652,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.SeriesPrimaryImageTag
|
tag: item.SeriesPrimaryImageTag
|
||||||
});
|
});
|
||||||
|
imgtag = item.SeriesPrimaryImageTag;
|
||||||
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
|
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
|
||||||
|
|
||||||
height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null;
|
height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null;
|
||||||
|
@ -647,6 +663,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.AlbumPrimaryImageTag
|
tag: item.AlbumPrimaryImageTag
|
||||||
});
|
});
|
||||||
|
imgtag = item.AlbumPrimaryImageTag;
|
||||||
|
|
||||||
if (primaryImageAspectRatio) {
|
if (primaryImageAspectRatio) {
|
||||||
uiAspect = getDesiredAspect(shape);
|
uiAspect = getDesiredAspect(shape);
|
||||||
|
@ -661,6 +678,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ImageTags.Thumb
|
tag: item.ImageTags.Thumb
|
||||||
});
|
});
|
||||||
|
imgtag = item.ImageTags.Thumb;
|
||||||
|
|
||||||
} else if (item.BackdropImageTags && item.BackdropImageTags.length) {
|
} else if (item.BackdropImageTags && item.BackdropImageTags.length) {
|
||||||
|
|
||||||
|
@ -669,6 +687,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.BackdropImageTags[0]
|
tag: item.BackdropImageTags[0]
|
||||||
});
|
});
|
||||||
|
imgtag = item.BackdropImageTags[0];
|
||||||
|
|
||||||
} else if (item.ImageTags && item.ImageTags.Thumb) {
|
} else if (item.ImageTags && item.ImageTags.Thumb) {
|
||||||
|
|
||||||
|
@ -677,6 +696,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ImageTags.Thumb
|
tag: item.ImageTags.Thumb
|
||||||
});
|
});
|
||||||
|
imgtag = item.ImageTags.Thumb;
|
||||||
|
|
||||||
} else if (item.SeriesThumbImageTag && options.inheritThumb !== false) {
|
} else if (item.SeriesThumbImageTag && options.inheritThumb !== false) {
|
||||||
|
|
||||||
|
@ -685,6 +705,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.SeriesThumbImageTag
|
tag: item.SeriesThumbImageTag
|
||||||
});
|
});
|
||||||
|
imgtag = item.SeriesThumbImageTag;
|
||||||
|
|
||||||
} else if (item.ParentThumbItemId && options.inheritThumb !== false) {
|
} else if (item.ParentThumbItemId && options.inheritThumb !== false) {
|
||||||
|
|
||||||
|
@ -693,6 +714,7 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ParentThumbImageTag
|
tag: item.ParentThumbImageTag
|
||||||
});
|
});
|
||||||
|
imgtag = item.ParentThumbImageTag;
|
||||||
|
|
||||||
} else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false) {
|
} else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false) {
|
||||||
|
|
||||||
|
@ -701,11 +723,15 @@ import 'programStyles';
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
tag: item.ParentBackdropImageTags[0]
|
tag: item.ParentBackdropImageTags[0]
|
||||||
});
|
});
|
||||||
|
imgtag = item.ParentBackdropImageTags[0];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blurhash = imageLoader.getImageBlurhashStr(blurhashimg, imgtag);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imgUrl: imgUrl,
|
imgUrl: imgUrl,
|
||||||
|
blurhash: blurhash,
|
||||||
forceName: forceName,
|
forceName: forceName,
|
||||||
coverImage: coverImage
|
coverImage: coverImage
|
||||||
};
|
};
|
||||||
|
@ -1321,6 +1347,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 +1472,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>';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,6 +12,82 @@ import 'css!./style';
|
||||||
fillImageElement(elem, source);
|
fillImageElement(elem, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getImageBlurhashStr(hashes, tags) {
|
||||||
|
if (hashes && tags) {
|
||||||
|
return hashes[tags];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// function destroyBlurhash(target) {
|
||||||
|
// let canvas = target.getElementsByClassName('blurhash-canvas')[0];
|
||||||
|
// target.removeChild(canvas);
|
||||||
|
// target.classList.remove('blurhashed');
|
||||||
|
// }
|
||||||
|
|
||||||
|
function itemBlurhashing(entry) {
|
||||||
|
// This intersection ratio ensures that items that are near the borders are also blurhashed, alongside items that are outside the viewport
|
||||||
|
// if (entry.intersectionRation <= 0.025)
|
||||||
|
if (entry.target) {
|
||||||
|
let target = entry.target;
|
||||||
|
// We only keep max 80 items blurhashed in screen to save memory
|
||||||
|
// if (document.getElementsByClassName('blurhashed').length <= 80) {
|
||||||
|
|
||||||
|
//} else {
|
||||||
|
// destroyBlurhash(target);
|
||||||
|
//}
|
||||||
|
let blurhashstr = target.getAttribute('data-blurhash');
|
||||||
|
if (blurhash.isBlurhashValid(blurhashstr) && target.getElementsByClassName('blurhash-canvas').length === 0) {
|
||||||
|
console.log('Blurhashing item ' + target.parentElement.parentElement.parentElement.getAttribute('data-index') + ' with intersection ratio ' + entry.intersectionRatio);
|
||||||
|
let width = target.offsetWidth;
|
||||||
|
let height = target.offsetHeight;
|
||||||
|
if (width && height) {
|
||||||
|
let pixels;
|
||||||
|
try {
|
||||||
|
pixels = blurhash.decode(blurhashstr, width, height);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Blurhash decode error: ' + err.toString());
|
||||||
|
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);
|
||||||
|
// Values taken from https://www.npmjs.com/package/blurhash
|
||||||
|
ctx.putImageData(imgData, 1, 1);
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchCanvas(elem) {
|
||||||
|
let child = elem.getElementsByClassName('blurhash-canvas')[0];
|
||||||
|
if (child) {
|
||||||
|
if (elem.getAttribute('data-src')) {
|
||||||
|
child.style.opacity = 1;
|
||||||
|
} else {
|
||||||
|
child.style.opacity = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
|
@ -23,6 +100,10 @@ import 'css!./style';
|
||||||
source = entry;
|
source = entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!entry.target.classList.contains('blurhashed')) {
|
||||||
|
itemBlurhashing(entry);
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.intersectionRatio > 0) {
|
if (entry.intersectionRatio > 0) {
|
||||||
if (source) fillImageElement(entry.target, source);
|
if (source) fillImageElement(entry.target, source);
|
||||||
} else if (!source) {
|
} else if (!source) {
|
||||||
|
@ -45,14 +126,12 @@ import 'css!./style';
|
||||||
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');
|
||||||
|
switchCanvas(elem);
|
||||||
});
|
});
|
||||||
|
// preloaderImg.onload = function () {
|
||||||
|
|
||||||
|
// };
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyImageElement(elem) {
|
function emptyImageElement(elem) {
|
||||||
|
@ -67,9 +146,7 @@ import 'css!./style';
|
||||||
}
|
}
|
||||||
|
|
||||||
elem.setAttribute('data-src', url);
|
elem.setAttribute('data-src', url);
|
||||||
|
switchCanvas(elem);
|
||||||
elem.classList.remove('lazy-image-fadein-fast');
|
|
||||||
elem.classList.remove('lazy-image-fadein');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lazyChildren(elem) {
|
export function lazyChildren(elem) {
|
||||||
|
@ -148,6 +225,7 @@ import 'css!./style';
|
||||||
export default {
|
export default {
|
||||||
fillImages: fillImages,
|
fillImages: fillImages,
|
||||||
fillImage: fillImage,
|
fillImage: fillImage,
|
||||||
|
getImageBlurhashStr: getImageBlurhashStr,
|
||||||
lazyImage: lazyImage,
|
lazyImage: lazyImage,
|
||||||
lazyChildren: lazyChildren,
|
lazyChildren: lazyChildren,
|
||||||
getPrimaryImageAspectRatio: getPrimaryImageAspectRatio
|
getPrimaryImageAspectRatio: getPrimaryImageAspectRatio
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
.cardImageContainer.lazy {
|
.lazy-blurhash-fadein-fast {
|
||||||
opacity: 0;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardImageContainer.lazy.lazy-image-fadein {
|
.lazy-blurhash-fadein {
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.7s;
|
transition: opacity 0.7s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardImageContainer.lazy.lazy-image-fadein-fast {
|
/* We let the canvas overflow a little, so it gives a cooler zoom effect when transitioning */
|
||||||
opacity: 1;
|
.blurhash-canvas {
|
||||||
transition: opacity 0.2s;
|
align-items: stretch;
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: -5px;
|
||||||
|
width: 105%;
|
||||||
|
height: 105%;
|
||||||
|
z-index: -1000;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue