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

Reduce cognitive complexity in card builder component

This commit is contained in:
Dmitriy Dubson 2023-10-08 11:46:49 -04:00
parent 8af76ca3e7
commit c8a7c7040a
15 changed files with 735 additions and 391 deletions

View file

@ -6,14 +6,12 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import cardBuilderUtils from './cardBuilderUtils';
import browser from 'scripts/browser'; import browser from 'scripts/browser';
import datetime from 'scripts/datetime'; import datetime from 'scripts/datetime';
import dom from 'scripts/dom'; import dom from 'scripts/dom';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card';
import imageHelper from 'utils/image'; import imageHelper from 'utils/image';
import { randomInt } from 'utils/number';
import focusManager from '../focusManager'; import focusManager from '../focusManager';
import imageLoader from '../images/imageLoader'; import imageLoader from '../images/imageLoader';
@ -29,6 +27,17 @@ import 'elements/emby-button/paper-icon-button-light';
import './card.scss'; import './card.scss';
import '../guide/programs.scss'; import '../guide/programs.scss';
import {
getDesiredAspect,
getPostersPerRow,
isResizable,
isUsingLiveTvNaming,
resolveAction,
resolveCardBoxCssClasses,
resolveCardCssClasses,
resolveCardImageContainerCssClasses,
resolveMixedShapeByAspectRatio
} from './cardBuilderUtils';
const enableFocusTransform = !browser.slow && !browser.edge; const enableFocusTransform = !browser.slow && !browser.edge;
@ -47,24 +56,6 @@ export function getCardsHtml(items, options) {
return buildCardsHtmlInternal(items, options); return buildCardsHtmlInternal(items, options);
} }
/**
* Checks if the window is resizable.
* @param {number} windowWidth - Width of the device's screen.
* @returns {boolean} - Result of the check.
*/
function isResizable(windowWidth) {
const screen = window.screen;
if (screen) {
const screenWidth = screen.availWidth;
if ((screenWidth - windowWidth) > 20) {
return true;
}
}
return false;
}
/** /**
* Gets the width of a card's image according to the shape and amount of cards per row. * Gets the width of a card's image according to the shape and amount of cards per row.
* @param {string} shape - Shape of the card. * @param {string} shape - Shape of the card.
@ -73,7 +64,7 @@ function isResizable(windowWidth) {
* @returns {number} Width of the image for a card. * @returns {number} Width of the image for a card.
*/ */
function getImageWidth(shape, screenWidth, isOrientationLandscape) { function getImageWidth(shape, screenWidth, isOrientationLandscape) {
const imagesPerRow = cardBuilderUtils.getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv); const imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape, layoutManager.tv);
return Math.round(screenWidth / imagesPerRow); return Math.round(screenWidth / imagesPerRow);
} }
@ -113,7 +104,7 @@ function setCardData(items, options) {
options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop'; options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop';
} }
options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape); options.uiAspect = getDesiredAspect(options.shape);
options.primaryImageAspectRatio = primaryImageAspectRatio; options.primaryImageAspectRatio = primaryImageAspectRatio;
if (!options.width && options.widths) { if (!options.width && options.widths) {
@ -280,7 +271,7 @@ function getCardImageUrl(item, apiClient, options, shape) {
let imgUrl = null; let imgUrl = null;
let imgTag = null; let imgTag = null;
let coverImage = false; let coverImage = false;
const uiAspect = cardBuilderUtils.getDesiredAspect(shape); const uiAspect = getDesiredAspect(shape);
let imgType = null; let imgType = null;
let itemId = null; let itemId = null;
@ -411,29 +402,6 @@ function getCardImageUrl(item, apiClient, options, shape) {
}; };
} }
/**
* Generates an index used to select the default color of a card based on a string.
* @param {?string} [str] - String to use for generating the index.
* @returns {number} Index of the color.
*/
function getDefaultColorIndex(str) {
const numRandomColors = 5;
if (str) {
const charIndex = Math.floor(str.length / 2);
const character = String(str.slice(charIndex, charIndex + 1).charCodeAt());
let sum = 0;
for (let i = 0; i < character.length; i++) {
sum += parseInt(character.charAt(i), 10);
}
const index = String(sum).slice(-1);
return (index % numRandomColors) + 1;
} else {
return randomInt(1, numRandomColors);
}
}
/** /**
* Generates the HTML markup for a card's text. * Generates the HTML markup for a card's text.
* @param {Array} lines - Array containing the text lines. * @param {Array} lines - Array containing the text lines.
@ -487,15 +455,6 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout
return html; return html;
} }
/**
* Determines if the item is live TV.
* @param {Object} item - Item to use for the check.
* @returns {boolean} Flag showing if the item is live TV.
*/
function isUsingLiveTvNaming(item) {
return item.Type === 'Program' || item.Type === 'Timer' || item.Type === 'Recording';
}
/** /**
* Returns the air time text for the item based on the given times. * Returns the air time text for the item based on the given times.
* @param {object} item - Item used to generate the air time text. * @param {object} item - Item used to generate the air time text.
@ -574,7 +533,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
} else { } else {
lines.push(escapeHtml(item.SeriesName)); lines.push(escapeHtml(item.SeriesName));
} }
} else if (isUsingLiveTvNaming(item)) { } else if (isUsingLiveTvNaming(item.Type)) {
lines.push(escapeHtml(item.Name)); lines.push(escapeHtml(item.Name));
if (!item.EpisodeTitle && !item.IndexNumber) { if (!item.EpisodeTitle && !item.IndexNumber) {
@ -616,7 +575,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
item.AlbumArtists[0].IsFolder = true; item.AlbumArtists[0].IsFolder = true;
lines.push(getTextActionButton(item.AlbumArtists[0], null, serverId)); lines.push(getTextActionButton(item.AlbumArtists[0], null, serverId));
} else { } else {
lines.push(escapeHtml(isUsingLiveTvNaming(item) ? item.Name : (item.SeriesName || item.Series || item.Album || item.AlbumArtist || ''))); lines.push(escapeHtml(isUsingLiveTvNaming(item.Type) ? item.Name : (item.SeriesName || item.Series || item.Album || item.AlbumArtist || '')));
} }
} }
@ -888,15 +847,6 @@ function importRefreshIndicator() {
} }
} }
/**
* Returns the default background class for a card based on a string.
* @param {?string} [str] - Text used to generate the background class.
* @returns {string} CSS classes for default card backgrounds.
*/
export function getDefaultBackgroundClass(str) {
return 'defaultCardBackground defaultCardBackground' + getDefaultColorIndex(str);
}
/** /**
* Builds the HTML markup for an individual card. * Builds the HTML markup for an individual card.
* @param {number} index - Index of the card * @param {number} index - Index of the card
@ -906,87 +856,32 @@ export function getDefaultBackgroundClass(str) {
* @returns {string} HTML markup for the generated card. * @returns {string} HTML markup for the generated card.
*/ */
function buildCard(index, item, apiClient, options) { function buildCard(index, item, apiClient, options) {
let action = options.action || 'link'; const action = resolveAction({
defaultAction: options.action || 'link',
if (action === 'play' && item.IsFolder) { isFolder: item.IsFolder,
// If this hard-coding is ever removed make sure to test nested photo albums isPhoto: item.MediaType === 'Photo'
action = 'link'; });
} else if (item.MediaType === 'Photo') {
action = 'play';
}
let shape = options.shape; let shape = options.shape;
if (shape === 'mixed') { if (shape === 'mixed') {
shape = null; shape = resolveMixedShapeByAspectRatio(item.PrimaryImageAspectRatio);
const primaryImageAspectRatio = item.PrimaryImageAspectRatio;
if (primaryImageAspectRatio) {
if (primaryImageAspectRatio >= 1.33) {
shape = 'mixedBackdrop';
} else if (primaryImageAspectRatio > 0.71) {
shape = 'mixedSquare';
} else {
shape = 'mixedPortrait';
}
}
shape = shape || 'mixedSquare';
} }
// TODO move card creation code to Card component // TODO move card creation code to Card component
let className = 'card';
if (shape) {
className += ' ' + shape + 'Card';
}
if (options.cardCssClass) {
className += ' ' + options.cardCssClass;
}
if (options.cardClass) {
className += ' ' + options.cardClass;
}
if (layoutManager.desktop) {
className += ' card-hoverable';
}
if (layoutManager.tv) {
className += ' show-focus';
if (enableFocusTransform) {
className += ' show-animation';
}
}
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 blurhash = imgInfo.blurhash;
const forceName = imgInfo.forceName; const forceName = imgInfo.forceName;
const overlayText = options.overlayText; const overlayText = options.overlayText;
let cardImageContainerClass = 'cardImageContainer'; const cardImageContainerClasses = resolveCardImageContainerCssClasses({
const coveredImage = options.coverImage || imgInfo.coverImage; itemType: item.Type,
itemName: item.Name,
if (coveredImage) { hasCoverImage: options.coverImage || imgInfo.coverImage,
cardImageContainerClass += ' coveredImage'; imgUrl
});
if (item.Type === 'TvChannel') {
cardImageContainerClass += ' coveredImage-contain';
}
}
if (!imgUrl) {
cardImageContainerClass += ' ' + getDefaultBackgroundClass(item.Name);
}
let cardBoxClass = options.cardLayout ? 'cardBox visualCardBox' : 'cardBox';
let footerCssClass; let footerCssClass;
let progressHtml = indicators.getProgressBarHtml(item); let progressHtml = indicators.getProgressBarHtml(item);
@ -1046,9 +941,10 @@ function buildCard(index, item, apiClient, options) {
outerCardFooter = getCardFooterText(item, apiClient, options, footerCssClass, progressHtml, { forceName, overlayText, isOuterFooter: true }, { imgUrl, logoUrl }); outerCardFooter = getCardFooterText(item, apiClient, options, footerCssClass, progressHtml, { forceName, overlayText, isOuterFooter: true }, { imgUrl, logoUrl });
} }
if (outerCardFooter && !options.cardLayout) { const cardBoxClass = resolveCardBoxCssClasses({
cardBoxClass += ' cardBox-bottompadded'; hasOuterCardFooter: outerCardFooter.length > 0,
} cardLayout: options.cardLayout
});
let overlayButtons = ''; let overlayButtons = '';
if (layoutManager.mobile) { if (layoutManager.mobile) {
@ -1073,10 +969,6 @@ function buildCard(index, item, apiClient, options) {
} }
} }
if (options.showChildCountIndicator && item.ChildCount) {
className += ' groupedCard';
}
// cardBox can be it's own separate element if an outer footer is ever needed // cardBox can be it's own separate element if an outer footer is ever needed
let cardImageContainerOpen; let cardImageContainerOpen;
let cardImageContainerClose = ''; let cardImageContainerClose = '';
@ -1092,7 +984,7 @@ function buildCard(index, item, apiClient, options) {
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 + '" ' + blurhashAttrib + '>') : ('<div class="' + cardImageContainerClass + ' ' + cardContentClass + '">'); cardImageContainerOpen = imgUrl ? ('<div class="' + cardImageContainerClasses + ' ' + cardContentClass + ' lazy" data-src="' + imgUrl + '" ' + blurhashAttrib + '>') : ('<div class="' + cardImageContainerClasses + ' ' + cardContentClass + '">');
cardImageContainerClose = '</div>'; cardImageContainerClose = '</div>';
} else { } else {
@ -1100,7 +992,7 @@ function buildCard(index, item, apiClient, options) {
const url = appRouter.getRouteUrl(item); const url = appRouter.getRouteUrl(item);
// 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 ? ('<a href="' + url + '" data-action="' + action + '" class="' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction lazy" data-src="' + imgUrl + '" ' + blurhashAttrib + cardImageContainerAriaLabelAttribute + '>') : ('<a href="' + url + '" data-action="' + action + '" class="' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction"' + cardImageContainerAriaLabelAttribute + '>'); cardImageContainerOpen = imgUrl ? ('<a href="' + url + '" data-action="' + action + '" class="' + cardImageContainerClasses + ' ' + cardContentClass + ' itemAction lazy" data-src="' + imgUrl + '" ' + blurhashAttrib + cardImageContainerAriaLabelAttribute + '>') : ('<a href="' + url + '" data-action="' + action + '" class="' + cardImageContainerClasses + ' ' + cardContentClass + ' itemAction"' + cardImageContainerAriaLabelAttribute + '>');
cardImageContainerClose = '</a>'; cardImageContainerClose = '</a>';
} }
@ -1178,16 +1070,24 @@ function buildCard(index, item, apiClient, options) {
let ariaLabelAttribute = ''; let ariaLabelAttribute = '';
if (tagName === 'button') { if (tagName === 'button') {
className += ' itemAction';
actionAttribute = ' data-action="' + action + '"'; actionAttribute = ' data-action="' + action + '"';
ariaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`; ariaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`;
} else { } else {
actionAttribute = ''; actionAttribute = '';
} }
if (item.Type !== 'MusicAlbum' && item.Type !== 'MusicArtist' && item.Type !== 'Audio') { const className = resolveCardCssClasses({
className += ' card-withuserdata'; shape: shape,
} cardCssClass: options.cardCssClass,
cardClass: options.cardClass,
isTV: layoutManager.tv,
enableFocusTransform: enableFocusTransform,
isDesktop: layoutManager.desktop,
showChildCountIndicator: options.showChildCountIndicator,
childCount: item.ChildCount,
tagName: tagName,
itemType: item.Type
});
const positionTicksData = item.UserData?.PlaybackPositionTicks ? (' data-positionticks="' + item.UserData.PlaybackPositionTicks + '"') : ''; const positionTicksData = item.UserData?.PlaybackPositionTicks ? (' data-positionticks="' + item.UserData.PlaybackPositionTicks + '"') : '';
const collectionIdData = options.collectionId ? (' data-collectionid="' + options.collectionId + '"') : ''; const collectionIdData = options.collectionId ? (' data-collectionid="' + options.collectionId + '"') : '';
@ -1296,7 +1196,7 @@ export function getDefaultText(item, options) {
return '<span class="cardImageIcon material-icons ' + options.defaultCardImageIcon + '" aria-hidden="true"></span>'; return '<span class="cardImageIcon material-icons ' + options.defaultCardImageIcon + '" aria-hidden="true"></span>';
} }
const defaultName = isUsingLiveTvNaming(item) ? item.Name : itemHelper.getDisplayName(item); const defaultName = isUsingLiveTvNaming(item.Type) ? item.Name : itemHelper.getDisplayName(item);
return '<div class="cardText cardDefaultText">' + escapeHtml(defaultName) + '</div>'; return '<div class="cardText cardDefaultText">' + escapeHtml(defaultName) + '</div>';
} }
@ -1515,7 +1415,6 @@ export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) {
export default { export default {
getCardsHtml: getCardsHtml, getCardsHtml: getCardsHtml,
getDefaultBackgroundClass: getDefaultBackgroundClass,
getDefaultText: getDefaultText, getDefaultText: getDefaultText,
buildCards: buildCards, buildCards: buildCards,
onUserDataChanged: onUserDataChanged, onUserDataChanged: onUserDataChanged,

View file

@ -1,173 +0,0 @@
const ASPECT_RATIOS = {
portrait: (2 / 3),
backdrop: (16 / 9),
square: 1,
banner: (1000 / 185)
};
/**
* Computes the aspect ratio for a card given its shape.
* @param {string} shape - Shape for which to get the aspect ratio.
* @returns {null|number} Ratio of the shape.
*/
function getDesiredAspect(shape) {
if (!shape) {
return null;
}
shape = shape.toLowerCase();
if (shape.indexOf('portrait') !== -1) {
return ASPECT_RATIOS.portrait;
}
if (shape.indexOf('backdrop') !== -1) {
return ASPECT_RATIOS.backdrop;
}
if (shape.indexOf('square') !== -1) {
return ASPECT_RATIOS.square;
}
if (shape.indexOf('banner') !== -1) {
return ASPECT_RATIOS.banner;
}
return null;
}
/**
* Computes the number of posters per row.
* @param {string} shape - Shape of the cards.
* @param {number} screenWidth - Width of the screen.
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
* @param {boolean} isTV - Flag to denote if posters are rendered on a television screen.
* @returns {number} Number of cards per row for an itemsContainer.
*/
function getPostersPerRow(shape, screenWidth, isOrientationLandscape, isTV) {
switch (shape) {
case 'portrait': return postersPerRowPortrait(screenWidth, isTV);
case 'square': return postersPerRowSquare(screenWidth, isTV);
case 'banner': return postersPerRowBanner(screenWidth);
case 'backdrop': return postersPerRowBackdrop(screenWidth, isTV);
case 'smallBackdrop': return postersPerRowSmallBackdrop(screenWidth);
case 'overflowSmallBackdrop': return postersPerRowOverflowSmallBackdrop(screenWidth, isOrientationLandscape, isTV);
case 'overflowPortrait': return postersPerRowOverflowPortrait(screenWidth, isOrientationLandscape, isTV);
case 'overflowSquare': return postersPerRowOverflowSquare(screenWidth, isOrientationLandscape, isTV);
case 'overflowBackdrop': return postersPerRowOverflowBackdrop(screenWidth, isOrientationLandscape, isTV);
default: return 4;
}
}
const postersPerRowPortrait = (screenWidth, isTV) => {
switch (true) {
case isTV: return 100 / 16.66666667;
case screenWidth >= 2200: return 10;
case screenWidth >= 1920: return 100 / 11.1111111111;
case screenWidth >= 1600: return 8;
case screenWidth >= 1400: return 100 / 14.28571428571;
case screenWidth >= 1200: return 100 / 16.66666667;
case screenWidth >= 800: return 5;
case screenWidth >= 700: return 4;
case screenWidth >= 500: return 100 / 33.33333333;
default: return 100 / 33.33333333;
}
};
const postersPerRowSquare = (screenWidth, isTV) => {
switch (true) {
case isTV: return 100 / 16.66666667;
case screenWidth >= 2200: return 10;
case screenWidth >= 1920: return 100 / 11.1111111111;
case screenWidth >= 1600: return 8;
case screenWidth >= 1400: return 100 / 14.28571428571;
case screenWidth >= 1200: return 100 / 16.66666667;
case screenWidth >= 800: return 5;
case screenWidth >= 700: return 4;
case screenWidth >= 500: return 100 / 33.33333333;
default: return 2;
}
};
const postersPerRowBanner = (screenWidth) => {
switch (true) {
case screenWidth >= 2200: return 4;
case screenWidth >= 1200: return 100 / 33.33333333;
case screenWidth >= 800: return 2;
default: return 1;
}
};
const postersPerRowBackdrop = (screenWidth, isTV) => {
switch (true) {
case isTV: return 4;
case screenWidth >= 2500: return 6;
case screenWidth >= 1600: return 5;
case screenWidth >= 1200: return 4;
case screenWidth >= 770: return 3;
case screenWidth >= 420: return 2;
default: return 1;
}
};
function postersPerRowSmallBackdrop(screenWidth) {
switch (true) {
case screenWidth >= 1600: return 8;
case screenWidth >= 1400: return 100 / 14.2857142857;
case screenWidth >= 1200: return 100 / 16.66666667;
case screenWidth >= 1000: return 5;
case screenWidth >= 800: return 4;
case screenWidth >= 500: return 100 / 33.33333333;
default: return 2;
}
}
const postersPerRowOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => {
switch (true) {
case isTV: return 100 / 18.9;
case isLandscape && screenWidth >= 800: return 100 / 15.5;
case isLandscape: return 100 / 23.3;
case screenWidth >= 540: return 100 / 30;
default: return 100 / 72;
}
};
const postersPerRowOverflowPortrait = (screenWidth, isLandscape, isTV) => {
switch (true) {
case isTV: return 100 / 15.5;
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
case isLandscape: return 100 / 15.5;
case screenWidth >= 1400: return 100 / 15;
case screenWidth >= 1200: return 100 / 18;
case screenWidth >= 760: return 100 / 23;
case screenWidth >= 400: return 100 / 31.5;
default: return 100 / 42;
}
};
const postersPerRowOverflowSquare = (screenWidth, isLandscape, isTV) => {
switch (true) {
case isTV: return 100 / 15.5;
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
case isLandscape: return 100 / 15.5;
case screenWidth >= 1400: return 100 / 15;
case screenWidth >= 1200: return 100 / 18;
case screenWidth >= 760: return 100 / 23;
case screenWidth >= 540: return 100 / 31.5;
default: return 100 / 42;
}
};
const postersPerRowOverflowBackdrop = (screenWidth, isLandscape, isTV) => {
switch (true) {
case isTV: return 100 / 23.3;
case isLandscape && screenWidth >= 1700: return 100 / 18.5;
case isLandscape: return 100 / 23.3;
case screenWidth >= 1800: return 100 / 23.5;
case screenWidth >= 1400: return 100 / 30;
case screenWidth >= 760: return 100 / 40;
case screenWidth >= 640: return 100 / 56;
default: return 100 / 72;
}
};
export default {
getDesiredAspect,
getPostersPerRow
};

View file

@ -1,47 +1,52 @@
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
import cardBuilderUtils from './cardBuilderUtils'; import {
getDefaultBackgroundClass,
getDefaultColorIndex,
getDesiredAspect,
getPostersPerRow,
isResizable,
isUsingLiveTvNaming,
resolveAction, resolveCardBoxCssClasses,
resolveCardCssClasses,
resolveCardImageContainerCssClasses,
resolveMixedShapeByAspectRatio
} from './cardBuilderUtils';
describe('getDesiredAspect', () => { describe('getDesiredAspect', () => {
test('"portrait" (case insensitive)', () => { test('"portrait" (case insensitive)', () => {
expect(cardBuilderUtils.getDesiredAspect('portrait')).toEqual((2 / 3)); expect(getDesiredAspect('portrait')).toEqual((2 / 3));
expect(cardBuilderUtils.getDesiredAspect('PorTRaIt')).toEqual((2 / 3)); expect(getDesiredAspect('PorTRaIt')).toEqual((2 / 3));
}); });
test('"backdrop" (case insensitive)', () => { test('"backdrop" (case insensitive)', () => {
expect(cardBuilderUtils.getDesiredAspect('backdrop')).toEqual((16 / 9)); expect(getDesiredAspect('backdrop')).toEqual((16 / 9));
expect(cardBuilderUtils.getDesiredAspect('BaCkDroP')).toEqual((16 / 9)); expect(getDesiredAspect('BaCkDroP')).toEqual((16 / 9));
}); });
test('"square" (case insensitive)', () => { test('"square" (case insensitive)', () => {
expect(cardBuilderUtils.getDesiredAspect('square')).toEqual(1); expect(getDesiredAspect('square')).toEqual(1);
expect(cardBuilderUtils.getDesiredAspect('sQuArE')).toEqual(1); expect(getDesiredAspect('sQuArE')).toEqual(1);
}); });
test('"banner" (case insensitive)', () => { test('"banner" (case insensitive)', () => {
expect(cardBuilderUtils.getDesiredAspect('banner')).toEqual((1000 / 185)); expect(getDesiredAspect('banner')).toEqual((1000 / 185));
expect(cardBuilderUtils.getDesiredAspect('BaNnEr')).toEqual((1000 / 185)); expect(getDesiredAspect('BaNnEr')).toEqual((1000 / 185));
}); });
test('invalid shape', () => { test('invalid shape', () => expect(getDesiredAspect('invalid')).toBeNull());
expect(cardBuilderUtils.getDesiredAspect('invalid')).toBeNull();
});
test('shape is not provided', () => { test('shape is not provided', () => expect(getDesiredAspect('')).toBeNull());
expect(cardBuilderUtils.getDesiredAspect('')).toBeNull();
});
}); });
describe('getPostersPerRow', () => { describe('getPostersPerRow', () => {
test('resolves to default of 4 posters per row if shape is not provided', () => { test('resolves to default of 4 posters per row if shape is not provided', () => {
expect(cardBuilderUtils.getPostersPerRow('', 0, false, false)).toEqual(4); expect(getPostersPerRow('', 0, false, false)).toEqual(4);
}); });
describe('portrait', () => { describe('portrait', () => {
const postersPerRowForPortrait = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('portrait', screenWidth, false, isTV)); const postersPerRowForPortrait = (screenWidth: number, isTV: boolean) => (getPostersPerRow('portrait', screenWidth, false, isTV));
test('television', () => { test('television', () => expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667));
expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667);
});
test('screen width less than 500px', () => { test('screen width less than 500px', () => {
expect(postersPerRowForPortrait(100, false)).toEqual(100 / 33.33333333); expect(postersPerRowForPortrait(100, false)).toEqual(100 / 33.33333333);
@ -90,11 +95,9 @@ describe('getPostersPerRow', () => {
}); });
describe('square', () => { describe('square', () => {
const postersPerRowForSquare = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('square', screenWidth, false, isTV)); const postersPerRowForSquare = (screenWidth: number, isTV: boolean) => (getPostersPerRow('square', screenWidth, false, isTV));
test('television', () => { test('television', () => expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667));
expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667);
});
test('screen width less than 500px', () => { test('screen width less than 500px', () => {
expect(postersPerRowForSquare(100, false)).toEqual(2); expect(postersPerRowForSquare(100, false)).toEqual(2);
@ -143,11 +146,9 @@ describe('getPostersPerRow', () => {
}); });
describe('banner', () => { describe('banner', () => {
const postersPerRowForBanner = (screenWidth) => (cardBuilderUtils.getPostersPerRow('banner', screenWidth, false, false)); const postersPerRowForBanner = (screenWidth: number) => (getPostersPerRow('banner', screenWidth, false, false));
test('screen width less than 800px', () => { test('screen width less than 800px', () => expect(postersPerRowForBanner(799)).toEqual(1));
expect(postersPerRowForBanner(799)).toEqual(1);
});
test('screen width greater than or equal to 800px', () => { test('screen width greater than or equal to 800px', () => {
expect(postersPerRowForBanner(800)).toEqual(2); expect(postersPerRowForBanner(800)).toEqual(2);
@ -166,11 +167,9 @@ describe('getPostersPerRow', () => {
}); });
describe('backdrop', () => { describe('backdrop', () => {
const postersPerRowForBackdrop = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('backdrop', screenWidth, false, isTV)); const postersPerRowForBackdrop = (screenWidth: number, isTV: boolean) => (getPostersPerRow('backdrop', screenWidth, false, isTV));
test('television', () => { test('television', () => expect(postersPerRowForBackdrop(0, true)).toEqual(4));
expect(postersPerRowForBackdrop(0, true)).toEqual(4);
});
test('screen width less than 420px', () => { test('screen width less than 420px', () => {
expect(postersPerRowForBackdrop(100, false)).toEqual(1); expect(postersPerRowForBackdrop(100, false)).toEqual(1);
@ -204,7 +203,7 @@ describe('getPostersPerRow', () => {
}); });
describe('small backdrop', () => { describe('small backdrop', () => {
const postersPerRowForSmallBackdrop = (screenWidth) => (cardBuilderUtils.getPostersPerRow('smallBackdrop', screenWidth, false, false)); const postersPerRowForSmallBackdrop = (screenWidth: number) => (getPostersPerRow('smallBackdrop', screenWidth, false, false));
test('screen width less than 500px', () => { test('screen width less than 500px', () => {
expect(postersPerRowForSmallBackdrop(100)).toEqual(2); expect(postersPerRowForSmallBackdrop(100)).toEqual(2);
@ -243,11 +242,9 @@ describe('getPostersPerRow', () => {
}); });
describe('overflow small backdrop', () => { describe('overflow small backdrop', () => {
const postersPerRowForOverflowSmallBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSmallBackdrop', screenWidth, isLandscape, isTV)); const postersPerRowForOverflowSmallBackdrop = (screenWidth: number, isLandscape = false, isTV = false) => (getPostersPerRow('overflowSmallBackdrop', screenWidth, isLandscape, isTV));
test('television', () => { test('television', () => expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual(100 / 18.9));
expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual( 100 / 18.9);
});
describe('non-landscape', () => { describe('non-landscape', () => {
test('screen width greater or equal to 540px', () => { test('screen width greater or equal to 540px', () => {
@ -275,11 +272,9 @@ describe('getPostersPerRow', () => {
}); });
describe('overflow portrait', () => { describe('overflow portrait', () => {
const postersPerRowForOverflowPortrait = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowPortrait', screenWidth, isLandscape, isTV)); const postersPerRowForOverflowPortrait = (screenWidth: number, isLandscape = false, isTV = false) => (getPostersPerRow('overflowPortrait', screenWidth, isLandscape, isTV));
test('television', () => { test('television', () => expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual(100 / 15.5));
expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual( 100 / 15.5);
});
describe('non-landscape', () => { describe('non-landscape', () => {
test('screen width greater or equal to 1400px', () => { test('screen width greater or equal to 1400px', () => {
@ -322,11 +317,9 @@ describe('getPostersPerRow', () => {
}); });
describe('overflow square', () => { describe('overflow square', () => {
const postersPerRowForOverflowSquare = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowSquare', screenWidth, isLandscape, isTV)); const postersPerRowForOverflowSquare = (screenWidth: number, isLandscape = false, isTV = false) => (getPostersPerRow('overflowSquare', screenWidth, isLandscape, isTV));
test('television', () => { test('television', () => expect(postersPerRowForOverflowSquare(0, false, true)).toEqual(100 / 15.5));
expect(postersPerRowForOverflowSquare(0, false, true)).toEqual( 100 / 15.5);
});
describe('non-landscape', () => { describe('non-landscape', () => {
test('screen width greater or equal to 1400px', () => { test('screen width greater or equal to 1400px', () => {
@ -369,11 +362,9 @@ describe('getPostersPerRow', () => {
}); });
describe('overflow backdrop', () => { describe('overflow backdrop', () => {
const postersPerRowForOverflowBackdrop = (screenWidth, isLandscape, isTV) => (cardBuilderUtils.getPostersPerRow('overflowBackdrop', screenWidth, isLandscape, isTV)); const postersPerRowForOverflowBackdrop = (screenWidth: number, isLandscape = false, isTV = false) => (getPostersPerRow('overflowBackdrop', screenWidth, isLandscape, isTV));
test('television', () => { test('television', () => expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual(100 / 23.3));
expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual( 100 / 23.3);
});
describe('non-landscape', () => { describe('non-landscape', () => {
test('screen width greater or equal to 1800px', () => { test('screen width greater or equal to 1800px', () => {
@ -415,3 +406,312 @@ describe('getPostersPerRow', () => {
}); });
}); });
}); });
test('isUsingLiveTvNaming', () => {
expect(isUsingLiveTvNaming('Program')).toEqual(true);
expect(isUsingLiveTvNaming('Timer')).toEqual(true);
expect(isUsingLiveTvNaming('Recording')).toEqual(true);
});
describe('isResizable', () => {
test('is resizable if difference between screen width and window width is greater than 20px', () => {
Object.defineProperty(window, 'screen', {
value: {
availWidth: 2048
}
});
expect(isResizable(1024)).toEqual(true);
});
test('is not resizable if difference between screen width and window width is less than or equal to 20px', () => {
Object.defineProperty(window, 'screen', {
value: {
availWidth: 1044
}
});
expect(isResizable(1024)).toEqual(false);
});
test('is not resizable if screen width is not provided', () => {
Object.defineProperty(window, 'screen', {
value: undefined
});
expect(isResizable(1024)).toEqual(false);
});
});
describe('resolveAction', () => {
test('default action', () => expect(resolveAction({ defaultAction: 'link', isFolder: false, isPhoto: false })).toEqual('link'));
test('photo', () => expect(resolveAction({ defaultAction: 'link', isFolder: false, isPhoto: true })).toEqual('play'));
test('default action is "play" and is folder', () => expect(resolveAction({ defaultAction: 'play', isFolder: true, isPhoto: true })).toEqual('link'));
});
describe('resolveMixedShapeByAspectRatio', () => {
test('primary aspect ratio is >= 1.33', () => {
expect(resolveMixedShapeByAspectRatio(1.33)).toEqual('mixedBackdrop');
expect(resolveMixedShapeByAspectRatio(1.34)).toEqual('mixedBackdrop');
});
test('primary aspect ratio is > 0.71', () => {
expect(resolveMixedShapeByAspectRatio(0.72)).toEqual('mixedSquare');
expect(resolveMixedShapeByAspectRatio(0.73)).toEqual('mixedSquare');
expect(resolveMixedShapeByAspectRatio(1.32)).toEqual('mixedSquare');
});
test('primary aspect ratio is <= 0.71', () => {
expect(resolveMixedShapeByAspectRatio(0.71)).toEqual('mixedPortrait');
expect(resolveMixedShapeByAspectRatio(0.70)).toEqual('mixedPortrait');
expect(resolveMixedShapeByAspectRatio(0.01)).toEqual('mixedPortrait');
});
test('primary aspect ratio is not provided', () => {
expect(resolveMixedShapeByAspectRatio(undefined)).toEqual('mixedSquare');
expect(resolveMixedShapeByAspectRatio(null)).toEqual('mixedSquare');
});
});
describe('resolveCardCssClasses', () => {
test('card CSS classes', () => {
expect(resolveCardCssClasses({
cardCssClass: 'custom-class',
itemType: 'non-music',
showChildCountIndicator: false,
isTV: false,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card custom-class card-withuserdata');
});
test('card classes', () => {
expect(resolveCardCssClasses({
cardClass: 'custom-card',
itemType: 'non-music',
showChildCountIndicator: false,
isTV: false,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card custom-card card-withuserdata');
});
test('shape', () => {
expect(resolveCardCssClasses({
shape: 'portrait',
itemType: 'non-music',
showChildCountIndicator: false,
isTV: false,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card portraitCard card-withuserdata');
});
test('desktop', () => {
expect(resolveCardCssClasses({
itemType: 'non-music',
showChildCountIndicator: false,
isTV: false,
enableFocusTransform: false,
isDesktop: true
})
).toEqual('card card-hoverable card-withuserdata');
});
test('tv', () => {
expect(resolveCardCssClasses({
itemType: 'non-music',
showChildCountIndicator: false,
isTV: true,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card show-focus card-withuserdata');
});
test('tv with focus transform', () => {
expect(resolveCardCssClasses({
itemType: 'non-music',
showChildCountIndicator: false,
isTV: true,
enableFocusTransform: true,
isDesktop: false
})
).toEqual('card show-focus show-animation card-withuserdata');
});
test('non-music item type', () => {
expect(resolveCardCssClasses({
itemType: 'non-music',
showChildCountIndicator: false,
isTV: false,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card card-withuserdata');
});
test('music item type', () => {
expect(resolveCardCssClasses({
itemType: 'MusicAlbum',
showChildCountIndicator: false,
isTV: false,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card');
expect(resolveCardCssClasses({
itemType: 'MusicArtist',
showChildCountIndicator: false,
isTV: false,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card');
expect(resolveCardCssClasses({
itemType: 'Audio',
showChildCountIndicator: false,
isTV: false,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card');
});
test('child count indicator', () => {
expect(resolveCardCssClasses({
itemType: 'non-music',
showChildCountIndicator: true,
childCount: 5,
isTV: false,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card groupedCard card-withuserdata');
});
test('button tag name', () => {
expect(resolveCardCssClasses({
tagName: 'button',
itemType: 'non-music',
showChildCountIndicator: false,
isTV: false,
enableFocusTransform: false,
isDesktop: false
})
).toEqual('card card-withuserdata itemAction');
});
test('all', () => {
expect(resolveCardCssClasses({
shape: 'portrait',
cardCssClass: 'card-css',
cardClass: 'card',
itemType: 'non-music',
showChildCountIndicator: true,
childCount: 5,
tagName: 'button',
isTV: true,
enableFocusTransform: true,
isDesktop: true
})
).toEqual('card portraitCard card-css card-hoverable show-focus show-animation groupedCard card-withuserdata itemAction');
});
});
describe('resolveCardImageContainerCssClasses', () => {
test('with image URL, no cover image', () => {
expect(resolveCardImageContainerCssClasses({
itemType: '',
itemName: 'Movie Name',
imgUrl: 'https://jellyfin.org/some-image',
hasCoverImage: false
})).toEqual('cardImageContainer');
});
test('no cover image, no image URL', () => {
expect(resolveCardImageContainerCssClasses({
itemType: '',
itemName: 'Movie Name',
hasCoverImage: false
})).toEqual('cardImageContainer defaultCardBackground defaultCardBackground1');
});
test('with cover image, no image URL', () => {
expect(resolveCardImageContainerCssClasses({
itemType: '',
itemName: 'Movie Name',
hasCoverImage: true
})).toEqual('cardImageContainer coveredImage defaultCardBackground defaultCardBackground1');
});
test('with cover image, item type is TV channel, no image URL', () => {
expect(resolveCardImageContainerCssClasses({
itemType: 'TvChannel',
itemName: 'Movie Name',
hasCoverImage: true
})).toEqual('cardImageContainer coveredImage coveredImage-contain defaultCardBackground defaultCardBackground1');
});
});
describe('resolveCardBoxCssClasses', () => {
test('non-card layout', () => expect(resolveCardBoxCssClasses({ cardLayout: false, hasOuterCardFooter: false })).toEqual('cardBox'));
test('card layout', () => expect(resolveCardBoxCssClasses({ cardLayout: true, hasOuterCardFooter: false })).toEqual('cardBox visualCardBox'));
test('has outer card footer', () => expect(resolveCardBoxCssClasses({ cardLayout: false, hasOuterCardFooter: true })).toEqual('cardBox cardBox-bottompadded'));
});
describe('getDefaultBackgroundClass', () => {
test('no randomization string provided', () => {
for (let i = 0; i < 100; i++) {
const bgClass = getDefaultBackgroundClass();
const colorIndex = parseInt(bgClass.slice(bgClass.length - 1), 10);
expect(colorIndex).toBeGreaterThanOrEqual(1);
expect(colorIndex).toBeLessThanOrEqual(5);
expect(bgClass).toEqual(`defaultCardBackground defaultCardBackground${colorIndex}`);
}
});
test('randomization string provided', () => {
const generateRandomString = (stringLength: number): string => (Math.random() + 1).toString(36).substring(stringLength);
for (let i = 0; i < 100; i++) {
const randomString = generateRandomString(6);
const bgClass = getDefaultBackgroundClass(randomString);
const colorIndex = getDefaultColorIndex(randomString);
expect(bgClass).toEqual(`defaultCardBackground defaultCardBackground${colorIndex}`);
}
});
});
describe('getDefaultColorIndex', () => {
test('no randomization string provided', () => {
for (let i = 0; i < 100; i++) {
const colorIndex = getDefaultColorIndex();
expect(colorIndex).toBeGreaterThanOrEqual(1);
expect(colorIndex).toBeLessThanOrEqual(5);
}
});
test('randomization string provided', () => {
expect(getDefaultColorIndex('Movie name')).toEqual(1);
expect(getDefaultColorIndex('Mo')).toEqual(4);
expect(getDefaultColorIndex('Mov')).toEqual(4);
expect(getDefaultColorIndex('Movi')).toEqual(1);
expect(getDefaultColorIndex('Movie')).toEqual(1);
expect(getDefaultColorIndex('Movie ')).toEqual(2);
expect(getDefaultColorIndex('Movie n')).toEqual(2);
expect(getDefaultColorIndex('Movie na')).toEqual(3);
expect(getDefaultColorIndex('Movie nam')).toEqual(3);
expect(getDefaultColorIndex('Movie name')).toEqual(1);
expect(getDefaultColorIndex('TV show')).toEqual(3);
expect(getDefaultColorIndex('Music album')).toEqual(1);
expect(getDefaultColorIndex('Song')).toEqual(3);
expect(getDefaultColorIndex('Musical artist')).toEqual(1);
});
});

View file

@ -0,0 +1,316 @@
import { randomInt } from '../../utils/number';
import classNames from 'classnames';
const ASPECT_RATIOS = {
portrait: (2 / 3),
backdrop: (16 / 9),
square: 1,
banner: (1000 / 185)
};
/**
* Determines if the item is live TV.
* @param {string} itemType - Item type to use for the check.
* @returns {boolean} Flag showing if the item is live TV.
*/
export const isUsingLiveTvNaming = (itemType: string): boolean => itemType === 'Program' || itemType === 'Timer' || itemType === 'Recording';
/**
* Resolves Card action to display
* @param opts options to determine the action to return
*/
export const resolveAction = (opts: { defaultAction: string, isFolder: boolean, isPhoto: boolean }): string => {
if (opts.defaultAction === 'play' && opts.isFolder) {
// If this hard-coding is ever removed make sure to test nested photo albums
return 'link';
} else if (opts.isPhoto) {
return 'play';
} else {
return opts.defaultAction;
}
};
/**
* Checks if the window is resizable.
* @param {number} windowWidth - Width of the device's screen.
* @returns {boolean} - Result of the check.
*/
export const isResizable = (windowWidth: number): boolean => {
const screen = window.screen;
if (screen) {
const screenWidth = screen.availWidth;
if ((screenWidth - windowWidth) > 20) {
return true;
}
}
return false;
};
/**
* Resolves mixed shape based on aspect ratio
* @param primaryImageAspectRatio image aspect ratio that determines mixed shape
*/
export const resolveMixedShapeByAspectRatio = (primaryImageAspectRatio: number | null | undefined) => {
if (primaryImageAspectRatio === undefined || primaryImageAspectRatio === null) {
return 'mixedSquare';
}
if (primaryImageAspectRatio >= 1.33) {
return 'mixedBackdrop';
} else if (primaryImageAspectRatio > 0.71) {
return 'mixedSquare';
} else {
return 'mixedPortrait';
}
};
type CardCssClassOpts = {
shape?: string,
cardCssClass?: string,
cardClass?: string,
tagName?: string,
itemType: string,
childCount?: number,
showChildCountIndicator: boolean,
isTV: boolean,
enableFocusTransform: boolean,
isDesktop: boolean
};
/**
* Resolves applicable Card CSS classes
* @param opts options for determining which CSS classes are applicable
*/
export const resolveCardCssClasses = (opts: CardCssClassOpts): string => {
return classNames({
'card': true,
[`${opts.shape}Card`]: opts.shape,
[`${opts.cardCssClass}`]: opts.cardCssClass,
[`${opts.cardClass}`]: opts.cardClass,
'card-hoverable': opts.isDesktop,
'show-focus': opts.isTV,
'show-animation': opts.isTV && opts.enableFocusTransform,
'groupedCard': opts.showChildCountIndicator && opts.childCount,
'card-withuserdata': !['MusicAlbum', 'MusicArtist', 'Audio'].includes(opts.itemType),
'itemAction': opts.tagName === 'button'
});
};
/**
* Resolves applicable Card Image container CSS classes
* @param opts options for determining which CSS classes are applicable
*/
export const resolveCardImageContainerCssClasses = (opts: { itemType: string, hasCoverImage: boolean, itemName?: string, imgUrl?: string}): string => {
return classNames({
'cardImageContainer': true,
'coveredImage': opts.hasCoverImage,
'coveredImage-contain': opts.hasCoverImage && opts.itemType === 'TvChannel',
[getDefaultBackgroundClass(opts.itemName)]: !opts.imgUrl
});
};
/**
* Resolves applicable Card Box CSS classes
* @param opts options for determining which CSS classes are applicable
*/
export const resolveCardBoxCssClasses = (opts: { cardLayout: boolean, hasOuterCardFooter: boolean }): string => {
return classNames({
'cardBox': true,
'visualCardBox': opts.cardLayout,
'cardBox-bottompadded': opts.hasOuterCardFooter && !opts.cardLayout
});
};
/**
* Returns the default background class for a card based on a string.
* @param {?string} [str] - Text used to generate the background class.
* @returns {string} CSS classes for default card backgrounds.
*/
export const getDefaultBackgroundClass = (str?: string | null): string => `defaultCardBackground defaultCardBackground${getDefaultColorIndex(str)}`;
/**
* Generates an index used to select the default color of a card based on a string.
* @param {?string} [str] - String to use for generating the index.
* @returns {number} Index of the color.
*/
export const getDefaultColorIndex = (str?: string | null): number => {
const numRandomColors = 5;
if (str) {
const charIndex = Math.floor(str.length / 2);
const character = String(str.slice(charIndex, charIndex + 1).charCodeAt(0));
let sum = 0;
for (let i = 0; i < character.length; i++) {
sum += parseInt(character.charAt(i), 10);
}
const index = parseInt(String(sum).slice(-1), 10);
return (index % numRandomColors) + 1;
} else {
return randomInt(1, numRandomColors);
}
};
/**
* Computes the aspect ratio for a card given its shape.
* @param {string} shape - Shape for which to get the aspect ratio.
* @returns {null|number} Ratio of the shape.
*/
export const getDesiredAspect = (shape: string | null | undefined): null | number => {
if (!shape) {
return null;
}
shape = shape.toLowerCase();
if (shape.indexOf('portrait') !== -1) {
return ASPECT_RATIOS.portrait;
}
if (shape.indexOf('backdrop') !== -1) {
return ASPECT_RATIOS.backdrop;
}
if (shape.indexOf('square') !== -1) {
return ASPECT_RATIOS.square;
}
if (shape.indexOf('banner') !== -1) {
return ASPECT_RATIOS.banner;
}
return null;
};
/**
* Computes the number of posters per row.
* @param {string} shape - Shape of the cards.
* @param {number} screenWidth - Width of the screen.
* @param {boolean} isOrientationLandscape - Flag for the orientation of the screen.
* @param {boolean} isTV - Flag to denote if posters are rendered on a television screen.
* @returns {number} Number of cards per row for an itemsContainer.
*/
export const getPostersPerRow = (shape: string, screenWidth: number, isOrientationLandscape: boolean, isTV: boolean): number => {
switch (shape) {
case 'portrait': return postersPerRowPortrait(screenWidth, isTV);
case 'square': return postersPerRowSquare(screenWidth, isTV);
case 'banner': return postersPerRowBanner(screenWidth);
case 'backdrop': return postersPerRowBackdrop(screenWidth, isTV);
case 'smallBackdrop': return postersPerRowSmallBackdrop(screenWidth);
case 'overflowSmallBackdrop': return postersPerRowOverflowSmallBackdrop(screenWidth, isOrientationLandscape, isTV);
case 'overflowPortrait': return postersPerRowOverflowPortrait(screenWidth, isOrientationLandscape, isTV);
case 'overflowSquare': return postersPerRowOverflowSquare(screenWidth, isOrientationLandscape, isTV);
case 'overflowBackdrop': return postersPerRowOverflowBackdrop(screenWidth, isOrientationLandscape, isTV);
default: return 4;
}
};
const postersPerRowPortrait = (screenWidth: number, isTV: boolean) => {
switch (true) {
case isTV: return 100 / 16.66666667;
case screenWidth >= 2200: return 10;
case screenWidth >= 1920: return 100 / 11.1111111111;
case screenWidth >= 1600: return 8;
case screenWidth >= 1400: return 100 / 14.28571428571;
case screenWidth >= 1200: return 100 / 16.66666667;
case screenWidth >= 800: return 5;
case screenWidth >= 700: return 4;
case screenWidth >= 500: return 100 / 33.33333333;
default: return 100 / 33.33333333;
}
};
const postersPerRowSquare = (screenWidth: number, isTV: boolean) => {
switch (true) {
case isTV: return 100 / 16.66666667;
case screenWidth >= 2200: return 10;
case screenWidth >= 1920: return 100 / 11.1111111111;
case screenWidth >= 1600: return 8;
case screenWidth >= 1400: return 100 / 14.28571428571;
case screenWidth >= 1200: return 100 / 16.66666667;
case screenWidth >= 800: return 5;
case screenWidth >= 700: return 4;
case screenWidth >= 500: return 100 / 33.33333333;
default: return 2;
}
};
const postersPerRowBanner = (screenWidth: number) => {
switch (true) {
case screenWidth >= 2200: return 4;
case screenWidth >= 1200: return 100 / 33.33333333;
case screenWidth >= 800: return 2;
default: return 1;
}
};
const postersPerRowBackdrop = (screenWidth: number, isTV: boolean) => {
switch (true) {
case isTV: return 4;
case screenWidth >= 2500: return 6;
case screenWidth >= 1600: return 5;
case screenWidth >= 1200: return 4;
case screenWidth >= 770: return 3;
case screenWidth >= 420: return 2;
default: return 1;
}
};
const postersPerRowSmallBackdrop = (screenWidth: number) => {
switch (true) {
case screenWidth >= 1600: return 8;
case screenWidth >= 1400: return 100 / 14.2857142857;
case screenWidth >= 1200: return 100 / 16.66666667;
case screenWidth >= 1000: return 5;
case screenWidth >= 800: return 4;
case screenWidth >= 500: return 100 / 33.33333333;
default: return 2;
}
};
const postersPerRowOverflowSmallBackdrop = (screenWidth: number, isLandscape: boolean, isTV: boolean) => {
switch (true) {
case isTV: return 100 / 18.9;
case isLandscape && screenWidth >= 800: return 100 / 15.5;
case isLandscape: return 100 / 23.3;
case screenWidth >= 540: return 100 / 30;
default: return 100 / 72;
}
};
const postersPerRowOverflowPortrait = (screenWidth: number, isLandscape: boolean, isTV: boolean) => {
switch (true) {
case isTV: return 100 / 15.5;
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
case isLandscape: return 100 / 15.5;
case screenWidth >= 1400: return 100 / 15;
case screenWidth >= 1200: return 100 / 18;
case screenWidth >= 760: return 100 / 23;
case screenWidth >= 400: return 100 / 31.5;
default: return 100 / 42;
}
};
const postersPerRowOverflowSquare = (screenWidth: number, isLandscape: boolean, isTV: boolean) => {
switch (true) {
case isTV: return 100 / 15.5;
case isLandscape && screenWidth >= 1700: return 100 / 11.6;
case isLandscape: return 100 / 15.5;
case screenWidth >= 1400: return 100 / 15;
case screenWidth >= 1200: return 100 / 18;
case screenWidth >= 760: return 100 / 23;
case screenWidth >= 540: return 100 / 31.5;
default: return 100 / 42;
}
};
const postersPerRowOverflowBackdrop = (screenWidth: number, isLandscape: boolean, isTV: boolean) => {
switch (true) {
case isTV: return 100 / 23.3;
case isLandscape && screenWidth >= 1700: return 100 / 18.5;
case isLandscape: return 100 / 23.3;
case screenWidth >= 1800: return 100 / 23.5;
case screenWidth >= 1400: return 100 / 30;
case screenWidth >= 760: return 100 / 40;
case screenWidth >= 640: return 100 / 56;
default: return 100 / 72;
}
};

View file

@ -3,9 +3,9 @@ import React, { FunctionComponent } from 'react';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale'; import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
import cardBuilder from '../../cardbuilder/cardBuilder';
import IconButtonElement from '../../../elements/IconButtonElement'; import IconButtonElement from '../../../elements/IconButtonElement';
import escapeHTML from 'escape-html'; import escapeHTML from 'escape-html';
import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils';
const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({
__html: `<a __html: `<a
@ -56,7 +56,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
const renderImgUrl = imgUrl ? const renderImgUrl = imgUrl ?
`<div class='${imageClass}' style='background-image:url(${imgUrl})'></div>` : `<div class='${imageClass}' style='background-image:url(${imgUrl})'></div>` :
`<div class='${imageClass} ${cardBuilder.getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center'> `<div class='${imageClass} ${getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center'>
<span class='material-icons cardImageIcon person' aria-hidden='true'></span> <span class='material-icons cardImageIcon person' aria-hidden='true'></span>
</div>`; </div>`;

View file

@ -16,6 +16,7 @@ import './listview.scss';
import '../../elements/emby-ratingbutton/emby-ratingbutton'; import '../../elements/emby-ratingbutton/emby-ratingbutton';
import '../../elements/emby-playstatebutton/emby-playstatebutton'; import '../../elements/emby-playstatebutton/emby-playstatebutton';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
function getIndex(item, options) { function getIndex(item, options) {
if (options.index === 'disc') { if (options.index === 'disc') {
@ -279,7 +280,7 @@ export function getListViewHtml(options) {
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 + '" item-icon>';
} else { } else {
html += '<div class="' + imageClass + ' cardImageContainer ' + cardBuilder.getDefaultBackgroundClass(item.Name) + '">' + cardBuilder.getDefaultText(item, options); html += '<div class="' + imageClass + ' cardImageContainer ' + getDefaultBackgroundClass(item.Name) + '">' + cardBuilder.getDefaultText(item, options);
} }
const mediaSourceCount = item.MediaSourceCount || 1; const mediaSourceCount = item.MediaSourceCount || 1;

View file

@ -10,7 +10,6 @@ import { appHost } from '../apphost';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import * as userSettings from '../../scripts/settings/userSettings'; import * as userSettings from '../../scripts/settings/userSettings';
import cardBuilder from '../cardbuilder/cardBuilder';
import itemContextMenu from '../itemContextMenu'; import itemContextMenu from '../itemContextMenu';
import '../cardbuilder/card.scss'; import '../cardbuilder/card.scss';
import '../../elements/emby-itemscontainer/emby-itemscontainer'; import '../../elements/emby-itemscontainer/emby-itemscontainer';
@ -19,6 +18,7 @@ import '../../elements/emby-ratingbutton/emby-ratingbutton';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
import toast from '../toast/toast'; import toast from '../toast/toast';
import { appRouter } from '../router/appRouter'; import { appRouter } from '../router/appRouter';
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
let showMuteButton = true; let showMuteButton = true;
let showVolumeSlider = true; let showVolumeSlider = true;
@ -248,7 +248,7 @@ function setImageUrl(context, state, url) {
context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImageAudio', item.Type === 'Audio'); context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImageAudio', item.Type === 'Audio');
context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImagePoster', item.Type !== 'Audio'); context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImagePoster', item.Type !== 'Audio');
} else { } else {
imgContainer.innerHTML = '<div class="nowPlayingPageImageContainerNoAlbum"><button data-action="link" class="cardImageContainer coveredImage ' + cardBuilder.getDefaultBackgroundClass(item.Name) + ' cardContent cardContent-shadow itemAction"><span class="cardImageIcon material-icons album" aria-hidden="true"></span></button></div>'; imgContainer.innerHTML = '<div class="nowPlayingPageImageContainerNoAlbum"><button data-action="link" class="cardImageContainer coveredImage ' + getDefaultBackgroundClass(item.Name) + ' cardContent cardContent-shadow itemAction"><span class="cardImageIcon material-icons album" aria-hidden="true"></span></button></div>';
} }
} }

View file

@ -23,6 +23,7 @@ import Dashboard from '../../utils/dashboard';
import ServerConnections from '../../components/ServerConnections'; import ServerConnections from '../../components/ServerConnections';
import alert from '../../components/alert'; import alert from '../../components/alert';
import confirm from '../../components/confirm/confirm'; import confirm from '../../components/confirm/confirm';
import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils';
function showPlaybackInfo(btn, session) { function showPlaybackInfo(btn, session) {
let title; let title;
@ -259,7 +260,7 @@ function renderActiveConnections(view, sessions) {
html += '<div class="cardBox visualCardBox">'; html += '<div class="cardBox visualCardBox">';
html += '<div class="cardScalable visualCardBox-cardScalable">'; html += '<div class="cardScalable visualCardBox-cardScalable">';
html += '<div class="cardPadder cardPadder-backdrop"></div>'; html += '<div class="cardPadder cardPadder-backdrop"></div>';
html += `<div class="cardContent ${cardBuilder.getDefaultBackgroundClass()}">`; html += `<div class="cardContent ${getDefaultBackgroundClass()}">`;
if (imgUrl) { if (imgUrl) {
html += '<div class="sessionNowPlayingContent sessionNowPlayingContent-withbackground"'; html += '<div class="sessionNowPlayingContent sessionNowPlayingContent-withbackground"';

View file

@ -1,5 +1,4 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import cardBuilder from '../../../components/cardbuilder/cardBuilder';
import loading from '../../../components/loading/loading'; import loading from '../../../components/loading/loading';
import dom from '../../../scripts/dom'; import dom from '../../../scripts/dom';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
@ -11,6 +10,7 @@ import '../../../elements/emby-itemscontainer/emby-itemscontainer';
import '../../../components/cardbuilder/card.scss'; import '../../../components/cardbuilder/card.scss';
import Dashboard from '../../../utils/dashboard'; import Dashboard from '../../../utils/dashboard';
import confirm from '../../../components/confirm/confirm'; import confirm from '../../../components/confirm/confirm';
import { getDefaultBackgroundClass } from '../../../components/cardbuilder/cardBuilderUtils';
// Local cache of loaded // Local cache of loaded
let deviceIds = []; let deviceIds = [];
@ -94,7 +94,7 @@ function load(page, devices) {
deviceHtml += '<div class="cardBox visualCardBox">'; deviceHtml += '<div class="cardBox visualCardBox">';
deviceHtml += '<div class="cardScalable">'; deviceHtml += '<div class="cardScalable">';
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>'; deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
deviceHtml += `<a is="emby-linkbutton" href="#/dashboard/devices/edit?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`; deviceHtml += `<a is="emby-linkbutton" href="#/dashboard/devices/edit?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${getDefaultBackgroundClass()}">`;
// audit note: getDeviceIcon returns static text // audit note: getDeviceIcon returns static text
const iconUrl = imageHelper.getDeviceIcon(device); const iconUrl = imageHelper.getDeviceIcon(device);

View file

@ -10,7 +10,7 @@ import '../../components/cardbuilder/card.scss';
import '../../elements/emby-itemrefreshindicator/emby-itemrefreshindicator'; import '../../elements/emby-itemrefreshindicator/emby-itemrefreshindicator';
import Dashboard, { pageClassOn, pageIdOn } from '../../utils/dashboard'; import Dashboard, { pageClassOn, pageIdOn } from '../../utils/dashboard';
import confirm from '../../components/confirm/confirm'; import confirm from '../../components/confirm/confirm';
import cardBuilder from '../../components/cardbuilder/cardBuilder'; import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils';
function addVirtualFolder(page) { function addVirtualFolder(page) {
import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: MediaLibraryCreator }) => { import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: MediaLibraryCreator }) => {
@ -275,11 +275,11 @@ function getVirtualFolderHtml(page, virtualFolder, index) {
let hasCardImageContainer; let hasCardImageContainer;
if (imgUrl) { if (imgUrl) {
html += `<div class="cardImageContainer editLibrary ${imgUrl ? '' : cardBuilder.getDefaultBackgroundClass()}" style="cursor:pointer">`; html += `<div class="cardImageContainer editLibrary ${imgUrl ? '' : getDefaultBackgroundClass()}" style="cursor:pointer">`;
html += `<img src="${imgUrl}" style="width:100%" />`; html += `<img src="${imgUrl}" style="width:100%" />`;
hasCardImageContainer = true; hasCardImageContainer = true;
} else if (!virtualFolder.showNameWithIcon) { } else if (!virtualFolder.showNameWithIcon) {
html += `<div class="cardImageContainer editLibrary ${cardBuilder.getDefaultBackgroundClass()}" style="cursor:pointer;">`; html += `<div class="cardImageContainer editLibrary ${getDefaultBackgroundClass()}" style="cursor:pointer;">`;
html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>'; html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>';
hasCardImageContainer = true; hasCardImageContainer = true;
} }

View file

@ -3,11 +3,11 @@ import escapeHTML from 'escape-html';
import loading from '../../../../components/loading/loading'; import loading from '../../../../components/loading/loading';
import libraryMenu from '../../../../scripts/libraryMenu'; import libraryMenu from '../../../../scripts/libraryMenu';
import globalize from '../../../../scripts/globalize'; import globalize from '../../../../scripts/globalize';
import * as cardBuilder from '../../../../components/cardbuilder/cardBuilder.js';
import '../../../../components/cardbuilder/card.scss'; import '../../../../components/cardbuilder/card.scss';
import '../../../../elements/emby-button/emby-button'; import '../../../../elements/emby-button/emby-button';
import '../../../../elements/emby-checkbox/emby-checkbox'; import '../../../../elements/emby-checkbox/emby-checkbox';
import '../../../../elements/emby-select/emby-select'; import '../../../../elements/emby-select/emby-select';
import { getDefaultBackgroundClass } from '../../../../components/cardbuilder/cardBuilderUtils';
function reloadList(page) { function reloadList(page) {
loading.show(); loading.show();
@ -137,7 +137,7 @@ function getPluginHtml(plugin, options, installedPlugins) {
if (plugin.imageUrl) { if (plugin.imageUrl) {
html += `<img src="${escapeHTML(plugin.imageUrl)}" style="width:100%" />`; html += `<img src="${escapeHTML(plugin.imageUrl)}" style="width:100%" />`;
} else { } else {
html += `<div class="cardImage flex align-items-center justify-content-center ${cardBuilder.getDefaultBackgroundClass()}">`; html += `<div class="cardImage flex align-items-center justify-content-center ${getDefaultBackgroundClass()}">`;
html += '<span class="cardImageIcon material-icons extension" aria-hidden="true"></span>'; html += '<span class="cardImageIcon material-icons extension" aria-hidden="true"></span>';
html += '</div>'; html += '</div>';
} }

View file

@ -2,11 +2,11 @@ import loading from '../../../../components/loading/loading';
import libraryMenu from '../../../../scripts/libraryMenu'; import libraryMenu from '../../../../scripts/libraryMenu';
import dom from '../../../../scripts/dom'; import dom from '../../../../scripts/dom';
import globalize from '../../../../scripts/globalize'; import globalize from '../../../../scripts/globalize';
import * as cardBuilder from '../../../../components/cardbuilder/cardBuilder.js';
import '../../../../components/cardbuilder/card.scss'; import '../../../../components/cardbuilder/card.scss';
import '../../../../elements/emby-button/emby-button'; import '../../../../elements/emby-button/emby-button';
import Dashboard, { pageIdOn } from '../../../../utils/dashboard'; import Dashboard, { pageIdOn } from '../../../../utils/dashboard';
import confirm from '../../../../components/confirm/confirm'; import confirm from '../../../../components/confirm/confirm';
import { getDefaultBackgroundClass } from '../../../../components/cardbuilder/cardBuilderUtils';
function deletePlugin(page, uniqueid, version, name) { function deletePlugin(page, uniqueid, version, name) {
const msg = globalize.translate('UninstallPluginConfirmation', name); const msg = globalize.translate('UninstallPluginConfirmation', name);
@ -73,7 +73,7 @@ function getPluginCardHtml(plugin, pluginConfigurationPages) {
const imageUrl = ApiClient.getUrl(`/Plugins/${plugin.Id}/${plugin.Version}/Image`); const imageUrl = ApiClient.getUrl(`/Plugins/${plugin.Id}/${plugin.Version}/Image`);
html += `<img src="${imageUrl}" style="width:100%" />`; html += `<img src="${imageUrl}" style="width:100%" />`;
} else { } else {
html += `<div class="cardImage flex align-items-center justify-content-center ${cardBuilder.getDefaultBackgroundClass()}">`; html += `<div class="cardImage flex align-items-center justify-content-center ${getDefaultBackgroundClass()}">`;
html += '<span class="cardImageIcon material-icons extension" aria-hidden="true"></span>'; html += '<span class="cardImageIcon material-icons extension" aria-hidden="true"></span>';
html += '</div>'; html += '</div>';
} }

View file

@ -2,7 +2,6 @@ import 'jquery';
import globalize from '../scripts/globalize'; import globalize from '../scripts/globalize';
import taskButton from '../scripts/taskbutton'; import taskButton from '../scripts/taskbutton';
import dom from '../scripts/dom'; import dom from '../scripts/dom';
import cardBuilder from '../components/cardbuilder/cardBuilder';
import layoutManager from '../components/layoutManager'; import layoutManager from '../components/layoutManager';
import loading from '../components/loading/loading'; import loading from '../components/loading/loading';
import browser from '../scripts/browser'; import browser from '../scripts/browser';
@ -14,6 +13,7 @@ import 'material-design-icons-iconfont';
import '../elements/emby-button/emby-button'; import '../elements/emby-button/emby-button';
import Dashboard from '../utils/dashboard'; import Dashboard from '../utils/dashboard';
import confirm from '../components/confirm/confirm'; import confirm from '../components/confirm/confirm';
import { getDefaultBackgroundClass } from '../components/cardbuilder/cardBuilderUtils';
const enableFocusTransform = !browser.slow && !browser.edge; const enableFocusTransform = !browser.slow && !browser.edge;
@ -38,7 +38,7 @@ function getDeviceHtml(device) {
html += '<div class="cardScalable visualCardBox-cardScalable">'; html += '<div class="cardScalable visualCardBox-cardScalable">';
html += '<div class="' + padderClass + '"></div>'; html += '<div class="' + padderClass + '"></div>';
html += '<div class="cardContent searchImage">'; html += '<div class="cardContent searchImage">';
html += `<div class="cardImageContainer coveredImage ${cardBuilder.getDefaultBackgroundClass()}"><span class="cardImageIcon material-icons dvr" aria-hidden="true"></span></div>`; html += `<div class="cardImageContainer coveredImage ${getDefaultBackgroundClass()}"><span class="cardImageIcon material-icons dvr" aria-hidden="true"></span></div>`;
html += '</div>'; html += '</div>';
html += '</div>'; html += '</div>';
html += '<div class="cardFooter visualCardBox-cardFooter">'; html += '<div class="cardFooter visualCardBox-cardFooter">';

View file

@ -15,8 +15,8 @@ import ServerConnections from '../../../components/ServerConnections';
import toast from '../../../components/toast/toast'; import toast from '../../../components/toast/toast';
import dialogHelper from '../../../components/dialogHelper/dialogHelper'; import dialogHelper from '../../../components/dialogHelper/dialogHelper';
import baseAlert from '../../../components/alert'; import baseAlert from '../../../components/alert';
import cardBuilder from '../../../components/cardbuilder/cardBuilder';
import './login.scss'; import './login.scss';
import { getDefaultBackgroundClass } from '../../../components/cardbuilder/cardBuilderUtils';
const enableFocusTransform = !browser.slow && !browser.edge; const enableFocusTransform = !browser.slow && !browser.edge;
@ -164,7 +164,7 @@ function loadUserList(context, apiClient, users) {
html += '<div class="cardImageContainer coveredImage" style="background-image:url(\'' + imgUrl + "');\"></div>"; html += '<div class="cardImageContainer coveredImage" style="background-image:url(\'' + imgUrl + "');\"></div>";
} else { } else {
html += `<div class="cardImage flex align-items-center justify-content-center ${cardBuilder.getDefaultBackgroundClass()}">`; html += `<div class="cardImage flex align-items-center justify-content-center ${getDefaultBackgroundClass()}">`;
html += '<span class="material-icons cardImageIcon person" aria-hidden="true"></span>'; html += '<span class="material-icons cardImageIcon person" aria-hidden="true"></span>';
html += '</div>'; html += '</div>';
} }

View file

@ -18,8 +18,8 @@ import '../../../elements/emby-button/emby-button';
import Dashboard from '../../../utils/dashboard'; import Dashboard from '../../../utils/dashboard';
import ServerConnections from '../../../components/ServerConnections'; import ServerConnections from '../../../components/ServerConnections';
import alert from '../../../components/alert'; import alert from '../../../components/alert';
import cardBuilder from '../../../components/cardbuilder/cardBuilder';
import { ConnectionState } from '../../../utils/jellyfin-apiclient/ConnectionState.ts'; import { ConnectionState } from '../../../utils/jellyfin-apiclient/ConnectionState.ts';
import { getDefaultBackgroundClass } from '../../../components/cardbuilder/cardBuilderUtils';
const enableFocusTransform = !browser.slow && !browser.edge; const enableFocusTransform = !browser.slow && !browser.edge;
@ -56,7 +56,7 @@ function renderSelectServerItems(view, servers) {
cardContainer += '<div class="cardPadder cardPadder-square">'; cardContainer += '<div class="cardPadder cardPadder-square">';
cardContainer += '</div>'; cardContainer += '</div>';
cardContainer += '<div class="cardContent">'; cardContainer += '<div class="cardContent">';
cardContainer += `<div class="cardImageContainer coveredImage ${cardBuilder.getDefaultBackgroundClass()}">`; cardContainer += `<div class="cardImageContainer coveredImage ${getDefaultBackgroundClass()}">`;
cardContainer += cardImageContainer; cardContainer += cardImageContainer;
cardContainer += '</div>'; cardContainer += '</div>';
cardContainer += '</div>'; cardContainer += '</div>';