diff --git a/ios/Jellify/Info.plist b/ios/Jellify/Info.plist
index 29db5a1f..23ba206b 100644
--- a/ios/Jellify/Info.plist
+++ b/ios/Jellify/Info.plist
@@ -94,7 +94,6 @@
audio
fetch
- processing
UILaunchStoryboardName
LaunchScreen
diff --git a/src/components/Album/header.tsx b/src/components/Album/header.tsx
index a6e77852..ed3bcd4f 100644
--- a/src/components/Album/header.tsx
+++ b/src/components/Album/header.tsx
@@ -1,10 +1,8 @@
-import { useLoadNewQueue } from '../../hooks/player/callbacks'
import { BaseStackParamList } from '../../screens/types'
-import { useApi } from '../../stores'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
-import { YStack, H5, XStack, Separator, Text, Paragraph } from 'tamagui'
+import { YStack, H5, XStack, Separator, Paragraph } from 'tamagui'
import Icon from '../Global/components/icon'
import ItemImage from '../Global/components/image'
import { RunTimeTicks } from '../Global/helpers/time-codes'
@@ -13,6 +11,7 @@ import { InstantMixButton } from '../Global/components/instant-mix-button'
import { useAlbumDiscs } from '../../api/queries/album'
import { formatArtistName } from '../../utils/formatting/artist-names'
import { BUTTON_PRESS_STYLES, ICON_PRESS_STYLES } from '../../configs/style.config'
+import { loadNewQueue } from '../../hooks/player/functions/queue'
/**
* Renders a header for an Album's track list
@@ -22,10 +21,6 @@ import { BUTTON_PRESS_STYLES, ICON_PRESS_STYLES } from '../../configs/style.conf
* @returns A React component
*/
export default function AlbumTrackListHeader({ album }: { album: BaseItemDto }): React.JSX.Element {
- const api = useApi()
-
- const loadNewQueue = useLoadNewQueue()
-
const { data: discs, isPending } = useAlbumDiscs(album)
const navigation = useNavigation>()
diff --git a/src/components/Artist/header.tsx b/src/components/Artist/header.tsx
index 9a221484..cb4c7005 100644
--- a/src/components/Artist/header.tsx
+++ b/src/components/Artist/header.tsx
@@ -11,11 +11,11 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '@/src/screens/types'
import IconButton from '../Global/helpers/icon-button'
import { fetchAlbumDiscs } from '../../api/queries/item'
-import { useLoadNewQueue } from '../../hooks/player/callbacks'
import { getApi } from '../../stores'
import Icon from '../Global/components/icon'
import { useArtistTracks } from '../../api/queries/track'
import { ICON_PRESS_STYLES } from '../../configs/style.config'
+import { loadNewQueue } from '../../hooks/player/functions/queue'
export default function ArtistHeader(): React.JSX.Element {
const { width } = useSafeAreaFrame()
@@ -24,8 +24,6 @@ export default function ArtistHeader(): React.JSX.Element {
const { artist, albums } = useArtistContext()
- const loadNewQueue = useLoadNewQueue()
-
const navigation = useNavigation>()
const playArtist = async (shuffled: boolean = false) => {
@@ -41,7 +39,7 @@ export default function ArtistHeader(): React.JSX.Element {
if (allTracks.length === 0) return
- loadNewQueue({
+ await loadNewQueue({
track: allTracks[0],
index: 0,
tracklist: allTracks,
diff --git a/src/components/Context/index.tsx b/src/components/Context/index.tsx
index 3f3cd463..fad7aeeb 100644
--- a/src/components/Context/index.tsx
+++ b/src/components/Context/index.tsx
@@ -3,7 +3,7 @@ import {
BaseItemKind,
MediaSourceInfo,
} from '@jellyfin/sdk/lib/generated-client/models'
-import { ListItem, Spinner, View, YGroup } from 'tamagui'
+import { ListItem, View, YGroup } from 'tamagui'
import { BaseStackParamList, RootStackParamList } from '../../screens/types'
import { Text } from '../Global/helpers/text'
import FavoriteContextMenuRow from '../Global/components/favorite-context-menu-row'
@@ -23,9 +23,7 @@ import ItemImage from '../Global/components/image'
import { StackActions } from '@react-navigation/native'
import TextTicker from 'react-native-text-ticker'
import { TextTickerConfig } from '../Player/component.config'
-import { useAddToQueue } from '../../hooks/player/callbacks'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
-import { Platform } from 'react-native'
import { useApi } from '../../stores'
import DeletePlaylistRow from './components/delete-playlist-row'
import RemoveFromPlaylistRow from './components/remove-from-playlist-row'
@@ -34,6 +32,7 @@ import { useIsDownloaded } from '../../hooks/downloads'
import { useDownloadProgress } from 'react-native-nitro-player'
import CircularProgressIndicator from '../Global/components/circular-progress-indicator'
import { useArtist } from '../../api/queries/artist'
+import { addToQueue } from '../../hooks/player/functions/queue'
type StackNavigation = Pick, 'navigate' | 'dispatch'>
@@ -188,8 +187,6 @@ function AddToPlaylistRow({
}
function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Element {
- const addToQueue = useAddToQueue()
-
const mutation: AddToQueueMutation = {
tracks,
}
diff --git a/src/components/Global/components/Track/index.tsx b/src/components/Global/components/Track/index.tsx
index 2c687f4c..3ebc91f8 100644
--- a/src/components/Global/components/Track/index.tsx
+++ b/src/components/Global/components/Track/index.tsx
@@ -9,7 +9,6 @@ import { useNetworkStatus } from '../../../../stores/network'
import navigationRef from '../../../../screens/navigation'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../../../screens/types'
-import { useAddToQueue, useLoadNewQueue } from '../../../../hooks/player/callbacks'
import SwipeableRow from '../SwipeableRow'
import { useSwipeSettingsStore } from '../../../../stores/settings/swipe'
import { buildSwipeConfig } from '../../helpers/swipe-actions'
@@ -20,6 +19,7 @@ import { StackActions } from '@react-navigation/native'
import { useHideRunTimesSetting } from '../../../../stores/settings/app'
import TrackRowContent from './content'
import { useIsDownloaded } from '../../../../hooks/downloads'
+import { addToQueue, loadNewQueue } from '../../../../hooks/player/functions/queue'
export interface TrackProps {
track: BaseItemDto
@@ -63,8 +63,6 @@ export default function Track({
const [hideRunTimes] = useHideRunTimesSetting()
const currentTrackId = useCurrentTrackId()
- const loadNewQueue = useLoadNewQueue()
- const addToQueue = useAddToQueue()
const [networkStatus] = useNetworkStatus()
const isDownloaded = useIsDownloaded([track.Id!])
diff --git a/src/components/Global/components/item-row.tsx b/src/components/Global/components/item-row.tsx
index aaa90743..b0d99e9f 100644
--- a/src/components/Global/components/item-row.tsx
+++ b/src/components/Global/components/item-row.tsx
@@ -9,7 +9,6 @@ import FavoriteIcon from './favorite-icon'
import navigationRef from '../../../screens/navigation'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../../screens/types'
-import { useAddToQueue, useLoadNewQueue } from '../../../hooks/player/callbacks'
import useItemContext from '../../../hooks/use-item-context'
import { RouteProp, useRoute } from '@react-navigation/native'
import React from 'react'
@@ -30,6 +29,7 @@ import { useAddFavorite, useRemoveFavorite } from '../../../api/mutations/favori
import { useHideRunTimesSetting } from '../../../stores/settings/app'
import { Queue } from '../../../services/types/queue-item'
import { formatArtistName } from '../../../utils/formatting/artist-names'
+import { addToQueue, loadNewQueue } from '../../../hooks/player/functions/queue'
interface ItemRowProps {
item: BaseItemDto
@@ -63,8 +63,6 @@ function ItemRow({
}: ItemRowProps): React.JSX.Element {
const artworkAreaWidth = useSharedValue(0)
- const loadNewQueue = useLoadNewQueue()
- const addToQueue = useAddToQueue()
const { mutate: addFavorite } = useAddFavorite()
const { mutate: removeFavorite } = useRemoveFavorite()
const [hideRunTimes] = useHideRunTimesSetting()
diff --git a/src/components/Home/helpers/frequent-tracks.tsx b/src/components/Home/helpers/frequent-tracks.tsx
index 870fc6a5..4f94ca3b 100644
--- a/src/components/Home/helpers/frequent-tracks.tsx
+++ b/src/components/Home/helpers/frequent-tracks.tsx
@@ -2,15 +2,14 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { H5, XStack } from 'tamagui'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import ItemCard from '../../../components/Global/components/item-card'
-import { QueuingType } from '../../../enums/queuing-type'
import Icon from '../../Global/components/icon'
-import { useLoadNewQueue } from '../../../hooks/player/callbacks'
import { useDisplayContext } from '../../../providers/Display/display-provider'
import HomeStackParamList from '../../../screens/Home/types'
import { useNavigation } from '@react-navigation/native'
import { RootStackParamList } from '../../../screens/types'
import { useFrequentlyPlayedTracks } from '../../../api/queries/frequents'
import AnimatedRow from '../../Global/helpers/animated-row'
+import { loadNewQueue } from '../../../hooks/player/functions/queue'
export default function FrequentlyPlayedTracks(): React.JSX.Element {
const tracksInfiniteQuery = useFrequentlyPlayedTracks()
@@ -19,7 +18,6 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
const rootNavigation = useNavigation>()
- const loadNewQueue = useLoadNewQueue()
const { horizontalItems } = useDisplayContext()
return tracksInfiniteQuery.data ? (
@@ -47,8 +45,8 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
caption={track.Name}
subCaption={`${track.Artists?.join(', ')}`}
squared
- onPress={() => {
- loadNewQueue({
+ onPress={async () => {
+ await loadNewQueue({
track,
index,
tracklist: tracksInfiniteQuery.data ?? [track],
diff --git a/src/components/Home/helpers/recently-played.tsx b/src/components/Home/helpers/recently-played.tsx
index ba321d49..52d35a8d 100644
--- a/src/components/Home/helpers/recently-played.tsx
+++ b/src/components/Home/helpers/recently-played.tsx
@@ -3,23 +3,20 @@ import { H5, XStack } from 'tamagui'
import ItemCard from '../../Global/components/item-card'
import { RootStackParamList } from '../../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
-import { QueuingType } from '../../../enums/queuing-type'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import Icon from '../../Global/components/icon'
-import { useLoadNewQueue } from '../../../hooks/player/callbacks'
import { useDisplayContext } from '../../../providers/Display/display-provider'
import { useNavigation } from '@react-navigation/native'
import HomeStackParamList from '../../../screens/Home/types'
import { useRecentlyPlayedTracks } from '../../../api/queries/recents'
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client'
import AnimatedRow from '../../Global/helpers/animated-row'
+import { loadNewQueue } from '../../../hooks/player/functions/queue'
export default function RecentlyPlayed(): React.JSX.Element {
const navigation = useNavigation>()
const rootNavigation = useNavigation>()
- const loadNewQueue = useLoadNewQueue()
-
const tracksInfiniteQuery = useRecentlyPlayedTracks()
const { horizontalItems } = useDisplayContext()
diff --git a/src/components/Player/components/controls.tsx b/src/components/Player/components/controls.tsx
index f6b567d5..ae4b70f6 100644
--- a/src/components/Player/components/controls.tsx
+++ b/src/components/Player/components/controls.tsx
@@ -2,30 +2,20 @@ import React from 'react'
import { Spacer, XStack, getToken } from 'tamagui'
import PlayPauseButton from './buttons'
import Icon from '../../Global/components/icon'
-import {
- usePrevious,
- useSkip,
- useToggleRepeatMode,
- useToggleShuffle,
-} from '../../../hooks/player/callbacks'
import { useRepeatMode, useShuffle } from '../../../stores/player/queue'
-import { RepeatMode } from '@jellyfin/sdk/lib/generated-client/models/repeat-mode'
+import { toggleRepeatMode } from '../../../hooks/player/functions/repeat-mode'
+import { toggleShuffle } from '../../../hooks/player/functions/shuffle'
+import { previous, skip } from '../../../hooks/player/functions/controls'
export default function Controls({
onLyricsScreen,
}: {
onLyricsScreen?: boolean
}): React.JSX.Element {
- const previous = usePrevious()
- const skip = useSkip()
const repeatMode = useRepeatMode()
- const toggleRepeatMode = useToggleRepeatMode()
-
const shuffled = useShuffle()
- const toggleShuffle = useToggleShuffle()
-
return (
{!onLyricsScreen && (
@@ -33,7 +23,7 @@ export default function Controls({
small
color={shuffled ? '$primary' : '$color'}
name='shuffle'
- onPress={() => toggleShuffle(shuffled)}
+ onPress={async () => await toggleShuffle(shuffled)}
/>
)}
@@ -42,7 +32,7 @@ export default function Controls({
await previous()}
large
testID='previous-button-test-id'
/>
@@ -53,7 +43,7 @@ export default function Controls({
skip(undefined)}
+ onPress={async () => await skip(undefined)}
large
testID='skip-button-test-id'
/>
@@ -65,7 +55,7 @@ export default function Controls({
small
color={repeatMode === 'off' ? '$color' : '$primary'}
name={repeatMode === 'track' ? 'repeat-once' : 'repeat'}
- onPress={async () => toggleRepeatMode()}
+ onPress={toggleRepeatMode}
/>
)}
diff --git a/src/components/Player/components/song-info.tsx b/src/components/Player/components/song-info.tsx
index da3ca434..4ab997c7 100644
--- a/src/components/Player/components/song-info.tsx
+++ b/src/components/Player/components/song-info.tsx
@@ -9,23 +9,11 @@ import { QueryKeys } from '../../../enums/query-keys'
import navigationRef from '../../../screens/navigation'
import Icon from '../../Global/components/icon'
import { CommonActions } from '@react-navigation/native'
-import { Gesture } from 'react-native-gesture-handler'
-import Animated, {
- Easing,
- FadeIn,
- FadeOut,
- LinearTransition,
- useSharedValue,
- withDelay,
- withSpring,
-} from 'react-native-reanimated'
+import Animated, { Easing, FadeIn, FadeOut } from 'react-native-reanimated'
import type { SharedValue } from 'react-native-reanimated'
-import { runOnJS } from 'react-native-worklets'
-import { usePrevious, useSkip } from '../../../hooks/player/callbacks'
import { useCurrentTrack } from '../../../stores/player/queue'
import { useApi } from '../../../stores'
import { isExplicit } from '../../../utils/trackDetails'
-import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
import { MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client'
import getTrackDto from '../../../utils/mapping/track-extra-payload'
import { ICON_PRESS_STYLES } from '../../../configs/style.config'
@@ -35,43 +23,8 @@ type SongInfoProps = {
swipeX?: SharedValue
}
-export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Element {
+export default function SongInfo(): React.JSX.Element {
const api = useApi()
- const skip = useSkip()
- const previous = usePrevious()
- // local fallback if no shared value was provided
- const localX = useSharedValue(0)
- const x = swipeX ?? localX
-
- const albumGesture = Gesture.Pan()
- .activeOffsetX([-12, 12])
- .onUpdate((e) => {
- if (Math.abs(e.translationY) < 40) {
- x.value = Math.max(-160, Math.min(160, e.translationX))
- }
- })
- .onEnd((e) => {
- const threshold = 120
- const minVelocity = 600
- const isHorizontal = Math.abs(e.translationY) < 40
- if (
- isHorizontal &&
- (Math.abs(e.translationX) > threshold || Math.abs(e.velocityX) > minVelocity)
- ) {
- if (e.translationX > 0) {
- x.value = withSpring(220)
- runOnJS(triggerHaptic)('notificationSuccess')
- runOnJS(skip)(undefined)
- } else {
- x.value = withSpring(-220)
- runOnJS(triggerHaptic)('notificationSuccess')
- runOnJS(previous)()
- }
- x.value = withDelay(160, withSpring(0))
- } else {
- x.value = withSpring(0)
- }
- })
const currentTrack = useCurrentTrack()
diff --git a/src/components/Player/index.tsx b/src/components/Player/index.tsx
index 3beb7b62..a970071e 100644
--- a/src/components/Player/index.tsx
+++ b/src/components/Player/index.tsx
@@ -11,15 +11,13 @@ import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
import { useSharedValue, withDelay, withSpring } from 'react-native-reanimated'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { runOnJS } from 'react-native-worklets'
-import { usePrevious, useSkip } from '../../hooks/player/callbacks'
import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import { useCurrentTrack } from '../../stores/player/queue'
+import { previous, skip } from '../../hooks/player/functions/controls'
export default function PlayerScreen(): React.JSX.Element {
usePerformanceMonitor('PlayerScreen', 5)
- const skip = useSkip()
- const previous = usePrevious()
const nowPlaying = useCurrentTrack()
const { width, height } = useWindowDimensions()
diff --git a/src/components/Player/mini-player.tsx b/src/components/Player/mini-player.tsx
index 1df01d29..51aee18e 100644
--- a/src/components/Player/mini-player.tsx
+++ b/src/components/Player/mini-player.tsx
@@ -11,15 +11,12 @@ import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
Easing,
FadeIn,
- FadeInDown,
FadeOut,
- FadeOutDown,
useSharedValue,
useAnimatedStyle,
withTiming,
useAnimatedReaction,
ReduceMotion,
- SlideInUp,
SlideInDown,
SlideOutDown,
interpolate,
@@ -28,17 +25,15 @@ import { runOnJS } from 'react-native-worklets'
import { RootStackParamList } from '../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import ItemImage from '../Global/components/image'
-import { usePrevious, useSkip } from '../../hooks/player/callbacks'
import { useCurrentTrack } from '../../stores/player/queue'
import getTrackDto from '../../utils/mapping/track-extra-payload'
import { ICON_PRESS_STYLES } from '../../configs/style.config'
+import { previous, skip } from '../../hooks/player/functions/controls'
export default function Miniplayer(): React.JSX.Element | null {
const nowPlaying = useCurrentTrack()
const item = getTrackDto(nowPlaying)
- const skip = useSkip()
- const previous = usePrevious()
const theme = useTheme()
const navigation = useNavigation>()
diff --git a/src/components/Playlist/components/header.tsx b/src/components/Playlist/components/header.tsx
index b42140e2..17b4f3ec 100644
--- a/src/components/Playlist/components/header.tsx
+++ b/src/components/Playlist/components/header.tsx
@@ -3,14 +3,9 @@ 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 { useNetworkStatus } from '../../../stores/network'
-import { QueuingType } from '../../../enums/queuing-type'
import { useNavigation } from '@react-navigation/native'
import LibraryStackParamList from '@/src/screens/Library/types'
-import { useLoadNewQueue } from '../../../hooks/player/callbacks'
-import useStreamingDeviceProfile from '../../../stores/device-profile'
import ItemImage from '../../Global/components/image'
-import { useApi } from '../../../stores'
import Input from '../../Global/helpers/input'
import Animated, { Easing, FadeInDown, FadeOutDown } from 'react-native-reanimated'
import { Dispatch, SetStateAction } from 'react'
@@ -18,6 +13,7 @@ import Button from '../../Global/helpers/button'
import { Text } from '../../Global/helpers/text'
import { RunTimeTicks } from '../../Global/helpers/time-codes'
import { BUTTON_PRESS_STYLES } from '../../../configs/style.config'
+import { loadNewQueue } from '../../../hooks/player/functions/queue'
export default function PlaylistTracklistHeader({
playlist,
@@ -97,12 +93,6 @@ function PlaylistHeaderControls({
playlist: BaseItemDto
playlistTracks: BaseItemDto[]
}): React.JSX.Element {
- const streamingDeviceProfile = useStreamingDeviceProfile()
- const loadNewQueue = useLoadNewQueue()
- const api = useApi()
-
- const [networkStatus] = useNetworkStatus()
-
const navigation = useNavigation>()
const playPlaylist = async (shuffled: boolean = false) => {
diff --git a/src/components/Playlist/index.tsx b/src/components/Playlist/index.tsx
index 907a811e..be5d201f 100644
--- a/src/components/Playlist/index.tsx
+++ b/src/components/Playlist/index.tsx
@@ -11,8 +11,6 @@ import { RenderItemInfo } from 'react-native-sortables/dist/typescript/types'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import PlaylistTracklistHeader from './components/header'
import navigationRef from '../../screens/navigation'
-import { useLoadNewQueue } from '../../hooks/player/callbacks'
-import { QueuingType } from '../../enums/queuing-type'
import { useApi } from '../../stores'
import useStreamingDeviceProfile from '../../stores/device-profile'
import { useEffect, useLayoutEffect, useState } from 'react'
@@ -36,6 +34,7 @@ import { queryClient } from '../../constants/query-client'
import { PlaylistTracksQueryKey } from '../../api/queries/playlist/keys'
import { useIsDownloaded } from '../../hooks/downloads'
import { useDeleteDownloads } from '../../hooks/downloads/mutations'
+import { loadNewQueue } from '../../hooks/player/functions/queue'
export default function Playlist({
playlist,
@@ -145,8 +144,6 @@ export default function Playlist({
if (!editing) refetch()
}, [editing])
- const loadNewQueue = useLoadNewQueue()
-
const isDownloaded = useIsDownloaded(playlistTracks?.map(({ Id }) => Id) ?? [])
const playlistDownloadPending = useIsDownloading(playlistTracks ?? [])
diff --git a/src/components/Queue/index.tsx b/src/components/Queue/index.tsx
index 143377bb..a58fb266 100644
--- a/src/components/Queue/index.tsx
+++ b/src/components/Queue/index.tsx
@@ -4,7 +4,6 @@ import { RootStackParamList } from '../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { Text, XStack } from 'tamagui'
import { useLayoutEffect, useRef } from 'react'
-import { useRemoveFromQueue, useReorderQueue, useSkip } from '../../hooks/player/callbacks'
import { useCurrentIndex, usePlayQueue, useQueueRef } from '../../stores/player/queue'
import Sortable from 'react-native-sortables'
import { OrderChangeParams, RenderItemInfo } from 'react-native-sortables/dist/typescript/types'
@@ -13,6 +12,8 @@ import Animated, { useAnimatedRef } from 'react-native-reanimated'
import { TrackItem } from 'react-native-nitro-player'
import getTrackDto from '../../utils/mapping/track-extra-payload'
import { View } from 'react-native'
+import { skip } from '../../hooks/player/functions/controls'
+import { removeItemFromQueue, reorderQueue } from '../../hooks/player/functions/queue'
export default function Queue({
navigation,
@@ -24,9 +25,6 @@ export default function Queue({
const currentIndex = useCurrentIndex()
const queueRef = useQueueRef()
- const removeFromQueue = useRemoveFromQueue()
- const reorderQueue = useReorderQueue()
- const skip = useSkip()
const scrollableRef = useAnimatedRef()
@@ -81,7 +79,7 @@ export default function Queue({
{
- await removeFromQueue(index)
+ await removeItemFromQueue(index)
}}
>
diff --git a/src/hooks/player/callbacks.ts b/src/hooks/player/callbacks.ts
index dad55411..d9692e01 100644
--- a/src/hooks/player/callbacks.ts
+++ b/src/hooks/player/callbacks.ts
@@ -1,27 +1,13 @@
-import {
- loadQueue,
- playLaterInQueue,
- playNextInQueue,
- removeItemFromQueue,
-} from './functions/queue'
+import { addToQueue, loadNewQueue, removeItemFromQueue } from './functions/queue'
import { previous, skip } from './functions/controls'
-import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces'
-import { QueuingType } from '../../enums/queuing-type'
-import Toast from 'react-native-toast-message'
+import { QueueOrderMutation } from './interfaces'
import { handleDeshuffle, handleShuffle } from './functions/shuffle'
import usePlayerEngineStore, { PlayerEngine } from '../../stores/player/engine'
import { useRemoteMediaClient } from 'react-native-google-cast'
import { triggerHaptic } from '../use-haptic-feedback'
import { usePlayerQueueStore } from '../../stores/player/queue'
-import {
- PlayerQueue,
- RepeatMode,
- TrackItem,
- TrackPlayer,
- TrackPlayerState,
-} from 'react-native-nitro-player'
-import reportPlaybackStarted from '../../api/mutations/playback/functions/playback-started'
-import { updateTrackMediaInfo } from '../../providers/Player/utils/event-handlers'
+import { PlayerQueue, TrackItem, TrackPlayer, TrackPlayerState } from 'react-native-nitro-player'
+import { toggleRepeatMode } from './functions/repeat-mode'
/**
* A mutation to handle toggling the playback state
@@ -48,27 +34,12 @@ export const useTogglePlayback = () => {
}
}
+/**
+ * @deprecated Let's just use the function this returns directly instead
+ * of subscribing to a hook
+ */
export const useToggleRepeatMode = () => {
- return () => {
- const currentMode = usePlayerQueueStore.getState().repeatMode
- triggerHaptic('impactLight')
-
- let nextMode: RepeatMode
-
- switch (currentMode) {
- case 'off':
- nextMode = 'Playlist'
- break
- case 'Playlist':
- nextMode = 'track'
- break
- default:
- nextMode = 'off'
- }
-
- TrackPlayer.setRepeatMode(nextMode)
- usePlayerQueueStore.getState().setRepeatMode(nextMode)
- }
+ return toggleRepeatMode
}
/**
@@ -104,109 +75,6 @@ const useSeekBy = () => {
}
}
-export const useAddToQueue = () => {
- return async (variables: AddToQueueMutation) => {
- try {
- if (variables.queuingType === QueuingType.PlayNext)
- await playNextInQueue({ ...variables })
- else await playLaterInQueue({ ...variables })
-
- triggerHaptic('notificationSuccess')
- Toast.show({
- text1:
- variables.queuingType === QueuingType.PlayNext
- ? 'Playing next'
- : 'Added to queue',
- type: 'success',
- })
- } catch (error) {
- triggerHaptic('notificationError')
- console.error(
- `Failed to ${variables.queuingType === QueuingType.PlayNext ? 'play next' : 'add to queue'}`,
- error,
- )
- Toast.show({
- text1:
- variables.queuingType === QueuingType.PlayNext
- ? 'Failed to play next'
- : 'Failed to add to queue',
- type: 'error',
- })
- } finally {
- const queue = await TrackPlayer.getActualQueue()
-
- usePlayerQueueStore.getState().setQueue(queue)
- }
- }
-}
-
-export const useLoadNewQueue = () => {
- return async (variables: QueueMutation) => {
- triggerHaptic('impactLight')
- usePlayerQueueStore.getState().setIsQueuing(true)
- const { tracks, finalStartIndex } = await loadQueue({ ...variables })
-
- // skipToIndex is now settled. Drive a single, authoritative URL-resolution
- // pass while isQueuing=true so any concurrent native callbacks are still
- // silenced. resolveTrackUrls bypasses the isQueuing guard intentionally.
- const tracksNeedingUrls = await TrackPlayer.getTracksNeedingUrls()
- if (tracksNeedingUrls.length > 0) {
- await updateTrackMediaInfo(tracksNeedingUrls)
- }
-
- usePlayerQueueStore.getState().setIsQueuing(false)
-
- if (variables.startPlayback) {
- TrackPlayer.play()
- reportPlaybackStarted(tracks[finalStartIndex], 0)
- }
- }
-}
-
-export const usePrevious = () => {
- return async () => {
- triggerHaptic('impactMedium')
-
- await previous()
- }
-}
-
-export const useSkip = () => {
- return async (index?: number | undefined) => {
- triggerHaptic('impactMedium')
-
- await skip(index)
- }
-}
-
-export const useRemoveFromQueue = () => {
- return async (index: number) => {
- await removeItemFromQueue(index)
- }
-}
-
-export const useReorderQueue = () => {
- return async ({ fromIndex, toIndex }: QueueOrderMutation) => {
- const playlistId = PlayerQueue.getCurrentPlaylistId()
-
- if (!playlistId) return
-
- const { tracks } = PlayerQueue.getPlaylist(playlistId)!
-
- PlayerQueue.reorderTrackInPlaylist(playlistId, tracks[fromIndex].id, toIndex)
-
- const { currentIndex } = await TrackPlayer.getState()
-
- const queue = await TrackPlayer.getActualQueue()
-
- usePlayerQueueStore.setState((state) => ({
- ...state,
- queue,
- currentIndex,
- }))
- }
-}
-
export const useResetQueue = () => () => {
usePlayerQueueStore.getState().setUnshuffledQueue([])
usePlayerQueueStore.getState().setShuffled(false)
@@ -214,21 +82,3 @@ export const useResetQueue = () => () => {
usePlayerQueueStore.getState().setQueue([])
usePlayerQueueStore.getState().setCurrentIndex(undefined)
}
-
-export const useToggleShuffle = () => {
- return async (shuffled: boolean) => {
- triggerHaptic('impactMedium')
-
- let result: { currentIndex: number; queue: TrackItem[] } | undefined
-
- if (shuffled) result = await handleDeshuffle()
- else result = await handleShuffle()
-
- usePlayerQueueStore.setState((state) => ({
- ...state,
- queue: result.queue,
- currentIndex: result.currentIndex,
- shuffled: !shuffled,
- }))
- }
-}
diff --git a/src/hooks/player/functions/controls.ts b/src/hooks/player/functions/controls.ts
index da5fc038..48687cd1 100644
--- a/src/hooks/player/functions/controls.ts
+++ b/src/hooks/player/functions/controls.ts
@@ -1,6 +1,7 @@
import { SKIP_TO_PREVIOUS_THRESHOLD } from '../../../configs/player.config'
import { isUndefined } from 'lodash'
import { TrackPlayer } from 'react-native-nitro-player'
+import { triggerHaptic } from '../../use-haptic-feedback'
/**
* A function that will skip to the previous track if
@@ -15,11 +16,13 @@ import { TrackPlayer } from 'react-native-nitro-player'
* Does not resume playback if the player was paused
*/
export async function previous(): Promise {
+ triggerHaptic('impactMedium')
+
const { currentState, currentIndex, currentPosition } = await TrackPlayer.getState()
if (isUndefined(currentIndex)) return
- if (Math.floor(currentPosition) < SKIP_TO_PREVIOUS_THRESHOLD) {
+ if (Math.floor(currentPosition) <= SKIP_TO_PREVIOUS_THRESHOLD) {
TrackPlayer.skipToPrevious()
} else {
TrackPlayer.seek(0)
@@ -39,6 +42,8 @@ export async function previous(): Promise {
* @param index The track index to skip to, to skip multiple tracks
*/
export async function skip(index: number | undefined): Promise {
+ triggerHaptic('impactMedium')
+
const { currentIndex } = await TrackPlayer.getState()
if (!isUndefined(index)) {
diff --git a/src/hooks/player/functions/queue.ts b/src/hooks/player/functions/queue.ts
index 4072fd92..09d2c890 100644
--- a/src/hooks/player/functions/queue.ts
+++ b/src/hooks/player/functions/queue.ts
@@ -1,7 +1,7 @@
import { mapDtoToTrack } from '../../../utils/mapping/item-to-track'
import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher'
import { clearPlaylists, filterTracksOnNetworkStatus } from './utils/queue'
-import { AddToQueueMutation, QueueMutation } from '../interfaces'
+import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from '../interfaces'
import { shuffleJellifyTracks } from './utils/shuffle'
import { setNewQueue, usePlayerQueueStore } from '../../../stores/player/queue'
@@ -10,12 +10,37 @@ import { useNetworkStore } from '../../../stores/network'
import { DownloadManager, PlayerQueue, TrackItem, TrackPlayer } from 'react-native-nitro-player'
import uuid from 'react-native-uuid'
import { triggerHaptic } from '../../use-haptic-feedback'
+import Toast from 'react-native-toast-message'
+import { QueuingType } from '../../../enums/queuing-type'
+import { updateTrackMediaInfo } from '../../../providers/Player/utils/event-handlers'
+import reportPlaybackStarted from '../../../api/mutations/playback/functions/playback-started'
type LoadQueueResult = {
finalStartIndex: number
tracks: TrackItem[]
}
+export const loadNewQueue = async (variables: QueueMutation) => {
+ triggerHaptic('impactLight')
+ usePlayerQueueStore.getState().setIsQueuing(true)
+ const { tracks, finalStartIndex } = await loadQueue({ ...variables })
+
+ // skipToIndex is now settled. Drive a single, authoritative URL-resolution
+ // pass while isQueuing=true so any concurrent native callbacks are still
+ // silenced. resolveTrackUrls bypasses the isQueuing guard intentionally.
+ const tracksNeedingUrls = await TrackPlayer.getTracksNeedingUrls()
+ if (tracksNeedingUrls.length > 0) {
+ await updateTrackMediaInfo(tracksNeedingUrls)
+ }
+
+ usePlayerQueueStore.getState().setIsQueuing(false)
+
+ if (variables.startPlayback) {
+ TrackPlayer.play()
+ reportPlaybackStarted(tracks[finalStartIndex], 0)
+ }
+}
+
export async function loadQueue({
index = 0,
tracklist,
@@ -120,6 +145,37 @@ export const playLaterInQueue = async ({ tracks }: AddToQueueMutation) => {
.setUnshuffledQueue([...usePlayerQueueStore.getState().unShuffledQueue, ...newTracks])
}
+export const addToQueue = async (variables: AddToQueueMutation) => {
+ try {
+ if (variables.queuingType === QueuingType.PlayNext) await playNextInQueue({ ...variables })
+ else await playLaterInQueue({ ...variables })
+
+ triggerHaptic('notificationSuccess')
+ Toast.show({
+ text1:
+ variables.queuingType === QueuingType.PlayNext ? 'Playing next' : 'Added to queue',
+ type: 'success',
+ })
+ } catch (error) {
+ triggerHaptic('notificationError')
+ console.error(
+ `Failed to ${variables.queuingType === QueuingType.PlayNext ? 'play next' : 'add to queue'}`,
+ error,
+ )
+ Toast.show({
+ text1:
+ variables.queuingType === QueuingType.PlayNext
+ ? 'Failed to play next'
+ : 'Failed to add to queue',
+ type: 'error',
+ })
+ } finally {
+ const queue = await TrackPlayer.getActualQueue()
+
+ usePlayerQueueStore.getState().setQueue(queue)
+ }
+}
+
export const removeItemFromQueue = async (index: number) => {
triggerHaptic('impactMedium')
@@ -171,3 +227,23 @@ export const removeItemFromQueue = async (index: number) => {
currentIndex: newCurrentIndex,
}))
}
+
+export const reorderQueue = async ({ fromIndex, toIndex }: QueueOrderMutation) => {
+ const playlistId = PlayerQueue.getCurrentPlaylistId()
+
+ if (!playlistId) return
+
+ const { tracks } = PlayerQueue.getPlaylist(playlistId)!
+
+ PlayerQueue.reorderTrackInPlaylist(playlistId, tracks[fromIndex].id, toIndex)
+
+ const { currentIndex } = await TrackPlayer.getState()
+
+ const queue = await TrackPlayer.getActualQueue()
+
+ usePlayerQueueStore.setState((state) => ({
+ ...state,
+ queue,
+ currentIndex,
+ }))
+}
diff --git a/src/hooks/player/functions/repeat-mode.ts b/src/hooks/player/functions/repeat-mode.ts
new file mode 100644
index 00000000..02a3dd21
--- /dev/null
+++ b/src/hooks/player/functions/repeat-mode.ts
@@ -0,0 +1,24 @@
+import { usePlayerQueueStore } from '../../../stores/player/queue'
+import { triggerHaptic } from '../../use-haptic-feedback'
+import { RepeatMode, TrackPlayer } from 'react-native-nitro-player'
+
+export const toggleRepeatMode = () => {
+ const currentMode = usePlayerQueueStore.getState().repeatMode
+ triggerHaptic('impactLight')
+
+ let nextMode: RepeatMode
+
+ switch (currentMode) {
+ case 'off':
+ nextMode = 'Playlist'
+ break
+ case 'Playlist':
+ nextMode = 'track'
+ break
+ default:
+ nextMode = 'off'
+ }
+
+ TrackPlayer.setRepeatMode(nextMode)
+ usePlayerQueueStore.getState().setRepeatMode(nextMode)
+}
diff --git a/src/hooks/player/functions/shuffle.ts b/src/hooks/player/functions/shuffle.ts
index eaf03758..0eb38a5b 100644
--- a/src/hooks/player/functions/shuffle.ts
+++ b/src/hooks/player/functions/shuffle.ts
@@ -19,6 +19,23 @@ import { ApiLimits } from '../../../configs/query.config'
import { mapDtoToTrack } from '../../../utils/mapping/item-to-track'
import getTrackDto from '../../../utils/mapping/track-extra-payload'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
+import { triggerHaptic } from '../../use-haptic-feedback'
+
+export const toggleShuffle = async (shuffled: boolean) => {
+ triggerHaptic('impactMedium')
+
+ let result: { currentIndex: number; queue: TrackItem[] } | undefined
+
+ if (shuffled) result = await handleDeshuffle()
+ else result = await handleShuffle()
+
+ usePlayerQueueStore.setState((state) => ({
+ ...state,
+ queue: result.queue,
+ currentIndex: result.currentIndex,
+ shuffled: !shuffled,
+ }))
+}
export async function handleShuffle(
keepCurrentTrack: boolean = true,
@@ -259,25 +276,21 @@ export async function handleShuffle(
// Save off unshuffledQueue
usePlayerQueueStore.getState().setUnshuffledQueue([...playQueue])
- // Tracks that come AFTER the currently playing track — these are what we shuffle.
- const tracksAfterCurrent = playQueue.filter((_, index) => index > currentIndex)
+ // Only shuffle tracks AFTER the current position. Tracks before it have already
+ // played and must stay in the native playlist prefix so ExoPlayer's window index
+ // keeps pointing at the right item — removing them would cause the next skip to
+ // jump to the wrong track.
+ const tracksAfterCurrent = playQueue.filter((_, i) => i > currentIndex)
const { shuffled: newShuffledQueue } = shuffleJellifyTracks(tracksAfterCurrent)
if (keepCurrentTrack) {
- // KEY: only touch tracks that are AFTER the current track.
- //
- // Android's ExoPlayer rebuilds via setMediaItems(list, resetPosition=false), which
- // preserves the current window INDEX — not the track identity. If we remove tracks
- // before the current track, the playlist shrinks and ExoPlayer's saved index can
- // become out-of-bounds, causing it to jump to the last item.
- //
- // By leaving all tracks before (and including) currentIndex in place, ExoPlayer's
- // current window index still points at the right track after the rebuild.
tracksAfterCurrent.forEach((track) =>
PlayerQueue.removeTrackFromPlaylist(playlistId!, track.id),
)
- // Insert the shuffled upcoming tracks right after currentIndex.
+ // Insert the shuffled upcoming tracks right after the current track.
+ // Must use currentIndex + 1 (not a hardcoded 1) so the insert position is
+ // correct regardless of how many tracks precede the current one.
PlayerQueue.addTracksToPlaylist(playlistId!, newShuffledQueue, currentIndex + 1)
// Present a clean queue to the JS store (current track first, then shuffled upcoming).
diff --git a/src/hooks/use-item-context.ts b/src/hooks/use-item-context.ts
index 67674094..5f040687 100644
--- a/src/hooks/use-item-context.ts
+++ b/src/hooks/use-item-context.ts
@@ -6,24 +6,26 @@ import { QueryKeys } from '../enums/query-keys'
import { fetchAlbumDiscs, fetchItem } from '../api/queries/item'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import fetchUserData from '../api/queries/user-data/utils'
-import { useRef } from 'react'
import UserDataQueryKey from '../api/queries/user-data/keys'
import { getApi, getUser } from '../stores'
+import { ArtistQueryKey } from '../api/queries/artist/keys'
+
+// Module-level dedup guard — no hook needed, this is just a long-lived Set
+const prefetchedContext = new Set()
export default function useItemContext(): (item: BaseItemDto) => void {
- const api = getApi()
- const user = getUser()
-
- const prefetchedContext = useRef>(new Set())
-
return (item: BaseItemDto) => {
const effectSig = `${item.Id}-${item.Type}`
// If we've already warmed the cache for this item, return
- if (prefetchedContext.current.has(effectSig)) return
+ if (prefetchedContext.has(effectSig)) return
// Mark this item's context as warmed, preventing reruns
- prefetchedContext.current.add(effectSig)
+ prefetchedContext.add(effectSig)
+
+ // Read api/user inside the callback so they're only resolved when actually needed
+ const api = getApi()
+ const user = getUser()
warmItemContext(api, user, item)
}
@@ -41,8 +43,7 @@ function warmItemContext(
if (Type === BaseItemKind.Audio) warmTrackContext(api, item)
- if (Type === BaseItemKind.MusicArtist)
- queryClient.setQueryData([QueryKeys.ArtistById, Id], item)
+ if (Type === BaseItemKind.MusicArtist) queryClient.setQueryData(ArtistQueryKey(Id), item)
if (Type === BaseItemKind.MusicAlbum) warmAlbumContext(api, item)
@@ -64,13 +65,11 @@ function warmItemContext(
staleTime: ONE_HOUR,
})
- if (queryClient.getQueryState(UserDataQueryKey(user, item.Id!))?.status !== 'success') {
- queryClient.ensureQueryData({
- queryKey: UserDataQueryKey(user, item.Id!),
- queryFn: () => fetchUserData(Id),
- staleTime: ONE_MINUTE * 15,
- })
- }
+ queryClient.ensureQueryData({
+ queryKey: UserDataQueryKey(user, item.Id!),
+ queryFn: () => fetchUserData(Id),
+ staleTime: ONE_MINUTE * 15,
+ })
}
function warmAlbumContext(api: Api | undefined, album: BaseItemDto): void {
@@ -92,17 +91,10 @@ function warmArtistContext(api: Api | undefined, artistId: string): void {
// Fail fast if we don't have an artist ID to work with
if (!artistId) return
- const queryKey = [QueryKeys.ArtistById, artistId]
-
- // Bail out if we have data
- if (queryClient.getQueryState(queryKey)?.status === 'success') return
-
- /**
- * Store queryable of artist item
- */
+ // ensureQueryData respects staleTime internally — no need to check getQueryState first
queryClient.ensureQueryData({
- queryKey,
- queryFn: () => fetchItem(api, artistId!),
+ queryKey: ArtistQueryKey(artistId),
+ queryFn: () => fetchItem(api, artistId),
staleTime: ONE_DAY,
})
}
@@ -110,12 +102,10 @@ function warmArtistContext(api: Api | undefined, artistId: string): void {
function warmTrackContext(api: Api | undefined, track: BaseItemDto): void {
const { AlbumId, ArtistItems } = track
- const albumQueryKey = [QueryKeys.Album, AlbumId]
-
- if (AlbumId && queryClient.getQueryState(albumQueryKey)?.status !== 'success')
+ if (AlbumId)
queryClient.ensureQueryData({
- queryKey: albumQueryKey,
- queryFn: () => fetchItem(api, AlbumId!),
+ queryKey: [QueryKeys.Album, AlbumId],
+ queryFn: () => fetchItem(api, AlbumId),
staleTime: ONE_DAY,
})