1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge pull request #6039 from grafixeyehero/Add-details-react-view

Add detail view buttons
This commit is contained in:
Bill Thornton 2024-09-27 23:47:14 -04:00 committed by GitHub
commit f405602bd0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 921 additions and 4 deletions

View file

@ -0,0 +1,68 @@
import React, { FC, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { IconButton } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import { useCancelSeriesTimer } from 'hooks/api/liveTvHooks';
import globalize from 'lib/globalize';
import loading from 'components/loading/loading';
import toast from 'components/toast/toast';
import confirm from 'components/confirm/confirm';
interface CancelSeriesTimerButtonProps {
itemId: string;
}
const CancelSeriesTimerButton: FC<CancelSeriesTimerButtonProps> = ({
itemId
}) => {
const navigate = useNavigate();
const cancelSeriesTimer = useCancelSeriesTimer();
const onCancelSeriesTimerClick = useCallback(() => {
confirm({
text: globalize.translate('MessageConfirmRecordingCancellation'),
primary: 'delete',
confirmText: globalize.translate('HeaderCancelSeries'),
cancelText: globalize.translate('HeaderKeepSeries')
})
.then(function () {
loading.show();
cancelSeriesTimer.mutate(
{
timerId: itemId
},
{
onSuccess: async () => {
toast(globalize.translate('SeriesCancelled'));
loading.hide();
navigate('/livetv.html');
},
onError: (err: unknown) => {
loading.hide();
toast(globalize.translate('MessageCancelSeriesTimerError'));
console.error(
'[cancelSeriesTimer] failed to cancel series timer',
err
);
}
}
);
})
.catch(() => {
// confirm dialog closed
});
}, [cancelSeriesTimer, navigate, itemId]);
return (
<IconButton
className='button-flat btnCancelSeriesTimer'
title={globalize.translate('CancelSeries')}
onClick={onCancelSeriesTimerClick}
>
<DeleteIcon />
</IconButton>
);
};
export default CancelSeriesTimerButton;

View file

@ -0,0 +1,60 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import StopIcon from '@mui/icons-material/Stop';
import { useQueryClient } from '@tanstack/react-query';
import { useCancelTimer } from 'hooks/api/liveTvHooks';
import globalize from 'lib/globalize';
import loading from 'components/loading/loading';
import toast from 'components/toast/toast';
interface CancelTimerButtonProps {
timerId: string;
queryKey?: string[];
}
const CancelTimerButton: FC<CancelTimerButtonProps> = ({
timerId,
queryKey
}) => {
const queryClient = useQueryClient();
const cancelTimer = useCancelTimer();
const onCancelTimerClick = useCallback(() => {
loading.show();
cancelTimer.mutate(
{
timerId: timerId
},
{
onSuccess: async () => {
toast(globalize.translate('RecordingCancelled'));
loading.hide();
await queryClient.invalidateQueries({
queryKey
});
},
onError: (err: unknown) => {
loading.hide();
toast(globalize.translate('MessageCancelTimerError'));
console.error(
'[cancelTimer] failed to cancel timer',
err
);
}
}
);
}, [cancelTimer, queryClient, queryKey, timerId]);
return (
<IconButton
className='button-flat btnCancelTimer'
title={globalize.translate('StopRecording')}
onClick={onCancelTimerClick}
>
<StopIcon />
</IconButton>
);
};
export default CancelTimerButton;

View file

@ -0,0 +1,42 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import { useGetDownload } from 'hooks/api/libraryHooks';
import globalize from 'lib/globalize';
import { download } from 'scripts/fileDownloader';
import type { NullableString } from 'types/base/common/shared/types';
interface DownloadButtonProps {
itemId: string;
itemServerId: NullableString,
itemName: NullableString,
itemPath: NullableString,
}
const DownloadButton: FC<DownloadButtonProps> = ({ itemId, itemServerId, itemName, itemPath }) => {
const { data: downloadHref } = useGetDownload({ itemId });
const onDownloadClick = useCallback(async () => {
download([
{
url: downloadHref,
itemId: itemId,
serverId: itemServerId,
title: itemName,
filename: itemPath?.replace(/^.*[\\/]/, '')
}
]);
}, [downloadHref, itemId, itemName, itemPath, itemServerId]);
return (
<IconButton
className='button-flat btnDownload'
title={globalize.translate('Download')}
onClick={onDownloadClick}
>
<FileDownloadIcon />
</IconButton>
);
};
export default DownloadButton;

View file

@ -0,0 +1,28 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import ExploreIcon from '@mui/icons-material/Explore';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'lib/globalize';
import type { ItemDto } from 'types/base/models/item-dto';
interface InstantMixButtonProps {
item?: ItemDto;
}
const InstantMixButton: FC<InstantMixButtonProps> = ({ item }) => {
const onInstantMixClick = useCallback(() => {
playbackManager.instantMix(item);
}, [item]);
return (
<IconButton
className='button-flat btnInstantMix'
title={globalize.translate('HeaderInstantMix')}
onClick={onInstantMixClick}
>
<ExploreIcon />
</IconButton>
);
};
export default InstantMixButton;

View file

@ -0,0 +1,219 @@
import React, { FC, useCallback, useMemo } from 'react';
import { IconButton } from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { useQueryClient } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { useGetItemByType } from '../../hooks/api/useGetItemByType';
import globalize from 'lib/globalize';
import itemContextMenu from 'components/itemContextMenu';
import { playbackManager } from 'components/playback/playbackmanager';
import { appRouter } from 'components/router/appRouter';
import { ItemKind } from 'types/base/models/item-kind';
import type { NullableString } from 'types/base/common/shared/types';
import type { ItemDto } from 'types/base/models/item-dto';
interface PlayAllFromHereOptions {
item: ItemDto;
items: ItemDto[];
serverId: NullableString;
queue?: boolean;
}
function playAllFromHere(opts: PlayAllFromHereOptions) {
const { item, items, serverId, queue } = opts;
const ids = [];
let foundCard = false;
let startIndex;
for (let i = 0, length = items?.length; i < length; i++) {
if (items[i] === item) {
foundCard = true;
startIndex = i;
}
if (foundCard || !queue) {
ids.push(items[i].Id);
}
}
if (!ids.length) {
return;
}
if (queue) {
return playbackManager.queue({
ids,
serverId
});
} else {
return playbackManager.play({
ids,
serverId,
startIndex
});
}
}
export interface ContextMenuOpts {
open?: boolean;
play?: boolean;
playAllFromHere?: boolean;
queueAllFromHere?: boolean;
cancelTimer?: boolean;
record?: boolean;
deleteItem?: boolean;
shuffle?: boolean;
instantMix?: boolean;
share?: boolean;
stopPlayback?: boolean;
clearQueue?: boolean;
queue?: boolean;
playlist?: boolean;
edit?: boolean;
editImages?: boolean;
editSubtitles?: boolean;
identify?: boolean;
moremediainfo?: boolean;
openAlbum?: boolean;
openArtist?: boolean;
openLyrics?: boolean;
}
interface MoreCommandsButtonProps {
itemType: ItemKind;
selectedItemId?: string;
itemId?: string;
items?: ItemDto[] | null;
collectionId?: NullableString;
playlistId?: NullableString;
canEditPlaylist?: boolean;
itemPlaylistItemId?: NullableString;
contextMenuOpts?: ContextMenuOpts;
queryKey?: string[];
}
const MoreCommandsButton: FC<MoreCommandsButtonProps> = ({
itemType,
selectedItemId,
itemId,
collectionId,
playlistId,
canEditPlaylist,
itemPlaylistItemId,
contextMenuOpts,
items,
queryKey
}) => {
const { user } = useApi();
const queryClient = useQueryClient();
const { data: item } = useGetItemByType({
itemType,
itemId: selectedItemId || itemId || ''
});
const parentId = item?.SeasonId || item?.SeriesId || item?.ParentId;
const playlistItem = useMemo(() => {
let PlaylistItemId: string | null = null;
let PlaylistIndex = -1;
let PlaylistItemCount = 0;
if (playlistId) {
PlaylistItemId = itemPlaylistItemId || null;
if (items?.length) {
PlaylistItemCount = items.length;
PlaylistIndex = items.findIndex(listItem => listItem.PlaylistItemId === PlaylistItemId);
}
}
return { PlaylistItemId, PlaylistIndex, PlaylistItemCount };
}, [itemPlaylistItemId, items, playlistId]);
const defaultMenuOptions = useMemo(() => {
return {
item: {
...item,
...playlistItem
},
user: user,
play: true,
queue: true,
playAllFromHere: item?.Type === ItemKind.Season || !item?.IsFolder,
queueAllFromHere: !item?.IsFolder,
canEditPlaylist: canEditPlaylist,
playlistId: playlistId,
collectionId: collectionId,
...contextMenuOpts
};
}, [canEditPlaylist, collectionId, contextMenuOpts, item, playlistId, playlistItem, user]);
const onMoreCommandsClick = useCallback(
async (e: React.MouseEvent<HTMLElement>) => {
itemContextMenu
.show({
...defaultMenuOptions,
positionTo: e.currentTarget
})
.then(async function (result) {
if (result.command === 'playallfromhere') {
console.log('handleItemClick', {
item,
items: items || [],
serverId: item?.ServerId
});
playAllFromHere({
item: item || {},
items: items || [],
serverId: item?.ServerId
});
} else if (result.command === 'queueallfromhere') {
playAllFromHere({
item: item || {},
items: items || [],
serverId: item?.ServerId,
queue: true
});
} else if (result.deleted) {
if (result?.itemId !== itemId) {
await queryClient.invalidateQueries({
queryKey
});
} else if (parentId) {
appRouter.showItem(parentId, item?.ServerId);
} else {
await appRouter.goHome();
}
} else if (result.updated) {
await queryClient.invalidateQueries({
queryKey
});
}
})
.catch(() => {
/* no-op */
});
},
[defaultMenuOptions, item, itemId, items, parentId, queryClient, queryKey]
);
if (
item
&& itemContextMenu.getCommands(defaultMenuOptions).length
) {
return (
<IconButton
className='button-flat btnMoreCommands'
title={globalize.translate('ButtonMore')}
onClick={onMoreCommandsClick}
>
<MoreVertIcon />
</IconButton>
);
}
return null;
};
export default MoreCommandsButton;

View file

@ -0,0 +1,87 @@
import React, { FC, useCallback, useMemo } from 'react';
import { IconButton } from '@mui/material';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import ReplayIcon from '@mui/icons-material/Replay';
import { useQueryClient } from '@tanstack/react-query';
import { useApi } from 'hooks/useApi';
import { getChannelQuery } from 'hooks/api/liveTvHooks/useGetChannel';
import globalize from 'lib/globalize';
import { playbackManager } from 'components/playback/playbackmanager';
import type { ItemDto } from 'types/base/models/item-dto';
import { ItemKind } from 'types/base/models/item-kind';
import itemHelper from 'components/itemHelper';
interface PlayOrResumeButtonProps {
item: ItemDto;
isResumable?: boolean;
selectedMediaSourceId?: string | null;
selectedAudioTrack?: number;
selectedSubtitleTrack?: number;
}
const PlayOrResumeButton: FC<PlayOrResumeButtonProps> = ({
item,
isResumable,
selectedMediaSourceId,
selectedAudioTrack,
selectedSubtitleTrack
}) => {
const apiContext = useApi();
const queryClient = useQueryClient();
const playOptions = useMemo(() => {
if (itemHelper.supportsMediaSourceSelection(item)) {
return {
startPositionTicks:
item.UserData && isResumable ?
item.UserData.PlaybackPositionTicks :
0,
mediaSourceId: selectedMediaSourceId,
audioStreamIndex: selectedAudioTrack || null,
subtitleStreamIndex: selectedSubtitleTrack
};
}
}, [
item,
isResumable,
selectedMediaSourceId,
selectedAudioTrack,
selectedSubtitleTrack
]);
const onPlayClick = useCallback(async () => {
if (item.Type === ItemKind.Program && item.ChannelId) {
const channel = await queryClient.fetchQuery(
getChannelQuery(apiContext, {
channelId: item.ChannelId
})
);
playbackManager.play({
items: [channel]
});
return;
}
playbackManager.play({
items: [item],
...playOptions
});
}, [apiContext, item, playOptions, queryClient]);
return (
<IconButton
className='button-flat btnPlayOrResume'
data-action={isResumable ? 'resume' : 'play'}
title={
isResumable ?
globalize.translate('ButtonResume') :
globalize.translate('Play')
}
onClick={onPlayClick}
>
{isResumable ? <ReplayIcon /> : <PlayArrowIcon />}
</IconButton>
);
};
export default PlayOrResumeButton;

View file

@ -0,0 +1,28 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import TheatersIcon from '@mui/icons-material/Theaters';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'lib/globalize';
import type { ItemDto } from 'types/base/models/item-dto';
interface PlayTrailerButtonProps {
item?: ItemDto;
}
const PlayTrailerButton: FC<PlayTrailerButtonProps> = ({ item }) => {
const onPlayTrailerClick = useCallback(async () => {
await playbackManager.playTrailers(item);
}, [item]);
return (
<IconButton
className='button-flat btnPlayTrailer'
title={globalize.translate('ButtonTrailer')}
onClick={onPlayTrailerClick}
>
<TheatersIcon />
</IconButton>
);
};
export default PlayTrailerButton;

View file

@ -0,0 +1,29 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import ShuffleIcon from '@mui/icons-material/Shuffle';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'lib/globalize';
import type { ItemDto } from 'types/base/models/item-dto';
interface ShuffleButtonProps {
item: ItemDto;
}
const ShuffleButton: FC<ShuffleButtonProps> = ({ item }) => {
const shuffle = useCallback(() => {
playbackManager.shuffle(item);
}, [item]);
return (
<IconButton
title={globalize.translate('Shuffle')}
className='button-flat btnShuffle'
onClick={shuffle}
>
<ShuffleIcon />
</IconButton>
);
};
export default ShuffleButton;

View file

@ -0,0 +1,68 @@
import React, { FC, useCallback } from 'react';
import { IconButton } from '@mui/material';
import CallSplitIcon from '@mui/icons-material/CallSplit';
import { useQueryClient } from '@tanstack/react-query';
import { useDeleteAlternateSources } from 'hooks/api/videosHooks';
import globalize from 'lib/globalize';
import confirm from 'components/confirm/confirm';
import loading from 'components/loading/loading';
import toast from 'components/toast/toast';
interface SplitVersionsButtonProps {
paramId: string;
queryKey?: string[];
}
const SplitVersionsButton: FC<SplitVersionsButtonProps> = ({
paramId,
queryKey
}) => {
const queryClient = useQueryClient();
const deleteAlternateSources = useDeleteAlternateSources();
const splitVersions = useCallback(() => {
confirm({
title: globalize.translate('HeaderSplitMediaApart'),
text: globalize.translate('MessageConfirmSplitMediaSources')
})
.then(function () {
loading.show();
deleteAlternateSources.mutate(
{
itemId: paramId
},
{
onSuccess: async () => {
loading.hide();
await queryClient.invalidateQueries({
queryKey
});
},
onError: (err: unknown) => {
loading.hide();
toast(globalize.translate('MessageSplitVersionsError'));
console.error(
'[splitVersions] failed to split versions',
err
);
}
}
);
})
.catch(() => {
// confirm dialog closed
});
}, [deleteAlternateSources, paramId, queryClient, queryKey]);
return (
<IconButton
className='button-flat btnSplitVersions'
title={globalize.translate('ButtonSplit')}
onClick={splitVersions}
>
<CallSplitIcon />
</IconButton>
);
};
export default SplitVersionsButton;

View file

@ -0,0 +1,62 @@
import type { AxiosRequestConfig } from 'axios';
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
import { useQuery } from '@tanstack/react-query';
import { type JellyfinApiContext, useApi } from 'hooks/useApi';
import type { ItemDto } from 'types/base/models/item-dto';
import { ItemKind } from 'types/base/models/item-kind';
const getItemByType = async (
apiContext: JellyfinApiContext,
itemType: ItemKind,
itemId: string,
options?: AxiosRequestConfig
) => {
const { api, user } = apiContext;
if (!api) throw new Error('[getItemByType] No API instance available');
if (!user?.Id) throw new Error('[getItemByType] No User ID provided');
let response;
switch (itemType) {
case ItemKind.Timer: {
response = await getLiveTvApi(api).getTimer(
{ timerId: itemId },
options
);
break;
}
case ItemKind.SeriesTimer:
response = await getLiveTvApi(api).getSeriesTimer(
{ timerId: itemId },
options
);
break;
default: {
response = await getUserLibraryApi(api).getItem(
{ userId: user.Id, itemId },
options
);
break;
}
}
return response.data as ItemDto;
};
interface UseGetItemByTypeProps {
itemType: ItemKind;
itemId: string;
}
export const useGetItemByType = ({
itemType,
itemId
}: UseGetItemByTypeProps) => {
const apiContext = useApi();
return useQuery({
queryKey: ['ItemByType', { itemType, itemId }],
queryFn: ({ signal }) =>
getItemByType(apiContext, itemType, itemId, { signal }),
enabled: !!apiContext.api && !!apiContext.user?.Id && !!itemId
});
};

View file

@ -351,12 +351,13 @@ export function getCommands(options) {
return commands;
}
function getResolveFunction(resolve, id, changed, deleted) {
function getResolveFunction(resolve, commandId, changed, deleted, itemId) {
return function () {
resolve({
command: id,
command: commandId,
updated: changed,
deleted: deleted
deleted: deleted,
itemId: itemId
});
};
}
@ -533,7 +534,7 @@ function executeCommand(item, id, options) {
getResolveFunction(resolve, id)();
break;
case 'delete':
deleteItem(apiClient, item).then(getResolveFunction(resolve, id, true, true), getResolveFunction(resolve, id));
deleteItem(apiClient, item).then(getResolveFunction(resolve, id, true, true, itemId), getResolveFunction(resolve, id));
break;
case 'share':
navigator.share({

View file

@ -0,0 +1 @@
export * from './useGetDownload';

View file

@ -0,0 +1,34 @@
import type { AxiosRequestConfig } from 'axios';
import type { LibraryApiGetDownloadRequest } from '@jellyfin/sdk/lib/generated-client';
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import { type JellyfinApiContext, useApi } from 'hooks/useApi';
const getDownload = async (
apiContext: JellyfinApiContext,
params: LibraryApiGetDownloadRequest,
options?: AxiosRequestConfig
) => {
const { api, user } = apiContext;
if (!api) throw new Error('[getDownload] No API instance available');
if (!user?.Id) throw new Error('[getDownload] No User ID provided');
const response = await getLibraryApi(api).getDownload(params, options);
return response.data;
};
export const getDownloadQuery = (
apiContext: JellyfinApiContext,
params: LibraryApiGetDownloadRequest
) =>
queryOptions({
queryKey: ['Download', params.itemId],
queryFn: ({ signal }) => getDownload(apiContext, params, { signal }),
enabled: !!apiContext.api && !!apiContext.user?.Id && !!params.itemId
});
export const useGetDownload = (params: LibraryApiGetDownloadRequest) => {
const apiContext = useApi();
return useQuery(getDownloadQuery(apiContext, params));
};

View file

@ -0,0 +1,5 @@
export * from './useCancelSeriesTimer';
export * from './useCancelTimer';
export * from './useGetChannel';
export * from './useGetSeriesTimer';
export * from './useGetTimer';

View file

@ -0,0 +1,24 @@
import type { LiveTvApiCancelSeriesTimerRequest } from '@jellyfin/sdk/lib/generated-client';
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
import { useMutation } from '@tanstack/react-query';
import { type JellyfinApiContext, useApi } from 'hooks/useApi';
const cancelSeriesTimer = async (
apiContext: JellyfinApiContext,
params: LiveTvApiCancelSeriesTimerRequest
) => {
const { api } = apiContext;
if (!api) throw new Error('[cancelSeriesTimer] No API instance available');
const response = await getLiveTvApi(api).cancelSeriesTimer(params);
return response.data;
};
export const useCancelSeriesTimer = () => {
const apiContext = useApi();
return useMutation({
mutationFn: (params: LiveTvApiCancelSeriesTimerRequest) =>
cancelSeriesTimer(apiContext, params)
});
};

View file

@ -0,0 +1,24 @@
import type { LiveTvApiCancelTimerRequest } from '@jellyfin/sdk/lib/generated-client';
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
import { useMutation } from '@tanstack/react-query';
import { type JellyfinApiContext, useApi } from 'hooks/useApi';
const cancelTimer = async (
apiContext: JellyfinApiContext,
params: LiveTvApiCancelTimerRequest
) => {
const { api } = apiContext;
if (!api) throw new Error('[cancelTimer] No API instance available');
const response = await getLiveTvApi(api).cancelTimer(params);
return response.data;
};
export const useCancelTimer = () => {
const apiContext = useApi();
return useMutation({
mutationFn: (params: LiveTvApiCancelTimerRequest) =>
cancelTimer(apiContext, params)
});
};

View file

@ -0,0 +1,41 @@
import type { AxiosRequestConfig } from 'axios';
import type { LiveTvApiGetChannelRequest } from '@jellyfin/sdk/lib/generated-client';
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import { type JellyfinApiContext, useApi } from 'hooks/useApi';
const getChannel = async (
apiContext: JellyfinApiContext,
params: LiveTvApiGetChannelRequest,
options?: AxiosRequestConfig
) => {
const { api, user } = apiContext;
if (!api) throw new Error('[getChannel] No API instance available');
if (!user?.Id) throw new Error('[getChannel] No User ID provided');
const response = await getLiveTvApi(api).getChannel(
{
userId: user.Id,
...params
},
options
);
return response.data;
};
export const getChannelQuery = (
apiContext: JellyfinApiContext,
params: LiveTvApiGetChannelRequest
) =>
queryOptions({
queryKey: ['Channel', params.channelId],
queryFn: ({ signal }) => getChannel(apiContext, params, { signal }),
enabled:
!!apiContext.api && !!apiContext.user?.Id && !!params.channelId
});
export const useGetChannel = (params: LiveTvApiGetChannelRequest) => {
const apiContext = useApi();
return useQuery(getChannelQuery(apiContext, params));
};

View file

@ -0,0 +1,35 @@
import type { AxiosRequestConfig } from 'axios';
import type { LiveTvApiGetSeriesTimerRequest } from '@jellyfin/sdk/lib/generated-client';
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import { type JellyfinApiContext, useApi } from 'hooks/useApi';
const getSeriesTimer = async (
apiContext: JellyfinApiContext,
params: LiveTvApiGetSeriesTimerRequest,
options?: AxiosRequestConfig
) => {
const { api } = apiContext;
if (!api) throw new Error('[getSeriesTimer] No API instance available');
const response = await getLiveTvApi(api).getSeriesTimer(params, options);
return response.data;
};
export const getSeriesTimerQuery = (
apiContext: JellyfinApiContext,
params: LiveTvApiGetSeriesTimerRequest
) =>
queryOptions({
queryKey: ['SeriesTimer', params.timerId],
queryFn: ({ signal }) => getSeriesTimer(apiContext, params, { signal }),
enabled: !!apiContext.api && !!apiContext.user?.Id && !!params.timerId
});
export const useGetSeriesTimer = (
requestParameters: LiveTvApiGetSeriesTimerRequest
) => {
const apiContext = useApi();
return useQuery(getSeriesTimerQuery(apiContext, requestParameters));
};

View file

@ -0,0 +1,33 @@
import type { AxiosRequestConfig } from 'axios';
import type { LiveTvApiGetTimerRequest } from '@jellyfin/sdk/lib/generated-client';
import { getLiveTvApi } from '@jellyfin/sdk/lib/utils/api/live-tv-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import { type JellyfinApiContext, useApi } from 'hooks/useApi';
const getTimer = async (
currentApi: JellyfinApiContext,
params: LiveTvApiGetTimerRequest,
options?: AxiosRequestConfig
) => {
const { api } = currentApi;
if (!api) throw new Error('[getTimer] No API instance available');
const response = await getLiveTvApi(api).getTimer(params, options);
return response.data;
};
export const getTimerQuery = (
apiContext: JellyfinApiContext,
params: LiveTvApiGetTimerRequest
) =>
queryOptions({
queryKey: ['Timer', params.timerId],
queryFn: ({ signal }) => getTimer(apiContext, params, { signal }),
enabled: !!apiContext.api && !!apiContext.user?.Id && !!params.timerId
});
export const useGetTimer = (requestParameters: LiveTvApiGetTimerRequest) => {
const apiContext = useApi();
return useQuery(getTimerQuery(apiContext, requestParameters));
};

View file

@ -0,0 +1 @@
export * from './useDeleteAlternateSources';

View file

@ -0,0 +1,24 @@
import type { VideosApiDeleteAlternateSourcesRequest } from '@jellyfin/sdk/lib/generated-client';
import { getVideosApi } from '@jellyfin/sdk/lib/utils/api/videos-api';
import { useMutation } from '@tanstack/react-query';
import { type JellyfinApiContext, useApi } from 'hooks/useApi';
const deleteAlternateSources = async (
apiContext: JellyfinApiContext,
params: VideosApiDeleteAlternateSourcesRequest
) => {
const { api } = apiContext;
if (!api) throw new Error('[deleteAlternateSources] No API instance available');
const response = await getVideosApi(api).deleteAlternateSources(params);
return response.data;
};
export const useDeleteAlternateSources = () => {
const apiContext = useApi();
return useMutation({
mutationFn: (params: VideosApiDeleteAlternateSourcesRequest) =>
deleteAlternateSources(apiContext, params)
});
};

View file

@ -1067,6 +1067,8 @@
"MessageAreYouSureDeleteSubtitles": "Are you sure you wish to delete this subtitle file?",
"MessageAreYouSureYouWishToRemoveMediaFolder": "Are you sure you wish to remove this media folder?",
"MessageBrowsePluginCatalog": "Browse our plugin catalog to view available plugins.",
"MessageCancelSeriesTimerError": "An error occurred while canceling the series timer",
"MessageCancelTimerError": "An error occurred while canceling the timer",
"MessageChangeRecordingPath": "Changing your recording folder will not migrate existing recordings from the old location to the new. You'll need to move them manually if desired.",
"MessageConfirmAppExit": "Do you want to exit?",
"MessageConfirmDeleteGuideProvider": "Are you sure you wish to delete this guide provider?",
@ -1119,6 +1121,7 @@
"MessageRenameMediaFolder": "Renaming a media library will cause all metadata to be lost, proceed with caution.",
"MessageRepositoryInstallDisclaimer": "WARNING: Installing a third party plugin repository carries risks. It may contain unstable or malicious code, and may change at any time. Only install repositories from authors that you trust.",
"MessageSent": "Message sent.",
"MessageSplitVersionsError": "An error occurred while splitting versions",
"MessageSyncPlayCreateGroupDenied": "Permission required to create a group.",
"MessageSyncPlayDisabled": "SyncPlay disabled.",
"MessageSyncPlayEnabled": "SyncPlay enabled.",