diff --git a/api/queries/functions/media.ts b/api/queries/functions/media.ts index 137cf675..5d153b74 100644 --- a/api/queries/functions/media.ts +++ b/api/queries/functions/media.ts @@ -10,7 +10,7 @@ export async function fetchMediaInfo(itemId: string): Promise { - console.debug(data) + console.debug('Received media info response') resolve(data) }) .catch((error) => { diff --git a/components/Global/components/item.tsx b/components/Global/components/item.tsx index 1abdb373..3d85615b 100644 --- a/components/Global/components/item.tsx +++ b/components/Global/components/item.tsx @@ -1,4 +1,3 @@ -import { usePlayerContext } from '../../../player/provider' import { StackParamList } from '../../../components/types' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { NativeStackNavigationProp } from '@react-navigation/native-stack' @@ -9,6 +8,9 @@ import BlurhashedImage from './blurhashed-image' import Icon from '../helpers/icon' import { QueuingType } from '../../../enums/queuing-type' import { RunTimeTicks } from '../helpers/time-codes' +import { useQueueContext } from '../../../player/queue-provider' +import { usePlayerContext } from '../../../player/provider' +import { State } from 'react-native-track-player' export default function Item({ item, @@ -19,7 +21,8 @@ export default function Item({ queueName: string navigation: NativeStackNavigationProp }): React.JSX.Element { - const { usePlayNewQueue } = usePlayerContext() + const { useStartPlayback } = usePlayerContext() + const { useLoadNewQueue } = useQueueContext() const { width } = useSafeAreaFrame() @@ -48,12 +51,20 @@ export default function Item({ } case 'Audio': { - usePlayNewQueue.mutate({ - track: item, - tracklist: [item], - queue: 'Search', - queuingType: QueuingType.FromSelection, - }) + useLoadNewQueue.mutate( + { + track: item, + tracklist: [item], + index: 0, + queue: 'Search', + queuingType: QueuingType.FromSelection, + }, + { + onSuccess: () => { + useStartPlayback.mutate() + }, + }, + ) break } } diff --git a/components/Global/components/track.tsx b/components/Global/components/track.tsx index 2f58b335..f300edfd 100644 --- a/components/Global/components/track.tsx +++ b/components/Global/components/track.tsx @@ -18,12 +18,14 @@ import { useNetworkContext } from '../../../components/Network/provider' import { useQuery } from '@tanstack/react-query' import { QueryKeys } from '../../../enums/query-keys' import { fetchMediaInfo } from '../../../api/queries/functions/media' +import { useQueueContext } from '../../../player/queue-provider' +import { State } from 'react-native-track-player' interface TrackProps { track: BaseItemDto navigation: NativeStackNavigationProp tracklist?: BaseItemDto[] | undefined - index?: number | undefined + index: number queue: Queue showArtwork?: boolean | undefined onPress?: () => void | undefined @@ -51,7 +53,8 @@ export default function Track({ onRemove, }: TrackProps): React.JSX.Element { const theme = useTheme() - const { nowPlaying, playQueue, usePlayNewQueue } = usePlayerContext() + const { nowPlaying, useStartPlayback } = usePlayerContext() + const { playQueue, useLoadNewQueue } = useQueueContext() const { downloadedTracks, networkStatus } = useNetworkContext() const isPlaying = nowPlaying?.item.Id === track.Id @@ -76,13 +79,20 @@ export default function Track({ if (onPress) { onPress() } else { - usePlayNewQueue.mutate({ - track, - index, - tracklist: tracklist ?? playQueue.map((track) => track.item), - queue, - queuingType: QueuingType.FromSelection, - }) + useLoadNewQueue.mutate( + { + track, + index, + tracklist: tracklist ?? playQueue.map((track) => track.item), + queue, + queuingType: QueuingType.FromSelection, + }, + { + onSuccess: () => { + useStartPlayback.mutate() + }, + }, + ) } }} onLongPress={ diff --git a/components/Home/helpers/frequent-tracks.tsx b/components/Home/helpers/frequent-tracks.tsx index 32680de5..1414fa9d 100644 --- a/components/Home/helpers/frequent-tracks.tsx +++ b/components/Home/helpers/frequent-tracks.tsx @@ -6,9 +6,10 @@ import HorizontalCardList from '../../../components/Global/components/horizontal import { ItemCard } from '../../../components/Global/components/item-card' import { QueuingType } from '../../../enums/queuing-type' import { trigger } from 'react-native-haptic-feedback' -import { usePlayerContext } from '../../../player/provider' import { H2 } from '../../../components/Global/helpers/text' import Icon from '../../../components/Global/helpers/icon' +import { useQueueContext } from '../../../player/queue-provider' +import { usePlayerContext } from '../../../player/provider' export default function FrequentlyPlayedTracks({ navigation, @@ -17,7 +18,8 @@ export default function FrequentlyPlayedTracks({ }): React.JSX.Element { const { frequentlyPlayed } = useHomeContext() - const { usePlayNewQueue } = usePlayerContext() + const { useStartPlayback } = usePlayerContext() + const { useLoadNewQueue } = useQueueContext() return ( @@ -48,13 +50,20 @@ export default function FrequentlyPlayedTracks({ subCaption={`${track.Artists?.join(', ')}`} squared onPress={() => { - usePlayNewQueue.mutate({ - track, - index, - tracklist: frequentlyPlayed ?? [track], - queue: 'On Repeat', - queuingType: QueuingType.FromSelection, - }) + useLoadNewQueue.mutate( + { + track, + index, + tracklist: frequentlyPlayed ?? [track], + queue: 'On Repeat', + queuingType: QueuingType.FromSelection, + }, + { + onSuccess: () => { + useStartPlayback.mutate() + }, + }, + ) }} onLongPress={() => { trigger('impactMedium') diff --git a/components/Home/helpers/recently-played.tsx b/components/Home/helpers/recently-played.tsx index 5f984371..74d18b87 100644 --- a/components/Home/helpers/recently-played.tsx +++ b/components/Home/helpers/recently-played.tsx @@ -9,15 +9,16 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { trigger } from 'react-native-haptic-feedback' import { QueuingType } from '../../../enums/queuing-type' import HorizontalCardList from '../../../components/Global/components/horizontal-list' -import { QueryKeys } from '../../../enums/query-keys' import Icon from '../../../components/Global/helpers/icon' +import { useQueueContext } from '../../../player/queue-provider' export default function RecentlyPlayed({ navigation, }: { navigation: NativeStackNavigationProp }): React.JSX.Element { - const { nowPlaying, usePlayNewQueue } = usePlayerContext() + const { nowPlaying, useStartPlayback } = usePlayerContext() + const { useLoadNewQueue } = useQueueContext() const { recentTracks } = useHomeContext() return useMemo(() => { @@ -49,13 +50,20 @@ export default function RecentlyPlayed({ squared item={recentlyPlayedTrack} onPress={() => { - usePlayNewQueue.mutate({ - track: recentlyPlayedTrack, - index: index, - tracklist: recentTracks ?? [recentlyPlayedTrack], - queue: 'Recently Played', - queuingType: QueuingType.FromSelection, - }) + useLoadNewQueue.mutate( + { + track: recentlyPlayedTrack, + index: index, + tracklist: recentTracks ?? [recentlyPlayedTrack], + queue: 'Recently Played', + queuingType: QueuingType.FromSelection, + }, + { + onSuccess: () => { + useStartPlayback.mutate() + }, + }, + ) }} onLongPress={() => { trigger('impactMedium') diff --git a/components/Home/stack.tsx b/components/Home/stack.tsx index b7ec58a7..fe6e1e93 100644 --- a/components/Home/stack.tsx +++ b/components/Home/stack.tsx @@ -6,11 +6,9 @@ import { AlbumScreen } from '../Album' import { PlaylistScreen } from '../Playlist/screens' import { ProvidedHome } from './component' import DetailsScreen from '../ItemDetail/screen' -import AddPlaylist from '../Library/components/add-playlist' import ArtistsScreen from '../Artists/screen' import TracksScreen from '../Tracks/screen' import { ArtistScreen } from '../Artist' -import { OfflineList } from '../offlineList' const Stack = createNativeStackNavigator() diff --git a/components/ItemDetail/helpers/TrackOptions.tsx b/components/ItemDetail/helpers/TrackOptions.tsx index 8c6ea707..d9076ad9 100644 --- a/components/ItemDetail/helpers/TrackOptions.tsx +++ b/components/ItemDetail/helpers/TrackOptions.tsx @@ -1,4 +1,3 @@ -import { usePlayerContext } from '../../../player/provider' import { StackParamList } from '../../../components/types' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { NativeStackNavigationProp } from '@react-navigation/native-stack' @@ -33,6 +32,7 @@ import { Image } from 'expo-image' import { getImageApi } from '@jellyfin/sdk/lib/utils/api' import Client from '../../../api/client' import { useNetworkContext } from '../../../components/Network/provider' +import { useQueueContext } from '../../../player/queue-provider' interface TrackOptionsProps { track: BaseItemDto @@ -67,7 +67,7 @@ export default function TrackOptions({ queryFn: () => fetchUserPlaylists(), }) - const { useAddToQueue } = usePlayerContext() + const { useAddToQueue } = useQueueContext() const { width } = useSafeAreaFrame() diff --git a/components/Player/helpers/controls.tsx b/components/Player/helpers/controls.tsx index f44b5071..e89785a7 100644 --- a/components/Player/helpers/controls.tsx +++ b/components/Player/helpers/controls.tsx @@ -5,11 +5,14 @@ import Icon from '../../../components/Global/helpers/icon' import { getProgress, seekBy, skipToNext } from 'react-native-track-player/lib/src/trackPlayer' import { usePlayerContext } from '../../../player/provider' import { useSafeAreaFrame } from 'react-native-safe-area-context' +import { useQueueContext } from '../../../player/queue-provider' export default function Controls(): React.JSX.Element { const { width } = useSafeAreaFrame() - const { usePrevious, useSeekTo } = usePlayerContext() + const { useSeekTo } = usePlayerContext() + + const { usePrevious } = useQueueContext() return ( @@ -22,13 +25,7 @@ export default function Controls(): React.JSX.Element { { - const progress = await getProgress() - if (progress.position < 3) usePrevious.mutate() - else { - useSeekTo.mutate(0) - } - }} + onPress={() => usePrevious.mutate()} large /> diff --git a/components/Player/helpers/scrubber.tsx b/components/Player/helpers/scrubber.tsx index 20938102..9367820f 100644 --- a/components/Player/helpers/scrubber.tsx +++ b/components/Player/helpers/scrubber.tsx @@ -9,12 +9,14 @@ import { usePlayerContext } from '../../../player/provider' import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes' import { UPDATE_INTERVAL } from '../../../player/config' import { ProgressMultiplier } from '../component.config' -import { useSharedValue } from 'react-native-reanimated' +import { useQueueContext } from '../../../player/queue-provider' const scrubGesture = Gesture.Pan() export default function Scrubber(): React.JSX.Element { - const { useSeekTo, useSkip, usePrevious } = usePlayerContext() + const { useSeekTo } = usePlayerContext() + + const { useSkip, usePrevious } = useQueueContext() const { width } = useSafeAreaFrame() diff --git a/components/Player/mini-player.tsx b/components/Player/mini-player.tsx index b6524cfa..4db3b388 100644 --- a/components/Player/mini-player.tsx +++ b/components/Player/mini-player.tsx @@ -11,6 +11,7 @@ import { TextTickerConfig } from './component.config' import { Image } from 'expo-image' import { getImageApi } from '@jellyfin/sdk/lib/utils/api' import Client from '../../api/client' +import { useQueueContext } from '../../player/queue-provider' export function Miniplayer({ navigation, @@ -19,7 +20,8 @@ export function Miniplayer({ }): React.JSX.Element { const theme = useTheme() - const { nowPlaying, useSkip } = usePlayerContext() + const { nowPlaying } = usePlayerContext() + const { useSkip } = useQueueContext() return ( }): React.JSX.Element { - const { nowPlayingIsFavorite, setNowPlayingIsFavorite, nowPlaying, queue } = usePlayerContext() + const { nowPlayingIsFavorite, setNowPlayingIsFavorite, nowPlaying } = usePlayerContext() + + const { queueRef } = useQueueContext() const { width } = useSafeAreaFrame() @@ -57,9 +60,9 @@ export default function PlayerScreen({ > { // If the Queue is a BaseItemDto, display the name of it - typeof queue === 'object' - ? (queue as BaseItemDto).Name ?? 'Untitled' - : queue + typeof queueRef === 'object' + ? queueRef.Name ?? 'Untitled' + : queueRef } @@ -91,7 +94,7 @@ export default function PlayerScreen({ ) - }, [nowPlaying, queue])} + }, [nowPlaying, queueRef])} {/** Memoize TextTickers otherwise they won't animate due to the progress being updated in the PlayerContext */} diff --git a/components/Player/screens/queue.tsx b/components/Player/screens/queue.tsx index 392c8f63..7ea7a9ee 100644 --- a/components/Player/screens/queue.tsx +++ b/components/Player/screens/queue.tsx @@ -7,6 +7,8 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context' import DraggableFlatList from 'react-native-draggable-flatlist' import { trigger } from 'react-native-haptic-feedback' import { Separator } from 'tamagui' +import { useQueueContext } from '../../../player/queue-provider' +import Animated from 'react-native-reanimated' export default function Queue({ navigation, @@ -14,15 +16,16 @@ export default function Queue({ navigation: NativeStackNavigationProp }): React.JSX.Element { const { width } = useSafeAreaFrame() + const { nowPlaying } = usePlayerContext() + const { playQueue, - queue, - useClearQueue, + queueRef, + useRemoveUpcomingTracks, useRemoveFromQueue, useReorderQueue, useSkip, - nowPlaying, - } = usePlayerContext() + } = useQueueContext() navigation.setOptions({ headerRight: () => { @@ -30,7 +33,7 @@ export default function Queue({ { - useClearQueue.mutate() + useRemoveUpcomingTracks.mutate() }} /> ) @@ -42,50 +45,52 @@ export default function Queue({ ) return ( - ({ - length: width / 9, - offset: (width / 9) * index, - index, - })} - initialScrollIndex={scrollIndex !== -1 ? scrollIndex : 0} - ItemSeparatorComponent={() => } - // itemEnteringAnimation={FadeIn} - // itemExitingAnimation={FadeOut} - // itemLayoutAnimation={SequencedTransition} - keyExtractor={({ item }, index) => { - return `${index}-${item.Id}` - }} - numColumns={1} - onDragEnd={({ data, from, to }) => { - useReorderQueue.mutate({ newOrder: data, from, to }) - }} - renderItem={({ item: queueItem, getIndex, drag, isActive }) => ( - { - useSkip.mutate(getIndex()) - }} - onLongPress={() => { - trigger('impactLight') - drag() - }} - isNested - showRemove - onRemove={() => { - if (getIndex()) useRemoveFromQueue.mutate(getIndex()!) - }} - /> - )} - /> + + ({ + length: width / 9, + offset: (width / 9) * index, + index, + })} + initialScrollIndex={scrollIndex !== -1 ? scrollIndex : 0} + ItemSeparatorComponent={() => } + // itemEnteringAnimation={FadeIn} + // itemExitingAnimation={FadeOut} + // itemLayoutAnimation={SequencedTransition} + keyExtractor={({ item }, index) => { + return `${index}-${item.Id}` + }} + numColumns={1} + onDragEnd={({ data, from, to }) => { + useReorderQueue.mutate({ newOrder: data, from, to }) + }} + renderItem={({ item: queueItem, getIndex, drag, isActive }) => ( + { + useSkip.mutate(getIndex() ?? 0) + }} + onLongPress={() => { + trigger('impactLight') + drag() + }} + isNested + showRemove + onRemove={() => { + if (getIndex()) useRemoveFromQueue.mutate(getIndex()!) + }} + /> + )} + /> + ) } diff --git a/components/Settings/helpers/sign-out.tsx b/components/Settings/helpers/sign-out.tsx index d0862dab..fedba2b5 100644 --- a/components/Settings/helpers/sign-out.tsx +++ b/components/Settings/helpers/sign-out.tsx @@ -3,7 +3,7 @@ import Button from '../../Global/helpers/button' import Client from '../../../api/client' import { useJellifyContext } from '../../../components/provider' import TrackPlayer from 'react-native-track-player' -import { StackParamList } from '@/components/types' +import { StackParamList } from '../../../components/types' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { useNavigation } from '@react-navigation/native' @@ -17,7 +17,6 @@ export default function SignOut(): React.JSX.Element { setLoggedIn(false) Client.signOut() TrackPlayer.reset() - // navigation.navigate('Offline') }} > Sign Out diff --git a/components/jellify.tsx b/components/jellify.tsx index 2d350624..98c3a859 100644 --- a/components/jellify.tsx +++ b/components/jellify.tsx @@ -10,6 +10,7 @@ import { JellifyProvider, useJellifyContext } from './provider' import { ToastProvider } from '@tamagui/toast' import { JellifyUserDataProvider } from './user-data-provider' import { NetworkContextProvider } from './Network/provider' +import { QueueProvider } from '../player/queue-provider' export default function Jellify(): React.JSX.Element { return ( @@ -30,9 +31,11 @@ function App(): React.JSX.Element { return loggedIn ? ( - - - + + + + + ) : ( diff --git a/components/navigation.tsx b/components/navigation.tsx index 5550ed20..a64689c1 100644 --- a/components/navigation.tsx +++ b/components/navigation.tsx @@ -3,8 +3,6 @@ import Player from './Player/stack' import { Tabs } from './tabs' import { StackParamList } from './types' import { useTheme } from 'tamagui' -import DetailsScreen from './ItemDetail/screen' -import { OfflineList } from './offlineList' export default function Navigation(): React.JSX.Element { const RootStack = createNativeStackNavigator() @@ -29,14 +27,6 @@ export default function Navigation(): React.JSX.Element { presentation: 'modal', }} /> - ) } diff --git a/components/offlineList/index.tsx b/components/offlineList/index.tsx deleted file mode 100644 index c48b22c0..00000000 --- a/components/offlineList/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { JellifyTrack } from '@/types/JellifyTrack' -import React from 'react' -import { FlatList, Pressable } from 'react-native' -import Animated, { FadeIn, FadeOut, Layout } from 'react-native-reanimated' -import { Image, Text, View, XStack, YStack } from 'tamagui' -import { getAudioCache } from '../Network/offlineModeUtils' -import { usePlayerContext } from '../../player/provider' -import { useNavigation } from '@react-navigation/native' - -interface Props { - tracks: JellifyTrack[] - onPress: (track: JellifyTrack) => void -} - -export function OfflineList() { - const tracks = getAudioCache() - const navigation = useNavigation() - const { usePlayNewQueueOffline } = usePlayerContext() - const onPress = (track: JellifyTrack) => { - console.log('onPress', track) - usePlayNewQueueOffline.mutate({ trackListOffline: track }) - navigation.navigate('Player') - } - - const renderItem = ({ item }: { item: JellifyTrack }) => ( - - onPress(item)}> - - - - - {item.title || 'Unknown Title'} - - - {item.artist || 'Unknown Artist'} - - - - - - ) - - return ( - item.item.Id as string} - renderItem={renderItem} - contentContainerStyle={{ - padding: 16, - paddingBottom: 100, - }} - showsVerticalScrollIndicator={false} - /> - ) -} diff --git a/components/provider.tsx b/components/provider.tsx index dc4049c3..550ba0d6 100644 --- a/components/provider.tsx +++ b/components/provider.tsx @@ -29,7 +29,6 @@ const JellifyContextInitializer = () => { if (loggedIn) { CarPlay.setRootTemplate(CarPlayNavigation) - CarPlay.pushTemplate(CarPlayNowPlaying) if (Platform.OS === 'ios') { CarPlay.enableNowPlaying(true) // https://github.com/birkir/react-native-carplay/issues/185 diff --git a/ios/Jellify.xcodeproj/project.pbxproj b/ios/Jellify.xcodeproj/project.pbxproj index 77cf0907..a08a90c2 100644 --- a/ios/Jellify.xcodeproj/project.pbxproj +++ b/ios/Jellify.xcodeproj/project.pbxproj @@ -57,7 +57,6 @@ CF6B19F92D55121E002464CB /* icon_tinted_20pt_3x.png in Resources */ = {isa = PBXBuildFile; fileRef = CF6B19EB2D55121E002464CB /* icon_tinted_20pt_3x.png */; }; CF6B19FA2D55121E002464CB /* icon_tinted_60pt_3x.png in Resources */ = {isa = PBXBuildFile; fileRef = CF6B19F12D55121E002464CB /* icon_tinted_60pt_3x.png */; }; CF6B19FB2D55121E002464CB /* icon_tinted_40pt_2x.png in Resources */ = {isa = PBXBuildFile; fileRef = CF6B19EE2D55121E002464CB /* icon_tinted_40pt_2x.png */; }; - CF71790B2CBC486C0021BCA3 /* dummy-rntp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */; }; CF98CA472D3E99E0003D88B7 /* CarScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA452D3E99DF003D88B7 /* CarScene.swift */; }; CF98CA482D3E99E0003D88B7 /* PhoneScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */; }; CF98CA492D3E99E0003D88B7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */; }; @@ -148,7 +147,6 @@ CF6B19F12D55121E002464CB /* icon_tinted_60pt_3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_tinted_60pt_3x.png; sourceTree = ""; }; CF6B19F22D55121E002464CB /* icon_tinted_original.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_tinted_original.png; sourceTree = ""; }; CF7179092CBC486C0021BCA3 /* Jellify-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Jellify-Bridging-Header.h"; sourceTree = ""; }; - CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "dummy-rntp.swift"; sourceTree = ""; }; CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; CF98CA452D3E99DF003D88B7 /* CarScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarScene.swift; sourceTree = ""; }; CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneScene.swift; sourceTree = ""; }; @@ -204,7 +202,6 @@ 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */, - CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */, CF7179092CBC486C0021BCA3 /* Jellify-Bridging-Header.h */, ); name = Jellify; @@ -522,10 +519,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n"; @@ -539,10 +540,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n"; @@ -607,7 +612,6 @@ CF98CA472D3E99E0003D88B7 /* CarScene.swift in Sources */, CF98CA482D3E99E0003D88B7 /* PhoneScene.swift in Sources */, CF98CA492D3E99E0003D88B7 /* AppDelegate.swift in Sources */, - CF71790B2CBC486C0021BCA3 /* dummy-rntp.swift in Sources */, 66BC9C5D1B536CD0799EEC89 /* ExpoModulesProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -843,10 +847,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -932,10 +933,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/ios/dummy-rntp.swift b/ios/dummy-rntp.swift deleted file mode 100644 index 2d7d87db..00000000 --- a/ios/dummy-rntp.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// dummy-rntp.swift -// Jellify -// -// Created by Violet Caulfield on 10/13/24. -// - -import Foundation diff --git a/package.json b/package.json index dfa0a5ea..23171814 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "invert-color": "^2.0.0", "jest-expo": "^52.0.6", "lodash": "^4.17.21", - "npm-bundle": "^3.0.3", "patch-package": "^8.0.0", "react": "18.3.1", "react-freeze": "^1.0.4", diff --git a/player/helpers/queue.ts b/player/helpers/queue.ts index 34400653..48733628 100644 --- a/player/helpers/queue.ts +++ b/player/helpers/queue.ts @@ -23,7 +23,7 @@ export function buildNewQueue( return newQueue } -export function networkStatusCheck( +export function filterTracksOnNetworkStatus( networkStatus: networkStatusTypes | undefined, queuedItems: BaseItemDto[], downloadedTracks: JellifyDownload[], diff --git a/player/interfaces.ts b/player/interfaces.ts index 032f8177..1c471709 100644 --- a/player/interfaces.ts +++ b/player/interfaces.ts @@ -9,7 +9,6 @@ export interface QueueMutation { tracklist: BaseItemDto[] queue: Queue queuingType?: QueuingType | undefined - trackListOffline?: JellifyTrack } export interface AddToQueueMutation { diff --git a/player/provider.tsx b/player/provider.tsx index a14cd6b1..ad25873d 100644 --- a/player/provider.tsx +++ b/player/provider.tsx @@ -2,7 +2,6 @@ import { createContext, ReactNode, SetStateAction, useContext, useEffect, useSta import { JellifyTrack } from '../types/JellifyTrack' import { storage } from '../constants/storage' import { MMKVStorageKeys } from '../enums/mmkv-storage-keys' -import { findPlayNextIndexStart, findPlayQueueIndexStart } from './helpers/index' import TrackPlayer, { Event, State, @@ -13,58 +12,27 @@ import { isEqual, isUndefined } from 'lodash' import { handlePlaybackProgressUpdated, handlePlaybackState } from './handlers' import { useUpdateOptions } from '../player/hooks' import { useMutation, UseMutationResult } from '@tanstack/react-query' -import { mapDtoToTrack } from '../helpers/mappings' -import { QueuingType } from '../enums/queuing-type' import { trigger } from 'react-native-haptic-feedback' -import { - getActiveTrackIndex, - getQueue, - pause, - seekTo, - skip, - skipToNext, - skipToPrevious, -} from 'react-native-track-player/lib/src/trackPlayer' +import { pause, seekTo } from 'react-native-track-player/lib/src/trackPlayer' import { convertRunTimeTicksToSeconds } from '../helpers/runtimeticks' import Client from '../api/client' -import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces' -import { Section } from '../components/Player/types' -import { Queue } from './types/queue-item' -import * as Burnt from 'burnt' -import { markItemPlayed } from '../api/mutations/functions/item' -import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api' -import { SKIP_TO_PREVIOUS_THRESHOLD } from './config' import { useNetworkContext } from '../components/Network/provider' -import { networkStatusCheck } from './helpers/queue' -import { QueueProvider } from './queue-provider' +import { useQueueContext } from './queue-provider' interface PlayerContext { initialized: boolean nowPlayingIsFavorite: boolean setNowPlayingIsFavorite: React.Dispatch> nowPlaying: JellifyTrack | undefined - playQueue: JellifyTrack[] - queue: Queue - getQueueSectionData: () => Section[] - useAddToQueue: UseMutationResult - useClearQueue: UseMutationResult - useRemoveFromQueue: UseMutationResult - useReorderQueue: UseMutationResult - useTogglePlayback: UseMutationResult + useStartPlayback: UseMutationResult + useTogglePlayback: UseMutationResult useSeekTo: UseMutationResult - useSkip: UseMutationResult - usePrevious: UseMutationResult - usePlayNewQueue: UseMutationResult - playbackState: State | undefined - setNowPlaying: (track: JellifyTrack) => void } const PlayerContextInitializer = () => { const nowPlayingJson = storage.getString(MMKVStorageKeys.NowPlaying) - const playQueueJson = storage.getString(MMKVStorageKeys.PlayQueue) - const queueJson = storage.getString(MMKVStorageKeys.Queue) const playStateApi = getPlaystateApi(Client.api!) @@ -76,146 +44,27 @@ const PlayerContextInitializer = () => { nowPlayingJson ? JSON.parse(nowPlayingJson) : undefined, ) - const [isSkipping, setIsSkipping] = useState(false) + const { playQueue, currentIndex } = useQueueContext() - const [playQueue, setPlayQueue] = useState( - playQueueJson ? JSON.parse(playQueueJson) : [], - ) - - const [queue, setQueue] = useState(queueJson ? JSON.parse(queueJson) : 'Queue') //#endregion State //#region Functions - const play = async (index?: number | undefined) => { - if (index && index > 0) { - TrackPlayer.skip(index) - } - - TrackPlayer.play() + const play = async () => { + await TrackPlayer.play() } - const getQueueSectionData: () => Section[] = () => { - return Object.keys(QueuingType).map((type) => { - return { - title: type, - data: playQueue.filter((track) => track.QueuingType === type), - } as Section - }) - } - - /** - * Takes a {@link BaseItemDto} of a track on Jellyfin, and updates it's - * position in the {@link queue} - * - * - * @param track The Jellyfin track object to update and replace in the queue - */ - const replaceQueueItem: (track: BaseItemDto) => Promise = async (track: BaseItemDto) => { - const queue = (await TrackPlayer.getQueue()) as JellifyTrack[] - - const queueItemIndex = queue.findIndex((queuedTrack) => queuedTrack.item.Id === track.Id!) - - // Update queued item at index if found, else silently do nothing - if (queueItemIndex !== -1) { - const queueItem = queue[queueItemIndex] - - TrackPlayer.remove([queueItemIndex]).then(() => { - TrackPlayer.add( - mapDtoToTrack(track, downloadedTracks ?? [], queueItem.QueuingType), - queueItemIndex, - ) - }) - } - } - - const resetQueue = async (hideMiniplayer?: boolean | undefined) => { - console.debug('Clearing queue') - await TrackPlayer.setQueue([]) - setPlayQueue([]) - } - - const addToQueue = async (tracks: JellifyTrack[]) => { - const insertIndex = await findPlayQueueIndexStart(playQueue) - console.debug(`Adding ${tracks.length} to queue at index ${insertIndex}`) - - await TrackPlayer.add(tracks, insertIndex) - - setPlayQueue((await getQueue()) as JellifyTrack[]) - - console.debug(`Queue has ${playQueue.length} tracks`) - } - - const addToNext = async (tracks: JellifyTrack[]) => { - const insertIndex = await findPlayNextIndexStart(playQueue) - - console.debug(`Adding ${tracks.length} to queue at index ${insertIndex}`) - - await TrackPlayer.add(tracks, insertIndex) - - setPlayQueue((await getQueue()) as JellifyTrack[]) - } //#endregion Functions //#region Hooks - const useAddToQueue = useMutation({ - mutationFn: async (mutation: AddToQueueMutation) => { - trigger('impactLight') - - if (mutation.queuingType === QueuingType.PlayingNext) - return addToNext([ - mapDtoToTrack(mutation.track, downloadedTracks ?? [], mutation.queuingType), - ]) - else - return addToQueue([ - mapDtoToTrack(mutation.track, downloadedTracks ?? [], mutation.queuingType), - ]) - }, - onSuccess: (data, { queuingType }) => { - trigger('notificationSuccess') - - Burnt.alert({ - title: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue', - duration: 1, - preset: 'done', - }) - }, - onError: () => { - trigger('notificationError') - }, - }) - - const useRemoveFromQueue = useMutation({ - mutationFn: async (index: number) => { - trigger('impactMedium') - - await TrackPlayer.remove([index]) - - setPlayQueue((await TrackPlayer.getQueue()) as JellifyTrack[]) - }, - }) - - const useClearQueue = useMutation({ - mutationFn: async () => { - trigger('effectDoubleClick') - - await TrackPlayer.removeUpcomingTracks() - - setPlayQueue((await getQueue()) as JellifyTrack[]) - }, - }) - - const useReorderQueue = useMutation({ - mutationFn: async (mutation: QueueOrderMutation) => { - setPlayQueue(mutation.newOrder) - await TrackPlayer.move(mutation.from, mutation.to) - }, + const useStartPlayback = useMutation({ + mutationFn: play, }) const useTogglePlayback = useMutation({ - mutationFn: (index?: number | undefined) => { + mutationFn: () => { trigger('impactMedium') if (playbackState === State.Playing) return pause() - else return play(index) + else return play() }, }) @@ -231,102 +80,6 @@ const PlayerContextInitializer = () => { }) }, }) - - const useSkip = useMutation({ - mutationFn: async (index?: number | undefined) => { - trigger('impactMedium') - - // Handle if this is the last track in the queue - if (playQueue.length - 1 === (await getActiveTrackIndex())) return - else { - if (!isUndefined(index)) { - setIsSkipping(true) - setNowPlaying(playQueue[index]) - await skip(index) - setIsSkipping(false) - } else { - const nowPlayingIndex = playQueue.findIndex( - (track) => track.item.Id === nowPlaying!.item.Id, - ) - setNowPlaying(playQueue[nowPlayingIndex + 1]) - await skipToNext() - } - } - }, - }) - - const usePrevious = useMutation({ - mutationFn: async () => { - trigger('impactMedium') - - const nowPlayingIndex = playQueue.findIndex( - (track) => track.item.Id === nowPlaying!.item.Id, - ) - - const { position } = await TrackPlayer.getProgress() - - if (nowPlayingIndex > 0 && position < SKIP_TO_PREVIOUS_THRESHOLD) { - setNowPlaying(playQueue[nowPlayingIndex - 1]) - await skipToPrevious() - } else await seekTo(0) - }, - }) - - /** - * A mutation hook that adds tracks to the play queue - * - * Respects the network status of the app - if we are - * online, tracks are always added to the queue for streaming - * - if we are *offline* then only tracks that are downloaded - * will be added to the queue - */ - const usePlayNewQueue = useMutation({ - mutationFn: async (mutation: QueueMutation) => { - trigger('effectDoubleClick') - - setIsSkipping(true) - - // Optimistically set now playing - - setNowPlaying( - mapDtoToTrack( - mutation.tracklist[mutation.index ?? 0], - downloadedTracks ?? [], - QueuingType.FromSelection, - ), - ) - - await resetQueue(false) - - const queueItems = networkStatusCheck( - networkStatus, - mutation.tracklist, - downloadedTracks ?? [], - ) - - console.debug(`Adding ${queueItems.length} to the queue`) - - const queueTracks = queueItems.map((queueItem) => { - return mapDtoToTrack(queueItem, downloadedTracks ?? [], QueuingType.FromSelection) - }) - - console.debug(`Slotting ${queueTracks.length} track(s)`) - - await addToQueue(queueTracks) - - setQueue(mutation.queue) - }, - onSuccess: async (data, mutation: QueueMutation) => { - setIsSkipping(false) - await play(mutation.index) - - if (typeof mutation.queue === 'object') await markItemPlayed(queue as BaseItemDto) - }, - onError: async () => { - setIsSkipping(false) - setNowPlaying((await TrackPlayer.getActiveTrack()) as JellifyTrack) - }, - }) //#endregion //#region RNTP Setup @@ -384,7 +137,7 @@ const PlayerContextInitializer = () => { } case Event.PlaybackActiveTrackChanged: { - if (initialized && !isSkipping) { + if (initialized) { const activeTrack = (await TrackPlayer.getActiveTrack()) as | JellifyTrack | undefined @@ -419,14 +172,6 @@ const PlayerContextInitializer = () => { //#endregion RNTP Setup //#region useEffects - useEffect(() => { - storage.set(MMKVStorageKeys.Queue, JSON.stringify(queue)) - }, [queue]) - - useEffect(() => { - if (initialized && playQueue) - storage.set(MMKVStorageKeys.PlayQueue, JSON.stringify(playQueue)) - }, [playQueue]) useEffect(() => { if (initialized && nowPlaying) @@ -435,15 +180,17 @@ const PlayerContextInitializer = () => { useEffect(() => { if (!initialized && playQueue.length > 0 && nowPlaying) { - TrackPlayer.setQueue(playQueue).then(() => { - TrackPlayer.skip( - playQueue.findIndex((track) => track.item.Id! === nowPlaying.item.Id!), - ) - }) + TrackPlayer.skip(playQueue.findIndex((track) => track.item.Id! === nowPlaying.item.Id!)) } setInitialized(true) }, [playQueue, nowPlaying]) + + useEffect(() => { + if (currentIndex > -1 && playQueue.length > currentIndex) + console.debug(`Setting now playing to queue index ${currentIndex}`) + setNowPlaying(playQueue[currentIndex]) + }, [currentIndex]) //#endregion useEffects //#region return @@ -452,19 +199,9 @@ const PlayerContextInitializer = () => { nowPlayingIsFavorite, setNowPlayingIsFavorite, nowPlaying, - playQueue, - queue, - getQueueSectionData, - useAddToQueue, - useClearQueue, - setNowPlaying, - useReorderQueue, - useRemoveFromQueue, + useStartPlayback, useTogglePlayback, useSeekTo, - useSkip, - usePrevious, - usePlayNewQueue, playbackState, } //#endregion return @@ -476,65 +213,7 @@ export const PlayerContext = createContext({ nowPlayingIsFavorite: false, setNowPlayingIsFavorite: () => {}, nowPlaying: undefined, - setNowPlaying: () => {}, - playQueue: [], - queue: 'Recently Played', - getQueueSectionData: () => [], - useAddToQueue: { - mutate: () => {}, - mutateAsync: async () => {}, - data: undefined, - error: null, - variables: undefined, - isError: false, - isIdle: true, - isPaused: false, - isPending: false, - isSuccess: false, - status: 'idle', - reset: () => {}, - context: {}, - failureCount: 0, - failureReason: null, - submittedAt: 0, - }, - useClearQueue: { - mutate: () => {}, - mutateAsync: async () => {}, - data: undefined, - error: null, - variables: undefined, - isError: false, - isIdle: true, - isPaused: false, - isPending: false, - isSuccess: false, - status: 'idle', - reset: () => {}, - context: {}, - failureCount: 0, - failureReason: null, - submittedAt: 0, - }, - useRemoveFromQueue: { - mutate: () => {}, - mutateAsync: async () => {}, - data: undefined, - error: null, - variables: undefined, - isError: false, - isIdle: true, - isPaused: false, - isPending: false, - isSuccess: false, - status: 'idle', - reset: () => {}, - context: {}, - failureCount: 0, - failureReason: null, - submittedAt: 0, - }, - useReorderQueue: { + useStartPlayback: { mutate: () => {}, mutateAsync: async () => {}, data: undefined, @@ -588,61 +267,6 @@ export const PlayerContext = createContext({ failureReason: null, submittedAt: 0, }, - useSkip: { - mutate: () => {}, - mutateAsync: async () => {}, - data: undefined, - error: null, - variables: undefined, - isError: false, - isIdle: true, - isPaused: false, - isPending: false, - isSuccess: false, - status: 'idle', - reset: () => {}, - context: {}, - failureCount: 0, - failureReason: null, - submittedAt: 0, - }, - usePrevious: { - mutate: () => {}, - mutateAsync: async () => {}, - data: undefined, - error: null, - variables: undefined, - isError: false, - isIdle: true, - isPaused: false, - isPending: false, - isSuccess: false, - status: 'idle', - reset: () => {}, - context: {}, - failureCount: 0, - failureReason: null, - submittedAt: 0, - }, - usePlayNewQueue: { - mutate: () => {}, - mutateAsync: async () => {}, - data: undefined, - error: null, - variables: undefined, - isError: false, - isIdle: true, - isPaused: false, - isPending: false, - isSuccess: false, - status: 'idle', - reset: () => {}, - context: {}, - failureCount: 0, - failureReason: null, - submittedAt: 0, - }, - playbackState: undefined, }) //#endregion Create PlayerContext @@ -651,55 +275,9 @@ export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JS }: { children: ReactNode }) => { - const { - initialized, - nowPlayingIsFavorite, - setNowPlayingIsFavorite, - nowPlaying, - playQueue, - queue, - getQueueSectionData, - useAddToQueue, - useClearQueue, - useRemoveFromQueue, - useReorderQueue, - useTogglePlayback, - useSeekTo, - useSkip, - usePrevious, - setNowPlaying, - usePlayNewQueue, - playbackState, - } = PlayerContextInitializer() + const context = PlayerContextInitializer() - return ( - - - {children} - - - ) + return {children} } export const usePlayerContext = () => useContext(PlayerContext) diff --git a/player/queue-provider.tsx b/player/queue-provider.tsx index d39a5b98..7359c121 100644 --- a/player/queue-provider.tsx +++ b/player/queue-provider.tsx @@ -1,20 +1,448 @@ -import React, { ReactNode } from 'react' +import React, { ReactNode, useContext, useEffect, useState } from 'react' import { createContext } from 'react' +import { Queue } from './types/queue-item' +import { Section } from '../components/Player/types' +import { useMutation, UseMutationResult } from '@tanstack/react-query' +import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces' +import { storage } from '../constants/storage' +import { MMKVStorageKeys } from '../enums/mmkv-storage-keys' +import { JellifyTrack } from '../types/JellifyTrack' +import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' +import { mapDtoToTrack } from '../helpers/mappings' +import { useNetworkContext } from '../components/Network/provider' +import { QueuingType } from '../enums/queuing-type' +import TrackPlayer from 'react-native-track-player' +import { findPlayQueueIndexStart } from './helpers' +import { getQueue, seekTo } from 'react-native-track-player/lib/src/trackPlayer' +import { trigger } from 'react-native-haptic-feedback' +import * as Burnt from 'burnt' +import { markItemPlayed } from '../api/mutations/functions/item' +import { filterTracksOnNetworkStatus } from './helpers/queue' +import { SKIP_TO_PREVIOUS_THRESHOLD } from './config' -interface QueueContext {} - -const QueueContextInitailizer = () => { - return {} +interface QueueContext { + queueRef: Queue + playQueue: JellifyTrack[] + currentIndex: number + fetchQueueSectionData: () => Section[] + useAddToQueue: UseMutationResult + useLoadNewQueue: UseMutationResult + useRemoveUpcomingTracks: UseMutationResult + useRemoveFromQueue: UseMutationResult + useReorderQueue: UseMutationResult + useSkip: UseMutationResult + usePrevious: UseMutationResult } -export const QueueContext = createContext({}) +const QueueContextInitailizer = () => { + const queueRefJson = storage.getString(MMKVStorageKeys.Queue) + const playQueueJson = storage.getString(MMKVStorageKeys.PlayQueue) + + const queueRefInit = queueRefJson ? JSON.parse(queueRefJson) : 'Recently Played' + const playQueueInit = playQueueJson ? JSON.parse(playQueueJson) : [] + + const [queueRef, setQueueRef] = useState(queueRefInit) + const [playQueue, setPlayQueue] = useState(playQueueInit) + + const [updateRntp, setUpdateRntp] = useState(true) + + const [currentIndex, setCurrentIndex] = useState(-1) + + const { downloadedTracks, networkStatus } = useNetworkContext() + + //#region Functions + const fetchQueueSectionData: () => Section[] = () => { + return Object.keys(QueuingType).map((type) => { + return { + title: type, + data: playQueue.filter((track) => track.QueuingType === type), + } as Section + }) + } + + const resetQueue = () => { + console.debug(`Clearing queue of ${playQueue.length}`) + setPlayQueue([]) + } + + /** + * Takes a {@link BaseItemDto} of a track on Jellyfin, and updates it's + * position in the {@link queue} + * + * + * @param track The Jellyfin track object to update and replace in the queue + */ + const replaceQueueItem: (track: BaseItemDto) => Promise = async (track: BaseItemDto) => { + const queue = (await TrackPlayer.getQueue()) as JellifyTrack[] + + const queueItemIndex = queue.findIndex((queuedTrack) => queuedTrack.item.Id === track.Id!) + + // Update queued item at index if found, else silently do nothing + if (queueItemIndex !== -1) { + const queueItem = queue[queueItemIndex] + + TrackPlayer.remove([queueItemIndex]).then(() => { + TrackPlayer.add( + mapDtoToTrack(track, downloadedTracks ?? [], queueItem.QueuingType), + queueItemIndex, + ) + }) + } + } + + const loadQueue = async ( + audioItems: BaseItemDto[], + queuingRef: Queue, + startIndex: number = 1, + ) => { + console.debug(`Queuing ${audioItems.length} items`) + + const availableAudioItems = filterTracksOnNetworkStatus( + networkStatus, + audioItems, + downloadedTracks ?? [], + ) + + console.debug( + `Filtered out ${ + audioItems.length - availableAudioItems.length + } due to network status being ${networkStatus}`, + ) + + const queue = availableAudioItems.map((item) => + mapDtoToTrack(item, downloadedTracks ?? [], QueuingType.FromSelection), + ) + + setPlayQueue(queue) + setQueueRef(queuingRef) + setCurrentIndex(startIndex) + + console.debug(`Queued ${queue.length} tracks, starting at ${startIndex}`) + } + + const playNextInQueue = async (item: BaseItemDto) => { + console.debug(`Playing item next in queue`) + + const playNextTrack = mapDtoToTrack(item, downloadedTracks ?? [], QueuingType.PlayingNext) + + setUpdateRntp(false) + TrackPlayer.add([playNextTrack], currentIndex + 1) + setPlayQueue([ + ...playQueue.slice(0, currentIndex + 1), + playNextTrack, + ...playQueue.slice(currentIndex + 1), + ]) + } + + const playInQueue = async (items: BaseItemDto[]) => { + const insertIndex = await findPlayQueueIndexStart(playQueue) + console.debug(`Adding ${items.length} to queue at index ${insertIndex}`) + + setUpdateRntp(false) + await TrackPlayer.add( + items.map((item) => + mapDtoToTrack(item, downloadedTracks ?? [], QueuingType.DirectlyQueued), + ), + insertIndex, + ) + + setPlayQueue((await getQueue()) as JellifyTrack[]) + + console.debug(`Queue has ${playQueue.length} tracks`) + } + //#endregion Functions + + //#region Hooks + const useAddToQueue = useMutation({ + mutationFn: ({ track, queuingType }: AddToQueueMutation) => { + return queuingType === QueuingType.PlayingNext + ? playNextInQueue(track) + : playInQueue([track]) + }, + onSuccess: (data, { queuingType }) => { + trigger('notificationSuccess') + + Burnt.alert({ + title: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue', + duration: 0.5, + preset: 'done', + }) + }, + onError: () => { + trigger('notificationError') + }, + }) + + const useLoadNewQueue = useMutation({ + mutationFn: async ({ index, track, tracklist, queuingType, queue }: QueueMutation) => + loadQueue(tracklist, queue, index), + onSuccess: async (data, { queue }: QueueMutation) => { + trigger('notificationSuccess') + + if (typeof queue === 'object') await markItemPlayed(queue) + }, + }) + + const useRemoveFromQueue = useMutation({ + mutationFn: async (index: number) => { + trigger('impactMedium') + + setUpdateRntp(false) + TrackPlayer.remove([index]) + setPlayQueue([ + ...playQueue.slice(0, index), + ...playQueue.slice(index + 1, playQueue.length - 1), + ]) + }, + }) + + /** + * + */ + const useRemoveUpcomingTracks = useMutation({ + mutationFn: async () => { + setUpdateRntp(false) + TrackPlayer.removeUpcomingTracks() + setPlayQueue([...playQueue.slice(0, currentIndex + 1)]) + }, + onSuccess: () => { + trigger('notificationSuccess') + }, + }) + + const useReorderQueue = useMutation({ + mutationFn: async ({ from, to, newOrder }: QueueOrderMutation) => { + setUpdateRntp(false) + TrackPlayer.move(from, to) + setPlayQueue(newOrder) + }, + onSuccess: () => { + trigger('notificationSuccess') + }, + }) + + const useSkip = useMutation({ + mutationFn: async (index?: number | undefined) => { + trigger('impactMedium') + + console.debug( + `Skip to next triggered. Index is ${`using ${ + index ? index : currentIndex + } as index ${index ? 'since it was provided' : ''}`}`, + ) + + if (index && index < playQueue.length - 1) setCurrentIndex(index) + else if (playQueue.length - 1 > currentIndex) setCurrentIndex(currentIndex + 1) + }, + }) + + const usePrevious = useMutation({ + mutationFn: async () => { + trigger('impactMedium') + + const { position } = await TrackPlayer.getProgress() + + console.debug(`Skip to previous triggered. Index is ${currentIndex}`) + + if (currentIndex > 0 && position < SKIP_TO_PREVIOUS_THRESHOLD) { + setCurrentIndex(currentIndex - 1) + } else await seekTo(0) + }, + }) + + const useUpdateRntpQueue = useMutation({ + mutationFn: async () => { + if (updateRntp) await TrackPlayer.setQueue(playQueue) + + setUpdateRntp(true) + }, + }) + + const useUpdateRntpIndex = useMutation({ + mutationFn: async (index: number) => { + await TrackPlayer.skip(index) + }, + }) + + //#endregion Hooks + + //#region useEffect(s) + /** + * Update RNTP Queue when our queue + * is updated + */ + useEffect(() => { + useUpdateRntpQueue.mutate() + storage.set(MMKVStorageKeys.PlayQueue, JSON.stringify(playQueue)) + }, [playQueue]) + + useEffect(() => { + storage.set(MMKVStorageKeys.Queue, JSON.stringify(queueRef)) + }, [queueRef]) + + useEffect(() => { + useUpdateRntpIndex.mutate(currentIndex) + }, [currentIndex]) + + //#endregion useEffect(s) + + return { + queueRef, + playQueue, + currentIndex, + setCurrentIndex, + fetchQueueSectionData, + useAddToQueue, + useLoadNewQueue, + useRemoveFromQueue, + useRemoveUpcomingTracks, + useReorderQueue, + useSkip, + usePrevious, + } +} + +export const QueueContext = createContext({ + queueRef: 'Recently Played', + playQueue: [], + currentIndex: -1, + fetchQueueSectionData: () => [], + useAddToQueue: { + mutate: () => {}, + mutateAsync: async () => {}, + data: undefined, + error: null, + variables: undefined, + isError: false, + isIdle: true, + isPaused: false, + isPending: false, + isSuccess: false, + status: 'idle', + reset: () => {}, + context: {}, + failureCount: 0, + failureReason: null, + submittedAt: 0, + }, + useLoadNewQueue: { + mutate: () => {}, + mutateAsync: async () => {}, + data: undefined, + error: null, + variables: undefined, + isError: false, + isIdle: true, + isPaused: false, + isPending: false, + isSuccess: false, + status: 'idle', + reset: () => {}, + context: {}, + failureCount: 0, + failureReason: null, + submittedAt: 0, + }, + useSkip: { + mutate: () => {}, + mutateAsync: async () => {}, + data: undefined, + error: null, + variables: undefined, + isError: false, + isIdle: true, + isPaused: false, + isPending: false, + isSuccess: false, + status: 'idle', + reset: () => {}, + context: {}, + failureCount: 0, + failureReason: null, + submittedAt: 0, + }, + usePrevious: { + mutate: () => {}, + mutateAsync: async () => {}, + data: undefined, + error: null, + variables: undefined, + isError: false, + isIdle: true, + isPaused: false, + isPending: false, + isSuccess: false, + status: 'idle', + reset: () => {}, + context: {}, + failureCount: 0, + failureReason: null, + submittedAt: 0, + }, + useRemoveFromQueue: { + mutate: () => {}, + mutateAsync: async () => {}, + data: undefined, + error: null, + variables: undefined, + isError: false, + isIdle: true, + isPaused: false, + isPending: false, + isSuccess: false, + status: 'idle', + reset: () => {}, + context: {}, + failureCount: 0, + failureReason: null, + submittedAt: 0, + }, + useRemoveUpcomingTracks: { + mutate: () => {}, + mutateAsync: async () => {}, + data: undefined, + error: null, + variables: undefined, + isError: false, + isIdle: true, + isPaused: false, + isPending: false, + isSuccess: false, + status: 'idle', + reset: () => {}, + context: {}, + failureCount: 0, + failureReason: null, + submittedAt: 0, + }, + useReorderQueue: { + mutate: () => {}, + mutateAsync: async () => {}, + data: undefined, + error: null, + variables: undefined, + isError: false, + isIdle: true, + isPaused: false, + isPending: false, + isSuccess: false, + status: 'idle', + reset: () => {}, + context: {}, + failureCount: 0, + failureReason: null, + submittedAt: 0, + }, +}) export const QueueProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({ children, }: { children: ReactNode }) => { - const urmo = QueueContextInitailizer() + const context = QueueContextInitailizer() - return {children} + return {children} } + +export const useQueueContext = () => useContext(QueueContext) diff --git a/yarn.lock b/yarn.lock index 13b15d6e..cee04f00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6539,17 +6539,6 @@ glob@^10.2.2, glob@^10.3.10, glob@^10.4.2: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^6.0.1: - version "6.0.4" - resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz" - integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A== - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" @@ -6846,11 +6835,6 @@ ini@~1.3.0: resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -insync@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/insync/-/insync-2.1.1.tgz" - integrity sha512-UzUhOZFpCMM22Xlig9iUPqalf8n7c4eYScamce1C+jN3ad8FtmVm42ryMwVq0hAxHbwUhWFhPvTFQQpFdDUKkw== - internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz" @@ -8478,13 +8462,6 @@ mimic-function@^5.0.0: resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== -"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - minimatch@^10.0.1: version "10.0.1" resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz" @@ -8492,6 +8469,13 @@ minimatch@^10.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + minimatch@^8.0.2: version "8.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz" @@ -8610,11 +8594,6 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -ncp@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz" - integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA== - negotiator@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" @@ -8684,17 +8663,6 @@ normalize-path@^3.0.0: resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-bundle@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/npm-bundle/-/npm-bundle-3.0.3.tgz" - integrity sha512-fHF7FR32YNgjqi0MQMLnE78Ff9/wYd4/7/Cke3dLLi2QzETKotIiWGCxwDoXAZDWVoTuVRYQa2ZdiZPuBL7QnA== - dependencies: - glob "^6.0.1" - insync "^2.1.1" - mkdirp "^0.5.1" - ncp "^2.0.0" - rimraf "^2.4.4" - npm-package-arg@^11.0.0: version "11.0.3" resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz" @@ -9911,7 +9879,7 @@ rfdc@^1.4.1: resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== -rimraf@^2.4.4, rimraf@^2.6.3: +rimraf@^2.6.3: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==