diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js
index 8c746554d..811bac185 100644
--- a/src/components/cardbuilder/cardBuilder.js
+++ b/src/components/cardbuilder/cardBuilder.js
@@ -261,7 +261,7 @@ function buildCardsHtmlInternal(items, options) {
* @param {string} shape - Shape of the desired image.
* @returns {CardImageUrl} Object representing the URL of the card's image.
*/
-function getCardImageUrl(item, apiClient, options, shape) {
+export function getCardImageUrl(item, apiClient, options, shape) {
item = item.ProgramInfo || item;
const width = options.width;
@@ -1049,7 +1049,7 @@ function buildCard(index, item, apiClient, options) {
cardImageContainerOpen += getDefaultText(item, options);
}
- const tagName = (layoutManager.tv) && !overlayButtons ? 'button' : 'div';
+ const tagName = layoutManager.tv && !overlayButtons ? 'button' : 'div';
const nameWithPrefix = (item.SortName || item.Name || '');
let prefix = nameWithPrefix.substring(0, Math.min(3, nameWithPrefix.length));
diff --git a/src/components/cardbuilder/cardImage.ts b/src/components/cardbuilder/cardImage.ts
new file mode 100644
index 000000000..b6d73744d
--- /dev/null
+++ b/src/components/cardbuilder/cardImage.ts
@@ -0,0 +1,74 @@
+import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
+import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
+import type { ApiClient } from 'jellyfin-apiclient';
+
+import type { CardOptions } from 'types/cardOptions';
+import { CardShape } from 'utils/card';
+
+import { getCardImageUrl, getDefaultText } from './cardBuilder';
+
+/**
+ * Builds an html string for a basic image only card.
+ */
+export function buildCardImage(
+ apiClient: ApiClient,
+ item: BaseItemDto,
+ options: CardOptions
+): string {
+ let shape: CardShape = CardShape.Square;
+ if (item.PrimaryImageAspectRatio) {
+ if (item.PrimaryImageAspectRatio >= 3) {
+ shape = CardShape.Banner;
+ } else if (item.PrimaryImageAspectRatio >= 1.33) {
+ shape = CardShape.Backdrop;
+ } else if (item.PrimaryImageAspectRatio > 0.71) {
+ shape = CardShape.Square;
+ } else {
+ shape = CardShape.Portrait;
+ }
+ }
+
+ const image = getCardImageUrl(
+ item,
+ apiClient,
+ options,
+ shape
+ );
+
+ if (!image) return '';
+
+ const { blurhash, imgUrl } = image;
+
+ let cardPadderIcon = '';
+ // TV Channel logos are transparent so skip the placeholder to avoid overlapping
+ if (imgUrl && item.Type !== BaseItemKind.TvChannel) {
+ cardPadderIcon = getDefaultText(item, {
+ // Always use an icon
+ defaultCardImageIcon: 'folder',
+ ...options
+ });
+ }
+
+ let blurhashAttrib = '';
+ if (blurhash && blurhash.length > 0) {
+ blurhashAttrib = `data-blurhash="${blurhash}"`;
+ }
+
+ return (
+ `
+
+
+
+ ${cardPadderIcon}
+
+
+
+
+
`
+ );
+}
diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js
index d8adec3ad..9f214856f 100644
--- a/src/controllers/itemDetails/index.js
+++ b/src/controllers/itemDetails/index.js
@@ -7,6 +7,7 @@ import isEqual from 'lodash-es/isEqual';
import { appHost } from 'components/apphost';
import { clearBackdrop, setBackdrops } from 'components/backdrop/backdrop';
import cardBuilder from 'components/cardbuilder/cardBuilder';
+import { buildCardImage } from 'components/cardbuilder/cardImage';
import confirm from 'components/confirm/confirm';
import imageLoader from 'components/images/imageLoader';
import itemContextMenu from 'components/itemContextMenu';
@@ -526,7 +527,7 @@ function reloadFromItem(instance, page, params, item, user) {
libraryMenu.setTitle('');
// Start rendering the artwork first
- renderImage(page, item);
+ renderImage(page, item, apiClient);
// Save some screen real estate in TV mode
if (!layoutManager.tv) {
renderLogo(page, item, apiClient);
@@ -717,31 +718,20 @@ function renderLinks(page, item) {
}
}
-function renderDetailImage(elem, item, loader) {
- const itemArray = [];
- itemArray.push(item);
- const cardHtml = cardBuilder.getCardsHtml(itemArray, {
- shape: 'auto',
- showTitle: false,
- centerText: true,
- overlayText: false,
- transition: false,
- disableHoverMenu: true,
- disableIndicators: true,
- overlayPlayButton: layoutManager.desktop,
- action: layoutManager.desktop ? 'resume' : 'none',
- width: dom.getWindowSize().innerWidth * 0.25
- });
+function renderDetailImage(apiClient, elem, item, loader) {
+ const html = buildCardImage(
+ apiClient,
+ item,
+ { width: dom.getWindowSize().innerWidth * 0.25 }
+ );
- elem.innerHTML = cardHtml;
+ elem.innerHTML = html;
loader.lazyChildren(elem);
-
- // Avoid breaking the design by preventing focus of the poster using the keyboard.
- elem.querySelector('a, button').tabIndex = -1;
}
-function renderImage(page, item) {
+function renderImage(page, item, apiClient) {
renderDetailImage(
+ apiClient,
page.querySelector('.detailImageContainer'),
item,
imageLoader
@@ -2016,7 +2006,6 @@ export default function (view, params) {
bindAll(view, '.btnCancelSeriesTimer', 'click', onCancelSeriesTimerClick);
bindAll(view, '.btnCancelTimer', 'click', onCancelTimerClick);
bindAll(view, '.btnDownload', 'click', onDownloadClick);
- view.querySelector('.detailImageContainer').addEventListener('click', onPlayClick);
view.querySelector('.trackSelections').addEventListener('submit', onTrackSelectionsSubmit);
view.querySelector('.btnSplitVersions').addEventListener('click', function () {
splitVersions(self, view, apiClient, params);
diff --git a/src/utils/card.ts b/src/utils/card.ts
index c3f047a79..bdb68e724 100644
--- a/src/utils/card.ts
+++ b/src/utils/card.ts
@@ -1,6 +1,7 @@
-enum CardShape {
+export enum CardShape {
Backdrop = 'backdrop',
BackdropOverflow = 'overflowBackdrop',
+ Banner = 'banner',
Portrait = 'portrait',
PortraitOverflow = 'overflowPortrait',
Square = 'square',