Use enum for item actions

This commit is contained in:
Bill Thornton
2025-12-03 17:28:45 -05:00
parent 7e663d57f1
commit cb8b2836c2
33 changed files with 318 additions and 216 deletions
@@ -9,6 +9,7 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { type FC, useCallback } from 'react'; import React, { type FC, useCallback } from 'react';
import { ItemAction } from 'constants/itemAction';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { useLocalStorage } from 'hooks/useLocalStorage'; import { useLocalStorage } from 'hooks/useLocalStorage';
import { useGetItemsViewByType } from 'hooks/useFetchItems'; import { useGetItemsViewByType } from 'hooks/useFetchItems';
@@ -99,7 +100,7 @@ const ItemsView: FC<ItemsViewProps> = ({
if (viewType === LibraryTab.Songs) { if (viewType === LibraryTab.Songs) {
listOptions.showParentTitle = true; listOptions.showParentTitle = true;
listOptions.action = 'playallfromhere'; listOptions.action = ItemAction.PlayAllFromHere;
listOptions.smallIcon = true; listOptions.smallIcon = true;
listOptions.showArtist = true; listOptions.showArtist = true;
listOptions.addToListButton = true; listOptions.addToListButton = true;
@@ -1,14 +1,16 @@
import React, { type FC } from 'react'; 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 NoItemsMessage from 'components/common/NoItemsMessage';
import SectionContainer from 'components/common/SectionContainer'; 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 { ParentId } from 'types/library';
import type { Section, SectionType } from 'types/sections'; import type { Section, SectionType } from 'types/sections';
import { CardShape } from 'utils/card';
interface ProgramsSectionViewProps { interface ProgramsSectionViewProps {
parentId: ParentId; parentId: ParentId;
@@ -92,7 +94,7 @@ const ProgramsSectionView: FC<ProgramsSectionViewProps> = ({
showChannelName: false, showChannelName: false,
cardLayout: true, cardLayout: true,
centerText: false, centerText: false,
action: 'edit', action: ItemAction.Edit,
cardFooterAside: 'none', cardFooterAside: 'none',
preferThumb: true, preferThumb: true,
coverImage: true, coverImage: true,
@@ -4,6 +4,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import ReplayIcon from '@mui/icons-material/Replay'; import ReplayIcon from '@mui/icons-material/Replay';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { ItemAction } from 'constants/itemAction';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import { getChannelQuery } from 'hooks/api/liveTvHooks/useGetChannel'; import { getChannelQuery } from 'hooks/api/liveTvHooks/useGetChannel';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
@@ -76,7 +77,7 @@ const PlayOrResumeButton: FC<PlayOrResumeButtonProps> = ({
return ( return (
<IconButton <IconButton
className='button-flat btnPlayOrResume' className='button-flat btnPlayOrResume'
data-action={isResumable ? 'resume' : 'play'} data-action={isResumable ? ItemAction.Resume : ItemAction.Play}
title={ title={
isResumable ? isResumable ?
globalize.translate('ButtonResume') : globalize.translate('ButtonResume') :
+6 -5
View File
@@ -1,17 +1,18 @@
import React, { type FC } from 'react'; import React, { type FC } from 'react';
import layoutManager from 'components/layoutManager'; import layoutManager from 'components/layoutManager';
import { ItemAction } from 'constants/itemAction';
import { CardShape } from 'utils/card';
import type { ItemDto } from 'types/base/models/item-dto';
import type { CardOptions } from 'types/cardOptions';
import CardOverlayButtons from './CardOverlayButtons'; import CardOverlayButtons from './CardOverlayButtons';
import CardHoverMenu from './CardHoverMenu'; import CardHoverMenu from './CardHoverMenu';
import CardOuterFooter from './CardOuterFooter'; import CardOuterFooter from './CardOuterFooter';
import CardContent from './CardContent'; import CardContent from './CardContent';
import { CardShape } from 'utils/card';
import type { ItemDto } from 'types/base/models/item-dto';
import type { CardOptions } from 'types/cardOptions';
interface CardBoxProps { interface CardBoxProps {
action: string; action: ItemAction;
item: ItemDto; item: ItemDto;
cardOptions: CardOptions; cardOptions: CardOptions;
className: string; className: string;
@@ -2,12 +2,14 @@ import React, { type FC } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import ButtonGroup from '@mui/material/ButtonGroup'; import ButtonGroup from '@mui/material/ButtonGroup';
import classNames from 'classnames'; import classNames from 'classnames';
import { appRouter } from 'components/router/appRouter'; import { appRouter } from 'components/router/appRouter';
import itemHelper from 'components/itemHelper'; import itemHelper from 'components/itemHelper';
import { playbackManager } from 'components/playback/playbackmanager'; import { playbackManager } from 'components/playback/playbackmanager';
import { ItemAction } from 'constants/itemAction';
import PlayedButton from 'elements/emby-playstatebutton/PlayedButton'; import PlayedButton from 'elements/emby-playstatebutton/PlayedButton';
import FavoriteButton from 'elements/emby-ratingbutton/FavoriteButton'; import FavoriteButton from 'elements/emby-ratingbutton/FavoriteButton';
import PlayArrowIconButton from '../../common/PlayArrowIconButton'; import PlayArrowIconButton from '../../common/PlayArrowIconButton';
import MoreVertIconButton from '../../common/MoreVertIconButton'; import MoreVertIconButton from '../../common/MoreVertIconButton';
@@ -15,7 +17,7 @@ import type { ItemDto } from 'types/base/models/item-dto';
import type { CardOptions } from 'types/cardOptions'; import type { CardOptions } from 'types/cardOptions';
interface CardHoverMenuProps { interface CardHoverMenuProps {
action: string, action: ItemAction,
item: ItemDto; item: ItemDto;
cardOptions: CardOptions; cardOptions: CardOptions;
} }
@@ -51,7 +53,7 @@ const CardHoverMenu: FC<CardHoverMenuProps> = ({
{playbackManager.canPlay(item) && ( {playbackManager.canPlay(item) && (
<PlayArrowIconButton <PlayArrowIconButton
className={centerPlayButtonClass} className={centerPlayButtonClass}
action='play' action={ItemAction.Play}
title='Play' title='Play'
/> />
)} )}
@@ -2,15 +2,17 @@ import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location
import React, { type FC } from 'react'; import React, { type FC } from 'react';
import ButtonGroup from '@mui/material/ButtonGroup'; import ButtonGroup from '@mui/material/ButtonGroup';
import classNames from 'classnames'; 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 { ItemKind } from 'types/base/models/item-kind';
import { ItemMediaKind } from 'types/base/models/item-media-kind'; import { ItemMediaKind } from 'types/base/models/item-media-kind';
import type { ItemDto } from 'types/base/models/item-dto'; import type { ItemDto } from 'types/base/models/item-dto';
import type { CardOptions } from 'types/cardOptions'; import type { CardOptions } from 'types/cardOptions';
import PlayArrowIconButton from '../../common/PlayArrowIconButton';
import MoreVertIconButton from '../../common/MoreVertIconButton';
const sholudShowOverlayPlayButton = ( const sholudShowOverlayPlayButton = (
overlayPlayButton: boolean | undefined, overlayPlayButton: boolean | undefined,
item: ItemDto item: ItemDto
@@ -78,7 +80,7 @@ const CardOverlayButtons: FC<CardOverlayButtonsProps> = ({
{cardOptions.centerPlayButton && ( {cardOptions.centerPlayButton && (
<PlayArrowIconButton <PlayArrowIconButton
className={centerPlayButtonClass} className={centerPlayButtonClass}
action='play' action={ItemAction.Play}
title='Play' title='Play'
/> />
)} )}
@@ -87,7 +89,7 @@ const CardOverlayButtons: FC<CardOverlayButtonsProps> = ({
{sholudShowOverlayPlayButton(overlayPlayButton, item) && ( {sholudShowOverlayPlayButton(overlayPlayButton, item) && (
<PlayArrowIconButton <PlayArrowIconButton
className={btnCssClass} className={btnCssClass}
action='play' action={ItemAction.Play}
title='Play' title='Play'
/> />
)} )}
@@ -7,6 +7,7 @@ import { getImageApi } from '@jellyfin/sdk/lib/utils/api/image-api';
import { appRouter } from 'components/router/appRouter'; import { appRouter } from 'components/router/appRouter';
import layoutManager from 'components/layoutManager'; import layoutManager from 'components/layoutManager';
import itemHelper from 'components/itemHelper'; import itemHelper from 'components/itemHelper';
import { ItemAction } from 'constants/itemAction';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import datetime from 'scripts/datetime'; import datetime from 'scripts/datetime';
import { isUsingLiveTvNaming } from '../cardBuilderUtils'; import { isUsingLiveTvNaming } from '../cardBuilderUtils';
@@ -88,7 +89,7 @@ export function getTextActionButton(
const dataAttributes = getDataAttributes( const dataAttributes = getDataAttributes(
{ {
action: 'link', action: ItemAction.Link,
itemServerId: serverId ?? item.ServerId, itemServerId: serverId ?? item.ServerId,
itemId: item.Id, itemId: item.Id,
itemChannelId: item.ChannelId, itemChannelId: item.ChannelId,
+11 -9
View File
@@ -1,17 +1,19 @@
import classNames from 'classnames'; 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 useCardImageUrl from './useCardImageUrl';
import { import {
resolveAction, resolveAction,
resolveMixedShapeByAspectRatio resolveMixedShapeByAspectRatio
} from '../cardBuilderUtils'; } 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 { interface UseCardProps {
item: ItemDto; item: ItemDto;
@@ -20,7 +22,7 @@ interface UseCardProps {
function useCard({ item, cardOptions }: UseCardProps) { function useCard({ item, cardOptions }: UseCardProps) {
const action = resolveAction({ const action = resolveAction({
defaultAction: cardOptions.action ?? 'link', defaultAction: cardOptions.action ?? ItemAction.Link,
isFolder: item.IsFolder ?? false, isFolder: item.IsFolder ?? false,
isPhoto: item.MediaType === ItemMediaKind.Photo isPhoto: item.MediaType === ItemMediaKind.Photo
}); });
+11 -10
View File
@@ -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 { PersonKind } from '@jellyfin/sdk/lib/generated-client/models/person-kind';
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import { ItemAction } from 'constants/itemAction';
import browser from 'scripts/browser'; import browser from 'scripts/browser';
import datetime from 'scripts/datetime'; import datetime from 'scripts/datetime';
import dom from 'utils/dom'; import dom from 'utils/dom';
@@ -514,7 +515,7 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
const showOtherText = flags.isOuterFooter ? !flags.overlayText : flags.overlayText; const showOtherText = flags.isOuterFooter ? !flags.overlayText : flags.overlayText;
if (flags.isOuterFooter && options.cardLayout && layoutManager.mobile && options.cardFooterAside !== 'none') { if (flags.isOuterFooter && options.cardLayout && layoutManager.mobile && options.cardFooterAside !== 'none') {
html += `<button is="paper-icon-button-light" class="itemAction btnCardOptions cardText-secondary" data-action="menu" title="${globalize.translate('ButtonMore')}"><span class="material-icons more_vert" aria-hidden="true"></span></button>`; html += `<button is="paper-icon-button-light" class="itemAction btnCardOptions cardText-secondary" data-action="${ItemAction.Menu}" title="${globalize.translate('ButtonMore')}"><span class="material-icons more_vert" aria-hidden="true"></span></button>`;
} }
const cssClass = options.centerText ? 'cardText cardTextCentered' : 'cardText'; const cssClass = options.centerText ? 'cardText cardTextCentered' : 'cardText';
@@ -776,7 +777,7 @@ function getTextActionButton(item, text, serverId) {
} }
const url = appRouter.getRouteUrl(item); const url = appRouter.getRouteUrl(item);
let html = '<a href="' + url + '" ' + itemShortcuts.getShortcutAttributesHtml(item, serverId) + ' class="itemAction textActionButton" title="' + text + '" data-action="link">'; let html = '<a href="' + url + '" ' + itemShortcuts.getShortcutAttributesHtml(item, serverId) + ' class="itemAction textActionButton" title="' + text + `" data-action="${ItemAction.Link}">`;
html += text; html += text;
html += '</a>'; html += '</a>';
@@ -885,7 +886,7 @@ function importRefreshIndicator() {
*/ */
function buildCard(index, item, apiClient, options) { function buildCard(index, item, apiClient, options) {
const action = resolveAction({ const action = resolveAction({
defaultAction: options.action || 'link', defaultAction: options.action || ItemAction.Link,
isFolder: item.IsFolder, isFolder: item.IsFolder,
isPhoto: item.MediaType === 'Photo' isPhoto: item.MediaType === 'Photo'
}); });
@@ -985,15 +986,15 @@ function buildCard(index, item, apiClient, options) {
const btnCssClass = 'cardOverlayButton cardOverlayButton-br itemAction'; const btnCssClass = 'cardOverlayButton cardOverlayButton-br itemAction';
if (options.centerPlayButton) { if (options.centerPlayButton) {
overlayButtons += `<button is="paper-icon-button-light" class="${btnCssClass} cardOverlayButton-centered" data-action="play" title="${globalize.translate('Play')}"><span class="material-icons cardOverlayButtonIcon play_arrow" aria-hidden="true"></span></button>`; overlayButtons += `<button is="paper-icon-button-light" class="${btnCssClass} cardOverlayButton-centered" data-action="${ItemAction.Play}" title="${globalize.translate('Play')}"><span class="material-icons cardOverlayButtonIcon play_arrow" aria-hidden="true"></span></button>`;
} }
if (overlayPlayButton && !item.IsPlaceHolder && (item.LocationType !== 'Virtual' || !item.MediaType || item.Type === 'Program') && item.Type !== 'Person') { if (overlayPlayButton && !item.IsPlaceHolder && (item.LocationType !== 'Virtual' || !item.MediaType || item.Type === 'Program') && item.Type !== 'Person') {
overlayButtons += `<button is="paper-icon-button-light" class="${btnCssClass}" data-action="play" title="${globalize.translate('Play')}"><span class="material-icons cardOverlayButtonIcon play_arrow" aria-hidden="true"></span></button>`; overlayButtons += `<button is="paper-icon-button-light" class="${btnCssClass}" data-action="${ItemAction.Play}" title="${globalize.translate('Play')}"><span class="material-icons cardOverlayButtonIcon play_arrow" aria-hidden="true"></span></button>`;
} }
if (options.overlayMoreButton) { if (options.overlayMoreButton) {
overlayButtons += `<button is="paper-icon-button-light" class="${btnCssClass}" data-action="menu" title="${globalize.translate('ButtonMore')}"><span class="material-icons cardOverlayButtonIcon more_vert" aria-hidden="true"></span></button>`; overlayButtons += `<button is="paper-icon-button-light" class="${btnCssClass}" data-action="${ItemAction.Menu}" title="${globalize.translate('ButtonMore')}"><span class="material-icons cardOverlayButtonIcon more_vert" aria-hidden="true"></span></button>`;
} }
} }
@@ -1156,7 +1157,7 @@ function getHoverMenuHtml(item, action) {
const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light'; const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light';
if (playbackManager.canPlay(item)) { if (playbackManager.canPlay(item)) {
html += '<button is="paper-icon-button-light" class="' + btnCssClass + ' cardOverlayFab-primary" data-action="resume"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover play_arrow" aria-hidden="true"></span></button>'; html += `<button is="paper-icon-button-light" class="${btnCssClass} cardOverlayFab-primary" data-action="${ItemAction.Resume}"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover play_arrow" aria-hidden="true"></span></button>`;
} }
html += '<div class="cardOverlayButton-br flex">'; html += '<div class="cardOverlayButton-br flex">';
@@ -1165,17 +1166,17 @@ function getHoverMenuHtml(item, action) {
if (itemHelper.canMarkPlayed(item)) { if (itemHelper.canMarkPlayed(item)) {
import('../../elements/emby-playstatebutton/emby-playstatebutton'); import('../../elements/emby-playstatebutton/emby-playstatebutton');
html += '<button is="emby-playstatebutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check" aria-hidden="true"></span></button>'; html += `<button is="emby-playstatebutton" type="button" data-action="${ItemAction.None}" class="${btnCssClass}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-itemtype="${item.Type}" data-played="${userData.Played}"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check" aria-hidden="true"></span></button>`;
} }
if (itemHelper.canRate(item)) { if (itemHelper.canRate(item)) {
const likes = userData.Likes == null ? '' : userData.Likes; const likes = userData.Likes == null ? '' : userData.Likes;
import('../../elements/emby-ratingbutton/emby-ratingbutton'); import('../../elements/emby-ratingbutton/emby-ratingbutton');
html += '<button is="emby-ratingbutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite" aria-hidden="true"></span></button>'; html += `<button is="emby-ratingbutton" type="button" data-action="${ItemAction.None}" class="${btnCssClass}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-itemtype="${item.Type}" data-likes="${likes}" data-isfavorite="${userData.IsFavorite}"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite" aria-hidden="true"></span></button>`;
} }
html += `<button is="paper-icon-button-light" class="${btnCssClass}" data-action="menu" title="${globalize.translate('ButtonMore')}"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover more_vert" aria-hidden="true"></span></button>`; html += `<button is="paper-icon-button-light" class="${btnCssClass}" data-action="${ItemAction.Menu}" title="${globalize.translate('ButtonMore')}"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover more_vert" aria-hidden="true"></span></button>`;
html += '</div>'; html += '</div>';
html += '</div>'; html += '</div>';
@@ -11,6 +11,7 @@ import {
resolveCardImageContainerCssClasses, resolveCardImageContainerCssClasses,
resolveMixedShapeByAspectRatio resolveMixedShapeByAspectRatio
} from './cardBuilderUtils'; } from './cardBuilderUtils';
import { ItemAction } from 'constants/itemAction';
describe('getDesiredAspect', () => { describe('getDesiredAspect', () => {
test('"portrait" (case insensitive)', () => { test('"portrait" (case insensitive)', () => {
@@ -441,11 +442,11 @@ describe('isResizable', () => {
}); });
describe('resolveAction', () => { 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', () => { describe('resolveMixedShapeByAspectRatio', () => {
@@ -1,7 +1,9 @@
import { CardShape } from '../../utils/card';
import { randomInt } from '../../utils/number';
import classNames from 'classnames'; import classNames from 'classnames';
import { ItemAction } from 'constants/itemAction';
import { CardShape } from 'utils/card';
import { randomInt } from 'utils/number';
const ASPECT_RATIOS = { const ASPECT_RATIOS = {
portrait: (2 / 3), portrait: (2 / 3),
backdrop: (16 / 9), backdrop: (16 / 9),
@@ -20,12 +22,12 @@ export const isUsingLiveTvNaming = (itemType: string | null | undefined): boolea
* Resolves Card action to display * Resolves Card action to display
* @param opts options to determine the action to return * @param opts options to determine the action to return
*/ */
export const resolveAction = (opts: { defaultAction: string, isFolder: boolean, isPhoto: boolean }): string => { export const resolveAction = (opts: { defaultAction: ItemAction, isFolder: boolean, isPhoto: boolean }): ItemAction => {
if (opts.defaultAction === 'play' && opts.isFolder) { if (opts.defaultAction === ItemAction.Play && opts.isFolder) {
// If this hard-coding is ever removed make sure to test nested photo albums // If this hard-coding is ever removed make sure to test nested photo albums
return 'link'; return ItemAction.Link;
} else if (opts.isPhoto) { } else if (opts.isPhoto) {
return 'play'; return ItemAction.Play;
} else { } else {
return opts.defaultAction; return opts.defaultAction;
} }
+3 -1
View File
@@ -1,6 +1,8 @@
import React, { type FC } from 'react'; import React, { type FC } from 'react';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import InfoIcon from '@mui/icons-material/Info'; import InfoIcon from '@mui/icons-material/Info';
import { ItemAction } from 'constants/itemAction';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
interface InfoIconButtonProps { interface InfoIconButtonProps {
@@ -11,7 +13,7 @@ const InfoIconButton: FC<InfoIconButtonProps> = ({ className }) => {
return ( return (
<IconButton <IconButton
className={className} className={className}
data-action='link' data-action={ItemAction.Link}
title={globalize.translate('ButtonInfo')} title={globalize.translate('ButtonInfo')}
> >
<InfoIcon /> <InfoIcon />
+3 -1
View File
@@ -1,6 +1,8 @@
import React, { type FC } from 'react'; import React, { type FC } from 'react';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import MoreVertIcon from '@mui/icons-material/MoreVert'; import MoreVertIcon from '@mui/icons-material/MoreVert';
import { ItemAction } from 'constants/itemAction';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
interface MoreVertIconButtonProps { interface MoreVertIconButtonProps {
@@ -12,7 +14,7 @@ const MoreVertIconButton: FC<MoreVertIconButtonProps> = ({ className, iconClassN
return ( return (
<IconButton <IconButton
className={className} className={className}
data-action='menu' data-action={ItemAction.Menu}
title={globalize.translate('ButtonMore')} title={globalize.translate('ButtonMore')}
> >
<MoreVertIcon className={iconClassName} /> <MoreVertIcon className={iconClassName} />
@@ -1,11 +1,13 @@
import React, { type FC } from 'react'; import React, { type FC } from 'react';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import { ItemAction } from 'constants/itemAction';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
interface PlayArrowIconButtonProps { interface PlayArrowIconButtonProps {
className: string; className: string;
action: string; action: ItemAction;
title: string; title: string;
iconClassName?: string; iconClassName?: string;
} }
@@ -1,6 +1,8 @@
import React, { type FC } from 'react'; import React, { type FC } from 'react';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import { ItemAction } from 'constants/itemAction';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
interface PlaylistAddIconButtonProps { interface PlaylistAddIconButtonProps {
@@ -11,7 +13,7 @@ const PlaylistAddIconButton: FC<PlaylistAddIconButtonProps> = ({ className }) =>
return ( return (
<IconButton <IconButton
className={className} className={className}
data-action='addtoplaylist' data-action={ItemAction.AddToPlaylist}
title={globalize.translate('AddToPlaylist')} title={globalize.translate('AddToPlaylist')}
> >
<PlaylistAddIcon /> <PlaylistAddIcon />
+3 -1
View File
@@ -1,6 +1,8 @@
import React, { type FC } from 'react'; import React, { type FC } from 'react';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import { ItemAction } from 'constants/itemAction';
interface RightIconButtonsProps { interface RightIconButtonsProps {
className?: string; className?: string;
id: string; id: string;
@@ -12,7 +14,7 @@ const RightIconButtons: FC<RightIconButtonsProps> = ({ className, id, title, ico
return ( return (
<IconButton <IconButton
className={className} className={className}
data-action='custom' data-action={ItemAction.Custom}
data-customaction={id} data-customaction={id}
title={title} title={title}
> >
+8 -3
View File
@@ -1,8 +1,11 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import { ItemAction } from 'constants/itemAction';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import inputManager from '../../scripts/inputManager'; import inputManager from '../../scripts/inputManager';
import browser from '../../scripts/browser'; import browser from '../../scripts/browser';
import globalize from '../../lib/globalize'; import globalize from '../../lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import Events from '../../utils/events.ts'; import Events from '../../utils/events.ts';
import scrollHelper from '../../scripts/scrollHelper'; import scrollHelper from '../../scripts/scrollHelper';
import serverNotifications from '../../scripts/serverNotifications'; import serverNotifications from '../../scripts/serverNotifications';
@@ -15,6 +18,7 @@ import imageLoader from '../images/imageLoader';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import itemShortcuts from '../shortcuts'; import itemShortcuts from '../shortcuts';
import dom from '../../utils/dom'; import dom from '../../utils/dom';
import './guide.scss'; import './guide.scss';
import './programs.scss'; import './programs.scss';
import 'material-design-icons-iconfont'; import 'material-design-icons-iconfont';
@@ -26,6 +30,7 @@ import '../../elements/emby-tabs/emby-tabs';
import '../../elements/emby-scroller/emby-scroller'; import '../../elements/emby-scroller/emby-scroller';
import '../../styles/flexstyles.scss'; import '../../styles/flexstyles.scss';
import 'webcomponents.js/webcomponents-lite'; import 'webcomponents.js/webcomponents-lite';
import template from './tvguide.template.html'; import template from './tvguide.template.html';
function showViewSettings(instance) { function showViewSettings(instance) {
@@ -441,7 +446,7 @@ function Guide(options) {
html += '<div class="' + outerCssClass + '" data-channelid="' + channel.Id + '">'; html += '<div class="' + outerCssClass + '" data-channelid="' + channel.Id + '">';
const clickAction = layoutManager.tv ? 'link' : 'programdialog'; const clickAction = layoutManager.tv ? ItemAction.Link : ItemAction.ProgramDialog;
const categories = self.categoryOptions.categories || []; const categories = self.categoryOptions.categories || [];
const displayMovieContent = !categories.length || categories.indexOf('movies') !== -1; const displayMovieContent = !categories.length || categories.indexOf('movies') !== -1;
@@ -607,7 +612,7 @@ function Guide(options) {
title.push(channel.Name); title.push(channel.Name);
} }
html += '<button title="' + escapeHtml(title.join(' ')) + '" type="button" class="' + cssClass + '"' + ' data-action="link" data-isfolder="' + channel.IsFolder + '" data-id="' + channel.Id + '" data-serverid="' + channel.ServerId + '" data-type="' + channel.Type + '">'; html += `<button title="${escapeHtml(title.join(' '))}" type="button" class="${cssClass}" data-action="${ItemAction.Link}" data-isfolder="${channel.IsFolder}" data-id="${channel.Id}" data-serverid="${channel.ServerId}" data-type="${channel.Type}">`;
if (hasChannelImage) { if (hasChannelImage) {
const url = apiClient.getScaledImageUrl(channel.Id, { const url = apiClient.getScaledImageUrl(channel.Id, {
+3 -1
View File
@@ -4,6 +4,8 @@ import DragHandleIcon from '@mui/icons-material/DragHandle';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import useIndicator from 'components/indicators/useIndicator'; import useIndicator from 'components/indicators/useIndicator';
import { ItemAction } from 'constants/itemAction';
import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo'; import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo';
import ListContentWrapper from './ListContentWrapper'; import ListContentWrapper from './ListContentWrapper';
import ListItemBody from './ListItemBody'; import ListItemBody from './ListItemBody';
@@ -20,7 +22,7 @@ interface ListContentProps {
enableOverview?: boolean; enableOverview?: boolean;
enableSideMediaInfo?: boolean; enableSideMediaInfo?: boolean;
clickEntireItem?: boolean; clickEntireItem?: boolean;
action?: string; action?: ItemAction;
isLargeStyle: boolean; isLargeStyle: boolean;
downloadWidth?: number; downloadWidth?: number;
} }
@@ -1,7 +1,14 @@
import React, { type FC } from 'react'; import React, { type FC } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Media from 'components/common/Media';
import PlayArrowIconButton from 'components/common/PlayArrowIconButton';
import { ItemAction } from 'constants/itemAction';
import { useApi } from 'hooks/useApi'; import { useApi } from 'hooks/useApi';
import type { ItemDto } from 'types/base/models/item-dto';
import type { ListOptions } from 'types/listOptions';
import useIndicator from '../../indicators/useIndicator'; import useIndicator from '../../indicators/useIndicator';
import layoutManager from '../../layoutManager'; import layoutManager from '../../layoutManager';
import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils'; import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils';
@@ -11,15 +18,10 @@ import {
getImageUrl getImageUrl
} from './listHelper'; } from './listHelper';
import Media from 'components/common/Media';
import PlayArrowIconButton from 'components/common/PlayArrowIconButton';
import type { ItemDto } from 'types/base/models/item-dto';
import type { ListOptions } from 'types/listOptions';
interface ListImageContainerProps { interface ListImageContainerProps {
item: ItemDto; item: ItemDto;
listOptions: ListOptions; listOptions: ListOptions;
action?: string | null; action?: ItemAction | null;
isLargeStyle: boolean; isLargeStyle: boolean;
clickEntireItem?: boolean; clickEntireItem?: boolean;
downloadWidth?: number; downloadWidth?: number;
@@ -55,7 +57,7 @@ const ListImageContainer: FC<ListImageContainerProps> = ({
const playOnImageClick = listOptions.imagePlayButton && !layoutManager.tv; const playOnImageClick = listOptions.imagePlayButton && !layoutManager.tv;
const imageAction = playOnImageClick ? 'link' : action; const imageAction = playOnImageClick ? ItemAction.Link : action;
const btnCssClass = const btnCssClass =
'paper-icon-button-light listItemImageButton itemAction'; 'paper-icon-button-light listItemImageButton itemAction';
@@ -85,7 +87,7 @@ const ListImageContainer: FC<ListImageContainerProps> = ({
<PlayArrowIconButton <PlayArrowIconButton
className={btnCssClass} className={btnCssClass}
action={ action={
canResume(playbackPositionTicks) ? 'resume' : 'play' canResume(playbackPositionTicks) ? ItemAction.Resume : ItemAction.Play
} }
title={ title={
canResume(playbackPositionTicks) ? canResume(playbackPositionTicks) ?
@@ -3,15 +3,16 @@ import classNames from 'classnames';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import TextLines from 'components/common/textLines/TextLines'; import TextLines from 'components/common/textLines/TextLines';
import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo'; import { ItemAction } from 'constants/itemAction';
import type { ItemDto } from 'types/base/models/item-dto'; import type { ItemDto } from 'types/base/models/item-dto';
import type { ListOptions } from 'types/listOptions'; import type { ListOptions } from 'types/listOptions';
import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo';
interface ListItemBodyProps { interface ListItemBodyProps {
item: ItemDto; item: ItemDto;
listOptions: ListOptions; listOptions: ListOptions;
action?: string | null; action?: ItemAction | null;
isLargeStyle?: boolean; isLargeStyle?: boolean;
clickEntireItem?: boolean; clickEntireItem?: boolean;
enableContentWrapper?: boolean; enableContentWrapper?: boolean;
+5 -2
View File
@@ -2,13 +2,16 @@ import classNames from 'classnames';
import React, { type FC, type PropsWithChildren } from 'react'; import React, { type FC, type PropsWithChildren } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import layoutManager from '../../layoutManager';
import { ItemAction } from 'constants/itemAction';
import type { DataAttributes } from 'types/dataAttributes'; import type { DataAttributes } from 'types/dataAttributes';
import layoutManager from '../../layoutManager';
interface ListWrapperProps { interface ListWrapperProps {
index: number | undefined; index: number | undefined;
title?: string | null; title?: string | null;
action?: string | null; action?: ItemAction | null;
dataAttributes?: DataAttributes; dataAttributes?: DataAttributes;
className?: string; className?: string;
} }
+4 -2
View File
@@ -1,6 +1,8 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { getDataAttributes } from 'utils/items';
import layoutManager from 'components/layoutManager'; import layoutManager from 'components/layoutManager';
import { ItemAction } from 'constants/itemAction';
import { getDataAttributes } from 'utils/items';
import type { ItemDto } from 'types/base/models/item-dto'; import type { ItemDto } from 'types/base/models/item-dto';
import type { ListOptions } from 'types/listOptions'; import type { ListOptions } from 'types/listOptions';
@@ -11,7 +13,7 @@ interface UseListProps {
} }
function useList({ item, listOptions }: UseListProps) { function useList({ item, listOptions }: UseListProps) {
const action = listOptions.action ?? 'link'; const action = listOptions.action ?? ItemAction.Link;
const isLargeStyle = listOptions.imageSize === 'large'; const isLargeStyle = listOptions.imageSize === 'large';
const enableOverview = listOptions.enableOverview; const enableOverview = listOptions.enableOverview;
const clickEntireItem = !!layoutManager.tv; const clickEntireItem = !!layoutManager.tv;
+14 -10
View File
@@ -4,7 +4,13 @@
* @module components/listview/listview * @module components/listview/listview
*/ */
import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import markdownIt from 'markdown-it';
import { ItemAction } from 'constants/itemAction';
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
import itemHelper from '../itemHelper'; import itemHelper from '../itemHelper';
import mediaInfo from '../mediainfo/mediainfo'; import mediaInfo from '../mediainfo/mediainfo';
import indicators from '../indicators/indicators'; import indicators from '../indicators/indicators';
@@ -13,12 +19,10 @@ import globalize from '../../lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import datetime from '../../scripts/datetime'; import datetime from '../../scripts/datetime';
import cardBuilder from '../cardbuilder/cardBuilder'; import cardBuilder from '../cardbuilder/cardBuilder';
import './listview.scss'; import './listview.scss';
import '../../elements/emby-ratingbutton/emby-ratingbutton'; import '../../elements/emby-ratingbutton/emby-ratingbutton';
import '../../elements/emby-playstatebutton/emby-playstatebutton'; import '../../elements/emby-playstatebutton/emby-playstatebutton';
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
import markdownIt from 'markdown-it';
import DOMPurify from 'dompurify';
function getIndex(item, options) { function getIndex(item, options) {
if (options.index === 'disc') { if (options.index === 'disc') {
@@ -165,7 +169,7 @@ function getRightButtonsHtml(options) {
for (let i = 0, length = options.rightButtons.length; i < length; i++) { for (let i = 0, length = options.rightButtons.length; i < length; i++) {
const button = options.rightButtons[i]; const button = options.rightButtons[i];
html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="custom" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}" aria-hidden="true"></span></button>`; html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="${ItemAction.Custom}" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}" aria-hidden="true"></span></button>`;
} }
return html; return html;
@@ -175,7 +179,7 @@ export function getListViewHtml(options) {
const items = options.items; const items = options.items;
let groupTitle = ''; let groupTitle = '';
const action = options.action || 'link'; const action = options.action || ItemAction.Link;
const isLargeStyle = options.imageSize === 'large'; const isLargeStyle = options.imageSize === 'large';
const enableOverview = options.enableOverview; const enableOverview = options.enableOverview;
@@ -277,7 +281,7 @@ export function getListViewHtml(options) {
imageClass += ' itemAction'; imageClass += ' itemAction';
} }
const imageAction = playOnImageClick ? 'link' : action; const imageAction = playOnImageClick ? ItemAction.Link : action;
if (imgUrl) { if (imgUrl) {
html += '<div data-action="' + imageAction + '" class="' + imageClass + ' lazy" data-src="' + imgUrl + '" item-icon>'; html += '<div data-action="' + imageAction + '" class="' + imageClass + ' lazy" data-src="' + imgUrl + '" item-icon>';
@@ -298,7 +302,7 @@ export function getListViewHtml(options) {
} }
if (playOnImageClick) { if (playOnImageClick) {
html += '<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="resume"><span class="material-icons listItemImageButton-icon play_arrow" aria-hidden="true"></span></button>'; html += `<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="${ItemAction.Resume}"><span class="material-icons listItemImageButton-icon play_arrow" aria-hidden="true"></span></button>`;
} }
const progressHtml = indicators.getProgressBarHtml(item, { const progressHtml = indicators.getProgressBarHtml(item, {
@@ -449,11 +453,11 @@ export function getListViewHtml(options) {
if (!clickEntireItem) { if (!clickEntireItem) {
if (options.addToListButton) { if (options.addToListButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="addtoplaylist"><span class="material-icons playlist_add" aria-hidden="true"></span></button>'; html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="${ItemAction.AddToPlaylist}"><span class="material-icons playlist_add" aria-hidden="true"></span></button>`;
} }
if (options.infoButton) { if (options.infoButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="link"><span class="material-icons info_outline" aria-hidden="true"></span></button>'; html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="${ItemAction.Link}"><span class="material-icons info_outline" aria-hidden="true"></span></button>`;
} }
if (options.rightButtons) { if (options.rightButtons) {
@@ -474,7 +478,7 @@ export function getListViewHtml(options) {
} }
if (options.moreButton !== false) { if (options.moreButton !== false) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>'; html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="${ItemAction.Menu}"><span class="material-icons more_vert" aria-hidden="true"></span></button>`;
} }
} }
html += '</div>'; html += '</div>';
@@ -3,6 +3,7 @@ import escapeHtml from 'escape-html';
import { getImageUrl } from 'apps/stable/features/playback/utils/image'; import { getImageUrl } from 'apps/stable/features/playback/utils/image';
import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText'; import { getItemTextLines } from 'apps/stable/features/playback/utils/itemText';
import { AppFeature } from 'constants/appFeature'; import { AppFeature } from 'constants/appFeature';
import { ItemAction } from 'constants/itemAction';
import datetime from '../../scripts/datetime'; import datetime from '../../scripts/datetime';
import { clearBackdrop, setBackdrops } from '../backdrop/backdrop'; import { clearBackdrop, setBackdrops } from '../backdrop/backdrop';
@@ -16,6 +17,9 @@ import { ServerConnections } from 'lib/jellyfin-apiclient';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import * as userSettings from '../../scripts/settings/userSettings'; import * as userSettings from '../../scripts/settings/userSettings';
import itemContextMenu from '../itemContextMenu'; import itemContextMenu from '../itemContextMenu';
import toast from '../toast/toast';
import { appRouter } from '../router/appRouter';
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
import '../cardbuilder/card.scss'; import '../cardbuilder/card.scss';
import '../../elements/emby-button/emby-button'; import '../../elements/emby-button/emby-button';
@@ -24,9 +28,6 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
import './remotecontrol.scss'; import './remotecontrol.scss';
import '../../elements/emby-ratingbutton/emby-ratingbutton'; import '../../elements/emby-ratingbutton/emby-ratingbutton';
import '../../elements/emby-slider/emby-slider'; 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 showMuteButton = true;
let showVolumeSlider = 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('nowPlayingPageImageAudio', item.Type === 'Audio');
context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImagePoster', item.Type !== 'Audio'); context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImagePoster', item.Type !== 'Audio');
} else { } else {
imgContainer.innerHTML = '<div class="nowPlayingPageImageContainerNoAlbum"><button data-action="link" class="cardImageContainer coveredImage ' + getDefaultBackgroundClass(item.Name) + ' cardContent cardContent-shadow itemAction"><span class="cardImageIcon material-icons album" aria-hidden="true"></span></button></div>'; imgContainer.innerHTML = `<div class="nowPlayingPageImageContainerNoAlbum"><button data-action="${ItemAction.Link}" class="cardImageContainer coveredImage ${getDefaultBackgroundClass(item.Name)} cardContent cardContent-shadow itemAction"><span class="cardImageIcon material-icons album" aria-hidden="true"></span></button></div>`;
} }
} }
+42 -22
View File
@@ -1,19 +1,20 @@
/** /**
* Module shortcuts. * "Shortcut" action handlers for BaseItems.
* @module components/shortcuts
*/ */
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api'; 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 { playbackManager } from './playback/playbackmanager';
import inputManager from '../scripts/inputManager'; import inputManager from '../scripts/inputManager';
import { appRouter } from './router/appRouter'; import { appRouter } from './router/appRouter';
import globalize from '../lib/globalize'; import globalize from '../lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import dom from '../utils/dom'; import dom from '../utils/dom';
import recordingHelper from './recordingcreator/recordinghelper'; import recordingHelper from './recordingcreator/recordinghelper';
import toast from './toast/toast'; import toast from './toast/toast';
import * as userSettings from '../scripts/settings/userSettings'; import * as userSettings from '../scripts/settings/userSettings';
import { toApi } from 'utils/jellyfin-apiclient/compat';
function playAllFromHere(card, serverId, queue) { function playAllFromHere(card, serverId, queue) {
const parent = card.parentNode; const parent = card.parentNode;
@@ -231,23 +232,28 @@ function executeAction(card, target, action) {
const playableItemId = type === 'Program' ? item.ChannelId : item.Id; const playableItemId = type === 'Program' ? item.ChannelId : item.Id;
if (item.MediaType === 'Photo' && action === 'link') { if (item.MediaType === 'Photo' && action === ItemAction.Link) {
action = 'play'; action = ItemAction.Play;
} }
if (action === 'link') { switch (action) {
case ItemAction.Link:
appRouter.showItem(item, { appRouter.showItem(item, {
context: card.getAttribute('data-context'), context: card.getAttribute('data-context'),
parentId: card.getAttribute('data-parentid') parentId: card.getAttribute('data-parentid')
}); });
} else if (action === 'programdialog') { break;
case ItemAction.ProgramDialog:
showProgramDialog(item); showProgramDialog(item);
} else if (action === 'instantmix') { break;
case ItemAction.InstantMix:
playbackManager.instantMix({ playbackManager.instantMix({
Id: playableItemId, Id: playableItemId,
ServerId: serverId ServerId: serverId
}); });
} else if (action === 'play' || action === 'resume') { break;
case ItemAction.Play:
case ItemAction.Resume: {
const startPositionTicks = parseInt(card.getAttribute('data-positionticks') || '0', 10); const startPositionTicks = parseInt(card.getAttribute('data-positionticks') || '0', 10);
const sortValues = userSettings.getSortValuesLegacy(sortParentId, 'SortName'); const sortValues = userSettings.getSortValuesLegacy(sortParentId, 'SortName');
@@ -264,7 +270,9 @@ function executeAction(card, target, action) {
} else { } else {
console.warn('Unable to play item', item); console.warn('Unable to play item', item);
} }
} else if (action === 'queue') { break;
}
case ItemAction.Queue:
if (playbackManager.isPlaying()) { if (playbackManager.isPlaying()) {
playbackManager.queue({ playbackManager.queue({
ids: [playableItemId], ids: [playableItemId],
@@ -277,15 +285,20 @@ function executeAction(card, target, action) {
serverId: serverId serverId: serverId
}); });
} }
} else if (action === 'playallfromhere') { break;
case ItemAction.PlayAllFromHere:
playAllFromHere(card, serverId); playAllFromHere(card, serverId);
} else if (action === 'queueallfromhere') { break;
case ItemAction.QueueAllFromHere:
playAllFromHere(card, serverId, true); playAllFromHere(card, serverId, true);
} else if (action === 'setplaylistindex') { break;
case ItemAction.SetPlaylistIndex:
playbackManager.setCurrentPlaylistItem(card.getAttribute('data-playlistitemid')); playbackManager.setCurrentPlaylistItem(card.getAttribute('data-playlistitemid'));
} else if (action === 'record') { break;
case ItemAction.Record:
onRecordCommand(serverId, id, type, card.getAttribute('data-timerid'), card.getAttribute('data-seriestimerid')); onRecordCommand(serverId, id, type, card.getAttribute('data-timerid'), card.getAttribute('data-seriestimerid'));
} else if (action === 'menu') { break;
case ItemAction.Menu: {
const options = target.getAttribute('data-playoptions') === 'false' ? const options = target.getAttribute('data-playoptions') === 'false' ?
{ {
shuffle: false, shuffle: false,
@@ -300,17 +313,23 @@ function executeAction(card, target, action) {
options.positionTo = target; options.positionTo = target;
showContextMenu(card, options); showContextMenu(card, options);
} else if (action === 'playmenu') { break;
}
case ItemAction.PlayMenu:
showPlayMenu(card, target); showPlayMenu(card, target);
} else if (action === 'edit') { break;
case ItemAction.Edit:
getItem(target).then(itemToEdit => { getItem(target).then(itemToEdit => {
editItem(itemToEdit, serverId); editItem(itemToEdit, serverId);
}); });
} else if (action === 'playtrailer') { break;
case ItemAction.PlayTrailer:
getItem(target).then(playTrailer); getItem(target).then(playTrailer);
} else if (action === 'addtoplaylist') { break;
case ItemAction.AddToPlaylist:
getItem(target).then(addToPlaylist); getItem(target).then(addToPlaylist);
} else if (action === 'custom') { break;
case ItemAction.Custom: {
const customAction = target.getAttribute('data-customaction'); const customAction = target.getAttribute('data-customaction');
card.dispatchEvent(new CustomEvent(`action-${customAction}`, { card.dispatchEvent(new CustomEvent(`action-${customAction}`, {
@@ -322,6 +341,7 @@ function executeAction(card, target, action) {
})); }));
} }
} }
}
function addToPlaylist(item) { function addToPlaylist(item) {
import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => { import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
@@ -390,7 +410,7 @@ export function onClick(e) {
} }
} }
if (action && action !== 'none') { if (action && action !== ItemAction.None) {
executeAction(card, actionElement, action); executeAction(card, actionElement, action);
e.preventDefault(); e.preventDefault();
+37
View File
@@ -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'
};
+6 -5
View File
@@ -22,6 +22,7 @@ import { playbackManager } from 'components/playback/playbackmanager';
import { appRouter } from 'components/router/appRouter'; import { appRouter } from 'components/router/appRouter';
import itemShortcuts from 'components/shortcuts'; import itemShortcuts from 'components/shortcuts';
import { AppFeature } from 'constants/appFeature'; import { AppFeature } from 'constants/appFeature';
import { ItemAction } from 'constants/itemAction';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import { ServerConnections } from 'lib/jellyfin-apiclient'; import { ServerConnections } from 'lib/jellyfin-apiclient';
import browser from 'scripts/browser'; import browser from 'scripts/browser';
@@ -434,19 +435,19 @@ function renderName(item, container, context) {
parentNameHtml.push(getArtistLinksHtml(item.ArtistItems, item.ServerId, context)); parentNameHtml.push(getArtistLinksHtml(item.ArtistItems, item.ServerId, context));
parentNameLast = true; parentNameLast = true;
} else if (item.SeriesName && item.Type === 'Episode') { } else if (item.SeriesName && item.Type === 'Episode') {
parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="link" data-id="${item.SeriesId}" data-serverid="${item.ServerId}" data-type="Series" data-isfolder="true">${escapeHtml(item.SeriesName)}</a>`); parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="${ItemAction.Link}" data-id="${item.SeriesId}" data-serverid="${item.ServerId}" data-type="Series" data-isfolder="true">${escapeHtml(item.SeriesName)}</a>`);
} else if (item.IsSeries || item.EpisodeTitle) { } else if (item.IsSeries || item.EpisodeTitle) {
parentNameHtml.push(escapeHtml(item.Name)); parentNameHtml.push(escapeHtml(item.Name));
} }
if (item.SeriesName && item.Type === 'Season') { if (item.SeriesName && item.Type === 'Season') {
parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="link" data-id="${item.SeriesId}" data-serverid="${item.ServerId}" data-type="Series" data-isfolder="true">${escapeHtml(item.SeriesName)}</a>`); parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="${ItemAction.Link}" data-id="${item.SeriesId}" data-serverid="${item.ServerId}" data-type="Series" data-isfolder="true">${escapeHtml(item.SeriesName)}</a>`);
} else if (item.ParentIndexNumber != null && item.Type === 'Episode') { } else if (item.ParentIndexNumber != null && item.Type === 'Episode') {
parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="link" data-id="${item.SeasonId}" data-serverid="${item.ServerId}" data-type="Season" data-isfolder="true">${escapeHtml(item.SeasonName)}</a>`); parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="${ItemAction.Link}" data-id="${item.SeasonId}" data-serverid="${item.ServerId}" data-type="Season" data-isfolder="true">${escapeHtml(item.SeasonName)}</a>`);
} else if (item.ParentIndexNumber != null && item.IsSeries) { } else if (item.ParentIndexNumber != null && item.IsSeries) {
parentNameHtml.push(escapeHtml(item.SeasonName || 'S' + item.ParentIndexNumber)); parentNameHtml.push(escapeHtml(item.SeasonName || 'S' + item.ParentIndexNumber));
} else if (item.Album && item.AlbumId && (item.Type === 'MusicVideo' || item.Type === 'Audio')) { } else if (item.Album && item.AlbumId && (item.Type === 'MusicVideo' || item.Type === 'Audio')) {
parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="link" data-id="${item.AlbumId}" data-serverid="${item.ServerId}" data-type="MusicAlbum" data-isfolder="true">${escapeHtml(item.Album)}</a>`); parentNameHtml.push(`<a style="color:inherit;" class="button-link itemAction" is="emby-linkbutton" href="#" data-action="${ItemAction.Link}" data-id="${item.AlbumId}" data-serverid="${item.ServerId}" data-type="MusicAlbum" data-isfolder="true">${escapeHtml(item.Album)}</a>`);
} else if (item.Album) { } else if (item.Album) {
parentNameHtml.push(escapeHtml(item.Album)); parentNameHtml.push(escapeHtml(item.Album));
} }
@@ -1982,7 +1983,7 @@ export default function (view, params) {
return; return;
} }
playItem(item, item.UserData && mode === 'resume' ? item.UserData.PlaybackPositionTicks : 0); playItem(item, item.UserData && mode === ItemAction.Resume ? item.UserData.PlaybackPositionTicks : 0);
} }
function onPlayClick() { function onPlayClick() {
@@ -3,8 +3,8 @@ import { useQueryClient } from '@tanstack/react-query';
import React, { type FC, useCallback } from 'react'; import React, { type FC, useCallback } from 'react';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
import classNames from 'classnames';
import { ItemAction } from 'constants/itemAction';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import { useTogglePlayedMutation } from 'hooks/useFetchItems'; import { useTogglePlayedMutation } from 'hooks/useFetchItems';
@@ -59,23 +59,17 @@ const PlayedButton: FC<PlayedButtonProps> = ({
} }
}, [itemId, togglePlayedMutation, isPlayed, queryClient, queryKey]); }, [itemId, togglePlayedMutation, isPlayed, queryClient, queryKey]);
const btnClass = classNames(
className,
{ 'playstatebutton-played': isPlayed }
);
const iconClass = classNames(
{ 'playstatebutton-icon-played': isPlayed }
);
return ( return (
<IconButton <IconButton
data-action='none' data-action={ItemAction.None}
title={getTitle()} title={getTitle()}
className={btnClass} className={className}
size='small' size='small'
onClick={onClick} onClick={onClick}
> >
<CheckIcon className={iconClass} /> <CheckIcon
color={isPlayed ? 'error' : undefined}
/>
</IconButton> </IconButton>
); );
}; };
@@ -3,7 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import FavoriteIcon from '@mui/icons-material/Favorite'; import FavoriteIcon from '@mui/icons-material/Favorite';
import classNames from 'classnames'; import { ItemAction } from 'constants/itemAction';
import { useToggleFavoriteMutation } from 'hooks/useFetchItems'; import { useToggleFavoriteMutation } from 'hooks/useFetchItems';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
@@ -45,24 +45,17 @@ const FavoriteButton: FC<FavoriteButtonProps> = ({
} }
}, [isFavorite, itemId, queryClient, queryKey, toggleFavoriteMutation]); }, [isFavorite, itemId, queryClient, queryKey, toggleFavoriteMutation]);
const btnClass = classNames(
className,
{ 'ratingbutton-withrating': isFavorite }
);
const iconClass = classNames(
{ 'ratingbutton-icon-withrating': isFavorite }
);
return ( return (
<IconButton <IconButton
data-action='none' data-action={ItemAction.None}
className={className}
title={isFavorite ? globalize.translate('Favorite') : globalize.translate('AddToFavorites')} title={isFavorite ? globalize.translate('Favorite') : globalize.translate('AddToFavorites')}
className={btnClass}
size='small' size='small'
onClick={onClick} onClick={onClick}
> >
<FavoriteIcon className={iconClass} /> <FavoriteIcon
color={isFavorite ? 'error' : undefined}
/>
</IconButton> </IconButton>
); );
}; };
+4 -1
View File
@@ -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 { 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 { 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 type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
import { ItemAction } from 'constants/itemAction';
import { CardShape } from 'utils/card'; import { CardShape } from 'utils/card';
import type { NullableString } from './base/common/shared/types'; import type { NullableString } from './base/common/shared/types';
import type { ItemDto } from './base/models/item-dto'; import type { ItemDto } from './base/models/item-dto';
import type { ParentId } from './library'; import type { ParentId } from './library';
@@ -44,7 +47,7 @@ export interface CardOptions {
showChildCountIndicator?: boolean; showChildCountIndicator?: boolean;
lines?: number; lines?: number;
context?: CollectionType; context?: CollectionType;
action?: string | null; action?: ItemAction | null;
indexBy?: string; indexBy?: string;
parentId?: ParentId; parentId?: ParentId;
showMenu?: boolean; showMenu?: boolean;
+5 -2
View File
@@ -1,5 +1,8 @@
import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; 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 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'; import type { NullableBoolean, NullableNumber, NullableString } from './base/common/shared/types';
export type AttributesOpts = { export type AttributesOpts = {
@@ -8,7 +11,7 @@ export type AttributesOpts = {
collectionId?: NullableString, collectionId?: NullableString,
playlistId?: NullableString, playlistId?: NullableString,
prefix?: NullableString, prefix?: NullableString,
action?: NullableString, action?: ItemAction | null,
itemServerId?: NullableString, itemServerId?: NullableString,
itemId?: NullableString, itemId?: NullableString,
itemTimerId?: NullableString, itemTimerId?: NullableString,
@@ -43,7 +46,7 @@ export type DataAttributes = {
'data-startdate'?: NullableString; 'data-startdate'?: NullableString;
'data-enddate'?: NullableString; 'data-enddate'?: NullableString;
'data-prefix'?: NullableString; 'data-prefix'?: NullableString;
'data-action'?: NullableString; 'data-action'?: ItemAction | null;
'data-positionticks'?: NullableNumber; 'data-positionticks'?: NullableNumber;
'data-isfolder'?: NullableBoolean; 'data-isfolder'?: NullableBoolean;
}; };
+5 -2
View File
@@ -1,13 +1,16 @@
import type { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; 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 { 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 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 { export interface ListOptions extends TextLineOpts {
items?: ItemDto[] | null; items?: ItemDto[] | null;
index?: string; index?: string;
showIndex?: boolean; showIndex?: boolean;
action?: string | null; action?: ItemAction | null;
imageSize?: string; imageSize?: string;
enableOverview?: boolean; enableOverview?: boolean;
enableSideMediaInfo?: boolean; enableSideMediaInfo?: boolean;
+5 -3
View File
@@ -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 { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'; import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by';
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'; import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order';
import { ItemAction } from 'constants/itemAction';
import * as userSettings from 'scripts/settings/userSettings'; import * as userSettings from 'scripts/settings/userSettings';
import { CardShape } from 'utils/card';
import { type Section, SectionType, SectionApiMethod } from 'types/sections'; import { type Section, SectionType, SectionApiMethod } from 'types/sections';
import { CardShape } from 'utils/card';
export const getSuggestionSections = (): Section[] => { export const getSuggestionSections = (): Section[] => {
const parametersOptions = { const parametersOptions = {
@@ -130,7 +132,7 @@ export const getSuggestionSections = (): Section[] => {
showUnplayedIndicator: false, showUnplayedIndicator: false,
shape: CardShape.SquareOverflow, shape: CardShape.SquareOverflow,
showParentTitle: true, showParentTitle: true,
action: 'instantmix', action: ItemAction.InstantMix,
overlayMoreButton: true, overlayMoreButton: true,
coverImage: true coverImage: true
} }
@@ -149,7 +151,7 @@ export const getSuggestionSections = (): Section[] => {
showUnplayedIndicator: false, showUnplayedIndicator: false,
shape: CardShape.SquareOverflow, shape: CardShape.SquareOverflow,
showParentTitle: true, showParentTitle: true,
action: 'instantmix', action: ItemAction.InstantMix,
overlayMoreButton: true, overlayMoreButton: true,
coverImage: true coverImage: true
} }