diff --git a/bun.lock b/bun.lock index b27f4902..d31158fd 100644 --- a/bun.lock +++ b/bun.lock @@ -51,7 +51,7 @@ "react-native-reanimated": "4.1.5", "react-native-safe-area-context": "5.6.2", "react-native-screens": "4.18.0", - "react-native-sortables": "^1.9.3", + "react-native-sortables": "1.9.4", "react-native-text-ticker": "^1.15.0", "react-native-toast-message": "^2.3.3", "react-native-track-player": "5.0.0-alpha0", @@ -1942,7 +1942,7 @@ "react-native-screens": ["react-native-screens@4.18.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ=="], - "react-native-sortables": ["react-native-sortables@1.9.3", "", { "optionalDependencies": { "react-native-haptic-feedback": ">=2.0.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-VLhW9+3AVEaJNwwQSgN+n/Qe+YRB0C0mNWTjHhyzcZ+YjY4BmJao4bZxl5lD6EsfqZ1Ij6B2ZdxjNlSkUXrvow=="], + "react-native-sortables": ["react-native-sortables@1.9.4", "", { "optionalDependencies": { "react-native-haptic-feedback": ">=2.0.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-a6hxT+gl14HA5Sm8UiLXJqF8KMEQVa+mUJd75OnzoVsmrxUDtjAatlMdV0kI9qTQDT/ZSFLPRmdUhOR762IA4g=="], "react-native-tab-view": ["react-native-tab-view@4.2.0", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0" } }, "sha512-TUbh7Yr0tE/99t1pJQLbQ+4/Px67xkT7/r3AhfV+93Q3WoUira0Lx7yuKUP2C118doqxub8NCLERwcqsHr29nQ=="], diff --git a/package.json b/package.json index 2b147680..53fc10ce 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "react-native-reanimated": "4.1.5", "react-native-safe-area-context": "5.6.2", "react-native-screens": "4.18.0", - "react-native-sortables": "^1.9.3", + "react-native-sortables": "1.9.4", "react-native-text-ticker": "^1.15.0", "react-native-toast-message": "^2.3.3", "react-native-track-player": "5.0.0-alpha0", diff --git a/src/components/Album/index.tsx b/src/components/Album/index.tsx index 3afcbdcd..192f6504 100644 --- a/src/components/Album/index.tsx +++ b/src/components/Album/index.tsx @@ -17,7 +17,6 @@ import { useNetworkContext } from '../../providers/Network' import { useNetworkStatus } from '../../stores/network' import { useLoadNewQueue } from '../../providers/Player/hooks/mutations' import { QueuingType } from '../../enums/queuing-type' -import { useAlbumContext } from '../../providers/Album' import { useNavigation } from '@react-navigation/native' import HomeStackParamList from '../../screens/Home/types' import LibraryStackParamList from '../../screens/Library/types' @@ -26,6 +25,9 @@ import { BaseStackParamList } from '../../screens/types' import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../../stores/device-profile' import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry' import { useApi } from '../../stores' +import { QueryKeys } from '../../enums/query-keys' +import { fetchAlbumDiscs } from '../../api/queries/item' +import { useQuery } from '@tanstack/react-query' /** * The screen for an Album's track list @@ -35,12 +37,16 @@ import { useApi } from '../../stores' * * @returns A React component */ -export function Album(): React.JSX.Element { +export function Album({ album }: { album: BaseItemDto }): React.JSX.Element { const navigation = useNavigation>() - const { album, discs, isPending } = useAlbumContext() - const api = useApi() + + const { data: discs, isPending } = useQuery({ + queryKey: [QueryKeys.ItemTracks, album.Id], + queryFn: () => fetchAlbumDiscs(api, album), + }) + const { addToDownloadQueue, pendingDownloads } = useNetworkContext() const downloadingDeviceProfile = useDownloadingDeviceProfile() @@ -92,7 +98,7 @@ export function Album(): React.JSX.Element { ) : null }} - ListHeaderComponent={AlbumTrackListHeader} + ListHeaderComponent={() => } renderItem={({ item: track, index }) => ( )} - ListFooterComponent={AlbumTrackListFooter} + ListFooterComponent={() => } ListEmptyComponent={() => ( {isPending ? : No tracks found} @@ -120,7 +126,7 @@ export function Album(): React.JSX.Element { * @param playAlbum The function to call to play the album * @returns A React component */ -function AlbumTrackListHeader(): React.JSX.Element { +function AlbumTrackListHeader({ album }: { album: BaseItemDto }): React.JSX.Element { const api = useApi() const { width } = useSafeAreaFrame() @@ -130,7 +136,10 @@ function AlbumTrackListHeader(): React.JSX.Element { const loadNewQueue = useLoadNewQueue() - const { album, discs } = useAlbumContext() + const { data: discs, isPending } = useQuery({ + queryKey: [QueryKeys.ItemTracks, album.Id], + queryFn: () => fetchAlbumDiscs(api, album), + }) const navigation = useNavigation>() @@ -235,8 +244,7 @@ function AlbumTrackListHeader(): React.JSX.Element { ) } -function AlbumTrackListFooter(): React.JSX.Element { - const { album } = useAlbumContext() +function AlbumTrackListFooter({ album }: { album: BaseItemDto }): React.JSX.Element { const navigation = useNavigation< NativeStackNavigationProp< diff --git a/src/components/Playlist/components/header.tsx b/src/components/Playlist/components/header.tsx index 036b00b6..bf9647d5 100644 --- a/src/components/Playlist/components/header.tsx +++ b/src/components/Playlist/components/header.tsx @@ -3,7 +3,6 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { H5, Spacer, XStack, YStack } from 'tamagui' import InstantMixButton from '../../Global/components/instant-mix-button' import Icon from '../../Global/components/icon' -import { usePlaylistContext } from '../../../providers/Playlist' import { useNetworkStatus } from '../../../../src/stores/network' import { useNetworkContext } from '../../../../src/providers/Network' import { ActivityIndicator } from 'react-native' @@ -19,15 +18,21 @@ import ItemImage from '../../Global/components/image' import { useApi } from '../../../stores' import Input from '../../Global/helpers/input' import Animated, { FadeInDown, FadeOutDown } from 'react-native-reanimated' +import { Dispatch, SetStateAction } from 'react' export default function PlaylistTracklistHeader({ - canEdit, + playlist, + playlistTracks, + editing, + newName, + setNewName, }: { - canEdit?: boolean + playlist: BaseItemDto + playlistTracks: BaseItemDto[] | undefined + editing: boolean + newName: string + setNewName: Dispatch> }): React.JSX.Element { - const { playlist, playlistTracks, editing, setEditing, newName, setNewName } = - usePlaylistContext() - return ( @@ -68,10 +73,8 @@ export default function PlaylistTracklistHeader({ ) : ( @@ -83,16 +86,12 @@ export default function PlaylistTracklistHeader({ function PlaylistHeaderControls({ editing, - setEditing, playlist, playlistTracks, - canEdit, }: { editing: boolean - setEditing: (editing: boolean) => void playlist: BaseItemDto playlistTracks: BaseItemDto[] - canEdit: boolean | undefined }): React.JSX.Element { const { addToDownloadQueue, pendingDownloads } = useNetworkContext() const streamingDeviceProfile = useStreamingDeviceProfile() @@ -133,18 +132,7 @@ function PlaylistHeaderControls({ return ( - {editing && canEdit ? ( - { - navigation.push('DeletePlaylist', { playlist }) - }} - small - /> - ) : ( - - )} + @@ -155,16 +143,6 @@ function PlaylistHeaderControls({ playPlaylist(true)} small /> - {canEdit && ( - - setEditing(!editing)} - small - /> - - )} {!isDownloading ? ( (false) + + const [newName, setNewName] = useState(playlist.Name ?? '') + + const [playlistTracks, setPlaylistTracks] = useState(undefined) + + const trigger = useHapticFeedback() + + const { data: tracks, isPending, refetch, isSuccess } = usePlaylistTracks(playlist) + + const { mutate: useUpdatePlaylist, isPending: isUpdating } = useMutation({ + mutationFn: ({ + playlist, + tracks, + newName, + }: { + playlist: BaseItemDto + tracks: BaseItemDto[] + newName: string + }) => { + return updatePlaylist( + api, + playlist.Id!, + newName, + tracks.map((track) => track.Id!), + ) + }, + onSuccess: () => { + trigger('notificationSuccess') + + // Refresh playlist component data + refetch() + }, + onError: () => { + trigger('notificationError') + setNewName(playlist.Name ?? '') + setPlaylistTracks(tracks ?? []) + }, + onSettled: () => { + setEditing(false) + }, + }) + + const handleCancel = () => { + setEditing(false) + setNewName(playlist.Name ?? '') + setPlaylistTracks(tracks) + } + + useEffect(() => { + if (!isPending && isSuccess) setPlaylistTracks(tracks) + }, [tracks, isPending, isSuccess]) + + useEffect(() => { + if (!editing) refetch() + }, [editing]) const loadNewQueue = useLoadNewQueue() @@ -128,9 +176,11 @@ export default function Playlist({ return ( {editing && ( - - - + + + + + )} } > - + fetchAlbumDiscs(api, album), - }) - - return { - album, - discs, - isPending, - } -} - -const AlbumContext = createContext({ - album: {}, - discs: undefined, - isPending: false, -}) - -export const AlbumProvider: ({ - album, - children, -}: { - album: BaseItemDto - children: ReactNode -}) => React.JSX.Element = ({ album, children }) => { - const context = AlbumContextInitializer(album) - - return {children} -} - -export const useAlbumContext = () => useContext(AlbumContext) diff --git a/src/providers/Playlist/index.tsx b/src/providers/Playlist/index.tsx deleted file mode 100644 index 2701c725..00000000 --- a/src/providers/Playlist/index.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' -import { UseMutateFunction, useMutation } from '@tanstack/react-query' -import { createContext, ReactNode, useContext, useEffect, useState } from 'react' -import { updatePlaylist } from '../../api/mutations/playlists' -import { SharedValue, useSharedValue } from 'react-native-reanimated' -import useHapticFeedback from '../../hooks/use-haptic-feedback' -import { useApi } from '../../stores' -import { usePlaylistTracks } from '../../api/queries/playlist' - -interface PlaylistContext { - playlist: BaseItemDto - playlistTracks: BaseItemDto[] | undefined - refetch: () => void - isPending: boolean - editing: boolean - setEditing: (editing: boolean) => void - newName: string - setNewName: (name: string) => void - setPlaylistTracks: (tracks: BaseItemDto[]) => void - useUpdatePlaylist: UseMutateFunction< - void, - Error, - { - playlist: BaseItemDto - tracks: BaseItemDto[] - newName: string - }, - unknown - > - isUpdating?: boolean - handleCancel: () => void -} - -const PlaylistContextInitializer = (playlist: BaseItemDto) => { - const api = useApi() - - const canEdit = playlist.CanDelete - const [editing, setEditing] = useState(false) - - const [newName, setNewName] = useState(playlist.Name ?? '') - - const [playlistTracks, setPlaylistTracks] = useState(undefined) - - const trigger = useHapticFeedback() - - const { data: tracks, isPending, refetch, isSuccess } = usePlaylistTracks(playlist) - - const { mutate: useUpdatePlaylist, isPending: isUpdating } = useMutation({ - mutationFn: ({ - playlist, - tracks, - newName, - }: { - playlist: BaseItemDto - tracks: BaseItemDto[] - newName: string - }) => { - return updatePlaylist( - api, - playlist.Id!, - newName, - tracks.map((track) => track.Id!), - ) - }, - onSuccess: () => { - trigger('notificationSuccess') - - // Refresh playlist component data - refetch() - }, - onError: () => { - trigger('notificationError') - setNewName(playlist.Name ?? '') - setPlaylistTracks(tracks ?? []) - }, - onSettled: () => { - setEditing(false) - }, - }) - - const handleCancel = () => { - setEditing(false) - setNewName(playlist.Name ?? '') - setPlaylistTracks(tracks) - } - - useEffect(() => { - if (!isPending && isSuccess) setPlaylistTracks(tracks) - }, [tracks, isPending, isSuccess]) - - useEffect(() => { - if (!editing) refetch() - }, [editing]) - - return { - playlist, - playlistTracks, - refetch, - isPending, - editing, - setEditing, - newName, - setNewName, - setPlaylistTracks, - useUpdatePlaylist, - handleCancel, - isUpdating, - } -} - -const PlaylistContext = createContext({ - playlist: {}, - playlistTracks: undefined, - refetch: () => {}, - isPending: false, - editing: false, - setEditing: () => {}, - newName: '', - setNewName: () => {}, - setPlaylistTracks: () => {}, - useUpdatePlaylist: () => {}, - handleCancel: () => {}, - isUpdating: false, -}) - -export const PlaylistProvider = ({ - playlist, - children, -}: { - playlist: BaseItemDto - children: ReactNode -}) => { - const context = PlaylistContextInitializer(playlist) - - return {children} -} - -export const usePlaylistContext = () => useContext(PlaylistContext) diff --git a/src/screens/Album/index.tsx b/src/screens/Album/index.tsx index c3c205be..ba5fc779 100644 --- a/src/screens/Album/index.tsx +++ b/src/screens/Album/index.tsx @@ -1,13 +1,8 @@ import { Album } from '../../components/Album' import { AlbumProps } from '../types' -import { AlbumProvider } from '../../providers/Album' export default function AlbumScreen({ route, navigation }: AlbumProps): React.JSX.Element { const { album } = route.params - return ( - - - - ) + return } diff --git a/src/screens/Playlist/index.tsx b/src/screens/Playlist/index.tsx index c036df8f..f1083a8e 100644 --- a/src/screens/Playlist/index.tsx +++ b/src/screens/Playlist/index.tsx @@ -1,9 +1,8 @@ -import { BaseStackParamList, RootStackParamList } from '../types' +import { BaseStackParamList } from '../types' import { RouteProp } from '@react-navigation/native' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import React from 'react' import Playlist from '../../components/Playlist/index' -import { PlaylistProvider } from '../../providers/Playlist' export function PlaylistScreen({ route, @@ -13,12 +12,10 @@ export function PlaylistScreen({ navigation: NativeStackNavigationProp }): React.JSX.Element { return ( - - - + ) }