From ed5811017113872fec3538139997f9fbc588ca45 Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:42:57 -0400 Subject: [PATCH] remove ripple --- eslint.config.js | 9 ++ .../queue/QueueContentView.kt | 20 ++-- .../astralibraryscanner/queue/QueuePalette.kt | 4 +- modules/astra-library-scanner/queue.ts | 4 +- package.json | 1 + scripts/run-release-tests.mjs | 1 + src/app/(tabs)/eq.tsx | 20 ++-- src/app/(tabs)/index.tsx | 58 +++++------ src/app/(tabs)/library/artist/[name].tsx | 13 +-- src/app/(tabs)/library/index.tsx | 22 ++--- src/app/(tabs)/library/playlist/[id].tsx | 15 ++- .../(tabs)/library/playlist/edit-dynamic.tsx | 55 +++++------ src/app/(tabs)/stats.tsx | 14 ++- src/app/desktop-remote.tsx | 49 +++++----- src/app/desktop-remote/scan.tsx | 12 +-- src/app/desktop-sync.tsx | 25 +++-- src/app/eq/import.tsx | 13 ++- src/app/eq/scan.tsx | 16 ++-- src/app/lastfm/edit.tsx | 20 ++-- src/app/lastfm/index.tsx | 24 +++-- src/app/settings/haptics-lab.tsx | 18 ++-- src/app/settings/playback.tsx | 11 +-- src/app/settings/troubleshooting.tsx | 11 +-- src/app/signal/import.tsx | 13 ++- src/app/signal/index.tsx | 17 ++-- src/app/signal/scan.tsx | 25 +++-- src/app/sources/edit.tsx | 20 ++-- src/app/sources/index.tsx | 12 +-- src/components/AppPressable.tsx | 95 +++++++++++++++++++ src/components/MiniPlayer.tsx | 60 ++++++------ src/components/PlaybackTargetPicker.tsx | 9 +- src/components/ScreenHeader.tsx | 17 ++-- src/components/SegmentedControl.tsx | 10 +- src/components/TabBar.tsx | 4 +- src/components/appPressableFeedback.test.mts | 45 +++++++++ src/components/appPressableFeedback.ts | 55 +++++++++++ src/components/dialogs/AppDialog.tsx | 10 +- src/components/eq/BandConsole.tsx | 30 +++--- src/components/eq/BandDetailPanel.tsx | 8 +- src/components/eq/BandStrip.tsx | 12 +-- src/components/eq/EQPresetNameSheet.tsx | 12 +-- src/components/eq/EQPresetPreviewSheet.tsx | 12 +-- src/components/eq/EQPresetQrSheet.tsx | 8 +- src/components/eq/EQSlider.tsx | 12 +-- src/components/eq/EQValueEditSheet.tsx | 12 +-- .../eq/PresetDeviceAssignmentSheet.tsx | 21 ++-- src/components/eq/PresetSheet.tsx | 17 ++-- src/components/eq/SavePresetSheet.tsx | 12 +-- src/components/library/AlbumGridItem.tsx | 8 +- src/components/library/AlbumRow.tsx | 8 +- src/components/library/ArtistGridItem.tsx | 8 +- .../library/ArtistImageSearchSheet.tsx | 15 ++- .../library/ArtistImageSweepStatus.tsx | 11 +-- src/components/library/ArtistRow.tsx | 8 +- src/components/library/CollapsingDetail.tsx | 28 +++--- src/components/library/EmptyLibrary.tsx | 12 +-- src/components/library/FoldersView.tsx | 34 ++++--- src/components/library/LibraryContextBar.tsx | 17 ++-- src/components/library/PlaylistRow.tsx | 10 +- .../ScanNotificationPermissionCard.tsx | 10 +- src/components/library/ScanProgress.tsx | 11 +-- src/components/library/SelectionActionBar.tsx | 10 +- src/components/library/TrackRow.tsx | 16 ++-- .../listening/ListeningPreviewCard.tsx | 11 +-- src/components/lyrics/LyricsBand.tsx | 15 ++- src/components/lyrics/LyricsLine.tsx | 9 +- src/components/lyrics/LyricsView.tsx | 19 ++-- .../ArtistImageDisclosurePrompt.tsx | 11 +-- src/components/onboarding/ArtistImageStep.tsx | 11 +-- .../onboarding/NotificationStep.tsx | 11 +-- src/components/onboarding/OnboardingFlow.tsx | 29 +++--- src/components/player/NowPlayingOverlay.tsx | 69 +++++++------- src/components/player/SleepTimerControls.tsx | 27 +++--- src/components/queue/RemoteQueueSheet.tsx | 13 ++- src/components/search/QuickSearchOverlay.tsx | 24 ++--- src/components/settings/AccentColorSheet.tsx | 16 ++-- src/components/settings/AccentSwatchRow.tsx | 15 ++- src/components/settings/ScopeStyleCards.tsx | 11 +-- src/components/settings/SettingsPanels.tsx | 32 +++---- .../settings/SettingsSectionScaffold.tsx | 8 +- src/components/sheets/ActionSheet.tsx | 9 +- src/components/sheets/AppSheet.tsx | 9 +- src/components/sheets/PlaylistPickerSheet.tsx | 12 +-- src/components/sheets/TextPromptModal.tsx | 11 +-- .../signal/SignalResolutionPanel.tsx | 35 ++++--- src/components/sync/SyncConflictPrompt.tsx | 19 ++-- src/theme/palettes.ts | 8 +- src/theme/resolve.ts | 2 - src/theme/ripple.ts | 53 ----------- 89 files changed, 843 insertions(+), 835 deletions(-) create mode 100644 src/components/AppPressable.tsx create mode 100644 src/components/appPressableFeedback.test.mts create mode 100644 src/components/appPressableFeedback.ts delete mode 100644 src/theme/ripple.ts diff --git a/eslint.config.js b/eslint.config.js index ba708ed..bf4872b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,5 +6,14 @@ module.exports = defineConfig([ expoConfig, { ignores: ["dist/*"], + rules: { + "no-restricted-syntax": [ + "error", + { + selector: "JSXAttribute[name.name='android_ripple']", + message: "Use AppPressable feedback instead of the Android radial ripple.", + }, + ], + }, } ]); diff --git a/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/queue/QueueContentView.kt b/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/queue/QueueContentView.kt index 2d4059b..7aedc77 100644 --- a/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/queue/QueueContentView.kt +++ b/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/queue/QueueContentView.kt @@ -9,7 +9,8 @@ import android.graphics.Path import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.graphics.drawable.GradientDrawable -import android.graphics.drawable.RippleDrawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.StateListDrawable import android.view.Gravity import android.view.MotionEvent import android.view.View @@ -1026,11 +1027,18 @@ class QueueContentView( fun setSurfaceColor(color: Int) { dividerColor = palette.divider - background = RippleDrawable( - ColorStateList.valueOf(palette.ripple), - ColorDrawable(color), - null, - ) + background = StateListDrawable().apply { + addState( + intArrayOf(android.R.attr.state_pressed), + LayerDrawable( + arrayOf( + ColorDrawable(color), + ColorDrawable(palette.pressOverlay), + ), + ), + ) + addState(intArrayOf(), ColorDrawable(color)) + } } override fun dispatchDraw(canvas: Canvas) { diff --git a/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/queue/QueuePalette.kt b/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/queue/QueuePalette.kt index 88bdf78..c4de8ee 100644 --- a/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/queue/QueuePalette.kt +++ b/modules/astra-library-scanner/android/src/main/java/expo/modules/astralibraryscanner/queue/QueuePalette.kt @@ -9,7 +9,7 @@ data class QueuePalette( val selectedSurface: Int = Color.rgb(57, 62, 75), val nowPlayingSurface: Int = Color.rgb(31, 34, 43), val divider: Int = Color.rgb(72, 72, 82), - val ripple: Int = Color.argb(38, 140, 162, 208), + val pressOverlay: Int = Color.argb(20, 140, 162, 208), val text: Int = Color.WHITE, val textSecondary: Int = Color.rgb(190, 190, 200), val textTertiary: Int = Color.rgb(135, 135, 148), @@ -30,7 +30,7 @@ data class QueuePalette( selectedSurface = color("selectedSurface", defaults.selectedSurface), nowPlayingSurface = color("nowPlayingSurface", defaults.nowPlayingSurface), divider = color("divider", defaults.divider), - ripple = color("ripple", defaults.ripple), + pressOverlay = color("pressOverlay", defaults.pressOverlay), text = color("text", defaults.text), textSecondary = color("textSecondary", defaults.textSecondary), textTertiary = color("textTertiary", defaults.textTertiary), diff --git a/modules/astra-library-scanner/queue.ts b/modules/astra-library-scanner/queue.ts index b02dd16..9a2d7dc 100644 --- a/modules/astra-library-scanner/queue.ts +++ b/modules/astra-library-scanner/queue.ts @@ -13,7 +13,7 @@ export interface NativeQueuePalette { selectedSurface: number; nowPlayingSurface: number; divider: number; - ripple: number; + pressOverlay: number; text: number; textSecondary: number; textTertiary: number; @@ -93,7 +93,7 @@ export function toNativeQueuePalette(colors: Palette): NativeQueuePalette { selectedSurface: nativeColor(colors.glassHighlight), nowPlayingSurface: nativeColor(colors.glassBg), divider: nativeColor(colors.glassBorder), - ripple: nativeColor(colors.ripple), + pressOverlay: nativeColor(colors.glassHighlight), text: nativeColor(colors.textPrimary), textSecondary: nativeColor(colors.textSecondary), textTertiary: nativeColor(colors.textTertiary), diff --git a/package.json b/package.json index 0edb5e1..743ae03 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "test:app-dialog": "node --experimental-strip-types --test src/components/dialogs/dialogQueue.test.mts", "test:home-greeting": "node --experimental-strip-types --test src/home/homeGreeting.test.mts src/home/homeLayout.test.mts", "test:theme": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/theme/accentColors.test.mts src/theme/artworkAccentMath.test.mts", + "test:press-feedback": "node --experimental-strip-types --test src/components/appPressableFeedback.test.mts", "test:session": "node --experimental-strip-types --test src/session/sessionState.test.mts src/session/playbackMaterialization.test.mts", "test:library-scan": "node --experimental-strip-types --test src/library/scanCancellation.test.mts", "test:release-config": "node --experimental-strip-types --test plugins/withAstraAndroidRelease.test.mjs scripts/release/android-release.test.mjs src/release/buildInfo.test.mts", diff --git a/scripts/run-release-tests.mjs b/scripts/run-release-tests.mjs index 5da0685..8d13163 100644 --- a/scripts/run-release-tests.mjs +++ b/scripts/run-release-tests.mjs @@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process'; const TEST_SCRIPTS = [ 'test:release-config', + 'test:press-feedback', 'test:queue-actions', 'test:desktop-remote', 'test:dynamic-playlists', diff --git a/src/app/(tabs)/eq.tsx b/src/app/(tabs)/eq.tsx index 31ffb81..0ca4fec 100644 --- a/src/app/(tabs)/eq.tsx +++ b/src/app/(tabs)/eq.tsx @@ -1,7 +1,6 @@ import { useCallback, useState } from 'react'; import { InteractionManager, - Pressable, StyleSheet, View, useWindowDimensions @@ -43,7 +42,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { hapticForToggle } from '@/lib/hapticCatalog'; import { playHaptic } from '@/lib/haptics'; import { useAppForeground } from '@/lib/useAppForeground'; @@ -99,7 +98,6 @@ const BAND_TYPES: EQBandType[] = ['lowshelf', 'peaking', 'highshelf', 'highpass' export default function EQScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const pathname = usePathname(); @@ -309,7 +307,7 @@ export default function EQScreen() { }; const presetRowEl = ( - setSheet('preset')} > @@ -317,7 +315,7 @@ export default function EQScreen() { {presetName} - + ); const modeSwitcherEl = ( @@ -436,7 +434,7 @@ export default function EQScreen() { onValuePress={() => setEditingValue('preamp')} /> - { playHaptic(hapticForToggle(!eq.enabled)); @@ -451,7 +449,7 @@ export default function EQScreen() { {eq.enabled ? 'EQ on' : 'EQ off'} - + ); @@ -470,12 +468,12 @@ export default function EQScreen() { - setSheet('save')} hitSlop={8}> + setSheet('save')} hitSlop={8}> - - setSheet('overflow')} hitSlop={8}> + + setSheet('overflow')} hitSlop={8}> - + diff --git a/src/app/(tabs)/index.tsx b/src/app/(tabs)/index.tsx index 02f16ac..8a70a70 100644 --- a/src/app/(tabs)/index.tsx +++ b/src/app/(tabs)/index.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { AppState, - Pressable, ScrollView, StyleSheet, View, @@ -33,7 +32,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { useLibraryStore } from '@/stores/libraryStore'; import { usePlaylistStore } from '@/stores/playlistStore'; import { usePlayerStore } from '@/stores/playerStore'; @@ -144,7 +143,6 @@ function HomeMasthead({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const [clockNow, setClockNow] = useState(() => new Date()); const [greeting, setGreeting] = useState(() => chooseHomeGreeting(null, new Date())); @@ -190,9 +188,9 @@ function HomeMasthead({ }, [mode]); const searchButton = ( - - + ); const scanButton = ( - - + ); const utilityButtons = ( @@ -265,7 +263,6 @@ function SectionHeader({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( @@ -279,12 +276,12 @@ function SectionHeader({ ) : null} {onActionPress && actionLabel ? ( - + {actionLabel} - + ) : null} ); @@ -355,9 +352,8 @@ function RecentlyAddedAlbum({ onPress: () => void; }) { const styles = useStyles(); - const ripple = useRipple(); return ( - + {album.album} @@ -365,7 +361,7 @@ function RecentlyAddedAlbum({ {album.artist} - + ); } @@ -390,7 +386,6 @@ function RandomSpotlightCard({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const disabled = !hasTracks; const title = spotlight.kind === 'album' ? spotlight.album.album : spotlight.artist.artist; const label = spotlight.kind === 'album' ? 'RANDOM ALBUM' : 'RANDOM ARTIST'; @@ -403,9 +398,9 @@ function RandomSpotlightCard({ }; return ( - - - - + - - + runAction(event, onReroll)} @@ -462,11 +457,11 @@ function RandomSpotlightCard({ accessibilityLabel="Pick another random album or artist" > - + - + ); } @@ -481,7 +476,6 @@ function EmptyHomeCard({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const fatal = status === 'fatalUserData'; const rebuilding = status === 'rebuilding'; const degraded = status === 'degraded'; @@ -517,8 +511,8 @@ function EmptyHomeCard({ ) : null} - {fatal ? 'Troubleshooting' : 'Folder settings'} - + ); } diff --git a/src/app/(tabs)/library/artist/[name].tsx b/src/app/(tabs)/library/artist/[name].tsx index 57e1915..5aa93cb 100644 --- a/src/app/(tabs)/library/artist/[name].tsx +++ b/src/app/(tabs)/library/artist/[name].tsx @@ -4,7 +4,6 @@ import { type ComponentProps } from 'react'; import { - Pressable, ScrollView, StyleSheet, View @@ -30,7 +29,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import type { Palette } from '@/theme/palettes'; import { usePlayerStore } from '@/stores/playerStore'; import { useSettingsStore } from '@/stores/settingsStore'; @@ -486,7 +485,6 @@ function SectionHeader({ onPress?: () => void; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); return ( @@ -499,12 +497,12 @@ function SectionHeader({ {onPress ? ( - + See all - + ) : null} ); @@ -518,7 +516,6 @@ function AlbumRail({ onAlbumPress: (album: ArtistAlbum) => void; }) { const styles = useStyles(); - const ripple = useRipple(); return ( {albums.map((album) => ( - onAlbumPress(album)} @@ -552,7 +549,7 @@ function AlbumRail({ .filter(Boolean) .join(' - ')} - + ))} ); diff --git a/src/app/(tabs)/library/index.tsx b/src/app/(tabs)/library/index.tsx index 91dc06f..76ec5b2 100644 --- a/src/app/(tabs)/library/index.tsx +++ b/src/app/(tabs)/library/index.tsx @@ -11,7 +11,6 @@ import { ActivityIndicator, BackHandler, View, - Pressable, StyleSheet, useWindowDimensions } from 'react-native'; @@ -64,7 +63,7 @@ import { import { spacing } from '@/theme'; import { motion } from '@/theme/motion'; import { useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { useShellLayout } from '@/navigation/useShellLayout'; import { useTabReselect } from '@/navigation/useTabReselect'; import { shouldAnimateScrollToTop } from '@/navigation/scrollToTopBehavior'; @@ -166,7 +165,6 @@ function libraryChromeHeight(options: { export default function LibraryScreen() { const colors = useColors(); - const ripple = useRipple(); const router = useRouter(); const viewMode = useLibraryStore((s) => s.viewMode); const setViewMode = useLibraryStore((s) => s.setViewMode); @@ -684,7 +682,7 @@ export default function LibraryScreen() { {/* The rail already names this destination, so in landscape there is no title row to carry search and it moves in beside the switcher. */} {!showScreenTitle ? ( - openQuickSearch()} @@ -692,7 +690,7 @@ export default function LibraryScreen() { accessibilityLabel="Search library" > - + ) : null} @@ -717,15 +715,15 @@ export default function LibraryScreen() { {selectedIds.size} selected - + Cancel - + ) : sortable ? ( - setSortSheetOpen(true)} accessibilityRole="button" @@ -733,10 +731,10 @@ export default function LibraryScreen() { > {sortLabel} - + {activeLayout && activeLayoutLabel ? ( - setLayoutSheetOpen(true)} @@ -748,7 +746,7 @@ export default function LibraryScreen() { size={18} color={colors.textSecondary} /> - + ) : null} ) : null} diff --git a/src/app/(tabs)/library/playlist/[id].tsx b/src/app/(tabs)/library/playlist/[id].tsx index 4880809..5a2e050 100644 --- a/src/app/(tabs)/library/playlist/[id].tsx +++ b/src/app/(tabs)/library/playlist/[id].tsx @@ -5,7 +5,6 @@ import { } from 'react'; import { View, - Pressable, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; @@ -27,7 +26,7 @@ import { showAppDialog } from '@/components/dialogs/AppDialog'; import { CollapsingHeader, useDetailCollapse } from '@/components/library/CollapsingDetail'; import { spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { usePlaylistStore } from '@/stores/playlistStore'; import { usePlayerStore } from '@/stores/playerStore'; import { playLibraryQuery } from '@/audio/playbackController'; @@ -56,11 +55,10 @@ function basename(path: string): string { function MissingRow({ entry, onLongPress }: { entry: PlaylistTrackEntry; onLongPress: () => void }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); return ( - { @@ -78,7 +76,7 @@ function MissingRow({ entry, onLongPress }: { entry: PlaylistTrackEntry; onLongP - + ); } @@ -87,7 +85,6 @@ type Prompt = { kind: 'rename'; playlist: Playlist } | null; export default function PlaylistScreen() { const sceneBottomInset = useSceneBottomInset(); const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const { id } = useLocalSearchParams<{ id: string }>(); @@ -328,7 +325,7 @@ export default function PlaylistScreen() { heroMeta={{meta}} heroExtra={ isDynamic && playlistId != null ? ( - router.push({ @@ -343,7 +340,7 @@ export default function PlaylistScreen() { Rules - + ) : null } disabled={playable.length === 0} diff --git a/src/app/(tabs)/library/playlist/edit-dynamic.tsx b/src/app/(tabs)/library/playlist/edit-dynamic.tsx index 84b5f9a..37f52b0 100644 --- a/src/app/(tabs)/library/playlist/edit-dynamic.tsx +++ b/src/app/(tabs)/library/playlist/edit-dynamic.tsx @@ -6,7 +6,6 @@ import { import { KeyboardAvoidingView, Platform, - Pressable, ScrollView, StyleSheet, TextInput, @@ -27,7 +26,7 @@ import { } from '@/components/sheets/AppSheet'; import { fonts, fontSize, radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { usePlaylistStore } from '@/stores/playlistStore'; import { DYNAMIC_PLAYLIST_PRESETS, @@ -329,16 +328,15 @@ function DraftActions({ onApply: () => void; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); return ( - + Cancel - - + Apply - + ); } @@ -532,7 +530,6 @@ function ConditionEditorSheet({ onApply: () => void; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const draft = target.draft; const error = validateCondition(draft); @@ -540,7 +537,7 @@ function ConditionEditorSheet({ return ( - + FIELD @@ -548,7 +545,7 @@ function ConditionEditorSheet({ {fieldLabel(draft)} - + @@ -572,12 +569,12 @@ function ConditionEditorSheet({ {target.mode === 'edit' ? ( - + Remove - + ) : ( )} @@ -717,12 +714,11 @@ function FilterCard({ onRemove: () => void; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const option = fieldOptionForKey(getConditionFieldKey(condition)); return ( - + @@ -734,7 +730,7 @@ function FilterCard({ {fieldGroupLabel(condition)} - - - + + ); } export default function DynamicPlaylistEditorScreen() { const sceneBottomInset = useSceneBottomInset(); const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const { id } = useLocalSearchParams<{ id?: string }>(); @@ -1053,7 +1048,7 @@ export default function DynamicPlaylistEditorScreen() { > - router.back()} hitSlop={8} style={styles.headerButton} @@ -1061,7 +1056,7 @@ export default function DynamicPlaylistEditorScreen() { accessibilityLabel="Back" > - + {isEditing ? 'Edit dynamic playlist' : 'New dynamic playlist'} @@ -1097,7 +1092,7 @@ export default function DynamicPlaylistEditorScreen() { contentContainerStyle={styles.presetRow} > {DYNAMIC_PLAYLIST_PRESETS.map((preset) => ( - applyPreset(preset.rules)} @@ -1107,7 +1102,7 @@ export default function DynamicPlaylistEditorScreen() { {preset.label} - + ))} @@ -1117,12 +1112,12 @@ export default function DynamicPlaylistEditorScreen() { FILTERS - openFieldPicker('new')} accessibilityRole="button"> + openFieldPicker('new')} accessibilityRole="button"> Add filter - + {rules.conditions.length === 0 ? ( @@ -1150,7 +1145,7 @@ export default function DynamicPlaylistEditorScreen() { ORDER - + @@ -1161,7 +1156,7 @@ export default function DynamicPlaylistEditorScreen() { - + @@ -1180,7 +1175,7 @@ export default function DynamicPlaylistEditorScreen() { {status.label} - setSheet({ kind: 'preview' })} @@ -1190,8 +1185,8 @@ export default function DynamicPlaylistEditorScreen() { Preview - - + {isSaving ? 'Saving' : 'Save'} - + {renderSheet()} diff --git a/src/app/(tabs)/stats.tsx b/src/app/(tabs)/stats.tsx index 4034b5c..46b51ad 100644 --- a/src/app/(tabs)/stats.tsx +++ b/src/app/(tabs)/stats.tsx @@ -22,7 +22,7 @@ import { useListeningStatsStore } from '@/stores/listeningStatsStore'; import { playLibraryQuery } from '@/audio/playbackController'; import { fonts, radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import type { ListeningStatsCategory, ListeningStatsDashboard, @@ -136,13 +136,12 @@ function RankingRow({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const copy = rankingCopy(item, category); const art = listeningArtworkSource(item, true); return ( - - + ); } @@ -206,16 +205,15 @@ function EmptyState({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( {title} {body} {action && onAction ? ( - + {action} - + ) : null} ); diff --git a/src/app/desktop-remote.tsx b/src/app/desktop-remote.tsx index 024a9eb..4a29286 100644 --- a/src/app/desktop-remote.tsx +++ b/src/app/desktop-remote.tsx @@ -7,7 +7,6 @@ import { ActivityIndicator, KeyboardAvoidingView, Platform, - Pressable, ScrollView, StyleSheet, TextInput, @@ -28,7 +27,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { isWideWindow, WIDE_MIN_WIDTH } from '@/theme/adaptive'; import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore'; import { usePlayerUiStore } from '@/stores/playerUiStore'; @@ -192,10 +191,9 @@ function DiscoveredDesktopRow({ desktop, onPair, disabled }: { disabled: boolean; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); return ( - onPair(desktop)} disabled={disabled} @@ -217,13 +215,12 @@ function DiscoveredDesktopRow({ desktop, onPair, disabled }: { - + ); } export default function DesktopRemoteScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const returnToTabs = useReturnToTabs(); @@ -436,7 +433,7 @@ export default function DesktopRemoteScreen() { maxLength={6} textContentType="oneTimeCode" /> - Confirm PIN - + ) : null} @@ -462,12 +459,12 @@ export default function DesktopRemoteScreen() { Open Astra Desktop settings, enable Phone Remote, then scan or paste the pairing link. - router.push('/desktop-remote/scan' as never)}> + router.push('/desktop-remote/scan' as never)}> Scan QR - + - void pairFromInput(pairingLink)} @@ -487,7 +484,7 @@ export default function DesktopRemoteScreen() { Pair from link - + @@ -507,7 +504,7 @@ export default function DesktopRemoteScreen() { autoCorrect={false} keyboardType="url" /> - Request secure PIN - + {pinPairing ? ( @@ -560,9 +557,9 @@ export default function DesktopRemoteScreen() { > - router.back()} hitSlop={12}> + router.back()} hitSlop={12}> - + DESKTOP REMOTE @@ -571,14 +568,14 @@ export default function DesktopRemoteScreen() { {remoteSource} - void reconnect()} hitSlop={12} accessibilityLabel="Reconnect to desktop" > - + @@ -619,7 +616,7 @@ export default function DesktopRemoteScreen() { - { // The player is an overlay, not a route: open it and pop this @@ -633,17 +630,17 @@ export default function DesktopRemoteScreen() { Open Now Playing - - void reconnect()}> + + void reconnect()}> Reconnect - - + + Forget desktop - + @@ -673,12 +670,12 @@ export default function DesktopRemoteScreen() { ) : ( <> - router.back()} hitSlop={8}> + router.back()} hitSlop={8}> Settings - + {renderSetup()} diff --git a/src/app/desktop-remote/scan.tsx b/src/app/desktop-remote/scan.tsx index 287a01d..7d98439 100644 --- a/src/app/desktop-remote/scan.tsx +++ b/src/app/desktop-remote/scan.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import { ActivityIndicator, - Pressable, StyleSheet, View } from 'react-native'; @@ -19,12 +18,11 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore'; export default function DesktopRemoteScanScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const pairFromInput = useDesktopRemoteStore((s) => s.pairFromInput); @@ -44,12 +42,12 @@ export default function DesktopRemoteScanScreen() { return ( - router.back()} hitSlop={8}> + router.back()} hitSlop={8}> Desktop Remote - + @@ -64,11 +62,11 @@ export default function DesktopRemoteScanScreen() { Camera access is needed to scan the desktop pairing QR. - void requestPermission()}> + void requestPermission()}> Allow camera - + ) : ( diff --git a/src/app/desktop-sync.tsx b/src/app/desktop-sync.tsx index d91d019..ba8b375 100644 --- a/src/app/desktop-sync.tsx +++ b/src/app/desktop-sync.tsx @@ -7,7 +7,6 @@ import { useEffect, useState } from 'react'; import { ActivityIndicator, - Pressable, ScrollView, StyleSheet, View, @@ -20,7 +19,7 @@ import { HapticSwitch } from '@/components/HapticSwitch'; import { SyncConflictDetails } from '@/components/sync/SyncConflictDetails'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { formatRelativeTime } from '@/lib/format'; import { getDesktopRemoteConnection } from '@/services/desktopRemoteCredentials'; import { useDesktopSyncStore } from '@/stores/desktopSyncStore'; @@ -68,7 +67,6 @@ function ConflictCard({ onResolve: (resolution: DesktopSyncConflictResolution) => void; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const [selectedResolution, setSelectedResolution] = useState(null); const desktopSnapshot = syncPlaylistToSnapshot(conflict.remote); @@ -87,7 +85,7 @@ function ConflictCard({ {options.map((resolution) => ( - setSelectedResolution(resolution)} > {RESOLUTION_LABELS[resolution]} - + ))} @@ -116,7 +114,7 @@ function ConflictCard({ previewResolution={selectedResolution} /> - selectedResolution ? onResolve(selectedResolution) : undefined} @@ -124,7 +122,7 @@ function ConflictCard({ Confirm - + ); @@ -132,7 +130,6 @@ function ConflictCard({ export default function DesktopSyncScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const status = useDesktopSyncStore((s) => s.status); @@ -175,12 +172,12 @@ export default function DesktopSyncScreen() { return ( - router.back()} hitSlop={8}> + router.back()} hitSlop={8}> Settings - + @@ -204,7 +201,7 @@ export default function DesktopSyncScreen() { Sync uses the same pairing as the Desktop Remote. Pair this phone with Astra Desktop once and both features work. - router.push('/desktop-remote' as never)} > @@ -212,7 +209,7 @@ export default function DesktopSyncScreen() { Pair with a desktop - + ) : ( <> @@ -228,7 +225,7 @@ export default function DesktopSyncScreen() { : 'Not synced yet'} - void syncNow()} @@ -242,7 +239,7 @@ export default function DesktopSyncScreen() { Sync now - + {summaryLine ? ( diff --git a/src/app/eq/import.tsx b/src/app/eq/import.tsx index 39141af..a70c4a5 100644 --- a/src/app/eq/import.tsx +++ b/src/app/eq/import.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams } from 'expo-router'; import { useReturnToTabs } from '@/navigation/returnToTabs'; @@ -11,12 +11,11 @@ import { genEqId } from '@/audio/eqPresets'; import { useEQStore } from '@/stores/eqStore'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import type { EQPreset } from '@/types/audio'; export default function EQPresetImportScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const returnToTabs = useReturnToTabs(); const importPreset = useEQStore((state) => state.importPreset); @@ -39,21 +38,21 @@ export default function EQPresetImportScreen() { return ( - + Equalizer - + This link does not contain a valid Astra EQ preset. - + Go to Equalizer - + ); diff --git a/src/app/eq/scan.tsx b/src/app/eq/scan.tsx index 50ff7b4..a19a562 100644 --- a/src/app/eq/scan.tsx +++ b/src/app/eq/scan.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { - Pressable, StyleSheet, View } from 'react-native'; @@ -23,12 +22,11 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import type { EQPreset } from '@/types/audio'; export default function EQPresetScanScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const returnToTabs = useReturnToTabs(); @@ -53,12 +51,12 @@ export default function EQPresetScanScreen() { return ( - router.back()} hitSlop={8}> + router.back()} hitSlop={8}> Equalizer - + @@ -71,11 +69,11 @@ export default function EQPresetScanScreen() { Camera access is needed to scan EQ preset QR codes. - void requestPermission()}> + void requestPermission()}> Allow camera - + ) : ( @@ -94,11 +92,11 @@ export default function EQPresetScanScreen() { {error} - setError(null)}> + setError(null)}> Scan again - + ) : null} diff --git a/src/app/lastfm/edit.tsx b/src/app/lastfm/edit.tsx index 9689292..8ae4b06 100644 --- a/src/app/lastfm/edit.tsx +++ b/src/app/lastfm/edit.tsx @@ -3,7 +3,6 @@ import { ActivityIndicator, KeyboardAvoidingView, Platform, - Pressable, ScrollView, StyleSheet, TextInput, @@ -19,7 +18,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { useLastFmSettingsStore } from '@/stores/lastFmSettingsStore'; import type { LastFmScrobbleProtocol } from '@/types/lastFm'; @@ -104,7 +103,6 @@ function Field({ export default function LastFmEditScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const { id } = useLocalSearchParams<{ id?: string }>(); @@ -202,12 +200,12 @@ export default function LastFmEditScreen() { behavior={Platform.OS === 'ios' ? 'padding' : undefined} > - + {backLabel} - + {step === 'type' ? ( @@ -221,7 +219,7 @@ export default function LastFmEditScreen() { {PROTOCOL_OPTIONS.map((option) => ( - chooseProtocol(option.protocol)} @@ -241,7 +239,7 @@ export default function LastFmEditScreen() { - + ))} @@ -295,7 +293,7 @@ export default function LastFmEditScreen() { ) : null} - void onSave()} disabled={!canSubmit || busy} @@ -307,15 +305,15 @@ export default function LastFmEditScreen() { {editing ? 'Save' : 'Add destination'} )} - + {editing ? ( - + Remove destination - + ) : null} )} diff --git a/src/app/lastfm/index.tsx b/src/app/lastfm/index.tsx index c8ffbbd..bc42e52 100644 --- a/src/app/lastfm/index.tsx +++ b/src/app/lastfm/index.tsx @@ -1,6 +1,5 @@ import { useEffect } from 'react'; import { - Pressable, StyleSheet, View } from 'react-native'; @@ -21,7 +20,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { useLastFmSettingsStore } from '@/stores/lastFmSettingsStore'; import { requestLastFmFlush } from '@/services/lastfm'; import type { LastFmProfileStatus } from '@/types/lastFm'; @@ -51,7 +50,6 @@ function customStatusLine(profile: LastFmProfileStatus): { text: string; tone: ' export default function LastFmScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const header = useScreenHeader({ actionCount: 1 }); @@ -137,16 +135,16 @@ export default function LastFmScreen() { {profile.username} ) : null} - confirmDisconnect(profile)} hitSlop={8} accessibilityLabel="Disconnect Last.fm" > - + ) : ( - connectOfficial(profile)} accessibilityRole="button" @@ -155,7 +153,7 @@ export default function LastFmScreen() { Connect - + )} ); @@ -164,7 +162,7 @@ export default function LastFmScreen() { const renderCustom = (profile: LastFmProfileStatus) => { const line = customStatusLine(profile); return ( - router.push({ pathname: '/lastfm/edit', params: { id: profile.id } })} @@ -200,7 +198,7 @@ export default function LastFmScreen() { ) : ( )} - + ); }; @@ -248,12 +246,12 @@ export default function LastFmScreen() { ) : null} {status && status.pendingScrobbles > 0 ? ( - requestLastFmFlush()}> + requestLastFmFlush()}> Retry {status.pendingScrobbles} queued now - + ) : null} - router.push('/lastfm/edit')}> + router.push('/lastfm/edit')}> Add destination - + A scrobble is sent once a track plays past half its length (or 4 minutes). Tracks under 30 diff --git a/src/app/settings/haptics-lab.tsx b/src/app/settings/haptics-lab.tsx index 002a6c1..c4117d3 100644 --- a/src/app/settings/haptics-lab.tsx +++ b/src/app/settings/haptics-lab.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { SettingsCard, SettingsSectionLabel, @@ -8,7 +8,7 @@ import { import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { playHaptic, type HapticEvent } from '@/lib/haptics'; import { HAPTIC_RECIPE_SECTIONS, @@ -60,7 +60,6 @@ function yesNo(value: boolean): string { export default function HapticsLabScreen() { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const [capabilities, setCapabilities] = useState(() => AstraHaptics.getCapabilities() ); @@ -97,14 +96,14 @@ export default function HapticsLabScreen() { label="Touch feedback enabled" value={yesNo(capabilities.touchFeedbackEnabled)} /> - Refresh capabilities - + SEMANTIC VOCABULARY @@ -259,10 +258,9 @@ function AuditionButton({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( - {label} - + ); } diff --git a/src/app/settings/playback.tsx b/src/app/settings/playback.tsx index aa6aa7a..d1976a6 100644 --- a/src/app/settings/playback.tsx +++ b/src/app/settings/playback.tsx @@ -1,5 +1,5 @@ import { SleepTimerControls } from '@/components/player/SleepTimerControls'; -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { AstraLibraryData } from '../../../modules/astra-library-scanner'; import { @@ -17,13 +17,12 @@ import { import { useSettingsStore } from '@/stores/settingsStore'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { notifyListeningHistoryChanged } from '@/listeningStats/events'; export default function PlaybackSettingsScreen() { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const historyEnabled = useSettingsStore((s) => s.listeningHistoryEnabled); const setHistoryEnabled = useSettingsStore((s) => s.setListeningHistoryEnabled); @@ -83,8 +82,8 @@ export default function PlaybackSettingsScreen() { }} /> - - + ); diff --git a/src/app/settings/troubleshooting.tsx b/src/app/settings/troubleshooting.tsx index 60c6dff..c5a29d2 100644 --- a/src/app/settings/troubleshooting.tsx +++ b/src/app/settings/troubleshooting.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { ActivityIndicator, Pressable, View } from 'react-native'; +import { ActivityIndicator, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { Text } from '@/components/Text'; @@ -21,7 +21,7 @@ import { useLibraryStore } from '@/stores/libraryStore'; import { useOnboardingStore } from '@/stores/onboardingStore'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; type ActionKey = 'scan' | 'rebuild' | 'lyrics' | 'waveform' | 'onboarding'; @@ -255,11 +255,10 @@ function MaintenanceRow({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( - {description} {running ? : } - + ); } diff --git a/src/app/signal/import.tsx b/src/app/signal/import.tsx index aeb7c12..65ec48b 100644 --- a/src/app/signal/import.tsx +++ b/src/app/signal/import.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { Screen } from '@/components/Screen'; @@ -8,12 +8,11 @@ import { SignalResultCard } from '@/components/signal/SignalResultCard'; import { decodeTrackSignalLink, SIGNAL_LINK_PREFIX } from '@/audio/signalShare'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import type { SignalPayload } from '@boof2015/astra-signal'; export default function SignalImportScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const { data } = useLocalSearchParams<{ data?: string }>(); @@ -32,12 +31,12 @@ export default function SignalImportScreen() { return ( - + Signal - + {payload ? ( @@ -46,11 +45,11 @@ export default function SignalImportScreen() { This link does not contain a valid Astra Signal. - + Go to Signal - + )} diff --git a/src/app/signal/index.tsx b/src/app/signal/index.tsx index de11edc..637b7a6 100644 --- a/src/app/signal/index.tsx +++ b/src/app/signal/index.tsx @@ -1,5 +1,5 @@ import { useMemo, useRef } from 'react'; -import { Pressable, Share, StyleSheet, useWindowDimensions, View } from 'react-native'; +import { Share, StyleSheet, useWindowDimensions, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { cacheDirectory, EncodingType, writeAsStringAsync } from 'expo-file-system/legacy'; @@ -13,7 +13,7 @@ import { playerBackdropArtworkSource } from '@/library/artwork'; import { usePlayerStore } from '@/stores/playerStore'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; // Keep the device presentation identical to the canonical shared PNG. The // near-white isolation field is part of the proven phone-to-phone geometry. @@ -23,7 +23,6 @@ const CODE_BG = '#f4f4f6'; export default function SignalScreen() { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const router = useRouter(); const track = usePlayerStore((s) => s.currentTrack); const codeRef = useRef(null); @@ -65,12 +64,12 @@ export default function SignalScreen() { offset={{ top: 0, left: -spacing.lg, right: -spacing.lg }} /> - router.back()} hitSlop={8}> + router.back()} hitSlop={8}> Back - + @@ -116,16 +115,16 @@ export default function SignalScreen() { - void shareImage()}> + void shareImage()}> Share image - - void shareLink()}> + + void shareLink()}> Share link - + )} diff --git a/src/app/signal/scan.tsx b/src/app/signal/scan.tsx index c86498b..1ed185f 100644 --- a/src/app/signal/scan.tsx +++ b/src/app/signal/scan.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { Linking, Pressable, StyleSheet, View } from 'react-native'; +import { Linking, StyleSheet, View } from 'react-native'; import { CameraView, useCameraPermissions } from 'expo-camera'; import * as DocumentPicker from 'expo-document-picker'; import { Ionicons } from '@expo/vector-icons'; @@ -23,7 +23,7 @@ import { dbTrackToTrack } from '@/library/trackAdapter'; import { playHaptic } from '@/lib/haptics'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import type { SignalPayload } from '@boof2015/astra-signal'; import type { DbTrack } from '@/types/library'; import { AstraLibraryData } from '../../../modules/astra-library-scanner'; @@ -33,7 +33,6 @@ const FAILURE_RETURN_MS = 480; export default function SignalScanScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const cameraRef = useRef(null); @@ -200,12 +199,12 @@ export default function SignalScanScreen() { return ( - router.back()} hitSlop={8}> + router.back()} hitSlop={8}> Back - + @@ -227,16 +226,16 @@ export default function SignalScanScreen() { Camera access is needed to scan a Signal. - void requestPermission()}> + void requestPermission()}> Allow camera - - void pickImage()}> + + void pickImage()}> Or pick a Signal image - + ) : ( @@ -253,12 +252,12 @@ export default function SignalScanScreen() { - void pickImage()} hitSlop={8}> + void pickImage()} hitSlop={8}> - - void capture()} hitSlop={8}> + + void capture()} hitSlop={8}> - + (); @@ -189,12 +187,12 @@ export default function SourceEditScreen() { behavior={Platform.OS === 'ios' ? 'padding' : undefined} > - + {backLabel} - + {step === 'type' ? ( @@ -208,7 +206,7 @@ export default function SourceEditScreen() { {TYPE_OPTIONS.map((option) => ( - chooseType(option.type)} @@ -228,7 +226,7 @@ export default function SourceEditScreen() { - + ))} @@ -276,7 +274,7 @@ export default function SourceEditScreen() { ) : null} - void onTest()} disabled={!!busy} @@ -288,8 +286,8 @@ export default function SourceEditScreen() { Test connection )} - - + )} - + )} diff --git a/src/app/sources/index.tsx b/src/app/sources/index.tsx index 610aef2..1e82d41 100644 --- a/src/app/sources/index.tsx +++ b/src/app/sources/index.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { - Pressable, StyleSheet, View } from 'react-native'; @@ -21,7 +20,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { useRemoteSourcesStore } from '@/stores/remoteSourcesStore'; import type { RemoteSourceRow, RemoteSyncProgress } from '@/types/remote'; @@ -45,7 +44,6 @@ function statusLine( export default function SourcesScreen() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const sources = useRemoteSourcesStore((s) => s.sources); @@ -125,18 +123,18 @@ export default function SourcesScreen() { No servers yet. Add a Subsonic or Jellyfin server to stream and browse your self-hosted library. - router.push('/sources/edit')}> + router.push('/sources/edit')}> Add server - + ) : ( sources.map((source) => { const status = statusLine(source, progressById[source.id] ?? null); return ( - setActionFor(source)} @@ -170,7 +168,7 @@ export default function SourcesScreen() { - + ); }) )} diff --git a/src/components/AppPressable.tsx b/src/components/AppPressable.tsx new file mode 100644 index 0000000..dd4ef6f --- /dev/null +++ b/src/components/AppPressable.tsx @@ -0,0 +1,95 @@ +import { Fragment, forwardRef } from 'react'; +import { + Pressable as NativePressable, + StyleSheet, + View, + type PressableProps, + type PressableStateCallbackType, + type View as NativeView, + type ViewStyle, +} from 'react-native'; +import { useColors } from '@/theme/themed'; +import { + composePressedStyle, + resolveFeedbackCorners, + resolvePressFeedback, + type AppPressFeedback, + type FeedbackCornerSource, +} from '@/components/appPressableFeedback'; + +/** + * Delay used only inside scrollable content. A scroll gesture claims the touch + * before feedback appears, while deliberate taps still receive press feedback. + */ +export const SCROLL_PRESS_DELAY = 80; + +export interface AppPressableProps extends Omit { + /** Astra's non-radial Android press treatment. */ + feedback?: AppPressFeedback; + /** Overrides radii inferred from the root style for surface/tile washes. */ + feedbackRadius?: number; +} + +/** + * The app-wide Android press surface. It preserves NativePressable semantics + * while replacing the platform ripple with a restrained wash or opacity shift. + */ +export const AppPressable = forwardRef( + function AppPressable( + { + feedback = 'surface', + feedbackRadius, + style, + children, + disabled = false, + ...rest + }, + ref, + ) { + const colors = useColors(); + const isDisabled = disabled === true; + + const resolveStyle = (state: PressableStateCallbackType) => + typeof style === 'function' ? style(state) : style; + + return ( + + composePressedStyle( + resolveStyle(state), + resolvePressFeedback(feedback, state.pressed, isDisabled), + ) + } + > + {(state) => { + const content = typeof children === 'function' ? children(state) : children; + const decision = resolvePressFeedback(feedback, state.pressed, isDisabled); + if (!decision.overlay) return content; + + const flattened = StyleSheet.flatten(resolveStyle(state)) as + | (ViewStyle & FeedbackCornerSource) + | undefined; + const wash = ( + + ); + + return decision.overlay === 'behind' ? ( + {wash}{content} + ) : ( + {content}{wash} + ); + }} + + ); + }, +); diff --git a/src/components/MiniPlayer.tsx b/src/components/MiniPlayer.tsx index d52f286..7322a95 100644 --- a/src/components/MiniPlayer.tsx +++ b/src/components/MiniPlayer.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { View, - Pressable, type LayoutChangeEvent, type StyleProp, type ViewStyle @@ -28,7 +27,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { motion } from '@/theme/motion'; import { usePlayerStore } from '@/stores/playerStore'; import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore'; @@ -190,7 +189,6 @@ export function MiniPlayer({ }: MiniPlayerProps = {}) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const playerOpen = usePlayerOnScreen(); const selectedTarget = usePlaybackTargetStore((s) => s.target); const track = usePlayerStore((s) => s.currentTrack); @@ -517,8 +515,8 @@ export function MiniPlayer({ style={[styles.barBlock, { width: miniWidth, height }]} onLayout={onLayout} > - - - - + - - + + {presentation.hasTrack ? ( isDesktop ? ( - usePlayerUiStore.getState().openPlayer()} accessibilityRole="button" @@ -705,11 +703,11 @@ export function MiniPlayer({ {displayedMedia.title} ) : null} - + - - - + - + - usePlayerUiStore.getState().openPlayer()} > @@ -814,30 +812,30 @@ export function MiniPlayer({ {isDesktop ? ( - setTargetPickerOpen(true)} style={styles.control} accessibilityLabel="Choose output device" > - + ) : null} - + - + {/* Secondary, so play/pause stays the brightest thing in the pill. A filled accent disc would read better on a flat surface, but here it would punch a hole through the live spectrum drawing behind all of this. */} - + - + {presentation.hasTrack ? ( @@ -853,7 +851,7 @@ export function MiniPlayer({ ) ) : null} - + )} - + ); } diff --git a/src/components/ScreenHeader.tsx b/src/components/ScreenHeader.tsx index caac9a1..8958ad2 100644 --- a/src/components/ScreenHeader.tsx +++ b/src/components/ScreenHeader.tsx @@ -2,7 +2,6 @@ import { useCallback, type ReactNode } from 'react'; import { PixelRatio, - Pressable, StyleSheet, View, useWindowDimensions, @@ -21,7 +20,7 @@ import { useShellRailPresent } from '@/navigation/shellRailContext'; import { useShellLayout } from '@/navigation/useShellLayout'; import { MAX_FONT_SCALE, fontSize, variantLineHeight } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { SCREEN_HEADER_ACTION_SIZE, barOpacityAt, @@ -153,7 +152,6 @@ export function ScreenHeader({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const insets = useSafeAreaInsets(); const { scrollY, layout } = header; @@ -203,8 +201,8 @@ export function ScreenHeader({ const chevron = onBack != null ? ( - - + ) : null; const actionCluster = @@ -391,10 +389,9 @@ export function ScreenHeaderAction({ children: ReactNode; }) { const styles = useStyles(); - const ripple = useRipple(); return ( - {children} - + ); } diff --git a/src/components/SegmentedControl.tsx b/src/components/SegmentedControl.tsx index 05c2582..f68b703 100644 --- a/src/components/SegmentedControl.tsx +++ b/src/components/SegmentedControl.tsx @@ -1,6 +1,5 @@ import { useEffect } from 'react'; import { - Pressable, StyleSheet, View, type LayoutChangeEvent @@ -19,7 +18,7 @@ import { variantLineHeight, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { motion } from '@/theme/motion'; import { playHaptic } from '@/lib/haptics'; import { useSelectionSlide } from './selectionSlide.ts'; @@ -94,7 +93,6 @@ function SegmentButton({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); // 0 = inactive, 1 = active; drives the label colour cross-fade. const progress = useSharedValue(focused ? 1 : 0); @@ -117,8 +115,8 @@ function SegmentButton({ }; return ( - {label} - + ); } diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 87773db..3e09405 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -26,7 +26,6 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; import { motion } from '@/theme/motion'; import { playHaptic } from '@/lib/haptics'; import { useSettingsStore } from '@/stores/settingsStore'; @@ -343,7 +342,6 @@ function TabButton({ }: TabButtonProps) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); // 0 = inactive, 1 = active. Drives the accent fill, label colour, and bloom. const progress = useSharedValue(focused ? 1 : 0); // 0 = at rest, 1 = finger down. @@ -375,7 +373,7 @@ function TabButton({ // Same transition as the card around it, so a resize is one movement // rather than a container gliding over children that jumped. layout={width ? NAV_TRANSITION : undefined} - android_ripple={ripple.icon(26)} + style={[ styles.tab, rail && styles.railTab, diff --git a/src/components/appPressableFeedback.test.mts b/src/components/appPressableFeedback.test.mts new file mode 100644 index 0000000..ef96acb --- /dev/null +++ b/src/components/appPressableFeedback.test.mts @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + composePressedStyle, + resolveFeedbackCorners, + resolvePressFeedback, +} from './appPressableFeedback.ts'; + +test('maps pressed feedback variants without changing idle or disabled targets', () => { + assert.deepEqual(resolvePressFeedback('surface', true, false), { overlay: 'behind', opacity: null }); + assert.deepEqual(resolvePressFeedback('tile', true, false), { overlay: 'above', opacity: null }); + assert.deepEqual(resolvePressFeedback('control', true, false), { overlay: null, opacity: 0.74 }); + assert.deepEqual(resolvePressFeedback('accent', true, false), { overlay: null, opacity: 0.88 }); + assert.deepEqual(resolvePressFeedback('control', false, false), { overlay: null, opacity: null }); + assert.deepEqual(resolvePressFeedback('accent', true, true), { overlay: null, opacity: null }); + assert.deepEqual(resolvePressFeedback('none', true, false), { overlay: null, opacity: null }); +}); + +test('resolves per-corner radii and lets the explicit radius override them', () => { + assert.deepEqual( + resolveFeedbackCorners({ borderRadius: 12, borderTopLeftRadius: 4 }), + { + borderTopLeftRadius: 4, + borderTopRightRadius: 12, + borderBottomLeftRadius: 12, + borderBottomRightRadius: 12, + }, + ); + assert.deepEqual(resolveFeedbackCorners({ borderRadius: 12 }, 8), { + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }); +}); + +test('preserves caller styles while appending pressed opacity', () => { + const callerStyle = [{ flex: 1 }, { opacity: 0.5 }]; + const decision = resolvePressFeedback('control', true, false); + assert.deepEqual(composePressedStyle(callerStyle, decision), [callerStyle, { opacity: 0.74 }]); + assert.equal( + composePressedStyle(callerStyle, resolvePressFeedback('surface', false, false)), + callerStyle, + ); +}); diff --git a/src/components/appPressableFeedback.ts b/src/components/appPressableFeedback.ts new file mode 100644 index 0000000..1012297 --- /dev/null +++ b/src/components/appPressableFeedback.ts @@ -0,0 +1,55 @@ +export type AppPressFeedback = 'surface' | 'tile' | 'control' | 'accent' | 'none'; + +export interface PressFeedbackDecision { + overlay: 'behind' | 'above' | null; + opacity: number | null; +} + +export interface FeedbackCornerSource { + borderRadius?: number; + borderTopLeftRadius?: number; + borderTopRightRadius?: number; + borderBottomLeftRadius?: number; + borderBottomRightRadius?: number; +} + +export interface FeedbackCorners { + borderTopLeftRadius?: number; + borderTopRightRadius?: number; + borderBottomLeftRadius?: number; + borderBottomRightRadius?: number; +} + +const IDLE_FEEDBACK: PressFeedbackDecision = { overlay: null, opacity: null }; + +export function resolvePressFeedback( + feedback: AppPressFeedback, + pressed: boolean, + disabled: boolean, +): PressFeedbackDecision { + if (!pressed || disabled || feedback === 'none') return IDLE_FEEDBACK; + if (feedback === 'surface') return { overlay: 'behind', opacity: null }; + if (feedback === 'tile') return { overlay: 'above', opacity: null }; + if (feedback === 'control') return { overlay: null, opacity: 0.74 }; + return { overlay: null, opacity: 0.88 }; +} + +export function resolveFeedbackCorners( + style: FeedbackCornerSource | undefined, + override?: number, +): FeedbackCorners { + const fallback = override ?? style?.borderRadius; + return { + borderTopLeftRadius: override ?? style?.borderTopLeftRadius ?? fallback, + borderTopRightRadius: override ?? style?.borderTopRightRadius ?? fallback, + borderBottomLeftRadius: override ?? style?.borderBottomLeftRadius ?? fallback, + borderBottomRightRadius: override ?? style?.borderBottomRightRadius ?? fallback, + }; +} + +export function composePressedStyle( + style: T, + decision: PressFeedbackDecision, +): T | [T, { opacity: number }] { + return decision.opacity == null ? style : [style, { opacity: decision.opacity }]; +} diff --git a/src/components/dialogs/AppDialog.tsx b/src/components/dialogs/AppDialog.tsx index 635fca4..586101c 100644 --- a/src/components/dialogs/AppDialog.tsx +++ b/src/components/dialogs/AppDialog.tsx @@ -1,7 +1,6 @@ import { useSyncExternalStore } from 'react'; import { Modal, - Pressable, ScrollView, StyleSheet, View, @@ -9,7 +8,7 @@ import { import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { dismissActiveDialog, EMPTY_DIALOG_QUEUE, @@ -65,7 +64,6 @@ export function AppDialogHost() { const active = queue.active; const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); if (!active) return null; @@ -119,9 +117,9 @@ export function AppDialogHost() { ? colors.textSecondary : colors.accent; return ( - [ styles.action, pressed ? styles.actionPressed : null, @@ -133,7 +131,7 @@ export function AppDialogHost() { {action.label} - + ); })} diff --git a/src/components/eq/BandConsole.tsx b/src/components/eq/BandConsole.tsx index 98708dd..1851c1f 100644 --- a/src/components/eq/BandConsole.tsx +++ b/src/components/eq/BandConsole.tsx @@ -1,10 +1,10 @@ -import { Pressable, ScrollView, StyleSheet, View } from 'react-native'; +import { ScrollView, StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { HapticSwitch } from '@/components/HapticSwitch'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { EQ_MAX_GAIN_DB, isPassEQBandType, @@ -68,7 +68,6 @@ export function BandConsole({ }: BandConsoleProps) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( - onEditType(band.id)} @@ -116,7 +115,7 @@ export function BandConsole({ {BAND_TYPE_LABEL[band.type]} - + {/* A pass filter has no gain to set. Keeping the rail in place rather @@ -156,13 +155,13 @@ export function BandConsole({ value={isShelf ? '—' : band.Q.toFixed(2)} onPress={isShelf ? undefined : () => onEditValue(band.id, 'Q')} /> - + ); })} {canAdd ? ( - - + ) : null} ); @@ -195,7 +194,6 @@ function Readout({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const body = ( <> {body}; } return ( - {body} - + ); } diff --git a/src/components/eq/BandDetailPanel.tsx b/src/components/eq/BandDetailPanel.tsx index 317bb2f..1c71531 100644 --- a/src/components/eq/BandDetailPanel.tsx +++ b/src/components/eq/BandDetailPanel.tsx @@ -1,5 +1,4 @@ import { - Pressable, StyleSheet, View } from 'react-native'; @@ -11,7 +10,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import type { EQBand } from '@/types/audio'; import { EQ_MAX_FREQUENCY, @@ -44,7 +43,6 @@ export type EQEditableValue = 'frequency' | 'gain' | 'Q'; /** "Band N" + type dropdown + On toggle + audible parameter sliders. */ export function BandDetailPanel({ band, bandNumber, onUpdate, onEditType, onEditValue }: BandDetailPanelProps) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); if (!band) { return ( @@ -63,12 +61,12 @@ export function BandDetailPanel({ band, bandNumber, onUpdate, onEditType, onEdit Band {bandNumber} - + {BAND_TYPE_LABEL[band.type]} - + {band.enabled ? 'On' : 'Off'} { const isActive = band.id === activeBandId; return ( - onSelect(band.id)} style={[styles.cell, isActive && styles.cellActive]} @@ -54,13 +52,13 @@ export function BandStrip({ bands, activeBandId, canAdd, onSelect, onAdd }: Band > {formatGain(band.gain)} - + ); })} {canAdd ? ( - + - + ) : null} ); diff --git a/src/components/eq/EQPresetNameSheet.tsx b/src/components/eq/EQPresetNameSheet.tsx index 1e06577..7e21c95 100644 --- a/src/components/eq/EQPresetNameSheet.tsx +++ b/src/components/eq/EQPresetNameSheet.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { - Pressable, StyleSheet, View } from 'react-native'; @@ -12,7 +11,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { EqSheet } from './EqSheet'; interface EQPresetNameSheetProps { @@ -29,7 +28,6 @@ export function EQPresetNameSheet({ onClose, }: EQPresetNameSheetProps) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const [name, setName] = useState(defaultName); const trimmed = name.trim(); @@ -58,16 +56,16 @@ export function EQPresetNameSheet({ onSubmitEditing={submit} /> - + Cancel - - + + {actionLabel} - + ); diff --git a/src/components/eq/EQPresetPreviewSheet.tsx b/src/components/eq/EQPresetPreviewSheet.tsx index 0ef3fd5..6510476 100644 --- a/src/components/eq/EQPresetPreviewSheet.tsx +++ b/src/components/eq/EQPresetPreviewSheet.tsx @@ -1,5 +1,4 @@ import { - Pressable, StyleSheet, View } from 'react-native'; @@ -9,7 +8,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import type { EQPreset } from '@/types/audio'; import { EqSheet } from './EqSheet'; import { formatGain } from './format'; @@ -30,7 +29,6 @@ export function EQPresetPreviewSheet({ onClose, }: EQPresetPreviewSheetProps) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const enabledBands = preset.bands.filter((band) => band.enabled).length; const modeLabel = preset.mode === 'graphic' ? 'Graphic' : 'Parametric'; @@ -70,12 +68,12 @@ export function EQPresetPreviewSheet({ - + Cancel - - + { onConfirm(); @@ -85,7 +83,7 @@ export function EQPresetPreviewSheet({ {confirmLabel} - + ); diff --git a/src/components/eq/EQPresetQrSheet.tsx b/src/components/eq/EQPresetQrSheet.tsx index 9513588..8244660 100644 --- a/src/components/eq/EQPresetQrSheet.tsx +++ b/src/components/eq/EQPresetQrSheet.tsx @@ -1,5 +1,4 @@ import { - Pressable, StyleSheet, View } from 'react-native'; @@ -10,7 +9,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { EqSheet } from './EqSheet'; interface EQPresetQrSheetProps { @@ -21,7 +20,6 @@ interface EQPresetQrSheetProps { export function EQPresetQrSheet({ presetName, value, onClose }: EQPresetQrSheetProps) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); return ( @@ -35,11 +33,11 @@ export function EQPresetQrSheet({ presetName, value, onClose }: EQPresetQrSheetP {presetName} - + Done - + ); } diff --git a/src/components/eq/EQSlider.tsx b/src/components/eq/EQSlider.tsx index 332f208..293e951 100644 --- a/src/components/eq/EQSlider.tsx +++ b/src/components/eq/EQSlider.tsx @@ -1,6 +1,5 @@ import { useRef, useState } from 'react'; import { - Pressable, View, StyleSheet, type GestureResponderEvent, @@ -12,7 +11,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; const THUMB = 16; @@ -44,7 +43,6 @@ export function EQSlider({ onValuePress, }: EQSliderProps) { const styles = useStyles(); - const ripple = useRipple(); const [width, setWidth] = useState(0); const [active, setActive] = useState(false); const widthRef = useRef(0); @@ -120,10 +118,10 @@ export function EQSlider({ /> {onValuePress && !disabled ? ( - [styles.valueButton, pressed && styles.valueButtonPressed]} onPress={onValuePress} accessibilityRole="button" @@ -132,7 +130,7 @@ export function EQSlider({ {format(value)} - + ) : ( {format(value)} diff --git a/src/components/eq/EQValueEditSheet.tsx b/src/components/eq/EQValueEditSheet.tsx index ea8e5f0..c1b066a 100644 --- a/src/components/eq/EQValueEditSheet.tsx +++ b/src/components/eq/EQValueEditSheet.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { - Pressable, StyleSheet, View, type KeyboardTypeOptions @@ -13,7 +12,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { EqSheet } from './EqSheet'; interface EQValueEditSheetProps { @@ -41,7 +40,6 @@ export function EQValueEditSheet({ onClose, }: EQValueEditSheetProps) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const [value, setValue] = useState(initialValue); const trimmed = value.trim(); @@ -82,12 +80,12 @@ export function EQValueEditSheet({ {valid || trimmed.length === 0 ? rangeLabel : 'Enter a valid number'} - + Cancel - - + Apply - + ); diff --git a/src/components/eq/PresetDeviceAssignmentSheet.tsx b/src/components/eq/PresetDeviceAssignmentSheet.tsx index b249d72..510e12d 100644 --- a/src/components/eq/PresetDeviceAssignmentSheet.tsx +++ b/src/components/eq/PresetDeviceAssignmentSheet.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react'; -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { EqSheet } from '@/components/eq/EqSheet'; @@ -7,7 +7,7 @@ import type { KnownEQOutputDevice } from '@/audio/eqDevicePresets'; import type { EQPreset } from '@/types/audio'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { playHaptic } from '@/lib/haptics'; interface PresetDeviceAssignmentSheetProps { @@ -49,7 +49,6 @@ export function PresetDeviceAssignmentSheet({ }: PresetDeviceAssignmentSheetProps) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const [selected, setSelected] = useState>( () => new Set(devices.filter((device) => assignments[device.key] === preset.id).map((device) => device.key)) ); @@ -101,9 +100,9 @@ export function PresetDeviceAssignmentSheet({ ? `Currently assigned to ${assignedPresetName}` : kindLabel(device); return ( - toggleDevice(device.key)} @@ -131,18 +130,18 @@ export function PresetDeviceAssignmentSheet({ size={22} color={checked ? colors.accent : colors.textTertiary} /> - + ); })} )} - + Cancel - - + { onSave([...selected]); @@ -150,7 +149,7 @@ export function PresetDeviceAssignmentSheet({ }} > Save assignments - + ); diff --git a/src/components/eq/PresetSheet.tsx b/src/components/eq/PresetSheet.tsx index a2c086d..fd6d8be 100644 --- a/src/components/eq/PresetSheet.tsx +++ b/src/components/eq/PresetSheet.tsx @@ -1,9 +1,9 @@ -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import type { KnownEQOutputDevice } from '@/audio/eqDevicePresets'; import type { EQPreset } from '@/types/audio'; import { @@ -45,7 +45,6 @@ export function PresetSheet({ onClose, }: PresetSheetProps) { const colors = useColors(); - const ripple = useRipple(); const builtIn = presets.filter((p) => !p.isCustom); const custom = presets.filter((p) => p.isCustom); const devicesByKey = new Map(knownDevices.map((device) => [device.key, device])); @@ -72,8 +71,8 @@ export function PresetSheet({ }} trailing={ - onAssign(preset)} @@ -84,10 +83,10 @@ export function PresetSheet({ Assign - + {preset.isCustom ? ( - onDelete(preset)} @@ -95,7 +94,7 @@ export function PresetSheet({ accessibilityLabel={`Delete preset ${preset.name}`} > - + ) : null} } diff --git a/src/components/eq/SavePresetSheet.tsx b/src/components/eq/SavePresetSheet.tsx index f44dcf2..157715c 100644 --- a/src/components/eq/SavePresetSheet.tsx +++ b/src/components/eq/SavePresetSheet.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { - Pressable, StyleSheet, View } from 'react-native'; @@ -13,7 +12,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { EqSheet } from './EqSheet'; interface SavePresetSheetProps { @@ -31,7 +30,6 @@ export function SavePresetSheet({ onClose, }: SavePresetSheetProps) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const [name, setName] = useState(defaultName); const [assignToCurrentDevice, setAssignToCurrentDevice] = useState(false); @@ -76,12 +74,12 @@ export function SavePresetSheet({ ) : null} - + Cancel - - + { @@ -92,7 +90,7 @@ export function SavePresetSheet({ Save - + ); diff --git a/src/components/library/AlbumGridItem.tsx b/src/components/library/AlbumGridItem.tsx index e6aff20..4743216 100644 --- a/src/components/library/AlbumGridItem.tsx +++ b/src/components/library/AlbumGridItem.tsx @@ -1,6 +1,5 @@ import { View, - Pressable, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; @@ -11,16 +10,15 @@ import { spacing, } from '@/theme'; import { createThemedStyles } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { albumArtworkSource } from '@/library/artwork'; import type { Album } from '@/types/library'; export function AlbumGridItem({ album, onPress }: { album: Album; onPress: () => void }) { const styles = useStyles(); - const ripple = useRipple(); const artUri = albumArtworkSource(album); return ( - + {artUri ? ( {album.artist} - + ); } diff --git a/src/components/library/AlbumRow.tsx b/src/components/library/AlbumRow.tsx index 5d134a9..76118cf 100644 --- a/src/components/library/AlbumRow.tsx +++ b/src/components/library/AlbumRow.tsx @@ -1,6 +1,5 @@ import { View, - Pressable, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; @@ -11,7 +10,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { albumArtworkSource } from '@/library/artwork'; import type { Album } from '@/types/library'; @@ -19,10 +18,9 @@ import type { Album } from '@/types/library'; export function AlbumRow({ album, onPress }: { album: Album; onPress: () => void }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const artUri = albumArtworkSource(album); return ( - + {artUri ? ( void - + ); } diff --git a/src/components/library/ArtistGridItem.tsx b/src/components/library/ArtistGridItem.tsx index d5d7d4f..898eed5 100644 --- a/src/components/library/ArtistGridItem.tsx +++ b/src/components/library/ArtistGridItem.tsx @@ -1,6 +1,5 @@ import { View, - Pressable, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; @@ -11,7 +10,7 @@ import { radius, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { artworkUri } from '@/library/artwork'; import type { Artist } from '@/types/library'; @@ -19,7 +18,6 @@ import type { Artist } from '@/types/library'; export function ArtistGridItem({ artist, onPress }: { artist: Artist; onPress: () => void }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const useMosaic = artist.artwork_hashes.length >= 4; const hashes = useMosaic ? artist.artwork_hashes.slice(0, 4) : artist.artwork_hashes.slice(0, 1); @@ -27,7 +25,7 @@ export function ArtistGridItem({ artist, onPress }: { artist: Artist; onPress: ( const tracks = `${artist.track_count} ${artist.track_count === 1 ? 'track' : 'tracks'}`; return ( - + {hashes.length === 0 ? ( @@ -58,7 +56,7 @@ export function ArtistGridItem({ artist, onPress }: { artist: Artist; onPress: ( {albums} · {tracks} - + ); } diff --git a/src/components/library/ArtistImageSearchSheet.tsx b/src/components/library/ArtistImageSearchSheet.tsx index 2107fb1..667ba87 100644 --- a/src/components/library/ArtistImageSearchSheet.tsx +++ b/src/components/library/ArtistImageSearchSheet.tsx @@ -13,7 +13,7 @@ import { AppSheet, AppSheetTitle } from '@/components/sheets/AppSheet'; import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { searchArtistImageCandidates } from '@/library/artistImageLookup'; import type { DeezerArtistCandidate } from '@/types/artistImages'; @@ -28,7 +28,6 @@ export function ArtistImageSearchSheet({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const [query, setQuery] = useState(artistName); const [candidates, setCandidates] = useState([]); const [loading, setLoading] = useState(false); @@ -101,8 +100,8 @@ export function ArtistImageSearchSheet({ style={styles.input} accessibilityLabel="Deezer artist search" /> - void search()} accessibilityRole="button" @@ -113,7 +112,7 @@ export function ArtistImageSearchSheet({ ) : ( )} - + {error ? ( @@ -127,9 +126,9 @@ export function ArtistImageSearchSheet({ {candidates.map((candidate) => ( - void choose(candidate)} accessibilityRole="button" @@ -166,7 +165,7 @@ export function ArtistImageSearchSheet({ ) : ( )} - + ))} diff --git a/src/components/library/ArtistImageSweepStatus.tsx b/src/components/library/ArtistImageSweepStatus.tsx index b683286..15ba0b2 100644 --- a/src/components/library/ArtistImageSweepStatus.tsx +++ b/src/components/library/ArtistImageSweepStatus.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; -import { ActivityIndicator, Pressable, StyleSheet, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { useArtistImageStore } from '@/stores/artistImageStore'; import { requeueMissingArtistImages } from '@/library/artistImageLookup'; @@ -19,7 +19,6 @@ const n = (value: number) => value.toLocaleString(); export function ArtistImageSweepStatus({ enabled }: { enabled: boolean }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const running = useArtistImageStore((s) => s.running); const processed = useArtistImageStore((s) => s.processed); const total = useArtistImageStore((s) => s.total); @@ -73,8 +72,8 @@ export function ArtistImageSweepStatus({ enabled }: { enabled: boolean }) { {missing === 1 ? '1 artist has no image' : `${n(missing)} artists have no image`} - void retry()} @@ -89,7 +88,7 @@ export function ArtistImageSweepStatus({ enabled }: { enabled: boolean }) { Look for missing images - + ); } diff --git a/src/components/library/ArtistRow.tsx b/src/components/library/ArtistRow.tsx index 56f20fa..f51ba5a 100644 --- a/src/components/library/ArtistRow.tsx +++ b/src/components/library/ArtistRow.tsx @@ -1,6 +1,5 @@ import { View, - Pressable, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; @@ -11,18 +10,17 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { artworkUri } from '@/library/artwork'; import type { Artist } from '@/types/library'; export function ArtistRow({ artist, onPress }: { artist: Artist; onPress: () => void }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const albums = `${artist.album_count} ${artist.album_count === 1 ? 'album' : 'albums'}`; const tracks = `${artist.track_count} ${artist.track_count === 1 ? 'track' : 'tracks'}`; return ( - + {artist.artwork_hash ? ( - + ); } diff --git a/src/components/library/CollapsingDetail.tsx b/src/components/library/CollapsingDetail.tsx index 2106341..e1fb43f 100644 --- a/src/components/library/CollapsingDetail.tsx +++ b/src/components/library/CollapsingDetail.tsx @@ -7,7 +7,6 @@ import { type ReactNode } from 'react'; import { - Pressable, StyleSheet, View, useWindowDimensions, @@ -37,7 +36,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { DETAIL_ART_COLLAPSED, DETAIL_BAR_H, @@ -202,7 +201,6 @@ export function CollapsingHeader({ onHeroBlockLayout: (e: LayoutChangeEvent) => void; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const insets = useSafeAreaInsets(); const { width: W, height: H } = useWindowDimensions(); @@ -362,7 +360,7 @@ export function CollapsingHeader({ {heroMeta} {heroExtra} - Play - - + Shuffle - + - - + - + - - + + - + {onMore ? ( - - + ) : null} {/* Rendered last so the large art sits on top of the header text until it tucks away. */} diff --git a/src/components/library/EmptyLibrary.tsx b/src/components/library/EmptyLibrary.tsx index 5936e6c..02bdbdf 100644 --- a/src/components/library/EmptyLibrary.tsx +++ b/src/components/library/EmptyLibrary.tsx @@ -1,6 +1,5 @@ import { - View, - Pressable + View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; @@ -10,12 +9,11 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { useLibraryStore } from '@/stores/libraryStore'; export function EmptyLibrary() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const router = useRouter(); const recoveryNotice = useLibraryStore((state) => state.recoveryNotice); @@ -50,8 +48,8 @@ export function EmptyLibrary() { : recoveryNotice ?? 'Pick a folder on this device and Astra will scan it into your library.'} - router.push(fatal ? '/settings/troubleshooting' : '/settings')} accessibilityRole="button" @@ -60,7 +58,7 @@ export function EmptyLibrary() { {fatal ? 'Troubleshooting' : 'Folder settings'} - + ); } diff --git a/src/components/library/FoldersView.tsx b/src/components/library/FoldersView.tsx index a338f78..d6967d0 100644 --- a/src/components/library/FoldersView.tsx +++ b/src/components/library/FoldersView.tsx @@ -28,7 +28,7 @@ import { formatDuration } from '@/lib/format'; import { playHaptic } from '@/lib/haptics'; import { spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { usePlayerStore } from '@/stores/playerStore'; import type { DbTrack } from '@/types/library'; import { useSceneBottomInset } from '@/navigation/useShellLayout'; @@ -81,15 +81,14 @@ function FolderNodeRow({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const { node } = state; const stop = (callback: () => void) => (event: GestureResponderEvent) => { event.stopPropagation(); callback(); }; return ( - {node.totalTrackCount} - - - + - - + + ); } @@ -157,10 +156,9 @@ function FolderTrackRow({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( - { @@ -195,8 +193,8 @@ function FolderTrackRow({ {track.artist} {formatDuration(track.duration)} - { @@ -208,8 +206,8 @@ function FolderTrackRow({ accessibilityLabel={`More actions for ${track.title}`} > - - + + ); } diff --git a/src/components/library/LibraryContextBar.tsx b/src/components/library/LibraryContextBar.tsx index 5b959b3..9d094db 100644 --- a/src/components/library/LibraryContextBar.tsx +++ b/src/components/library/LibraryContextBar.tsx @@ -1,7 +1,6 @@ /* eslint-disable react-hooks/immutability -- Reanimated shared values are mutable gesture state. */ import { useEffect, useMemo } from 'react'; import { - Pressable, StyleSheet, View, useWindowDimensions, @@ -20,7 +19,7 @@ import { useSelectionSlide } from '@/components/selectionSlide'; import { radius, spacing } from '@/theme'; import { motion } from '@/theme/motion'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { playHaptic } from '@/lib/haptics'; import { LIBRARY_CONTEXT_BAR_HEIGHT, @@ -79,7 +78,6 @@ export function LibraryContextBar({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const { width, fontScale } = useWindowDimensions(); const showActiveLabel = libraryDockShowsActiveLabel(width, fontScale); const modeIndex = LIBRARY_VIEW_MODES.findIndex((entry) => entry.key === mode); @@ -256,9 +254,9 @@ export function LibraryContextBar({ {LIBRARY_VIEW_MODES.map((entry) => { const selected = entry.key === mode; return ( - ) : null} - + ); })} @@ -329,10 +327,9 @@ function ContextAction({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( - - + ); } diff --git a/src/components/library/PlaylistRow.tsx b/src/components/library/PlaylistRow.tsx index 77f750c..6652774 100644 --- a/src/components/library/PlaylistRow.tsx +++ b/src/components/library/PlaylistRow.tsx @@ -1,6 +1,5 @@ import { View, - Pressable, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; @@ -11,7 +10,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { artworkUri } from '@/library/artwork'; import { playHaptic } from '@/lib/haptics'; @@ -41,10 +40,9 @@ export function PlaylistRow({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( - - + ); } diff --git a/src/components/library/ScanNotificationPermissionCard.tsx b/src/components/library/ScanNotificationPermissionCard.tsx index 5ef3cd3..1c0cd4d 100644 --- a/src/components/library/ScanNotificationPermissionCard.tsx +++ b/src/components/library/ScanNotificationPermissionCard.tsx @@ -1,6 +1,5 @@ import { ActivityIndicator, - Pressable, StyleSheet, View, type StyleProp, @@ -10,7 +9,7 @@ import { Ionicons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { useScanNotificationPermission } from '@/library/useScanNotificationPermission'; /** @@ -31,7 +30,6 @@ export function ScanNotificationPermissionCard({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const { state, granted, denied, working, resolve } = useScanNotificationPermission(); // Also hidden while the first check is in flight, so the row never appears @@ -53,8 +51,8 @@ export function ScanNotificationPermissionCard({ - {denied ? 'Open Settings' : 'Allow scan notifications'} - + {denied ? ( diff --git a/src/components/library/ScanProgress.tsx b/src/components/library/ScanProgress.tsx index f230b3d..05e72e0 100644 --- a/src/components/library/ScanProgress.tsx +++ b/src/components/library/ScanProgress.tsx @@ -1,7 +1,7 @@ -import { Pressable, View } from 'react-native'; +import { View } from 'react-native'; import { Text } from '@/components/Text'; import { spacing } from '@/theme'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { createThemedStyles, useColors } from '@/theme/themed'; import { useLibraryStore } from '@/stores/libraryStore'; @@ -9,7 +9,6 @@ import { useLibraryStore } from '@/stores/libraryStore'; export function ScanProgress() { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const isScanning = useLibraryStore((s) => s.isScanning); const isCancelling = useLibraryStore((s) => s.isCancelling); const progress = useLibraryStore((s) => s.scanProgress); @@ -42,8 +41,8 @@ export function ScanProgress() { > {label} - {isCancelling ? 'Cancelling…' : 'Cancel'} - + {label} - + ); } diff --git a/src/components/library/TrackRow.tsx b/src/components/library/TrackRow.tsx index 71402be..7bc68c5 100644 --- a/src/components/library/TrackRow.tsx +++ b/src/components/library/TrackRow.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import { View, - Pressable, StyleSheet, type GestureResponderEvent } from 'react-native'; @@ -17,7 +16,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { formatDuration } from '@/lib/format'; import { playHaptic } from '@/lib/haptics'; import { trackArtworkThumbSource } from '@/library/artwork'; @@ -68,7 +67,6 @@ export function TrackRow({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); // Key the artwork by hash (local) or identity path (remote) so the error fallback // and FlashList recycling work for both. const artKey = track.source_type !== 'local' ? track.path : track.artwork_hash; @@ -94,8 +92,8 @@ export function TrackRow({ }; const row = ( - {onOpenActions && !selectionMode ? ( - - + ) : null} - + ); if (!swipeToQueue || selectionMode) return row; diff --git a/src/components/listening/ListeningPreviewCard.tsx b/src/components/listening/ListeningPreviewCard.tsx index 21068f6..0ffdeb2 100644 --- a/src/components/listening/ListeningPreviewCard.tsx +++ b/src/components/listening/ListeningPreviewCard.tsx @@ -1,13 +1,13 @@ import { Image } from 'expo-image'; import { Ionicons } from '@expo/vector-icons'; -import { Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native'; +import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native'; import { AstraLogo } from '@/components/AstraLogo'; import { Text } from '@/components/Text'; import { listeningArtworkSource } from '@/library/artwork'; import { formatListeningTime } from '@/listeningStats/format'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import type { ListeningStatsDashboard } from '@/types/listeningStats'; /** @@ -30,7 +30,6 @@ export function ListeningPreviewCard({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); if (!hasListeningPreview(dashboard) || !dashboard) return null; const topTrack = dashboard.topTracks[0] ?? null; @@ -38,9 +37,9 @@ export function ListeningPreviewCard({ const paused = !dashboard.status.enabled; return ( - )} - + ); } diff --git a/src/components/lyrics/LyricsBand.tsx b/src/components/lyrics/LyricsBand.tsx index c6c341d..c45fef4 100644 --- a/src/components/lyrics/LyricsBand.tsx +++ b/src/components/lyrics/LyricsBand.tsx @@ -7,12 +7,12 @@ // static scroll; loading/not-found/error states get a centered message. import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { ActivityIndicator, Pressable, ScrollView, View, type LayoutChangeEvent } from 'react-native'; +import { ActivityIndicator, ScrollView, View, type LayoutChangeEvent } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { useSmoothPlaybackTime } from '@/audio/useSmoothPlaybackTime'; import { useLyricsStore } from '@/stores/lyricsStore'; import { useLyricsSettingsStore } from '@/stores/lyricsSettingsStore'; @@ -84,7 +84,6 @@ export function LyricsBand({ }: LyricsBandProps) { const { fixedSize, maxSize, hPadding } = SURFACE[surface]; const colors = useColors(); - const ripple = useRipple(); const entry = useLyricsStore((s) => s.byPath[track.path]); const loadForTrack = useLyricsStore((s) => s.loadForTrack); const wordTimingEnabled = useLyricsSettingsStore((s) => s.wordTimingEnabled); @@ -201,8 +200,8 @@ export function LyricsBand({ {emptyState.message} {emptyState.retryable ? ( - void loadForTrack(track, { force: true })} accessibilityRole="button" @@ -232,7 +231,7 @@ export function LyricsBand({ {isLoading ? 'Retrying…' : 'Retry'} - + ) : null} ); @@ -310,7 +309,7 @@ export function LyricsBand({ {followPaused ? ( - Recenter - + ) : null} ); diff --git a/src/components/lyrics/LyricsLine.tsx b/src/components/lyrics/LyricsLine.tsx index 9ea276d..baf97e5 100644 --- a/src/components/lyrics/LyricsLine.tsx +++ b/src/components/lyrics/LyricsLine.tsx @@ -1,9 +1,9 @@ import { memo, useEffect, useMemo, useState } from 'react'; -import { Pressable, View, type LayoutChangeEvent } from 'react-native'; +import { View, type LayoutChangeEvent } from 'react-native'; import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { Text } from '@/components/Text'; import { useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { getPreferredLyricsTranslation, resolveLyricsWordTiming } from '@/lyrics/presentation'; import type { LyricsFurigana, LyricsLine as LyricsLineData, LyricsWord } from '@/lyrics/types'; @@ -183,7 +183,6 @@ function LyricsLineComponent({ onLayout, }: LyricsLineProps) { const colors = useColors(); - const ripple = useRipple(); const target = TIER[tier]; const size = baseSize; const lineHeight = Math.round(size * 1.2); @@ -218,7 +217,7 @@ function LyricsLineComponent({ return ( - + {voiceLabelsEnabled && line.voice?.trim() ? ( @@ -268,7 +267,7 @@ function LyricsLineComponent({ ) : null} - + ); } diff --git a/src/components/lyrics/LyricsView.tsx b/src/components/lyrics/LyricsView.tsx index 44833b3..c690aec 100644 --- a/src/components/lyrics/LyricsView.tsx +++ b/src/components/lyrics/LyricsView.tsx @@ -6,7 +6,7 @@ // art view; swipe-down still closes the player. import { Image } from 'expo-image'; -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { MarqueeText } from '@/components/MarqueeText'; @@ -20,7 +20,7 @@ import { import { LyricsBand } from './LyricsBand'; import { spacing, radius } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { usePlayerStore } from '@/stores/playerStore'; import { useLyricsStore } from '@/stores/lyricsStore'; import { getLyricsPayloadSourceLabel } from '@/lyrics/presentation'; @@ -57,7 +57,6 @@ export function LyricsView({ onDismiss, }: LyricsViewProps) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); // Lyrics mode is phone-target only, so progress comes straight from the // player store — the 2Hz tick re-renders this takeover, not the whole screen. @@ -71,9 +70,9 @@ export function LyricsView({ return ( - + - + - - + - + - + - (initialPolicy); const [saving, setSaving] = useState(false); @@ -72,8 +71,8 @@ function ArtistImageDisclosureContent({ Ethernet is included in Wi-Fi mode. You can change this later in Settings › Library, and manual searches work while automatic downloads are off. - void continueSetup()} disabled={saving} @@ -84,7 +83,7 @@ function ArtistImageDisclosureContent({ ) : ( Continue )} - + diff --git a/src/components/onboarding/ArtistImageStep.tsx b/src/components/onboarding/ArtistImageStep.tsx index d8e6340..f86fdd8 100644 --- a/src/components/onboarding/ArtistImageStep.tsx +++ b/src/components/onboarding/ArtistImageStep.tsx @@ -1,5 +1,5 @@ import { useEffect, type ComponentProps, type ReactNode } from 'react'; -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, @@ -11,7 +11,7 @@ import { StepHeader } from '@/components/onboarding/StepHeader'; import { radius, spacing } from '@/theme'; import { motion } from '@/theme/motion'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { playHaptic } from '@/lib/haptics'; import { useSettingsStore } from '@/stores/settingsStore'; import type { ArtistImageAutoPolicy } from '@/types/artistImages'; @@ -175,10 +175,9 @@ function PolicyCard({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( - )} - + ); } diff --git a/src/components/onboarding/NotificationStep.tsx b/src/components/onboarding/NotificationStep.tsx index af30001..6747e6b 100644 --- a/src/components/onboarding/NotificationStep.tsx +++ b/src/components/onboarding/NotificationStep.tsx @@ -1,10 +1,10 @@ -import { ActivityIndicator, Pressable, StyleSheet, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { StepHeader } from '@/components/onboarding/StepHeader'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { useLibraryStore } from '@/stores/libraryStore'; import type { ScanNotificationPermission } from '@/library/useScanNotificationPermission'; @@ -22,7 +22,6 @@ export function NotificationStep({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const isScanning = useLibraryStore((s) => s.isScanning); const { state, granted, denied, working, resolve } = permission; @@ -52,8 +51,8 @@ export function NotificationStep({ ) : ( - {denied ? 'Open Android settings' : 'Allow notifications'} - + )} diff --git a/src/components/onboarding/OnboardingFlow.tsx b/src/components/onboarding/OnboardingFlow.tsx index 03df019..cd7b1ba 100644 --- a/src/components/onboarding/OnboardingFlow.tsx +++ b/src/components/onboarding/OnboardingFlow.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react'; import { ActivityIndicator, - Pressable, ScrollView, StyleSheet, View, @@ -30,7 +29,7 @@ import { formatFolderCount, formatTrackCount } from '@/components/settings/Setti import { radius, spacing } from '@/theme'; import { motion } from '@/theme/motion'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { playHaptic } from '@/lib/haptics'; import type { BaseThemeId } from '@/theme/resolve'; import { useScanNotificationPermission } from '@/library/useScanNotificationPermission'; @@ -77,7 +76,6 @@ const WIZARD_THEME_OPTIONS: { id: BaseThemeId; title: string }[] = [ */ export function OnboardingFlow({ onDone }: { onDone: () => void }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const insets = useSafeAreaInsets(); const { width, height } = useWindowDimensions(); @@ -173,7 +171,7 @@ export function OnboardingFlow({ onDone }: { onDone: () => void }) { {canGoBack ? ( - void }) { Back - + ) : null} - void }) { {primaryLabel} - + @@ -223,7 +221,6 @@ function WelcomeStep() { function LibraryStep() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const folders = useLibraryStore((s) => s.folders); const totalTrackCount = useLibraryStore((s) => s.totalTrackCount); @@ -238,7 +235,7 @@ function LibraryStep() { subtitle="Point Astra at the folders where your music lives. It scans them into your library — files on disk are never modified." /> - void addFolder()} @@ -248,7 +245,7 @@ function LibraryStep() { {folders.length > 0 ? 'Add another folder' : 'Choose music folder'} - + @@ -275,7 +272,6 @@ function LibraryStep() { function ThemeStep() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const baseTheme = useThemeStore((s) => s.baseTheme); const materialYouAvailable = useThemeStore((s) => s.materialYouAvailable); @@ -298,7 +294,7 @@ function ThemeStep() { {options.map((option) => { const selected = option.id === baseTheme; return ( - { if (selected) return; @@ -315,7 +311,7 @@ function ThemeStep() { > {option.title} - + ); })} @@ -375,7 +371,6 @@ function DoneStep() { function ScanBanner() { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const isScanning = useLibraryStore((s) => s.isScanning); const isCancelling = useLibraryStore((s) => s.isCancelling); const progress = useLibraryStore((s) => s.scanProgress); @@ -402,8 +397,8 @@ function ScanBanner() { ? 'Cancelling library scan…' : `Scanning your library${detail ? ` · ${detail}` : '…'}`} - {isCancelling ? 'Cancelling…' : 'Cancel'} - + ); } diff --git a/src/components/player/NowPlayingOverlay.tsx b/src/components/player/NowPlayingOverlay.tsx index a5a4d2d..207a55e 100644 --- a/src/components/player/NowPlayingOverlay.tsx +++ b/src/components/player/NowPlayingOverlay.tsx @@ -68,7 +68,7 @@ import { ScopedPaletteProvider, useColors, } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { motion } from '@/theme/motion'; import { paletteWithAccent } from '@/theme/scopedAccent'; import { useNowPlayingArtworkAccent } from '@/theme/useNowPlayingArtworkAccent'; @@ -193,7 +193,6 @@ export function NowPlayingOverlay({ }: NowPlayingOverlayProps = {}) { const dock = presentation === 'dock'; const appColors = useColors(); - const ripple = useRipple(); const router = useRouter(); const returnToTabs = useReturnToTabs(); const rawInsets = useSafeAreaInsets(); @@ -1094,9 +1093,9 @@ export function NowPlayingOverlay({ downward, the pane collapses sideways back to the bar card. Adding a second control alongside this one just gave the dock two chevrons that did different things. */} - dock ? void useSettingsStore.getState().setPlayerDockOpen(false) @@ -1111,7 +1110,7 @@ export function NowPlayingOverlay({ size={26} color={colors.textSecondary} /> - + @@ -1126,25 +1125,25 @@ export function NowPlayingOverlay({ dock used to draw its own expand button on top of this one, which is what made it read as tacked on. */} {dock ? ( - usePlayerUiStore.getState().openPlayer()} hitSlop={12} accessibilityRole="button" accessibilityLabel="Open full screen player" > - + ) : null} - - + )} @@ -1234,7 +1233,7 @@ export function NowPlayingOverlay({ void sendDesktopControl('toggle-favorite')} @@ -1272,7 +1271,7 @@ export function NowPlayingOverlay({ void reconnectDesktop()} accessibilityLabel="Reconnect to desktop" > @@ -1409,7 +1408,7 @@ export function NowPlayingOverlay({ {desktopQueue ? ( - { if (desktopConnection) { void reconnectDesktop(); @@ -1447,7 +1446,7 @@ export function NowPlayingOverlay({ {desktopConnection ? 'Reconnect' : 'Pair desktop'} - + ) ) : track ? ( @@ -1658,7 +1657,7 @@ export function NowPlayingOverlay({ hitSlop={10} haptic="selection" style={[styles.creditChip, { height: deck.artistLineHeight }]} - android_ripple={ripple.bounded} + onPress={() => setArtistPickerOpen(true)} accessibilityRole="button" accessibilityLabel={`All ${artistCreditTokens.length} artists`} @@ -1673,7 +1672,7 @@ export function NowPlayingOverlay({ void toggleFavorite(track)} @@ -1716,7 +1715,7 @@ export function NowPlayingOverlay({ void toggleShuffle()} accessibilityLabel="Shuffle" @@ -1737,7 +1736,7 @@ export function NowPlayingOverlay({ onPress={skipToPrevious} haptic="action" hitSlop={12} - style={styles.transportMainBtn} android_ripple={ripple.icon(26)} + style={styles.transportMainBtn} accessibilityLabel="Previous" > void cycleRepeat()} accessibilityLabel="Repeat" @@ -1811,7 +1810,7 @@ export function NowPlayingOverlay({ void setScopeStageVisible(!scopeStageVisible)} accessibilityLabel={scopeStageVisible ? 'Hide visualizer' : 'Show visualizer'} @@ -1839,7 +1838,7 @@ export function NowPlayingOverlay({ {menuItems.map((item) => ( - {item.label} - + ))} diff --git a/src/components/player/SleepTimerControls.tsx b/src/components/player/SleepTimerControls.tsx index f8d6be3..dc12ca5 100644 --- a/src/components/player/SleepTimerControls.tsx +++ b/src/components/player/SleepTimerControls.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Pressable, TextInput, View } from 'react-native'; +import { TextInput, View } from 'react-native'; import { Text } from '@/components/Text'; import { SLEEP_TIMER_PRESETS, formatSleepTimerStatus, normalizeSleepTimerMinutes } from '@/audio/sleepTimerState'; import { supportsNativePauseAtEndOfItem } from '@/audio/trackPlayerExtensions'; @@ -8,12 +8,11 @@ import { usePlaybackTargetStore } from '@/stores/playbackTargetStore'; import { useSleepTimerStore } from '@/stores/sleepTimerStore'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; export function SleepTimerControls() { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const timer = useSleepTimerStore((s) => s.timer); const remainingMs = useSleepTimerStore((s) => s.remainingMs); const hydrate = useSleepTimerStore((s) => s.hydrate); @@ -76,17 +75,17 @@ export function SleepTimerControls() { {SLEEP_TIMER_PRESETS.map((minutes) => ( - void run(() => startMinutes(minutes), `Timer set for ${minutes} minutes.`)} style={({ pressed }) => [styles.preset, !available && styles.disabled, pressed && available && styles.pressed]} accessibilityRole="button" > {minutes} min - + ))} @@ -103,19 +102,19 @@ export function SleepTimerControls() { style={[styles.input, !available && styles.disabled]} accessibilityLabel="Custom sleep timer minutes" /> - [styles.action, !available && styles.disabled, pressed && available && styles.pressed]} > Set custom - + - void run(startEndOfTrack, 'Timer set for the end of the track.')} style={({ pressed }) => [ styles.fullAction, @@ -127,12 +126,12 @@ export function SleepTimerControls() { {supportsNativePauseAtEndOfItem() ? 'Pause exactly before the next track begins.' : 'Requires the Android playback engine.'} - + {timer ? ( - void run(cancel, 'Sleep timer canceled.')} style={styles.cancel}> + void run(cancel, 'Sleep timer canceled.')} style={styles.cancel}> Cancel sleep timer - + ) : null} {feedback ? {feedback} : null} diff --git a/src/components/queue/RemoteQueueSheet.tsx b/src/components/queue/RemoteQueueSheet.tsx index 8872963..fac52a2 100644 --- a/src/components/queue/RemoteQueueSheet.tsx +++ b/src/components/queue/RemoteQueueSheet.tsx @@ -7,7 +7,7 @@ // queue-tray-sheet gotcha). import { useCallback, useEffect, useMemo } from 'react'; -import { Pressable, View } from 'react-native'; +import { View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import BottomSheet, { BottomSheetBackdrop, @@ -19,7 +19,7 @@ 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 { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { formatDuration } from '@/lib/format'; import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore'; import type { DesktopRemoteQueueItem } from '@/types/desktopRemote'; @@ -33,7 +33,6 @@ interface RemoteQueueSheetProps { 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(); @@ -71,8 +70,8 @@ export function RemoteQueueSheet({ onClose, embedded = false }: RemoteQueueSheet const renderItem = useCallback( ({ item }: ListRenderItemInfo) => ( - playItem(item)} disabled={item.isCurrent} @@ -100,9 +99,9 @@ export function RemoteQueueSheet({ onClose, embedded = false }: RemoteQueueSheet {formatDuration(item.durationSeconds)} ) : null} - + ), - [playItem, colors, styles, ripple] + [playItem, colors, styles] ); const topSection = useMemo( diff --git a/src/components/search/QuickSearchOverlay.tsx b/src/components/search/QuickSearchOverlay.tsx index aae9705..c1da68b 100644 --- a/src/components/search/QuickSearchOverlay.tsx +++ b/src/components/search/QuickSearchOverlay.tsx @@ -7,7 +7,6 @@ import { import { Keyboard, Modal, - Pressable, StyleSheet, TextInput, View, @@ -28,7 +27,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { rgbaFromHex } from '@/theme/colorUtils'; import { enqueueTop, playLibraryQuery } from '@/audio/playbackController'; import { dbTrackToTrack } from '@/library/trackAdapter'; @@ -514,7 +513,6 @@ function ResultRow({ onQueueTrack: (track: DbTrack) => void; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const isTrack = result.kind === 'track'; const isShowMode = result.kind === 'show-all' || result.kind === 'show-top'; @@ -526,7 +524,7 @@ function ResultRow({ }; return ( - {isTrack ? ( - - + ) : result.kind === 'playlist' && result.playlist.remote ? ( ) : ( )} - + ); } @@ -567,7 +565,6 @@ function QuickSearchPanel({ onClose: () => void; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const returnToTabs = useReturnToTabs(); const insets = useSafeAreaInsets(); @@ -1084,18 +1081,18 @@ function QuickSearchPanel({ style={styles.input} /> {query.length > 0 ? ( - updateQuery('')} hitSlop={8} accessibilityRole="button" accessibilityLabel="Clear search" > - + ) : null} - + - + {showAllLibrary ? ( @@ -1145,7 +1142,6 @@ function QuickSearchPanel({ export function QuickSearchOverlay() { const styles = useStyles(); - const ripple = useRipple(); const isOpen = useSearchStore((s) => s.isQuickSearchOpen); const initialQuery = useSearchStore((s) => s.initialQuery); const openVersion = useSearchStore((s) => s.openVersion); @@ -1165,7 +1161,7 @@ export function QuickSearchOverlay() { onRequestClose={close} > - + {isOpen ? ( ) : null} diff --git a/src/components/settings/AccentColorSheet.tsx b/src/components/settings/AccentColorSheet.tsx index dbad7c2..67156f2 100644 --- a/src/components/settings/AccentColorSheet.tsx +++ b/src/components/settings/AccentColorSheet.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Modal, - Pressable, StyleSheet, View, type LayoutChangeEvent, @@ -23,7 +22,7 @@ import { AppSheet, AppSheetTitle } from '@/components/sheets/AppSheet'; import { Text } from '@/components/Text'; import { fonts, radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { hexToHsv, hsvToHex, @@ -59,7 +58,6 @@ export function AccentColorSheet({ }: AccentColorSheetProps) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const normalizedInitial = normalizeHexColor(initialHex) ?? '#5b8aff'; const [hsv, setHsv] = useState(() => hexToHsv(normalizedInitial)); const hsvRef = useRef(hsv); @@ -245,21 +243,21 @@ export function AccentColorSheet({ - Cancel - - + Apply - + diff --git a/src/components/settings/AccentSwatchRow.tsx b/src/components/settings/AccentSwatchRow.tsx index 97ef18d..11165ff 100644 --- a/src/components/settings/AccentSwatchRow.tsx +++ b/src/components/settings/AccentSwatchRow.tsx @@ -1,12 +1,12 @@ import { useState } from 'react'; -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Canvas, LinearGradient, Rect, vec } from '@shopify/react-native-skia'; import { Text } from '@/components/Text'; import { spacing } from '@/theme'; import { ACCENTS, ACCENT_IDS, accentPreferenceBase } from '@/theme/accents'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { playHaptic } from '@/lib/haptics'; import { useThemeStore } from '@/stores/themeStore'; import { AccentColorSheet } from '@/components/settings/AccentColorSheet'; @@ -17,7 +17,6 @@ const RAINBOW = ['#ff5c5c', '#ffb454', '#2dd4a0', '#00b3ff', '#9d7bff', '#ff6b9d /** Circular accent swatches; the selected one gets a ring + checkmark. */ export function AccentSwatchRow() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const [pickerOpen, setPickerOpen] = useState(false); const preference = useThemeStore((s) => s.accentPreference); @@ -31,7 +30,7 @@ export function AccentSwatchRow() { {ACCENT_IDS.map((id) => { const selected = preference.kind === 'preset' && id === preference.id; return ( - { if (selected) return; @@ -51,11 +50,11 @@ export function AccentSwatchRow() { {selected ? ( ) : null} - + ); })} - { playHaptic('selection'); setPickerOpen(true); @@ -86,7 +85,7 @@ export function AccentSwatchRow() { ) : ( )} - + Accent · {preference.kind === 'preset' diff --git a/src/components/settings/ScopeStyleCards.tsx b/src/components/settings/ScopeStyleCards.tsx index b75be40..726ec70 100644 --- a/src/components/settings/ScopeStyleCards.tsx +++ b/src/components/settings/ScopeStyleCards.tsx @@ -1,8 +1,8 @@ -import { Pressable, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import type { NowPlayingScopeStyle } from '@/stores/settingsStore'; import { playHaptic } from '@/lib/haptics'; @@ -58,7 +58,6 @@ function StyleCard({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const handlePress = () => { if (selected) return; @@ -67,8 +66,8 @@ function StyleCard({ }; return ( - {description} - + ); } diff --git a/src/components/settings/SettingsPanels.tsx b/src/components/settings/SettingsPanels.tsx index a56fae4..5243ffb 100644 --- a/src/components/settings/SettingsPanels.tsx +++ b/src/components/settings/SettingsPanels.tsx @@ -1,5 +1,4 @@ import { - Pressable, StyleSheet, View, } from 'react-native'; @@ -18,7 +17,7 @@ import { } from '@/components/settings/SettingsSectionScaffold'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import type { ReplayGainMode } from '@/audio/normalization'; import type { ArtistGroupingMode } from '@/library/artistGrouping'; import type { BaseThemeId, PreferredDark } from '@/theme/resolve'; @@ -108,7 +107,6 @@ export function themeOptionTitle(id: BaseThemeId): string { export function AppearanceSettingsPanel() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const baseTheme = useThemeStore((s) => s.baseTheme); const preferredDark = useThemeStore((s) => s.preferredDark); @@ -136,7 +134,7 @@ export function AppearanceSettingsPanel() { {options.map((option) => { const selected = option.id === baseTheme; return ( - { @@ -160,7 +158,7 @@ export function AppearanceSettingsPanel() { ) : ( )} - + ); })} @@ -249,7 +247,6 @@ function LibraryFolderSettingsRow({ onRemove: (folder: FolderWithCount) => void; }) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); return ( @@ -272,7 +269,7 @@ function LibraryFolderSettingsRow({ : 'Access lost. Remove and add again.'} - onRemove(folder)} @@ -281,14 +278,13 @@ function LibraryFolderSettingsRow({ style={disabled && styles.actionDisabled} > - + ); } function LibraryFoldersSettings() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const folders = useLibraryStore((s) => s.folders); const isScanning = useLibraryStore((s) => s.isScanning); @@ -326,7 +322,7 @@ function LibraryFoldersSettings() { : `${formatFolderCount(folders.length)} / ${formatTrackCount(totalTracks)}`} - void addFolder()} @@ -336,12 +332,12 @@ function LibraryFoldersSettings() { Add - + {folders.length > 0 ? ( - void rescan()} @@ -351,7 +347,7 @@ function LibraryFoldersSettings() { Rescan all - + ) : null} @@ -387,7 +383,6 @@ function LibraryFoldersSettings() { export function LibrarySettingsPanel() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const groupingMode = useSettingsStore((s) => s.artistGroupingMode); const setArtistGroupingMode = useSettingsStore((s) => s.setArtistGroupingMode); @@ -453,7 +448,7 @@ export function LibrarySettingsPanel() { {ARTIST_GROUPING_OPTIONS.map((option) => { const selected = option.mode === groupingMode; return ( - { @@ -477,7 +472,7 @@ export function LibrarySettingsPanel() { ) : ( )} - + ); })} @@ -506,7 +501,6 @@ export function LibrarySettingsPanel() { export function AudioSettingsPanel() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const normalizationEnabled = useAudioSettingsStore((s) => s.normalizationEnabled); const normalizationTargetLufs = useAudioSettingsStore((s) => s.normalizationTargetLufs); @@ -554,7 +548,7 @@ export function AudioSettingsPanel() { {REPLAYGAIN_MODES.map((m) => { const selected = m.mode === replayGainMode; return ( - { @@ -566,7 +560,7 @@ export function AudioSettingsPanel() { {m.label} - + ); })} diff --git a/src/components/settings/SettingsSectionScaffold.tsx b/src/components/settings/SettingsSectionScaffold.tsx index aada7b7..dd4411a 100644 --- a/src/components/settings/SettingsSectionScaffold.tsx +++ b/src/components/settings/SettingsSectionScaffold.tsx @@ -1,6 +1,5 @@ import type { ReactNode } from 'react'; import { - Pressable, StyleSheet, View, type StyleProp, @@ -14,7 +13,7 @@ import { ScreenHeader, useScreenHeader } from '@/components/ScreenHeader'; 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 { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { HapticSwitch } from '@/components/HapticSwitch'; export type SettingsIconName = keyof typeof Ionicons.glyphMap; @@ -101,9 +100,8 @@ export function SettingsNavRow({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); return ( - + @@ -119,7 +117,7 @@ export function SettingsNavRow({ - + ); } diff --git a/src/components/sheets/ActionSheet.tsx b/src/components/sheets/ActionSheet.tsx index ee88d5b..7174904 100644 --- a/src/components/sheets/ActionSheet.tsx +++ b/src/components/sheets/ActionSheet.tsx @@ -12,7 +12,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { AppPressable, SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { useSceneBottomInset } from '@/navigation/useShellLayout'; export interface ActionSheetItem { @@ -41,7 +41,6 @@ export function ActionSheet({ }) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const insets = useSafeAreaInsets(); const sceneBottomInset = useSceneBottomInset(); @@ -62,9 +61,9 @@ export function ActionSheet({ ) : null} {items.map((item) => ( - ) : null} - + ))} diff --git a/src/components/sheets/AppSheet.tsx b/src/components/sheets/AppSheet.tsx index 5ef8c84..12d933c 100644 --- a/src/components/sheets/AppSheet.tsx +++ b/src/components/sheets/AppSheet.tsx @@ -17,7 +17,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple'; +import { SCROLL_PRESS_DELAY } from '@/components/AppPressable'; import { playHaptic } from '@/lib/haptics'; import { useSceneBottomInset } from '@/navigation/useShellLayout'; @@ -121,7 +121,6 @@ export function AppSheetItem({ }: AppSheetItemProps) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const tint = destructive ? colors.warning : selected ? colors.accentTextStrong : colors.textPrimary; const selectable = selected !== undefined; @@ -139,9 +138,8 @@ export function AppSheetItem({ later contact, leaving the row visibly tapped but never pressed. */} [styles.item, pressed && styles.itemPressed]} onPress={handlePress} accessibilityRole={selectable ? 'radio' : 'button'} accessibilityState={selectable ? { selected } : undefined} @@ -205,6 +203,9 @@ const useStyles = createThemedStyles((colors) => ({ gap: spacing.md, paddingVertical: spacing.md, }, + itemPressed: { + backgroundColor: colors.glassHighlight, + }, itemLabel: { flexShrink: 1, }, diff --git a/src/components/sheets/PlaylistPickerSheet.tsx b/src/components/sheets/PlaylistPickerSheet.tsx index c615acf..8f43d93 100644 --- a/src/components/sheets/PlaylistPickerSheet.tsx +++ b/src/components/sheets/PlaylistPickerSheet.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { - Pressable, StyleSheet, View } from 'react-native'; @@ -17,7 +16,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { usePlaylistStore } from '@/stores/playlistStore'; import type { DbTrack } from '@/types/library'; @@ -42,7 +41,6 @@ export function PlaylistPickerSheet({ onAdded, }: PlaylistPickerSheetProps) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const [step, setStep] = useState<'pick' | 'create'>('pick'); const [playlistName, setPlaylistName] = useState(''); @@ -84,12 +82,12 @@ export function PlaylistPickerSheet({ selectionColor={colors.accent} /> - setStep('pick')}> + setStep('pick')}> Back - - + Create - + ); diff --git a/src/components/sheets/TextPromptModal.tsx b/src/components/sheets/TextPromptModal.tsx index fb2f0c6..02e4ae5 100644 --- a/src/components/sheets/TextPromptModal.tsx +++ b/src/components/sheets/TextPromptModal.tsx @@ -14,7 +14,7 @@ import { spacing, } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; interface TextPromptModalProps { visible: boolean; @@ -42,7 +42,6 @@ function TextPromptModalInner({ onClose, }: TextPromptModalProps) { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const [value, setValue] = useState(initialValue); const trimmed = value.trim(); @@ -71,12 +70,12 @@ function TextPromptModalInner({ selectionColor={colors.accent} /> - + Cancel - - + {submitLabel} - + diff --git a/src/components/signal/SignalResolutionPanel.tsx b/src/components/signal/SignalResolutionPanel.tsx index 295eab4..3bfac50 100644 --- a/src/components/signal/SignalResolutionPanel.tsx +++ b/src/components/signal/SignalResolutionPanel.tsx @@ -1,4 +1,4 @@ -import { Pressable, ScrollView, StyleSheet, View } from 'react-native'; +import { ScrollView, StyleSheet, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { Text } from '@/components/Text'; import { TrackRow } from '@/components/library/TrackRow'; @@ -8,7 +8,7 @@ import type { DbTrack } from '@/types/library'; import type { SignalPayload } from '@boof2015/astra-signal'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; export type SignalResultActionState = 'idle' | 'playing' | 'queueing' | 'queued'; @@ -37,12 +37,11 @@ export function SignalResolutionPanel({ }: SignalResolutionPanelProps) { const styles = useStyles(); const colors = useColors(); - const ripple = useRipple(); const actionBusy = actionState === 'playing' || actionState === 'queueing'; const onlineAction = (primary = false) => ( - Find online - + ); const footer = ( - Scan another - - + Done - + ); @@ -192,8 +191,8 @@ export function SignalResolutionPanel({ /> - onPlay(track)} disabled={actionBusy} @@ -203,9 +202,9 @@ export function SignalResolutionPanel({ {actionState === 'playing' ? 'Starting…' : 'Play now'} - - + onQueue(track)} disabled={actionBusy || actionState === 'queued'} @@ -223,7 +222,7 @@ export function SignalResolutionPanel({ ? 'Added' : 'Add to queue'} - + {onlineAction()} diff --git a/src/components/sync/SyncConflictPrompt.tsx b/src/components/sync/SyncConflictPrompt.tsx index eeef211..d674ee9 100644 --- a/src/components/sync/SyncConflictPrompt.tsx +++ b/src/components/sync/SyncConflictPrompt.tsx @@ -13,7 +13,7 @@ import { router, usePathname } from 'expo-router'; import { Text } from '@/components/Text'; import { radius, spacing } from '@/theme'; import { createThemedStyles, useColors } from '@/theme/themed'; -import { useRipple } from '@/theme/ripple'; +import { AppPressable } from '@/components/AppPressable'; import { formatRelativeTime } from '@/lib/format'; import { buildSyncConflictResolutionPreview, @@ -65,7 +65,6 @@ function diffLine(desktop: SyncPlaylistSnapshot, phone: SyncPlaylistSnapshot): s export function SyncConflictPrompt() { const styles = useStyles(); - const ripple = useRipple(); const colors = useColors(); const conflicts = useDesktopSyncStore((s) => s.conflicts); const status = useDesktopSyncStore((s) => s.status); @@ -160,7 +159,7 @@ export function SyncConflictPrompt() { {options.map((resolution) => ( - setChoice({ syncUid: firstConflict.syncUid, resolution })} > {RESOLUTION_LABELS[resolution]} - + ))} @@ -185,20 +184,20 @@ export function SyncConflictPrompt() { {count > 1 ? ( - + Review all {count} conflicts - + ) : null} - + Not now - - + Confirm - + diff --git a/src/theme/palettes.ts b/src/theme/palettes.ts index 6db594c..c5fe261 100644 --- a/src/theme/palettes.ts +++ b/src/theme/palettes.ts @@ -5,7 +5,7 @@ * INVARIANT: solid-color tokens must stay 6-digit `#rrggbb` hex — the Skia * visualizers (SpectrumCurve, OscilloscopeWave, EQGraph, GraphicResponseCurve) * slice hex chars to build alpha variants. Only `glass*`, `*Glow`, - * `overlayFaint`, `ripple`, and `backdrop` may be `rgba()` strings (they are + * `overlayFaint` and `backdrop` may be `rgba()` strings (they are * never fed through those helpers). */ @@ -26,8 +26,6 @@ export interface Palette { */ glassBorderStrong: string; glassHighlight: string; - /** Native android_ripple tint (glassHighlight family, transient so slightly stronger). */ - ripple: string; // Text ramp textPrimary: string; @@ -75,7 +73,6 @@ export const midnightBase: BasePalette = { glassBorder: 'rgba(124, 146, 196, 0.16)', glassBorderStrong: 'rgba(124, 146, 196, 0.34)', glassHighlight: 'rgba(140, 162, 208, 0.08)', - ripple: 'rgba(140, 162, 208, 0.13)', textPrimary: '#e2e8f4', textSecondary: '#8a98b8', @@ -104,7 +101,6 @@ export const darkBase: BasePalette = { glassBorder: 'rgba(165, 175, 195, 0.16)', glassBorderStrong: 'rgba(165, 175, 195, 0.34)', glassHighlight: 'rgba(175, 185, 205, 0.08)', - ripple: 'rgba(175, 185, 205, 0.13)', textPrimary: '#e6e8ec', textSecondary: '#979da8', @@ -134,7 +130,6 @@ export const amoledBase: BasePalette = { glassBorder: 'rgba(124, 146, 196, 0.20)', glassBorderStrong: 'rgba(124, 146, 196, 0.38)', glassHighlight: 'rgba(140, 162, 208, 0.10)', - ripple: 'rgba(140, 162, 208, 0.14)', textPrimary: '#e2e8f4', textSecondary: '#8a98b8', @@ -164,7 +159,6 @@ export const lightBase: BasePalette = { glassBorder: 'rgba(52, 74, 130, 0.18)', glassBorderStrong: 'rgba(52, 74, 130, 0.32)', glassHighlight: 'rgba(52, 74, 130, 0.10)', - ripple: 'rgba(52, 74, 130, 0.12)', textPrimary: '#171d2e', textSecondary: '#4d5a78', diff --git a/src/theme/resolve.ts b/src/theme/resolve.ts index e2f4d2c..37e93c7 100644 --- a/src/theme/resolve.ts +++ b/src/theme/resolve.ts @@ -84,7 +84,6 @@ export function buildMaterialYouPalette(ramps: SystemPalette, isDark: boolean): glassBorder: rgbaFromHex(tone(ramps.accent2, 200), 0.18), glassBorderStrong: rgbaFromHex(tone(ramps.accent2, 200), 0.36), glassHighlight: rgbaFromHex(tone(ramps.accent2, 200), 0.09), - ripple: rgbaFromHex(tone(ramps.accent2, 200), 0.13), textPrimary: tone(ramps.neutral1, 50), textSecondary: tone(ramps.neutral2, 300), textTertiary: tone(ramps.neutral2, 500), @@ -110,7 +109,6 @@ export function buildMaterialYouPalette(ramps: SystemPalette, isDark: boolean): glassBorder: rgbaFromHex(tone(ramps.neutral2, 700), 0.16), glassBorderStrong: rgbaFromHex(tone(ramps.neutral2, 700), 0.32), glassHighlight: rgbaFromHex(tone(ramps.neutral2, 700), 0.09), - ripple: rgbaFromHex(tone(ramps.neutral2, 700), 0.12), textPrimary: tone(ramps.neutral1, 900), textSecondary: tone(ramps.neutral2, 700), textTertiary: tone(ramps.neutral2, 500), diff --git a/src/theme/ripple.ts b/src/theme/ripple.ts deleted file mode 100644 index 1c8f034..0000000 --- a/src/theme/ripple.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Native android_ripple configs — touch feedback drawn by the Android render -// thread the frame the finger lands, independent of JS-thread load. This is -// the app's ONE press-feedback system: JS `({ pressed })` dim styles were -// removed in its favor (persistent selected/active styles are state, not -// feedback, and stay). - -import { useMemo } from 'react'; -import type { PressableAndroidRippleConfig } from 'react-native'; -import { useColors } from '@/theme/themed'; - -/** - * Fixed light overlay for accent-filled buttons — the theme `ripple` token is - * invisible on a saturated accent fill. Theme-invariant on purpose (Material's - * on-color ripple), so it is not a palette token. - */ -const ON_ACCENT_COLOR = 'rgba(255, 255, 255, 0.24)'; - -/** - * `unstable_pressDelay` for Pressables inside SCROLLABLE content (rows, tiles, - * sheet items): a scroll flick claims the touch before the delay elapses, so - * scrolling never flashes ripples on everything it grazes — mirroring Android's - * ViewConfiguration tap timeout in native lists. Quick real taps still ripple - * (RN fires press-in/out on release even under the delay). Fixed chrome - * (tab bar, transport, action bars) stays instant — no delay there. - */ -export const SCROLL_PRESS_DELAY = 80; - -export interface RippleSet { - /** Clips to the Pressable rect/borderRadius — rows, cards, pills, sheet items. */ - bounded: PressableAndroidRippleConfig; - /** Bounded but drawn ABOVE children — tiles/cards whose artwork covers the Pressable. */ - tile: PressableAndroidRippleConfig; - /** Circular unbounded ripple for transparent icon buttons; radius ≈ half the touch target. */ - icon: (radius: number) => PressableAndroidRippleConfig; - /** Light overlay for accent-filled buttons (bounded unless a radius is given). */ - onAccent: (radius?: number) => PressableAndroidRippleConfig; -} - -export function useRipple(): RippleSet { - const colors = useColors(); - return useMemo( - () => ({ - bounded: { color: colors.ripple }, - tile: { color: colors.ripple, foreground: true }, - icon: (radius: number) => ({ color: colors.ripple, borderless: true, radius }), - onAccent: (radius?: number) => - radius == null - ? { color: ON_ACCENT_COLOR, foreground: true } - : { color: ON_ACCENT_COLOR, borderless: true, radius }, - }), - [colors.ripple] - ); -}