diff --git a/app/(auth)/now-playing.tsx b/app/(auth)/now-playing.tsx index ba62fea2..cc53340d 100644 --- a/app/(auth)/now-playing.tsx +++ b/app/(auth)/now-playing.tsx @@ -5,6 +5,7 @@ import { useRouter } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { + ActivityIndicator, Dimensions, FlatList, Platform, @@ -39,6 +40,7 @@ export default function NowPlayingScreen() { queue, queueIndex, isPlaying, + isLoading, progress, duration, repeatMode, @@ -205,6 +207,7 @@ export default function NowPlayingScreen() { progressText={progressText} durationText={durationText} isPlaying={isPlaying} + isLoading={isLoading} repeatMode={repeatMode} shuffleEnabled={shuffleEnabled} canGoNext={canGoNext} @@ -242,6 +245,7 @@ interface PlayerViewProps { progressText: string; durationText: string; isPlaying: boolean; + isLoading: boolean; repeatMode: RepeatMode; shuffleEnabled: boolean; canGoNext: boolean; @@ -266,6 +270,7 @@ const PlayerView: React.FC = ({ progressText, durationText, isPlaying, + isLoading, repeatMode, shuffleEnabled, canGoNext, @@ -355,30 +360,35 @@ const PlayerView: React.FC = ({ - + {isLoading ? ( + + ) : ( + + )} diff --git a/components/music/MiniPlayerBar.tsx b/components/music/MiniPlayerBar.tsx index 3768ef7b..cf96e57d 100644 --- a/components/music/MiniPlayerBar.tsx +++ b/components/music/MiniPlayerBar.tsx @@ -4,7 +4,13 @@ import { Image } from "expo-image"; import { useRouter } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useMemo } from "react"; -import { Platform, StyleSheet, TouchableOpacity, View } from "react-native"; +import { + ActivityIndicator, + Platform, + StyleSheet, + TouchableOpacity, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { apiAtom } from "@/providers/JellyfinProvider"; @@ -18,8 +24,15 @@ export const MiniPlayerBar: React.FC = () => { const [api] = useAtom(apiAtom); const insets = useSafeAreaInsets(); const router = useRouter(); - const { currentTrack, isPlaying, progress, duration, togglePlayPause, next } = - useMusicPlayer(); + const { + currentTrack, + isPlaying, + isLoading, + progress, + duration, + togglePlayPause, + next, + } = useMusicPlayer(); const imageUrl = useMemo(() => { if (!api || !currentTrack) return null; @@ -87,24 +100,30 @@ export const MiniPlayerBar: React.FC = () => { {/* Controls */} - - - - - - + {isLoading ? ( + + ) : ( + <> + + + + + + + + )} {/* Progress bar at bottom */} @@ -219,6 +238,9 @@ const styles = StyleSheet.create({ controlButton: { padding: 8, }, + loader: { + marginHorizontal: 16, + }, progressContainer: { position: "absolute", bottom: 0, diff --git a/components/music/MusicTrackItem.tsx b/components/music/MusicTrackItem.tsx index bb0e2d05..0642fa2f 100644 --- a/components/music/MusicTrackItem.tsx +++ b/components/music/MusicTrackItem.tsx @@ -4,7 +4,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import React, { useCallback, useMemo } from "react"; -import { TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { Text } from "@/components/common/Text"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useMusicPlayer } from "@/providers/MusicPlayerProvider"; @@ -26,8 +26,14 @@ export const MusicTrackItem: React.FC = ({ }) => { const [api] = useAtom(apiAtom); const { showActionSheetWithOptions } = useActionSheet(); - const { playTrack, playNext, addToQueue, currentTrack, isPlaying } = - useMusicPlayer(); + const { + playTrack, + playNext, + addToQueue, + currentTrack, + isPlaying, + loadingTrackId, + } = useMusicPlayer(); const imageUrl = useMemo(() => { const albumId = track.AlbumId || track.ParentId; @@ -38,6 +44,7 @@ export const MusicTrackItem: React.FC = ({ }, [api, track]); const isCurrentTrack = currentTrack?.Id === track.Id; + const isTrackLoading = loadingTrackId === track.Id; const duration = useMemo(() => { if (!track.RunTimeTicks) return ""; @@ -109,6 +116,22 @@ export const MusicTrackItem: React.FC = ({ )} + {isTrackLoading && ( + + + + )} )} diff --git a/providers/MusicPlayerProvider.tsx b/providers/MusicPlayerProvider.tsx index 00321f4f..5a607eee 100644 --- a/providers/MusicPlayerProvider.tsx +++ b/providers/MusicPlayerProvider.tsx @@ -40,6 +40,7 @@ interface MusicPlayerState { queueIndex: number; isPlaying: boolean; isLoading: boolean; + loadingTrackId: string | null; // Track ID being loaded progress: number; duration: number; streamUrl: string | null; @@ -273,6 +274,7 @@ export const MusicPlayerProvider: React.FC = ({ queueIndex: 0, isPlaying: false, isLoading: false, + loadingTrackId: null, progress: 0, duration: 0, streamUrl: null, @@ -459,7 +461,12 @@ export const MusicPlayerProvider: React.FC = ({ async (queue: BaseItemDto[], startIndex: number) => { if (!api || !user?.Id || queue.length === 0) return; - setState((prev) => ({ ...prev, isLoading: true })); + const trackToLoad = queue[startIndex]; + setState((prev) => ({ + ...prev, + isLoading: true, + loadingTrackId: trackToLoad?.Id ?? null, + })); try { // Get stream URLs for all tracks @@ -480,7 +487,11 @@ export const MusicPlayerProvider: React.FC = ({ } if (tracks.length === 0) { - setState((prev) => ({ ...prev, isLoading: false })); + setState((prev) => ({ + ...prev, + isLoading: false, + loadingTrackId: null, + })); return; } @@ -498,6 +509,7 @@ export const MusicPlayerProvider: React.FC = ({ queueIndex: startIndex, currentTrack, isLoading: false, + loadingTrackId: null, isPlaying: true, streamUrl: tracks[startIndex]?.url || null, duration: currentTrack?.RunTimeTicks @@ -507,7 +519,11 @@ export const MusicPlayerProvider: React.FC = ({ reportPlaybackStart(currentTrack, state.playSessionId); } catch (_error) { - setState((prev) => ({ ...prev, isLoading: false })); + setState((prev) => ({ + ...prev, + isLoading: false, + loadingTrackId: null, + })); } }, [api, user?.Id, reportPlaybackStart, state.playSessionId], @@ -784,6 +800,7 @@ export const MusicPlayerProvider: React.FC = ({ queueIndex: 0, isPlaying: false, isLoading: false, + loadingTrackId: null, progress: 0, duration: 0, streamUrl: null,