mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
remove ripple
This commit is contained in:
@@ -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.",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
]);
|
||||
|
||||
+14
-6
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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
@@ -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()}
|
||||
</>
|
||||
|
||||
@@ -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
@@ -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}>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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 }];
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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. */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user