mirror of
https://github.com/Jellify-Music/App.git
synced 2026-04-20 08:43:42 -05:00
ui fixes
This commit is contained in:
@@ -19,6 +19,10 @@ import Animated, {
|
||||
withTiming,
|
||||
useAnimatedReaction,
|
||||
ReduceMotion,
|
||||
SlideInUp,
|
||||
SlideInDown,
|
||||
SlideOutDown,
|
||||
interpolate,
|
||||
} from 'react-native-reanimated'
|
||||
import { runOnJS } from 'react-native-worklets'
|
||||
import { RootStackParamList } from '../../screens/types'
|
||||
@@ -27,6 +31,7 @@ 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'
|
||||
|
||||
export default function Miniplayer(): React.JSX.Element | null {
|
||||
const nowPlaying = useCurrentTrack()
|
||||
@@ -80,9 +85,6 @@ export default function Miniplayer(): React.JSX.Element | null {
|
||||
navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
|
||||
}
|
||||
|
||||
const pressStyle = {
|
||||
opacity: 0.6,
|
||||
}
|
||||
if (!nowPlaying) return null
|
||||
|
||||
// Guard: during track transitions nowPlaying can be briefly null
|
||||
@@ -104,14 +106,13 @@ export default function Miniplayer(): React.JSX.Element | null {
|
||||
<Animated.View
|
||||
collapsable={false}
|
||||
testID='miniplayer-test-id'
|
||||
entering={FadeInDown.springify()}
|
||||
exiting={FadeOutDown.springify()}
|
||||
entering={SlideInDown.springify()}
|
||||
exiting={SlideOutDown.springify()}
|
||||
>
|
||||
<YStack
|
||||
pressStyle={pressStyle}
|
||||
transition={'quick'}
|
||||
onPress={openPlayer}
|
||||
backgroundColor={theme.background.val}
|
||||
{...ICON_PRESS_STYLES}
|
||||
>
|
||||
<MiniPlayerProgress />
|
||||
<XStack alignItems='center' padding={'$2'}>
|
||||
@@ -167,25 +168,21 @@ function MiniPlayerProgress(): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
const progressValue = useSharedValue(position === 0 ? 0 : (position / totalDuration) * 100)
|
||||
|
||||
const handleDisplayPositionChange = (newPosition: number) => {
|
||||
if (newPosition === 0) {
|
||||
progressValue.value = withTiming(0, {
|
||||
duration: 300,
|
||||
})
|
||||
} else {
|
||||
const percentage = calculateProgressPercentage(newPosition, totalDuration)
|
||||
progressValue.value = withTiming(percentage, {
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
reduceMotion: ReduceMotion.System,
|
||||
})
|
||||
}
|
||||
const handleDisplayPositionChange = (newPosition: number, prevPosition: number | null) => {
|
||||
const timingDuration =
|
||||
Math.round(Math.abs(newPosition - (prevPosition ?? 0))) === 1 ? 1000 : 200
|
||||
|
||||
progressValue.value = withTiming(interpolate(newPosition, [0, totalDuration], [0, 100]), {
|
||||
duration: timingDuration,
|
||||
easing: Easing.linear,
|
||||
reduceMotion: ReduceMotion.Never,
|
||||
})
|
||||
}
|
||||
|
||||
useAnimatedReaction(
|
||||
() => position,
|
||||
(cur, prev) => {
|
||||
if (cur !== prev) runOnJS(handleDisplayPositionChange)(cur)
|
||||
if (cur !== prev) runOnJS(handleDisplayPositionChange)(cur, prev)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -202,7 +199,7 @@ function MiniPlayerProgress(): React.JSX.Element {
|
||||
height: '100%',
|
||||
backgroundColor: theme.primary.val,
|
||||
shadowColor: theme.background.val,
|
||||
shadowOffset: { width: 1, height: 1 },
|
||||
shadowOffset: { width: 2, height: 1 },
|
||||
shadowOpacity: 0.75,
|
||||
shadowRadius: 1,
|
||||
borderRadius: 4,
|
||||
|
||||
@@ -9,7 +9,6 @@ export default function LabsTab(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<SettingsListGroup
|
||||
borderColor={'$warning'}
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Clear Artists Cache',
|
||||
|
||||
@@ -8,7 +8,13 @@ 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, TrackPlayer, TrackPlayerState } from 'react-native-nitro-player'
|
||||
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'
|
||||
|
||||
@@ -232,12 +238,16 @@ export const useToggleShuffle = () => {
|
||||
return async (shuffled: boolean) => {
|
||||
triggerHaptic('impactMedium')
|
||||
|
||||
if (shuffled) await handleDeshuffle()
|
||||
else await handleShuffle()
|
||||
let result: { currentIndex: number; queue: TrackItem[] } | undefined
|
||||
|
||||
const newQueue = PlayerQueue.getPlaylist(PlayerQueue.getCurrentPlaylistId()!)!.tracks
|
||||
if (shuffled) result = await handleDeshuffle()
|
||||
else result = await handleShuffle()
|
||||
|
||||
usePlayerQueueStore.getState().setQueue(newQueue)
|
||||
usePlayerQueueStore.getState().setShuffled(!shuffled)
|
||||
usePlayerQueueStore.setState((state) => ({
|
||||
...state,
|
||||
queue: result.queue,
|
||||
currentIndex: result.currentIndex,
|
||||
shuffled: !shuffled,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@ import { mapDtoToTrack } from '../../../utils/mapping/item-to-track'
|
||||
import getTrackDto from '../../../utils/mapping/track-extra-payload'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<TrackItem[]> {
|
||||
export async function handleShuffle(
|
||||
keepCurrentTrack: boolean = true,
|
||||
): Promise<{ currentIndex: number; queue: TrackItem[] }> {
|
||||
const playlistId = PlayerQueue.getCurrentPlaylistId()
|
||||
|
||||
const currentIndex = usePlayerQueueStore.getState().currentIndex
|
||||
const currentTrack = usePlayerQueueStore.getState().currentTrack
|
||||
const playQueue = usePlayerQueueStore.getState().queue
|
||||
const currentTrack = playQueue[currentIndex ?? 0]
|
||||
|
||||
const queueRef = usePlayerQueueStore.getState().queueRef
|
||||
|
||||
@@ -37,7 +39,7 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<T
|
||||
!currentTrack ||
|
||||
!playlistId
|
||||
) {
|
||||
return []
|
||||
return { currentIndex: 0, queue: [] }
|
||||
}
|
||||
|
||||
const { currentPosition } = await TrackPlayer.getState()
|
||||
@@ -58,7 +60,10 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<T
|
||||
})
|
||||
// Fall through to regular shuffle if there's a queue
|
||||
if (!playQueue || playQueue.length === 0) {
|
||||
return []
|
||||
return {
|
||||
currentIndex: 0,
|
||||
queue: [],
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get current filters from the store
|
||||
@@ -81,7 +86,7 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<T
|
||||
text1: 'No downloaded tracks available',
|
||||
type: 'info',
|
||||
})
|
||||
return []
|
||||
return { currentIndex: 0, queue: [] }
|
||||
}
|
||||
|
||||
// Filter downloaded tracks
|
||||
@@ -194,7 +199,7 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<T
|
||||
: randomTracks.filter((track) => track.id !== currentTrack?.id)
|
||||
if (finalQueue.length === 0) {
|
||||
Toast.show({ text1: 'No tracks to shuffle', type: 'info' })
|
||||
return []
|
||||
return { currentIndex: 0, queue: [] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +223,7 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<T
|
||||
// Update state
|
||||
setNewQueue(finalQueue, 'Library', startIndex, true)
|
||||
|
||||
return [finalQueue[startIndex], ...finalQueue]
|
||||
return { currentIndex: startIndex, queue: finalQueue }
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -229,7 +234,7 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<T
|
||||
})
|
||||
// Fall through to regular shuffle if there's a queue
|
||||
if (!playQueue || playQueue.length === 0) {
|
||||
return []
|
||||
return { currentIndex: 0, queue: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,7 +245,7 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<T
|
||||
text1: 'Nothing to shuffle',
|
||||
type: 'info',
|
||||
})
|
||||
return []
|
||||
return { currentIndex: 0, queue: [] }
|
||||
}
|
||||
|
||||
if (isUndefined(currentIndex) || !currentTrack) {
|
||||
@@ -248,111 +253,94 @@ export async function handleShuffle(keepCurrentTrack: boolean = true): Promise<T
|
||||
text1: 'No track currently playing',
|
||||
type: 'info',
|
||||
})
|
||||
return []
|
||||
return { currentIndex: 0, queue: [] }
|
||||
}
|
||||
|
||||
// Save off unshuffledQueue
|
||||
usePlayerQueueStore.getState().setUnshuffledQueue([...playQueue])
|
||||
|
||||
const unusedTracks = playQueue
|
||||
.filter((_, index) => currentIndex != index)
|
||||
.map((track, index) => {
|
||||
return { track, index }
|
||||
})
|
||||
// Tracks that come AFTER the currently playing track — these are what we shuffle.
|
||||
const tracksAfterCurrent = playQueue.filter((_, index) => index > currentIndex)
|
||||
const { shuffled: newShuffledQueue } = shuffleJellifyTracks(tracksAfterCurrent)
|
||||
|
||||
if (keepCurrentTrack) {
|
||||
// Reorder current track to the front
|
||||
PlayerQueue.reorderTrackInPlaylist(playlistId!, currentTrack.id, 0)
|
||||
|
||||
// Remove the rest of the tracks from the playlist
|
||||
playQueue
|
||||
.filter((_, index) => currentIndex != index)
|
||||
.forEach((track) => PlayerQueue.removeTrackFromPlaylist(playlistId!, track.id))
|
||||
} else {
|
||||
// Remove all tracks (including current) - we'll replace with shuffled queue
|
||||
playQueue.forEach((track) => PlayerQueue.removeTrackFromPlaylist(playlistId!, track.id))
|
||||
}
|
||||
|
||||
// Get the current track (if any)
|
||||
let newShuffledQueue: TrackItem[] = []
|
||||
|
||||
// If there are upcoming tracks to shuffle
|
||||
if (unusedTracks.length > 0) {
|
||||
const { shuffled: shuffledUpcoming } = shuffleJellifyTracks(
|
||||
unusedTracks.map(({ track }) => track),
|
||||
// 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),
|
||||
)
|
||||
|
||||
// Create new queue: shuffled upcoming (with or without current based on keepCurrentTrack)
|
||||
newShuffledQueue = shuffledUpcoming
|
||||
} else {
|
||||
// Approach 2: If no upcoming tracks, shuffle entire queue but keep current track position
|
||||
// This handles the case where user is at the end of the queue
|
||||
if (currentTrack && keepCurrentTrack) {
|
||||
// Remove current track, shuffle the rest, then put current track back at its position
|
||||
const otherTracks = playQueue!.filter((_, index) => index !== currentIndex)
|
||||
const { shuffled: shuffledOthers } = shuffleJellifyTracks(otherTracks)
|
||||
// Insert the shuffled upcoming tracks right after currentIndex.
|
||||
PlayerQueue.addTracksToPlaylist(playlistId!, newShuffledQueue, currentIndex + 1)
|
||||
|
||||
// Create new queue with current track in the middle
|
||||
newShuffledQueue = [
|
||||
...shuffledOthers.slice(0, currentIndex),
|
||||
currentTrack,
|
||||
...shuffledOthers.slice(currentIndex),
|
||||
]
|
||||
} else {
|
||||
// No current track or keepCurrentTrack=false, shuffle everything
|
||||
const tracksToShuffle = keepCurrentTrack
|
||||
? playQueue!
|
||||
: playQueue!.filter((_, index) => index !== currentIndex)
|
||||
const { shuffled: shuffledAll } = shuffleJellifyTracks(tracksToShuffle)
|
||||
newShuffledQueue = shuffledAll
|
||||
}
|
||||
}
|
||||
|
||||
if (keepCurrentTrack) {
|
||||
PlayerQueue.addTracksToPlaylist(playlistId!, newShuffledQueue, 1)
|
||||
return [currentTrack, ...newShuffledQueue]
|
||||
// Present a clean queue to the JS store (current track first, then shuffled upcoming).
|
||||
return { currentIndex: 0, queue: [currentTrack, ...newShuffledQueue] }
|
||||
} else {
|
||||
// keepCurrentTrack=false: replacing the entire queue is intentional so skipToIndex is fine.
|
||||
playQueue.forEach((track) => PlayerQueue.removeTrackFromPlaylist(playlistId!, track.id))
|
||||
PlayerQueue.addTracksToPlaylist(playlistId!, newShuffledQueue, 0)
|
||||
// Start playback from first track
|
||||
TrackPlayer.skipToIndex(0)
|
||||
return newShuffledQueue
|
||||
return { currentIndex: 0, queue: newShuffledQueue }
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDeshuffle() {
|
||||
export async function handleDeshuffle(): Promise<{ currentIndex: number; queue: TrackItem[] }> {
|
||||
const playlistId = PlayerQueue.getCurrentPlaylistId()
|
||||
|
||||
const shuffled = usePlayerQueueStore.getState().shuffled
|
||||
const unshuffledQueue = usePlayerQueueStore.getState().unShuffledQueue
|
||||
const currentTrack = usePlayerQueueStore.getState().currentTrack
|
||||
const queueRef = usePlayerQueueStore.getState().queueRef
|
||||
const {
|
||||
currentIndex,
|
||||
shuffled,
|
||||
unShuffledQueue,
|
||||
queue: playQueue,
|
||||
queueRef,
|
||||
} = usePlayerQueueStore.getState()
|
||||
|
||||
const currentTrack = !isUndefined(currentIndex) ? playQueue[currentIndex] : undefined
|
||||
|
||||
if (queueRef === 'Library') {
|
||||
return await handleShuffle()
|
||||
}
|
||||
|
||||
// Don't deshuffle if not shuffled or no unshuffled queue stored
|
||||
if (!shuffled || !unshuffledQueue || unshuffledQueue.length === 0 || !playlistId) return
|
||||
if (
|
||||
!shuffled ||
|
||||
!unShuffledQueue ||
|
||||
unShuffledQueue.length === 0 ||
|
||||
!playlistId ||
|
||||
!currentTrack
|
||||
) {
|
||||
return { currentIndex: -1, queue: playQueue }
|
||||
}
|
||||
|
||||
// Move currently playing track to beginning of queue to preserve playback
|
||||
PlayerQueue.reorderTrackInPlaylist(playlistId, currentTrack!.id, 0)
|
||||
// Find where the currently playing track belongs in the original queue.
|
||||
const newCurrentIndex = unShuffledQueue.findIndex((track) => track.id === currentTrack.id)
|
||||
|
||||
// Find tracks that aren't currently playing, these will be used to repopulate the queue
|
||||
const missingQueueItems = unshuffledQueue.filter((track) => track.id !== currentTrack?.id)
|
||||
// The JS store's shuffled queue is [currentTrack, ...shuffledUpcoming].
|
||||
// The native PlaylistManager was NOT modified for tracks before currentIndex during shuffle,
|
||||
// so it looks like: [...originalBefore, currentTrack, ...shuffledUpcoming].
|
||||
//
|
||||
// To deshuffle without touching the current track's native position (same
|
||||
// ExoPlayer/AVPlayer window-index rule): remove only the shuffled upcoming tracks
|
||||
// (what the JS store knows as queue.slice(1)), then append the original upcoming tracks.
|
||||
const shuffledUpcoming = playQueue.slice((currentIndex ?? 0) + 1)
|
||||
const originalUpcoming = unShuffledQueue.slice(newCurrentIndex + 1)
|
||||
|
||||
// Find where the currently playing track belonged in the original queue, it will be moved to that position later
|
||||
const newCurrentIndex = unshuffledQueue.findIndex((track) => track.id === currentTrack?.id)
|
||||
// Remove the shuffled tracks that are after the current track.
|
||||
shuffledUpcoming.forEach(({ id }) => PlayerQueue.removeTrackFromPlaylist(playlistId, id))
|
||||
|
||||
// Clear Upcoming tracks
|
||||
missingQueueItems.forEach(({ id }) => PlayerQueue.removeTrackFromPlaylist(playlistId, id))
|
||||
// Add the original upcoming tracks right after currentTrack's native position.
|
||||
if (originalUpcoming.length > 0) {
|
||||
PlayerQueue.addTracksToPlaylist(playlistId, originalUpcoming, newCurrentIndex + 1)
|
||||
}
|
||||
|
||||
// Add the original queue to the end, without the currently playing track since that's still in the queue
|
||||
PlayerQueue.addTracksToPlaylist(playlistId, missingQueueItems, 1)
|
||||
|
||||
// Move the currently playing track into position
|
||||
PlayerQueue.reorderTrackInPlaylist(playlistId, currentTrack!.id, newCurrentIndex)
|
||||
|
||||
// Just-in-time approach: Don't disrupt current playback
|
||||
// The queue will be updated when user skips or when tracks change
|
||||
usePlayerQueueStore.getState().setUnshuffledQueue([])
|
||||
|
||||
return { currentIndex: newCurrentIndex, queue: unShuffledQueue }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user