From c8a7c7040ad92e2dc42d3cee462d6752a929aad9 Mon Sep 17 00:00:00 2001 From: Dmitriy Dubson Date: Sun, 8 Oct 2023 11:46:49 -0400 Subject: [PATCH] Reduce cognitive complexity in card builder component --- src/components/cardbuilder/cardBuilder.js | 195 +++------ .../cardbuilder/cardBuilderUtils.js | 173 -------- ...Utils.test.js => cardBuilderUtils.test.ts} | 398 +++++++++++++++--- .../cardbuilder/cardBuilderUtils.ts | 316 ++++++++++++++ .../dashboard/users/UserCardBox.tsx | 4 +- src/components/listview/listview.js | 3 +- src/components/remotecontrol/remotecontrol.js | 4 +- src/controllers/dashboard/dashboard.js | 3 +- src/controllers/dashboard/devices/devices.js | 4 +- src/controllers/dashboard/library.js | 6 +- .../dashboard/plugins/available/index.js | 4 +- .../dashboard/plugins/installed/index.js | 4 +- src/controllers/livetvstatus.js | 4 +- src/controllers/session/login/index.js | 4 +- src/controllers/session/selectServer/index.js | 4 +- 15 files changed, 735 insertions(+), 391 deletions(-) delete mode 100644 src/components/cardbuilder/cardBuilderUtils.js rename src/components/cardbuilder/{cardBuilderUtils.test.js => cardBuilderUtils.test.ts} (52%) create mode 100644 src/components/cardbuilder/cardBuilderUtils.ts diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index c1c746006..8c746554d 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -6,14 +6,12 @@ import escapeHtml from 'escape-html'; -import cardBuilderUtils from './cardBuilderUtils'; import browser from 'scripts/browser'; import datetime from 'scripts/datetime'; import dom from 'scripts/dom'; import globalize from 'scripts/globalize'; import { getBackdropShape, getPortraitShape, getSquareShape } from 'utils/card'; import imageHelper from 'utils/image'; -import { randomInt } from 'utils/number'; import focusManager from '../focusManager'; import imageLoader from '../images/imageLoader'; @@ -29,6 +27,17 @@ import 'elements/emby-button/paper-icon-button-light'; import './card.scss'; import '../guide/programs.scss'; +import { + getDesiredAspect, + getPostersPerRow, + isResizable, + isUsingLiveTvNaming, + resolveAction, + resolveCardBoxCssClasses, + resolveCardCssClasses, + resolveCardImageContainerCssClasses, + resolveMixedShapeByAspectRatio +} from './cardBuilderUtils'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -47,24 +56,6 @@ export function getCardsHtml(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. * @param {string} shape - Shape of the card. @@ -73,7 +64,7 @@ function isResizable(windowWidth) { * @returns {number} Width of the image for a card. */ 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); } @@ -113,7 +104,7 @@ function setCardData(items, options) { options.preferThumb = options.shape === 'backdrop' || options.shape === 'overflowBackdrop'; } - options.uiAspect = cardBuilderUtils.getDesiredAspect(options.shape); + options.uiAspect = getDesiredAspect(options.shape); options.primaryImageAspectRatio = primaryImageAspectRatio; if (!options.width && options.widths) { @@ -280,7 +271,7 @@ function getCardImageUrl(item, apiClient, options, shape) { let imgUrl = null; let imgTag = null; let coverImage = false; - const uiAspect = cardBuilderUtils.getDesiredAspect(shape); + const uiAspect = getDesiredAspect(shape); let imgType = 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. * @param {Array} lines - Array containing the text lines. @@ -487,15 +455,6 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout 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. * @param {object} item - Item used to generate the air time text. @@ -574,7 +533,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, } else { lines.push(escapeHtml(item.SeriesName)); } - } else if (isUsingLiveTvNaming(item)) { + } else if (isUsingLiveTvNaming(item.Type)) { lines.push(escapeHtml(item.Name)); if (!item.EpisodeTitle && !item.IndexNumber) { @@ -616,7 +575,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, item.AlbumArtists[0].IsFolder = true; lines.push(getTextActionButton(item.AlbumArtists[0], null, serverId)); } 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. * @param {number} index - Index of the card @@ -906,87 +856,32 @@ export function getDefaultBackgroundClass(str) { * @returns {string} HTML markup for the generated card. */ function buildCard(index, item, apiClient, options) { - let action = options.action || 'link'; - - if (action === 'play' && item.IsFolder) { - // If this hard-coding is ever removed make sure to test nested photo albums - action = 'link'; - } else if (item.MediaType === 'Photo') { - action = 'play'; - } + const action = resolveAction({ + defaultAction: options.action || 'link', + isFolder: item.IsFolder, + isPhoto: item.MediaType === 'Photo' + }); let shape = options.shape; if (shape === 'mixed') { - shape = null; - - 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'; + shape = resolveMixedShapeByAspectRatio(item.PrimaryImageAspectRatio); } // 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 imgUrl = imgInfo.imgUrl; const blurhash = imgInfo.blurhash; - const forceName = imgInfo.forceName; - const overlayText = options.overlayText; - let cardImageContainerClass = 'cardImageContainer'; - const coveredImage = options.coverImage || imgInfo.coverImage; - - if (coveredImage) { - cardImageContainerClass += ' coveredImage'; - - if (item.Type === 'TvChannel') { - cardImageContainerClass += ' coveredImage-contain'; - } - } - - if (!imgUrl) { - cardImageContainerClass += ' ' + getDefaultBackgroundClass(item.Name); - } - - let cardBoxClass = options.cardLayout ? 'cardBox visualCardBox' : 'cardBox'; + const cardImageContainerClasses = resolveCardImageContainerCssClasses({ + itemType: item.Type, + itemName: item.Name, + hasCoverImage: options.coverImage || imgInfo.coverImage, + imgUrl + }); let footerCssClass; 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 }); } - if (outerCardFooter && !options.cardLayout) { - cardBoxClass += ' cardBox-bottompadded'; - } + const cardBoxClass = resolveCardBoxCssClasses({ + hasOuterCardFooter: outerCardFooter.length > 0, + cardLayout: options.cardLayout + }); let overlayButtons = ''; 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 let cardImageContainerOpen; let cardImageContainerClose = ''; @@ -1092,7 +984,7 @@ function buildCard(index, item, apiClient, options) { if (layoutManager.tv) { // Don't use the IMG tag with safari because it puts a white border around it - cardImageContainerOpen = imgUrl ? ('
') : ('
'); + cardImageContainerOpen = imgUrl ? ('
') : ('
'); cardImageContainerClose = '
'; } else { @@ -1100,7 +992,7 @@ function buildCard(index, item, apiClient, options) { const url = appRouter.getRouteUrl(item); // Don't use the IMG tag with safari because it puts a white border around it - cardImageContainerOpen = imgUrl ? ('') : (''); + cardImageContainerOpen = imgUrl ? ('') : (''); cardImageContainerClose = ''; } @@ -1178,16 +1070,24 @@ function buildCard(index, item, apiClient, options) { let ariaLabelAttribute = ''; if (tagName === 'button') { - className += ' itemAction'; actionAttribute = ' data-action="' + action + '"'; ariaLabelAttribute = ` aria-label="${escapeHtml(item.Name)}"`; } else { actionAttribute = ''; } - if (item.Type !== 'MusicAlbum' && item.Type !== 'MusicArtist' && item.Type !== 'Audio') { - className += ' card-withuserdata'; - } + const className = resolveCardCssClasses({ + 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 collectionIdData = options.collectionId ? (' data-collectionid="' + options.collectionId + '"') : ''; @@ -1296,7 +1196,7 @@ export function getDefaultText(item, options) { return ''; } - const defaultName = isUsingLiveTvNaming(item) ? item.Name : itemHelper.getDisplayName(item); + const defaultName = isUsingLiveTvNaming(item.Type) ? item.Name : itemHelper.getDisplayName(item); return '
' + escapeHtml(defaultName) + '
'; } @@ -1515,7 +1415,6 @@ export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) { export default { getCardsHtml: getCardsHtml, - getDefaultBackgroundClass: getDefaultBackgroundClass, getDefaultText: getDefaultText, buildCards: buildCards, onUserDataChanged: onUserDataChanged, diff --git a/src/components/cardbuilder/cardBuilderUtils.js b/src/components/cardbuilder/cardBuilderUtils.js deleted file mode 100644 index 494dcaf64..000000000 --- a/src/components/cardbuilder/cardBuilderUtils.js +++ /dev/null @@ -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 -}; diff --git a/src/components/cardbuilder/cardBuilderUtils.test.js b/src/components/cardbuilder/cardBuilderUtils.test.ts similarity index 52% rename from src/components/cardbuilder/cardBuilderUtils.test.js rename to src/components/cardbuilder/cardBuilderUtils.test.ts index 46599135d..501a395f9 100644 --- a/src/components/cardbuilder/cardBuilderUtils.test.js +++ b/src/components/cardbuilder/cardBuilderUtils.test.ts @@ -1,47 +1,52 @@ 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', () => { test('"portrait" (case insensitive)', () => { - expect(cardBuilderUtils.getDesiredAspect('portrait')).toEqual((2 / 3)); - expect(cardBuilderUtils.getDesiredAspect('PorTRaIt')).toEqual((2 / 3)); + expect(getDesiredAspect('portrait')).toEqual((2 / 3)); + expect(getDesiredAspect('PorTRaIt')).toEqual((2 / 3)); }); test('"backdrop" (case insensitive)', () => { - expect(cardBuilderUtils.getDesiredAspect('backdrop')).toEqual((16 / 9)); - expect(cardBuilderUtils.getDesiredAspect('BaCkDroP')).toEqual((16 / 9)); + expect(getDesiredAspect('backdrop')).toEqual((16 / 9)); + expect(getDesiredAspect('BaCkDroP')).toEqual((16 / 9)); }); test('"square" (case insensitive)', () => { - expect(cardBuilderUtils.getDesiredAspect('square')).toEqual(1); - expect(cardBuilderUtils.getDesiredAspect('sQuArE')).toEqual(1); + expect(getDesiredAspect('square')).toEqual(1); + expect(getDesiredAspect('sQuArE')).toEqual(1); }); test('"banner" (case insensitive)', () => { - expect(cardBuilderUtils.getDesiredAspect('banner')).toEqual((1000 / 185)); - expect(cardBuilderUtils.getDesiredAspect('BaNnEr')).toEqual((1000 / 185)); + expect(getDesiredAspect('banner')).toEqual((1000 / 185)); + expect(getDesiredAspect('BaNnEr')).toEqual((1000 / 185)); }); - test('invalid shape', () => { - expect(cardBuilderUtils.getDesiredAspect('invalid')).toBeNull(); - }); + test('invalid shape', () => expect(getDesiredAspect('invalid')).toBeNull()); - test('shape is not provided', () => { - expect(cardBuilderUtils.getDesiredAspect('')).toBeNull(); - }); + test('shape is not provided', () => expect(getDesiredAspect('')).toBeNull()); }); describe('getPostersPerRow', () => { 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', () => { - const postersPerRowForPortrait = (screenWidth, isTV) => (cardBuilderUtils.getPostersPerRow('portrait', screenWidth, false, isTV)); + const postersPerRowForPortrait = (screenWidth: number, isTV: boolean) => (getPostersPerRow('portrait', screenWidth, false, isTV)); - test('television', () => { - expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667); - }); + test('television', () => expect(postersPerRowForPortrait(0, true)).toEqual(100 / 16.66666667)); test('screen width less than 500px', () => { expect(postersPerRowForPortrait(100, false)).toEqual(100 / 33.33333333); @@ -90,11 +95,9 @@ describe('getPostersPerRow', () => { }); 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', () => { - expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667); - }); + test('television', () => expect(postersPerRowForSquare(0, true)).toEqual(100 / 16.66666667)); test('screen width less than 500px', () => { expect(postersPerRowForSquare(100, false)).toEqual(2); @@ -143,11 +146,9 @@ describe('getPostersPerRow', () => { }); 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', () => { - expect(postersPerRowForBanner(799)).toEqual(1); - }); + test('screen width less than 800px', () => expect(postersPerRowForBanner(799)).toEqual(1)); test('screen width greater than or equal to 800px', () => { expect(postersPerRowForBanner(800)).toEqual(2); @@ -166,11 +167,9 @@ describe('getPostersPerRow', () => { }); 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', () => { - expect(postersPerRowForBackdrop(0, true)).toEqual(4); - }); + test('television', () => expect(postersPerRowForBackdrop(0, true)).toEqual(4)); test('screen width less than 420px', () => { expect(postersPerRowForBackdrop(100, false)).toEqual(1); @@ -204,7 +203,7 @@ describe('getPostersPerRow', () => { }); 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', () => { expect(postersPerRowForSmallBackdrop(100)).toEqual(2); @@ -243,11 +242,9 @@ describe('getPostersPerRow', () => { }); 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', () => { - expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual( 100 / 18.9); - }); + test('television', () => expect(postersPerRowForOverflowSmallBackdrop(0, false, true)).toEqual(100 / 18.9)); describe('non-landscape', () => { test('screen width greater or equal to 540px', () => { @@ -275,11 +272,9 @@ describe('getPostersPerRow', () => { }); 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', () => { - expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual( 100 / 15.5); - }); + test('television', () => expect(postersPerRowForOverflowPortrait(0, false, true)).toEqual(100 / 15.5)); describe('non-landscape', () => { test('screen width greater or equal to 1400px', () => { @@ -322,11 +317,9 @@ describe('getPostersPerRow', () => { }); 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', () => { - expect(postersPerRowForOverflowSquare(0, false, true)).toEqual( 100 / 15.5); - }); + test('television', () => expect(postersPerRowForOverflowSquare(0, false, true)).toEqual(100 / 15.5)); describe('non-landscape', () => { test('screen width greater or equal to 1400px', () => { @@ -369,11 +362,9 @@ describe('getPostersPerRow', () => { }); 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', () => { - expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual( 100 / 23.3); - }); + test('television', () => expect(postersPerRowForOverflowBackdrop(0, false, true)).toEqual(100 / 23.3)); describe('non-landscape', () => { 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); + }); +}); diff --git a/src/components/cardbuilder/cardBuilderUtils.ts b/src/components/cardbuilder/cardBuilderUtils.ts new file mode 100644 index 000000000..d7215b190 --- /dev/null +++ b/src/components/cardbuilder/cardBuilderUtils.ts @@ -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; + } +}; diff --git a/src/components/dashboard/users/UserCardBox.tsx b/src/components/dashboard/users/UserCardBox.tsx index f0fbdf96a..e4bc40d2b 100644 --- a/src/components/dashboard/users/UserCardBox.tsx +++ b/src/components/dashboard/users/UserCardBox.tsx @@ -3,9 +3,9 @@ import React, { FunctionComponent } from 'react'; import { formatDistanceToNow } from 'date-fns'; import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale'; import globalize from '../../../scripts/globalize'; -import cardBuilder from '../../cardbuilder/cardBuilder'; import IconButtonElement from '../../../elements/IconButtonElement'; import escapeHTML from 'escape-html'; +import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils'; const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({ __html: ` = ({ user = {} }: IProps) => { const renderImgUrl = imgUrl ? `
` : - `
+ `
`; diff --git a/src/components/listview/listview.js b/src/components/listview/listview.js index 4c05be198..018a8a7b6 100644 --- a/src/components/listview/listview.js +++ b/src/components/listview/listview.js @@ -16,6 +16,7 @@ import './listview.scss'; import '../../elements/emby-ratingbutton/emby-ratingbutton'; import '../../elements/emby-playstatebutton/emby-playstatebutton'; import ServerConnections from '../ServerConnections'; +import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils'; function getIndex(item, options) { if (options.index === 'disc') { @@ -279,7 +280,7 @@ export function getListViewHtml(options) { if (imgUrl) { html += '
'; } else { - html += '
' + cardBuilder.getDefaultText(item, options); + html += '
' + cardBuilder.getDefaultText(item, options); } const mediaSourceCount = item.MediaSourceCount || 1; diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 0c6fe4982..a786f6fdd 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -10,7 +10,6 @@ import { appHost } from '../apphost'; import globalize from '../../scripts/globalize'; import layoutManager from '../layoutManager'; import * as userSettings from '../../scripts/settings/userSettings'; -import cardBuilder from '../cardbuilder/cardBuilder'; import itemContextMenu from '../itemContextMenu'; import '../cardbuilder/card.scss'; import '../../elements/emby-itemscontainer/emby-itemscontainer'; @@ -19,6 +18,7 @@ import '../../elements/emby-ratingbutton/emby-ratingbutton'; import ServerConnections from '../ServerConnections'; import toast from '../toast/toast'; import { appRouter } from '../router/appRouter'; +import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils'; let showMuteButton = 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('nowPlayingPageImagePoster', item.Type !== 'Audio'); } else { - imgContainer.innerHTML = '
'; + imgContainer.innerHTML = '
'; } } diff --git a/src/controllers/dashboard/dashboard.js b/src/controllers/dashboard/dashboard.js index 30f37764b..393e4c603 100644 --- a/src/controllers/dashboard/dashboard.js +++ b/src/controllers/dashboard/dashboard.js @@ -23,6 +23,7 @@ import Dashboard from '../../utils/dashboard'; import ServerConnections from '../../components/ServerConnections'; import alert from '../../components/alert'; import confirm from '../../components/confirm/confirm'; +import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils'; function showPlaybackInfo(btn, session) { let title; @@ -259,7 +260,7 @@ function renderActiveConnections(view, sessions) { html += '
'; html += '
'; html += '
'; - html += `
`; + html += `
`; if (imgUrl) { html += '
`; + deviceHtml += ``; // audit note: getDeviceIcon returns static text const iconUrl = imageHelper.getDeviceIcon(device); diff --git a/src/controllers/dashboard/library.js b/src/controllers/dashboard/library.js index 987169ecd..897ad3ecb 100644 --- a/src/controllers/dashboard/library.js +++ b/src/controllers/dashboard/library.js @@ -10,7 +10,7 @@ import '../../components/cardbuilder/card.scss'; import '../../elements/emby-itemrefreshindicator/emby-itemrefreshindicator'; import Dashboard, { pageClassOn, pageIdOn } from '../../utils/dashboard'; import confirm from '../../components/confirm/confirm'; -import cardBuilder from '../../components/cardbuilder/cardBuilder'; +import { getDefaultBackgroundClass } from '../../components/cardbuilder/cardBuilderUtils'; function addVirtualFolder(page) { import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: MediaLibraryCreator }) => { @@ -275,11 +275,11 @@ function getVirtualFolderHtml(page, virtualFolder, index) { let hasCardImageContainer; if (imgUrl) { - html += `
`; + html += `
`; html += ``; hasCardImageContainer = true; } else if (!virtualFolder.showNameWithIcon) { - html += `
`; + html += `
`; html += ''; hasCardImageContainer = true; } diff --git a/src/controllers/dashboard/plugins/available/index.js b/src/controllers/dashboard/plugins/available/index.js index b3445b5cb..8ce093229 100644 --- a/src/controllers/dashboard/plugins/available/index.js +++ b/src/controllers/dashboard/plugins/available/index.js @@ -3,11 +3,11 @@ import escapeHTML from 'escape-html'; import loading from '../../../../components/loading/loading'; import libraryMenu from '../../../../scripts/libraryMenu'; import globalize from '../../../../scripts/globalize'; -import * as cardBuilder from '../../../../components/cardbuilder/cardBuilder.js'; import '../../../../components/cardbuilder/card.scss'; import '../../../../elements/emby-button/emby-button'; import '../../../../elements/emby-checkbox/emby-checkbox'; import '../../../../elements/emby-select/emby-select'; +import { getDefaultBackgroundClass } from '../../../../components/cardbuilder/cardBuilderUtils'; function reloadList(page) { loading.show(); @@ -137,7 +137,7 @@ function getPluginHtml(plugin, options, installedPlugins) { if (plugin.imageUrl) { html += ``; } else { - html += `
`; + html += `
`; html += ''; html += '
'; } diff --git a/src/controllers/dashboard/plugins/installed/index.js b/src/controllers/dashboard/plugins/installed/index.js index 91ebcfdff..9600eb0a3 100644 --- a/src/controllers/dashboard/plugins/installed/index.js +++ b/src/controllers/dashboard/plugins/installed/index.js @@ -2,11 +2,11 @@ import loading from '../../../../components/loading/loading'; import libraryMenu from '../../../../scripts/libraryMenu'; import dom from '../../../../scripts/dom'; import globalize from '../../../../scripts/globalize'; -import * as cardBuilder from '../../../../components/cardbuilder/cardBuilder.js'; import '../../../../components/cardbuilder/card.scss'; import '../../../../elements/emby-button/emby-button'; import Dashboard, { pageIdOn } from '../../../../utils/dashboard'; import confirm from '../../../../components/confirm/confirm'; +import { getDefaultBackgroundClass } from '../../../../components/cardbuilder/cardBuilderUtils'; function deletePlugin(page, uniqueid, version, 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`); html += ``; } else { - html += `
`; + html += `
`; html += ''; html += '
'; } diff --git a/src/controllers/livetvstatus.js b/src/controllers/livetvstatus.js index 8532e8ae2..35c219b93 100644 --- a/src/controllers/livetvstatus.js +++ b/src/controllers/livetvstatus.js @@ -2,7 +2,6 @@ import 'jquery'; import globalize from '../scripts/globalize'; import taskButton from '../scripts/taskbutton'; import dom from '../scripts/dom'; -import cardBuilder from '../components/cardbuilder/cardBuilder'; import layoutManager from '../components/layoutManager'; import loading from '../components/loading/loading'; import browser from '../scripts/browser'; @@ -14,6 +13,7 @@ import 'material-design-icons-iconfont'; import '../elements/emby-button/emby-button'; import Dashboard from '../utils/dashboard'; import confirm from '../components/confirm/confirm'; +import { getDefaultBackgroundClass } from '../components/cardbuilder/cardBuilderUtils'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -38,7 +38,7 @@ function getDeviceHtml(device) { html += '
'; html += '
'; html += '
'; - html += `
`; + html += `
`; html += '
'; html += '
'; html += '
'; diff --git a/src/controllers/session/login/index.js b/src/controllers/session/login/index.js index 6e75af6f0..e304f463e 100644 --- a/src/controllers/session/login/index.js +++ b/src/controllers/session/login/index.js @@ -15,8 +15,8 @@ import ServerConnections from '../../../components/ServerConnections'; import toast from '../../../components/toast/toast'; import dialogHelper from '../../../components/dialogHelper/dialogHelper'; import baseAlert from '../../../components/alert'; -import cardBuilder from '../../../components/cardbuilder/cardBuilder'; import './login.scss'; +import { getDefaultBackgroundClass } from '../../../components/cardbuilder/cardBuilderUtils'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -164,7 +164,7 @@ function loadUserList(context, apiClient, users) { html += '
"; } else { - html += `
`; + html += `
`; html += ''; html += '
'; } diff --git a/src/controllers/session/selectServer/index.js b/src/controllers/session/selectServer/index.js index fe14164dd..763dcda96 100644 --- a/src/controllers/session/selectServer/index.js +++ b/src/controllers/session/selectServer/index.js @@ -18,8 +18,8 @@ import '../../../elements/emby-button/emby-button'; import Dashboard from '../../../utils/dashboard'; import ServerConnections from '../../../components/ServerConnections'; import alert from '../../../components/alert'; -import cardBuilder from '../../../components/cardbuilder/cardBuilder'; import { ConnectionState } from '../../../utils/jellyfin-apiclient/ConnectionState.ts'; +import { getDefaultBackgroundClass } from '../../../components/cardbuilder/cardBuilderUtils'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -56,7 +56,7 @@ function renderSelectServerItems(view, servers) { cardContainer += '
'; cardContainer += '
'; cardContainer += '
'; - cardContainer += `
`; + cardContainer += `
`; cardContainer += cardImageContainer; cardContainer += '
'; cardContainer += '
';