mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 12:14:47 +02:00
theming + material you
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import Svg, { G, Path } from 'react-native-svg';
|
||||
import { colors } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
|
||||
/**
|
||||
* Astra mark — ported from desktop `astraLogoShared.ts` (same viewBox, paths,
|
||||
@@ -26,11 +26,9 @@ interface AstraLogoProps {
|
||||
includeBackground?: boolean;
|
||||
}
|
||||
|
||||
export function AstraLogo({
|
||||
size = 28,
|
||||
color = colors.logoMain,
|
||||
includeBackground = false,
|
||||
}: AstraLogoProps) {
|
||||
export function AstraLogo({ size = 28, color, includeBackground = false }: AstraLogoProps) {
|
||||
const colors = useColors();
|
||||
const mainFill = color ?? colors.logoMain;
|
||||
return (
|
||||
<Svg width={size} height={size} viewBox={VIEWBOX} fill="none">
|
||||
{includeBackground && (
|
||||
@@ -47,8 +45,8 @@ export function AstraLogo({
|
||||
</G>
|
||||
</G>
|
||||
<G transform={MAIN_TRANSFORM}>
|
||||
<Path d={LEFT_PATH} fill={color} />
|
||||
<Path d={RIGHT_PATH} fill={color} />
|
||||
<Path d={LEFT_PATH} fill={mainFill} />
|
||||
<Path d={RIGHT_PATH} fill={mainFill} />
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { Text } from './Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
import type { Track } from '@/types/audio';
|
||||
|
||||
/** A single mono pill (e.g. "FLAC", "24-BIT", "48.0 kHz"). */
|
||||
export function Badge({ label }: { label: string }) {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<View style={styles.badge}>
|
||||
<Text variant="mono" style={styles.text}>
|
||||
@@ -34,6 +35,7 @@ export function FormatBadges({
|
||||
wrap?: boolean;
|
||||
variant?: 'pill' | 'plain';
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const labels: string[] = [];
|
||||
if (track.format) labels.push(track.format.toUpperCase());
|
||||
if (track.bitDepth) labels.push(`${track.bitDepth}-BIT`);
|
||||
@@ -66,7 +68,7 @@ export function FormatBadges({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
@@ -93,6 +95,6 @@ const styles = StyleSheet.create({
|
||||
fontSize: 10,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default FormatBadges;
|
||||
|
||||
@@ -12,10 +12,10 @@ import { Text } from './Text';
|
||||
import { AstraLogo } from './AstraLogo';
|
||||
import { SpectrumCurve } from './SpectrumCurve';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
import { usePlaybackTargetStore } from '@/stores/playbackTargetStore';
|
||||
@@ -46,6 +46,7 @@ function MiniProgress({
|
||||
duration: number;
|
||||
isPlaying: boolean;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const smoothTime = useSmoothPlaybackTime(currentTime, duration, isPlaying);
|
||||
const progress = duration > 0 ? Math.min(1, smoothTime / duration) : 0;
|
||||
return (
|
||||
@@ -61,6 +62,8 @@ function MiniProgress({
|
||||
* opens the full now-playing screen.
|
||||
*/
|
||||
export function MiniPlayer({ visible = true }: MiniPlayerProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const selectedTarget = usePlaybackTargetStore((s) => s.target);
|
||||
const track = usePlayerStore((s) => s.currentTrack);
|
||||
@@ -205,7 +208,7 @@ export function MiniPlayer({ visible = true }: MiniPlayerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
pill: {
|
||||
height: PILL_HEIGHT,
|
||||
marginHorizontal: spacing.md,
|
||||
@@ -234,7 +237,7 @@ const styles = StyleSheet.create({
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(8, 10, 15, 0.24)',
|
||||
backgroundColor: colors.overlayFaint,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
@@ -279,6 +282,6 @@ const styles = StyleSheet.create({
|
||||
height: 2,
|
||||
backgroundColor: colors.accent,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default MiniPlayer;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { AstraScope, OSCILLOSCOPE_POINTS } from '../../modules/astra-scope';
|
||||
import { useScopeStore } from '@/scope/scopeStore';
|
||||
import { DEFAULT_OSC_GAIN } from '@/scope/oscilloscopeGain';
|
||||
import { colors } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
|
||||
interface OscilloscopeWaveProps {
|
||||
active: boolean;
|
||||
@@ -111,11 +111,13 @@ export function OscilloscopeWave({
|
||||
active,
|
||||
width,
|
||||
height,
|
||||
color = colors.accent,
|
||||
color: colorProp,
|
||||
lineWidth = 2,
|
||||
glow = false,
|
||||
edgeFade: _edgeFade = false,
|
||||
}: OscilloscopeWaveProps) {
|
||||
const themeColors = useColors();
|
||||
const color = colorProp ?? themeColors.accent;
|
||||
const viewRef = useRef<SkiaPictureView | null>(null);
|
||||
const initialPicture = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -2,14 +2,14 @@ import { useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from './Text';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
import { usePlaybackTargetStore, type PlaybackTarget } from '@/stores/playbackTargetStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
@@ -24,6 +24,8 @@ interface PlaybackTargetPickerProps {
|
||||
}
|
||||
|
||||
export function PlaybackTargetPicker({ visible, onClose }: PlaybackTargetPickerProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const selectedTarget = usePlaybackTargetStore((s) => s.target);
|
||||
@@ -114,6 +116,8 @@ function TargetRow({
|
||||
selected: boolean;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.row, pressed && styles.rowPressed]}
|
||||
@@ -141,11 +145,11 @@ function TargetRow({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.58)',
|
||||
backgroundColor: colors.backdrop,
|
||||
},
|
||||
sheet: {
|
||||
borderTopLeftRadius: radius.lg,
|
||||
@@ -190,6 +194,6 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default PlaybackTargetPicker;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { colors } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import type { TrackSourceType } from '@/types/library';
|
||||
|
||||
/**
|
||||
@@ -9,18 +9,19 @@ import type { TrackSourceType } from '@/types/library';
|
||||
export function RemoteSourceBadge({
|
||||
sourceType,
|
||||
size = 12,
|
||||
color = colors.accent,
|
||||
color,
|
||||
}: {
|
||||
sourceType?: TrackSourceType | null;
|
||||
size?: number;
|
||||
color?: string;
|
||||
}) {
|
||||
const colors = useColors();
|
||||
if (!sourceType || sourceType === 'local') return null;
|
||||
return (
|
||||
<Ionicons
|
||||
name="cloud"
|
||||
size={size}
|
||||
color={color}
|
||||
color={color ?? colors.accent}
|
||||
accessibilityLabel={`Streaming from ${sourceType}`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
type ViewProps
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
|
||||
interface ScreenProps extends ViewProps {
|
||||
/** Apply default horizontal padding. */
|
||||
@@ -13,6 +13,7 @@ interface ScreenProps extends ViewProps {
|
||||
|
||||
/** Base screen container: black background + top safe-area inset. */
|
||||
export function Screen({ children, style, padded = true, ...rest }: ScreenProps) {
|
||||
const styles = useStyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
return (
|
||||
<View style={[styles.root, { paddingTop: insets.top }, style]} {...rest}>
|
||||
@@ -21,7 +22,7 @@ export function Screen({ children, style, padded = true, ...rest }: ScreenProps)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
root: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.bgPrimary,
|
||||
@@ -32,6 +33,6 @@ const styles = StyleSheet.create({
|
||||
padded: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default Screen;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
type GestureResponderEvent,
|
||||
type LayoutChangeEvent
|
||||
} from 'react-native';
|
||||
import { Text } from './Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
import { formatDuration } from '@/lib/format';
|
||||
|
||||
const THUMB_SIZE = 12;
|
||||
@@ -30,6 +29,7 @@ const clamp = (fraction: number) => Math.min(1, Math.max(0, fraction));
|
||||
* waveform seek bar port (desktop WaveformSeekBar) replaces the visuals at M3+.
|
||||
*/
|
||||
export function SeekBar({ currentTime, duration, onSeek, trackKey }: SeekBarProps) {
|
||||
const styles = useStyles();
|
||||
const [scrubFraction, setScrubFraction] = useState<number | null>(null);
|
||||
const [barWidth, setBarWidth] = useState(0);
|
||||
// Last released seek. Displayed instead of live progress until playback
|
||||
@@ -129,7 +129,7 @@ export function SeekBar({ currentTime, duration, onSeek, trackKey }: SeekBarProp
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
touchArea: {
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md, // generous touch target around the 4px track
|
||||
@@ -166,6 +166,6 @@ const styles = StyleSheet.create({
|
||||
timeActive: {
|
||||
color: colors.accentText,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default SeekBar;
|
||||
|
||||
@@ -12,10 +12,10 @@ import Animated, {
|
||||
withTiming
|
||||
} from 'react-native-reanimated';
|
||||
import {
|
||||
colors,
|
||||
fonts,
|
||||
radius
|
||||
radius,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { motion } from '@/theme/motion';
|
||||
|
||||
const THUMB_INSET = 3;
|
||||
@@ -37,6 +37,7 @@ interface SegmentedControlProps {
|
||||
* accent via interpolateColor on Animated.Text. Spring-free per theme/motion.
|
||||
*/
|
||||
export function SegmentedControl({ segments, value, onChange }: SegmentedControlProps) {
|
||||
const styles = useStyles();
|
||||
const count = segments.length;
|
||||
const activeIndex = Math.max(
|
||||
0,
|
||||
@@ -86,6 +87,8 @@ function SegmentButton({
|
||||
focused: boolean;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
// 0 = inactive, 1 = active; drives the label colour cross-fade.
|
||||
const progress = useSharedValue(focused ? 1 : 0);
|
||||
|
||||
@@ -93,12 +96,12 @@ function SegmentButton({
|
||||
progress.value = withTiming(focused ? 1 : 0, motion.quick);
|
||||
}, [focused, progress]);
|
||||
|
||||
// Locals so the worklet captures plain strings: a theme switch re-renders,
|
||||
// the captured values change, and Reanimated rebuilds the worklet.
|
||||
const inactiveColor = colors.textSecondary;
|
||||
const activeColor = colors.accentTextStrong;
|
||||
const labelStyle = useAnimatedStyle(() => ({
|
||||
color: interpolateColor(
|
||||
progress.value,
|
||||
[0, 1],
|
||||
[colors.textSecondary, colors.accentTextStrong],
|
||||
),
|
||||
color: interpolateColor(progress.value, [0, 1], [inactiveColor, activeColor]),
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -115,7 +118,7 @@ function SegmentButton({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
track: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.glassBg,
|
||||
@@ -147,6 +150,6 @@ const styles = StyleSheet.create({
|
||||
fontSize: 12,
|
||||
fontFamily: fonts.sans.medium,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default SegmentedControl;
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type SkPicture
|
||||
} from '@shopify/react-native-skia';
|
||||
import { AstraScope, SPECTRUM_BINS } from '../../modules/astra-scope';
|
||||
import { colors } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
|
||||
interface SpectrumCurveProps {
|
||||
/** Normalized magnitudes in [0,1] for static rendering. Live rendering ignores this. */
|
||||
@@ -305,16 +305,19 @@ export function SpectrumCurve({
|
||||
dbMin = DISPLAY_DB_MIN,
|
||||
dbMax = DISPLAY_DB_MAX,
|
||||
tiltDbPerOctave = TILT_DB_PER_OCT,
|
||||
color = colors.accent,
|
||||
color: colorProp,
|
||||
lineWidth = 2,
|
||||
lineOpacity = 1,
|
||||
fillOpacity = 1,
|
||||
glow = false,
|
||||
glowOpacity = 0.18,
|
||||
edgeFade = false,
|
||||
edgeFadeColor = colors.bgPrimary,
|
||||
edgeFadeColor: edgeFadeColorProp,
|
||||
edgeFadeWidth = 28,
|
||||
}: SpectrumCurveProps) {
|
||||
const themeColors = useColors();
|
||||
const color = colorProp ?? themeColors.accent;
|
||||
const edgeFadeColor = edgeFadeColorProp ?? themeColors.bgPrimary;
|
||||
const viewRef = useRef<SkiaPictureView | null>(null);
|
||||
const activePointCount = Math.max(2, Math.floor(width));
|
||||
const resolvedPointCount = pointCount ?? values?.length ?? (active ? activePointCount : DEFAULT_POINTS);
|
||||
|
||||
@@ -16,7 +16,7 @@ import Animated, {
|
||||
useSharedValue,
|
||||
withTiming
|
||||
} from 'react-native-reanimated';
|
||||
import { colors } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { motion } from '@/theme/motion';
|
||||
import { commitHaptic, tickHaptic } from '@/lib/haptics';
|
||||
|
||||
@@ -59,6 +59,7 @@ export function SwipeableRow({
|
||||
enabled = true,
|
||||
children,
|
||||
}: SwipeableRowProps) {
|
||||
const colors = useColors();
|
||||
const tx = useSharedValue(0);
|
||||
const armed = useSharedValue(false);
|
||||
const [rowWidth, setRowWidth] = useState(0);
|
||||
|
||||
@@ -15,11 +15,11 @@ import Animated, {
|
||||
} from 'react-native-reanimated';
|
||||
import { MiniPlayer } from './MiniPlayer';
|
||||
import {
|
||||
colors,
|
||||
fonts,
|
||||
layout,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { motion } from '@/theme/motion';
|
||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
import { usePlaybackTargetStore } from '@/stores/playbackTargetStore';
|
||||
@@ -54,6 +54,7 @@ interface TabBarProps {
|
||||
* logic stays in the layout's `tabBar` callback.
|
||||
*/
|
||||
export function TabBar({ items, onPress }: TabBarProps) {
|
||||
const styles = useStyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const tabs = items.filter((item) => TAB_META[item.name]);
|
||||
const homeFocused = items.some((item) => item.name === 'index' && item.focused);
|
||||
@@ -158,6 +159,8 @@ interface TabButtonProps {
|
||||
* theme/motion.
|
||||
*/
|
||||
function TabButton({ meta, focused, onPress }: TabButtonProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
// 0 = inactive, 1 = active. Drives the accent fill, label colour, and bloom.
|
||||
const progress = useSharedValue(focused ? 1 : 0);
|
||||
// 0 = at rest, 1 = finger down.
|
||||
@@ -171,12 +174,12 @@ function TabButton({ meta, focused, onPress }: TabButtonProps) {
|
||||
transform: [{ scale: 1 - press.value * 0.12 }],
|
||||
}));
|
||||
const accentStyle = useAnimatedStyle(() => ({ opacity: progress.value }));
|
||||
// Locals so the worklet captures plain strings: a theme switch re-renders,
|
||||
// the captured values change, and Reanimated rebuilds the worklet.
|
||||
const inactiveColor = colors.textTertiary;
|
||||
const activeColor = colors.accent;
|
||||
const labelStyle = useAnimatedStyle(() => ({
|
||||
color: interpolateColor(
|
||||
progress.value,
|
||||
[0, 1],
|
||||
[colors.textTertiary, colors.accent],
|
||||
),
|
||||
color: interpolateColor(progress.value, [0, 1], [inactiveColor, activeColor]),
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -204,7 +207,7 @@ function TabButton({ meta, focused, onPress }: TabButtonProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
wrap: {
|
||||
backgroundColor: colors.bgSecondary,
|
||||
},
|
||||
@@ -237,6 +240,6 @@ const styles = StyleSheet.create({
|
||||
fontSize: 10,
|
||||
fontFamily: fonts.sans.regular,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default TabBar;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text as RNText,
|
||||
type TextProps as RNTextProps
|
||||
} from 'react-native';
|
||||
import {
|
||||
colors,
|
||||
fonts,
|
||||
fontSize
|
||||
fontSize,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
|
||||
type Variant = 'title' | 'heading' | 'body' | 'label' | 'caption' | 'mono';
|
||||
|
||||
@@ -55,6 +54,7 @@ function collectText(node: ReactNode): string {
|
||||
|
||||
/** Themed Text — applies Astra fonts/colors. Import this instead of RN's Text. */
|
||||
export function Text({ variant = 'body', color, style, ...rest }: TextProps) {
|
||||
const styles = useStyles();
|
||||
const fallback = NON_LATIN.test(collectText(rest.children));
|
||||
return (
|
||||
<RNText
|
||||
@@ -72,7 +72,7 @@ export function Text({ variant = 'body', color, style, ...rest }: TextProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
title: {
|
||||
fontSize: fontSize.xxl,
|
||||
color: colors.textPrimary,
|
||||
@@ -97,6 +97,6 @@ const styles = StyleSheet.create({
|
||||
fontSize: fontSize.sm,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default Text;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -8,7 +7,8 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from './Text';
|
||||
import { SpectrumCurve } from './SpectrumCurve';
|
||||
import { OscilloscopeWave } from './OscilloscopeWave';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useScopeActive } from '@/scope/scopeStore';
|
||||
|
||||
const CANVAS_HEIGHT = 96;
|
||||
@@ -38,6 +38,8 @@ export function Visualizer({
|
||||
mode: controlledMode,
|
||||
edgeFade = false,
|
||||
}: VisualizerProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [uncontrolledMode, setUncontrolledMode] = useState<Mode>('spectrum');
|
||||
const mode = controlledMode ?? uncontrolledMode;
|
||||
const scopeActive = useScopeActive();
|
||||
@@ -103,7 +105,7 @@ export function Visualizer({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
wrap: {
|
||||
paddingVertical: spacing.xs,
|
||||
},
|
||||
@@ -121,6 +123,6 @@ const styles = StyleSheet.create({
|
||||
letterSpacing: 1.5,
|
||||
fontSize: 10,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default Visualizer;
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
useState
|
||||
} from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
type GestureResponderEvent,
|
||||
type LayoutChangeEvent
|
||||
@@ -18,7 +17,8 @@ import {
|
||||
rect
|
||||
} from '@shopify/react-native-skia';
|
||||
import { Text } from './Text';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { formatDuration } from '@/lib/format';
|
||||
import { downsampleWaveform, getWaveform } from '@/scope/waveform';
|
||||
import { useSmoothPlaybackTime } from '@/audio/useSmoothPlaybackTime';
|
||||
@@ -58,6 +58,8 @@ export function WaveformSeekBar({
|
||||
touchPadding = spacing.md,
|
||||
trackPath,
|
||||
}: WaveformSeekBarProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [scrubFraction, setScrubFraction] = useState<number | null>(null);
|
||||
const [barWidth, setBarWidth] = useState(0);
|
||||
const pendingSeek = usePlayerStore((s) => s.pendingSeek);
|
||||
@@ -199,7 +201,7 @@ export function WaveformSeekBar({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
touchArea: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@@ -215,6 +217,6 @@ const styles = StyleSheet.create({
|
||||
timeActive: {
|
||||
color: colors.accentText,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default WaveformSeekBar;
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import type { EQBand } from '@/types/audio';
|
||||
import {
|
||||
EQ_MAX_FREQUENCY,
|
||||
@@ -41,6 +41,8 @@ export type EQEditableValue = 'frequency' | 'gain' | 'Q';
|
||||
|
||||
/** "Band N" + type dropdown + On toggle + Frequency / Gain / Q sliders. */
|
||||
export function BandDetailPanel({ band, bandNumber, onUpdate, onEditType, onEditValue }: BandDetailPanelProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
if (!band) {
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
@@ -108,7 +110,7 @@ export function BandDetailPanel({ band, bandNumber, onUpdate, onEditType, onEdit
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
card: {
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
@@ -140,6 +142,6 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default BandDetailPanel;
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import type { EQBand } from '@/types/audio';
|
||||
import {
|
||||
formatFreq,
|
||||
@@ -27,6 +27,8 @@ 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 colors = useColors();
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal
|
||||
@@ -46,7 +48,7 @@ export function BandStrip({ bands, activeBandId, canAdd, onSelect, onAdd }: Band
|
||||
</Text>
|
||||
<Text
|
||||
variant="label"
|
||||
style={[styles.gain, { color: band.enabled ? gainColor(band.gain) : colors.textTertiary }]}
|
||||
style={[styles.gain, { color: band.enabled ? gainColor(band.gain, colors) : colors.textTertiary }]}
|
||||
>
|
||||
{formatGain(band.gain)}
|
||||
</Text>
|
||||
@@ -62,7 +64,7 @@ export function BandStrip({ bands, activeBandId, canAdd, onSelect, onAdd }: Band
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
content: {
|
||||
gap: spacing.sm,
|
||||
paddingVertical: spacing.xs,
|
||||
@@ -94,6 +96,6 @@ const styles = StyleSheet.create({
|
||||
gain: {
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default BandStrip;
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@shopify/react-native-skia';
|
||||
import { Text } from '@/components/Text';
|
||||
import { SpectrumCurve } from '@/components/SpectrumCurve';
|
||||
import { colors } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import type { EQBand } from '@/types/audio';
|
||||
import {
|
||||
FREQ_TICKS,
|
||||
@@ -59,6 +59,8 @@ export function EQGraph({
|
||||
onSelectBand,
|
||||
onChangeBand,
|
||||
}: EQGraphProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [size, setSize] = useStableSize();
|
||||
const width = size.width;
|
||||
const height = size.height;
|
||||
@@ -302,7 +304,7 @@ function useStableSize(): [
|
||||
return [size, set];
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
borderRadius: 16,
|
||||
@@ -329,6 +331,6 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
color: colors.textTertiary,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default EQGraph;
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
} from 'react-native';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
|
||||
const THUMB = 16;
|
||||
|
||||
@@ -42,6 +42,7 @@ export function EQSlider({
|
||||
disabled,
|
||||
onValuePress,
|
||||
}: EQSliderProps) {
|
||||
const styles = useStyles();
|
||||
const [width, setWidth] = useState(0);
|
||||
const [active, setActive] = useState(false);
|
||||
const widthRef = useRef(0);
|
||||
@@ -136,7 +137,7 @@ export function EQSlider({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -195,6 +196,6 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'right',
|
||||
color: colors.textPrimary,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default EQSlider;
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
fonts,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { EqSheet } from './EqSheet';
|
||||
|
||||
interface EQValueEditSheetProps {
|
||||
@@ -39,6 +39,8 @@ export function EQValueEditSheet({
|
||||
onApply,
|
||||
onClose,
|
||||
}: EQValueEditSheetProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const trimmed = value.trim();
|
||||
const parsed = trimmed.length > 0 ? parseValue(trimmed) : null;
|
||||
@@ -97,7 +99,7 @@ export function EQValueEditSheet({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
title: {
|
||||
marginTop: spacing.xs,
|
||||
marginBottom: spacing.md,
|
||||
@@ -156,6 +158,6 @@ const styles = StyleSheet.create({
|
||||
applyDisabled: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default EQValueEditSheet;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Text } from '@/components/Text';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { EQ_MAX_GAIN_DB, EQ_MIN_GAIN_DB } from '@/audio/eq';
|
||||
import { GRAPHIC_BANDS } from '@/audio/graphicEq';
|
||||
import { GraphicResponseCurve } from './GraphicResponseCurve';
|
||||
@@ -25,6 +26,8 @@ interface GraphicEQPanelProps {
|
||||
* curve's evenly spaced band positions.
|
||||
*/
|
||||
export function GraphicEQPanel({ gains, enabled, onChangeGain }: GraphicEQPanelProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.metaRow}>
|
||||
@@ -32,7 +35,7 @@ export function GraphicEQPanel({ gains, enabled, onChangeGain }: GraphicEQPanelP
|
||||
<Text
|
||||
key={def.key}
|
||||
variant="mono"
|
||||
style={[styles.value, { color: gainColor(gains[i] ?? 0) }]}
|
||||
style={[styles.value, { color: gainColor(gains[i] ?? 0, colors) }]}
|
||||
>
|
||||
{formatGain(gains[i] ?? 0)}
|
||||
</Text>
|
||||
@@ -71,7 +74,7 @@ export function GraphicEQPanel({ gains, enabled, onChangeGain }: GraphicEQPanelP
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -100,6 +103,6 @@ const styles = StyleSheet.create({
|
||||
caption: {
|
||||
color: colors.textTertiary,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default GraphicEQPanel;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Skia,
|
||||
type SkPath
|
||||
} from '@shopify/react-native-skia';
|
||||
import { colors } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import type { EQBand } from '@/types/audio';
|
||||
import {
|
||||
EQ_MAX_FREQUENCY,
|
||||
@@ -39,6 +39,7 @@ interface GraphicResponseCurveProps {
|
||||
* the chrome).
|
||||
*/
|
||||
export function GraphicResponseCurve({ gains, enabled }: GraphicResponseCurveProps) {
|
||||
const colors = useColors();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
const width = size.width;
|
||||
const height = size.height;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Pressable, StyleSheet } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import type { EQPreset } from '@/types/audio';
|
||||
import {
|
||||
EqSheet,
|
||||
@@ -35,6 +36,7 @@ export function PresetSheet({
|
||||
onSaveNew,
|
||||
onClose,
|
||||
}: PresetSheetProps) {
|
||||
const colors = useColors();
|
||||
const builtIn = presets.filter((p) => !p.isCustom);
|
||||
const custom = presets.filter((p) => p.isCustom);
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
fonts,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { EqSheet } from './EqSheet';
|
||||
|
||||
interface SavePresetSheetProps {
|
||||
@@ -22,6 +22,8 @@ interface SavePresetSheetProps {
|
||||
|
||||
/** Name + save a custom preset from the current bands/preamp. */
|
||||
export function SavePresetSheet({ defaultName, onSave, onClose }: SavePresetSheetProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [name, setName] = useState(defaultName);
|
||||
const trimmed = name.trim();
|
||||
|
||||
@@ -70,7 +72,7 @@ export function SavePresetSheet({ defaultName, onSave, onClose }: SavePresetShee
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
title: {
|
||||
marginTop: spacing.xs,
|
||||
marginBottom: spacing.md,
|
||||
@@ -109,6 +111,6 @@ const styles = StyleSheet.create({
|
||||
saveDisabled: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default SavePresetSheet;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
type GestureResponderEvent,
|
||||
type LayoutChangeEvent
|
||||
} from 'react-native';
|
||||
import { colors, radius } from '@/theme';
|
||||
import { radius } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
|
||||
// Vertical fader cap (mixer-style): tall capsule with a horizontal grip line
|
||||
// marking the exact value position. Half its height overshoots the rail at the
|
||||
@@ -37,6 +37,8 @@ const clamp01 = (f: number) => Math.min(1, Math.max(0, f));
|
||||
* same code as the readouts).
|
||||
*/
|
||||
export function VerticalEQSlider({ label, value, min, max, onChange }: VerticalEQSliderProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [height, setHeight] = useState(0);
|
||||
const [active, setActive] = useState(false);
|
||||
const heightRef = useRef(0);
|
||||
@@ -106,7 +108,7 @@ export function VerticalEQSlider({ label, value, min, max, onChange }: VerticalE
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
touch: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
@@ -151,6 +153,6 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 1,
|
||||
backgroundColor: colors.bgSecondary,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default VerticalEQSlider;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Shared EQ value formatting for the band strip + detail panel.
|
||||
|
||||
import { colors } from '@/theme';
|
||||
import type { Palette } from '@/theme/palettes';
|
||||
import type { EQBandType } from '@/types/audio';
|
||||
|
||||
export function formatFreq(hz: number): string {
|
||||
@@ -25,7 +25,7 @@ export function formatGain(db: number): string {
|
||||
return `${db > 0 ? '+' : ''}${db.toFixed(1)}`;
|
||||
}
|
||||
|
||||
export function gainColor(db: number): string {
|
||||
export function gainColor(db: number, colors: Palette): string {
|
||||
if (db > 0.05) return colors.accentText;
|
||||
if (db < -0.05) return colors.warning;
|
||||
return colors.textTertiary;
|
||||
|
||||
@@ -7,14 +7,15 @@ import { Image } from 'expo-image';
|
||||
import { Text } from '@/components/Text';
|
||||
import { AstraLogo } from '@/components/AstraLogo';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
import { albumArtworkSource } from '@/library/artwork';
|
||||
import type { Album } from '@/types/library';
|
||||
|
||||
export function AlbumGridItem({ album, onPress }: { album: Album; onPress: () => void }) {
|
||||
const styles = useStyles();
|
||||
const artUri = albumArtworkSource(album);
|
||||
return (
|
||||
<Pressable style={styles.item} onPress={onPress} accessibilityRole="button">
|
||||
@@ -41,7 +42,7 @@ export function AlbumGridItem({ album, onPress }: { album: Album; onPress: () =>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
item: {
|
||||
flex: 1,
|
||||
marginBottom: spacing.lg,
|
||||
@@ -64,4 +65,4 @@ const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -7,15 +7,17 @@ import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { albumArtworkSource } from '@/library/artwork';
|
||||
import type { Album } from '@/types/library';
|
||||
|
||||
/** Compact album list row (search results) — the grid uses AlbumGridItem. */
|
||||
export function AlbumRow({ album, onPress }: { album: Album; onPress: () => void }) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const artUri = albumArtworkSource(album);
|
||||
return (
|
||||
<Pressable style={styles.row} onPress={onPress} accessibilityRole="button">
|
||||
@@ -43,7 +45,7 @@ export function AlbumRow({ album, onPress }: { album: Album; onPress: () => void
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -71,4 +73,4 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -4,7 +4,9 @@ import { StyleSheet, View, type LayoutChangeEvent } from 'react-native';
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
|
||||
import { Text } from '@/components/Text';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
import { rgbaFromHex } from '@/theme/colorUtils';
|
||||
import { tickHaptic } from '@/lib/haptics';
|
||||
import { usePullSearchGestureRef } from '@/components/search/PullSearchGesture';
|
||||
import { RAIL_LETTERS } from '@/lib/letterIndex';
|
||||
@@ -29,6 +31,7 @@ interface AlphabetRailProps {
|
||||
* scroll-top never arms the search indicator.
|
||||
*/
|
||||
export function AlphabetRail({ activeLetters, onJumpToLetter }: AlphabetRailProps) {
|
||||
const styles = useStyles();
|
||||
const pullSearchRef = usePullSearchGestureRef();
|
||||
const [scrubLetter, setScrubLetter] = useState<string | null>(null);
|
||||
const lastLetter = useSharedValue('');
|
||||
@@ -128,7 +131,7 @@ export function AlphabetRail({ activeLetters, onJumpToLetter }: AlphabetRailProp
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
wrap: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
@@ -144,7 +147,7 @@ const styles = StyleSheet.create({
|
||||
width: 16,
|
||||
paddingVertical: RAIL_PAD,
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(8, 10, 15, 0.35)',
|
||||
backgroundColor: rgbaFromHex(colors.bgPrimary, 0.35),
|
||||
borderRadius: radius.pill,
|
||||
},
|
||||
cell: {
|
||||
@@ -189,4 +192,4 @@ const styles = StyleSheet.create({
|
||||
lineHeight: 30,
|
||||
color: colors.accentTextStrong,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -7,15 +7,17 @@ import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
spacing,
|
||||
radius
|
||||
radius,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { artworkUri } from '@/library/artwork';
|
||||
import type { Artist } from '@/types/library';
|
||||
|
||||
/** 2-column grid cell: square art (2x2 album mosaic when available) + counts, matching the album grid. */
|
||||
export function ArtistGridItem({ artist, onPress }: { artist: Artist; onPress: () => void }) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const useMosaic = artist.artwork_hashes.length >= 4;
|
||||
const hashes = useMosaic ? artist.artwork_hashes.slice(0, 4) : artist.artwork_hashes.slice(0, 1);
|
||||
|
||||
@@ -58,7 +60,7 @@ export function ArtistGridItem({ artist, onPress }: { artist: Artist; onPress: (
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
item: {
|
||||
flex: 1,
|
||||
marginBottom: spacing.lg,
|
||||
@@ -87,4 +89,4 @@ const styles = StyleSheet.create({
|
||||
name: {
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -7,14 +7,16 @@ import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
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();
|
||||
return (
|
||||
<Pressable style={styles.row} onPress={onPress} accessibilityRole="button">
|
||||
<View style={styles.art}>
|
||||
@@ -41,7 +43,7 @@ export function ArtistRow({ artist, onPress }: { artist: Artist; onPress: () =>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -66,4 +68,4 @@ const styles = StyleSheet.create({
|
||||
meta: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -30,10 +30,10 @@ import {
|
||||
} from '@shopify/react-native-skia';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
|
||||
// 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
|
||||
@@ -97,6 +97,8 @@ export function useDetailCollapse() {
|
||||
}
|
||||
|
||||
function BottomFade() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [width, setWidth] = useState(0);
|
||||
return (
|
||||
<View style={styles.fade} onLayout={(e) => setWidth(e.nativeEvent.layout.width)}>
|
||||
@@ -152,6 +154,8 @@ export function CollapsingHeader({
|
||||
expandedHeight: number;
|
||||
onHeroBlockLayout: (e: LayoutChangeEvent) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width: W } = useWindowDimensions();
|
||||
const dist = expandedHeight - BAR_H;
|
||||
@@ -330,7 +334,7 @@ export function CollapsingHeader({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
@@ -481,4 +485,4 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Pressable
|
||||
} from 'react-native';
|
||||
@@ -7,12 +6,14 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
|
||||
export function EmptyLibrary() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
@@ -34,7 +35,7 @@ export function EmptyLibrary() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
empty: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
@@ -63,4 +64,4 @@ const styles = StyleSheet.create({
|
||||
color: colors.bgPrimary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -32,10 +32,10 @@ import {
|
||||
} from '@/library/folderTree';
|
||||
import { formatDuration } from '@/lib/format';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import type { DbTrack } from '@/types/library';
|
||||
@@ -58,6 +58,8 @@ function FolderRow({
|
||||
onShuffle: (node: FolderTreeNode) => void;
|
||||
onOpenActions: (node: FolderTreeNode) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const { node, depth, isExpanded } = row;
|
||||
|
||||
const play = (event: GestureResponderEvent) => {
|
||||
@@ -132,6 +134,8 @@ function FolderTrackRow({
|
||||
active: boolean;
|
||||
onOpenActions: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const index = row.folderTracks.findIndex((track) => track.path === row.track.path);
|
||||
|
||||
const playFolderTrack = () => {
|
||||
@@ -180,6 +184,8 @@ function FolderTrackRow({
|
||||
}
|
||||
|
||||
export function FoldersView({ onScroll, scrollEventThrottle }: FoldersViewProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const folders = useLibraryStore((s) => s.folders);
|
||||
const tracks = useLibraryStore((s) => s.tracks);
|
||||
const currentPath = usePlayerStore((s) => s.currentTrack?.path);
|
||||
@@ -306,7 +312,7 @@ export function FoldersView({ onScroll, scrollEventThrottle }: FoldersViewProps)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
listContent: {
|
||||
paddingBottom: spacing.xxl,
|
||||
},
|
||||
@@ -398,4 +404,4 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
maxWidth: 260,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -7,10 +7,10 @@ import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { artworkUri } from '@/library/artwork';
|
||||
|
||||
export function PlaylistRow({
|
||||
@@ -37,6 +37,8 @@ export function PlaylistRow({
|
||||
onPress: () => void;
|
||||
onLongPress?: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.row}
|
||||
@@ -84,7 +86,7 @@ export function PlaylistRow({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -120,4 +122,4 @@ const styles = StyleSheet.create({
|
||||
title: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -20,10 +20,10 @@ import { TextPromptModal } from '@/components/sheets/TextPromptModal';
|
||||
import { PlaylistRow } from '@/components/library/PlaylistRow';
|
||||
import { PullSearchScrollView } from '@/components/search/PullSearchGesture';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||
import type { Playlist } from '@/types/playlist';
|
||||
|
||||
@@ -46,6 +46,8 @@ export function PlaylistsView({
|
||||
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
scrollEventThrottle?: number;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const playlists = usePlaylistStore((s) => s.playlists);
|
||||
const favoriteCount = usePlaylistStore((s) => s.favoriteTracks.length);
|
||||
@@ -273,7 +275,7 @@ export function PlaylistsView({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -308,4 +310,4 @@ const styles = StyleSheet.create({
|
||||
borderRadius: radius.pill,
|
||||
backgroundColor: colors.accentGlow,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import { Text } from '@/components/Text';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
|
||||
/** Thin accent bar + caption shown under the library header while scanning. */
|
||||
export function ScanProgress() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const isScanning = useLibraryStore((s) => s.isScanning);
|
||||
const progress = useLibraryStore((s) => s.scanProgress);
|
||||
|
||||
@@ -42,7 +45,7 @@ export function ScanProgress() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
container: {
|
||||
gap: spacing.xs,
|
||||
marginBottom: spacing.md,
|
||||
@@ -61,4 +64,4 @@ const styles = StyleSheet.create({
|
||||
width: '100%',
|
||||
opacity: 0.35,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
|
||||
interface SelectionActionBarProps {
|
||||
count: number;
|
||||
@@ -21,6 +22,7 @@ export function SelectionActionBar({
|
||||
onAddToQueue,
|
||||
onAddToPlaylist,
|
||||
}: SelectionActionBarProps) {
|
||||
const styles = useStyles();
|
||||
const disabled = count === 0;
|
||||
return (
|
||||
<View style={styles.bar}>
|
||||
@@ -62,6 +64,8 @@ function BarButton({
|
||||
disabled: boolean;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
@@ -82,7 +86,7 @@ function BarButton({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
bar: {
|
||||
flexDirection: 'row',
|
||||
borderTopColor: colors.glassBorder,
|
||||
@@ -106,4 +110,4 @@ const styles = StyleSheet.create({
|
||||
label: {
|
||||
color: colors.accent,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -13,10 +13,10 @@ import { FormatBadges } from '@/components/FormatBadge';
|
||||
import { RemoteSourceBadge } from '@/components/RemoteSourceBadge';
|
||||
import { SwipeableRow } from '@/components/SwipeableRow';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { formatDuration } from '@/lib/format';
|
||||
import { trackArtworkThumbSource } from '@/library/artwork';
|
||||
import { dbTrackToTrack } from '@/library/trackAdapter';
|
||||
@@ -58,6 +58,8 @@ export function TrackRow({
|
||||
selected?: boolean;
|
||||
onToggleSelect?: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
// 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;
|
||||
@@ -174,7 +176,7 @@ export function TrackRow({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -258,4 +260,4 @@ const styles = StyleSheet.create({
|
||||
actionsButtonPressed: {
|
||||
backgroundColor: colors.glassBg,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -39,10 +39,10 @@ import { Text } from '@/components/Text';
|
||||
import { AstraLogo } from '@/components/AstraLogo';
|
||||
import { SwipeableRow } from '@/components/SwipeableRow';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { motion } from '@/theme/motion';
|
||||
import { dragArmHaptic, tickHaptic } from '@/lib/haptics';
|
||||
import { useQueueStore } from '@/stores/queueStore';
|
||||
@@ -139,6 +139,8 @@ interface QueueTrayProps {
|
||||
}
|
||||
|
||||
export function QueueTray({ onClose }: QueueTrayProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { height: windowHeight } = useWindowDimensions();
|
||||
const snapPoints = useMemo(() => ['58%', '100%'], []);
|
||||
@@ -530,7 +532,7 @@ export function QueueTray({ onClose }: QueueTrayProps) {
|
||||
)}
|
||||
</View>
|
||||
),
|
||||
[isLoadingQueue]
|
||||
[isLoadingQueue, colors, styles]
|
||||
);
|
||||
|
||||
const selectedCount = visibleSelectedKeys.size;
|
||||
@@ -645,6 +647,7 @@ export function QueueTray({ onClose }: QueueTrayProps) {
|
||||
}
|
||||
|
||||
const Artwork = memo(function Artwork({ uri, title }: { uri?: string; title?: string }) {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<View style={styles.art}>
|
||||
{uri ? (
|
||||
@@ -711,6 +714,8 @@ const QueueRow = memo(function QueueRow({
|
||||
onRemoveIndex,
|
||||
onToggleSelectKey,
|
||||
}: QueueRowProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const entryKey = entry.key;
|
||||
const title = trackTitle(entry.track);
|
||||
const artist = trackArtist(entry.track);
|
||||
@@ -791,13 +796,18 @@ const QueueRow = memo(function QueueRow({
|
||||
};
|
||||
});
|
||||
|
||||
// Locals so the worklet captures plain strings: a theme switch re-renders,
|
||||
// the captured values change, and Reanimated rebuilds the worklet.
|
||||
const dragSurface = colors.bgTertiary;
|
||||
const selectedSurface = colors.glassHighlight;
|
||||
const restSurface = colors.bgSecondary;
|
||||
const rowSurfaceStyle = useAnimatedStyle(() => ({
|
||||
backgroundColor:
|
||||
dActive.value && dKey.value === entryKey
|
||||
? colors.bgTertiary
|
||||
? dragSurface
|
||||
: selected
|
||||
? colors.glassHighlight
|
||||
: colors.bgSecondary,
|
||||
? selectedSurface
|
||||
: restSurface,
|
||||
}));
|
||||
|
||||
const rowContent = (
|
||||
@@ -894,7 +904,7 @@ const QueueRow = memo(function QueueRow({
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
sheetBg: {
|
||||
backgroundColor: colors.bgSecondary,
|
||||
borderTopLeftRadius: radius.lg,
|
||||
@@ -1058,6 +1068,6 @@ const styles = StyleSheet.create({
|
||||
actionTextDestructive: {
|
||||
color: colors.warning,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default QueueTray;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// this app's screen setups (see queue-tray-sheet gotcha).
|
||||
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Pressable, StyleSheet, View } from 'react-native';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import BottomSheet, {
|
||||
BottomSheetBackdrop,
|
||||
@@ -15,7 +15,8 @@ import BottomSheet, {
|
||||
} from '@gorhom/bottom-sheet';
|
||||
import { FlashList, type ListRenderItemInfo } from '@shopify/flash-list';
|
||||
import { Text } from '@/components/Text';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { formatDuration } from '@/lib/format';
|
||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
import type { DesktopRemoteQueueItem } from '@/types/desktopRemote';
|
||||
@@ -25,6 +26,8 @@ interface RemoteQueueSheetProps {
|
||||
}
|
||||
|
||||
export function RemoteQueueSheet({ onClose }: RemoteQueueSheetProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const queue = useDesktopRemoteStore((s) => s.queue);
|
||||
const snapPoints = useMemo(() => ['58%', '100%'], []);
|
||||
const renderFlashListScrollComponent = useBottomSheetScrollableCreator();
|
||||
@@ -92,7 +95,7 @@ export function RemoteQueueSheet({ onClose }: RemoteQueueSheetProps) {
|
||||
) : null}
|
||||
</Pressable>
|
||||
),
|
||||
[playItem]
|
||||
[playItem, colors, styles]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -131,7 +134,7 @@ export function RemoteQueueSheet({ onClose }: RemoteQueueSheetProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
sheetBg: {
|
||||
backgroundColor: colors.bgSecondary,
|
||||
borderRadius: radius.lg,
|
||||
@@ -170,4 +173,4 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.xl,
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -33,10 +33,10 @@ import {
|
||||
} from 'react-native-reanimated';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { commitHaptic, tickHaptic } from '@/lib/haptics';
|
||||
|
||||
const OPEN_THRESHOLD = 76;
|
||||
@@ -133,6 +133,8 @@ export function PullSearchGesture({
|
||||
atTop: boolean;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [pull, setPull] = useState(0);
|
||||
const [armed, setArmed] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
@@ -269,7 +271,7 @@ export function PullSearchGesture({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
root: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -293,4 +295,4 @@ const styles = StyleSheet.create({
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
elevation: 10,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -22,12 +22,13 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Text } from '@/components/Text';
|
||||
import { AstraLogo } from '@/components/AstraLogo';
|
||||
import {
|
||||
colors,
|
||||
fonts,
|
||||
fontSize,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { rgbaFromHex } from '@/theme/colorUtils';
|
||||
import { enqueueTop, playTracks } from '@/audio/playbackController';
|
||||
import { dbTrackToTrack } from '@/library/trackAdapter';
|
||||
import {
|
||||
@@ -336,6 +337,7 @@ function resultIcon(result: SearchResult): IconName {
|
||||
}
|
||||
|
||||
function HighlightedLabel({ text, query }: { text: string; query: string }) {
|
||||
const styles = useStyles();
|
||||
const normalizedText = text.toLocaleLowerCase();
|
||||
const normalizedQuery = query.toLocaleLowerCase().trim();
|
||||
|
||||
@@ -390,6 +392,8 @@ function HighlightedLabel({ text, query }: { text: string; query: string }) {
|
||||
}
|
||||
|
||||
function ResultThumb({ result }: { result: SearchResult }) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const uri =
|
||||
result.kind === 'track'
|
||||
? trackArtworkThumbSource(result.track)
|
||||
@@ -431,6 +435,8 @@ function ResultRow({
|
||||
onPress: () => void;
|
||||
onQueueTrack: (track: DbTrack) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const isTrack = result.kind === 'track';
|
||||
const isShowMode = result.kind === 'show-all' || result.kind === 'show-top';
|
||||
|
||||
@@ -481,6 +487,8 @@ function QuickSearchPanel({
|
||||
initialQuery: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { height } = useWindowDimensions();
|
||||
@@ -978,6 +986,7 @@ function QuickSearchPanel({
|
||||
}
|
||||
|
||||
export function QuickSearchOverlay() {
|
||||
const styles = useStyles();
|
||||
const isOpen = useSearchStore((s) => s.isQuickSearchOpen);
|
||||
const initialQuery = useSearchStore((s) => s.initialQuery);
|
||||
const openVersion = useSearchStore((s) => s.openVersion);
|
||||
@@ -1006,11 +1015,11 @@ export function QuickSearchOverlay() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
modalRoot: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(2, 4, 8, 0.66)',
|
||||
backgroundColor: colors.backdrop,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
panel: {
|
||||
@@ -1077,7 +1086,7 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
resultRowActive: {
|
||||
backgroundColor: 'rgba(91, 138, 255, 0.12)',
|
||||
backgroundColor: rgbaFromHex(colors.accent, 0.12),
|
||||
},
|
||||
showModeRow: {
|
||||
marginTop: spacing.sm,
|
||||
@@ -1119,7 +1128,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
highlight: {
|
||||
color: colors.accentTextStrong,
|
||||
backgroundColor: 'rgba(91, 138, 255, 0.22)',
|
||||
backgroundColor: rgbaFromHex(colors.accent, 0.22),
|
||||
borderRadius: 2,
|
||||
},
|
||||
queueButton: {
|
||||
@@ -1143,4 +1152,4 @@ const styles = StyleSheet.create({
|
||||
emptyText: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Pressable, StyleSheet, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import { spacing } from '@/theme';
|
||||
import { ACCENTS, ACCENT_IDS, type AccentId } from '@/theme/accents';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
|
||||
const SWATCH_SIZE = 36;
|
||||
|
||||
interface AccentSwatchRowProps {
|
||||
value: AccentId;
|
||||
onChange: (id: AccentId) => void;
|
||||
}
|
||||
|
||||
/** Circular accent swatches; the selected one gets a ring + checkmark. */
|
||||
export function AccentSwatchRow({ value, onChange }: AccentSwatchRowProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.wrap}>
|
||||
<View style={styles.row}>
|
||||
{ACCENT_IDS.map((id) => {
|
||||
const selected = id === value;
|
||||
return (
|
||||
<Pressable
|
||||
key={id}
|
||||
onPress={() => onChange(id)}
|
||||
accessibilityRole="radio"
|
||||
accessibilityState={{ selected }}
|
||||
accessibilityLabel={`${ACCENTS[id].label} accent`}
|
||||
style={[
|
||||
styles.swatch,
|
||||
{ backgroundColor: ACCENTS[id].base },
|
||||
selected && styles.swatchSelected,
|
||||
]}
|
||||
hitSlop={4}
|
||||
>
|
||||
{selected ? (
|
||||
<Ionicons name="checkmark" size={18} color={colors.bgPrimary} />
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
<Text variant="caption" color={colors.textSecondary}>
|
||||
Accent · {ACCENTS[value].label}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
wrap: {
|
||||
gap: spacing.sm,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.md,
|
||||
},
|
||||
swatch: {
|
||||
width: SWATCH_SIZE,
|
||||
height: SWATCH_SIZE,
|
||||
borderRadius: SWATCH_SIZE / 2,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
},
|
||||
swatchSelected: {
|
||||
borderWidth: 2,
|
||||
borderColor: colors.textPrimary,
|
||||
},
|
||||
}));
|
||||
@@ -8,10 +8,10 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
|
||||
export interface ActionSheetItem {
|
||||
key: string;
|
||||
@@ -37,6 +37,8 @@ export function ActionSheet({
|
||||
items: ActionSheetItem[];
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
@@ -88,10 +90,10 @@ export function ActionSheet({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.55)',
|
||||
backgroundColor: colors.backdrop,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
card: {
|
||||
@@ -126,4 +128,4 @@ const styles = StyleSheet.create({
|
||||
itemLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, type ReactNode } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
View
|
||||
} from 'react-native';
|
||||
@@ -13,12 +12,13 @@ import BottomSheet, {
|
||||
} from '@gorhom/bottom-sheet';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
|
||||
export function AppSheet({ onClose, children }: { onClose: () => void; children: ReactNode }) {
|
||||
const styles = useStyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
@@ -51,6 +51,7 @@ export function AppSheet({ onClose, children }: { onClose: () => void; children:
|
||||
}
|
||||
|
||||
export function AppSheetSection({ label }: { label: string }) {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<Text variant="caption" style={styles.section}>
|
||||
{label}
|
||||
@@ -59,6 +60,8 @@ export function AppSheetSection({ label }: { label: string }) {
|
||||
}
|
||||
|
||||
export function AppSheetTitle({ title, subtitle }: { title: string; subtitle?: string }) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.titleBlock}>
|
||||
<Text variant="heading" numberOfLines={1} style={styles.title}>
|
||||
@@ -90,6 +93,8 @@ export function AppSheetItem({
|
||||
onPress,
|
||||
trailing,
|
||||
}: AppSheetItemProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const tint = destructive ? colors.warning : selected ? colors.accentTextStrong : colors.textPrimary;
|
||||
|
||||
return (
|
||||
@@ -112,7 +117,7 @@ export function AppSheetItem({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
sheetBg: {
|
||||
backgroundColor: colors.bgSecondary,
|
||||
borderTopLeftRadius: radius.lg,
|
||||
@@ -157,6 +162,6 @@ const styles = StyleSheet.create({
|
||||
itemLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
export default AppSheet;
|
||||
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
AppSheetTitle
|
||||
} from '@/components/sheets/AppSheet';
|
||||
import {
|
||||
colors,
|
||||
fonts,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||
import type { DbTrack } from '@/types/library';
|
||||
|
||||
@@ -40,6 +40,8 @@ export function PlaylistPickerSheet({
|
||||
onBackToMenu,
|
||||
onAdded,
|
||||
}: PlaylistPickerSheetProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [step, setStep] = useState<'pick' | 'create'>('pick');
|
||||
const [playlistName, setPlaylistName] = useState('');
|
||||
const playlists = usePlaylistStore((s) => s.playlists);
|
||||
@@ -123,7 +125,7 @@ export function PlaylistPickerSheet({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
empty: {
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
@@ -161,4 +163,4 @@ const styles = StyleSheet.create({
|
||||
createDisabled: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
} from 'react-native';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
fonts,
|
||||
fontSize,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
|
||||
interface TextPromptModalProps {
|
||||
visible: boolean;
|
||||
@@ -40,6 +40,8 @@ function TextPromptModalInner({
|
||||
onSubmit,
|
||||
onClose,
|
||||
}: TextPromptModalProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const trimmed = value.trim();
|
||||
|
||||
@@ -89,10 +91,10 @@ function TextPromptModalInner({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.55)',
|
||||
backgroundColor: colors.backdrop,
|
||||
justifyContent: 'center',
|
||||
padding: spacing.xl,
|
||||
},
|
||||
@@ -130,4 +132,4 @@ const styles = StyleSheet.create({
|
||||
actionDisabled: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Text } from '@/components/Text';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { formatRelativeTime } from '@/lib/format';
|
||||
import {
|
||||
buildSyncPlaylistEntryDiff,
|
||||
@@ -77,6 +78,8 @@ function TrackDiffRow({
|
||||
side: 'desktop' | 'phone';
|
||||
previewResolution: DesktopSyncConflictResolution | null;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const subtitle = [row.artist, row.album].filter((part) => part.trim().length > 0).join(' · ');
|
||||
const previewLabel = previewStatusLabel(row, side, previewResolution);
|
||||
const moveLabel = row.status === 'moved' && !previewLabel ? moveStatusLabel(row, side) : null;
|
||||
@@ -125,6 +128,8 @@ function SideTrackList({
|
||||
previewResolution: DesktopSyncConflictResolution | null;
|
||||
maxRows: number;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const rows = [...sideOnlyRows, ...movedRows].slice(0, maxRows);
|
||||
const hiddenCount = Math.max(0, sideOnlyRows.length + movedRows.length - rows.length);
|
||||
const sideName = side === 'desktop' ? 'desktop' : 'phone';
|
||||
@@ -177,6 +182,8 @@ export function SyncConflictDetails({
|
||||
maxRows?: number;
|
||||
previewResolution?: DesktopSyncConflictResolution | null;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const desktop = syncPlaylistToSnapshot(conflict.remote);
|
||||
const phone = syncPlaylistToSnapshot(conflict.local);
|
||||
const isNormal = desktop.kind === 'normal' && phone.kind === 'normal';
|
||||
@@ -266,7 +273,7 @@ export function SyncConflictDetails({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
container: {
|
||||
gap: spacing.sm,
|
||||
},
|
||||
@@ -344,4 +351,4 @@ const styles = StyleSheet.create({
|
||||
padding: spacing.sm,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -11,7 +11,8 @@ import { Modal, Pressable, ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router, usePathname } from 'expo-router';
|
||||
import { Text } from '@/components/Text';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { formatRelativeTime } from '@/lib/format';
|
||||
import {
|
||||
buildSyncConflictResolutionPreview,
|
||||
@@ -62,6 +63,8 @@ function diffLine(desktop: SyncPlaylistSnapshot, phone: SyncPlaylistSnapshot): s
|
||||
}
|
||||
|
||||
export function SyncConflictPrompt() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const conflicts = useDesktopSyncStore((s) => s.conflicts);
|
||||
const status = useDesktopSyncStore((s) => s.status);
|
||||
const promptVisible = useDesktopSyncStore((s) => s.conflictPromptVisible);
|
||||
@@ -209,10 +212,10 @@ export function SyncConflictPrompt() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.55)',
|
||||
backgroundColor: colors.backdrop,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: spacing.xl,
|
||||
@@ -311,4 +314,4 @@ const styles = StyleSheet.create({
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user