mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
optimize further
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
} from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { usePathname, useRouter } from 'expo-router';
|
||||
import { usePlayerUiStore } from '@/stores/playerUiStore';
|
||||
import { Text } from './Text';
|
||||
import { AstraLogo } from './AstraLogo';
|
||||
import { SpectrumCurve } from './SpectrumCurve';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
import { usePlaybackTargetStore } from '@/stores/playbackTargetStore';
|
||||
@@ -72,8 +73,8 @@ function PhoneMiniProgress({ isPlaying }: { isPlaying: boolean }) {
|
||||
export function MiniPlayer({ visible = true }: MiniPlayerProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const ripple = useRipple();
|
||||
const playerOpen = usePlayerUiStore((s) => s.playerOpen);
|
||||
const selectedTarget = usePlaybackTargetStore((s) => s.target);
|
||||
const track = usePlayerStore((s) => s.currentTrack);
|
||||
const playbackState = usePlayerStore((s) => s.playbackState);
|
||||
@@ -107,9 +108,9 @@ export function MiniPlayer({ visible = true }: MiniPlayerProps) {
|
||||
const isDesktop = presentation.target === 'desktop';
|
||||
const isPlaying = presentation.playbackState === 'playing';
|
||||
const isLoading = presentation.playbackState === 'loading';
|
||||
// The pill sits underneath the now-playing transparentModal; don't burn a
|
||||
// second live-scope frame loop while it's fully occluded.
|
||||
const liveScopeActive = visible && scopeActive && !isDesktop && pathname !== '/now-playing';
|
||||
// The pill sits underneath the now-playing overlay; don't burn a second
|
||||
// live-scope frame loop while it's fully occluded.
|
||||
const liveScopeActive = visible && scopeActive && !isDesktop && !playerOpen;
|
||||
|
||||
const onLayout = (e: LayoutChangeEvent) => setPillWidth(e.nativeEvent.layout.width);
|
||||
const onTogglePlay = () => {
|
||||
@@ -135,8 +136,9 @@ export function MiniPlayer({ visible = true }: MiniPlayerProps) {
|
||||
<>
|
||||
<Pressable
|
||||
pointerEvents={visible ? 'auto' : 'none'}
|
||||
android_ripple={ripple.bounded}
|
||||
style={[styles.pill, !visible && styles.hidden]}
|
||||
onPress={() => router.push('/now-playing')}
|
||||
onPress={() => usePlayerUiStore.getState().openPlayer()}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
{liveScopeActive && pillWidth > 0 && (
|
||||
@@ -183,6 +185,7 @@ export function MiniPlayer({ visible = true }: MiniPlayerProps) {
|
||||
{isDesktop ? (
|
||||
<Pressable
|
||||
hitSlop={10}
|
||||
android_ripple={ripple.icon(22)}
|
||||
onPress={() => setTargetPickerOpen(true)}
|
||||
style={styles.control}
|
||||
accessibilityLabel="Choose output device"
|
||||
@@ -190,14 +193,14 @@ export function MiniPlayer({ visible = true }: MiniPlayerProps) {
|
||||
<Ionicons name="desktop-outline" size={21} color={colors.textSecondary} />
|
||||
</Pressable>
|
||||
) : null}
|
||||
<Pressable hitSlop={10} onPress={onTogglePlay} style={styles.control}>
|
||||
<Pressable hitSlop={10} android_ripple={ripple.icon(22)} onPress={onTogglePlay} style={styles.control}>
|
||||
<Ionicons
|
||||
name={isLoading ? 'ellipsis-horizontal' : isPlaying ? 'pause' : 'play'}
|
||||
size={24}
|
||||
color={colors.accent}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable hitSlop={10} onPress={onSkipNext} style={styles.control}>
|
||||
<Pressable hitSlop={10} android_ripple={ripple.icon(22)} onPress={onSkipNext} style={styles.control}>
|
||||
<Ionicons name="play-skip-forward" size={22} color={colors.textPrimary} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
@@ -10,6 +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 { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
import { usePlaybackTargetStore, type PlaybackTarget } from '@/stores/playbackTargetStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
@@ -118,9 +119,11 @@ function TargetRow({
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
|
||||
android_ripple={ripple.bounded}
|
||||
style={styles.row}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected }}
|
||||
@@ -176,9 +179,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
rowPressed: {
|
||||
opacity: 0.65,
|
||||
},
|
||||
iconWrap: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
radius,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { motion } from '@/theme/motion';
|
||||
|
||||
const THUMB_INSET = 3;
|
||||
@@ -89,6 +90,7 @@ 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);
|
||||
|
||||
@@ -106,7 +108,8 @@ function SegmentButton({
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.segment, pressed && styles.segmentPressed]}
|
||||
android_ripple={ripple.bounded}
|
||||
style={styles.segment}
|
||||
onPress={onPress}
|
||||
accessibilityRole="tab"
|
||||
accessibilityState={{ selected: focused }}
|
||||
@@ -143,9 +146,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 7,
|
||||
},
|
||||
segmentPressed: {
|
||||
opacity: 0.72,
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontFamily: fonts.sans.medium,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { motion } from '@/theme/motion';
|
||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
import { usePlaybackTargetStore } from '@/stores/playbackTargetStore';
|
||||
@@ -161,6 +162,7 @@ interface TabButtonProps {
|
||||
function TabButton({ meta, focused, onPress }: 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.
|
||||
@@ -184,6 +186,7 @@ function TabButton({ meta, focused, onPress }: TabButtonProps) {
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
android_ripple={ripple.icon(26)}
|
||||
style={styles.tab}
|
||||
onPress={onPress}
|
||||
onPressIn={() => {
|
||||
|
||||
@@ -36,6 +36,11 @@ interface WaveformSeekBarProps {
|
||||
touchPadding?: number;
|
||||
/** Track file URI used to load/cache the offline waveform peaks. */
|
||||
trackPath?: string;
|
||||
/**
|
||||
* False while the bar is mounted but hidden (the now-playing overlay stays
|
||||
* mounted when closed): pins progress and stops the smooth-time rAF loop.
|
||||
*/
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const clamp = (fraction: number) => Math.min(1, Math.max(0, fraction));
|
||||
@@ -54,12 +59,13 @@ export function WaveformSeekBar({
|
||||
height = CANVAS_HEIGHT,
|
||||
touchPadding = spacing.md,
|
||||
trackPath,
|
||||
active = true,
|
||||
}: WaveformSeekBarProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const currentTime = usePlayerStore((s) => s.currentTime);
|
||||
const currentTime = usePlayerStore((s) => (active ? s.currentTime : 0));
|
||||
const duration = usePlayerStore((s) => s.duration);
|
||||
const isPlaying = usePlayerStore((s) => s.playbackState === 'playing');
|
||||
const isPlaying = usePlayerStore((s) => active && s.playbackState === 'playing');
|
||||
const [scrubFraction, setScrubFraction] = useState<number | null>(null);
|
||||
const [barWidth, setBarWidth] = useState(0);
|
||||
const pendingSeek = usePlayerStore((s) => s.pendingSeek);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import type { EQBand } from '@/types/audio';
|
||||
import {
|
||||
EQ_MAX_FREQUENCY,
|
||||
@@ -43,6 +44,7 @@ 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 (
|
||||
@@ -61,7 +63,7 @@ export function BandDetailPanel({ band, bandNumber, onUpdate, onEditType, onEdit
|
||||
<View style={styles.card}>
|
||||
<View style={styles.header}>
|
||||
<Text variant="heading">Band {bandNumber}</Text>
|
||||
<Pressable style={styles.typeButton} onPress={onEditType}>
|
||||
<Pressable android_ripple={ripple.bounded} style={styles.typeButton} onPress={onEditType}>
|
||||
<Text variant="label" color={colors.textPrimary}>
|
||||
{BAND_TYPE_LABEL[band.type]}
|
||||
</Text>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import type { EQBand } from '@/types/audio';
|
||||
import {
|
||||
formatFreq,
|
||||
@@ -28,6 +29,7 @@ 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
|
||||
@@ -38,7 +40,7 @@ export function BandStrip({ bands, activeBandId, canAdd, onSelect, onAdd }: Band
|
||||
{bands.map((band) => {
|
||||
const isActive = band.id === activeBandId;
|
||||
return (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
key={band.id}
|
||||
onPress={() => onSelect(band.id)}
|
||||
style={[styles.cell, isActive && styles.cellActive]}
|
||||
@@ -56,7 +58,7 @@ export function BandStrip({ bands, activeBandId, canAdd, onSelect, onAdd }: Band
|
||||
);
|
||||
})}
|
||||
{canAdd ? (
|
||||
<Pressable onPress={onAdd} style={[styles.cell, styles.addCell]} accessibilityLabel="Add band">
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onAdd} style={[styles.cell, styles.addCell]} accessibilityLabel="Add band">
|
||||
<Ionicons name="add" size={22} color={colors.accentText} />
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { EqSheet } from './EqSheet';
|
||||
|
||||
interface EQPresetNameSheetProps {
|
||||
@@ -28,6 +29,7 @@ export function EQPresetNameSheet({
|
||||
onClose,
|
||||
}: EQPresetNameSheetProps) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const [name, setName] = useState(defaultName);
|
||||
const trimmed = name.trim();
|
||||
@@ -56,12 +58,12 @@ export function EQPresetNameSheet({
|
||||
onSubmitEditing={submit}
|
||||
/>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.btn, styles.cancel]} onPress={onClose}>
|
||||
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={onClose}>
|
||||
<Text variant="label" color={colors.textSecondary}>
|
||||
Cancel
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.btn, styles.primary, !trimmed && styles.disabled]} disabled={!trimmed} onPress={submit}>
|
||||
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.primary, !trimmed && styles.disabled]} disabled={!trimmed} onPress={submit}>
|
||||
<Text variant="label" color={colors.accentTextStrong}>
|
||||
{actionLabel}
|
||||
</Text>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import type { EQPreset } from '@/types/audio';
|
||||
import { EqSheet } from './EqSheet';
|
||||
import { formatGain } from './format';
|
||||
@@ -29,6 +30,7 @@ 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';
|
||||
@@ -68,12 +70,12 @@ export function EQPresetPreviewSheet({
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.btn, styles.cancel]} onPress={onClose}>
|
||||
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={onClose}>
|
||||
<Text variant="label" color={colors.textSecondary}>
|
||||
Cancel
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
style={[styles.btn, styles.primary]}
|
||||
onPress={() => {
|
||||
onConfirm();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { EqSheet } from './EqSheet';
|
||||
|
||||
interface EQPresetQrSheetProps {
|
||||
@@ -20,6 +21,7 @@ interface EQPresetQrSheetProps {
|
||||
|
||||
export function EQPresetQrSheet({ presetName, value, onClose }: EQPresetQrSheetProps) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
|
||||
return (
|
||||
@@ -33,7 +35,7 @@ export function EQPresetQrSheet({ presetName, value, onClose }: EQPresetQrSheetP
|
||||
<Text variant="label" numberOfLines={1} color={colors.textSecondary} style={styles.name}>
|
||||
{presetName}
|
||||
</Text>
|
||||
<Pressable style={styles.done} onPress={onClose}>
|
||||
<Pressable android_ripple={ripple.bounded} style={styles.done} onPress={onClose}>
|
||||
<Text variant="label" color={colors.accentTextStrong}>
|
||||
Done
|
||||
</Text>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
|
||||
const THUMB = 16;
|
||||
|
||||
@@ -43,6 +44,7 @@ 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);
|
||||
@@ -119,6 +121,9 @@ export function EQSlider({
|
||||
</View>
|
||||
{onValuePress && !disabled ? (
|
||||
<Pressable
|
||||
android_ripple={ripple.bounded}
|
||||
// Deliberate two-property pressed emphasis (accent border + fill), kept
|
||||
// alongside the ripple — it marks the value as editable, not just pressed.
|
||||
style={({ pressed }) => [styles.valueButton, pressed && styles.valueButtonPressed]}
|
||||
onPress={onValuePress}
|
||||
accessibilityRole="button"
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { EqSheet } from './EqSheet';
|
||||
|
||||
interface EQValueEditSheetProps {
|
||||
@@ -40,6 +41,7 @@ export function EQValueEditSheet({
|
||||
onClose,
|
||||
}: EQValueEditSheetProps) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const trimmed = value.trim();
|
||||
@@ -80,12 +82,12 @@ export function EQValueEditSheet({
|
||||
{valid || trimmed.length === 0 ? rangeLabel : 'Enter a valid number'}
|
||||
</Text>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.btn, styles.cancel]} onPress={onClose}>
|
||||
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={onClose}>
|
||||
<Text variant="label" color={colors.textSecondary}>
|
||||
Cancel
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
style={[styles.btn, styles.apply, !valid && styles.applyDisabled]}
|
||||
disabled={!valid}
|
||||
onPress={apply}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import { spacing } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import type { EQPreset } from '@/types/audio';
|
||||
import {
|
||||
EqSheet,
|
||||
@@ -37,6 +38,7 @@ 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);
|
||||
|
||||
@@ -76,7 +78,7 @@ export function PresetSheet({
|
||||
onClose();
|
||||
}}
|
||||
trailing={
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
hitSlop={10}
|
||||
onPress={() => onDelete(p.id)}
|
||||
style={styles.delete}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { EqSheet } from './EqSheet';
|
||||
|
||||
interface SavePresetSheetProps {
|
||||
@@ -23,6 +24,7 @@ interface SavePresetSheetProps {
|
||||
/** Name + save a custom preset from the current bands/preamp. */
|
||||
export function SavePresetSheet({ defaultName, onSave, onClose }: SavePresetSheetProps) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const [name, setName] = useState(defaultName);
|
||||
const trimmed = name.trim();
|
||||
@@ -50,12 +52,12 @@ export function SavePresetSheet({ defaultName, onSave, onClose }: SavePresetShee
|
||||
}}
|
||||
/>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.btn, styles.cancel]} onPress={onClose}>
|
||||
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={onClose}>
|
||||
<Text variant="label" color={colors.textSecondary}>
|
||||
Cancel
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
style={[styles.btn, styles.save, !trimmed && styles.saveDisabled]}
|
||||
disabled={!trimmed}
|
||||
onPress={() => {
|
||||
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
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 style={styles.item} onPress={onPress} accessibilityRole="button">
|
||||
<Pressable android_ripple={ripple.tile} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.item} onPress={onPress} accessibilityRole="button">
|
||||
<View style={styles.art}>
|
||||
{artUri ? (
|
||||
<Image
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { albumArtworkSource } from '@/library/artwork';
|
||||
import type { Album } from '@/types/library';
|
||||
|
||||
@@ -18,9 +19,10 @@ 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 style={styles.row} onPress={onPress} accessibilityRole="button">
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button">
|
||||
<View style={styles.art}>
|
||||
{artUri ? (
|
||||
<Image
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
radius,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { artworkUri } from '@/library/artwork';
|
||||
import type { Artist } from '@/types/library';
|
||||
|
||||
@@ -18,6 +19,7 @@ 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);
|
||||
|
||||
@@ -25,7 +27,7 @@ export function ArtistGridItem({ artist, onPress }: { artist: Artist; onPress: (
|
||||
const tracks = `${artist.track_count} ${artist.track_count === 1 ? 'track' : 'tracks'}`;
|
||||
|
||||
return (
|
||||
<Pressable style={styles.item} onPress={onPress} accessibilityRole="button">
|
||||
<Pressable android_ripple={ripple.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} />
|
||||
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
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();
|
||||
return (
|
||||
<Pressable style={styles.row} onPress={onPress} accessibilityRole="button">
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button">
|
||||
<View style={styles.art}>
|
||||
{artist.artwork_hash ? (
|
||||
<Image
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
|
||||
// Collapsing detail header. An absolute container whose height shrinks with the
|
||||
// scroll and clips its faded content, so the track list (padded to the expanded
|
||||
@@ -155,6 +156,7 @@ export function CollapsingHeader({
|
||||
onHeroBlockLayout: (e: LayoutChangeEvent) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width: W } = useWindowDimensions();
|
||||
@@ -246,7 +248,7 @@ export function CollapsingHeader({
|
||||
{heroMeta}
|
||||
{heroExtra}
|
||||
<Animated.View style={[styles.actionRow, heroButtonsStyle]}>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={[styles.actionButton, styles.primaryAction, disabled && styles.disabledAction]}
|
||||
onPress={onPlay}
|
||||
disabled={disabled}
|
||||
@@ -257,7 +259,7 @@ export function CollapsingHeader({
|
||||
Play
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={[styles.actionButton, styles.secondaryAction, disabled && styles.disabledAction]}
|
||||
onPress={onShuffle}
|
||||
disabled={disabled}
|
||||
@@ -271,7 +273,7 @@ export function CollapsingHeader({
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
onPress={onBack}
|
||||
hitSlop={8}
|
||||
style={[styles.chevron, { top: barCenterY - 12, left: spacing.md }]}
|
||||
@@ -299,16 +301,16 @@ export function CollapsingHeader({
|
||||
style={[styles.barIcons, { top: barCenterY - 16, right: onMore ? spacing.md + 40 : spacing.md }, barIconsStyle]}
|
||||
pointerEvents={collapsed ? 'auto' : 'none'}
|
||||
>
|
||||
<Pressable onPress={onPlay} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onPlay} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
|
||||
<Ionicons name="play" size={20} color={colors.accent} />
|
||||
</Pressable>
|
||||
<Pressable onPress={onShuffle} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onShuffle} disabled={disabled} hitSlop={6} style={styles.iconBtn}>
|
||||
<Ionicons name="shuffle" size={20} color={colors.accent} />
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
|
||||
{onMore ? (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
onPress={onMore}
|
||||
hitSlop={8}
|
||||
style={[styles.moreButton, { top: barCenterY - 16, right: spacing.md }]}
|
||||
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
|
||||
export function EmptyLibrary() {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -25,7 +27,7 @@ export function EmptyLibrary() {
|
||||
<Text variant="body" color={colors.textSecondary} style={styles.body}>
|
||||
Pick a folder on this device and Astra will scan it into your library.
|
||||
</Text>
|
||||
<Pressable style={styles.cta} onPress={() => router.push('/settings')} accessibilityRole="button">
|
||||
<Pressable android_ripple={ripple.bounded} style={styles.cta} onPress={() => router.push('/settings')} accessibilityRole="button">
|
||||
<Ionicons name="folder-open-outline" size={18} color={colors.bgPrimary} />
|
||||
<Text variant="body" style={styles.ctaLabel}>
|
||||
Folder settings
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import type { DbTrack } from '@/types/library';
|
||||
@@ -60,6 +61,7 @@ function FolderRow({
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const { node, depth, isExpanded } = row;
|
||||
|
||||
const play = (event: GestureResponderEvent) => {
|
||||
@@ -73,7 +75,8 @@ function FolderRow({
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.folderRow, pressed && styles.rowPressed]}
|
||||
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.folderRow}
|
||||
onPress={() => onToggle(node.id)}
|
||||
onLongPress={() => onOpenActions(node)}
|
||||
accessibilityRole="button"
|
||||
@@ -104,7 +107,8 @@ function FolderRow({
|
||||
{node.totalTrackCount}
|
||||
</Text>
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.folderButton, pressed && styles.folderButtonPressed]}
|
||||
android_ripple={ripple.icon(20)} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.folderButton}
|
||||
onPress={play}
|
||||
hitSlop={6}
|
||||
accessibilityRole="button"
|
||||
@@ -113,7 +117,8 @@ function FolderRow({
|
||||
<Ionicons name="play" size={16} color={colors.accent} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.folderButton, pressed && styles.folderButtonPressed]}
|
||||
android_ripple={ripple.icon(20)} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.folderButton}
|
||||
onPress={shuffle}
|
||||
hitSlop={6}
|
||||
accessibilityRole="button"
|
||||
@@ -136,6 +141,7 @@ function FolderTrackRow({
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const index = row.folderTracks.findIndex((track) => track.path === row.track.path);
|
||||
|
||||
const playFolderTrack = () => {
|
||||
@@ -148,11 +154,8 @@ function FolderTrackRow({
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.trackRow,
|
||||
active && styles.trackRowActive,
|
||||
pressed && styles.rowPressed,
|
||||
]}
|
||||
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={[styles.trackRow, active && styles.trackRowActive]}
|
||||
onPress={playFolderTrack}
|
||||
onLongPress={onOpenActions}
|
||||
accessibilityRole="button"
|
||||
@@ -171,7 +174,8 @@ function FolderTrackRow({
|
||||
{formatDuration(row.track.duration)}
|
||||
</Text>
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.actionsButton, pressed && styles.actionsButtonPressed]}
|
||||
android_ripple={ripple.icon(21)} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.actionsButton}
|
||||
onPress={openActions}
|
||||
hitSlop={8}
|
||||
accessibilityRole="button"
|
||||
@@ -347,9 +351,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
folderButtonPressed: {
|
||||
backgroundColor: colors.glassBg,
|
||||
},
|
||||
trackRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -362,9 +363,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
trackRowActive: {
|
||||
backgroundColor: colors.accentGlow,
|
||||
},
|
||||
rowPressed: {
|
||||
opacity: 0.72,
|
||||
},
|
||||
trackMeta: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
@@ -390,9 +388,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
actionsButtonPressed: {
|
||||
backgroundColor: colors.glassBg,
|
||||
},
|
||||
empty: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { artworkUri } from '@/library/artwork';
|
||||
|
||||
export function PlaylistRow({
|
||||
@@ -39,8 +40,10 @@ export function PlaylistRow({
|
||||
}) {
|
||||
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}
|
||||
onLongPress={onLongPress}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||
import type { Playlist } from '@/types/playlist';
|
||||
|
||||
@@ -47,6 +48,7 @@ export function PlaylistsView({
|
||||
scrollEventThrottle?: number;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const playlists = usePlaylistStore((s) => s.playlists);
|
||||
@@ -205,7 +207,7 @@ export function PlaylistsView({
|
||||
/>
|
||||
|
||||
<View style={styles.addBar}>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.addButton}
|
||||
onPress={() => setAddSheetOpen(true)}
|
||||
accessibilityRole="button"
|
||||
|
||||
@@ -7,6 +7,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';
|
||||
|
||||
interface SelectionActionBarProps {
|
||||
count: number;
|
||||
@@ -66,13 +67,11 @@ function BarButton({
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.button,
|
||||
pressed && styles.buttonPressed,
|
||||
disabled && styles.buttonDisabled,
|
||||
]}
|
||||
android_ripple={ripple.bounded}
|
||||
style={[styles.button, disabled && styles.buttonDisabled]}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
accessibilityRole="button"
|
||||
@@ -101,9 +100,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
gap: spacing.xs,
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
buttonPressed: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { formatDuration } from '@/lib/format';
|
||||
import { trackArtworkThumbSource } from '@/library/artwork';
|
||||
import { dbTrackToTrack } from '@/library/trackAdapter';
|
||||
@@ -60,6 +61,7 @@ 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;
|
||||
@@ -74,6 +76,7 @@ export function TrackRow({
|
||||
|
||||
const row = (
|
||||
<Pressable
|
||||
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={[styles.row, selectionMode && selected && styles.rowSelected]}
|
||||
onPress={selectionMode ? onToggleSelect : onPress}
|
||||
onLongPress={selectionMode ? onToggleSelect : (onLongPress ?? onOpenActions)}
|
||||
@@ -143,7 +146,8 @@ export function TrackRow({
|
||||
|
||||
{onOpenActions && !selectionMode ? (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.actionsButton, pressed && styles.actionsButtonPressed]}
|
||||
android_ripple={ripple.icon(ACTIONS_BUTTON / 2 + 4)} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.actionsButton}
|
||||
onPress={openActions}
|
||||
hitSlop={8}
|
||||
accessibilityRole="button"
|
||||
@@ -257,7 +261,4 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
actionsButtonPressed: {
|
||||
backgroundColor: colors.glassBg,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Pressable, ScrollView, View, type LayoutChangeEvent } from 'react-native';
|
||||
import { Text } from '@/components/Text';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { useSmoothPlaybackTime } from '@/audio/useSmoothPlaybackTime';
|
||||
import { useLyricsStore } from '@/stores/lyricsStore';
|
||||
import {
|
||||
@@ -44,6 +45,7 @@ function clamp(value: number, min: number, max: number): number {
|
||||
|
||||
export function LyricsBand({ track, currentTime, duration, isPlaying, onSeek }: LyricsBandProps) {
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const entry = useLyricsStore((s) => s.byPath[track.path]);
|
||||
const loadForTrack = useLyricsStore((s) => s.loadForTrack);
|
||||
|
||||
@@ -228,7 +230,7 @@ export function LyricsBand({ track, currentTime, duration, isPlaying, onSeek }:
|
||||
</ScrollView>
|
||||
|
||||
{followPaused ? (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
onPress={recenter}
|
||||
hitSlop={10}
|
||||
style={{
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Pressable, 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 { getPreferredLyricsTranslation } from '@/lyrics/presentation';
|
||||
import type { LyricsFurigana, LyricsLine as LyricsLineData } from '@/lyrics/types';
|
||||
|
||||
@@ -68,6 +69,7 @@ function buildSegments(text: string, furigana: LyricsFurigana[] | undefined): Se
|
||||
|
||||
function LyricsLineComponent({ line, tier, baseSize, translationPriority, onSeek, onLayout }: LyricsLineProps) {
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const target = TIER[tier];
|
||||
// Uniform metrics for every line (the whole point — no wrap/reflow between tiers).
|
||||
const size = baseSize;
|
||||
@@ -100,7 +102,7 @@ function LyricsLineComponent({ line, tier, baseSize, translationPriority, onSeek
|
||||
|
||||
return (
|
||||
<Animated.View onLayout={onLayout} style={[{ width: '100%', transformOrigin: 'left center' }, animatedStyle]}>
|
||||
<Pressable onPress={onSeek} style={{ paddingVertical: 7 }}>
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={onSeek} style={{ paddingVertical: 7 }}>
|
||||
<View style={{ alignItems: 'flex-start' }}>
|
||||
{hasFurigana ? (
|
||||
<View
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SeekBar } from '@/components/SeekBar';
|
||||
import { LyricsBand } from './LyricsBand';
|
||||
import { spacing, radius } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useLyricsStore } from '@/stores/lyricsStore';
|
||||
import { getLyricsPayloadSourceLabel } from '@/lyrics/presentation';
|
||||
@@ -22,6 +23,8 @@ import type { Track } from '@/types/audio';
|
||||
|
||||
interface LyricsViewProps {
|
||||
track: Track;
|
||||
/** False while mounted but hidden (closed now-playing overlay): pins progress, stops rAF loops. */
|
||||
active?: boolean;
|
||||
isPlaying: boolean;
|
||||
isLoading: boolean;
|
||||
isFavorite: boolean;
|
||||
@@ -36,6 +39,7 @@ interface LyricsViewProps {
|
||||
|
||||
export function LyricsView({
|
||||
track,
|
||||
active = true,
|
||||
isPlaying,
|
||||
isLoading,
|
||||
isFavorite,
|
||||
@@ -48,10 +52,12 @@ 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.
|
||||
const currentTime = usePlayerStore((s) => s.currentTime);
|
||||
// While inactive the selector pins to 0 so hidden ticks don't re-render.
|
||||
const currentTime = usePlayerStore((s) => (active ? s.currentTime : 0));
|
||||
const duration = usePlayerStore((s) => s.duration);
|
||||
const result = useLyricsStore((s) => s.byPath[track.path]?.result ?? null);
|
||||
const sourceLabel = result?.status === 'hit' ? getLyricsPayloadSourceLabel(result.lyrics) : null;
|
||||
@@ -59,7 +65,7 @@ export function LyricsView({
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<View style={styles.strip}>
|
||||
<Pressable onPress={onDismiss} hitSlop={12} style={styles.stripBtn} accessibilityLabel="Close player">
|
||||
<Pressable android_ripple={ripple.bounded} onPress={onDismiss} hitSlop={12} style={styles.stripBtn} accessibilityLabel="Close player">
|
||||
<Ionicons name="chevron-down" size={24} color={colors.textSecondary} />
|
||||
</Pressable>
|
||||
|
||||
@@ -87,7 +93,7 @@ export function LyricsView({
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
onPress={onToggleFavorite}
|
||||
hitSlop={10}
|
||||
style={styles.stripBtn}
|
||||
@@ -101,7 +107,7 @@ export function LyricsView({
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
onPress={onExitLyrics}
|
||||
hitSlop={10}
|
||||
style={styles.stripBtn}
|
||||
@@ -116,24 +122,24 @@ export function LyricsView({
|
||||
track={track}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isPlaying={isPlaying}
|
||||
isPlaying={isPlaying && active}
|
||||
onSeek={onSeek}
|
||||
/>
|
||||
|
||||
<View style={styles.controls}>
|
||||
<SeekBar currentTime={currentTime} duration={duration} trackKey={track.id} onSeek={onSeek} />
|
||||
<View style={styles.transport}>
|
||||
<Pressable onPress={onPrev} hitSlop={12} style={styles.transportBtn} accessibilityLabel="Previous">
|
||||
<Pressable android_ripple={ripple.bounded} onPress={onPrev} hitSlop={12} style={styles.transportBtn} accessibilityLabel="Previous">
|
||||
<Ionicons name="play-skip-back" size={28} color={colors.textPrimary} />
|
||||
</Pressable>
|
||||
<Pressable onPress={onPlayPause} hitSlop={12} style={styles.playButton} accessibilityLabel={isPlaying ? 'Pause' : 'Play'}>
|
||||
<Pressable android_ripple={ripple.bounded} onPress={onPlayPause} hitSlop={12} style={styles.playButton} accessibilityLabel={isPlaying ? 'Pause' : 'Play'}>
|
||||
<Ionicons
|
||||
name={isLoading ? 'ellipsis-horizontal' : isPlaying ? 'pause' : 'play'}
|
||||
size={28}
|
||||
color={colors.bgPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable onPress={onNext} hitSlop={12} style={styles.transportBtn} accessibilityLabel="Next">
|
||||
<Pressable android_ripple={ripple.bounded} onPress={onNext} hitSlop={12} style={styles.transportBtn} accessibilityLabel="Next">
|
||||
<Ionicons name="play-skip-forward" size={28} color={colors.textPrimary} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
@@ -26,6 +26,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 type { BaseThemeId } from '@/theme/resolve';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
@@ -51,6 +52,7 @@ 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();
|
||||
@@ -124,7 +126,7 @@ export function OnboardingFlow({ onDone }: { onDone: () => void }) {
|
||||
</View>
|
||||
<View style={styles.navRow}>
|
||||
{canGoBack ? (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
onPress={goBack}
|
||||
style={styles.secondaryButton}
|
||||
accessibilityRole="button"
|
||||
@@ -135,7 +137,7 @@ export function OnboardingFlow({ onDone }: { onDone: () => void }) {
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
onPress={goNext}
|
||||
style={styles.primaryButton}
|
||||
accessibilityRole="button"
|
||||
@@ -174,6 +176,7 @@ 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);
|
||||
@@ -187,7 +190,7 @@ function LibraryStep() {
|
||||
title="Add your music"
|
||||
subtitle="Point Astra at the folders where your music lives. It scans them into your library — files on disk are never modified."
|
||||
/>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
style={[styles.choiceButton, isScanning && styles.disabled]}
|
||||
disabled={isScanning}
|
||||
onPress={() => void addFolder()}
|
||||
@@ -224,6 +227,7 @@ 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);
|
||||
@@ -248,7 +252,7 @@ function ThemeStep() {
|
||||
{options.map((option) => {
|
||||
const selected = option.id === baseTheme;
|
||||
return (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
key={option.id}
|
||||
onPress={() => void setBaseTheme(option.id)}
|
||||
style={[styles.themePill, selected && styles.themePillSelected]}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useEffect } from 'react';
|
||||
import { NowPlayingOverlay } from '@/components/player/NowPlayingOverlay';
|
||||
import { usePlayerUiStore } from '@/stores/playerUiStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
|
||||
const PREWARM_DELAY_MS = 2000;
|
||||
|
||||
/**
|
||||
* Mount gate for the always-mounted now-playing overlay. Nothing mounts until a
|
||||
* track exists (cold start unchanged); shortly after playback first starts the
|
||||
* overlay pre-warms hidden so even the FIRST open is a pure slide, no mount cost.
|
||||
*/
|
||||
export function NowPlayingHost() {
|
||||
const everOpened = usePlayerUiStore((s) => s.everOpened);
|
||||
const hasTrack = usePlayerStore((s) => Boolean(s.currentTrack));
|
||||
|
||||
useEffect(() => {
|
||||
if (everOpened || !hasTrack) return;
|
||||
const timer = setTimeout(() => usePlayerUiStore.getState().prewarm(), PREWARM_DELAY_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [everOpened, hasTrack]);
|
||||
|
||||
if (!everOpened) return null;
|
||||
return <NowPlayingOverlay />;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { motion } from '@/theme/motion';
|
||||
import { artworkThumbFromSource } from '@/library/artwork';
|
||||
import { dragArmHaptic, tickHaptic } from '@/lib/haptics';
|
||||
@@ -155,6 +156,7 @@ interface QueueTrayProps {
|
||||
export const QueueTray = memo(function QueueTray({ onClose }: QueueTrayProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { height: windowHeight } = useWindowDimensions();
|
||||
const snapPoints = useMemo(() => ['58%', '100%'], []);
|
||||
@@ -595,7 +597,7 @@ export const QueueTray = memo(function QueueTray({ onClose }: QueueTrayProps) {
|
||||
</Text>
|
||||
</View>
|
||||
{canEdit ? (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
hitSlop={10}
|
||||
onPress={() => (editMode ? exitEdit() : setEditMode(true))}
|
||||
accessibilityRole="button"
|
||||
@@ -654,7 +656,8 @@ export const QueueTray = memo(function QueueTray({ onClose }: QueueTrayProps) {
|
||||
{editMode && selectedCount > 0 ? (
|
||||
<View style={[styles.actionBar, { paddingBottom: insets.bottom + spacing.sm }]}>
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.actionBtn, pressed && styles.actionBtnPressed]}
|
||||
android_ripple={ripple.bounded}
|
||||
style={styles.actionBtn}
|
||||
onPress={groupPlayNext}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Play ${selectedCount} selected songs next`}
|
||||
@@ -665,7 +668,8 @@ export const QueueTray = memo(function QueueTray({ onClose }: QueueTrayProps) {
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.actionBtn, pressed && styles.actionBtnPressed]}
|
||||
android_ripple={ripple.bounded}
|
||||
style={styles.actionBtn}
|
||||
onPress={groupRemove}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Remove ${selectedCount} selected songs from queue`}
|
||||
@@ -751,6 +755,7 @@ const QueueRow = memo(function QueueRow({
|
||||
}: QueueRowProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const entryKey = entry.key;
|
||||
const title = trackTitle(entry.track);
|
||||
const artist = trackArtist(entry.track);
|
||||
@@ -848,7 +853,7 @@ const QueueRow = memo(function QueueRow({
|
||||
const rowContent = (
|
||||
<Animated.View style={[styles.row, rowSurfaceStyle]}>
|
||||
{editMode ? (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
hitSlop={8}
|
||||
onPress={onToggleSelect}
|
||||
style={styles.checkbox}
|
||||
@@ -891,7 +896,8 @@ const QueueRow = memo(function QueueRow({
|
||||
<Animated.View style={[styles.rowOuter, rowMotionStyle]}>
|
||||
<Pressable
|
||||
onPress={onJump}
|
||||
style={({ pressed }) => [styles.rowPressable, pressed && styles.rowPressed]}
|
||||
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.rowPressable}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Play ${title}`}
|
||||
accessibilityHint="Opens this song in the queue"
|
||||
@@ -907,7 +913,8 @@ const QueueRow = memo(function QueueRow({
|
||||
<Animated.View style={[styles.rowOuter, rowMotionStyle]}>
|
||||
<Pressable
|
||||
onPress={onToggleSelect}
|
||||
style={({ pressed }) => [styles.rowPressable, pressed && styles.rowPressed]}
|
||||
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.rowPressable}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected }}
|
||||
accessibilityLabel={`${selected ? 'Deselect' : 'Select'} ${title}`}
|
||||
@@ -927,7 +934,8 @@ const QueueRow = memo(function QueueRow({
|
||||
>
|
||||
<Pressable
|
||||
onPress={onJump}
|
||||
style={({ pressed }) => [styles.rowPressable, pressed && styles.rowPressed]}
|
||||
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.rowPressable}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Play ${title}`}
|
||||
accessibilityHint="Opens this song in the queue"
|
||||
@@ -1023,9 +1031,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
rowPressable: {
|
||||
height: QUEUE_ROW_HEIGHT,
|
||||
},
|
||||
rowPressed: {
|
||||
opacity: 0.72,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -1090,9 +1095,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
gap: spacing.sm,
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
actionBtnPressed: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
actionText: {
|
||||
color: colors.accent,
|
||||
},
|
||||
|
||||
@@ -17,6 +17,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 { formatDuration } from '@/lib/format';
|
||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
import type { DesktopRemoteQueueItem } from '@/types/desktopRemote';
|
||||
@@ -28,6 +29,7 @@ interface RemoteQueueSheetProps {
|
||||
export function RemoteQueueSheet({ onClose }: RemoteQueueSheetProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const queue = useDesktopRemoteStore((s) => s.queue);
|
||||
const snapPoints = useMemo(() => ['58%', '100%'], []);
|
||||
const renderFlashListScrollComponent = useBottomSheetScrollableCreator();
|
||||
@@ -66,7 +68,8 @@ export function RemoteQueueSheet({ onClose }: RemoteQueueSheetProps) {
|
||||
const renderItem = useCallback(
|
||||
({ item }: ListRenderItemInfo<DesktopRemoteQueueItem>) => (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.row, pressed && !item.isCurrent && styles.rowPressed]}
|
||||
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.row}
|
||||
onPress={() => playItem(item)}
|
||||
disabled={item.isCurrent}
|
||||
accessibilityRole="button"
|
||||
@@ -95,7 +98,7 @@ export function RemoteQueueSheet({ onClose }: RemoteQueueSheetProps) {
|
||||
) : null}
|
||||
</Pressable>
|
||||
),
|
||||
[playItem, colors, styles]
|
||||
[playItem, colors, styles, ripple]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -159,9 +162,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
rowPressed: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
rowText: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
import { rgbaFromHex } from '@/theme/colorUtils';
|
||||
import { enqueueTop, playTracks } from '@/audio/playbackController';
|
||||
import { dbTrackToTrack } from '@/library/trackAdapter';
|
||||
@@ -483,6 +484,7 @@ 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';
|
||||
@@ -494,7 +496,7 @@ function ResultRow({
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={[styles.resultRow, active && styles.resultRowActive, isShowMode && styles.showModeRow]}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
@@ -509,7 +511,7 @@ function ResultRow({
|
||||
</Text>
|
||||
</View>
|
||||
{isTrack ? (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.queueButton}
|
||||
onPress={queueTrack}
|
||||
hitSlop={8}
|
||||
@@ -535,6 +537,7 @@ function QuickSearchPanel({
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -1005,7 +1008,7 @@ function QuickSearchPanel({
|
||||
style={styles.input}
|
||||
/>
|
||||
{query.length > 0 ? (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
onPress={() => updateQuery('')}
|
||||
hitSlop={8}
|
||||
accessibilityRole="button"
|
||||
@@ -1014,7 +1017,7 @@ function QuickSearchPanel({
|
||||
<Ionicons name="close-circle" size={18} color={colors.textTertiary} />
|
||||
</Pressable>
|
||||
) : null}
|
||||
<Pressable onPress={close} hitSlop={8} accessibilityRole="button" accessibilityLabel="Close search">
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} onPress={close} hitSlop={8} accessibilityRole="button" accessibilityLabel="Close search">
|
||||
<Ionicons name="close" size={20} color={colors.textSecondary} />
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -1066,6 +1069,7 @@ 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);
|
||||
@@ -1085,7 +1089,7 @@ export function QuickSearchOverlay() {
|
||||
onRequestClose={close}
|
||||
>
|
||||
<View style={styles.modalRoot}>
|
||||
<Pressable style={StyleSheet.absoluteFill} onPress={close} accessibilityRole="button" />
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={StyleSheet.absoluteFill} onPress={close} accessibilityRole="button" />
|
||||
{isOpen ? (
|
||||
<QuickSearchPanel key={openVersion} initialQuery={initialQuery} onClose={close} />
|
||||
) : null}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Text } from '@/components/Text';
|
||||
import { spacing } from '@/theme';
|
||||
import { ACCENTS, ACCENT_IDS, type AccentId } from '@/theme/accents';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
|
||||
const SWATCH_SIZE = 36;
|
||||
|
||||
@@ -15,6 +16,7 @@ interface AccentSwatchRowProps {
|
||||
/** Circular accent swatches; the selected one gets a ring + checkmark. */
|
||||
export function AccentSwatchRow({ value, onChange }: AccentSwatchRowProps) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.wrap}>
|
||||
@@ -22,7 +24,7 @@ export function AccentSwatchRow({ value, onChange }: AccentSwatchRowProps) {
|
||||
{ACCENT_IDS.map((id) => {
|
||||
const selected = id === value;
|
||||
return (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
key={id}
|
||||
onPress={() => onChange(id)}
|
||||
accessibilityRole="radio"
|
||||
|
||||
@@ -16,6 +16,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 type { ReplayGainMode } from '@/audio/normalization';
|
||||
import type { ArtistGroupingMode } from '@/library/artistGrouping';
|
||||
import type { BaseThemeId, PreferredDark } from '@/theme/resolve';
|
||||
@@ -81,6 +82,7 @@ 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);
|
||||
@@ -102,7 +104,7 @@ export function AppearanceSettingsPanel() {
|
||||
{options.map((option) => {
|
||||
const selected = option.id === baseTheme;
|
||||
return (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
key={option.id}
|
||||
style={[styles.option, selected && styles.optionSelected]}
|
||||
onPress={() => void setBaseTheme(option.id)}
|
||||
@@ -159,6 +161,7 @@ function LibraryFolderSettingsRow({
|
||||
onRemove: (folder: FolderWithCount) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.folderSettingsRow}>
|
||||
@@ -181,7 +184,7 @@ function LibraryFolderSettingsRow({
|
||||
: 'Access lost. Remove and add again.'}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
hitSlop={8}
|
||||
disabled={disabled}
|
||||
onPress={() => onRemove(folder)}
|
||||
@@ -197,6 +200,7 @@ function LibraryFolderSettingsRow({
|
||||
|
||||
function LibraryFoldersSettings() {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const folders = useLibraryStore((s) => s.folders);
|
||||
const isScanning = useLibraryStore((s) => s.isScanning);
|
||||
@@ -230,7 +234,7 @@ function LibraryFoldersSettings() {
|
||||
: `${formatFolderCount(folders.length)} / ${formatTrackCount(totalTracks)}`}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={[styles.folderPrimaryAction, isScanning && styles.actionDisabled]}
|
||||
disabled={isScanning}
|
||||
onPress={() => void addFolder()}
|
||||
@@ -245,7 +249,7 @@ function LibraryFoldersSettings() {
|
||||
|
||||
{folders.length > 0 ? (
|
||||
<View style={styles.folderSettingsActions}>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={[styles.folderSecondaryAction, isScanning && styles.actionDisabled]}
|
||||
disabled={isScanning}
|
||||
onPress={() => void rescan()}
|
||||
@@ -291,6 +295,7 @@ 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);
|
||||
@@ -314,7 +319,7 @@ export function LibrarySettingsPanel() {
|
||||
{ARTIST_GROUPING_OPTIONS.map((option) => {
|
||||
const selected = option.mode === groupingMode;
|
||||
return (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
key={option.mode}
|
||||
style={[styles.option, selected && styles.optionSelected]}
|
||||
onPress={() => void setArtistGroupingMode(option.mode)}
|
||||
@@ -353,6 +358,7 @@ 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);
|
||||
@@ -400,7 +406,7 @@ export function AudioSettingsPanel() {
|
||||
{REPLAYGAIN_MODES.map((m) => {
|
||||
const selected = m.mode === replayGainMode;
|
||||
return (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
key={m.mode}
|
||||
style={[styles.modePill, selected && styles.modePillSelected]}
|
||||
onPress={() => void setReplayGainMode(m.mode)}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
|
||||
export type SettingsIconName = keyof typeof Ionicons.glyphMap;
|
||||
|
||||
@@ -26,12 +27,18 @@ export function SettingsSectionScreen({
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<View style={styles.header}>
|
||||
<Pressable style={styles.back} onPress={() => router.back()} hitSlop={8}>
|
||||
<Pressable
|
||||
android_ripple={ripple.bounded}
|
||||
style={styles.back}
|
||||
onPress={() => router.back()}
|
||||
hitSlop={8}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
|
||||
<Text variant="body" color={colors.textSecondary}>
|
||||
Settings
|
||||
@@ -98,8 +105,9 @@ export function SettingsNavRow({
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
return (
|
||||
<Pressable style={styles.row} onPress={onPress} accessibilityRole="button">
|
||||
<Pressable android_ripple={ripple.bounded} 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>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
|
||||
export interface ActionSheetItem {
|
||||
key: string;
|
||||
@@ -39,6 +40,7 @@ export function ActionSheet({
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
@@ -60,7 +62,8 @@ export function ActionSheet({
|
||||
{items.map((item) => (
|
||||
<Pressable
|
||||
key={item.key}
|
||||
style={({ pressed }) => [styles.item, pressed && styles.itemPressed]}
|
||||
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.item}
|
||||
onPress={item.onPress}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
@@ -122,9 +125,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
gap: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
itemPressed: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
itemLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
|
||||
|
||||
export function AppSheet({ onClose, children }: { onClose: () => void; children: ReactNode }) {
|
||||
const styles = useStyles();
|
||||
@@ -95,12 +96,14 @@ export function AppSheetItem({
|
||||
}: AppSheetItemProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const ripple = useRipple();
|
||||
const tint = destructive ? colors.warning : selected ? colors.accentTextStrong : colors.textPrimary;
|
||||
|
||||
return (
|
||||
<View style={styles.itemRow}>
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.item, pressed && styles.itemPressed]}
|
||||
android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY}
|
||||
style={styles.item}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
@@ -156,9 +159,6 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
gap: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
itemPressed: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
itemLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||
import type { DbTrack } from '@/types/library';
|
||||
|
||||
@@ -41,6 +42,7 @@ 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('');
|
||||
@@ -82,12 +84,12 @@ export function PlaylistPickerSheet({
|
||||
selectionColor={colors.accent}
|
||||
/>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={[styles.btn, styles.cancel]} onPress={() => setStep('pick')}>
|
||||
<Pressable android_ripple={ripple.bounded} style={[styles.btn, styles.cancel]} onPress={() => setStep('pick')}>
|
||||
<Text variant="label" color={colors.textSecondary}>
|
||||
Back
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
style={[styles.btn, styles.create, !trimmedPlaylistName && styles.createDisabled]}
|
||||
disabled={!trimmedPlaylistName}
|
||||
onPress={addToNewPlaylist}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
|
||||
interface TextPromptModalProps {
|
||||
visible: boolean;
|
||||
@@ -41,6 +42,7 @@ function TextPromptModalInner({
|
||||
onClose,
|
||||
}: TextPromptModalProps) {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const trimmed = value.trim();
|
||||
@@ -69,12 +71,12 @@ function TextPromptModalInner({
|
||||
selectionColor={colors.accent}
|
||||
/>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={styles.action} onPress={onClose} accessibilityRole="button">
|
||||
<Pressable android_ripple={ripple.bounded} style={styles.action} onPress={onClose} accessibilityRole="button">
|
||||
<Text variant="body" color={colors.textSecondary}>
|
||||
Cancel
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
style={[styles.action, !trimmed && styles.actionDisabled]}
|
||||
disabled={!trimmed}
|
||||
onPress={submit}
|
||||
|
||||
@@ -13,6 +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 { formatRelativeTime } from '@/lib/format';
|
||||
import {
|
||||
buildSyncConflictResolutionPreview,
|
||||
@@ -64,6 +65,7 @@ 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);
|
||||
@@ -158,7 +160,7 @@ export function SyncConflictPrompt() {
|
||||
</Text>
|
||||
<View style={styles.choiceList}>
|
||||
{options.map((resolution) => (
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
key={resolution}
|
||||
style={[
|
||||
styles.choiceRow,
|
||||
@@ -183,7 +185,7 @@ export function SyncConflictPrompt() {
|
||||
</Text>
|
||||
</View>
|
||||
{count > 1 ? (
|
||||
<Pressable onPress={review} style={styles.reviewLink}>
|
||||
<Pressable android_ripple={ripple.bounded} onPress={review} style={styles.reviewLink}>
|
||||
<Text variant="caption" color={colors.accent}>
|
||||
Review all {count} conflicts
|
||||
</Text>
|
||||
@@ -191,12 +193,12 @@ export function SyncConflictPrompt() {
|
||||
) : null}
|
||||
</ScrollView>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={styles.secondaryButton} onPress={dismissConflictPrompt}>
|
||||
<Pressable android_ripple={ripple.bounded} style={styles.secondaryButton} onPress={dismissConflictPrompt}>
|
||||
<Text variant="body" color={colors.textSecondary}>
|
||||
Not now
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
<Pressable android_ripple={ripple.bounded}
|
||||
style={[styles.primaryButton, (!selectedResolution || busy) && styles.disabled]}
|
||||
disabled={!selectedResolution || busy}
|
||||
onPress={confirm}
|
||||
|
||||
Reference in New Issue
Block a user