import { Ionicons } from "@expo/vector-icons"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useRouter } from "expo-router"; import { useAtom } from "jotai"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Dimensions, FlatList, Platform, ScrollView, TouchableOpacity, View, } from "react-native"; import { Slider } from "react-native-awesome-slider"; import { useSharedValue } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { apiAtom } from "@/providers/JellyfinProvider"; import { type RepeatMode, useMusicPlayer, } from "@/providers/MusicPlayerProvider"; import { formatDuration } from "@/utils/time"; const { width: SCREEN_WIDTH } = Dimensions.get("window"); const ARTWORK_SIZE = SCREEN_WIDTH - 80; type ViewMode = "player" | "queue"; export default function NowPlayingScreen() { const [api] = useAtom(apiAtom); const router = useRouter(); const insets = useSafeAreaInsets(); const [viewMode, setViewMode] = useState("player"); const { currentTrack, queue, queueIndex, isPlaying, progress, duration, repeatMode, shuffleEnabled, togglePlayPause, next, previous, seek, setRepeatMode, toggleShuffle, jumpToIndex, removeFromQueue, stop, } = useMusicPlayer(); const sliderProgress = useSharedValue(0); const sliderMin = useSharedValue(0); const sliderMax = useSharedValue(1); useEffect(() => { sliderProgress.value = progress; }, [progress, sliderProgress]); useEffect(() => { sliderMax.value = duration > 0 ? duration : 1; }, [duration, sliderMax]); const imageUrl = useMemo(() => { if (!api || !currentTrack) return null; const albumId = currentTrack.AlbumId || currentTrack.ParentId; if (albumId) { return `${api.basePath}/Items/${albumId}/Images/Primary?maxHeight=600&maxWidth=600`; } return `${api.basePath}/Items/${currentTrack.Id}/Images/Primary?maxHeight=600&maxWidth=600`; }, [api, currentTrack]); const progressText = useMemo(() => { const progressTicks = progress * 10000000; return formatDuration(progressTicks); }, [progress]); const durationText = useMemo(() => { const durationTicks = duration * 10000000; return formatDuration(durationTicks); }, [duration]); const handleSliderComplete = useCallback( (value: number) => { seek(value); }, [seek], ); const handleClose = useCallback(() => { router.back(); }, [router]); const handleStop = useCallback(() => { stop(); router.back(); }, [stop, router]); const cycleRepeatMode = useCallback(() => { const modes: RepeatMode[] = ["off", "all", "one"]; const currentIndex = modes.indexOf(repeatMode); const nextMode = modes[(currentIndex + 1) % modes.length]; setRepeatMode(nextMode); }, [repeatMode, setRepeatMode]); const getRepeatIcon = (): string => { switch (repeatMode) { case "one": return "repeat"; case "all": return "repeat"; default: return "repeat"; } }; const canGoNext = queueIndex < queue.length - 1 || repeatMode === "all"; const canGoPrevious = queueIndex > 0 || progress > 3 || repeatMode === "all"; if (!currentTrack) { return ( No track playing ); } return ( {/* Header */} setViewMode("player")} className='px-3 py-1' > Now Playing setViewMode("queue")} className='px-3 py-1' > Queue ({queue.length}) {viewMode === "player" ? ( ) : ( )} ); } interface PlayerViewProps { api: any; currentTrack: BaseItemDto; imageUrl: string | null; sliderProgress: any; sliderMin: any; sliderMax: any; progressText: string; durationText: string; isPlaying: boolean; repeatMode: RepeatMode; shuffleEnabled: boolean; canGoNext: boolean; canGoPrevious: boolean; onSliderComplete: (value: number) => void; onTogglePlayPause: () => void; onNext: () => void; onPrevious: () => void; onCycleRepeat: () => void; onToggleShuffle: () => void; getRepeatIcon: () => string; queue: BaseItemDto[]; queueIndex: number; } const PlayerView: React.FC = ({ currentTrack, imageUrl, sliderProgress, sliderMin, sliderMax, progressText, durationText, isPlaying, repeatMode, shuffleEnabled, canGoNext, canGoPrevious, onSliderComplete, onTogglePlayPause, onNext, onPrevious, onCycleRepeat, onToggleShuffle, getRepeatIcon, queue, queueIndex, }) => { return ( {/* Album artwork */} {imageUrl ? ( ) : ( )} {/* Track info */} {currentTrack.Name} {currentTrack.Artists?.join(", ") || currentTrack.AlbumArtist} {currentTrack.Album && ( {currentTrack.Album} )} {/* Progress slider */} null} /> {progressText} {durationText} {/* Main Controls */} {/* Shuffle & Repeat Controls */} {repeatMode === "one" && ( 1 )} {/* Queue info */} {queue.length > 1 && ( {queueIndex + 1} of {queue.length} )} ); }; interface QueueViewProps { api: any; queue: BaseItemDto[]; queueIndex: number; onJumpToIndex: (index: number) => void; onRemoveFromQueue: (index: number) => void; } const QueueView: React.FC = ({ api, queue, queueIndex, onJumpToIndex, onRemoveFromQueue, }) => { const renderQueueItem = useCallback( ({ item, index }: { item: BaseItemDto; index: number }) => { const isCurrentTrack = index === queueIndex; const isPast = index < queueIndex; const albumId = item.AlbumId || item.ParentId; const imageUrl = api ? albumId ? `${api.basePath}/Items/${albumId}/Images/Primary?maxHeight=80&maxWidth=80` : `${api.basePath}/Items/${item.Id}/Images/Primary?maxHeight=80&maxWidth=80` : null; return ( onJumpToIndex(index)} className={`flex-row items-center px-4 py-3 ${isCurrentTrack ? "bg-purple-900/30" : ""}`} style={{ opacity: isPast ? 0.5 : 1 }} > {/* Track number / Now playing indicator */} {isCurrentTrack ? ( ) : ( {index + 1} )} {/* Album art */} {imageUrl ? ( ) : ( )} {/* Track info */} {item.Name} {item.Artists?.join(", ") || item.AlbumArtist} {/* Remove button (not for current track) */} {!isCurrentTrack && ( onRemoveFromQueue(index)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} className='p-2' > )} ); }, [api, queueIndex, onJumpToIndex, onRemoveFromQueue], ); const _upNext = queue.slice(queueIndex + 1); const history = queue.slice(0, queueIndex); return ( `${item.Id}-${index}`} renderItem={renderQueueItem} showsVerticalScrollIndicator={false} initialScrollIndex={queueIndex > 2 ? queueIndex - 2 : 0} getItemLayout={(_, index) => ({ length: 72, offset: 72 * index, index, })} ListHeaderComponent={ {history.length > 0 ? "Playing from queue" : "Up next"} } ListEmptyComponent={ Queue is empty } /> ); };