diff --git a/src/components/queue/QueueSheetHandle.tsx b/src/components/queue/QueueSheetHandle.tsx new file mode 100644 index 0000000..4782e12 --- /dev/null +++ b/src/components/queue/QueueSheetHandle.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react'; +import { View, type StyleProp, type ViewStyle } from 'react-native'; +import { + BottomSheetHandle, + type BottomSheetHandleProps, +} from '@gorhom/bottom-sheet'; + +interface QueueSheetHandleProps extends BottomSheetHandleProps { + children: ReactNode; + style?: StyleProp; + indicatorStyle?: StyleProp; +} + +/** + * Keeps the library's visual/accessibility handle while extending its gesture + * surface across the fixed queue header rendered below it. + */ +export function QueueSheetHandle({ + children, + style, + indicatorStyle, + ...handleProps +}: QueueSheetHandleProps) { + return ( + + + {children} + + ); +} diff --git a/src/components/queue/QueueTray.tsx b/src/components/queue/QueueTray.tsx index d2fa635..ae6cf6e 100644 --- a/src/components/queue/QueueTray.tsx +++ b/src/components/queue/QueueTray.tsx @@ -18,6 +18,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import BottomSheet, { BottomSheetBackdrop, type BottomSheetBackdropProps, + type BottomSheetHandleProps, useBottomSheetScrollableCreator } from '@gorhom/bottom-sheet'; import { FlashList, type ListRenderItemInfo } from '@shopify/flash-list'; @@ -72,6 +73,7 @@ import { QUEUE_ROW_HEIGHT, queuePreviewRowCount, } from './queuePerformance'; +import { QueueSheetHandle } from './QueueSheetHandle'; const ART = 42; const EMPTY_KEY_SET = new Set(); @@ -805,7 +807,7 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }: const selectedCount = visibleSelectedKeys.size; const canEdit = queueReady && entries.length > 0; - const body = ( + const topSection = useMemo(() => ( <> @@ -853,7 +855,20 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }: Up next + + ), [ + canEdit, + colors.accent, + currentTrack, + editMode, + exitEdit, + ripple.bounded, + styles, + upcomingTotal, + ]); + const content = ( + <> {listReady ? ( ); + const renderSheetHandle = useCallback( + (props: BottomSheetHandleProps) => ( + + {topSection} + + ), + [topSection] + ); + if (embedded) { - return {body}; + return ( + + {topSection} + {content} + + ); } return ( @@ -938,9 +967,10 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }: onClose={onClose} backdropComponent={renderBackdrop} backgroundStyle={styles.sheetBg} + handleComponent={renderSheetHandle} handleIndicatorStyle={styles.handle} > - {body} + {content} ); }); diff --git a/src/components/queue/RemoteQueueSheet.tsx b/src/components/queue/RemoteQueueSheet.tsx index 2245661..b2a9789 100644 --- a/src/components/queue/RemoteQueueSheet.tsx +++ b/src/components/queue/RemoteQueueSheet.tsx @@ -11,6 +11,7 @@ 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'; @@ -21,6 +22,7 @@ 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; @@ -102,34 +104,52 @@ export function RemoteQueueSheet({ onClose, embedded = false }: RemoteQueueSheet [playItem, colors, styles, ripple] ); - const body = ( - <> + const topSection = useMemo( + () => ( Desktop queue {upcomingCount === 1 ? '1 song up next' : `${upcomingCount} songs up next`} - item.queueId} - renderScrollComponent={embedded ? undefined : renderFlashListScrollComponent} - renderItem={renderItem} - contentContainerStyle={styles.listContent} - showsVerticalScrollIndicator={false} - ListEmptyComponent={ - - - The desktop queue is empty. - - - } - /> - + ), + [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 {body}; + return ( + + {topSection} + {content} + + ); } return ( @@ -141,9 +161,10 @@ export function RemoteQueueSheet({ onClose, embedded = false }: RemoteQueueSheet onClose={onClose} backdropComponent={renderBackdrop} backgroundStyle={styles.sheetBg} + handleComponent={renderSheetHandle} handleIndicatorStyle={styles.handle} > - {body} + {content} ); }