optimize further

This commit is contained in:
Boof2015
2026-07-12 12:16:52 -04:00
parent 5cc1099853
commit 804e47a7ca
69 changed files with 588 additions and 292 deletions
+6 -4
View File
@@ -38,6 +38,7 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { isWideWindow } from '@/theme/adaptive';
import { useEQStore } from '@/stores/eqStore';
import { useScopeActive } from '@/scope/scopeStore';
@@ -75,6 +76,7 @@ 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 eq = useEQStore();
@@ -217,7 +219,7 @@ export default function EQScreen() {
};
const presetRowEl = (
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.presetRow, isWide && styles.sideItem]}
onPress={() => setSheet('preset')}
>
@@ -285,7 +287,7 @@ export default function EQScreen() {
onChange={eq.setPreamp}
/>
</View>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.eqToggle, eq.enabled && styles.eqToggleOn]}
onPress={eq.toggleEnabled}
>
@@ -321,10 +323,10 @@ export default function EQScreen() {
</Text>
</View>
<View style={styles.headerActions}>
<Pressable style={styles.iconButton} onPress={() => setSheet('save')} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.iconButton} onPress={() => setSheet('save')} hitSlop={8}>
<Ionicons name="save-outline" size={20} color={colors.textSecondary} />
</Pressable>
<Pressable style={styles.iconButton} onPress={() => setSheet('overflow')} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.iconButton} onPress={() => setSheet('overflow')} hitSlop={8}>
<Ionicons name="ellipsis-vertical" size={20} color={colors.textSecondary} />
</Pressable>
</View>
+21 -11
View File
@@ -28,10 +28,12 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
import { rgbaFromHex } from '@/theme/colorUtils';
import { useLibraryStore } from '@/stores/libraryStore';
import { usePlaylistStore } from '@/stores/playlistStore';
import { usePlayerStore } from '@/stores/playerStore';
import { usePlayerUiStore } from '@/stores/playerUiStore';
import { useSearchStore } from '@/stores/searchStore';
import {
playTracks,
@@ -93,6 +95,7 @@ function SectionHeader({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
return (
<View style={styles.sectionHeader}>
<View style={styles.sectionTitleGroup}>
@@ -106,7 +109,7 @@ function SectionHeader({
) : null}
</View>
{onActionPress && actionLabel ? (
<Pressable style={styles.seeAllButton} onPress={onActionPress} accessibilityRole="button">
<Pressable style={styles.seeAllButton} android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onActionPress} accessibilityRole="button">
<Text variant="label" color={colors.accentText}>
{actionLabel}
</Text>
@@ -160,6 +163,7 @@ function NowPlayingCard({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
const isPlaying = playbackState === 'playing';
const isLoading = playbackState === 'loading';
const scopeActive = useScopeActive();
@@ -194,7 +198,7 @@ function NowPlayingCard({
{scopeActive && cardSize.width > 0 ? (
<View pointerEvents="none" style={styles.playerSpectrumVeil} />
) : null}
<Pressable style={styles.playerArt} onPress={onOpen} accessibilityRole="button">
<Pressable style={styles.playerArt} android_ripple={ripple.tile} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onOpen} accessibilityRole="button">
{track.artworkData ? (
<Image source={{ uri: track.artworkData }} style={styles.image} contentFit="cover" />
) : (
@@ -213,20 +217,20 @@ function NowPlayingCard({
</Text>
<NowPlayingSeekStrip />
<View style={styles.placeholderControls}>
<Pressable hitSlop={10} onPress={() => void skipToPrevious()}>
<Pressable hitSlop={10} android_ripple={ripple.icon(20)} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={() => void skipToPrevious()}>
<Ionicons name="play-skip-back" size={22} color={colors.textSecondary} />
</Pressable>
<Pressable hitSlop={10} onPress={() => void togglePlay()} style={styles.playCircle}>
<Pressable hitSlop={10} onPress={() => void togglePlay()} style={styles.playCircle} android_ripple={ripple.onAccent()} unstable_pressDelay={SCROLL_PRESS_DELAY}>
<Ionicons
name={isLoading ? 'ellipsis-horizontal' : isPlaying ? 'pause' : 'play'}
size={18}
color={colors.bgPrimary}
/>
</Pressable>
<Pressable hitSlop={10} onPress={() => void skipToNext()}>
<Pressable hitSlop={10} android_ripple={ripple.icon(20)} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={() => void skipToNext()}>
<Ionicons name="play-skip-forward" size={22} color={colors.textSecondary} />
</Pressable>
<Pressable hitSlop={10} onPress={onOpen}>
<Pressable hitSlop={10} android_ripple={ripple.icon(20)} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onOpen}>
<Ionicons name="expand-outline" size={21} color={colors.textTertiary} />
</Pressable>
</View>
@@ -243,8 +247,9 @@ function RecentlyAddedAlbum({
onPress: () => void;
}) {
const styles = useStyles();
const ripple = useRipple();
return (
<Pressable style={styles.recentAlbum} onPress={onPress} accessibilityRole="button">
<Pressable style={styles.recentAlbum} android_ripple={ripple.tile} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onPress} accessibilityRole="button">
<AlbumCover album={album} size={112} />
<Text variant="body" numberOfLines={1} style={styles.recentAlbumTitle}>
{album.album}
@@ -273,11 +278,12 @@ function RandomAlbumCard({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
const disabled = tracks.length === 0;
return (
<View style={styles.randomCard}>
<Pressable style={styles.randomMain} onPress={onOpen} accessibilityRole="button">
<Pressable style={styles.randomMain} android_ripple={ripple.tile} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onOpen} accessibilityRole="button">
<AlbumCover album={album} size={96} />
<View style={styles.randomMeta}>
<Text variant="label" color={colors.textTertiary}>
@@ -294,7 +300,7 @@ function RandomAlbumCard({
</Text>
</View>
<Pressable
style={styles.reroll}
style={styles.reroll} android_ripple={ripple.icon(20)} unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={onReroll}
hitSlop={8}
accessibilityRole="button"
@@ -306,6 +312,7 @@ function RandomAlbumCard({
<View style={styles.randomActions}>
<Pressable
android_ripple={ripple.onAccent()} unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.primaryButton, disabled && styles.buttonDisabled]}
disabled={disabled}
onPress={onPlay}
@@ -317,6 +324,7 @@ function RandomAlbumCard({
</Text>
</Pressable>
<Pressable
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.secondaryButton, disabled && styles.buttonDisabled]}
disabled={disabled}
onPress={onShuffle}
@@ -341,6 +349,7 @@ function EmptyHomeCard({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
return (
<View style={styles.emptyCard}>
<Ionicons name="folder-open-outline" size={34} color={colors.textTertiary} />
@@ -356,6 +365,7 @@ function EmptyHomeCard({
) : null}
</View>
<Pressable
android_ripple={ripple.onAccent()} unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.primaryButton}
onPress={onManageFolders}
accessibilityRole="button"
@@ -486,7 +496,7 @@ export default function HomeScreen() {
<NowPlayingCard
track={currentTrack}
playbackState={playbackState}
onOpen={() => router.push('/now-playing')}
onOpen={() => usePlayerUiStore.getState().openPlayer()}
/>
</View>
) : null}
@@ -502,7 +512,7 @@ export default function HomeScreen() {
<NowPlayingCard
track={currentTrack}
playbackState={playbackState}
onOpen={() => router.push('/now-playing')}
onOpen={() => usePlayerUiStore.getState().openPlayer()}
/>
) : randomAlbum ? (
<RandomAlbumCard
+5 -2
View File
@@ -26,6 +26,7 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
import type { Palette } from '@/theme/palettes';
import { useLibraryStore } from '@/stores/libraryStore';
import { usePlayerStore } from '@/stores/playerStore';
@@ -291,6 +292,7 @@ function SectionHeader({
onPress?: () => void;
}) {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
return (
<View style={styles.sectionHeader}>
@@ -303,7 +305,7 @@ function SectionHeader({
</Text>
</View>
{onPress ? (
<Pressable style={styles.seeAllButton} onPress={onPress} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.seeAllButton} onPress={onPress} accessibilityRole="button">
<Text variant="label" color={colors.accentText}>
See all
</Text>
@@ -322,6 +324,7 @@ function AlbumRail({
onAlbumPress: (album: ArtistAlbum) => void;
}) {
const styles = useStyles();
const ripple = useRipple();
return (
<ScrollView
horizontal
@@ -329,7 +332,7 @@ function AlbumRail({
contentContainerStyle={styles.albumRail}
>
{albums.map((album) => (
<Pressable
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
key={album.identity_key}
style={styles.albumCard}
onPress={() => onAlbumPress(album)}
@@ -12,12 +12,14 @@ import { Text } from '@/components/Text';
import { AlbumGridItem } from '@/components/library/AlbumGridItem';
import { spacing } from '@/theme';
import { useColors } from '@/theme/themed';
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
import { useLibraryStore } from '@/stores/libraryStore';
import { useSettingsStore } from '@/stores/settingsStore';
import { buildArtistDetail } from '@/library/artistDetail';
export default function ArtistAlbumsScreen() {
const colors = useColors();
const ripple = useRipple();
const router = useRouter();
const { name = 'Artist' } = useLocalSearchParams<{ name: string }>();
const allTracks = useLibraryStore((s) => s.tracks);
@@ -30,7 +32,7 @@ export default function ArtistAlbumsScreen() {
return (
<Screen>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary} numberOfLines={1}>
{name}
@@ -13,6 +13,7 @@ import { TrackRow } from '@/components/library/TrackRow';
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
import { spacing } from '@/theme';
import { useColors } from '@/theme/themed';
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
import { useLibraryStore } from '@/stores/libraryStore';
import { usePlayerStore } from '@/stores/playerStore';
import { useSettingsStore } from '@/stores/settingsStore';
@@ -23,6 +24,7 @@ import type { DbTrack } from '@/types/library';
export default function ArtistAppearancesScreen() {
const colors = useColors();
const ripple = useRipple();
const router = useRouter();
const { name = 'Artist' } = useLocalSearchParams<{ name: string }>();
const allTracks = useLibraryStore((s) => s.tracks);
@@ -43,7 +45,7 @@ export default function ArtistAppearancesScreen() {
return (
<Screen>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary} numberOfLines={1}>
{name}
@@ -13,6 +13,7 @@ import { TrackRow } from '@/components/library/TrackRow';
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
import { spacing } from '@/theme';
import { useColors } from '@/theme/themed';
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
import { useLibraryStore } from '@/stores/libraryStore';
import { usePlayerStore } from '@/stores/playerStore';
import { useSettingsStore } from '@/stores/settingsStore';
@@ -23,6 +24,7 @@ import type { DbTrack } from '@/types/library';
export default function ArtistSongsScreen() {
const colors = useColors();
const ripple = useRipple();
const router = useRouter();
const { name = 'Artist' } = useLocalSearchParams<{ name: string }>();
const allTracks = useLibraryStore((s) => s.tracks);
@@ -43,7 +45,7 @@ export default function ArtistSongsScreen() {
return (
<Screen>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary} numberOfLines={1}>
{name}
+5 -3
View File
@@ -39,6 +39,7 @@ import {
} from '@/components/search/PullSearchGesture';
import { spacing } from '@/theme';
import { useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { useLibraryStore } from '@/stores/libraryStore';
import { usePlayerStore } from '@/stores/playerStore';
import { useSearchStore } from '@/stores/searchStore';
@@ -77,6 +78,7 @@ const ARTIST_SORT_OPTIONS: ArtistSort[] = ['name', 'track_count'];
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);
@@ -244,7 +246,7 @@ export default function LibraryScreen() {
Library
</Text>
{!isEmpty ? (
<Pressable
<Pressable android_ripple={ripple.bounded}
hitSlop={8}
onPress={() => openQuickSearch()}
accessibilityRole="button"
@@ -281,14 +283,14 @@ export default function LibraryScreen() {
<Text variant="label">
{selectedIds.size} selected
</Text>
<Pressable onPress={exitSelection} hitSlop={8} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} onPress={exitSelection} hitSlop={8} accessibilityRole="button">
<Text variant="label" color={colors.accentText}>
Cancel
</Text>
</Pressable>
</View>
) : sortable ? (
<Pressable
<Pressable android_ripple={ripple.bounded}
style={styles.sortTrigger}
onPress={() => setSortSheetOpen(true)}
accessibilityRole="button"
+5 -2
View File
@@ -27,6 +27,7 @@ import { TextPromptModal } from '@/components/sheets/TextPromptModal';
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 { usePlaylistStore } from '@/stores/playlistStore';
import { usePlayerStore } from '@/stores/playerStore';
import { playTracks, shuffleTracks } from '@/audio/playbackController';
@@ -54,9 +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 (
<Pressable style={styles.missingRow} onLongPress={onLongPress} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.missingRow} onLongPress={onLongPress} accessibilityRole="button">
<View style={styles.missingMeta}>
<Text variant="body" numberOfLines={1} color={colors.textTertiary}>
{entry.fallback_title ?? basename(entry.track_path)}
@@ -74,6 +76,7 @@ type Prompt = { kind: 'rename'; playlist: Playlist } | null;
export default function PlaylistScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const { id } = useLocalSearchParams<{ id: string }>();
@@ -283,7 +286,7 @@ export default function PlaylistScreen() {
heroMeta={<Text variant="label">{meta}</Text>}
heroExtra={
isDynamic && playlistId != null ? (
<Pressable
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.editRules}
onPress={() =>
router.push({
@@ -27,6 +27,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 { usePlaylistStore } from '@/stores/playlistStore';
import {
DYNAMIC_PLAYLIST_PRESETS,
@@ -327,15 +328,16 @@ function DraftActions({
onApply: () => void;
}) {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
return (
<View style={styles.sheetActions}>
<Pressable style={[styles.sheetButton, styles.cancelButton]} onPress={onCancel} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} style={[styles.sheetButton, styles.cancelButton]} onPress={onCancel} accessibilityRole="button">
<Text variant="label" color={colors.textSecondary}>
Cancel
</Text>
</Pressable>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.sheetButton, styles.applyButton, disabled && styles.applyDisabled]}
disabled={disabled}
onPress={onApply}
@@ -529,6 +531,7 @@ function ConditionEditorSheet({
onApply: () => void;
}) {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const draft = target.draft;
const error = validateCondition(draft);
@@ -536,7 +539,7 @@ function ConditionEditorSheet({
return (
<AppSheet onClose={onCancel}>
<AppSheetTitle title={target.mode === 'new' ? 'Add filter' : 'Edit filter'} />
<Pressable style={styles.sheetSelectRow} onPress={onChangeField} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} style={styles.sheetSelectRow} onPress={onChangeField} accessibilityRole="button">
<View style={styles.sheetSelectText}>
<Text variant="caption" color={colors.textTertiary}>
FIELD
@@ -568,7 +571,7 @@ function ConditionEditorSheet({
<View style={styles.conditionSheetFooter}>
{target.mode === 'edit' ? (
<Pressable style={styles.removeButton} onPress={onRemove} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} style={styles.removeButton} onPress={onRemove} accessibilityRole="button">
<Ionicons name="trash-outline" size={17} color={colors.warning} />
<Text variant="label" color={colors.warning}>
Remove
@@ -713,11 +716,12 @@ function FilterCard({
onRemove: () => void;
}) {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const option = fieldOptionForKey(getConditionFieldKey(condition));
return (
<Pressable style={styles.ruleCard} onPress={onPress} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} style={styles.ruleCard} onPress={onPress} accessibilityRole="button">
<View style={styles.cardIcon}>
<Ionicons name={option?.icon ?? 'options-outline'} size={18} color={colors.accent} />
</View>
@@ -729,7 +733,7 @@ function FilterCard({
{fieldGroupLabel(condition)}
</Text>
</View>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={styles.cardRemove}
onPress={onRemove}
hitSlop={8}
@@ -744,6 +748,7 @@ function FilterCard({
export default function DynamicPlaylistEditorScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const { id } = useLocalSearchParams<{ id?: string }>();
@@ -1036,7 +1041,7 @@ export default function DynamicPlaylistEditorScreen() {
>
<Screen padded={false}>
<View style={styles.header}>
<Pressable
<Pressable android_ripple={ripple.bounded}
onPress={() => router.back()}
hitSlop={8}
style={styles.headerButton}
@@ -1080,7 +1085,7 @@ export default function DynamicPlaylistEditorScreen() {
contentContainerStyle={styles.presetRow}
>
{DYNAMIC_PLAYLIST_PRESETS.map((preset) => (
<Pressable
<Pressable android_ripple={ripple.bounded}
key={preset.id}
style={styles.presetChip}
onPress={() => applyPreset(preset.rules)}
@@ -1100,7 +1105,7 @@ export default function DynamicPlaylistEditorScreen() {
<Text variant="caption" style={styles.sectionLabel}>
FILTERS
</Text>
<Pressable style={styles.inlineAction} onPress={() => openFieldPicker('new')} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} style={styles.inlineAction} onPress={() => openFieldPicker('new')} accessibilityRole="button">
<Ionicons name="add" size={16} color={colors.accent} />
<Text variant="label" color={colors.accent}>
Add filter
@@ -1133,7 +1138,7 @@ export default function DynamicPlaylistEditorScreen() {
<Text variant="caption" style={styles.sectionLabel}>
ORDER
</Text>
<Pressable style={styles.sortCard} onPress={openSortSheet} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} style={styles.sortCard} onPress={openSortSheet} accessibilityRole="button">
<View style={styles.cardIcon}>
<Ionicons name="swap-vertical" size={18} color={colors.accent} />
</View>
@@ -1161,7 +1166,7 @@ export default function DynamicPlaylistEditorScreen() {
{status.label}
</Text>
</View>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.previewButton, normalizedRulesError !== null && styles.disabled]}
disabled={normalizedRulesError !== null}
onPress={() => setSheet({ kind: 'preview' })}
@@ -1172,7 +1177,7 @@ export default function DynamicPlaylistEditorScreen() {
Preview
</Text>
</Pressable>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.saveButton, saveDisabled && styles.disabled]}
disabled={saveDisabled}
onPress={save}
+2 -2
View File
@@ -1,6 +1,6 @@
import {
getNotificationClickRedirectPath,
isNotificationClickPath,
resolveNotificationClick,
} from '@/audio/notificationIntent';
import { getEQPresetShareRedirectPath } from '@/audio/eqShareIntent';
@@ -10,7 +10,7 @@ type RedirectSystemPathEvent = {
};
export async function redirectSystemPath({ path }: RedirectSystemPathEvent): Promise<string> {
if (isNotificationClickPath(path)) return getNotificationClickRedirectPath();
if (isNotificationClickPath(path)) return resolveNotificationClick();
const eqShareRedirect = getEQPresetShareRedirectPath(path);
if (eqShareRedirect) return eqShareRedirect;
+10 -13
View File
@@ -17,6 +17,7 @@ import {
JetBrainsMono_500Medium,
} from '@expo-google-fonts/jetbrains-mono';
import { usePlaybackSync } from '@/audio/usePlaybackSync';
import { NowPlayingHost } from '@/components/player/NowPlayingHost';
import { QuickSearchOverlay } from '@/components/search/QuickSearchOverlay';
import { useScopeLifecycle } from '@/scope/useScopeLifecycle';
import { useLibraryStore } from '@/stores/libraryStore';
@@ -45,10 +46,12 @@ import { useOnboardingStore } from '@/stores/onboardingStore';
import { OnboardingFlow } from '@/components/onboarding/OnboardingFlow';
import { useTheme } from '@/theme/themed';
// Anchor the root stack at the tabs so a deep link straight to a top-level route (the
// widget/notification opening `now-playing`, or `recently-played`) builds `[(tabs), route]`
// instead of just `[route]`. Without this, dismissing the now-playing modal pops to an empty
// stack → blank screen. Only affects deep-link/launch ordering; normal nav is unchanged.
// Anchor the root stack at the tabs so a deep link straight to a top-level route
// (the widget's `recently-played`, the notification-click redirect) builds
// `[(tabs), route]` instead of just `[route]` — backing out of a deep-linked
// route must never land on an empty stack. Only affects deep-link/launch
// ordering; normal nav is unchanged. (Now-playing itself is no longer a route —
// it's the store-gated NowPlayingHost overlay.)
export const unstable_settings = {
initialRouteName: '(tabs)',
};
@@ -357,18 +360,12 @@ export default function RootLayout() {
}}
>
<Stack.Screen name="(tabs)" />
<Stack.Screen
name="now-playing"
options={{
presentation: 'transparentModal',
animation: 'none',
gestureEnabled: false,
contentStyle: { backgroundColor: 'transparent' },
}}
/>
</Stack>
{onboardingComplete ? (
<>
{/* Always-mounted player overlay (store-gated); open/close is a pure
UI-thread slide with zero mount cost after the first mount. */}
<NowPlayingHost />
<QuickSearchOverlay />
<SyncConflictPrompt />
</>
+22 -12
View File
@@ -27,8 +27,10 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { isWideWindow, WIDE_MIN_WIDTH } from '@/theme/adaptive';
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
import { usePlayerUiStore } from '@/stores/playerUiStore';
import type { DesktopRemoteDiscoveredDesktop } from '@/types/desktopRemote';
// Layout mirrors now-playing's adaptive pattern (minus the scope stage): a
@@ -189,9 +191,10 @@ function DiscoveredDesktopRow({ desktop, onPair, disabled }: {
disabled: boolean;
}) {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
return (
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.discoveredRow, disabled && styles.buttonDisabled]}
onPress={() => onPair(desktop)}
disabled={disabled}
@@ -219,6 +222,7 @@ function DiscoveredDesktopRow({ desktop, onPair, disabled }: {
export default function DesktopRemoteScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const { pair } = useLocalSearchParams<{ pair?: string }>();
@@ -390,7 +394,7 @@ export default function DesktopRemoteScreen() {
maxLength={6}
textContentType="oneTimeCode"
/>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[
styles.primaryButton,
(normalizedPinInput.length !== 6 || !pinPairingActive) && styles.buttonDisabled,
@@ -416,7 +420,7 @@ export default function DesktopRemoteScreen() {
Open Astra Desktop settings, enable Phone Remote, then scan or paste the pairing link.
</Text>
<View style={styles.actionRow}>
<Pressable style={styles.primaryButton} onPress={() => router.push('/desktop-remote/scan' as never)}>
<Pressable android_ripple={ripple.bounded} style={styles.primaryButton} onPress={() => router.push('/desktop-remote/scan' as never)}>
<Ionicons name="scan" size={18} color={colors.accentTextStrong} />
<Text variant="body" color={colors.accentTextStrong}>
Scan QR
@@ -433,7 +437,7 @@ export default function DesktopRemoteScreen() {
autoCorrect={false}
keyboardType="url"
/>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.secondaryButton, !pairingLink.trim() && styles.buttonDisabled]}
disabled={!pairingLink.trim()}
onPress={() => void pairFromInput(pairingLink)}
@@ -470,7 +474,7 @@ export default function DesktopRemoteScreen() {
autoCapitalize="none"
autoCorrect={false}
/>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[
styles.secondaryButton,
(!manualBaseUrl.trim() || !manualTicket.trim()) && styles.buttonDisabled,
@@ -523,7 +527,7 @@ export default function DesktopRemoteScreen() {
>
<View style={[styles.remoteShell, { width: remoteLayout.contentWidth }]}>
<View style={styles.remoteNowHeader}>
<Pressable style={styles.headerBtn} onPress={() => router.back()} hitSlop={12}>
<Pressable android_ripple={ripple.bounded} style={styles.headerBtn} onPress={() => router.back()} hitSlop={12}>
<Ionicons name="chevron-down" size={26} color={colors.textSecondary} />
</Pressable>
<View style={styles.headerMid}>
@@ -534,7 +538,7 @@ export default function DesktopRemoteScreen() {
{remoteSource}
</Text>
</View>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={styles.headerBtn}
onPress={() => void reconnect()}
hitSlop={12}
@@ -582,20 +586,26 @@ export default function DesktopRemoteScreen() {
</View>
<View style={styles.manageActions}>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={styles.primaryButton}
onPress={() => router.replace('/now-playing' as never)}
onPress={() => {
// The player is an overlay, not a route: open it and pop this
// screen so it slides in above wherever the user came from.
usePlayerUiStore.getState().openPlayer();
if (router.canGoBack()) router.back();
else router.replace('/');
}}
>
<Ionicons name="musical-notes-outline" size={18} color={colors.accentTextStrong} />
<Text variant="body" color={colors.accentTextStrong}>
Open Now Playing
</Text>
</Pressable>
<Pressable style={styles.secondaryButton} onPress={() => void reconnect()}>
<Pressable android_ripple={ripple.bounded} style={styles.secondaryButton} onPress={() => void reconnect()}>
<Ionicons name="refresh" size={18} color={colors.textPrimary} />
<Text variant="body">Reconnect</Text>
</Pressable>
<Pressable style={styles.dangerButton} onPress={confirmForget}>
<Pressable android_ripple={ripple.bounded} style={styles.dangerButton} onPress={confirmForget}>
<Ionicons name="trash-outline" size={18} color={colors.warning} />
<Text variant="body" color={colors.warning}>
Forget desktop
@@ -630,7 +640,7 @@ export default function DesktopRemoteScreen() {
) : (
<>
<View style={styles.topBar}>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Settings
+4 -2
View File
@@ -19,10 +19,12 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
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);
@@ -42,7 +44,7 @@ export default function DesktopRemoteScanScreen() {
return (
<Screen>
<View style={styles.header}>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Desktop Remote
@@ -62,7 +64,7 @@ export default function DesktopRemoteScanScreen() {
<View style={styles.permissionCard}>
<Ionicons name="camera-outline" size={28} color={colors.accent} />
<Text variant="body">Camera access is needed to scan the desktop pairing QR.</Text>
<Pressable style={styles.primaryButton} onPress={() => void requestPermission()}>
<Pressable android_ripple={ripple.bounded} style={styles.primaryButton} onPress={() => void requestPermission()}>
<Text variant="body" color={colors.accentTextStrong}>
Allow camera
</Text>
+8 -5
View File
@@ -20,6 +20,7 @@ import { Text } from '@/components/Text';
import { SyncConflictDetails } from '@/components/sync/SyncConflictDetails';
import { radius, spacing } from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { formatRelativeTime } from '@/lib/format';
import { getDesktopRemoteConnection } from '@/services/desktopRemoteCredentials';
import { useDesktopSyncStore } from '@/stores/desktopSyncStore';
@@ -67,6 +68,7 @@ function ConflictCard({
onResolve: (resolution: DesktopSyncConflictResolution) => void;
}) {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const [selectedResolution, setSelectedResolution] = useState<DesktopSyncConflictResolution | null>(null);
const desktopSnapshot = syncPlaylistToSnapshot(conflict.remote);
@@ -85,7 +87,7 @@ function ConflictCard({
</Text>
<View style={styles.conflictActions}>
{options.map((resolution) => (
<Pressable
<Pressable android_ripple={ripple.bounded}
key={resolution}
style={[
styles.conflictBtn,
@@ -114,7 +116,7 @@ function ConflictCard({
previewResolution={selectedResolution}
/>
<View style={styles.conflictConfirmRow}>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.primaryButton, (!selectedResolution || busy) && styles.disabled]}
disabled={!selectedResolution || busy}
onPress={() => selectedResolution ? onResolve(selectedResolution) : undefined}
@@ -130,6 +132,7 @@ function ConflictCard({
export default function DesktopSyncScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const status = useDesktopSyncStore((s) => s.status);
@@ -172,7 +175,7 @@ export default function DesktopSyncScreen() {
return (
<Screen>
<View style={styles.topBar}>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Settings
@@ -201,7 +204,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.
</Text>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={styles.primaryButton}
onPress={() => router.push('/desktop-remote' as never)}
>
@@ -225,7 +228,7 @@ export default function DesktopSyncScreen() {
: 'Not synced yet'}
</Text>
</View>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.primaryButton, syncing && styles.disabled]}
disabled={syncing}
onPress={() => void syncNow()}
+4 -2
View File
@@ -10,10 +10,12 @@ 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 type { EQPreset } from '@/types/audio';
export default function EQPresetImportScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const importPreset = useEQStore((state) => state.importPreset);
@@ -34,7 +36,7 @@ export default function EQPresetImportScreen() {
return (
<Screen>
<View style={styles.header}>
<Pressable style={styles.back} onPress={goToEq} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={goToEq} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Equalizer
@@ -44,7 +46,7 @@ export default function EQPresetImportScreen() {
<View style={styles.errorCard}>
<Ionicons name="alert-circle-outline" size={28} color={colors.warning} />
<Text variant="body">This link does not contain a valid Astra EQ preset.</Text>
<Pressable style={styles.primaryButton} onPress={goToEq}>
<Pressable android_ripple={ripple.bounded} style={styles.primaryButton} onPress={goToEq}>
<Text variant="body" color={colors.accentTextStrong}>
Go to Equalizer
</Text>
+5 -3
View File
@@ -22,10 +22,12 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import type { EQPreset } from '@/types/audio';
export default function EQPresetScanScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const importPreset = useEQStore((state) => state.importPreset);
@@ -49,7 +51,7 @@ export default function EQPresetScanScreen() {
return (
<Screen>
<View style={styles.header}>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Equalizer
@@ -67,7 +69,7 @@ export default function EQPresetScanScreen() {
<View style={styles.permissionCard}>
<Ionicons name="camera-outline" size={28} color={colors.accent} />
<Text variant="body">Camera access is needed to scan EQ preset QR codes.</Text>
<Pressable style={styles.primaryButton} onPress={() => void requestPermission()}>
<Pressable android_ripple={ripple.bounded} style={styles.primaryButton} onPress={() => void requestPermission()}>
<Text variant="body" color={colors.accentTextStrong}>
Allow camera
</Text>
@@ -90,7 +92,7 @@ export default function EQPresetScanScreen() {
{error}
</Text>
</View>
<Pressable style={styles.retryButton} onPress={() => setError(null)}>
<Pressable android_ripple={ripple.bounded} style={styles.retryButton} onPress={() => setError(null)}>
<Text variant="label" color={colors.accentTextStrong}>
Scan again
</Text>
+6 -4
View File
@@ -19,6 +19,7 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { useLastFmSettingsStore } from '@/stores/lastFmSettingsStore';
import type { LastFmScrobbleProtocol } from '@/types/lastFm';
@@ -103,6 +104,7 @@ function Field({
export default function LastFmEditScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const { id } = useLocalSearchParams<{ id?: string }>();
@@ -200,7 +202,7 @@ export default function LastFmEditScreen() {
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View style={styles.header}>
<Pressable style={styles.back} onPress={goBack} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={goBack} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
{backLabel}
@@ -219,7 +221,7 @@ export default function LastFmEditScreen() {
<View style={styles.typeCards}>
{PROTOCOL_OPTIONS.map((option) => (
<Pressable
<Pressable android_ripple={ripple.bounded}
key={option.protocol}
style={styles.typeCard}
onPress={() => chooseProtocol(option.protocol)}
@@ -293,7 +295,7 @@ export default function LastFmEditScreen() {
</Text>
) : null}
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.saveButton, !canSubmit || busy ? styles.buttonDisabled : null]}
onPress={() => void onSave()}
disabled={!canSubmit || busy}
@@ -308,7 +310,7 @@ export default function LastFmEditScreen() {
</Pressable>
{editing ? (
<Pressable style={styles.removeButton} onPress={onRemove} accessibilityRole="button">
<Pressable android_ripple={ripple.bounded} style={styles.removeButton} onPress={onRemove} accessibilityRole="button">
<Ionicons name="trash-outline" size={18} color={colors.warning} />
<Text variant="body" color={colors.warning}>
Remove destination
+9 -7
View File
@@ -16,6 +16,7 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { useLastFmSettingsStore } from '@/stores/lastFmSettingsStore';
import { requestLastFmFlush } from '@/services/lastfm';
import type { LastFmProfileStatus } from '@/types/lastFm';
@@ -45,6 +46,7 @@ function customStatusLine(profile: LastFmProfileStatus): { text: string; tone: '
export default function LastFmScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const status = useLastFmSettingsStore((s) => s.status);
@@ -125,7 +127,7 @@ export default function LastFmScreen() {
{profile.username}
</Text>
) : null}
<Pressable
<Pressable android_ripple={ripple.bounded}
onPress={() => confirmDisconnect(profile)}
hitSlop={8}
accessibilityLabel="Disconnect Last.fm"
@@ -134,7 +136,7 @@ export default function LastFmScreen() {
</Pressable>
</View>
) : (
<Pressable
<Pressable android_ripple={ripple.bounded}
style={styles.connectButton}
onPress={() => connectOfficial(profile)}
accessibilityRole="button"
@@ -152,7 +154,7 @@ export default function LastFmScreen() {
const renderCustom = (profile: LastFmProfileStatus) => {
const line = customStatusLine(profile);
return (
<Pressable
<Pressable android_ripple={ripple.bounded}
key={profile.id}
style={styles.row}
onPress={() => router.push({ pathname: '/lastfm/edit', params: { id: profile.id } })}
@@ -195,13 +197,13 @@ export default function LastFmScreen() {
return (
<Screen>
<View style={styles.header}>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Settings
</Text>
</Pressable>
<Pressable
<Pressable android_ripple={ripple.bounded}
onPress={() => router.push('/lastfm/edit')}
hitSlop={8}
accessibilityLabel="Add destination"
@@ -249,7 +251,7 @@ export default function LastFmScreen() {
) : null}
{status && status.pendingScrobbles > 0 ? (
<Pressable style={styles.retryButton} onPress={() => requestLastFmFlush()}>
<Pressable android_ripple={ripple.bounded} style={styles.retryButton} onPress={() => requestLastFmFlush()}>
<Ionicons name="sync" size={16} color={colors.accentText} />
<Text variant="label" color={colors.accentText}>
Retry {status.pendingScrobbles} queued now
@@ -271,7 +273,7 @@ export default function LastFmScreen() {
)}
</View>
<Pressable style={styles.addButton} onPress={() => router.push('/lastfm/edit')}>
<Pressable android_ripple={ripple.bounded} style={styles.addButton} onPress={() => router.push('/lastfm/edit')}>
<Ionicons name="add" size={18} color={colors.accentTextStrong} />
<Text variant="body" color={colors.accentTextStrong}>
Add destination
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { View } from 'react-native';
import { useRouter } from 'expo-router';
import { getNotificationClickRedirectPath } from '@/audio/notificationIntent';
import { resolveNotificationClick } from '@/audio/notificationIntent';
import { createThemedStyles } from '@/theme/themed';
export default function NotificationClickRoute() {
@@ -11,7 +11,7 @@ export default function NotificationClickRoute() {
useEffect(() => {
let cancelled = false;
getNotificationClickRedirectPath()
resolveNotificationClick()
.then((href) => {
if (!cancelled) router.replace(href);
})
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -13,6 +13,7 @@ import { TrackRow } from '@/components/library/TrackRow';
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
import { spacing } from '@/theme';
import { useColors } from '@/theme/themed';
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
import { useLibraryStore } from '@/stores/libraryStore';
import { usePlayerStore } from '@/stores/playerStore';
import { playTracks } from '@/audio/playbackController';
@@ -37,6 +38,7 @@ function EmptyList() {
export default function RecentlyPlayedScreen() {
const colors = useColors();
const ripple = useRipple();
const router = useRouter();
const tracks = useLibraryStore((s) => s.recentlyPlayedTracks);
const currentPath = usePlayerStore((s) => s.currentTrack?.path);
@@ -49,7 +51,7 @@ export default function RecentlyPlayedScreen() {
return (
<Screen>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Home
+6 -4
View File
@@ -18,6 +18,7 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { useRemoteSourcesStore } from '@/stores/remoteSourcesStore';
import type { RemoteSourceType } from '@/types/remote';
@@ -84,6 +85,7 @@ function Field({
export default function SourceEditScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const { id } = useLocalSearchParams<{ id?: string }>();
@@ -187,7 +189,7 @@ export default function SourceEditScreen() {
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View style={styles.header}>
<Pressable style={styles.back} onPress={goBack} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={goBack} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
{backLabel}
@@ -206,7 +208,7 @@ export default function SourceEditScreen() {
<View style={styles.typeCards}>
{TYPE_OPTIONS.map((option) => (
<Pressable
<Pressable android_ripple={ripple.bounded}
key={option.type}
style={styles.typeCard}
onPress={() => chooseType(option.type)}
@@ -274,7 +276,7 @@ export default function SourceEditScreen() {
) : null}
<View style={styles.actions}>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[styles.button, styles.secondaryButton, busy ? styles.buttonDisabled : null]}
onPress={() => void onTest()}
disabled={!!busy}
@@ -287,7 +289,7 @@ export default function SourceEditScreen() {
</Text>
)}
</Pressable>
<Pressable
<Pressable android_ripple={ripple.bounded}
style={[
styles.button,
styles.primaryButton,
+7 -5
View File
@@ -16,6 +16,7 @@ import {
spacing,
} from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { useRemoteSourcesStore } from '@/stores/remoteSourcesStore';
import type { RemoteSourceRow, RemoteSyncProgress } from '@/types/remote';
@@ -39,6 +40,7 @@ function statusLine(
export default function SourcesScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const sources = useRemoteSourcesStore((s) => s.sources);
@@ -103,7 +105,7 @@ export default function SourcesScreen() {
return (
<Screen>
<View style={styles.header}>
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Settings
@@ -111,11 +113,11 @@ export default function SourcesScreen() {
</Pressable>
<View style={styles.headerActions}>
{sources.length > 0 ? (
<Pressable onPress={() => void syncAll()} hitSlop={8} accessibilityLabel="Sync all">
<Pressable android_ripple={ripple.bounded} onPress={() => void syncAll()} hitSlop={8} accessibilityLabel="Sync all">
<Ionicons name="sync" size={20} color={colors.textSecondary} />
</Pressable>
) : null}
<Pressable
<Pressable android_ripple={ripple.bounded}
onPress={() => router.push('/sources/edit')}
hitSlop={8}
accessibilityLabel="Add server"
@@ -137,7 +139,7 @@ export default function SourcesScreen() {
No servers yet. Add a Subsonic or Jellyfin server to stream and browse your
self-hosted library.
</Text>
<Pressable style={styles.addButton} onPress={() => router.push('/sources/edit')}>
<Pressable android_ripple={ripple.bounded} style={styles.addButton} onPress={() => router.push('/sources/edit')}>
<Ionicons name="add" size={18} color={colors.accentTextStrong} />
<Text variant="body" color={colors.accentTextStrong}>
Add server
@@ -148,7 +150,7 @@ export default function SourcesScreen() {
sources.map((source) => {
const status = statusLine(source, progressById[source.id] ?? null);
return (
<Pressable
<Pressable android_ripple={ripple.bounded}
key={source.id}
style={styles.row}
onPress={() => setActionFor(source)}