From cb8b2836c2a8f2a09e3633e5674d84200cec2212 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 3 Dec 2025 17:28:45 -0500 Subject: [PATCH 1/2] Use enum for item actions --- .../components/library/ItemsView.tsx | 3 +- .../library/ProgramsSectionView.tsx | 16 +- .../components/buttons/PlayOrResumeButton.tsx | 3 +- src/components/cardbuilder/Card/CardBox.tsx | 11 +- .../cardbuilder/Card/CardHoverMenu.tsx | 10 +- .../cardbuilder/Card/CardOverlayButtons.tsx | 12 +- src/components/cardbuilder/Card/cardHelper.ts | 3 +- src/components/cardbuilder/Card/useCard.ts | 20 +- src/components/cardbuilder/cardBuilder.js | 21 +- .../cardbuilder/cardBuilderUtils.test.ts | 7 +- .../cardbuilder/cardBuilderUtils.ts | 14 +- src/components/common/InfoIconButton.tsx | 4 +- src/components/common/MoreVertIconButton.tsx | 4 +- src/components/common/PlayArrowIconButton.tsx | 4 +- .../common/PlaylistAddIconButton.tsx | 4 +- src/components/common/RightIconButtons.tsx | 4 +- src/components/guide/guide.js | 11 +- src/components/listview/List/ListContent.tsx | 4 +- .../listview/List/ListImageContainer.tsx | 18 +- src/components/listview/List/ListItemBody.tsx | 7 +- src/components/listview/List/ListWrapper.tsx | 7 +- src/components/listview/List/useList.ts | 6 +- src/components/listview/listview.js | 24 ++- src/components/remotecontrol/remotecontrol.js | 9 +- src/components/shortcuts.js | 196 ++++++++++-------- src/constants/itemAction.ts | 37 ++++ src/controllers/itemDetails/index.js | 11 +- .../emby-playstatebutton/PlayedButton.tsx | 18 +- .../emby-ratingbutton/FavoriteButton.tsx | 19 +- src/types/cardOptions.ts | 5 +- src/types/dataAttributes.ts | 7 +- src/types/listOptions.ts | 7 +- src/utils/sections.ts | 8 +- 33 files changed, 318 insertions(+), 216 deletions(-) create mode 100644 src/constants/itemAction.ts diff --git a/src/apps/experimental/components/library/ItemsView.tsx b/src/apps/experimental/components/library/ItemsView.tsx index 3fc2a18c96..0df72f9a7b 100644 --- a/src/apps/experimental/components/library/ItemsView.tsx +++ b/src/apps/experimental/components/library/ItemsView.tsx @@ -9,6 +9,7 @@ import useMediaQuery from '@mui/material/useMediaQuery'; import classNames from 'classnames'; import React, { type FC, useCallback } from 'react'; +import { ItemAction } from 'constants/itemAction'; import { useApi } from 'hooks/useApi'; import { useLocalStorage } from 'hooks/useLocalStorage'; import { useGetItemsViewByType } from 'hooks/useFetchItems'; @@ -99,7 +100,7 @@ const ItemsView: FC = ({ if (viewType === LibraryTab.Songs) { listOptions.showParentTitle = true; - listOptions.action = 'playallfromhere'; + listOptions.action = ItemAction.PlayAllFromHere; listOptions.smallIcon = true; listOptions.showArtist = true; listOptions.addToListButton = true; diff --git a/src/apps/experimental/components/library/ProgramsSectionView.tsx b/src/apps/experimental/components/library/ProgramsSectionView.tsx index 87b882dd0b..ed73752b20 100644 --- a/src/apps/experimental/components/library/ProgramsSectionView.tsx +++ b/src/apps/experimental/components/library/ProgramsSectionView.tsx @@ -1,14 +1,16 @@ import React, { type FC } from 'react'; -import { useApi } from 'hooks/useApi'; -import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems'; -import { appRouter } from 'components/router/appRouter'; -import globalize from 'lib/globalize'; -import Loading from 'components/loading/LoadingComponent'; + import NoItemsMessage from 'components/common/NoItemsMessage'; import SectionContainer from 'components/common/SectionContainer'; -import { CardShape } from 'utils/card'; +import Loading from 'components/loading/LoadingComponent'; +import { appRouter } from 'components/router/appRouter'; +import { ItemAction } from 'constants/itemAction'; +import { useApi } from 'hooks/useApi'; +import { useGetProgramsSectionsWithItems, useGetTimers } from 'hooks/useFetchItems'; +import globalize from 'lib/globalize'; import type { ParentId } from 'types/library'; import type { Section, SectionType } from 'types/sections'; +import { CardShape } from 'utils/card'; interface ProgramsSectionViewProps { parentId: ParentId; @@ -92,7 +94,7 @@ const ProgramsSectionView: FC = ({ showChannelName: false, cardLayout: true, centerText: false, - action: 'edit', + action: ItemAction.Edit, cardFooterAside: 'none', preferThumb: true, coverImage: true, diff --git a/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx b/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx index 123f45c263..7fcca7703f 100644 --- a/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx +++ b/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx @@ -4,6 +4,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import ReplayIcon from '@mui/icons-material/Replay'; import { useQueryClient } from '@tanstack/react-query'; +import { ItemAction } from 'constants/itemAction'; import { useApi } from 'hooks/useApi'; import { getChannelQuery } from 'hooks/api/liveTvHooks/useGetChannel'; import globalize from 'lib/globalize'; @@ -76,7 +77,7 @@ const PlayOrResumeButton: FC = ({ return ( = ({ parentId: cardOptions.parentId }); const btnCssClass = - 'paper-icon-button-light cardOverlayButton cardOverlayButton-hover itemAction'; + 'paper-icon-button-light cardOverlayButton cardOverlayButton-hover itemAction'; const centerPlayButtonClass = classNames( btnCssClass, @@ -51,7 +53,7 @@ const CardHoverMenu: FC = ({ {playbackManager.canPlay(item) && ( )} diff --git a/src/components/cardbuilder/Card/CardOverlayButtons.tsx b/src/components/cardbuilder/Card/CardOverlayButtons.tsx index 5de5ea6ed3..7530d7aaa3 100644 --- a/src/components/cardbuilder/Card/CardOverlayButtons.tsx +++ b/src/components/cardbuilder/Card/CardOverlayButtons.tsx @@ -2,15 +2,17 @@ import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location import React, { type FC } from 'react'; import ButtonGroup from '@mui/material/ButtonGroup'; import classNames from 'classnames'; -import { appRouter } from 'components/router/appRouter'; -import PlayArrowIconButton from '../../common/PlayArrowIconButton'; -import MoreVertIconButton from '../../common/MoreVertIconButton'; +import { appRouter } from 'components/router/appRouter'; +import { ItemAction } from 'constants/itemAction'; import { ItemKind } from 'types/base/models/item-kind'; import { ItemMediaKind } from 'types/base/models/item-media-kind'; import type { ItemDto } from 'types/base/models/item-dto'; import type { CardOptions } from 'types/cardOptions'; +import PlayArrowIconButton from '../../common/PlayArrowIconButton'; +import MoreVertIconButton from '../../common/MoreVertIconButton'; + const sholudShowOverlayPlayButton = ( overlayPlayButton: boolean | undefined, item: ItemDto @@ -78,7 +80,7 @@ const CardOverlayButtons: FC = ({ {cardOptions.centerPlayButton && ( )} @@ -87,7 +89,7 @@ const CardOverlayButtons: FC = ({ {sholudShowOverlayPlayButton(overlayPlayButton, item) && ( )} diff --git a/src/components/cardbuilder/Card/cardHelper.ts b/src/components/cardbuilder/Card/cardHelper.ts index 3044105a5f..08adcf7adb 100644 --- a/src/components/cardbuilder/Card/cardHelper.ts +++ b/src/components/cardbuilder/Card/cardHelper.ts @@ -7,6 +7,7 @@ import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api'; import { appRouter } from 'components/router/appRouter'; import layoutManager from 'components/layoutManager'; import itemHelper from 'components/itemHelper'; +import { ItemAction } from 'constants/itemAction'; import globalize from 'lib/globalize'; import datetime from 'scripts/datetime'; import { isUsingLiveTvNaming } from '../cardBuilderUtils'; @@ -88,7 +89,7 @@ export function getTextActionButton( const dataAttributes = getDataAttributes( { - action: 'link', + action: ItemAction.Link, itemServerId: serverId ?? item.ServerId, itemId: item.Id, itemChannelId: item.ChannelId, diff --git a/src/components/cardbuilder/Card/useCard.ts b/src/components/cardbuilder/Card/useCard.ts index fa77b6dfd2..afa3081ee5 100644 --- a/src/components/cardbuilder/Card/useCard.ts +++ b/src/components/cardbuilder/Card/useCard.ts @@ -1,17 +1,19 @@ import classNames from 'classnames'; + +import layoutManager from 'components/layoutManager'; +import { ItemAction } from 'constants/itemAction'; +import { ItemKind } from 'types/base/models/item-kind'; +import { ItemMediaKind } from 'types/base/models/item-media-kind'; +import type { ItemDto } from 'types/base/models/item-dto'; +import type { CardOptions } from 'types/cardOptions'; +import { CardShape } from 'utils/card'; +import { getDataAttributes } from 'utils/items'; + import useCardImageUrl from './useCardImageUrl'; import { resolveAction, resolveMixedShapeByAspectRatio } from '../cardBuilderUtils'; -import { getDataAttributes } from 'utils/items'; -import { CardShape } from 'utils/card'; -import layoutManager from 'components/layoutManager'; - -import { ItemKind } from 'types/base/models/item-kind'; -import { ItemMediaKind } from 'types/base/models/item-media-kind'; -import type { ItemDto } from 'types/base/models/item-dto'; -import type { CardOptions } from 'types/cardOptions'; interface UseCardProps { item: ItemDto; @@ -20,7 +22,7 @@ interface UseCardProps { function useCard({ item, cardOptions }: UseCardProps) { const action = resolveAction({ - defaultAction: cardOptions.action ?? 'link', + defaultAction: cardOptions.action ?? ItemAction.Link, isFolder: item.IsFolder ?? false, isPhoto: item.MediaType === ItemMediaKind.Photo }); diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 43298704db..6e92acfb26 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -8,6 +8,7 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-ite import { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind'; import escapeHtml from 'escape-html'; +import { ItemAction } from 'constants/itemAction'; import browser from 'scripts/browser'; import datetime from 'scripts/datetime'; import dom from 'utils/dom'; @@ -514,7 +515,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml, const showOtherText = flags.isOuterFooter ? !flags.overlayText : flags.overlayText; if (flags.isOuterFooter && options.cardLayout && layoutManager.mobile && options.cardFooterAside !== 'none') { - html += ``; + html += ``; } const cssClass = options.centerText ? 'cardText cardTextCentered' : 'cardText'; @@ -776,7 +777,7 @@ function getTextActionButton(item, text, serverId) { } const url = appRouter.getRouteUrl(item); - let html = ''; + let html = '`; html += text; html += ''; @@ -885,7 +886,7 @@ function importRefreshIndicator() { */ function buildCard(index, item, apiClient, options) { const action = resolveAction({ - defaultAction: options.action || 'link', + defaultAction: options.action || ItemAction.Link, isFolder: item.IsFolder, isPhoto: item.MediaType === 'Photo' }); @@ -985,15 +986,15 @@ function buildCard(index, item, apiClient, options) { const btnCssClass = 'cardOverlayButton cardOverlayButton-br itemAction'; if (options.centerPlayButton) { - overlayButtons += ``; + overlayButtons += ``; } if (overlayPlayButton && !item.IsPlaceHolder && (item.LocationType !== 'Virtual' || !item.MediaType || item.Type === 'Program') && item.Type !== 'Person') { - overlayButtons += ``; + overlayButtons += ``; } if (options.overlayMoreButton) { - overlayButtons += ``; + overlayButtons += ``; } } @@ -1156,7 +1157,7 @@ function getHoverMenuHtml(item, action) { const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light'; if (playbackManager.canPlay(item)) { - html += ''; + html += ``; } html += '
'; @@ -1165,17 +1166,17 @@ function getHoverMenuHtml(item, action) { if (itemHelper.canMarkPlayed(item)) { import('../../elements/emby-playstatebutton/emby-playstatebutton'); - html += ''; + html += ``; } if (itemHelper.canRate(item)) { const likes = userData.Likes == null ? '' : userData.Likes; import('../../elements/emby-ratingbutton/emby-ratingbutton'); - html += ''; + html += ``; } - html += ``; + html += ``; html += '
'; html += ''; diff --git a/src/components/cardbuilder/cardBuilderUtils.test.ts b/src/components/cardbuilder/cardBuilderUtils.test.ts index af5d189f36..8b159c2ccb 100644 --- a/src/components/cardbuilder/cardBuilderUtils.test.ts +++ b/src/components/cardbuilder/cardBuilderUtils.test.ts @@ -11,6 +11,7 @@ import { resolveCardImageContainerCssClasses, resolveMixedShapeByAspectRatio } from './cardBuilderUtils'; +import { ItemAction } from 'constants/itemAction'; describe('getDesiredAspect', () => { test('"portrait" (case insensitive)', () => { @@ -441,11 +442,11 @@ describe('isResizable', () => { }); describe('resolveAction', () => { - test('default action', () => expect(resolveAction({ defaultAction: 'link', isFolder: false, isPhoto: false })).toEqual('link')); + test('default action', () => expect(resolveAction({ defaultAction: ItemAction.Link, isFolder: false, isPhoto: false })).toEqual(ItemAction.Link)); - test('photo', () => expect(resolveAction({ defaultAction: 'link', isFolder: false, isPhoto: true })).toEqual('play')); + test('photo', () => expect(resolveAction({ defaultAction: ItemAction.Link, isFolder: false, isPhoto: true })).toEqual(ItemAction.Play)); - test('default action is "play" and is folder', () => expect(resolveAction({ defaultAction: 'play', isFolder: true, isPhoto: true })).toEqual('link')); + test('default action is "play" and is folder', () => expect(resolveAction({ defaultAction: ItemAction.Play, isFolder: true, isPhoto: true })).toEqual(ItemAction.Link)); }); describe('resolveMixedShapeByAspectRatio', () => { diff --git a/src/components/cardbuilder/cardBuilderUtils.ts b/src/components/cardbuilder/cardBuilderUtils.ts index 02db1fc23a..f2a1af1b6a 100644 --- a/src/components/cardbuilder/cardBuilderUtils.ts +++ b/src/components/cardbuilder/cardBuilderUtils.ts @@ -1,7 +1,9 @@ -import { CardShape } from '../../utils/card'; -import { randomInt } from '../../utils/number'; import classNames from 'classnames'; +import { ItemAction } from 'constants/itemAction'; +import { CardShape } from 'utils/card'; +import { randomInt } from 'utils/number'; + const ASPECT_RATIOS = { portrait: (2 / 3), backdrop: (16 / 9), @@ -20,12 +22,12 @@ export const isUsingLiveTvNaming = (itemType: string | null | undefined): boolea * 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) { +export const resolveAction = (opts: { defaultAction: ItemAction, isFolder: boolean, isPhoto: boolean }): ItemAction => { + if (opts.defaultAction === ItemAction.Play && opts.isFolder) { // If this hard-coding is ever removed make sure to test nested photo albums - return 'link'; + return ItemAction.Link; } else if (opts.isPhoto) { - return 'play'; + return ItemAction.Play; } else { return opts.defaultAction; } diff --git a/src/components/common/InfoIconButton.tsx b/src/components/common/InfoIconButton.tsx index 0d1788c5f3..7d116b61f6 100644 --- a/src/components/common/InfoIconButton.tsx +++ b/src/components/common/InfoIconButton.tsx @@ -1,6 +1,8 @@ import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; import InfoIcon from '@mui/icons-material/Info'; + +import { ItemAction } from 'constants/itemAction'; import globalize from 'lib/globalize'; interface InfoIconButtonProps { @@ -11,7 +13,7 @@ const InfoIconButton: FC = ({ className }) => { return ( diff --git a/src/components/common/MoreVertIconButton.tsx b/src/components/common/MoreVertIconButton.tsx index bd42a2732d..a6759e31f0 100644 --- a/src/components/common/MoreVertIconButton.tsx +++ b/src/components/common/MoreVertIconButton.tsx @@ -1,6 +1,8 @@ import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; import MoreVertIcon from '@mui/icons-material/MoreVert'; + +import { ItemAction } from 'constants/itemAction'; import globalize from 'lib/globalize'; interface MoreVertIconButtonProps { @@ -12,7 +14,7 @@ const MoreVertIconButton: FC = ({ className, iconClassN return ( diff --git a/src/components/common/PlayArrowIconButton.tsx b/src/components/common/PlayArrowIconButton.tsx index 18eb37169b..dc02e9cc35 100644 --- a/src/components/common/PlayArrowIconButton.tsx +++ b/src/components/common/PlayArrowIconButton.tsx @@ -1,11 +1,13 @@ import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; + +import { ItemAction } from 'constants/itemAction'; import globalize from 'lib/globalize'; interface PlayArrowIconButtonProps { className: string; - action: string; + action: ItemAction; title: string; iconClassName?: string; } diff --git a/src/components/common/PlaylistAddIconButton.tsx b/src/components/common/PlaylistAddIconButton.tsx index 481e1ee094..5e1e6af81b 100644 --- a/src/components/common/PlaylistAddIconButton.tsx +++ b/src/components/common/PlaylistAddIconButton.tsx @@ -1,6 +1,8 @@ import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; + +import { ItemAction } from 'constants/itemAction'; import globalize from 'lib/globalize'; interface PlaylistAddIconButtonProps { @@ -11,7 +13,7 @@ const PlaylistAddIconButton: FC = ({ className }) => return ( diff --git a/src/components/common/RightIconButtons.tsx b/src/components/common/RightIconButtons.tsx index cfe65e451c..306c2b53c6 100644 --- a/src/components/common/RightIconButtons.tsx +++ b/src/components/common/RightIconButtons.tsx @@ -1,6 +1,8 @@ import React, { type FC } from 'react'; import IconButton from '@mui/material/IconButton'; +import { ItemAction } from 'constants/itemAction'; + interface RightIconButtonsProps { className?: string; id: string; @@ -12,7 +14,7 @@ const RightIconButtons: FC = ({ className, id, title, ico return ( diff --git a/src/components/guide/guide.js b/src/components/guide/guide.js index c96e2df4c8..1b5354914d 100644 --- a/src/components/guide/guide.js +++ b/src/components/guide/guide.js @@ -1,8 +1,11 @@ import escapeHtml from 'escape-html'; + +import { ItemAction } from 'constants/itemAction'; +import { ServerConnections } from 'lib/jellyfin-apiclient'; + import inputManager from '../../scripts/inputManager'; import browser from '../../scripts/browser'; import globalize from '../../lib/globalize'; -import { ServerConnections } from 'lib/jellyfin-apiclient'; import Events from '../../utils/events.ts'; import scrollHelper from '../../scripts/scrollHelper'; import serverNotifications from '../../scripts/serverNotifications'; @@ -15,6 +18,7 @@ import imageLoader from '../images/imageLoader'; import layoutManager from '../layoutManager'; import itemShortcuts from '../shortcuts'; import dom from '../../utils/dom'; + import './guide.scss'; import './programs.scss'; import 'material-design-icons-iconfont'; @@ -26,6 +30,7 @@ import '../../elements/emby-tabs/emby-tabs'; import '../../elements/emby-scroller/emby-scroller'; import '../../styles/flexstyles.scss'; import 'webcomponents.js/webcomponents-lite'; + import template from './tvguide.template.html'; function showViewSettings(instance) { @@ -441,7 +446,7 @@ function Guide(options) { html += '
'; - const clickAction = layoutManager.tv ? 'link' : 'programdialog'; + const clickAction = layoutManager.tv ? ItemAction.Link : ItemAction.ProgramDialog; const categories = self.categoryOptions.categories || []; const displayMovieContent = !categories.length || categories.indexOf('movies') !== -1; @@ -607,7 +612,7 @@ function Guide(options) { title.push(channel.Name); } - html += '`; + html += ``; } return html; @@ -175,7 +179,7 @@ export function getListViewHtml(options) { const items = options.items; let groupTitle = ''; - const action = options.action || 'link'; + const action = options.action || ItemAction.Link; const isLargeStyle = options.imageSize === 'large'; const enableOverview = options.enableOverview; @@ -277,7 +281,7 @@ export function getListViewHtml(options) { imageClass += ' itemAction'; } - const imageAction = playOnImageClick ? 'link' : action; + const imageAction = playOnImageClick ? ItemAction.Link : action; if (imgUrl) { html += '
'; @@ -298,7 +302,7 @@ export function getListViewHtml(options) { } if (playOnImageClick) { - html += ''; + html += ``; } const progressHtml = indicators.getProgressBarHtml(item, { @@ -449,11 +453,11 @@ export function getListViewHtml(options) { if (!clickEntireItem) { if (options.addToListButton) { - html += ''; + html += ``; } if (options.infoButton) { - html += ''; + html += ``; } if (options.rightButtons) { @@ -474,7 +478,7 @@ export function getListViewHtml(options) { } if (options.moreButton !== false) { - html += ''; + html += ``; } } html += '
'; diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index bbef1931bc..57ab21c0a2 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -3,6 +3,7 @@ import escapeHtml from 'escape-html'; import { getImageUrl } from 'apps/stable/features/playback/utils/image'; import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText'; import { AppFeature } from 'constants/appFeature'; +import { ItemAction } from 'constants/itemAction'; import datetime from '../../scripts/datetime'; import { clearBackdrop, setBackdrops } from '../backdrop/backdrop'; @@ -16,6 +17,9 @@ import { ServerConnections } from 'lib/jellyfin-apiclient'; import layoutManager from '../layoutManager'; import * as userSettings from '../../scripts/settings/userSettings'; import itemContextMenu from '../itemContextMenu'; +import toast from '../toast/toast'; +import { appRouter } from '../router/appRouter'; +import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils'; import '../cardbuilder/card.scss'; import '../../elements/emby-button/emby-button'; @@ -24,9 +28,6 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer'; import './remotecontrol.scss'; import '../../elements/emby-ratingbutton/emby-ratingbutton'; import '../../elements/emby-slider/emby-slider'; -import toast from '../toast/toast'; -import { appRouter } from '../router/appRouter'; -import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils'; let showMuteButton = true; let showVolumeSlider = true; @@ -208,7 +209,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/components/shortcuts.js b/src/components/shortcuts.js index 73aaf13abf..a655d8f6ed 100644 --- a/src/components/shortcuts.js +++ b/src/components/shortcuts.js @@ -1,19 +1,20 @@ /** - * Module shortcuts. - * @module components/shortcuts + * "Shortcut" action handlers for BaseItems. */ import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api'; +import { ItemAction } from 'constants/itemAction'; +import { ServerConnections } from 'lib/jellyfin-apiclient'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; + import { playbackManager } from './playback/playbackmanager'; import inputManager from '../scripts/inputManager'; import { appRouter } from './router/appRouter'; import globalize from '../lib/globalize'; -import { ServerConnections } from 'lib/jellyfin-apiclient'; import dom from '../utils/dom'; import recordingHelper from './recordingcreator/recordinghelper'; import toast from './toast/toast'; import * as userSettings from '../scripts/settings/userSettings'; -import { toApi } from 'utils/jellyfin-apiclient/compat'; function playAllFromHere(card, serverId, queue) { const parent = card.parentNode; @@ -231,95 +232,114 @@ function executeAction(card, target, action) { const playableItemId = type === 'Program' ? item.ChannelId : item.Id; - if (item.MediaType === 'Photo' && action === 'link') { - action = 'play'; + if (item.MediaType === 'Photo' && action === ItemAction.Link) { + action = ItemAction.Play; } - if (action === 'link') { - appRouter.showItem(item, { - context: card.getAttribute('data-context'), - parentId: card.getAttribute('data-parentid') - }); - } else if (action === 'programdialog') { - showProgramDialog(item); - } else if (action === 'instantmix') { - playbackManager.instantMix({ - Id: playableItemId, - ServerId: serverId - }); - } else if (action === 'play' || action === 'resume') { - const startPositionTicks = parseInt(card.getAttribute('data-positionticks') || '0', 10); - const sortValues = userSettings.getSortValuesLegacy(sortParentId, 'SortName'); - - if (playbackManager.canPlay(item)) { - playbackManager.play({ - ids: [playableItemId], - startPositionTicks: startPositionTicks, - serverId: serverId, - queryOptions: { - SortBy: sortValues.sortBy, - SortOrder: sortValues.sortOrder - } + switch (action) { + case ItemAction.Link: + appRouter.showItem(item, { + context: card.getAttribute('data-context'), + parentId: card.getAttribute('data-parentid') }); - } else { - console.warn('Unable to play item', item); + break; + case ItemAction.ProgramDialog: + showProgramDialog(item); + break; + case ItemAction.InstantMix: + playbackManager.instantMix({ + Id: playableItemId, + ServerId: serverId + }); + break; + case ItemAction.Play: + case ItemAction.Resume: { + const startPositionTicks = parseInt(card.getAttribute('data-positionticks') || '0', 10); + const sortValues = userSettings.getSortValuesLegacy(sortParentId, 'SortName'); + + if (playbackManager.canPlay(item)) { + playbackManager.play({ + ids: [playableItemId], + startPositionTicks: startPositionTicks, + serverId: serverId, + queryOptions: { + SortBy: sortValues.sortBy, + SortOrder: sortValues.sortOrder + } + }); + } else { + console.warn('Unable to play item', item); + } + break; } - } else if (action === 'queue') { - if (playbackManager.isPlaying()) { - playbackManager.queue({ - ids: [playableItemId], - serverId: serverId - }); - toast(globalize.translate('MediaQueued')); - } else { - playbackManager.queue({ - ids: [playableItemId], - serverId: serverId - }); + case ItemAction.Queue: + if (playbackManager.isPlaying()) { + playbackManager.queue({ + ids: [playableItemId], + serverId: serverId + }); + toast(globalize.translate('MediaQueued')); + } else { + playbackManager.queue({ + ids: [playableItemId], + serverId: serverId + }); + } + break; + case ItemAction.PlayAllFromHere: + playAllFromHere(card, serverId); + break; + case ItemAction.QueueAllFromHere: + playAllFromHere(card, serverId, true); + break; + case ItemAction.SetPlaylistIndex: + playbackManager.setCurrentPlaylistItem(card.getAttribute('data-playlistitemid')); + break; + case ItemAction.Record: + onRecordCommand(serverId, id, type, card.getAttribute('data-timerid'), card.getAttribute('data-seriestimerid')); + break; + case ItemAction.Menu: { + const options = target.getAttribute('data-playoptions') === 'false' ? + { + shuffle: false, + instantMix: false, + play: false, + playAllFromHere: false, + queue: false, + queueAllFromHere: false + } : + {}; + + options.positionTo = target; + + showContextMenu(card, options); + break; } - } else if (action === 'playallfromhere') { - playAllFromHere(card, serverId); - } else if (action === 'queueallfromhere') { - playAllFromHere(card, serverId, true); - } else if (action === 'setplaylistindex') { - playbackManager.setCurrentPlaylistItem(card.getAttribute('data-playlistitemid')); - } else if (action === 'record') { - onRecordCommand(serverId, id, type, card.getAttribute('data-timerid'), card.getAttribute('data-seriestimerid')); - } else if (action === 'menu') { - const options = target.getAttribute('data-playoptions') === 'false' ? - { - shuffle: false, - instantMix: false, - play: false, - playAllFromHere: false, - queue: false, - queueAllFromHere: false - } : - {}; + case ItemAction.PlayMenu: + showPlayMenu(card, target); + break; + case ItemAction.Edit: + getItem(target).then(itemToEdit => { + editItem(itemToEdit, serverId); + }); + break; + case ItemAction.PlayTrailer: + getItem(target).then(playTrailer); + break; + case ItemAction.AddToPlaylist: + getItem(target).then(addToPlaylist); + break; + case ItemAction.Custom: { + const customAction = target.getAttribute('data-customaction'); - options.positionTo = target; - - showContextMenu(card, options); - } else if (action === 'playmenu') { - showPlayMenu(card, target); - } else if (action === 'edit') { - getItem(target).then(itemToEdit => { - editItem(itemToEdit, serverId); - }); - } else if (action === 'playtrailer') { - getItem(target).then(playTrailer); - } else if (action === 'addtoplaylist') { - getItem(target).then(addToPlaylist); - } else if (action === 'custom') { - const customAction = target.getAttribute('data-customaction'); - - card.dispatchEvent(new CustomEvent(`action-${customAction}`, { - detail: { - playlistItemId: card.getAttribute('data-playlistitemid') - }, - cancelable: false, - bubbles: true - })); + card.dispatchEvent(new CustomEvent(`action-${customAction}`, { + detail: { + playlistItemId: card.getAttribute('data-playlistitemid') + }, + cancelable: false, + bubbles: true + })); + } } } @@ -390,7 +410,7 @@ export function onClick(e) { } } - if (action && action !== 'none') { + if (action && action !== ItemAction.None) { executeAction(card, actionElement, action); e.preventDefault(); diff --git a/src/constants/itemAction.ts b/src/constants/itemAction.ts new file mode 100644 index 0000000000..a45308b286 --- /dev/null +++ b/src/constants/itemAction.ts @@ -0,0 +1,37 @@ +/** Actions that can be performed on a BaseItem. */ +export enum ItemAction { + /** Add the Item to a playlist. */ + AddToPlaylist = 'addtoplaylist', + /** Trigger a custom action via an Event. */ + Custom = 'custom', + /** Open an editor for the Item. */ + Edit = 'edit', + /** Create an instant mix based on the Item. */ + InstantMix = 'instantmix', + /** Open the details view for the Item. */ + Link = 'link', + /** Open the context menu for the Item. */ + Menu = 'menu', + /** Perform no action. Used to prevent a parent element's action being triggered. */ + None = 'none', + /** Play the Item. */ + Play = 'play', + /** Queue the Item and all subsequent Items and start playback. */ + PlayAllFromHere = 'playallfromhere', + /** Open the play menu for the Item. */ + PlayMenu = 'playmenu', + /** Play the trailer for the Item. */ + PlayTrailer = 'playtrailer', + /** Open the program dialog for the Item. */ + ProgramDialog = 'programdialog', + /** Queue the Item. */ + Queue = 'queue', + /** Queue the Item and all subsequent Items. */ + QueueAllFromHere = 'queueallfromhere', + /** Record the Item. */ + Record = 'record', + /** Resume playback of the Item. */ + Resume = 'resume', + /** Set this Item as the Item to be currently played from a playlist. */ + SetPlaylistIndex = 'setplaylistindex' +}; diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index e425285050..4e37a057aa 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -22,6 +22,7 @@ import { playbackManager } from 'components/playback/playbackmanager'; import { appRouter } from 'components/router/appRouter'; import itemShortcuts from 'components/shortcuts'; import { AppFeature } from 'constants/appFeature'; +import { ItemAction } from 'constants/itemAction'; import globalize from 'lib/globalize'; import { ServerConnections } from 'lib/jellyfin-apiclient'; import browser from 'scripts/browser'; @@ -434,19 +435,19 @@ function renderName(item, container, context) { parentNameHtml.push(getArtistLinksHtml(item.ArtistItems, item.ServerId, context)); parentNameLast = true; } else if (item.SeriesName && item.Type === 'Episode') { - parentNameHtml.push(`${escapeHtml(item.SeriesName)}`); + parentNameHtml.push(`${escapeHtml(item.SeriesName)}`); } else if (item.IsSeries || item.EpisodeTitle) { parentNameHtml.push(escapeHtml(item.Name)); } if (item.SeriesName && item.Type === 'Season') { - parentNameHtml.push(`${escapeHtml(item.SeriesName)}`); + parentNameHtml.push(`${escapeHtml(item.SeriesName)}`); } else if (item.ParentIndexNumber != null && item.Type === 'Episode') { - parentNameHtml.push(`${escapeHtml(item.SeasonName)}`); + parentNameHtml.push(`${escapeHtml(item.SeasonName)}`); } else if (item.ParentIndexNumber != null && item.IsSeries) { parentNameHtml.push(escapeHtml(item.SeasonName || 'S' + item.ParentIndexNumber)); } else if (item.Album && item.AlbumId && (item.Type === 'MusicVideo' || item.Type === 'Audio')) { - parentNameHtml.push(`${escapeHtml(item.Album)}`); + parentNameHtml.push(`${escapeHtml(item.Album)}`); } else if (item.Album) { parentNameHtml.push(escapeHtml(item.Album)); } @@ -1982,7 +1983,7 @@ export default function (view, params) { return; } - playItem(item, item.UserData && mode === 'resume' ? item.UserData.PlaybackPositionTicks : 0); + playItem(item, item.UserData && mode === ItemAction.Resume ? item.UserData.PlaybackPositionTicks : 0); } function onPlayClick() { diff --git a/src/elements/emby-playstatebutton/PlayedButton.tsx b/src/elements/emby-playstatebutton/PlayedButton.tsx index 1c146d396f..a85976e02e 100644 --- a/src/elements/emby-playstatebutton/PlayedButton.tsx +++ b/src/elements/emby-playstatebutton/PlayedButton.tsx @@ -3,8 +3,8 @@ import { useQueryClient } from '@tanstack/react-query'; import React, { type FC, useCallback } from 'react'; import IconButton from '@mui/material/IconButton'; import CheckIcon from '@mui/icons-material/Check'; -import classNames from 'classnames'; +import { ItemAction } from 'constants/itemAction'; import globalize from 'lib/globalize'; import { useTogglePlayedMutation } from 'hooks/useFetchItems'; @@ -59,23 +59,17 @@ const PlayedButton: FC = ({ } }, [itemId, togglePlayedMutation, isPlayed, queryClient, queryKey]); - const btnClass = classNames( - className, - { 'playstatebutton-played': isPlayed } - ); - - const iconClass = classNames( - { 'playstatebutton-icon-played': isPlayed } - ); return ( - + ); }; diff --git a/src/elements/emby-ratingbutton/FavoriteButton.tsx b/src/elements/emby-ratingbutton/FavoriteButton.tsx index 018314cea8..4dfdf3a36b 100644 --- a/src/elements/emby-ratingbutton/FavoriteButton.tsx +++ b/src/elements/emby-ratingbutton/FavoriteButton.tsx @@ -3,7 +3,7 @@ import { useQueryClient } from '@tanstack/react-query'; import IconButton from '@mui/material/IconButton'; import FavoriteIcon from '@mui/icons-material/Favorite'; -import classNames from 'classnames'; +import { ItemAction } from 'constants/itemAction'; import { useToggleFavoriteMutation } from 'hooks/useFetchItems'; import globalize from 'lib/globalize'; @@ -45,24 +45,17 @@ const FavoriteButton: FC = ({ } }, [isFavorite, itemId, queryClient, queryKey, toggleFavoriteMutation]); - const btnClass = classNames( - className, - { 'ratingbutton-withrating': isFavorite } - ); - - const iconClass = classNames( - { 'ratingbutton-icon-withrating': isFavorite } - ); - return ( - + ); }; diff --git a/src/types/cardOptions.ts b/src/types/cardOptions.ts index 3e675d530b..c4fa9f0008 100644 --- a/src/types/cardOptions.ts +++ b/src/types/cardOptions.ts @@ -3,7 +3,10 @@ import type { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image- import type { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models/user-item-data-dto'; import type { BaseItemDtoImageBlurHashes } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto-image-blur-hashes'; import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; + +import { ItemAction } from 'constants/itemAction'; import { CardShape } from 'utils/card'; + import type { NullableString } from './base/common/shared/types'; import type { ItemDto } from './base/models/item-dto'; import type { ParentId } from './library'; @@ -44,7 +47,7 @@ export interface CardOptions { showChildCountIndicator?: boolean; lines?: number; context?: CollectionType; - action?: string | null; + action?: ItemAction | null; indexBy?: string; parentId?: ParentId; showMenu?: boolean; diff --git a/src/types/dataAttributes.ts b/src/types/dataAttributes.ts index b9ae880f7a..44d0101454 100644 --- a/src/types/dataAttributes.ts +++ b/src/types/dataAttributes.ts @@ -1,5 +1,8 @@ import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import type { UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models/user-item-data-dto'; + +import { ItemAction } from 'constants/itemAction'; + import type { NullableBoolean, NullableNumber, NullableString } from './base/common/shared/types'; export type AttributesOpts = { @@ -8,7 +11,7 @@ export type AttributesOpts = { collectionId?: NullableString, playlistId?: NullableString, prefix?: NullableString, - action?: NullableString, + action?: ItemAction | null, itemServerId?: NullableString, itemId?: NullableString, itemTimerId?: NullableString, @@ -43,7 +46,7 @@ export type DataAttributes = { 'data-startdate'?: NullableString; 'data-enddate'?: NullableString; 'data-prefix'?: NullableString; - 'data-action'?: NullableString; + 'data-action'?: ItemAction | null; 'data-positionticks'?: NullableNumber; 'data-isfolder'?: NullableBoolean; }; diff --git a/src/types/listOptions.ts b/src/types/listOptions.ts index 4de41fd353..4f1653fe4c 100644 --- a/src/types/listOptions.ts +++ b/src/types/listOptions.ts @@ -1,13 +1,16 @@ import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import type { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'; -import type { ItemDto } from './base/models/item-dto'; + import type { TextLineOpts } from 'components/common/textLines/types'; +import { ItemAction } from 'constants/itemAction'; + +import type { ItemDto } from './base/models/item-dto'; export interface ListOptions extends TextLineOpts { items?: ItemDto[] | null; index?: string; showIndex?: boolean; - action?: string | null; + action?: ItemAction | null; imageSize?: string; enableOverview?: boolean; enableSideMediaInfo?: boolean; diff --git a/src/utils/sections.ts b/src/utils/sections.ts index c0f4500fb2..c3a25980e0 100644 --- a/src/utils/sections.ts +++ b/src/utils/sections.ts @@ -4,9 +4,11 @@ import { ItemFilter } from '@jellyfin/sdk/lib/generated-client/models/item-filte import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'; import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; + +import { ItemAction } from 'constants/itemAction'; import * as userSettings from 'scripts/settings/userSettings'; -import { CardShape } from 'utils/card'; import { type Section, SectionType, SectionApiMethod } from 'types/sections'; +import { CardShape } from 'utils/card'; export const getSuggestionSections = (): Section[] => { const parametersOptions = { @@ -130,7 +132,7 @@ export const getSuggestionSections = (): Section[] => { showUnplayedIndicator: false, shape: CardShape.SquareOverflow, showParentTitle: true, - action: 'instantmix', + action: ItemAction.InstantMix, overlayMoreButton: true, coverImage: true } @@ -149,7 +151,7 @@ export const getSuggestionSections = (): Section[] => { showUnplayedIndicator: false, shape: CardShape.SquareOverflow, showParentTitle: true, - action: 'instantmix', + action: ItemAction.InstantMix, overlayMoreButton: true, coverImage: true } From 56d23e13ebe132172f04b9fee771b6e5459fa61e Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 3 Dec 2025 17:53:19 -0500 Subject: [PATCH 2/2] Fix vite path support --- package-lock.json | 73 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + vite.config.ts | 2 ++ 3 files changed, 76 insertions(+) diff --git a/package-lock.json b/package-lock.json index 151970d217..eb59beb572 100644 --- a/package-lock.json +++ b/package-lock.json @@ -129,6 +129,7 @@ "ts-loader": "9.5.2", "typescript": "5.8.3", "typescript-eslint": "8.35.1", + "vite-tsconfig-paths": "5.1.4", "vitest": "3.2.4", "webpack": "5.99.9", "webpack-bundle-analyzer": "4.10.2", @@ -12368,6 +12369,13 @@ "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=", "dev": true }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, "node_modules/gonzales-pe": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", @@ -23186,6 +23194,27 @@ "node": ">=8" } }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -23836,6 +23865,26 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", @@ -32683,6 +32732,12 @@ "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=", "dev": true }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "gonzales-pe": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", @@ -40117,6 +40172,13 @@ } } }, + "tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "requires": {} + }, "tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -40558,6 +40620,17 @@ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" } }, + "vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + } + }, "vitest": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", diff --git a/package.json b/package.json index f46cb3b95f..ce650cc38e 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "ts-loader": "9.5.2", "typescript": "5.8.3", "typescript-eslint": "8.35.1", + "vite-tsconfig-paths": "5.1.4", "vitest": "3.2.4", "webpack": "5.99.9", "webpack-bundle-analyzer": "4.10.2", diff --git a/vite.config.ts b/vite.config.ts index 062c71f68d..3b67d7441d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,10 @@ /// /// import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ + plugins: [ tsconfigPaths() ], test: { coverage: { include: [ 'src' ]