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