diff --git a/src/app/(tabs)/index.tsx b/src/app/(tabs)/index.tsx index 9589125..a0bb0e5 100644 --- a/src/app/(tabs)/index.tsx +++ b/src/app/(tabs)/index.tsx @@ -8,6 +8,7 @@ import { Text } from '@/components/Text'; import { AstraLogo } from '@/components/AstraLogo'; import { SpectrumCurve } from '@/components/SpectrumCurve'; import { TrackRow } from '@/components/library/TrackRow'; +import { TrackActionsSheet } from '@/components/library/TrackActionsSheet'; import { PlaylistRow } from '@/components/library/PlaylistRow'; import { ScanProgress } from '@/components/library/ScanProgress'; import { @@ -357,6 +358,7 @@ export default function HomeScreen() { const [randomAlbumKey, setRandomAlbumKey] = useState(null); const [randomSeed] = useState(() => Math.random()); + const [actionTrack, setActionTrack] = useState(null); const scrollTop = useScrollTopGate(); const hasLibrary = tracks.length > 0; @@ -511,6 +513,8 @@ export default function HomeScreen() { active={track.path === currentPath} swipeToQueue={false} onPress={() => playTrackList(recentTracks, index)} + onLongPress={() => setActionTrack(track)} + onOpenActions={() => setActionTrack(track)} /> ))} @@ -564,6 +568,7 @@ export default function HomeScreen() { )} + setActionTrack(null)} /> ); } diff --git a/src/app/(tabs)/library/album/[key].tsx b/src/app/(tabs)/library/album/[key].tsx index 1fd767b..100dfd0 100644 --- a/src/app/(tabs)/library/album/[key].tsx +++ b/src/app/(tabs)/library/album/[key].tsx @@ -111,6 +111,7 @@ export default function AlbumScreen() { active={item.path === currentPath} onPress={() => playFrom(index)} onLongPress={() => setActionTrack(item)} + onOpenActions={() => setActionTrack(item)} /> )} /> diff --git a/src/app/(tabs)/library/artist/[name].tsx b/src/app/(tabs)/library/artist/[name].tsx index 9a04be8..5911790 100644 --- a/src/app/(tabs)/library/artist/[name].tsx +++ b/src/app/(tabs)/library/artist/[name].tsx @@ -119,6 +119,7 @@ export default function ArtistScreen() { active={item.track.path === currentPath} onPress={() => playTrackListFrom(sourceTracks, item.index)} onLongPress={() => setActionTrack(item.track)} + onOpenActions={() => setActionTrack(item.track)} /> ); } diff --git a/src/app/(tabs)/library/artist/[name]/appearances.tsx b/src/app/(tabs)/library/artist/[name]/appearances.tsx index 2614a02..73ea63b 100644 --- a/src/app/(tabs)/library/artist/[name]/appearances.tsx +++ b/src/app/(tabs)/library/artist/[name]/appearances.tsx @@ -62,6 +62,7 @@ export default function ArtistAppearancesScreen() { active={item.path === currentPath} onPress={() => playFrom(index)} onLongPress={() => setActionTrack(item)} + onOpenActions={() => setActionTrack(item)} /> )} ListEmptyComponent={} diff --git a/src/app/(tabs)/library/artist/[name]/songs.tsx b/src/app/(tabs)/library/artist/[name]/songs.tsx index 19ab604..17738cf 100644 --- a/src/app/(tabs)/library/artist/[name]/songs.tsx +++ b/src/app/(tabs)/library/artist/[name]/songs.tsx @@ -62,6 +62,7 @@ export default function ArtistSongsScreen() { active={item.path === currentPath} onPress={() => playFrom(index)} onLongPress={() => setActionTrack(item)} + onOpenActions={() => setActionTrack(item)} /> )} ListEmptyComponent={} diff --git a/src/app/(tabs)/library/index.tsx b/src/app/(tabs)/library/index.tsx index 7cfb884..2a10b9b 100644 --- a/src/app/(tabs)/library/index.tsx +++ b/src/app/(tabs)/library/index.tsx @@ -175,6 +175,7 @@ export default function LibraryScreen() { active={item.path === currentPath} onPress={() => playAllFrom(index)} onLongPress={() => setActionTrack(item)} + onOpenActions={() => setActionTrack(item)} /> )} /> diff --git a/src/app/(tabs)/library/playlist/[id].tsx b/src/app/(tabs)/library/playlist/[id].tsx index e6d2263..09d6c97 100644 --- a/src/app/(tabs)/library/playlist/[id].tsx +++ b/src/app/(tabs)/library/playlist/[id].tsx @@ -231,6 +231,7 @@ export default function PlaylistScreen() { active={item.track.path === currentPath} onPress={() => startPlayback(playableIndexByEntryId.get(item.id) ?? 0)} onLongPress={() => setActionEntry(item)} + onOpenActions={() => setActionEntry(item)} /> ) : ( setMissingEntry(item)} /> diff --git a/src/app/recently-played.tsx b/src/app/recently-played.tsx index 82ec4ad..335f0d1 100644 --- a/src/app/recently-played.tsx +++ b/src/app/recently-played.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Pressable, StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { FlashList } from '@shopify/flash-list'; @@ -5,11 +6,13 @@ import { useRouter } from 'expo-router'; import { Screen } from '@/components/Screen'; import { Text } from '@/components/Text'; import { TrackRow } from '@/components/library/TrackRow'; +import { TrackActionsSheet } from '@/components/library/TrackActionsSheet'; import { colors, spacing } from '@/theme'; import { useLibraryStore } from '@/stores/libraryStore'; import { usePlayerStore } from '@/stores/playerStore'; import { playTracks } from '@/audio/playbackController'; import { dbTrackToTrack } from '@/library/trackAdapter'; +import type { DbTrack } from '@/types/library'; function formatCount(count: number, noun: string): string { return `${count} ${count === 1 ? noun : `${noun}s`}`; @@ -30,6 +33,7 @@ export default function RecentlyPlayedScreen() { const router = useRouter(); const tracks = useLibraryStore((s) => s.recentlyPlayedTracks); const currentPath = usePlayerStore((s) => s.currentTrack?.path); + const [actionTrack, setActionTrack] = useState(null); const playFrom = (index: number) => { if (tracks.length === 0) return; @@ -62,11 +66,14 @@ export default function RecentlyPlayedScreen() { active={item.path === currentPath} swipeToQueue={false} onPress={() => playFrom(index)} + onLongPress={() => setActionTrack(item)} + onOpenActions={() => setActionTrack(item)} /> )} ListEmptyComponent={} contentContainerStyle={styles.listContent} /> + setActionTrack(null)} /> ); } diff --git a/src/components/eq/EqSheet.tsx b/src/components/eq/EqSheet.tsx index a60aa7a..23133f8 100644 --- a/src/components/eq/EqSheet.tsx +++ b/src/components/eq/EqSheet.tsx @@ -1,132 +1,17 @@ -import { useCallback, type ReactNode } from 'react'; -import { Pressable, StyleSheet, View } from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import BottomSheet, { - BottomSheetBackdrop, - BottomSheetView, - type BottomSheetBackdropProps, -} from '@gorhom/bottom-sheet'; -import { Text } from '@/components/Text'; -import { colors, radius, spacing } from '@/theme'; +import { + AppSheet, + AppSheetItem, + AppSheetSection, + type AppSheetItemProps, +} from '@/components/sheets/AppSheet'; /** - * Bottom sheet for the EQ screen's menus — same chrome/behaviour as the now-playing - * QueueTray (inline gorhom BottomSheet, dimmed backdrop, grab handle, pan-to-close) - * so trays stay consistent across the app. Dynamically sized to its content; render - * it conditionally ({open && }). + * EQ-named wrappers around the shared app sheet chrome. Keeping these exports + * avoids churn in EQ call sites while the same bottom-sheet UX is reused elsewhere. */ -export function EqSheet({ onClose, children }: { onClose: () => void; children: ReactNode }) { - const insets = useSafeAreaInsets(); - const renderBackdrop = useCallback( - (props: BottomSheetBackdropProps) => ( - - ), - [] - ); - - return ( - - - {children} - - - ); -} - -/** Section label inside a sheet. */ -export function EqSheetSection({ label }: { label: string }) { - return ( - - {label} - - ); -} - -interface EqSheetItemProps { - label: string; - icon?: keyof typeof Ionicons.glyphMap; - selected?: boolean; - destructive?: boolean; - onPress: () => void; - /** Optional trailing control (e.g. a delete button). */ - trailing?: ReactNode; -} - -/** One tappable row, styled like the ActionSheet items it replaces. */ -export function EqSheetItem({ label, icon, selected, destructive, onPress, trailing }: EqSheetItemProps) { - const tint = destructive ? colors.warning : selected ? colors.accentTextStrong : colors.textPrimary; - return ( - - [styles.item, pressed && styles.itemPressed]} - onPress={onPress} - accessibilityRole="button" - > - {icon ? ( - - ) : null} - - {label} - - {selected ? : null} - - {trailing} - - ); -} - -const styles = StyleSheet.create({ - sheetBg: { - backgroundColor: colors.bgSecondary, - borderTopLeftRadius: radius.lg, - borderTopRightRadius: radius.lg, - }, - handle: { - backgroundColor: colors.glassBorder, - width: 38, - }, - content: { - paddingHorizontal: spacing.lg, - paddingTop: spacing.xs, - }, - section: { - color: colors.textTertiary, - letterSpacing: 1, - marginTop: spacing.md, - marginBottom: spacing.xs, - }, - itemRow: { - flexDirection: 'row', - alignItems: 'center', - }, - item: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - gap: spacing.md, - paddingVertical: spacing.md, - }, - itemPressed: { - opacity: 0.6, - }, - itemLabel: { - flex: 1, - }, -}); +export const EqSheet = AppSheet; +export const EqSheetSection = AppSheetSection; +export const EqSheetItem = AppSheetItem; +export type EqSheetItemProps = AppSheetItemProps; export default EqSheet; diff --git a/src/components/library/FoldersView.tsx b/src/components/library/FoldersView.tsx index 3ffbb16..a9ba905 100644 --- a/src/components/library/FoldersView.tsx +++ b/src/components/library/FoldersView.tsx @@ -3,12 +3,14 @@ import { Pressable, StyleSheet, View, + type GestureResponderEvent, type NativeScrollEvent, type NativeSyntheticEvent, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { FlashList } from '@shopify/flash-list'; import { Text } from '@/components/Text'; +import { TrackActionsSheet } from '@/components/library/TrackActionsSheet'; import { PullSearchScrollView } from '@/components/search/PullSearchGesture'; import { playTracks } from '@/audio/playbackController'; import { dbTrackToTrack } from '@/library/trackAdapter'; @@ -18,9 +20,10 @@ import { type FlattenedFolderTreeRow, } from '@/library/folderTree'; import { formatDuration } from '@/lib/format'; -import { colors, spacing } from '@/theme'; +import { colors, radius, spacing } from '@/theme'; import { useLibraryStore } from '@/stores/libraryStore'; import { usePlayerStore } from '@/stores/playerStore'; +import type { DbTrack } from '@/types/library'; interface FoldersViewProps { onScroll?: (event: NativeSyntheticEvent) => void; @@ -74,20 +77,27 @@ function FolderRow({ function FolderTrackRow({ row, active, + onOpenActions, }: { row: Extract; active: boolean; + onOpenActions: () => void; }) { const index = row.folderTracks.findIndex((track) => track.path === row.track.path); const playFolderTrack = () => { void playTracks(row.folderTracks.map(dbTrackToTrack), Math.max(0, index)); }; + const openActions = (event: GestureResponderEvent) => { + event.stopPropagation(); + onOpenActions(); + }; return ( @@ -103,6 +113,15 @@ function FolderTrackRow({ {formatDuration(row.track.duration)} + [styles.actionsButton, pressed && styles.actionsButtonPressed]} + onPress={openActions} + hitSlop={8} + accessibilityRole="button" + accessibilityLabel={`More actions for ${row.track.title}`} + > + + ); } @@ -112,6 +131,7 @@ export function FoldersView({ onScroll, scrollEventThrottle }: FoldersViewProps) const tracks = useLibraryStore((s) => s.tracks); const currentPath = usePlayerStore((s) => s.currentTrack?.path); const [expandedNodeIds, setExpandedNodeIds] = useState>(() => new Set()); + const [actionTrack, setActionTrack] = useState(null); const tree = useMemo(() => buildFolderTree(folders, tracks), [folders, tracks]); const rows = useMemo(() => flattenFolderTree(tree, expandedNodeIds), [expandedNodeIds, tree]); @@ -141,23 +161,30 @@ export function FoldersView({ onScroll, scrollEventThrottle }: FoldersViewProps) } return ( - row.id} - showsVerticalScrollIndicator={false} - overScrollMode="never" - renderScrollComponent={PullSearchScrollView} - onScroll={onScroll} - scrollEventThrottle={scrollEventThrottle} - contentContainerStyle={styles.listContent} - renderItem={({ item }) => - item.type === 'folder' ? ( - - ) : ( - - ) - } - /> + <> + row.id} + showsVerticalScrollIndicator={false} + overScrollMode="never" + renderScrollComponent={PullSearchScrollView} + onScroll={onScroll} + scrollEventThrottle={scrollEventThrottle} + contentContainerStyle={styles.listContent} + renderItem={({ item }) => + item.type === 'folder' ? ( + + ) : ( + setActionTrack(item.track)} + /> + ) + } + /> + setActionTrack(null)} /> + ); } @@ -217,6 +244,17 @@ const styles = StyleSheet.create({ color: colors.textTertiary, fontSize: 12, }, + actionsButton: { + width: 34, + height: 34, + flexShrink: 0, + borderRadius: radius.pill, + alignItems: 'center', + justifyContent: 'center', + }, + actionsButtonPressed: { + backgroundColor: colors.glassBg, + }, empty: { flex: 1, alignItems: 'center', diff --git a/src/components/library/TrackActionsSheet.tsx b/src/components/library/TrackActionsSheet.tsx index dcfb52b..b2c9d0c 100644 --- a/src/components/library/TrackActionsSheet.tsx +++ b/src/components/library/TrackActionsSheet.tsx @@ -1,9 +1,21 @@ import { useState } from 'react'; -import { ActionSheet, type ActionSheetItem } from '@/components/sheets/ActionSheet'; -import { TextPromptModal } from '@/components/sheets/TextPromptModal'; +import { Pressable, StyleSheet, View } from 'react-native'; +import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; +import { useRouter } from 'expo-router'; +import { Text } from '@/components/Text'; +import { AppSheet, AppSheetItem, AppSheetSection, type AppSheetItemProps } from '@/components/sheets/AppSheet'; +import { enqueueEnd, enqueueTop } from '@/audio/playbackController'; +import { dbTrackToTrack } from '@/library/trackAdapter'; +import { resolveCanonicalBrowseArtist, resolveStrictBrowseArtist } from '@/library/artistGrouping'; +import { colors, fonts, radius, spacing } from '@/theme'; import { usePlaylistStore } from '@/stores/playlistStore'; +import { useSettingsStore } from '@/stores/settingsStore'; import type { DbTrack } from '@/types/library'; +export interface TrackActionSheetItem extends AppSheetItemProps { + key: string; +} + interface TrackActionsSheetProps { /** null = hidden. */ track: DbTrack | null; @@ -11,10 +23,10 @@ interface TrackActionsSheetProps { /** Allows callers with their own menu to jump straight to playlist picking. */ initialStep?: 'menu' | 'pickPlaylist'; /** Screen-specific extras (e.g. playlist detail: remove / move). Handlers should close. */ - extraItems?: ActionSheetItem[]; + extraItems?: TrackActionSheetItem[]; } -/** Long-press track menu: favorite toggle, add-to-playlist (pick or create), extras. */ +/** Track menu: queue, playlist, navigation, favorites, and optional screen extras. */ export function TrackActionsSheet(props: TrackActionsSheetProps) { // Mount fresh per track so the step state resets. if (!props.track) return null; @@ -27,78 +39,232 @@ function TrackActionsSheetInner({ initialStep = 'menu', extraItems = [], }: TrackActionsSheetProps & { track: DbTrack }) { + const router = useRouter(); const [step, setStep] = useState<'menu' | 'pickPlaylist' | 'newPlaylist'>(initialStep); + const [playlistName, setPlaylistName] = useState(''); + const groupingMode = useSettingsStore((s) => s.artistGroupingMode); const playlists = usePlaylistStore((s) => s.playlists); const isFavorite = usePlaylistStore((s) => s.favoritePaths.has(track.path)); const toggleFavorite = usePlaylistStore((s) => s.toggleFavorite); const addTracksToPlaylist = usePlaylistStore((s) => s.addTracksToPlaylist); const createPlaylist = usePlaylistStore((s) => s.createPlaylist); - const menuItems: ActionSheetItem[] = [ + const artistName = + groupingMode === 'fileTags' ? resolveStrictBrowseArtist(track) : resolveCanonicalBrowseArtist(track); + const trimmedPlaylistName = playlistName.trim(); + + const closeAndRun = (run: () => void) => { + onClose(); + run(); + }; + + const addToNewPlaylist = () => { + if (!trimmedPlaylistName) return; + void (async () => { + const playlist = await createPlaylist(trimmedPlaylistName); + await addTracksToPlaylist(playlist.id, [track]); + })(); + onClose(); + }; + + const menuItems: TrackActionSheetItem[] = [ + { + key: 'play-next', + label: 'Play next', + icon: 'play-skip-forward', + onPress: () => closeAndRun(() => void enqueueTop(dbTrackToTrack(track))), + }, + { + key: 'add-to-queue', + label: 'Add to queue', + icon: 'list-outline', + onPress: () => closeAndRun(() => void enqueueEnd(dbTrackToTrack(track))), + }, + { + key: 'add-to-playlist', + label: 'Add to playlist...', + icon: 'add-circle-outline', + onPress: () => setStep('pickPlaylist'), + }, + { + key: 'view-album', + label: 'View album', + icon: 'albums-outline', + onPress: () => + closeAndRun(() => + router.push({ + pathname: '/library/album/[key]', + params: { key: track.album_identity_key }, + }) + ), + }, + { + key: 'view-artist', + label: 'View artist', + icon: 'person-outline', + onPress: () => + closeAndRun(() => + router.push({ + pathname: '/library/artist/[name]', + params: { name: artistName }, + }) + ), + }, { key: 'favorite', label: isFavorite ? 'Remove from favorites' : 'Add to favorites', icon: isFavorite ? 'heart-dislike-outline' : 'heart-outline', - onPress: () => { - void toggleFavorite(track); - onClose(); - }, + selected: isFavorite, + onPress: () => closeAndRun(() => void toggleFavorite(track)), }, - { - key: 'add-to-playlist', - label: 'Add to playlist…', - icon: 'add-circle-outline', - onPress: () => setStep('pickPlaylist'), - }, - ...extraItems, ]; - const pickItems: ActionSheetItem[] = [ + const pickItems: TrackActionSheetItem[] = [ ...playlists.map((playlist) => ({ key: `playlist-${playlist.id}`, label: playlist.name, icon: 'musical-notes-outline' as const, - onPress: () => { - void addTracksToPlaylist(playlist.id, [track]); - onClose(); - }, + onPress: () => closeAndRun(() => void addTracksToPlaylist(playlist.id, [track])), })), { key: 'new-playlist', - label: 'New playlist…', + label: 'New playlist...', icon: 'add', onPress: () => setStep('newPlaylist'), }, ]; + if (step === 'pickPlaylist') { + return ( + + + {initialStep === 'menu' ? ( + setStep('menu')} /> + ) : null} + {playlists.length === 0 ? ( + + No playlists yet. + + ) : null} + {pickItems.map(({ key, ...item }) => ( + + ))} + + ); + } + + if (step === 'newPlaylist') { + return ( + + + + + setStep('pickPlaylist')}> + + Back + + + + + Create + + + + + ); + } + return ( - <> - - - { - void (async () => { - const playlist = await createPlaylist(name); - await addTracksToPlaylist(playlist.id, [track]); - })(); - onClose(); - }} - onClose={onClose} - /> - + + + {menuItems.map(({ key, ...item }) => ( + + ))} + {extraItems.length > 0 ? ( + <> + + {extraItems.map(({ key, ...item }) => ( + + ))} + + ) : null} + ); } + +function SheetTitle({ title, subtitle }: { title: string; subtitle?: string }) { + return ( + + + {title} + + {subtitle ? ( + + {subtitle} + + ) : null} + + ); +} + +const styles = StyleSheet.create({ + titleBlock: { + marginTop: spacing.xs, + marginBottom: spacing.sm, + gap: 2, + }, + title: { + paddingRight: spacing.lg, + }, + empty: { + paddingVertical: spacing.sm, + }, + input: { + color: colors.textPrimary, + fontFamily: fonts.sans.regular, + fontSize: 16, + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + borderRadius: radius.md, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + backgroundColor: colors.glassBg, + }, + actions: { + flexDirection: 'row', + justifyContent: 'flex-end', + gap: spacing.sm, + marginTop: spacing.lg, + }, + btn: { + paddingHorizontal: spacing.xl, + paddingVertical: spacing.md, + borderRadius: radius.pill, + }, + cancel: { + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + }, + create: { + backgroundColor: colors.accentGlow, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.accent, + }, + createDisabled: { + opacity: 0.4, + }, +}); diff --git a/src/components/library/TrackRow.tsx b/src/components/library/TrackRow.tsx index 587f719..f63ad83 100644 --- a/src/components/library/TrackRow.tsx +++ b/src/components/library/TrackRow.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; -import { View, Pressable, StyleSheet } from 'react-native'; +import { View, Pressable, StyleSheet, type GestureResponderEvent } from 'react-native'; import { Image } from 'expo-image'; +import { Ionicons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { AstraLogo } from '@/components/AstraLogo'; import { FormatBadges } from '@/components/FormatBadge'; @@ -20,6 +21,7 @@ export function TrackRow({ track, onPress, onLongPress, + onOpenActions, showArtist = true, subtitle, active = false, @@ -29,6 +31,8 @@ export function TrackRow({ onPress: () => void; /** Opens the track actions sheet where wired. */ onLongPress?: () => void; + /** Visible trailing affordance for the track actions sheet. */ + onOpenActions?: () => void; /** Hide on album detail where every row shares the artist. */ showArtist?: boolean; /** Overrides the secondary line; useful for artist pages that need album context. */ @@ -44,12 +48,16 @@ export function TrackRow({ const thumbUri = failedArtKey !== artKey ? trackArtworkThumbSource(track) : null; const secondaryText = subtitle ?? (showArtist ? track.artist : null); + const openActions = (event: GestureResponderEvent) => { + event.stopPropagation(); + onOpenActions?.(); + }; const row = ( @@ -103,6 +111,18 @@ export function TrackRow({ {formatDuration(track.duration)} + + {onOpenActions ? ( + [styles.actionsButton, pressed && styles.actionsButtonPressed]} + onPress={openActions} + hitSlop={8} + accessibilityRole="button" + accessibilityLabel={`More actions for ${track.title}`} + > + + + ) : null} ); @@ -186,4 +206,15 @@ const styles = StyleSheet.create({ color: colors.textTertiary, textAlign: 'right', }, + actionsButton: { + width: 34, + height: 34, + flexShrink: 0, + borderRadius: radius.pill, + alignItems: 'center', + justifyContent: 'center', + }, + actionsButtonPressed: { + backgroundColor: colors.glassBg, + }, }); diff --git a/src/components/sheets/AppSheet.tsx b/src/components/sheets/AppSheet.tsx new file mode 100644 index 0000000..d733cb6 --- /dev/null +++ b/src/components/sheets/AppSheet.tsx @@ -0,0 +1,131 @@ +import { useCallback, type ReactNode } from 'react'; +import { Pressable, StyleSheet, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import BottomSheet, { + BottomSheetBackdrop, + BottomSheetView, + type BottomSheetBackdropProps, +} from '@gorhom/bottom-sheet'; +import { Text } from '@/components/Text'; +import { colors, radius, spacing } from '@/theme'; + +export function AppSheet({ onClose, children }: { onClose: () => void; children: ReactNode }) { + const insets = useSafeAreaInsets(); + const renderBackdrop = useCallback( + (props: BottomSheetBackdropProps) => ( + + ), + [] + ); + + return ( + + + {children} + + + ); +} + +export function AppSheetSection({ label }: { label: string }) { + return ( + + {label} + + ); +} + +export interface AppSheetItemProps { + label: string; + icon?: keyof typeof Ionicons.glyphMap; + selected?: boolean; + destructive?: boolean; + onPress: () => void; + trailing?: ReactNode; +} + +export function AppSheetItem({ + label, + icon, + selected, + destructive, + onPress, + trailing, +}: AppSheetItemProps) { + const tint = destructive ? colors.warning : selected ? colors.accentTextStrong : colors.textPrimary; + + return ( + + [styles.item, pressed && styles.itemPressed]} + onPress={onPress} + accessibilityRole="button" + > + {icon ? ( + + ) : null} + + {label} + + {selected ? : null} + + {trailing} + + ); +} + +const styles = StyleSheet.create({ + sheetBg: { + backgroundColor: colors.bgSecondary, + borderTopLeftRadius: radius.lg, + borderTopRightRadius: radius.lg, + }, + handle: { + backgroundColor: colors.glassBorder, + width: 38, + }, + content: { + paddingHorizontal: spacing.lg, + paddingTop: spacing.xs, + }, + section: { + color: colors.textTertiary, + letterSpacing: 1, + marginTop: spacing.md, + marginBottom: spacing.xs, + }, + itemRow: { + flexDirection: 'row', + alignItems: 'center', + }, + item: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + paddingVertical: spacing.md, + }, + itemPressed: { + opacity: 0.6, + }, + itemLabel: { + flex: 1, + }, +}); + +export default AppSheet;