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 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 ? ('<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>';
} 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 ? ('<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>';
}
@ -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 '<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>';
}
@ -1515,7 +1415,6 @@ export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) {
export default {
getCardsHtml: getCardsHtml,
getDefaultBackgroundClass: getDefaultBackgroundClass,
getDefaultText: getDefaultText,
buildCards: buildCards,
onUserDataChanged: onUserDataChanged,