// Read-only queue sheet for the Desktop Remote: the desktop's current + // upcoming tracks, tap-to-play. Deliberately NOT the native queue — that reads // the local playback session straight out of Room (drag-reorder, swipe-remove, // multi-select), none of which applies to a remote snapshot. This is the only // remaining React Native queue surface. It uses an INLINE BottomSheet because // BottomSheetModal's portal does not work in this app's screen setups (see // queue-tray-sheet gotcha). import { useCallback, useEffect, useMemo } from 'react'; import { Pressable, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import BottomSheet, { BottomSheetBackdrop, type BottomSheetBackdropProps, type BottomSheetHandleProps, useBottomSheetScrollableCreator, } from '@gorhom/bottom-sheet'; import { FlashList, type ListRenderItemInfo } from '@shopify/flash-list'; import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; import { formatDuration } from '@/lib/format'; import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore'; import type { DesktopRemoteQueueItem } from '@/types/desktopRemote'; import { QueueSheetHandle } from './QueueSheetHandle'; interface RemoteQueueSheetProps { onClose: () => void; embedded?: boolean; } export function RemoteQueueSheet({ onClose, embedded = false }: RemoteQueueSheetProps) { const styles = useStyles(); const colors = useColors(); const ripple = useRipple(); const queue = useDesktopRemoteStore((s) => s.queue); const snapPoints = useMemo(() => ['58%', '100%'], []); const renderFlashListScrollComponent = useBottomSheetScrollableCreator(); // The SSE stream keeps the queue fresh while connected; refresh once on open // in case the stream fell back to snapshot polling (which has no queue). useEffect(() => { void useDesktopRemoteStore.getState().refreshQueue(); }, []); const items = queue?.items ?? []; const upcomingCount = items.filter((item) => !item.isCurrent).length; const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( ), [] ); const playItem = useCallback( (item: DesktopRemoteQueueItem) => { if (item.isCurrent) return; void useDesktopRemoteStore.getState().playQueueItem(item.queueId); onClose(); }, [onClose] ); const renderItem = useCallback( ({ item }: ListRenderItemInfo) => ( playItem(item)} disabled={item.isCurrent} accessibilityRole="button" accessibilityLabel={ item.isCurrent ? `Now playing: ${item.title}` : `Play ${item.title} on desktop` } > {item.title || 'Unknown title'} {item.artist || 'Unknown artist'} {item.isCurrent ? ( ) : item.durationSeconds !== null ? ( {formatDuration(item.durationSeconds)} ) : null} ), [playItem, colors, styles, ripple] ); const topSection = useMemo( () => ( Desktop queue {upcomingCount === 1 ? '1 song up next' : `${upcomingCount} songs up next`} ), [colors.textTertiary, styles.headerRow, upcomingCount] ); const content = ( item.queueId} renderScrollComponent={embedded ? undefined : renderFlashListScrollComponent} renderItem={renderItem} contentContainerStyle={styles.listContent} showsVerticalScrollIndicator={false} ListEmptyComponent={ The desktop queue is empty. } /> ); const renderSheetHandle = useCallback( (props: BottomSheetHandleProps) => ( {topSection} ), [topSection] ); if (embedded) { return ( {topSection} {content} ); } return ( {content} ); } const useStyles = createThemedStyles((colors) => ({ sheetBg: { backgroundColor: colors.bgSecondary, borderRadius: radius.lg, }, handle: { backgroundColor: colors.textTertiary, }, embeddedRoot: { flex: 1, overflow: 'hidden', }, headerRow: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'space-between', paddingHorizontal: spacing.lg, paddingBottom: spacing.sm, }, listContent: { paddingHorizontal: spacing.lg, paddingBottom: spacing.xl, }, row: { minHeight: 56, flexDirection: 'row', alignItems: 'center', gap: spacing.md, }, rowText: { flex: 1, minWidth: 0, }, titleActive: { color: colors.accent, }, empty: { paddingVertical: spacing.xl, alignItems: 'center', }, }));