From f7fcf44f94c6d71d417d0ef7985a7d76c167f293 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sun, 8 Sep 2024 20:12:37 +0300 Subject: [PATCH 1/5] Add global Api hooks --- src/hooks/api/libraryHooks/index.ts | 1 + src/hooks/api/libraryHooks/useGetDownload.ts | 38 ++++++++++++++++++ src/hooks/api/liveTvHooks/index.ts | 5 +++ .../api/liveTvHooks/useCancelSeriesTimer.ts | 23 +++++++++++ src/hooks/api/liveTvHooks/useCancelTimer.ts | 23 +++++++++++ src/hooks/api/liveTvHooks/useGetChannel.ts | 40 +++++++++++++++++++ .../api/liveTvHooks/useGetSeriesTimer.ts | 34 ++++++++++++++++ src/hooks/api/liveTvHooks/useGetTimer.ts | 34 ++++++++++++++++ src/hooks/api/videosHooks/index.ts | 1 + .../videosHooks/useDeleteAlternateSources.ts | 23 +++++++++++ 10 files changed, 222 insertions(+) create mode 100644 src/hooks/api/libraryHooks/index.ts create mode 100644 src/hooks/api/libraryHooks/useGetDownload.ts create mode 100644 src/hooks/api/liveTvHooks/index.ts create mode 100644 src/hooks/api/liveTvHooks/useCancelSeriesTimer.ts create mode 100644 src/hooks/api/liveTvHooks/useCancelTimer.ts create mode 100644 src/hooks/api/liveTvHooks/useGetChannel.ts create mode 100644 src/hooks/api/liveTvHooks/useGetSeriesTimer.ts create mode 100644 src/hooks/api/liveTvHooks/useGetTimer.ts create mode 100644 src/hooks/api/videosHooks/index.ts create mode 100644 src/hooks/api/videosHooks/useDeleteAlternateSources.ts diff --git a/src/hooks/api/libraryHooks/index.ts b/src/hooks/api/libraryHooks/index.ts new file mode 100644 index 0000000000..53dadb17fd --- /dev/null +++ b/src/hooks/api/libraryHooks/index.ts @@ -0,0 +1 @@ +export * from './useGetDownload'; diff --git a/src/hooks/api/libraryHooks/useGetDownload.ts b/src/hooks/api/libraryHooks/useGetDownload.ts new file mode 100644 index 0000000000..031e0e49b8 --- /dev/null +++ b/src/hooks/api/libraryHooks/useGetDownload.ts @@ -0,0 +1,38 @@ +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('No API instance available'); + if (!user?.Id) throw new Error('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)); +}; diff --git a/src/hooks/api/liveTvHooks/index.ts b/src/hooks/api/liveTvHooks/index.ts new file mode 100644 index 0000000000..458bb62e07 --- /dev/null +++ b/src/hooks/api/liveTvHooks/index.ts @@ -0,0 +1,5 @@ +export * from './useCancelSeriesTimer'; +export * from './useCancelTimer'; +export * from './useGetChannel'; +export * from './useGetSeriesTimer'; +export * from './useGetTimer'; diff --git a/src/hooks/api/liveTvHooks/useCancelSeriesTimer.ts b/src/hooks/api/liveTvHooks/useCancelSeriesTimer.ts new file mode 100644 index 0000000000..85c98b00e5 --- /dev/null +++ b/src/hooks/api/liveTvHooks/useCancelSeriesTimer.ts @@ -0,0 +1,23 @@ +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) { + const response = await getLiveTvApi(api).cancelSeriesTimer(params); + return response.data; + } +}; + +export const useCancelSeriesTimer = () => { + const apiContext = useApi(); + return useMutation({ + mutationFn: (params: LiveTvApiCancelSeriesTimerRequest) => + cancelSeriesTimer(apiContext, params) + }); +}; diff --git a/src/hooks/api/liveTvHooks/useCancelTimer.ts b/src/hooks/api/liveTvHooks/useCancelTimer.ts new file mode 100644 index 0000000000..7ef8985cba --- /dev/null +++ b/src/hooks/api/liveTvHooks/useCancelTimer.ts @@ -0,0 +1,23 @@ +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) { + const response = await getLiveTvApi(api).cancelTimer(params); + return response.data; + } +}; + +export const useCancelTimer = () => { + const apiContext = useApi(); + return useMutation({ + mutationFn: (params: LiveTvApiCancelTimerRequest) => + cancelTimer(apiContext, params) + }); +}; diff --git a/src/hooks/api/liveTvHooks/useGetChannel.ts b/src/hooks/api/liveTvHooks/useGetChannel.ts new file mode 100644 index 0000000000..93e241880c --- /dev/null +++ b/src/hooks/api/liveTvHooks/useGetChannel.ts @@ -0,0 +1,40 @@ +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('No API instance available'); + if (!user?.Id) throw new Error('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)); +}; diff --git a/src/hooks/api/liveTvHooks/useGetSeriesTimer.ts b/src/hooks/api/liveTvHooks/useGetSeriesTimer.ts new file mode 100644 index 0000000000..d7342902a2 --- /dev/null +++ b/src/hooks/api/liveTvHooks/useGetSeriesTimer.ts @@ -0,0 +1,34 @@ +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('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)); +}; diff --git a/src/hooks/api/liveTvHooks/useGetTimer.ts b/src/hooks/api/liveTvHooks/useGetTimer.ts new file mode 100644 index 0000000000..f219dea059 --- /dev/null +++ b/src/hooks/api/liveTvHooks/useGetTimer.ts @@ -0,0 +1,34 @@ +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('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)); +}; diff --git a/src/hooks/api/videosHooks/index.ts b/src/hooks/api/videosHooks/index.ts new file mode 100644 index 0000000000..1c600b2209 --- /dev/null +++ b/src/hooks/api/videosHooks/index.ts @@ -0,0 +1 @@ +export * from './useDeleteAlternateSources'; diff --git a/src/hooks/api/videosHooks/useDeleteAlternateSources.ts b/src/hooks/api/videosHooks/useDeleteAlternateSources.ts new file mode 100644 index 0000000000..c504c1bef9 --- /dev/null +++ b/src/hooks/api/videosHooks/useDeleteAlternateSources.ts @@ -0,0 +1,23 @@ +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) { + const response = await getVideosApi(api).deleteAlternateSources(params); + return response.data; + } +}; + +export const useDeleteAlternateSources = () => { + const apiContext = useApi(); + return useMutation({ + mutationFn: (params: VideosApiDeleteAlternateSourcesRequest) => + deleteAlternateSources(apiContext, params) + }); +}; From 690b1fbed549d316d09b0cf124269fdd54fa4989 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Sun, 8 Sep 2024 20:15:20 +0300 Subject: [PATCH 2/5] Add detail view buttons --- .../buttons/CancelSeriesTimerButton.tsx | 66 ++++++ .../components/buttons/CancelTimerButton.tsx | 58 +++++ .../components/buttons/DownloadButton.tsx | 42 ++++ .../components/buttons/InstantMixButton.tsx | 28 +++ .../components/buttons/MoreCommandsButton.tsx | 221 ++++++++++++++++++ .../components/buttons/PlayOrResumeButton.tsx | 87 +++++++ .../components/buttons/PlayTrailerButton.tsx | 28 +++ .../components/buttons/ShuffleButton.tsx | 29 +++ .../buttons/SplitVersionsButton.tsx | 65 ++++++ .../details/hooks/api/useGetItemByType.ts | 63 +++++ src/components/itemContextMenu.js | 9 +- 11 files changed, 692 insertions(+), 4 deletions(-) create mode 100644 src/apps/experimental/features/details/components/buttons/CancelSeriesTimerButton.tsx create mode 100644 src/apps/experimental/features/details/components/buttons/CancelTimerButton.tsx create mode 100644 src/apps/experimental/features/details/components/buttons/DownloadButton.tsx create mode 100644 src/apps/experimental/features/details/components/buttons/InstantMixButton.tsx create mode 100644 src/apps/experimental/features/details/components/buttons/MoreCommandsButton.tsx create mode 100644 src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx create mode 100644 src/apps/experimental/features/details/components/buttons/PlayTrailerButton.tsx create mode 100644 src/apps/experimental/features/details/components/buttons/ShuffleButton.tsx create mode 100644 src/apps/experimental/features/details/components/buttons/SplitVersionsButton.tsx create mode 100644 src/apps/experimental/features/details/hooks/api/useGetItemByType.ts diff --git a/src/apps/experimental/features/details/components/buttons/CancelSeriesTimerButton.tsx b/src/apps/experimental/features/details/components/buttons/CancelSeriesTimerButton.tsx new file mode 100644 index 0000000000..7f3acd1678 --- /dev/null +++ b/src/apps/experimental/features/details/components/buttons/CancelSeriesTimerButton.tsx @@ -0,0 +1,66 @@ +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 = ({ + 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) => { + console.error( + '[cancelSeriesTimer] failed to cancel series timer', + err + ); + } + } + ); + }) + .catch(() => { + // confirm dialog closed + }); + }, [cancelSeriesTimer, navigate, itemId]); + + return ( + + + + ); +}; + +export default CancelSeriesTimerButton; diff --git a/src/apps/experimental/features/details/components/buttons/CancelTimerButton.tsx b/src/apps/experimental/features/details/components/buttons/CancelTimerButton.tsx new file mode 100644 index 0000000000..0687df7f5d --- /dev/null +++ b/src/apps/experimental/features/details/components/buttons/CancelTimerButton.tsx @@ -0,0 +1,58 @@ +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 = ({ + 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) => { + console.error( + '[cancelTimer] failed to cancel timer', + err + ); + } + } + ); + }, [cancelTimer, queryClient, queryKey, timerId]); + + return ( + + + + ); +}; + +export default CancelTimerButton; diff --git a/src/apps/experimental/features/details/components/buttons/DownloadButton.tsx b/src/apps/experimental/features/details/components/buttons/DownloadButton.tsx new file mode 100644 index 0000000000..284984239e --- /dev/null +++ b/src/apps/experimental/features/details/components/buttons/DownloadButton.tsx @@ -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 = ({ 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 ( + + + + ); +}; + +export default DownloadButton; diff --git a/src/apps/experimental/features/details/components/buttons/InstantMixButton.tsx b/src/apps/experimental/features/details/components/buttons/InstantMixButton.tsx new file mode 100644 index 0000000000..9cb223178f --- /dev/null +++ b/src/apps/experimental/features/details/components/buttons/InstantMixButton.tsx @@ -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 = ({ item }) => { + const onInstantMixClick = useCallback(() => { + playbackManager.instantMix(item); + }, [item]); + + return ( + + + + ); +}; + +export default InstantMixButton; diff --git a/src/apps/experimental/features/details/components/buttons/MoreCommandsButton.tsx b/src/apps/experimental/features/details/components/buttons/MoreCommandsButton.tsx new file mode 100644 index 0000000000..e679f9e772 --- /dev/null +++ b/src/apps/experimental/features/details/components/buttons/MoreCommandsButton.tsx @@ -0,0 +1,221 @@ +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 { + className?: string; + itemType: ItemKind; + selectedItemId?: string; + itemId?: string; + items?: ItemDto[] | null; + collectionId?: NullableString; + playlistId?: NullableString; + canEditPlaylist?: boolean; + itemPlaylistItemId?: NullableString; + contextMenuOpts?: ContextMenuOpts; + queryKey?: string[]; +} + +const MoreCommandsButton: FC = ({ + className, + 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) => { + 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 ( + + + + ); + } + + return null; +}; + +export default MoreCommandsButton; diff --git a/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx b/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx new file mode 100644 index 0000000000..faed1104cc --- /dev/null +++ b/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx @@ -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 = ({ + 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 ( + + {isResumable ? : } + + ); +}; + +export default PlayOrResumeButton; diff --git a/src/apps/experimental/features/details/components/buttons/PlayTrailerButton.tsx b/src/apps/experimental/features/details/components/buttons/PlayTrailerButton.tsx new file mode 100644 index 0000000000..0c82c06690 --- /dev/null +++ b/src/apps/experimental/features/details/components/buttons/PlayTrailerButton.tsx @@ -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 = ({ item }) => { + const onPlayTrailerClick = useCallback(async () => { + await playbackManager.playTrailers(item); + }, [item]); + + return ( + + + + ); +}; + +export default PlayTrailerButton; diff --git a/src/apps/experimental/features/details/components/buttons/ShuffleButton.tsx b/src/apps/experimental/features/details/components/buttons/ShuffleButton.tsx new file mode 100644 index 0000000000..258e26fc79 --- /dev/null +++ b/src/apps/experimental/features/details/components/buttons/ShuffleButton.tsx @@ -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 = ({ item }) => { + const shuffle = useCallback(() => { + playbackManager.shuffle(item); + }, [item]); + + return ( + + + + ); +}; + +export default ShuffleButton; diff --git a/src/apps/experimental/features/details/components/buttons/SplitVersionsButton.tsx b/src/apps/experimental/features/details/components/buttons/SplitVersionsButton.tsx new file mode 100644 index 0000000000..b7bb101693 --- /dev/null +++ b/src/apps/experimental/features/details/components/buttons/SplitVersionsButton.tsx @@ -0,0 +1,65 @@ +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'; + +interface SplitVersionsButtonProps { + paramId: string; + queryKey?: string[]; +} + +const SplitVersionsButton: FC = ({ + 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) => { + console.error( + '[splitVersions] failed to delete Videos', + err + ); + } + } + ); + }) + .catch(() => { + // confirm dialog closed + }); + }, [deleteAlternateSources, paramId, queryClient, queryKey]); + + return ( + + + + ); +}; + +export default SplitVersionsButton; diff --git a/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts b/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts new file mode 100644 index 0000000000..dc72771b40 --- /dev/null +++ b/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts @@ -0,0 +1,63 @@ +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 type{ NullableString } from 'types/base/common/shared/types'; +import { ItemKind } from 'types/base/models/item-kind'; + +const getItemByType = async ( + apiContext: JellyfinApiContext, + itemType: ItemKind, + itemId: NullableString, + options?: AxiosRequestConfig +) => { + const { api, user } = apiContext; + if (!api) throw new Error('No API instance available'); + if (!user?.Id) throw new Error('No User ID provided'); + if (!itemId) throw new Error('No item 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: NullableString; +} + +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 + }); +}; diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index ee70af15cf..42b6885fab 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -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({ From 49b0ba3071bb77bee9fff31d346c737c0cf70163 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Mon, 9 Sep 2024 02:30:21 +0300 Subject: [PATCH 3/5] apply suggestion Co-authored-by: dmitrylyzo <56478732+dmitrylyzo@users.noreply.github.com> --- .../details/components/buttons/CancelSeriesTimerButton.tsx | 2 ++ .../details/components/buttons/CancelTimerButton.tsx | 2 ++ .../details/components/buttons/MoreCommandsButton.tsx | 6 ++---- .../details/components/buttons/PlayOrResumeButton.tsx | 2 +- .../features/details/components/buttons/ShuffleButton.tsx | 2 +- .../details/components/buttons/SplitVersionsButton.tsx | 5 ++++- .../features/details/hooks/api/useGetItemByType.ts | 6 ++---- src/strings/en-us.json | 3 +++ 8 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/apps/experimental/features/details/components/buttons/CancelSeriesTimerButton.tsx b/src/apps/experimental/features/details/components/buttons/CancelSeriesTimerButton.tsx index 7f3acd1678..2f9cb360b7 100644 --- a/src/apps/experimental/features/details/components/buttons/CancelSeriesTimerButton.tsx +++ b/src/apps/experimental/features/details/components/buttons/CancelSeriesTimerButton.tsx @@ -39,6 +39,8 @@ const CancelSeriesTimerButton: FC = ({ navigate('/livetv.html'); }, onError: (err: unknown) => { + loading.hide(); + toast(globalize.translate('MessageCancelSeriesTimerError')); console.error( '[cancelSeriesTimer] failed to cancel series timer', err diff --git a/src/apps/experimental/features/details/components/buttons/CancelTimerButton.tsx b/src/apps/experimental/features/details/components/buttons/CancelTimerButton.tsx index 0687df7f5d..0745bc204e 100644 --- a/src/apps/experimental/features/details/components/buttons/CancelTimerButton.tsx +++ b/src/apps/experimental/features/details/components/buttons/CancelTimerButton.tsx @@ -35,6 +35,8 @@ const CancelTimerButton: FC = ({ }, onError: (err: unknown) => { + loading.hide(); + toast(globalize.translate('MessageCancelTimerError')); console.error( '[cancelTimer] failed to cancel timer', err diff --git a/src/apps/experimental/features/details/components/buttons/MoreCommandsButton.tsx b/src/apps/experimental/features/details/components/buttons/MoreCommandsButton.tsx index e679f9e772..e767712364 100644 --- a/src/apps/experimental/features/details/components/buttons/MoreCommandsButton.tsx +++ b/src/apps/experimental/features/details/components/buttons/MoreCommandsButton.tsx @@ -82,7 +82,6 @@ export interface ContextMenuOpts { } interface MoreCommandsButtonProps { - className?: string; itemType: ItemKind; selectedItemId?: string; itemId?: string; @@ -96,7 +95,6 @@ interface MoreCommandsButtonProps { } const MoreCommandsButton: FC = ({ - className, itemType, selectedItemId, itemId, @@ -112,7 +110,7 @@ const MoreCommandsButton: FC = ({ const queryClient = useQueryClient(); const { data: item } = useGetItemByType({ itemType, - itemId: selectedItemId || itemId + itemId: selectedItemId || itemId || '' }); const parentId = item?.SeasonId || item?.SeriesId || item?.ParentId; @@ -206,7 +204,7 @@ const MoreCommandsButton: FC = ({ ) { return ( diff --git a/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx b/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx index faed1104cc..a52453656f 100644 --- a/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx +++ b/src/apps/experimental/features/details/components/buttons/PlayOrResumeButton.tsx @@ -70,7 +70,7 @@ const PlayOrResumeButton: FC = ({ return ( = ({ item }) => { return ( diff --git a/src/apps/experimental/features/details/components/buttons/SplitVersionsButton.tsx b/src/apps/experimental/features/details/components/buttons/SplitVersionsButton.tsx index b7bb101693..6754281be0 100644 --- a/src/apps/experimental/features/details/components/buttons/SplitVersionsButton.tsx +++ b/src/apps/experimental/features/details/components/buttons/SplitVersionsButton.tsx @@ -6,6 +6,7 @@ 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; @@ -38,8 +39,10 @@ const SplitVersionsButton: FC = ({ }); }, onError: (err: unknown) => { + loading.hide(); + toast(globalize.translate('MessageSplitVersionsError')); console.error( - '[splitVersions] failed to delete Videos', + '[splitVersions] failed to split versions', err ); } diff --git a/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts b/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts index dc72771b40..3cb2a9c716 100644 --- a/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts +++ b/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts @@ -4,19 +4,17 @@ 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 type{ NullableString } from 'types/base/common/shared/types'; import { ItemKind } from 'types/base/models/item-kind'; const getItemByType = async ( apiContext: JellyfinApiContext, itemType: ItemKind, - itemId: NullableString, + itemId: string, options?: AxiosRequestConfig ) => { const { api, user } = apiContext; if (!api) throw new Error('No API instance available'); if (!user?.Id) throw new Error('No User ID provided'); - if (!itemId) throw new Error('No item ID provided'); let response; switch (itemType) { @@ -46,7 +44,7 @@ const getItemByType = async ( interface UseGetItemByTypeProps { itemType: ItemKind; - itemId: NullableString; + itemId: string; } export const useGetItemByType = ({ diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 560b469b90..f75735aa21 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1045,6 +1045,9 @@ "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 cancel series timer", + "MessageCancelTimerError": "An error occurred while cancel timer", + "MessageSplitVersionsError": "An error occurred while split versions", "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?", From 1c18fa8fb2e18c277ae38487418096aea15573a6 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Tue, 10 Sep 2024 04:46:48 +0300 Subject: [PATCH 4/5] apply suggestion Co-authored-by: dmitrylyzo <56478732+dmitrylyzo@users.noreply.github.com> --- src/strings/en-us.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/en-us.json b/src/strings/en-us.json index f75735aa21..eed6dc6722 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1047,7 +1047,6 @@ "MessageBrowsePluginCatalog": "Browse our plugin catalog to view available plugins.", "MessageCancelSeriesTimerError": "An error occurred while cancel series timer", "MessageCancelTimerError": "An error occurred while cancel timer", - "MessageSplitVersionsError": "An error occurred while split versions", "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?", @@ -1100,6 +1099,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 split versions", "MessageSyncPlayCreateGroupDenied": "Permission required to create a group.", "MessageSyncPlayDisabled": "SyncPlay disabled.", "MessageSyncPlayEnabled": "SyncPlay enabled.", From c5bbd5bca9fbf54e6bb035fa7c85b0b1b01ac159 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Tue, 24 Sep 2024 04:15:12 +0300 Subject: [PATCH 5/5] apply suggestion Co-authored-by: Bill Thornton --- .../details/hooks/api/useGetItemByType.ts | 5 ++-- src/hooks/api/libraryHooks/useGetDownload.ts | 26 ++++++++----------- .../api/liveTvHooks/useCancelSeriesTimer.ts | 9 ++++--- src/hooks/api/liveTvHooks/useCancelTimer.ts | 9 ++++--- src/hooks/api/liveTvHooks/useGetChannel.ts | 21 ++++++++------- .../api/liveTvHooks/useGetSeriesTimer.ts | 23 ++++++++-------- src/hooks/api/liveTvHooks/useGetTimer.ts | 19 +++++++------- .../videosHooks/useDeleteAlternateSources.ts | 9 ++++--- src/strings/en-us.json | 6 ++--- 9 files changed, 64 insertions(+), 63 deletions(-) diff --git a/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts b/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts index 3cb2a9c716..c2966e97d4 100644 --- a/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts +++ b/src/apps/experimental/features/details/hooks/api/useGetItemByType.ts @@ -13,8 +13,9 @@ const getItemByType = async ( options?: AxiosRequestConfig ) => { const { api, user } = apiContext; - if (!api) throw new Error('No API instance available'); - if (!user?.Id) throw new Error('No User ID provided'); + + 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) { diff --git a/src/hooks/api/libraryHooks/useGetDownload.ts b/src/hooks/api/libraryHooks/useGetDownload.ts index 031e0e49b8..a57b7b4c01 100644 --- a/src/hooks/api/libraryHooks/useGetDownload.ts +++ b/src/hooks/api/libraryHooks/useGetDownload.ts @@ -10,29 +10,25 @@ const getDownload = async ( options?: AxiosRequestConfig ) => { const { api, user } = apiContext; - if (!api) throw new Error('No API instance available'); - if (!user?.Id) throw new Error('No User ID provided'); - const response = await getLibraryApi(api).getDownload( - params, - options - ); + 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 -}); +) => + queryOptions({ + queryKey: ['Download', params.itemId], + queryFn: ({ signal }) => getDownload(apiContext, params, { signal }), + enabled: !!apiContext.api && !!apiContext.user?.Id && !!params.itemId + }); -export const useGetDownload = ( - params: LibraryApiGetDownloadRequest -) => { +export const useGetDownload = (params: LibraryApiGetDownloadRequest) => { const apiContext = useApi(); return useQuery(getDownloadQuery(apiContext, params)); }; diff --git a/src/hooks/api/liveTvHooks/useCancelSeriesTimer.ts b/src/hooks/api/liveTvHooks/useCancelSeriesTimer.ts index 85c98b00e5..20747299ea 100644 --- a/src/hooks/api/liveTvHooks/useCancelSeriesTimer.ts +++ b/src/hooks/api/liveTvHooks/useCancelSeriesTimer.ts @@ -8,10 +8,11 @@ const cancelSeriesTimer = async ( params: LiveTvApiCancelSeriesTimerRequest ) => { const { api } = apiContext; - if (api) { - const response = await getLiveTvApi(api).cancelSeriesTimer(params); - return response.data; - } + + if (!api) throw new Error('[cancelSeriesTimer] No API instance available'); + + const response = await getLiveTvApi(api).cancelSeriesTimer(params); + return response.data; }; export const useCancelSeriesTimer = () => { diff --git a/src/hooks/api/liveTvHooks/useCancelTimer.ts b/src/hooks/api/liveTvHooks/useCancelTimer.ts index 7ef8985cba..ed5b7846bf 100644 --- a/src/hooks/api/liveTvHooks/useCancelTimer.ts +++ b/src/hooks/api/liveTvHooks/useCancelTimer.ts @@ -8,10 +8,11 @@ const cancelTimer = async ( params: LiveTvApiCancelTimerRequest ) => { const { api } = apiContext; - if (api) { - const response = await getLiveTvApi(api).cancelTimer(params); - return response.data; - } + + if (!api) throw new Error('[cancelTimer] No API instance available'); + + const response = await getLiveTvApi(api).cancelTimer(params); + return response.data; }; export const useCancelTimer = () => { diff --git a/src/hooks/api/liveTvHooks/useGetChannel.ts b/src/hooks/api/liveTvHooks/useGetChannel.ts index 93e241880c..53b119d485 100644 --- a/src/hooks/api/liveTvHooks/useGetChannel.ts +++ b/src/hooks/api/liveTvHooks/useGetChannel.ts @@ -10,8 +10,9 @@ const getChannel = async ( options?: AxiosRequestConfig ) => { const { api, user } = apiContext; - if (!api) throw new Error('No API instance available'); - if (!user?.Id) throw new Error('No User ID provided'); + + 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( { @@ -26,15 +27,15 @@ const getChannel = async ( 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 -}); +) => + queryOptions({ + queryKey: ['Channel', params.channelId], + queryFn: ({ signal }) => getChannel(apiContext, params, { signal }), + enabled: + !!apiContext.api && !!apiContext.user?.Id && !!params.channelId + }); -export const useGetChannel = ( - params: LiveTvApiGetChannelRequest -) => { +export const useGetChannel = (params: LiveTvApiGetChannelRequest) => { const apiContext = useApi(); return useQuery(getChannelQuery(apiContext, params)); }; diff --git a/src/hooks/api/liveTvHooks/useGetSeriesTimer.ts b/src/hooks/api/liveTvHooks/useGetSeriesTimer.ts index d7342902a2..91e43baf72 100644 --- a/src/hooks/api/liveTvHooks/useGetSeriesTimer.ts +++ b/src/hooks/api/liveTvHooks/useGetSeriesTimer.ts @@ -10,25 +10,26 @@ const getSeriesTimer = async ( options?: AxiosRequestConfig ) => { const { api } = apiContext; - if (!api) throw new Error('No API instance available'); - const response = await getLiveTvApi(api).getSeriesTimer( - params, - options - ); + 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 -}); +) => + queryOptions({ + queryKey: ['SeriesTimer', params.timerId], + queryFn: ({ signal }) => getSeriesTimer(apiContext, params, { signal }), + enabled: !!apiContext.api && !!apiContext.user?.Id && !!params.timerId + }); -export const useGetSeriesTimer = (requestParameters: LiveTvApiGetSeriesTimerRequest) => { +export const useGetSeriesTimer = ( + requestParameters: LiveTvApiGetSeriesTimerRequest +) => { const apiContext = useApi(); return useQuery(getSeriesTimerQuery(apiContext, requestParameters)); }; diff --git a/src/hooks/api/liveTvHooks/useGetTimer.ts b/src/hooks/api/liveTvHooks/useGetTimer.ts index f219dea059..f328ebfd7a 100644 --- a/src/hooks/api/liveTvHooks/useGetTimer.ts +++ b/src/hooks/api/liveTvHooks/useGetTimer.ts @@ -10,23 +10,22 @@ const getTimer = async ( options?: AxiosRequestConfig ) => { const { api } = currentApi; - if (!api) throw new Error('No API instance available'); - const response = await getLiveTvApi(api).getTimer( - params, - options - ); + 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 -}); +) => + 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(); diff --git a/src/hooks/api/videosHooks/useDeleteAlternateSources.ts b/src/hooks/api/videosHooks/useDeleteAlternateSources.ts index c504c1bef9..6378a236a9 100644 --- a/src/hooks/api/videosHooks/useDeleteAlternateSources.ts +++ b/src/hooks/api/videosHooks/useDeleteAlternateSources.ts @@ -8,10 +8,11 @@ const deleteAlternateSources = async ( params: VideosApiDeleteAlternateSourcesRequest ) => { const { api } = apiContext; - if (api) { - const response = await getVideosApi(api).deleteAlternateSources(params); - return response.data; - } + + if (!api) throw new Error('[deleteAlternateSources] No API instance available'); + + const response = await getVideosApi(api).deleteAlternateSources(params); + return response.data; }; export const useDeleteAlternateSources = () => { diff --git a/src/strings/en-us.json b/src/strings/en-us.json index eed6dc6722..a707b0a3c8 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1045,8 +1045,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 cancel series timer", - "MessageCancelTimerError": "An error occurred while cancel timer", + "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?", @@ -1099,7 +1099,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 split versions", + "MessageSplitVersionsError": "An error occurred while splitting versions", "MessageSyncPlayCreateGroupDenied": "Permission required to create a group.", "MessageSyncPlayDisabled": "SyncPlay disabled.", "MessageSyncPlayEnabled": "SyncPlay enabled.",