mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2026-01-15 16:33:35 -03:00
Merge pull request #7389 from thornbill/refactor-item-actions
This commit is contained in:
73
package-lock.json
generated
73
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<ItemsViewProps> = ({
|
||||
|
||||
if (viewType === LibraryTab.Songs) {
|
||||
listOptions.showParentTitle = true;
|
||||
listOptions.action = 'playallfromhere';
|
||||
listOptions.action = ItemAction.PlayAllFromHere;
|
||||
listOptions.smallIcon = true;
|
||||
listOptions.showArtist = true;
|
||||
listOptions.addToListButton = true;
|
||||
|
||||
@@ -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<ProgramsSectionViewProps> = ({
|
||||
showChannelName: false,
|
||||
cardLayout: true,
|
||||
centerText: false,
|
||||
action: 'edit',
|
||||
action: ItemAction.Edit,
|
||||
cardFooterAside: 'none',
|
||||
preferThumb: true,
|
||||
coverImage: true,
|
||||
|
||||
@@ -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<PlayOrResumeButtonProps> = ({
|
||||
return (
|
||||
<IconButton
|
||||
className='button-flat btnPlayOrResume'
|
||||
data-action={isResumable ? 'resume' : 'play'}
|
||||
data-action={isResumable ? ItemAction.Resume : ItemAction.Play}
|
||||
title={
|
||||
isResumable ?
|
||||
globalize.translate('ButtonResume') :
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React, { type FC } from 'react';
|
||||
|
||||
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 CardHoverMenu from './CardHoverMenu';
|
||||
import CardOuterFooter from './CardOuterFooter';
|
||||
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 {
|
||||
action: string;
|
||||
action: ItemAction;
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
className: string;
|
||||
|
||||
@@ -2,12 +2,14 @@ import React, { type FC } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import itemHelper from 'components/itemHelper';
|
||||
import { playbackManager } from 'components/playback/playbackmanager';
|
||||
|
||||
import { ItemAction } from 'constants/itemAction';
|
||||
import PlayedButton from 'elements/emby-playstatebutton/PlayedButton';
|
||||
import FavoriteButton from 'elements/emby-ratingbutton/FavoriteButton';
|
||||
|
||||
import PlayArrowIconButton from '../../common/PlayArrowIconButton';
|
||||
import MoreVertIconButton from '../../common/MoreVertIconButton';
|
||||
|
||||
@@ -15,7 +17,7 @@ import type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { CardOptions } from 'types/cardOptions';
|
||||
|
||||
interface CardHoverMenuProps {
|
||||
action: string,
|
||||
action: ItemAction,
|
||||
item: ItemDto;
|
||||
cardOptions: CardOptions;
|
||||
}
|
||||
@@ -51,7 +53,7 @@ const CardHoverMenu: FC<CardHoverMenuProps> = ({
|
||||
{playbackManager.canPlay(item) && (
|
||||
<PlayArrowIconButton
|
||||
className={centerPlayButtonClass}
|
||||
action='play'
|
||||
action={ItemAction.Play}
|
||||
title='Play'
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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<CardOverlayButtonsProps> = ({
|
||||
{cardOptions.centerPlayButton && (
|
||||
<PlayArrowIconButton
|
||||
className={centerPlayButtonClass}
|
||||
action='play'
|
||||
action={ItemAction.Play}
|
||||
title='Play'
|
||||
/>
|
||||
)}
|
||||
@@ -87,7 +89,7 @@ const CardOverlayButtons: FC<CardOverlayButtonsProps> = ({
|
||||
{sholudShowOverlayPlayButton(overlayPlayButton, item) && (
|
||||
<PlayArrowIconButton
|
||||
className={btnCssClass}
|
||||
action='play'
|
||||
action={ItemAction.Play}
|
||||
title='Play'
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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 += `<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';
|
||||
@@ -776,7 +777,7 @@ function getTextActionButton(item, text, serverId) {
|
||||
}
|
||||
|
||||
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 += '</a>';
|
||||
|
||||
@@ -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 += `<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') {
|
||||
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) {
|
||||
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';
|
||||
|
||||
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">';
|
||||
@@ -1165,17 +1166,17 @@ function getHoverMenuHtml(item, action) {
|
||||
|
||||
if (itemHelper.canMarkPlayed(item)) {
|
||||
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)) {
|
||||
const likes = userData.Likes == null ? '' : userData.Likes;
|
||||
|
||||
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>';
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<InfoIconButtonProps> = ({ className }) => {
|
||||
return (
|
||||
<IconButton
|
||||
className={className}
|
||||
data-action='link'
|
||||
data-action={ItemAction.Link}
|
||||
title={globalize.translate('ButtonInfo')}
|
||||
>
|
||||
<InfoIcon />
|
||||
|
||||
@@ -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<MoreVertIconButtonProps> = ({ className, iconClassN
|
||||
return (
|
||||
<IconButton
|
||||
className={className}
|
||||
data-action='menu'
|
||||
data-action={ItemAction.Menu}
|
||||
title={globalize.translate('ButtonMore')}
|
||||
>
|
||||
<MoreVertIcon className={iconClassName} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<PlaylistAddIconButtonProps> = ({ className }) =>
|
||||
return (
|
||||
<IconButton
|
||||
className={className}
|
||||
data-action='addtoplaylist'
|
||||
data-action={ItemAction.AddToPlaylist}
|
||||
title={globalize.translate('AddToPlaylist')}
|
||||
>
|
||||
<PlaylistAddIcon />
|
||||
|
||||
@@ -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<RightIconButtonsProps> = ({ className, id, title, ico
|
||||
return (
|
||||
<IconButton
|
||||
className={className}
|
||||
data-action='custom'
|
||||
data-action={ItemAction.Custom}
|
||||
data-customaction={id}
|
||||
title={title}
|
||||
>
|
||||
|
||||
@@ -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 += '<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 displayMovieContent = !categories.length || categories.indexOf('movies') !== -1;
|
||||
@@ -607,7 +612,7 @@ function Guide(options) {
|
||||
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) {
|
||||
const url = apiClient.getScaledImageUrl(channel.Id, {
|
||||
|
||||
@@ -4,6 +4,8 @@ import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
import useIndicator from 'components/indicators/useIndicator';
|
||||
import { ItemAction } from 'constants/itemAction';
|
||||
|
||||
import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo';
|
||||
import ListContentWrapper from './ListContentWrapper';
|
||||
import ListItemBody from './ListItemBody';
|
||||
@@ -20,7 +22,7 @@ interface ListContentProps {
|
||||
enableOverview?: boolean;
|
||||
enableSideMediaInfo?: boolean;
|
||||
clickEntireItem?: boolean;
|
||||
action?: string;
|
||||
action?: ItemAction;
|
||||
isLargeStyle: boolean;
|
||||
downloadWidth?: number;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import React, { type FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
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 type { ItemDto } from 'types/base/models/item-dto';
|
||||
import type { ListOptions } from 'types/listOptions';
|
||||
|
||||
import useIndicator from '../../indicators/useIndicator';
|
||||
import layoutManager from '../../layoutManager';
|
||||
import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils';
|
||||
@@ -11,15 +18,10 @@ import {
|
||||
getImageUrl
|
||||
} 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 {
|
||||
item: ItemDto;
|
||||
listOptions: ListOptions;
|
||||
action?: string | null;
|
||||
action?: ItemAction | null;
|
||||
isLargeStyle: boolean;
|
||||
clickEntireItem?: boolean;
|
||||
downloadWidth?: number;
|
||||
@@ -55,7 +57,7 @@ const ListImageContainer: FC<ListImageContainerProps> = ({
|
||||
|
||||
const playOnImageClick = listOptions.imagePlayButton && !layoutManager.tv;
|
||||
|
||||
const imageAction = playOnImageClick ? 'link' : action;
|
||||
const imageAction = playOnImageClick ? ItemAction.Link : action;
|
||||
|
||||
const btnCssClass =
|
||||
'paper-icon-button-light listItemImageButton itemAction';
|
||||
@@ -85,7 +87,7 @@ const ListImageContainer: FC<ListImageContainerProps> = ({
|
||||
<PlayArrowIconButton
|
||||
className={btnCssClass}
|
||||
action={
|
||||
canResume(playbackPositionTicks) ? 'resume' : 'play'
|
||||
canResume(playbackPositionTicks) ? ItemAction.Resume : ItemAction.Play
|
||||
}
|
||||
title={
|
||||
canResume(playbackPositionTicks) ?
|
||||
|
||||
@@ -3,15 +3,16 @@ import classNames from 'classnames';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
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 { ListOptions } from 'types/listOptions';
|
||||
|
||||
import PrimaryMediaInfo from '../../mediainfo/PrimaryMediaInfo';
|
||||
|
||||
interface ListItemBodyProps {
|
||||
item: ItemDto;
|
||||
listOptions: ListOptions;
|
||||
action?: string | null;
|
||||
action?: ItemAction | null;
|
||||
isLargeStyle?: boolean;
|
||||
clickEntireItem?: boolean;
|
||||
enableContentWrapper?: boolean;
|
||||
|
||||
@@ -2,13 +2,16 @@ import classNames from 'classnames';
|
||||
import React, { type FC, type PropsWithChildren } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import layoutManager from '../../layoutManager';
|
||||
|
||||
import { ItemAction } from 'constants/itemAction';
|
||||
import type { DataAttributes } from 'types/dataAttributes';
|
||||
|
||||
import layoutManager from '../../layoutManager';
|
||||
|
||||
interface ListWrapperProps {
|
||||
index: number | undefined;
|
||||
title?: string | null;
|
||||
action?: string | null;
|
||||
action?: ItemAction | null;
|
||||
dataAttributes?: DataAttributes;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import { getDataAttributes } from 'utils/items';
|
||||
|
||||
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 { ListOptions } from 'types/listOptions';
|
||||
@@ -11,7 +13,7 @@ interface UseListProps {
|
||||
}
|
||||
|
||||
function useList({ item, listOptions }: UseListProps) {
|
||||
const action = listOptions.action ?? 'link';
|
||||
const action = listOptions.action ?? ItemAction.Link;
|
||||
const isLargeStyle = listOptions.imageSize === 'large';
|
||||
const enableOverview = listOptions.enableOverview;
|
||||
const clickEntireItem = !!layoutManager.tv;
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
* @module components/listview/listview
|
||||
*/
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
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 mediaInfo from '../mediainfo/mediainfo';
|
||||
import indicators from '../indicators/indicators';
|
||||
@@ -13,12 +19,10 @@ import globalize from '../../lib/globalize';
|
||||
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
||||
import datetime from '../../scripts/datetime';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
|
||||
import './listview.scss';
|
||||
import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
||||
import '../../elements/emby-playstatebutton/emby-playstatebutton';
|
||||
import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils';
|
||||
import markdownIt from 'markdown-it';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
function getIndex(item, options) {
|
||||
if (options.index === 'disc') {
|
||||
@@ -165,7 +169,7 @@ function getRightButtonsHtml(options) {
|
||||
for (let i = 0, length = options.rightButtons.length; i < length; 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;
|
||||
@@ -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 += '<div data-action="' + imageAction + '" class="' + imageClass + ' lazy" data-src="' + imgUrl + '" item-icon>';
|
||||
@@ -298,7 +302,7 @@ export function getListViewHtml(options) {
|
||||
}
|
||||
|
||||
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, {
|
||||
@@ -449,11 +453,11 @@ export function getListViewHtml(options) {
|
||||
|
||||
if (!clickEntireItem) {
|
||||
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) {
|
||||
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) {
|
||||
@@ -474,7 +478,7 @@ export function getListViewHtml(options) {
|
||||
}
|
||||
|
||||
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>';
|
||||
|
||||
@@ -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 = '<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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,23 +232,28 @@ 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') {
|
||||
switch (action) {
|
||||
case ItemAction.Link:
|
||||
appRouter.showItem(item, {
|
||||
context: card.getAttribute('data-context'),
|
||||
parentId: card.getAttribute('data-parentid')
|
||||
});
|
||||
} else if (action === 'programdialog') {
|
||||
break;
|
||||
case ItemAction.ProgramDialog:
|
||||
showProgramDialog(item);
|
||||
} else if (action === 'instantmix') {
|
||||
break;
|
||||
case ItemAction.InstantMix:
|
||||
playbackManager.instantMix({
|
||||
Id: playableItemId,
|
||||
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 sortValues = userSettings.getSortValuesLegacy(sortParentId, 'SortName');
|
||||
|
||||
@@ -264,7 +270,9 @@ function executeAction(card, target, action) {
|
||||
} else {
|
||||
console.warn('Unable to play item', item);
|
||||
}
|
||||
} else if (action === 'queue') {
|
||||
break;
|
||||
}
|
||||
case ItemAction.Queue:
|
||||
if (playbackManager.isPlaying()) {
|
||||
playbackManager.queue({
|
||||
ids: [playableItemId],
|
||||
@@ -277,15 +285,20 @@ function executeAction(card, target, action) {
|
||||
serverId: serverId
|
||||
});
|
||||
}
|
||||
} else if (action === 'playallfromhere') {
|
||||
break;
|
||||
case ItemAction.PlayAllFromHere:
|
||||
playAllFromHere(card, serverId);
|
||||
} else if (action === 'queueallfromhere') {
|
||||
break;
|
||||
case ItemAction.QueueAllFromHere:
|
||||
playAllFromHere(card, serverId, true);
|
||||
} else if (action === 'setplaylistindex') {
|
||||
break;
|
||||
case ItemAction.SetPlaylistIndex:
|
||||
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'));
|
||||
} else if (action === 'menu') {
|
||||
break;
|
||||
case ItemAction.Menu: {
|
||||
const options = target.getAttribute('data-playoptions') === 'false' ?
|
||||
{
|
||||
shuffle: false,
|
||||
@@ -300,17 +313,23 @@ function executeAction(card, target, action) {
|
||||
options.positionTo = target;
|
||||
|
||||
showContextMenu(card, options);
|
||||
} else if (action === 'playmenu') {
|
||||
break;
|
||||
}
|
||||
case ItemAction.PlayMenu:
|
||||
showPlayMenu(card, target);
|
||||
} else if (action === 'edit') {
|
||||
break;
|
||||
case ItemAction.Edit:
|
||||
getItem(target).then(itemToEdit => {
|
||||
editItem(itemToEdit, serverId);
|
||||
});
|
||||
} else if (action === 'playtrailer') {
|
||||
break;
|
||||
case ItemAction.PlayTrailer:
|
||||
getItem(target).then(playTrailer);
|
||||
} else if (action === 'addtoplaylist') {
|
||||
break;
|
||||
case ItemAction.AddToPlaylist:
|
||||
getItem(target).then(addToPlaylist);
|
||||
} else if (action === 'custom') {
|
||||
break;
|
||||
case ItemAction.Custom: {
|
||||
const customAction = target.getAttribute('data-customaction');
|
||||
|
||||
card.dispatchEvent(new CustomEvent(`action-${customAction}`, {
|
||||
@@ -321,6 +340,7 @@ function executeAction(card, target, action) {
|
||||
bubbles: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToPlaylist(item) {
|
||||
@@ -390,7 +410,7 @@ export function onClick(e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (action && action !== 'none') {
|
||||
if (action && action !== ItemAction.None) {
|
||||
executeAction(card, actionElement, action);
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
37
src/constants/itemAction.ts
Normal file
37
src/constants/itemAction.ts
Normal 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'
|
||||
};
|
||||
@@ -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(`<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) {
|
||||
parentNameHtml.push(escapeHtml(item.Name));
|
||||
}
|
||||
|
||||
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') {
|
||||
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) {
|
||||
parentNameHtml.push(escapeHtml(item.SeasonName || 'S' + item.ParentIndexNumber));
|
||||
} 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) {
|
||||
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() {
|
||||
|
||||
@@ -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<PlayedButtonProps> = ({
|
||||
}
|
||||
}, [itemId, togglePlayedMutation, isPlayed, queryClient, queryKey]);
|
||||
|
||||
const btnClass = classNames(
|
||||
className,
|
||||
{ 'playstatebutton-played': isPlayed }
|
||||
);
|
||||
|
||||
const iconClass = classNames(
|
||||
{ 'playstatebutton-icon-played': isPlayed }
|
||||
);
|
||||
return (
|
||||
<IconButton
|
||||
data-action='none'
|
||||
data-action={ItemAction.None}
|
||||
title={getTitle()}
|
||||
className={btnClass}
|
||||
className={className}
|
||||
size='small'
|
||||
onClick={onClick}
|
||||
>
|
||||
<CheckIcon className={iconClass} />
|
||||
<CheckIcon
|
||||
color={isPlayed ? 'error' : undefined}
|
||||
/>
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<FavoriteButtonProps> = ({
|
||||
}
|
||||
}, [isFavorite, itemId, queryClient, queryKey, toggleFavoriteMutation]);
|
||||
|
||||
const btnClass = classNames(
|
||||
className,
|
||||
{ 'ratingbutton-withrating': isFavorite }
|
||||
);
|
||||
|
||||
const iconClass = classNames(
|
||||
{ 'ratingbutton-icon-withrating': isFavorite }
|
||||
);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
data-action='none'
|
||||
data-action={ItemAction.None}
|
||||
className={className}
|
||||
title={isFavorite ? globalize.translate('Favorite') : globalize.translate('AddToFavorites')}
|
||||
className={btnClass}
|
||||
size='small'
|
||||
onClick={onClick}
|
||||
>
|
||||
<FavoriteIcon className={iconClass} />
|
||||
<FavoriteIcon
|
||||
color={isFavorite ? 'error' : undefined}
|
||||
/>
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/// <reference types="vitest" />
|
||||
/// <reference types="vite/client" />
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [ tsconfigPaths() ],
|
||||
test: {
|
||||
coverage: {
|
||||
include: [ 'src' ]
|
||||
|
||||
Reference in New Issue
Block a user