remove ripple

This commit is contained in:
Boof2015
2026-08-11 17:42:57 -04:00
parent 95a28b3c50
commit ed58110171
89 changed files with 843 additions and 835 deletions
+9
View File
@@ -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.",
},
],
},
}
]);
@@ -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) {
@@ -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),
+2 -2
View File
@@ -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),
+1
View File
@@ -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",
+1
View File
@@ -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',
+9 -11
View File
@@ -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 = (
<Pressable android_ripple={ripple.bounded}
<AppPressable
style={[styles.presetRow, isWide && styles.sideItem]}
onPress={() => setSheet('preset')}
>
@@ -317,7 +315,7 @@ export default function EQScreen() {
{presetName}
</Text>
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
</Pressable>
</AppPressable>
);
const modeSwitcherEl = (
@@ -436,7 +434,7 @@ export default function EQScreen() {
onValuePress={() => setEditingValue('preamp')}
/>
</View>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={[styles.eqToggle, eq.enabled && styles.eqToggleOn]}
onPress={() => {
playHaptic(hapticForToggle(!eq.enabled));
@@ -451,7 +449,7 @@ export default function EQScreen() {
<Text variant="label" color={eq.enabled ? colors.accentTextStrong : colors.textSecondary}>
{eq.enabled ? 'EQ on' : 'EQ off'}
</Text>
</Pressable>
</AppPressable>
</View>
);
@@ -470,12 +468,12 @@ export default function EQScreen() {
</Text>
</View>
<View style={styles.headerActions}>
<Pressable android_ripple={ripple.bounded} style={styles.iconButton} onPress={() => setSheet('save')} hitSlop={8}>
<AppPressable feedback="control" style={styles.iconButton} onPress={() => setSheet('save')} hitSlop={8}>
<Ionicons name="save-outline" size={20} color={colors.textSecondary} />
</Pressable>
<Pressable android_ripple={ripple.bounded} style={styles.iconButton} onPress={() => setSheet('overflow')} hitSlop={8}>
</AppPressable>
<AppPressable feedback="control" style={styles.iconButton} onPress={() => setSheet('overflow')} hitSlop={8}>
<Ionicons name="ellipsis-vertical" size={20} color={colors.textSecondary} />
</Pressable>
</AppPressable>
</View>
</View>
+26 -32
View File
@@ -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 = (
<Pressable
<AppPressable
style={styles.mastheadSearch}
android_ripple={ripple.icon(22)}
feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={onSearch}
accessibilityRole="button"
@@ -200,13 +198,13 @@ function HomeMasthead({
hitSlop={4}
>
<Ionicons name="search" size={22} color={colors.textPrimary} />
</Pressable>
</AppPressable>
);
const scanButton = (
<Pressable
<AppPressable
style={styles.mastheadSearch}
android_ripple={ripple.icon(22)}
feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={onScan}
accessibilityRole="button"
@@ -214,7 +212,7 @@ function HomeMasthead({
hitSlop={4}
>
<Ionicons name="scan-outline" size={22} color={colors.textPrimary} />
</Pressable>
</AppPressable>
);
const utilityButtons = (
@@ -265,7 +263,6 @@ function SectionHeader({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
return (
<View style={styles.sectionHeader}>
<View style={styles.sectionTitleGroup}>
@@ -279,12 +276,12 @@ function SectionHeader({
) : null}
</View>
{onActionPress && actionLabel ? (
<Pressable style={styles.seeAllButton} android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onActionPress} accessibilityRole="button">
<AppPressable feedback="control" style={styles.seeAllButton} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onActionPress} accessibilityRole="button">
<Text variant="label" color={colors.accentText}>
{actionLabel}
</Text>
<Ionicons name="chevron-forward" size={14} color={colors.accentText} />
</Pressable>
</AppPressable>
) : null}
</View>
);
@@ -355,9 +352,8 @@ function RecentlyAddedAlbum({
onPress: () => void;
}) {
const styles = useStyles();
const ripple = useRipple();
return (
<Pressable style={[styles.recentAlbum, { width: size }]} android_ripple={ripple.tile} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onPress} accessibilityRole="button">
<AppPressable style={[styles.recentAlbum, { width: size }]} feedback="tile" unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onPress} accessibilityRole="button">
<AlbumCover album={album} size={size} />
<Text variant="body" numberOfLines={1} style={styles.recentAlbumTitle}>
{album.album}
@@ -365,7 +361,7 @@ function RecentlyAddedAlbum({
<Text variant="label" numberOfLines={1}>
{album.artist}
</Text>
</Pressable>
</AppPressable>
);
}
@@ -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 (
<Pressable
<AppPressable
style={[styles.randomCard, style]}
android_ripple={ripple.tile}
feedback="tile"
unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={onOpen}
accessibilityRole="button"
@@ -428,8 +423,8 @@ function RandomSpotlightCard({
{meta}
</Text>
<View style={styles.randomActions}>
<Pressable
android_ripple={ripple.onAccent()}
<AppPressable
feedback="accent"
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.randomPrimaryAction, disabled && styles.buttonDisabled]}
disabled={disabled}
@@ -439,9 +434,9 @@ function RandomSpotlightCard({
accessibilityLabel={`Play ${title}`}
>
<Ionicons name="play" size={16} color={colors.bgPrimary} />
</Pressable>
<Pressable
android_ripple={ripple.icon(18)}
</AppPressable>
<AppPressable
feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.randomAction, disabled && styles.buttonDisabled]}
disabled={disabled}
@@ -451,9 +446,9 @@ function RandomSpotlightCard({
accessibilityLabel={`Shuffle ${title}`}
>
<Ionicons name="shuffle" size={17} color={colors.accent} />
</Pressable>
<Pressable
android_ripple={ripple.icon(18)}
</AppPressable>
<AppPressable
feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.randomAction}
onPress={(event) => runAction(event, onReroll)}
@@ -462,11 +457,11 @@ function RandomSpotlightCard({
accessibilityLabel="Pick another random album or artist"
>
<Ionicons name="refresh" size={17} color={colors.textSecondary} />
</Pressable>
</AppPressable>
</View>
</View>
</View>
</Pressable>
</AppPressable>
);
}
@@ -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({
</Text>
) : null}
</View>
<Pressable
android_ripple={ripple.onAccent()} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable
feedback="accent" unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.primaryButton}
onPress={onManageFolders}
accessibilityRole="button"
@@ -527,7 +521,7 @@ function EmptyHomeCard({
<Text variant="body" style={styles.primaryButtonText}>
{fatal ? 'Troubleshooting' : 'Folder settings'}
</Text>
</Pressable>
</AppPressable>
</View>
);
}
+5 -8
View File
@@ -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 (
<View style={styles.sectionHeader}>
@@ -499,12 +497,12 @@ function SectionHeader({
</Text>
</View>
{onPress ? (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.seeAllButton} onPress={onPress} accessibilityRole="button">
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.seeAllButton} onPress={onPress} accessibilityRole="button">
<Text variant="label" color={colors.accentText}>
See all
</Text>
<Ionicons name="chevron-forward" size={14} color={colors.accentText} />
</Pressable>
</AppPressable>
) : null}
</View>
);
@@ -518,7 +516,6 @@ function AlbumRail({
onAlbumPress: (album: ArtistAlbum) => void;
}) {
const styles = useStyles();
const ripple = useRipple();
return (
<ScrollView
horizontal
@@ -526,7 +523,7 @@ function AlbumRail({
contentContainerStyle={styles.albumRail}
>
{albums.map((album) => (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY}
key={album.identity_key}
style={styles.albumCard}
onPress={() => onAlbumPress(album)}
@@ -552,7 +549,7 @@ function AlbumRail({
.filter(Boolean)
.join(' - ')}
</Text>
</Pressable>
</AppPressable>
))}
</ScrollView>
);
+10 -12
View File
@@ -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 ? (
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
hitSlop={8}
style={styles.switcherSearch}
onPress={() => openQuickSearch()}
@@ -692,7 +690,7 @@ export default function LibraryScreen() {
accessibilityLabel="Search library"
>
<Ionicons name="search" size={22} color={colors.textSecondary} />
</Pressable>
</AppPressable>
) : null}
</View>
@@ -717,15 +715,15 @@ export default function LibraryScreen() {
<Text variant="label">
{selectedIds.size} selected
</Text>
<Pressable android_ripple={ripple.bounded} onPress={exitSelection} hitSlop={8} accessibilityRole="button">
<AppPressable feedback="control" onPress={exitSelection} hitSlop={8} accessibilityRole="button">
<Text variant="label" color={colors.accentText}>
Cancel
</Text>
</Pressable>
</AppPressable>
</View>
) : sortable ? (
<View style={[styles.controlsRow, { height: CHROME_CONTROLS_H }]}>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={styles.sortTrigger}
onPress={() => setSortSheetOpen(true)}
accessibilityRole="button"
@@ -733,10 +731,10 @@ export default function LibraryScreen() {
>
<Ionicons name="swap-vertical" size={14} color={colors.textSecondary} />
<Text variant="label">{sortLabel}</Text>
</Pressable>
</AppPressable>
{activeLayout && activeLayoutLabel ? (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={styles.layoutTrigger}
hitSlop={8}
onPress={() => setLayoutSheetOpen(true)}
@@ -748,7 +746,7 @@ export default function LibraryScreen() {
size={18}
color={colors.textSecondary}
/>
</Pressable>
</AppPressable>
) : null}
</View>
) : null}
+6 -9
View File
@@ -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 (
<Pressable
android_ripple={ripple.bounded}
<AppPressable
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.missingRow}
onLongPress={() => {
@@ -78,7 +76,7 @@ function MissingRow({ entry, onLongPress }: { entry: PlaylistTrackEntry; onLongP
</Text>
</View>
<Ionicons name="alert-circle-outline" size={18} color={colors.warning} />
</Pressable>
</AppPressable>
);
}
@@ -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={<Text variant="label">{meta}</Text>}
heroExtra={
isDynamic && playlistId != null ? (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.editRules}
onPress={() =>
router.push({
@@ -343,7 +340,7 @@ export default function PlaylistScreen() {
<Text variant="label" color={colors.accent}>
Rules
</Text>
</Pressable>
</AppPressable>
) : null
}
disabled={playable.length === 0}
@@ -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 (
<View style={styles.sheetActions}>
<Pressable android_ripple={ripple.bounded} style={[styles.sheetButton, styles.cancelButton]} onPress={onCancel} accessibilityRole="button">
<AppPressable feedback="control" style={[styles.sheetButton, styles.cancelButton]} onPress={onCancel} accessibilityRole="button">
<Text variant="label" color={colors.textSecondary}>
Cancel
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="accent"
style={[styles.sheetButton, styles.applyButton, disabled && styles.applyDisabled]}
disabled={disabled}
onPress={onApply}
@@ -347,7 +345,7 @@ function DraftActions({
<Text variant="label" color={colors.accentTextStrong}>
Apply
</Text>
</Pressable>
</AppPressable>
</View>
);
}
@@ -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 (
<AppSheet onClose={onCancel} scrollable>
<AppSheetTitle title={target.mode === 'new' ? 'Add filter' : 'Edit filter'} />
<Pressable android_ripple={ripple.bounded} style={styles.sheetSelectRow} onPress={onChangeField} accessibilityRole="button">
<AppPressable style={styles.sheetSelectRow} onPress={onChangeField} accessibilityRole="button">
<View style={styles.sheetSelectText}>
<Text variant="caption" color={colors.textTertiary}>
FIELD
@@ -548,7 +545,7 @@ function ConditionEditorSheet({
<Text variant="body">{fieldLabel(draft)}</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
<View style={styles.sheetBlock}>
<Text variant="caption" color={colors.textTertiary}>
@@ -572,12 +569,12 @@ function ConditionEditorSheet({
<View style={styles.conditionSheetFooter}>
{target.mode === 'edit' ? (
<Pressable android_ripple={ripple.bounded} style={styles.removeButton} onPress={onRemove} accessibilityRole="button">
<AppPressable feedback="control" style={styles.removeButton} onPress={onRemove} accessibilityRole="button">
<Ionicons name="trash-outline" size={17} color={colors.warning} />
<Text variant="label" color={colors.warning}>
Remove
</Text>
</Pressable>
</AppPressable>
) : (
<View />
)}
@@ -717,12 +714,11 @@ function FilterCard({
onRemove: () => void;
}) {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const option = fieldOptionForKey(getConditionFieldKey(condition));
return (
<Pressable android_ripple={ripple.bounded} style={styles.ruleCard} onPress={onPress} accessibilityRole="button">
<AppPressable style={styles.ruleCard} onPress={onPress} accessibilityRole="button">
<View style={styles.cardIcon}>
<Ionicons name={option?.icon ?? 'options-outline'} size={18} color={colors.accent} />
</View>
@@ -734,7 +730,7 @@ function FilterCard({
{fieldGroupLabel(condition)}
</Text>
</View>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={styles.cardRemove}
onPress={onRemove}
hitSlop={8}
@@ -742,15 +738,14 @@ function FilterCard({
accessibilityLabel="Remove filter"
>
<Ionicons name="close" size={18} color={colors.textTertiary} />
</Pressable>
</Pressable>
</AppPressable>
</AppPressable>
);
}
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() {
>
<Screen padded={false}>
<View style={styles.header}>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
onPress={() => router.back()}
hitSlop={8}
style={styles.headerButton}
@@ -1061,7 +1056,7 @@ export default function DynamicPlaylistEditorScreen() {
accessibilityLabel="Back"
>
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
</Pressable>
</AppPressable>
<Text variant="heading" numberOfLines={1} style={styles.headerTitle}>
{isEditing ? 'Edit dynamic playlist' : 'New dynamic playlist'}
</Text>
@@ -1097,7 +1092,7 @@ export default function DynamicPlaylistEditorScreen() {
contentContainerStyle={styles.presetRow}
>
{DYNAMIC_PLAYLIST_PRESETS.map((preset) => (
<Pressable android_ripple={ripple.bounded}
<AppPressable
key={preset.id}
style={styles.presetChip}
onPress={() => applyPreset(preset.rules)}
@@ -1107,7 +1102,7 @@ export default function DynamicPlaylistEditorScreen() {
<Text variant="label" color={colors.accentText}>
{preset.label}
</Text>
</Pressable>
</AppPressable>
))}
</ScrollView>
</View>
@@ -1117,12 +1112,12 @@ export default function DynamicPlaylistEditorScreen() {
<Text variant="caption" style={styles.sectionLabel}>
FILTERS
</Text>
<Pressable android_ripple={ripple.bounded} style={styles.inlineAction} onPress={() => openFieldPicker('new')} accessibilityRole="button">
<AppPressable feedback="control" style={styles.inlineAction} onPress={() => openFieldPicker('new')} accessibilityRole="button">
<Ionicons name="add" size={16} color={colors.accent} />
<Text variant="label" color={colors.accent}>
Add filter
</Text>
</Pressable>
</AppPressable>
</View>
{rules.conditions.length === 0 ? (
@@ -1150,7 +1145,7 @@ export default function DynamicPlaylistEditorScreen() {
<Text variant="caption" style={styles.sectionLabel}>
ORDER
</Text>
<Pressable android_ripple={ripple.bounded} style={styles.sortCard} onPress={openSortSheet} accessibilityRole="button">
<AppPressable style={styles.sortCard} onPress={openSortSheet} accessibilityRole="button">
<View style={styles.cardIcon}>
<Ionicons name="swap-vertical" size={18} color={colors.accent} />
</View>
@@ -1161,7 +1156,7 @@ export default function DynamicPlaylistEditorScreen() {
</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
</View>
</ScrollView>
@@ -1180,7 +1175,7 @@ export default function DynamicPlaylistEditorScreen() {
{status.label}
</Text>
</View>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={[styles.previewButton, normalizedRulesError !== null && styles.disabled]}
disabled={normalizedRulesError !== null}
onPress={() => setSheet({ kind: 'preview' })}
@@ -1190,8 +1185,8 @@ export default function DynamicPlaylistEditorScreen() {
<Text variant="label" color={colors.textSecondary}>
Preview
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="accent"
style={[styles.saveButton, saveDisabled && styles.disabled]}
disabled={saveDisabled}
onPress={save}
@@ -1200,7 +1195,7 @@ export default function DynamicPlaylistEditorScreen() {
<Text variant="label" color={colors.accentTextStrong}>
{isSaving ? 'Saving' : 'Save'}
</Text>
</Pressable>
</AppPressable>
</View>
{renderSheet()}
+6 -8
View File
@@ -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 (
<Pressable
<AppPressable
style={[styles.rankingRow, !item.available && styles.unavailable]}
android_ripple={item.available ? ripple.bounded : undefined}
disabled={!item.available}
onPress={onPress}
accessibilityRole="button"
@@ -187,7 +186,7 @@ function RankingRow({
{formatListeningTime(item.listenedSeconds, true)}
</Text>
</View>
</Pressable>
</AppPressable>
);
}
@@ -206,16 +205,15 @@ function EmptyState({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
return (
<View style={styles.empty}>
<Ionicons name={icon} size={34} color={colors.textTertiary} />
<Text variant="heading">{title}</Text>
<Text variant="body" color={colors.textSecondary} style={styles.emptyBody}>{body}</Text>
{action && onAction ? (
<Pressable style={styles.primaryButton} android_ripple={ripple.onAccent()} onPress={onAction}>
<AppPressable style={styles.primaryButton} feedback="accent" onPress={onAction}>
<Text variant="body" style={styles.primaryButtonText}>{action}</Text>
</Pressable>
</AppPressable>
) : null}
</View>
);
+23 -26
View File
@@ -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 (
<Pressable android_ripple={ripple.bounded}
<AppPressable
style={[styles.discoveredRow, disabled && styles.buttonDisabled]}
onPress={() => onPair(desktop)}
disabled={disabled}
@@ -217,13 +215,12 @@ function DiscoveredDesktopRow({ desktop, onPair, disabled }: {
</Text>
<Ionicons name="keypad-outline" size={17} color={colors.accent} />
</View>
</Pressable>
</AppPressable>
);
}
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"
/>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={[
styles.primaryButton,
(normalizedPinInput.length !== 6 || !pinPairingActive) && styles.buttonDisabled,
@@ -448,7 +445,7 @@ export default function DesktopRemoteScreen() {
<Text variant="body" color={colors.accentTextStrong}>
Confirm PIN
</Text>
</Pressable>
</AppPressable>
</View>
) : null}
</View>
@@ -462,12 +459,12 @@ export default function DesktopRemoteScreen() {
Open Astra Desktop settings, enable Phone Remote, then scan or paste the pairing link.
</Text>
<View style={styles.actionRow}>
<Pressable android_ripple={ripple.bounded} style={styles.primaryButton} onPress={() => router.push('/desktop-remote/scan' as never)}>
<AppPressable feedback="accent" 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
</Text>
</Pressable>
</AppPressable>
</View>
<TextInput
style={styles.input}
@@ -479,7 +476,7 @@ export default function DesktopRemoteScreen() {
autoCorrect={false}
keyboardType="url"
/>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={[styles.secondaryButton, !pairingLink.trim() && styles.buttonDisabled]}
disabled={!pairingLink.trim()}
onPress={() => void pairFromInput(pairingLink)}
@@ -487,7 +484,7 @@ export default function DesktopRemoteScreen() {
<Text variant="body" color={pairingLink.trim() ? colors.textPrimary : colors.textTertiary}>
Pair from link
</Text>
</Pressable>
</AppPressable>
</View>
<View style={styles.card}>
@@ -507,7 +504,7 @@ export default function DesktopRemoteScreen() {
autoCorrect={false}
keyboardType="url"
/>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={[
styles.secondaryButton,
!manualBaseUrl.trim() && styles.buttonDisabled,
@@ -521,7 +518,7 @@ export default function DesktopRemoteScreen() {
>
Request secure PIN
</Text>
</Pressable>
</AppPressable>
</View>
{pinPairing ? (
@@ -560,9 +557,9 @@ export default function DesktopRemoteScreen() {
>
<View style={[styles.remoteShell, { width: remoteLayout.contentWidth }]}>
<View style={styles.remoteNowHeader}>
<Pressable android_ripple={ripple.bounded} style={styles.headerBtn} onPress={() => router.back()} hitSlop={12}>
<AppPressable feedback="control" style={styles.headerBtn} onPress={() => router.back()} hitSlop={12}>
<Ionicons name="chevron-down" size={26} color={colors.textSecondary} />
</Pressable>
</AppPressable>
<View style={styles.headerMid}>
<Text variant="caption" style={styles.eyebrow}>
DESKTOP REMOTE
@@ -571,14 +568,14 @@ export default function DesktopRemoteScreen() {
{remoteSource}
</Text>
</View>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={styles.headerBtn}
onPress={() => void reconnect()}
hitSlop={12}
accessibilityLabel="Reconnect to desktop"
>
<Ionicons name="refresh" size={20} color={colors.textSecondary} />
</Pressable>
</AppPressable>
</View>
<View style={styles.managePanel}>
@@ -619,7 +616,7 @@ export default function DesktopRemoteScreen() {
</View>
<View style={styles.manageActions}>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={styles.primaryButton}
onPress={() => {
// The player is an overlay, not a route: open it and pop this
@@ -633,17 +630,17 @@ export default function DesktopRemoteScreen() {
<Text variant="body" color={colors.accentTextStrong}>
Open Now Playing
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded} style={styles.secondaryButton} onPress={() => void reconnect()}>
</AppPressable>
<AppPressable feedback="control" style={styles.secondaryButton} onPress={() => void reconnect()}>
<Ionicons name="refresh" size={18} color={colors.textPrimary} />
<Text variant="body">Reconnect</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded} style={styles.dangerButton} onPress={confirmForget}>
</AppPressable>
<AppPressable feedback="control" style={styles.dangerButton} onPress={confirmForget}>
<Ionicons name="trash-outline" size={18} color={colors.warning} />
<Text variant="body" color={colors.warning}>
Forget desktop
</Text>
</Pressable>
</AppPressable>
</View>
</View>
</View>
@@ -673,12 +670,12 @@ export default function DesktopRemoteScreen() {
) : (
<>
<View style={styles.topBar}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<AppPressable feedback="control" 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>
</AppPressable>
</View>
{renderSetup()}
</>
+5 -7
View File
@@ -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 (
<Screen>
<View style={styles.header}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<AppPressable feedback="control" 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
</Text>
</Pressable>
</AppPressable>
</View>
<Text variant="title" style={styles.heading}>
@@ -64,11 +62,11 @@ 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 android_ripple={ripple.bounded} style={styles.primaryButton} onPress={() => void requestPermission()}>
<AppPressable feedback="accent" style={styles.primaryButton} onPress={() => void requestPermission()}>
<Text variant="body" color={colors.accentTextStrong}>
Allow camera
</Text>
</Pressable>
</AppPressable>
</View>
) : (
<View style={styles.scannerFrame}>
+11 -14
View File
@@ -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<DesktopSyncConflictResolution | null>(null);
const desktopSnapshot = syncPlaylistToSnapshot(conflict.remote);
@@ -87,7 +85,7 @@ function ConflictCard({
</Text>
<View style={styles.conflictActions}>
{options.map((resolution) => (
<Pressable android_ripple={ripple.bounded}
<AppPressable
key={resolution}
style={[
styles.conflictBtn,
@@ -98,7 +96,7 @@ function ConflictCard({
onPress={() => setSelectedResolution(resolution)}
>
<Text variant="label">{RESOLUTION_LABELS[resolution]}</Text>
</Pressable>
</AppPressable>
))}
</View>
<View style={styles.previewBox}>
@@ -116,7 +114,7 @@ function ConflictCard({
previewResolution={selectedResolution}
/>
<View style={styles.conflictConfirmRow}>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={[styles.primaryButton, (!selectedResolution || busy) && styles.disabled]}
disabled={!selectedResolution || busy}
onPress={() => selectedResolution ? onResolve(selectedResolution) : undefined}
@@ -124,7 +122,7 @@ function ConflictCard({
<Text variant="body" color={colors.accentTextStrong}>
Confirm
</Text>
</Pressable>
</AppPressable>
</View>
</View>
);
@@ -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 (
<Screen>
<View style={styles.topBar}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<AppPressable feedback="control" 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>
</AppPressable>
</View>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.content}>
<View style={styles.hero}>
@@ -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.
</Text>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={styles.primaryButton}
onPress={() => router.push('/desktop-remote' as never)}
>
@@ -212,7 +209,7 @@ export default function DesktopSyncScreen() {
<Text variant="body" color={colors.accentTextStrong}>
Pair with a desktop
</Text>
</Pressable>
</AppPressable>
</View>
) : (
<>
@@ -228,7 +225,7 @@ export default function DesktopSyncScreen() {
: 'Not synced yet'}
</Text>
</View>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={[styles.primaryButton, (syncing || !desktopSyncEnabled) && styles.disabled]}
disabled={syncing || !desktopSyncEnabled}
onPress={() => void syncNow()}
@@ -242,7 +239,7 @@ export default function DesktopSyncScreen() {
<Text variant="body" color={colors.accentTextStrong}>
Sync now
</Text>
</Pressable>
</AppPressable>
</View>
{summaryLine ? (
<Text variant="caption" color={colors.textTertiary}>
+6 -7
View File
@@ -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 (
<Screen>
<View style={styles.header}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={goToEq} hitSlop={8}>
<AppPressable feedback="control" style={styles.back} onPress={goToEq} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Equalizer
</Text>
</Pressable>
</AppPressable>
</View>
<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 android_ripple={ripple.bounded} style={styles.primaryButton} onPress={goToEq}>
<AppPressable feedback="accent" style={styles.primaryButton} onPress={goToEq}>
<Text variant="body" color={colors.accentTextStrong}>
Go to Equalizer
</Text>
</Pressable>
</AppPressable>
</View>
</Screen>
);
+7 -9
View File
@@ -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 (
<Screen>
<View style={styles.header}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<AppPressable feedback="control" style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Equalizer
</Text>
</Pressable>
</AppPressable>
</View>
<Text variant="title" style={styles.heading}>
@@ -71,11 +69,11 @@ 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 android_ripple={ripple.bounded} style={styles.primaryButton} onPress={() => void requestPermission()}>
<AppPressable feedback="accent" style={styles.primaryButton} onPress={() => void requestPermission()}>
<Text variant="body" color={colors.accentTextStrong}>
Allow camera
</Text>
</Pressable>
</AppPressable>
</View>
) : (
<View style={styles.scannerFrame}>
@@ -94,11 +92,11 @@ export default function EQPresetScanScreen() {
{error}
</Text>
</View>
<Pressable android_ripple={ripple.bounded} style={styles.retryButton} onPress={() => setError(null)}>
<AppPressable feedback="control" style={styles.retryButton} onPress={() => setError(null)}>
<Text variant="label" color={colors.accentTextStrong}>
Scan again
</Text>
</Pressable>
</AppPressable>
</View>
) : null}
</View>
+9 -11
View File
@@ -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}
>
<View style={styles.header}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={goBack} hitSlop={8}>
<AppPressable feedback="control" style={styles.back} onPress={goBack} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
{backLabel}
</Text>
</Pressable>
</AppPressable>
</View>
{step === 'type' ? (
@@ -221,7 +219,7 @@ export default function LastFmEditScreen() {
<View style={styles.typeCards}>
{PROTOCOL_OPTIONS.map((option) => (
<Pressable android_ripple={ripple.bounded}
<AppPressable
key={option.protocol}
style={styles.typeCard}
onPress={() => chooseProtocol(option.protocol)}
@@ -241,7 +239,7 @@ export default function LastFmEditScreen() {
</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
))}
</View>
</ScrollView>
@@ -295,7 +293,7 @@ export default function LastFmEditScreen() {
</Text>
) : null}
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={[styles.saveButton, !canSubmit || busy ? styles.buttonDisabled : null]}
onPress={() => void onSave()}
disabled={!canSubmit || busy}
@@ -307,15 +305,15 @@ export default function LastFmEditScreen() {
{editing ? 'Save' : 'Add destination'}
</Text>
)}
</Pressable>
</AppPressable>
{editing ? (
<Pressable android_ripple={ripple.bounded} style={styles.removeButton} onPress={onRemove} accessibilityRole="button">
<AppPressable feedback="control" style={styles.removeButton} onPress={onRemove} accessibilityRole="button">
<Ionicons name="trash-outline" size={18} color={colors.warning} />
<Text variant="body" color={colors.warning}>
Remove destination
</Text>
</Pressable>
</AppPressable>
) : null}
</ScrollView>
)}
+11 -13
View File
@@ -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}
</Text>
) : null}
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
onPress={() => confirmDisconnect(profile)}
hitSlop={8}
accessibilityLabel="Disconnect Last.fm"
>
<Ionicons name="close-circle" size={22} color={colors.textTertiary} />
</Pressable>
</AppPressable>
</View>
) : (
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={styles.connectButton}
onPress={() => connectOfficial(profile)}
accessibilityRole="button"
@@ -155,7 +153,7 @@ export default function LastFmScreen() {
<Text variant="label" color={colors.accentTextStrong}>
Connect
</Text>
</Pressable>
</AppPressable>
)}
</View>
);
@@ -164,7 +162,7 @@ export default function LastFmScreen() {
const renderCustom = (profile: LastFmProfileStatus) => {
const line = customStatusLine(profile);
return (
<Pressable android_ripple={ripple.bounded}
<AppPressable
key={profile.id}
style={styles.row}
onPress={() => router.push({ pathname: '/lastfm/edit', params: { id: profile.id } })}
@@ -200,7 +198,7 @@ export default function LastFmScreen() {
) : (
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
)}
</Pressable>
</AppPressable>
);
};
@@ -248,12 +246,12 @@ export default function LastFmScreen() {
) : null}
{status && status.pendingScrobbles > 0 ? (
<Pressable android_ripple={ripple.bounded} style={styles.retryButton} onPress={() => requestLastFmFlush()}>
<AppPressable feedback="control" style={styles.retryButton} onPress={() => requestLastFmFlush()}>
<Ionicons name="sync" size={16} color={colors.accentText} />
<Text variant="label" color={colors.accentText}>
Retry {status.pendingScrobbles} queued now
</Text>
</Pressable>
</AppPressable>
) : null}
<Text
@@ -270,12 +268,12 @@ export default function LastFmScreen() {
)}
</View>
<Pressable android_ripple={ripple.bounded} style={styles.addButton} onPress={() => router.push('/lastfm/edit')}>
<AppPressable feedback="accent" style={styles.addButton} onPress={() => router.push('/lastfm/edit')}>
<Ionicons name="add" size={18} color={colors.accentTextStrong} />
<Text variant="body" color={colors.accentTextStrong}>
Add destination
</Text>
</Pressable>
</AppPressable>
<Text variant="caption" color={colors.textTertiary} style={styles.footnote}>
A scrobble is sent once a track plays past half its length (or 4 minutes). Tracks under 30
+8 -10
View File
@@ -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<HapticCapabilities>(() =>
AstraHaptics.getCapabilities()
);
@@ -97,14 +96,14 @@ export default function HapticsLabScreen() {
label="Touch feedback enabled"
value={yesNo(capabilities.touchFeedbackEnabled)}
/>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={styles.refreshButton}
onPress={refresh}
accessibilityRole="button"
>
<Text variant="label" color={colors.accentTextStrong}>Refresh capabilities</Text>
</Pressable>
</AppPressable>
</SettingsCard>
<SettingsSectionLabel spaced>SEMANTIC VOCABULARY</SettingsSectionLabel>
@@ -259,10 +258,9 @@ function AuditionButton({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
return (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
disabled={disabled}
onPress={onPress}
onLongPress={onLongPress}
@@ -274,7 +272,7 @@ function AuditionButton({
<Text variant="label" color={disabled ? colors.textTertiary : colors.accentTextStrong}>
{label}
</Text>
</Pressable>
</AppPressable>
);
}
+5 -6
View File
@@ -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() {
}}
/>
<View style={styles.divider} />
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={styles.clearRow}
onPress={confirmClear}
accessibilityRole="button"
@@ -98,7 +97,7 @@ export default function PlaybackSettingsScreen() {
Keeps play counts, recents, favorites, and playlists.
</Text>
</View>
</Pressable>
</AppPressable>
</SettingsCard>
</SettingsSectionScreen>
);
+5 -6
View File
@@ -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 (
<Pressable
<AppPressable
disabled={disabled}
android_ripple={ripple.bounded}
unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={onPress}
accessibilityRole="button"
@@ -274,7 +273,7 @@ function MaintenanceRow({
<Text variant="caption" color={colors.textSecondary} style={styles.description}>{description}</Text>
</View>
{running ? <ActivityIndicator size="small" color={colors.accent} /> : <Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />}
</Pressable>
</AppPressable>
);
}
+6 -7
View File
@@ -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 (
<Screen>
<View style={styles.header}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={goToSignal} hitSlop={8}>
<AppPressable feedback="control" style={styles.back} onPress={goToSignal} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Signal
</Text>
</Pressable>
</AppPressable>
</View>
{payload ? (
@@ -46,11 +45,11 @@ export default function SignalImportScreen() {
<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 Signal.</Text>
<Pressable android_ripple={ripple.bounded} style={styles.primaryButton} onPress={goToSignal}>
<AppPressable feedback="accent" style={styles.primaryButton} onPress={goToSignal}>
<Text variant="body" color={colors.accentTextStrong}>
Go to Signal
</Text>
</Pressable>
</AppPressable>
</View>
)}
</Screen>
+8 -9
View File
@@ -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<SignalCodeHandle>(null);
@@ -65,12 +64,12 @@ export default function SignalScreen() {
offset={{ top: 0, left: -spacing.lg, right: -spacing.lg }}
/>
<View style={styles.header}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<AppPressable feedback="control" style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Back
</Text>
</Pressable>
</AppPressable>
</View>
<Text variant="title" style={styles.heading}>
@@ -116,16 +115,16 @@ export default function SignalScreen() {
</Text>
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded} style={styles.primaryButton} onPress={() => void shareImage()}>
<AppPressable feedback="accent" style={styles.primaryButton} onPress={() => void shareImage()}>
<Ionicons name="share-outline" size={18} color={colors.accentTextStrong} />
<Text variant="body" color={colors.accentTextStrong}>
Share image
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded} style={styles.secondaryButton} onPress={() => void shareLink()}>
</AppPressable>
<AppPressable feedback="control" style={styles.secondaryButton} onPress={() => void shareLink()}>
<Ionicons name="link-outline" size={18} color={colors.textPrimary} />
<Text variant="body">Share link</Text>
</Pressable>
</AppPressable>
</View>
</View>
)}
+12 -13
View File
@@ -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<CameraView>(null);
@@ -200,12 +199,12 @@ export default function SignalScanScreen() {
return (
<Screen>
<View style={styles.header}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={() => router.back()} hitSlop={8}>
<AppPressable feedback="control" style={styles.back} onPress={() => router.back()} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Back
</Text>
</Pressable>
</AppPressable>
</View>
<Text variant="title" style={styles.heading}>
@@ -227,16 +226,16 @@ export default function SignalScanScreen() {
<View style={styles.permissionCard}>
<Ionicons name="camera-outline" size={28} color={colors.accent} />
<Text variant="body">Camera access is needed to scan a Signal.</Text>
<Pressable android_ripple={ripple.bounded} style={styles.primaryButton} onPress={() => void requestPermission()}>
<AppPressable feedback="accent" style={styles.primaryButton} onPress={() => void requestPermission()}>
<Text variant="body" color={colors.accentTextStrong}>
Allow camera
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded} style={styles.linkButton} onPress={() => void pickImage()}>
</AppPressable>
<AppPressable feedback="control" style={styles.linkButton} onPress={() => void pickImage()}>
<Text variant="body" color={colors.accent}>
Or pick a Signal image
</Text>
</Pressable>
</AppPressable>
</View>
) : (
<View style={styles.scannerFrame}>
@@ -253,12 +252,12 @@ export default function SignalScanScreen() {
<View style={[styles.guideCorner, styles.guideBottomRight]} />
</View>
<View style={styles.controls}>
<Pressable android_ripple={ripple.icon(28)} style={styles.iconButton} onPress={() => void pickImage()} hitSlop={8}>
<AppPressable feedback="control" style={styles.iconButton} onPress={() => void pickImage()} hitSlop={8}>
<Ionicons name="image-outline" size={24} color={colors.textPrimary} />
</Pressable>
<Pressable android_ripple={ripple.bounded} style={styles.shutter} onPress={() => void capture()} hitSlop={8}>
</AppPressable>
<AppPressable feedback="accent" style={styles.shutter} onPress={() => void capture()} hitSlop={8}>
<Ionicons name="pulse" size={26} color={colors.accentTextStrong} />
</Pressable>
</AppPressable>
<View style={styles.iconButton} />
</View>
<SignalScanTransition
+9 -11
View File
@@ -3,7 +3,6 @@ import {
ActivityIndicator,
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
StyleSheet,
TextInput,
@@ -18,7 +17,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 { RemoteSourceType } from '@/types/remote';
@@ -85,7 +84,6 @@ function Field({
export default function SourceEditScreen() {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
const router = useRouter();
const { id } = useLocalSearchParams<{ id?: string }>();
@@ -189,12 +187,12 @@ export default function SourceEditScreen() {
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View style={styles.header}>
<Pressable android_ripple={ripple.bounded} style={styles.back} onPress={goBack} hitSlop={8}>
<AppPressable feedback="control" style={styles.back} onPress={goBack} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
{backLabel}
</Text>
</Pressable>
</AppPressable>
</View>
{step === 'type' ? (
@@ -208,7 +206,7 @@ export default function SourceEditScreen() {
<View style={styles.typeCards}>
{TYPE_OPTIONS.map((option) => (
<Pressable android_ripple={ripple.bounded}
<AppPressable
key={option.type}
style={styles.typeCard}
onPress={() => chooseType(option.type)}
@@ -228,7 +226,7 @@ export default function SourceEditScreen() {
</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
))}
</View>
</ScrollView>
@@ -276,7 +274,7 @@ export default function SourceEditScreen() {
) : null}
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={[styles.button, styles.secondaryButton, busy ? styles.buttonDisabled : null]}
onPress={() => void onTest()}
disabled={!!busy}
@@ -288,8 +286,8 @@ export default function SourceEditScreen() {
Test connection
</Text>
)}
</Pressable>
<Pressable android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="accent"
style={[
styles.button,
styles.primaryButton,
@@ -305,7 +303,7 @@ export default function SourceEditScreen() {
{editing ? 'Save' : 'Add server'}
</Text>
)}
</Pressable>
</AppPressable>
</View>
</ScrollView>
)}
+5 -7
View File
@@ -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.
</Text>
<Pressable android_ripple={ripple.bounded} style={styles.addButton} onPress={() => router.push('/sources/edit')}>
<AppPressable feedback="accent" style={styles.addButton} onPress={() => router.push('/sources/edit')}>
<Ionicons name="add" size={18} color={colors.accentTextStrong} />
<Text variant="body" color={colors.accentTextStrong}>
Add server
</Text>
</Pressable>
</AppPressable>
</View>
) : (
sources.map((source) => {
const status = statusLine(source, progressById[source.id] ?? null);
return (
<Pressable android_ripple={ripple.bounded}
<AppPressable
key={source.id}
style={styles.row}
onPress={() => setActionFor(source)}
@@ -170,7 +168,7 @@ export default function SourcesScreen() {
</Text>
</View>
<Ionicons name="ellipsis-horizontal" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
);
})
)}
+95
View File
@@ -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<PressableProps, 'android_ripple'> {
/** 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<NativeView, AppPressableProps>(
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 (
<NativePressable
{...rest}
ref={ref}
disabled={disabled}
style={(state) =>
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 = (
<View
pointerEvents="none"
style={[
StyleSheet.absoluteFill,
{ backgroundColor: colors.glassHighlight },
resolveFeedbackCorners(flattened, feedbackRadius),
]}
/>
);
return decision.overlay === 'behind' ? (
<Fragment>{wash}{content}</Fragment>
) : (
<Fragment>{content}{wash}</Fragment>
);
}}
</NativePressable>
);
},
);
+29 -31
View File
@@ -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}
>
<Pressable
android_ripple={ripple.bounded}
<AppPressable
style={styles.barTap}
// Where a dock is available the pane is the next step up, not
// fullscreen: tap the card → pane, expand the pane → fullscreen.
@@ -599,9 +597,9 @@ export function MiniPlayer({
</Animated.View>
</View>
<Pressable
<AppPressable
hitSlop={8}
android_ripple={ripple.icon(22)}
feedback="control"
onPress={onTogglePlay}
style={[styles.control, { width: controlSize, height: controlSize }]}
accessibilityLabel={isPlaying ? 'Pause' : 'Play'}
@@ -611,17 +609,17 @@ export function MiniPlayer({
size={24}
color={colors.accent}
/>
</Pressable>
<Pressable
</AppPressable>
<AppPressable
hitSlop={8}
android_ripple={ripple.icon(22)}
feedback="control"
onPress={onSkipNext}
style={[styles.control, { width: controlSize, height: controlSize }]}
accessibilityLabel="Next"
>
<Ionicons name="play-skip-forward" size={22} color={colors.textSecondary} />
</Pressable>
</Pressable>
</AppPressable>
</AppPressable>
{presentation.hasTrack ? (
isDesktop ? (
<MiniProgress
@@ -650,8 +648,8 @@ export function MiniPlayer({
return (
<>
<View style={[styles.railBlock, { height: blockHeight - RAIL_MINI_TOP_MARGIN }]}>
<Pressable
android_ripple={ripple.bounded}
<AppPressable
style={styles.railTap}
onPress={() => usePlayerUiStore.getState().openPlayer()}
accessibilityRole="button"
@@ -705,11 +703,11 @@ export function MiniPlayer({
{displayedMedia.title}
</Text>
) : null}
</Pressable>
</AppPressable>
<View style={[styles.railControls, { height: controlSize, marginTop: gap }]}>
<Pressable
<AppPressable
hitSlop={8}
android_ripple={ripple.icon(20)}
feedback="control"
onPress={onTogglePlay}
style={[styles.control, { width: controlSize, height: controlSize }]}
accessibilityLabel={isPlaying ? 'Pause' : 'Play'}
@@ -719,16 +717,16 @@ export function MiniPlayer({
size={22}
color={colors.accent}
/>
</Pressable>
<Pressable
</AppPressable>
<AppPressable
hitSlop={8}
android_ripple={ripple.icon(20)}
feedback="control"
onPress={onSkipNext}
style={[styles.control, { width: controlSize, height: controlSize }]}
accessibilityLabel="Next"
>
<Ionicons name="play-skip-forward" size={20} color={colors.textSecondary} />
</Pressable>
</AppPressable>
</View>
</View>
<PlaybackTargetPicker
@@ -749,8 +747,8 @@ export function MiniPlayer({
<MiniPlayerScrim />
<GestureDetector gesture={swipeGesture}>
<Animated.View style={styles.pill} onLayout={onLayout}>
<Pressable
android_ripple={ripple.bounded}
<AppPressable
style={styles.pillPressable}
onPress={() => usePlayerUiStore.getState().openPlayer()}
>
@@ -814,30 +812,30 @@ export function MiniPlayer({
</View>
{isDesktop ? (
<Pressable
<AppPressable
hitSlop={10}
android_ripple={ripple.icon(22)}
feedback="control"
onPress={() => setTargetPickerOpen(true)}
style={styles.control}
accessibilityLabel="Choose output device"
>
<Ionicons name="desktop-outline" size={21} color={colors.textSecondary} />
</Pressable>
</AppPressable>
) : null}
<Pressable hitSlop={10} android_ripple={ripple.icon(22)} onPress={onTogglePlay} style={styles.control}>
<AppPressable hitSlop={10} feedback="control" onPress={onTogglePlay} style={styles.control}>
<Ionicons
name={isLoading ? 'ellipsis-horizontal' : isPlaying ? 'pause' : 'play'}
size={24}
color={colors.accent}
/>
</Pressable>
</AppPressable>
{/* 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. */}
<Pressable hitSlop={10} android_ripple={ripple.icon(22)} onPress={onSkipNext} style={styles.control}>
<AppPressable hitSlop={10} feedback="control" onPress={onSkipNext} style={styles.control}>
<Ionicons name="play-skip-forward" size={22} color={colors.textSecondary} />
</Pressable>
</AppPressable>
</View>
{presentation.hasTrack ? (
@@ -853,7 +851,7 @@ export function MiniPlayer({
<PhoneMiniProgress isPlaying={isPlaying} active={!playerOpen} />
)
) : null}
</Pressable>
</AppPressable>
</Animated.View>
</GestureDetector>
<PlaybackTargetPicker
+4 -5
View File
@@ -10,7 +10,7 @@ import { Ionicons } from '@expo/vector-icons';
import { Text } from './Text';
import { radius, spacing } from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { AppPressable } from '@/components/AppPressable';
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
import { usePlaybackTargetStore, type PlaybackTarget } from '@/stores/playbackTargetStore';
import { usePlayerStore } from '@/stores/playerStore';
@@ -124,10 +124,9 @@ function TargetRow({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
return (
<Pressable
android_ripple={ripple.bounded}
<AppPressable
style={styles.row}
onPress={onPress}
accessibilityRole="button"
@@ -149,7 +148,7 @@ function TargetRow({
) : (
<Ionicons name="ellipse-outline" size={22} color={colors.textTertiary} />
)}
</Pressable>
</AppPressable>
);
}
+7 -10
View File
@@ -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 ? (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={onBack}
hitSlop={12}
@@ -221,7 +219,7 @@ export function ScreenHeader({
accessibilityLabel="Back"
>
<Ionicons name="chevron-back" size={layout.chevronSize} color={colors.textPrimary} />
</Pressable>
</AppPressable>
) : null;
const actionCluster =
@@ -391,10 +389,9 @@ export function ScreenHeaderAction({
children: ReactNode;
}) {
const styles = useStyles();
const ripple = useRipple();
return (
<Pressable
android_ripple={ripple.icon(SCREEN_HEADER_ACTION_SIZE / 2)}
<AppPressable
feedback="control"
style={styles.action}
onPress={onPress}
hitSlop={6}
@@ -402,7 +399,7 @@ export function ScreenHeaderAction({
accessibilityLabel={accessibilityLabel}
>
{children}
</Pressable>
</AppPressable>
);
}
+4 -6
View File
@@ -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 (
<Pressable
android_ripple={ripple.bounded}
<AppPressable
style={styles.segment}
onPress={handlePress}
onLayout={onLayout}
@@ -135,7 +133,7 @@ function SegmentButton({
>
{label}
</Animated.Text>
</Pressable>
</AppPressable>
);
}
+1 -3
View File
@@ -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,
@@ -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,
);
});
+55
View File
@@ -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<T>(
style: T,
decision: PressFeedbackDecision,
): T | [T, { opacity: number }] {
return decision.opacity == null ? style : [style, { opacity: decision.opacity }];
}
+4 -6
View File
@@ -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 (
<Pressable
<AppPressable feedback="none"
key={`${action.label}-${index}`}
android_ripple={ripple.bounded}
style={({ pressed }) => [
styles.action,
pressed ? styles.actionPressed : null,
@@ -133,7 +131,7 @@ export function AppDialogHost() {
<Text variant="body" color={color}>
{action.label}
</Text>
</Pressable>
</AppPressable>
);
})}
</View>
+14 -16
View File
@@ -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 (
<ScrollView
@@ -81,9 +80,9 @@ export function BandConsole({
const isPass = isPassEQBandType(band.type);
const isShelf = isShelfEQBandType(band.type);
return (
<Pressable
<AppPressable
key={band.id}
android_ripple={ripple.bounded}
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.strip, { width: stripWidth }, isActive && styles.stripActive]}
// Selecting a band highlights its node on the curve. The strip is
@@ -104,8 +103,8 @@ export function BandConsole({
/>
</View>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.typeButton}
onPress={() => onEditType(band.id)}
@@ -116,7 +115,7 @@ export function BandConsole({
{BAND_TYPE_LABEL[band.type]}
</Text>
<Ionicons name="chevron-down" size={12} color={colors.textSecondary} />
</Pressable>
</AppPressable>
{/*
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')}
/>
</Pressable>
</AppPressable>
);
})}
{canAdd ? (
<Pressable
android_ripple={ripple.bounded}
<AppPressable
unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={onAdd}
style={[styles.strip, styles.addStrip, { width: CONSOLE_ADD_WIDTH }]}
@@ -170,7 +169,7 @@ export function BandConsole({
accessibilityLabel="Add band"
>
<Ionicons name="add" size={22} color={colors.accentText} />
</Pressable>
</AppPressable>
) : null}
</ScrollView>
);
@@ -195,7 +194,6 @@ function Readout({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
const body = (
<>
<Text
@@ -215,8 +213,8 @@ function Readout({
return <View style={styles.readout}>{body}</View>;
}
return (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.readout}
onPress={onPress}
@@ -224,7 +222,7 @@ function Readout({
accessibilityLabel={`${value} ${label}`}
>
{body}
</Pressable>
</AppPressable>
);
}
+3 -5
View File
@@ -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
<View style={styles.card}>
<View style={styles.header}>
<Text variant="heading">Band {bandNumber}</Text>
<Pressable android_ripple={ripple.bounded} style={styles.typeButton} onPress={onEditType}>
<AppPressable feedback="control" style={styles.typeButton} onPress={onEditType}>
<Text variant="label" color={colors.textPrimary}>
{BAND_TYPE_LABEL[band.type]}
</Text>
<Ionicons name="chevron-down" size={14} color={colors.textSecondary} />
</Pressable>
</AppPressable>
<View style={styles.toggle}>
<Text variant="label">{band.enabled ? 'On' : 'Off'}</Text>
<HapticSwitch
+5 -7
View File
@@ -1,5 +1,4 @@
import {
Pressable,
ScrollView,
StyleSheet
} from 'react-native';
@@ -10,7 +9,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 { EQBand } from '@/types/audio';
import {
formatFreq,
@@ -29,7 +28,6 @@ interface BandStripProps {
/** Horizontal strip of per-band cells (freq + gain) + a trailing "+" add cell. */
export function BandStrip({ bands, activeBandId, canAdd, onSelect, onAdd }: BandStripProps) {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
return (
<ScrollView
@@ -40,7 +38,7 @@ export function BandStrip({ bands, activeBandId, canAdd, onSelect, onAdd }: Band
{bands.map((band) => {
const isActive = band.id === activeBandId;
return (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY}
key={band.id}
onPress={() => 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)}
</Text>
</Pressable>
</AppPressable>
);
})}
{canAdd ? (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onAdd} style={[styles.cell, styles.addCell]} accessibilityLabel="Add band">
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onAdd} style={[styles.cell, styles.addCell]} accessibilityLabel="Add band">
<Ionicons name="add" size={22} color={colors.accentText} />
</Pressable>
</AppPressable>
) : null}
</ScrollView>
);
+5 -7
View File
@@ -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}
/>
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={onClose}>
<AppPressable feedback="control" style={[styles.btn, styles.cancel]} onPress={onClose}>
<Text variant="label" color={colors.textSecondary}>
Cancel
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.primary, !trimmed && styles.disabled]} disabled={!trimmed} onPress={submit}>
</AppPressable>
<AppPressable feedback="accent" style={[styles.btn, styles.primary, !trimmed && styles.disabled]} disabled={!trimmed} onPress={submit}>
<Text variant="label" color={colors.accentTextStrong}>
{actionLabel}
</Text>
</Pressable>
</AppPressable>
</View>
</EqSheet>
);
+5 -7
View File
@@ -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({
</View>
</View>
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={onClose}>
<AppPressable feedback="control" style={[styles.btn, styles.cancel]} onPress={onClose}>
<Text variant="label" color={colors.textSecondary}>
Cancel
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="accent"
style={[styles.btn, styles.primary]}
onPress={() => {
onConfirm();
@@ -85,7 +83,7 @@ export function EQPresetPreviewSheet({
<Text variant="label" color={colors.accentTextStrong}>
{confirmLabel}
</Text>
</Pressable>
</AppPressable>
</View>
</EqSheet>
);
+3 -5
View File
@@ -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
<Text variant="label" numberOfLines={1} color={colors.textSecondary} style={styles.name}>
{presetName}
</Text>
<Pressable android_ripple={ripple.bounded} style={styles.done} onPress={onClose}>
<AppPressable feedback="accent" style={styles.done} onPress={onClose}>
<Text variant="label" color={colors.accentTextStrong}>
Done
</Text>
</Pressable>
</AppPressable>
</EqSheet>
);
}
+5 -7
View File
@@ -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({
/>
</View>
{onValuePress && !disabled ? (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="none"
// Deliberate two-property pressed emphasis (accent border + fill), kept
// alongside the ripple — it marks the value as editable, not just pressed.
// alongside press feedback — it marks the value as editable, not just pressed.
style={({ pressed }) => [styles.valueButton, pressed && styles.valueButtonPressed]}
onPress={onValuePress}
accessibilityRole="button"
@@ -132,7 +130,7 @@ export function EQSlider({
<Text variant="mono" style={styles.value}>
{format(value)}
</Text>
</Pressable>
</AppPressable>
) : (
<Text variant="mono" style={styles.value}>
{format(value)}
+5 -7
View File
@@ -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'}
</Text>
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={onClose}>
<AppPressable feedback="control" style={[styles.btn, styles.cancel]} onPress={onClose}>
<Text variant="label" color={colors.textSecondary}>
Cancel
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="accent"
style={[styles.btn, styles.apply, !valid && styles.applyDisabled]}
disabled={!valid}
onPress={apply}
@@ -95,7 +93,7 @@ export function EQValueEditSheet({
<Text variant="label" color={colors.accentTextStrong}>
Apply
</Text>
</Pressable>
</AppPressable>
</View>
</EqSheet>
);
@@ -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<Set<string>>(
() => 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 (
<Pressable
<AppPressable
key={device.key}
android_ripple={ripple.bounded}
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.deviceRow}
onPress={() => toggleDevice(device.key)}
@@ -131,18 +130,18 @@ export function PresetDeviceAssignmentSheet({
size={22}
color={checked ? colors.accent : colors.textTertiary}
/>
</Pressable>
</AppPressable>
);
})}
</View>
)}
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded} style={[styles.button, styles.cancel]} onPress={onClose}>
<AppPressable feedback="control" style={[styles.button, styles.cancel]} onPress={onClose}>
<Text variant="label" color={colors.textSecondary}>Cancel</Text>
</Pressable>
<Pressable
android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="accent"
style={[styles.button, styles.save]}
onPress={() => {
onSave([...selected]);
@@ -150,7 +149,7 @@ export function PresetDeviceAssignmentSheet({
}}
>
<Text variant="label" color={colors.accentTextStrong}>Save assignments</Text>
</Pressable>
</AppPressable>
</View>
</EqSheet>
);
+8 -9
View File
@@ -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={
<View style={styles.trailingActions}>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
hitSlop={6}
onPress={() => onAssign(preset)}
@@ -84,10 +83,10 @@ export function PresetSheet({
Assign
</Text>
<Ionicons name="chevron-forward" size={14} color={colors.textTertiary} />
</Pressable>
</AppPressable>
{preset.isCustom ? (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
hitSlop={8}
onPress={() => onDelete(preset)}
@@ -95,7 +94,7 @@ export function PresetSheet({
accessibilityLabel={`Delete preset ${preset.name}`}
>
<Ionicons name="trash-outline" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
) : null}
</View>
}
+5 -7
View File
@@ -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({
</View>
) : null}
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={onClose}>
<AppPressable feedback="control" style={[styles.btn, styles.cancel]} onPress={onClose}>
<Text variant="label" color={colors.textSecondary}>
Cancel
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="accent"
style={[styles.btn, styles.save, !trimmed && styles.saveDisabled]}
disabled={!trimmed}
onPress={() => {
@@ -92,7 +90,7 @@ export function SavePresetSheet({
<Text variant="label" color={colors.accentTextStrong}>
Save
</Text>
</Pressable>
</AppPressable>
</View>
</EqSheet>
);
+3 -5
View File
@@ -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 (
<Pressable android_ripple={ripple.tile} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.item} onPress={onPress} accessibilityRole="button">
<AppPressable feedback="tile" unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.item} onPress={onPress} accessibilityRole="button">
<View style={styles.art}>
{artUri ? (
<Image
@@ -40,7 +38,7 @@ export function AlbumGridItem({ album, onPress }: { album: Album; onPress: () =>
<Text variant="label" numberOfLines={1}>
{album.artist}
</Text>
</Pressable>
</AppPressable>
);
}
+3 -5
View File
@@ -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 (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button">
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button">
<View style={styles.art}>
{artUri ? (
<Image
@@ -45,7 +43,7 @@ export function AlbumRow({ album, onPress }: { album: Album; onPress: () => void
</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={colors.textTertiary} />
</Pressable>
</AppPressable>
);
}
+3 -5
View File
@@ -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 (
<Pressable android_ripple={ripple.tile} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.item} onPress={onPress} accessibilityRole="button">
<AppPressable feedback="tile" unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.item} onPress={onPress} accessibilityRole="button">
<View style={styles.art}>
{hashes.length === 0 ? (
<Ionicons name="person" size={44} color={colors.textTertiary} />
@@ -58,7 +56,7 @@ export function ArtistGridItem({ artist, onPress }: { artist: Artist; onPress: (
<Text variant="label" numberOfLines={1}>
{albums} · {tracks}
</Text>
</Pressable>
</AppPressable>
);
}
@@ -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<DeezerArtistCandidate[]>([]);
const [loading, setLoading] = useState(false);
@@ -101,8 +100,8 @@ export function ArtistImageSearchSheet({
style={styles.input}
accessibilityLabel="Deezer artist search"
/>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={styles.searchButton}
onPress={() => void search()}
accessibilityRole="button"
@@ -113,7 +112,7 @@ export function ArtistImageSearchSheet({
) : (
<Ionicons name="search" size={20} color={colors.accentTextStrong} />
)}
</Pressable>
</AppPressable>
</View>
{error ? (
@@ -127,9 +126,9 @@ export function ArtistImageSearchSheet({
<View style={styles.results}>
{candidates.map((candidate) => (
<Pressable
<AppPressable
key={candidate.id}
android_ripple={ripple.bounded}
style={styles.candidate}
onPress={() => void choose(candidate)}
accessibilityRole="button"
@@ -166,7 +165,7 @@ export function ArtistImageSearchSheet({
) : (
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
)}
</Pressable>
</AppPressable>
))}
</View>
@@ -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 }) {
<Text variant="caption" color={colors.textSecondary}>
{missing === 1 ? '1 artist has no image' : `${n(missing)} artists have no image`}
</Text>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={styles.button}
disabled={retrying}
onPress={() => void retry()}
@@ -89,7 +88,7 @@ export function ArtistImageSweepStatus({ enabled }: { enabled: boolean }) {
<Text variant="label" color={colors.accentTextStrong}>
Look for missing images
</Text>
</Pressable>
</AppPressable>
</View>
);
}
+3 -5
View File
@@ -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 (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button">
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button">
<View style={styles.art}>
{artist.artwork_hash ? (
<Image
@@ -45,7 +43,7 @@ export function ArtistRow({ artist, onPress }: { artist: Artist; onPress: () =>
</Text>
</View>
<Ionicons name="chevron-forward" size={16} color={colors.textTertiary} />
</Pressable>
</AppPressable>
);
}
+13 -15
View File
@@ -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}
<Animated.View style={[styles.actionRow, heroButtonsStyle]}>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="accent" unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.actionButton, styles.primaryAction, disabled && styles.disabledAction]}
onPress={onPlay}
disabled={disabled}
@@ -372,8 +370,8 @@ export function CollapsingHeader({
<Text variant="body" style={styles.primaryActionText}>
Play
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
</AppPressable>
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.actionButton, styles.secondaryAction, disabled && styles.disabledAction]}
onPress={onShuffle}
disabled={disabled}
@@ -383,11 +381,11 @@ export function CollapsingHeader({
<Text variant="body" color={colors.accent} style={styles.secondaryActionText}>
Shuffle
</Text>
</Pressable>
</AppPressable>
</Animated.View>
</Animated.View>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={onBack}
hitSlop={8}
style={[styles.chevron, { top: barCenterY - 12, left: spacing.md }]}
@@ -395,7 +393,7 @@ export function CollapsingHeader({
accessibilityLabel="Back"
>
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
</Pressable>
</AppPressable>
<Animated.Text
numberOfLines={1}
style={[
@@ -424,16 +422,16 @@ export function CollapsingHeader({
style={[styles.barIcons, { top: barCenterY - 16, right: onMore ? spacing.md + 40 : spacing.md }, barIconsStyle]}
pointerEvents={collapsed ? 'auto' : 'none'}
>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onPlay} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onPlay} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
<Ionicons name="play" size={20} color={colors.accent} />
</Pressable>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onShuffle} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
</AppPressable>
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onShuffle} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
<Ionicons name="shuffle" size={20} color={colors.accent} />
</Pressable>
</AppPressable>
</Animated.View>
{onMore ? (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={onMore}
hitSlop={8}
style={[styles.moreButton, { top: barCenterY - 16, right: spacing.md }]}
@@ -441,7 +439,7 @@ export function CollapsingHeader({
accessibilityLabel={moreAccessibilityLabel}
>
<Ionicons name="ellipsis-horizontal" size={22} color={colors.textPrimary} />
</Pressable>
</AppPressable>
) : null}
{/* Rendered last so the large art sits on top of the header text until it tucks away. */}
+5 -7
View File
@@ -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.'}
</Text>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={styles.cta}
onPress={() => router.push(fatal ? '/settings/troubleshooting' : '/settings')}
accessibilityRole="button"
@@ -60,7 +58,7 @@ export function EmptyLibrary() {
<Text variant="body" style={styles.ctaLabel}>
{fatal ? 'Troubleshooting' : 'Folder settings'}
</Text>
</Pressable>
</AppPressable>
</View>
);
}
+16 -18
View File
@@ -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 (
<Pressable
android_ripple={ripple.bounded}
<AppPressable
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.folderRow}
onPress={onToggle}
@@ -118,8 +117,8 @@ function FolderNodeRow({
) : null}
</View>
<Text variant="mono" style={styles.count}>{node.totalTrackCount}</Text>
<Pressable
android_ripple={ripple.icon(20)}
<AppPressable
feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.folderButton}
onPress={stop(onPlay)}
@@ -128,9 +127,9 @@ function FolderNodeRow({
accessibilityLabel={`Play ${node.name}`}
>
<Ionicons name="play" size={16} color={colors.accent} />
</Pressable>
<Pressable
android_ripple={ripple.icon(20)}
</AppPressable>
<AppPressable
feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.folderButton}
onPress={stop(onShuffle)}
@@ -139,8 +138,8 @@ function FolderNodeRow({
accessibilityLabel={`Shuffle ${node.name}`}
>
<Ionicons name="shuffle" size={16} color={colors.textSecondary} />
</Pressable>
</Pressable>
</AppPressable>
</AppPressable>
);
}
@@ -157,10 +156,9 @@ function FolderTrackRow({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
return (
<Pressable
android_ripple={ripple.bounded}
<AppPressable
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.trackRow, active && styles.trackRowActive]}
onPress={() => {
@@ -195,8 +193,8 @@ function FolderTrackRow({
<Text variant="label" numberOfLines={1}>{track.artist}</Text>
</View>
<Text variant="mono" style={styles.duration}>{formatDuration(track.duration)}</Text>
<Pressable
android_ripple={ripple.icon(21)}
<AppPressable
feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.actionsButton}
onPress={(event) => {
@@ -208,8 +206,8 @@ function FolderTrackRow({
accessibilityLabel={`More actions for ${track.title}`}
>
<Ionicons name="ellipsis-horizontal" size={18} color={colors.textTertiary} />
</Pressable>
</Pressable>
</AppPressable>
</AppPressable>
);
}
+7 -10
View File
@@ -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 (
<Pressable
<AppPressable feedback="control"
key={entry.key}
android_ripple={ripple.bounded}
unstable_pressDelay={SCROLL_PRESS_DELAY}
onLayout={slide.measure(entry.key)}
style={[
@@ -292,7 +290,7 @@ export function LibraryContextBar({
{entry.label}
</Text>
) : null}
</Pressable>
</AppPressable>
);
})}
</View>
@@ -329,10 +327,9 @@ function ContextAction({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
return (
<Pressable
android_ripple={ripple.icon(LIBRARY_CONTEXT_ACTION_SIZE / 2)}
<AppPressable
feedback="control"
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.action, disabled && styles.disabled]}
disabled={disabled}
@@ -342,7 +339,7 @@ function ContextAction({
accessibilityState={{ disabled }}
>
<Ionicons name={icon} size={20} color={colors.textSecondary} />
</Pressable>
</AppPressable>
);
}
+4 -6
View File
@@ -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 (
<Pressable
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.row}
onPress={onPress}
onLongPress={
@@ -93,7 +91,7 @@ export function PlaylistRow({
</Text>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
);
}
@@ -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({
</View>
</View>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={styles.button}
disabled={working}
onPress={resolve}
@@ -72,7 +70,7 @@ export function ScanNotificationPermissionCard({
<Text variant="label" color={colors.accentTextStrong}>
{denied ? 'Open Settings' : 'Allow scan notifications'}
</Text>
</Pressable>
</AppPressable>
{denied ? (
<Text variant="caption" color={colors.textTertiary}>
+5 -6
View File
@@ -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}
</Text>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
disabled={isCancelling}
onPress={cancelScan}
accessibilityRole="button"
@@ -58,7 +57,7 @@ export function ScanProgress() {
>
{isCancelling ? 'Cancelling…' : 'Cancel'}
</Text>
</Pressable>
</AppPressable>
</View>
<View style={styles.track}>
<View
@@ -1,5 +1,4 @@
import {
Pressable,
StyleSheet,
View
} from 'react-native';
@@ -7,7 +6,7 @@ import { Ionicons } from '@expo/vector-icons';
import { Text } from '@/components/Text';
import { spacing } from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { AppPressable } from '@/components/AppPressable';
import { useSceneBottomInset } from '@/navigation/useShellLayout';
interface SelectionActionBarProps {
@@ -72,10 +71,9 @@ function BarButton({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
return (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={[styles.button, disabled && styles.buttonDisabled]}
onPress={onPress}
disabled={disabled}
@@ -86,7 +84,7 @@ function BarButton({
<Text variant="label" style={styles.label} numberOfLines={1}>
{label}
</Text>
</Pressable>
</AppPressable>
);
}
+7 -9
View File
@@ -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 = (
<Pressable
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.row, selectionMode && selected && styles.rowSelected]}
onPress={selectionMode ? onToggleSelect : onPress}
onLongPress={handleLongPress}
@@ -168,8 +166,8 @@ export function TrackRow({
</Text>
{onOpenActions && !selectionMode ? (
<Pressable
android_ripple={ripple.icon(ACTIONS_BUTTON / 2 + 4)} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable
feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.actionsButton}
onPress={openActions}
hitSlop={8}
@@ -177,9 +175,9 @@ export function TrackRow({
accessibilityLabel={`More actions for ${track.title}`}
>
<Ionicons name="ellipsis-horizontal" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
) : null}
</Pressable>
</AppPressable>
);
if (!swipeToQueue || selectionMode) return row;
@@ -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 (
<Pressable
<AppPressable
style={[styles.card, style]}
android_ripple={ripple.tile}
feedback="tile"
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Open Listening Stats"
@@ -103,7 +102,7 @@ export function ListeningPreviewCard({
No qualified plays in the last 7 days.
</Text>
)}
</Pressable>
</AppPressable>
);
}
+7 -8
View File
@@ -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}
</Text>
{emptyState.retryable ? (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="none"
disabled={isLoading}
onPress={() => void loadForTrack(track, { force: true })}
accessibilityRole="button"
@@ -232,7 +231,7 @@ export function LyricsBand({
<Text variant="label" color={colors.accentTextStrong}>
{isLoading ? 'Retrying…' : 'Retry'}
</Text>
</Pressable>
</AppPressable>
) : null}
</View>
);
@@ -310,7 +309,7 @@ export function LyricsBand({
</ScrollView>
{followPaused ? (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={recenter}
hitSlop={10}
style={{
@@ -328,7 +327,7 @@ export function LyricsBand({
<Text variant="mono" color={colors.accentText} style={{ fontSize: 9, letterSpacing: 1, textTransform: 'uppercase' }}>
Recenter
</Text>
</Pressable>
</AppPressable>
) : null}
</View>
);
+4 -5
View File
@@ -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 (
<Animated.View onLayout={onLayout} style={[{ width: '100%', transformOrigin: 'left center' }, animatedStyle]}>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onSeek} style={{ paddingVertical: 7 }}>
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onSeek} style={{ paddingVertical: 7 }}>
<View style={{ alignItems: 'flex-start' }}>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', gap: 5 }}>
{voiceLabelsEnabled && line.voice?.trim() ? (
@@ -268,7 +267,7 @@ function LyricsLineComponent({
</Text>
) : null}
</View>
</Pressable>
</AppPressable>
</Animated.View>
);
}
+9 -10
View File
@@ -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 (
<View style={styles.root}>
<View style={styles.strip}>
<Pressable android_ripple={ripple.bounded} onPress={onDismiss} hitSlop={12} style={styles.stripBtn} accessibilityLabel="Close player">
<AppPressable feedback="control" onPress={onDismiss} hitSlop={12} style={styles.stripBtn} accessibilityLabel="Close player">
<Ionicons name="chevron-down" size={24} color={colors.textSecondary} />
</Pressable>
</AppPressable>
<View style={styles.stripTrackFrame}>
<NowPlayingTrackFadeThrough
@@ -125,7 +124,7 @@ export function LyricsView({
<View style={styles.controls}>
<SeekBar currentTime={currentTime} duration={duration} trackKey={track.id} onSeek={onSeek} />
<View style={styles.transport}>
<TactilePressable android_ripple={ripple.bounded}
<TactilePressable
onPress={onToggleFavorite}
haptic={isFavorite ? 'toggleOff' : 'toggleOn'}
confirmationScale={1.08}
@@ -140,20 +139,20 @@ export function LyricsView({
color={isFavorite ? colors.accent : colors.textTertiary}
/>
</TactilePressable>
<TactilePressable android_ripple={ripple.bounded} onPress={onPrev} haptic="action" hitSlop={12} style={styles.transportBtn} accessibilityLabel="Previous">
<TactilePressable onPress={onPrev} haptic="action" hitSlop={12} style={styles.transportBtn} accessibilityLabel="Previous">
<Ionicons name="play-skip-back" size={28} color={colors.textPrimary} />
</TactilePressable>
<TactilePressable android_ripple={ripple.bounded} onPress={onPlayPause} haptic="action" pressedScale={0.97} hitSlop={12} style={styles.playButton} accessibilityLabel={isPlaying ? 'Pause' : 'Play'}>
<TactilePressable onPress={onPlayPause} haptic="action" pressedScale={0.97} hitSlop={12} style={styles.playButton} accessibilityLabel={isPlaying ? 'Pause' : 'Play'}>
<Ionicons
name={isLoading ? 'ellipsis-horizontal' : isPlaying ? 'pause' : 'play'}
size={28}
color={colors.bgPrimary}
/>
</TactilePressable>
<TactilePressable android_ripple={ripple.bounded} onPress={onNext} haptic="action" hitSlop={12} style={styles.transportBtn} accessibilityLabel="Next">
<TactilePressable onPress={onNext} haptic="action" hitSlop={12} style={styles.transportBtn} accessibilityLabel="Next">
<Ionicons name="play-skip-forward" size={28} color={colors.textPrimary} />
</TactilePressable>
<TactilePressable android_ripple={ripple.bounded}
<TactilePressable
onPress={onExitLyrics}
haptic="selection"
hitSlop={10}
@@ -1,11 +1,11 @@
import { useState } from 'react';
import { ActivityIndicator, Modal, Pressable, StyleSheet, View } from 'react-native';
import { ActivityIndicator, Modal, StyleSheet, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Text } from '@/components/Text';
import { SegmentedControl } from '@/components/SegmentedControl';
import { radius, spacing } from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { AppPressable } from '@/components/AppPressable';
import { useSettingsStore } from '@/stores/settingsStore';
import type { ArtistImageAutoPolicy } from '@/types/artistImages';
@@ -24,7 +24,6 @@ function ArtistImageDisclosureContent({
}) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
const [policy, setPolicy] = useState<ArtistImageAutoPolicy>(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.
</Text>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={styles.button}
onPress={() => void continueSetup()}
disabled={saving}
@@ -84,7 +83,7 @@ function ArtistImageDisclosureContent({
) : (
<Text variant="label" color={colors.bgPrimary}>Continue</Text>
)}
</Pressable>
</AppPressable>
</View>
</View>
</Modal>
@@ -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 (
<Pressable
android_ripple={ripple.bounded}
<AppPressable
style={[styles.card, selected && styles.cardSelected]}
onPress={onPress}
accessibilityRole="radio"
@@ -210,7 +209,7 @@ function PolicyCard({
<View style={styles.cardCheckEmpty} />
)}
</View>
</Pressable>
</AppPressable>
);
}
@@ -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({
</Text>
</View>
) : (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={styles.button}
disabled={working}
onPress={resolve}
@@ -72,7 +71,7 @@ export function NotificationStep({
<Text variant="label" color={colors.accentTextStrong}>
{denied ? 'Open Android settings' : 'Allow notifications'}
</Text>
</Pressable>
</AppPressable>
)}
<Text variant="caption" color={colors.textTertiary} style={styles.footnote}>
+12 -17
View File
@@ -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 }) {
</View>
<View style={styles.navRow}>
{canGoBack ? (
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
onPress={goBack}
style={styles.secondaryButton}
accessibilityRole="button"
@@ -182,9 +180,9 @@ export function OnboardingFlow({ onDone }: { onDone: () => void }) {
<Text variant="label" color={colors.textSecondary}>
Back
</Text>
</Pressable>
</AppPressable>
) : null}
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="accent"
onPress={goNext}
style={styles.primaryButton}
accessibilityRole="button"
@@ -193,7 +191,7 @@ export function OnboardingFlow({ onDone }: { onDone: () => void }) {
<Text variant="label" color={colors.bgPrimary} style={styles.primaryButtonText}>
{primaryLabel}
</Text>
</Pressable>
</AppPressable>
</View>
</View>
</View>
@@ -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."
/>
<Pressable android_ripple={ripple.bounded}
<AppPressable
style={[styles.choiceButton, isScanning && styles.disabled]}
disabled={isScanning}
onPress={() => void addFolder()}
@@ -248,7 +245,7 @@ function LibraryStep() {
<Text variant="body" color={colors.textPrimary}>
{folders.length > 0 ? 'Add another folder' : 'Choose music folder'}
</Text>
</Pressable>
</AppPressable>
<ScanProgress />
@@ -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 (
<Pressable android_ripple={ripple.bounded}
<AppPressable
key={option.id}
onPress={() => {
if (selected) return;
@@ -315,7 +311,7 @@ function ThemeStep() {
>
{option.title}
</Text>
</Pressable>
</AppPressable>
);
})}
</View>
@@ -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}` : '…'}`}
</Text>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
disabled={isCancelling}
onPress={cancelScan}
accessibilityRole="button"
@@ -418,7 +413,7 @@ function ScanBanner() {
>
{isCancelling ? 'Cancelling…' : 'Cancel'}
</Text>
</Pressable>
</AppPressable>
</Animated.View>
);
}
+34 -35
View File
@@ -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. */}
<Pressable
<AppPressable
style={styles.headerBtn}
android_ripple={ripple.icon(22)}
feedback="control"
onPress={() =>
dock
? void useSettingsStore.getState().setPlayerDockOpen(false)
@@ -1111,7 +1110,7 @@ export function NowPlayingOverlay({
size={26}
color={colors.textSecondary}
/>
</Pressable>
</AppPressable>
</View>
<View style={styles.headerMid}>
<Text variant="caption" style={styles.eyebrow}>
@@ -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 ? (
<Pressable
<AppPressable
style={styles.headerBtn}
android_ripple={ripple.icon(22)}
feedback="control"
onPress={() => usePlayerUiStore.getState().openPlayer()}
hitSlop={12}
accessibilityRole="button"
accessibilityLabel="Open full screen player"
>
<Ionicons name="expand-outline" size={20} color={colors.textSecondary} />
</Pressable>
</AppPressable>
) : null}
<Pressable
style={styles.headerBtn} android_ripple={ripple.icon(22)}
<AppPressable
style={styles.headerBtn} feedback="control"
onPress={openMenu}
hitSlop={12}
accessibilityLabel="More options"
>
<Ionicons name="ellipsis-vertical" size={20} color={colors.textSecondary} />
</Pressable>
</AppPressable>
</View>
</View>
)}
@@ -1234,7 +1233,7 @@ export function NowPlayingOverlay({
</View>
<TactilePressable
hitSlop={10}
style={styles.inlineActionBtn} android_ripple={ripple.icon(22)}
style={styles.inlineActionBtn}
haptic={activeTrack.isFavorite ? 'toggleOff' : 'toggleOn'}
confirmationScale={1.08}
onPress={() => void sendDesktopControl('toggle-favorite')}
@@ -1272,7 +1271,7 @@ export function NowPlayingOverlay({
<View style={[styles.transport, { height: deck.transportRowHeight }]}>
<TactilePressable
hitSlop={10}
android_ripple={ripple.icon(24)}
style={[
styles.transportSideBtn,
desktopSnapshot?.shuffle === undefined && styles.controlDisabled,
@@ -1301,7 +1300,7 @@ export function NowPlayingOverlay({
}}
haptic="action"
hitSlop={12}
style={styles.transportMainBtn} android_ripple={ripple.icon(26)}
style={styles.transportMainBtn}
accessibilityLabel="Previous"
>
<Ionicons
@@ -1316,7 +1315,7 @@ export function NowPlayingOverlay({
pressedScale={0.97}
hitSlop={12}
style={[styles.playButton, playButtonSizing]}
android_ripple={ripple.onAccent()}
accessibilityLabel={isPlaying ? 'Pause desktop' : 'Play desktop'}
>
<Ionicons
@@ -1332,7 +1331,7 @@ export function NowPlayingOverlay({
}}
haptic="action"
hitSlop={12}
style={styles.transportMainBtn} android_ripple={ripple.icon(26)}
style={styles.transportMainBtn}
accessibilityLabel="Next"
>
<Ionicons
@@ -1343,7 +1342,7 @@ export function NowPlayingOverlay({
</TactilePressable>
<TactilePressable
hitSlop={10}
android_ripple={ripple.icon(24)}
style={[
styles.transportSideBtn,
desktopSnapshot?.repeat === undefined && styles.controlDisabled,
@@ -1400,7 +1399,7 @@ export function NowPlayingOverlay({
<View style={styles.subActions}>
<TactilePressable
hitSlop={10}
style={styles.subBtn} android_ripple={ripple.icon(20)}
style={styles.subBtn}
onPress={() => void reconnectDesktop()}
accessibilityLabel="Reconnect to desktop"
>
@@ -1409,7 +1408,7 @@ export function NowPlayingOverlay({
{desktopQueue ? (
<TactilePressable
hitSlop={10}
style={styles.subBtn} android_ripple={ripple.icon(20)}
style={styles.subBtn}
haptic="selection"
onPress={showQueue}
accessibilityLabel="Desktop queue"
@@ -1432,8 +1431,8 @@ export function NowPlayingOverlay({
? desktopConnectionLabel(desktopConnectionState)
: 'Pair with Astra Desktop to control it here.'}
</Text>
<Pressable
style={styles.emptyAction} android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={styles.emptyAction}
onPress={() => {
if (desktopConnection) {
void reconnectDesktop();
@@ -1447,7 +1446,7 @@ export function NowPlayingOverlay({
<Text variant="label" color={colors.accentTextStrong}>
{desktopConnection ? 'Reconnect' : 'Pair desktop'}
</Text>
</Pressable>
</AppPressable>
</View>
)
) : 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({
<TactilePressable
hitSlop={10}
style={[styles.inlineActionBtn, subButtonSizing]}
android_ripple={ripple.icon(22)}
haptic={isFavorite ? 'toggleOff' : 'toggleOn'}
confirmationScale={1.08}
onPress={() => void toggleFavorite(track)}
@@ -1716,7 +1715,7 @@ export function NowPlayingOverlay({
<View style={[styles.transport, { height: deck.transportRowHeight }]}>
<TactilePressable
hitSlop={10}
style={styles.transportSideBtn} android_ripple={ripple.icon(24)}
style={styles.transportSideBtn}
haptic={shuffle ? 'toggleOff' : 'toggleOn'}
onPress={() => 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"
>
<Ionicons
@@ -1752,7 +1751,7 @@ export function NowPlayingOverlay({
pressedScale={0.97}
hitSlop={12}
style={[styles.playButton, playButtonSizing]}
android_ripple={ripple.onAccent()}
accessibilityLabel={isPlaying ? 'Pause' : 'Play'}
>
<Ionicons
@@ -1765,7 +1764,7 @@ export function NowPlayingOverlay({
onPress={skipToNext}
haptic="action"
hitSlop={12}
style={styles.transportMainBtn} android_ripple={ripple.icon(26)}
style={styles.transportMainBtn}
accessibilityLabel="Next"
>
<Ionicons
@@ -1776,7 +1775,7 @@ export function NowPlayingOverlay({
</TactilePressable>
<TactilePressable
hitSlop={10}
style={styles.transportSideBtn} android_ripple={ripple.icon(24)}
style={styles.transportSideBtn}
haptic="modeCycle"
onPress={() => void cycleRepeat()}
accessibilityLabel="Repeat"
@@ -1811,7 +1810,7 @@ export function NowPlayingOverlay({
<TactilePressable
hitSlop={10}
style={[styles.subBtn, subButtonSizing]}
android_ripple={ripple.icon(20)}
haptic={scopeStageVisible ? 'toggleOff' : 'toggleOn'}
onPress={() => void setScopeStageVisible(!scopeStageVisible)}
accessibilityLabel={scopeStageVisible ? 'Hide visualizer' : 'Show visualizer'}
@@ -1839,7 +1838,7 @@ export function NowPlayingOverlay({
<TactilePressable
hitSlop={10}
style={[styles.subBtn, subButtonSizing]}
android_ripple={ripple.icon(20)}
haptic="selection"
onPress={showQueue}
accessibilityLabel="Queue"
@@ -1853,7 +1852,7 @@ export function NowPlayingOverlay({
<TactilePressable
hitSlop={10}
style={[styles.subBtn, subButtonSizing]}
android_ripple={ripple.icon(20)}
haptic="selection"
onPress={showLyrics}
accessibilityLabel={
@@ -1941,9 +1940,9 @@ export function NowPlayingOverlay({
style={[styles.menuCard, { top: menuTop, right: shellRight }, menuCardStyle]}
>
{menuItems.map((item) => (
<Pressable
<AppPressable
key={item.key}
android_ripple={ripple.bounded}
style={styles.menuItem}
onPress={item.onPress}
accessibilityRole="button"
@@ -1952,7 +1951,7 @@ export function NowPlayingOverlay({
<Text variant="body" numberOfLines={1} style={styles.menuItemLabel}>
{item.label}
</Text>
</Pressable>
</AppPressable>
))}
</Animated.View>
</Animated.View>
+13 -14
View File
@@ -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() {
<View style={styles.presets}>
{SLEEP_TIMER_PRESETS.map((minutes) => (
<Pressable
<AppPressable feedback="none"
key={minutes}
disabled={!available}
android_ripple={ripple.bounded}
unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={() => void run(() => startMinutes(minutes), `Timer set for ${minutes} minutes.`)}
style={({ pressed }) => [styles.preset, !available && styles.disabled, pressed && available && styles.pressed]}
accessibilityRole="button"
>
<Text variant="label" color={colors.textPrimary}>{minutes} min</Text>
</Pressable>
</AppPressable>
))}
</View>
@@ -103,19 +102,19 @@ export function SleepTimerControls() {
style={[styles.input, !available && styles.disabled]}
accessibilityLabel="Custom sleep timer minutes"
/>
<Pressable
<AppPressable feedback="none"
disabled={!available}
android_ripple={ripple.bounded}
onPress={startCustom}
style={({ pressed }) => [styles.action, !available && styles.disabled, pressed && available && styles.pressed]}
>
<Text variant="label" color={colors.accentTextStrong}>Set custom</Text>
</Pressable>
</AppPressable>
</View>
<Pressable
<AppPressable feedback="none"
disabled={!available || !supportsNativePauseAtEndOfItem()}
android_ripple={ripple.bounded}
onPress={() => void run(startEndOfTrack, 'Timer set for the end of the track.')}
style={({ pressed }) => [
styles.fullAction,
@@ -127,12 +126,12 @@ export function SleepTimerControls() {
<Text variant="caption" color={colors.textSecondary}>
{supportsNativePauseAtEndOfItem() ? 'Pause exactly before the next track begins.' : 'Requires the Android playback engine.'}
</Text>
</Pressable>
</AppPressable>
{timer ? (
<Pressable android_ripple={ripple.bounded} onPress={() => void run(cancel, 'Sleep timer canceled.')} style={styles.cancel}>
<AppPressable feedback="control" onPress={() => void run(cancel, 'Sleep timer canceled.')} style={styles.cancel}>
<Text variant="label" color={colors.warning}>Cancel sleep timer</Text>
</Pressable>
</AppPressable>
) : null}
{feedback ? <Text variant="caption" color={colors.textSecondary}>{feedback}</Text> : null}
+6 -7
View File
@@ -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<DesktopRemoteQueueItem>) => (
<Pressable
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.row}
onPress={() => playItem(item)}
disabled={item.isCurrent}
@@ -100,9 +99,9 @@ export function RemoteQueueSheet({ onClose, embedded = false }: RemoteQueueSheet
{formatDuration(item.durationSeconds)}
</Text>
) : null}
</Pressable>
</AppPressable>
),
[playItem, colors, styles, ripple]
[playItem, colors, styles]
);
const topSection = useMemo(
+10 -14
View File
@@ -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 (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.resultRow, active && styles.resultRowActive, isShowMode && styles.showModeRow]}
onPress={onPress}
accessibilityRole="button"
@@ -541,7 +539,7 @@ function ResultRow({
</Text>
</View>
{isTrack ? (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.queueButton}
onPress={queueTrack}
hitSlop={8}
@@ -549,13 +547,13 @@ function ResultRow({
accessibilityLabel={`Play ${result.track.title} next`}
>
<Ionicons name={queued ? 'checkmark' : 'add'} size={18} color={colors.accentTextStrong} />
</Pressable>
</AppPressable>
) : result.kind === 'playlist' && result.playlist.remote ? (
<Ionicons name="cloud" size={14} color={colors.accent} />
) : (
<Ionicons name="chevron-forward" size={16} color={colors.textTertiary} />
)}
</Pressable>
</AppPressable>
);
}
@@ -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 ? (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
onPress={() => updateQuery('')}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel="Clear search"
>
<Ionicons name="close-circle" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
) : null}
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={close} hitSlop={8} accessibilityRole="button" accessibilityLabel="Close search">
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={close} hitSlop={8} accessibilityRole="button" accessibilityLabel="Close search">
<Ionicons name="close" size={20} color={colors.textSecondary} />
</Pressable>
</AppPressable>
</View>
{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}
>
<View style={styles.modalRoot}>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={StyleSheet.absoluteFill} onPress={close} accessibilityRole="button" />
<AppPressable feedback="none" unstable_pressDelay={SCROLL_PRESS_DELAY} style={StyleSheet.absoluteFill} onPress={close} accessibilityRole="button" />
{isOpen ? (
<QuickSearchPanel key={openVersion} initialQuery={initialQuery} onClose={close} />
) : null}
+7 -9
View File
@@ -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<HsvColor>(() => hexToHsv(normalizedInitial));
const hsvRef = useRef(hsv);
@@ -245,21 +243,21 @@ export function AccentColorSheet({
</Text>
<View style={styles.actions}>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={[styles.button, styles.cancel]}
onPress={onClose}
>
<Text variant="label" color={colors.textSecondary}>Cancel</Text>
</Pressable>
<Pressable
android_ripple={ripple.onAccent()}
</AppPressable>
<AppPressable
feedback="accent"
style={[styles.button, styles.apply, !validInput && styles.disabled]}
disabled={!validInput}
onPress={apply}
>
<Text variant="label" color={colors.bgPrimary}>Apply</Text>
</Pressable>
</AppPressable>
</View>
</AppSheet>
</GestureHandlerRootView>
+7 -8
View File
@@ -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 (
<Pressable android_ripple={ripple.bounded}
<AppPressable feedback="control"
key={id}
onPress={() => {
if (selected) return;
@@ -51,11 +50,11 @@ export function AccentSwatchRow() {
{selected ? (
<Ionicons name="checkmark" size={18} color={colors.bgPrimary} />
) : null}
</Pressable>
</AppPressable>
);
})}
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
onPress={() => {
playHaptic('selection');
setPickerOpen(true);
@@ -86,7 +85,7 @@ export function AccentSwatchRow() {
) : (
<Ionicons name="add" size={18} color="#ffffff" />
)}
</Pressable>
</AppPressable>
</View>
<Text variant="caption" color={colors.textSecondary}>
Accent · {preference.kind === 'preset'
+5 -6
View File
@@ -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 (
<Pressable
android_ripple={ripple.bounded}
<AppPressable
style={[styles.card, selected && styles.cardSelected]}
onPress={handlePress}
accessibilityRole="radio"
@@ -86,7 +85,7 @@ function StyleCard({
<Text variant="caption" color={colors.textSecondary} style={styles.cardDescription}>
{description}
</Text>
</Pressable>
</AppPressable>
);
}
+13 -19
View File
@@ -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 (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY}
key={option.id}
style={[styles.option, selected && styles.optionSelected]}
onPress={() => {
@@ -160,7 +158,7 @@ export function AppearanceSettingsPanel() {
) : (
<Ionicons name="ellipse-outline" size={20} color={colors.textTertiary} />
)}
</Pressable>
</AppPressable>
);
})}
</View>
@@ -249,7 +247,6 @@ function LibraryFolderSettingsRow({
onRemove: (folder: FolderWithCount) => void;
}) {
const styles = useStyles();
const ripple = useRipple();
const colors = useColors();
return (
<View style={styles.folderSettingsRow}>
@@ -272,7 +269,7 @@ function LibraryFolderSettingsRow({
: 'Access lost. Remove and add again.'}
</Text>
</View>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
hitSlop={8}
disabled={disabled}
onPress={() => onRemove(folder)}
@@ -281,14 +278,13 @@ function LibraryFolderSettingsRow({
style={disabled && styles.actionDisabled}
>
<Ionicons name="trash-outline" size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
</View>
);
}
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)}`}
</Text>
</View>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="accent" unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.folderPrimaryAction, isScanning && styles.actionDisabled]}
disabled={isScanning}
onPress={() => void addFolder()}
@@ -336,12 +332,12 @@ function LibraryFoldersSettings() {
<Text variant="label" style={styles.folderPrimaryActionText}>
Add
</Text>
</Pressable>
</AppPressable>
</View>
{folders.length > 0 ? (
<View style={styles.folderSettingsActions}>
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable feedback="control" unstable_pressDelay={SCROLL_PRESS_DELAY}
style={[styles.folderSecondaryAction, isScanning && styles.actionDisabled]}
disabled={isScanning}
onPress={() => void rescan()}
@@ -351,7 +347,7 @@ function LibraryFoldersSettings() {
<Text variant="label" color={colors.textSecondary}>
Rescan all
</Text>
</Pressable>
</AppPressable>
</View>
) : 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 (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY}
key={option.mode}
style={[styles.option, selected && styles.optionSelected]}
onPress={() => {
@@ -477,7 +472,7 @@ export function LibrarySettingsPanel() {
) : (
<Ionicons name="ellipse-outline" size={20} color={colors.textTertiary} />
)}
</Pressable>
</AppPressable>
);
})}
</View>
@@ -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 (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY}
key={m.mode}
style={[styles.modePill, selected && styles.modePillSelected]}
onPress={() => {
@@ -566,7 +560,7 @@ export function AudioSettingsPanel() {
<Text variant="label" color={selected ? colors.accentTextStrong : colors.textSecondary}>
{m.label}
</Text>
</Pressable>
</AppPressable>
);
})}
</View>
@@ -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 (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button">
<AppPressable unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button">
<View style={styles.rowIcon}>
<Ionicons name={icon} size={20} color={colors.accent} />
</View>
@@ -119,7 +117,7 @@ export function SettingsNavRow({
</Text>
</View>
<Ionicons name={rightIcon} size={18} color={colors.textTertiary} />
</Pressable>
</AppPressable>
);
}
+4 -5
View File
@@ -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({
</Text>
) : null}
{items.map((item) => (
<Pressable
<AppPressable
key={item.key}
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.item}
onPress={item.onPress}
accessibilityRole="button"
@@ -87,7 +86,7 @@ export function ActionSheet({
{item.selected ? (
<Ionicons name="checkmark" size={18} color={colors.accent} />
) : null}
</Pressable>
</AppPressable>
))}
</Pressable>
</Pressable>
+5 -4
View File
@@ -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.
*/}
<GesturePressable
android_ripple={ripple.bounded}
unstable_pressDelay={SCROLL_PRESS_DELAY}
style={styles.item}
style={({ 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,
},
@@ -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}
/>
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={() => setStep('pick')}>
<AppPressable feedback="control" style={[styles.btn, styles.cancel]} onPress={() => setStep('pick')}>
<Text variant="label" color={colors.textSecondary}>
Back
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="accent"
style={[styles.btn, styles.create, !trimmedPlaylistName && styles.createDisabled]}
disabled={!trimmedPlaylistName}
onPress={addToNewPlaylist}
@@ -97,7 +95,7 @@ export function PlaylistPickerSheet({
<Text variant="label" color={colors.accentTextStrong}>
Create
</Text>
</Pressable>
</AppPressable>
</View>
</AppSheet>
);
+5 -6
View File
@@ -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}
/>
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded} style={styles.action} onPress={onClose} accessibilityRole="button">
<AppPressable feedback="control" style={styles.action} onPress={onClose} accessibilityRole="button">
<Text variant="body" color={colors.textSecondary}>
Cancel
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="control"
style={[styles.action, !trimmed && styles.actionDisabled]}
disabled={!trimmed}
onPress={submit}
@@ -85,7 +84,7 @@ function TextPromptModalInner({
<Text variant="body" color={colors.accent}>
{submitLabel}
</Text>
</Pressable>
</AppPressable>
</View>
</Pressable>
</Pressable>
+17 -18
View File
@@ -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) => (
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback={primary ? 'accent' : 'control'}
style={[
styles.onlineButton,
primary ? styles.onlineButtonPrimary : styles.onlineButtonSecondary,
@@ -60,13 +59,13 @@ export function SignalResolutionPanel({
<Text variant="body" color={primary ? colors.accentTextStrong : colors.textPrimary}>
Find online
</Text>
</Pressable>
</AppPressable>
);
const footer = (
<View style={styles.footerActions}>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="control"
style={styles.footerButton}
onPress={onScanAnother}
disabled={actionBusy}
@@ -74,16 +73,16 @@ export function SignalResolutionPanel({
>
<Ionicons name="scan-outline" size={17} color={colors.textSecondary} />
<Text variant="label">Scan another</Text>
</Pressable>
<Pressable
android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="control"
style={styles.footerButton}
onPress={onDone}
disabled={actionBusy}
accessibilityRole="button"
>
<Text variant="label">Done</Text>
</Pressable>
</AppPressable>
</View>
);
@@ -192,8 +191,8 @@ export function SignalResolutionPanel({
/>
<View style={styles.playActions}>
<Pressable
android_ripple={ripple.bounded}
<AppPressable feedback="accent"
style={[styles.primaryButton, actionBusy && styles.disabledButton]}
onPress={() => onPlay(track)}
disabled={actionBusy}
@@ -203,9 +202,9 @@ export function SignalResolutionPanel({
<Text variant="body" color={colors.accentTextStrong}>
{actionState === 'playing' ? 'Starting…' : 'Play now'}
</Text>
</Pressable>
<Pressable
android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="control"
style={[styles.secondaryButton, actionBusy && styles.disabledButton]}
onPress={() => onQueue(track)}
disabled={actionBusy || actionState === 'queued'}
@@ -223,7 +222,7 @@ export function SignalResolutionPanel({
? 'Added'
: 'Add to queue'}
</Text>
</Pressable>
</AppPressable>
</View>
{onlineAction()}
+9 -10
View File
@@ -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() {
</Text>
<View style={styles.choiceList}>
{options.map((resolution) => (
<Pressable android_ripple={ripple.bounded}
<AppPressable
key={resolution}
style={[
styles.choiceRow,
@@ -171,7 +170,7 @@ export function SyncConflictPrompt() {
onPress={() => setChoice({ syncUid: firstConflict.syncUid, resolution })}
>
<Text variant="label">{RESOLUTION_LABELS[resolution]}</Text>
</Pressable>
</AppPressable>
))}
</View>
<View style={styles.previewBox}>
@@ -185,20 +184,20 @@ export function SyncConflictPrompt() {
</Text>
</View>
{count > 1 ? (
<Pressable android_ripple={ripple.bounded} onPress={review} style={styles.reviewLink}>
<AppPressable feedback="control" onPress={review} style={styles.reviewLink}>
<Text variant="caption" color={colors.accent}>
Review all {count} conflicts
</Text>
</Pressable>
</AppPressable>
) : null}
</ScrollView>
<View style={styles.actions}>
<Pressable android_ripple={ripple.bounded} style={styles.secondaryButton} onPress={dismissConflictPrompt}>
<AppPressable feedback="control" style={styles.secondaryButton} onPress={dismissConflictPrompt}>
<Text variant="body" color={colors.textSecondary}>
Not now
</Text>
</Pressable>
<Pressable android_ripple={ripple.bounded}
</AppPressable>
<AppPressable feedback="accent"
style={[styles.primaryButton, (!selectedResolution || busy) && styles.disabled]}
disabled={!selectedResolution || busy}
onPress={confirm}
@@ -206,7 +205,7 @@ export function SyncConflictPrompt() {
<Text variant="body" color={colors.accentTextStrong}>
Confirm
</Text>
</Pressable>
</AppPressable>
</View>
</View>
</View>
+1 -7
View File
@@ -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',
-2
View File
@@ -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),
-53
View File
@@ -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]
);
}