theming + material you

This commit is contained in:
Boof2015
2026-07-06 20:38:42 -04:00
parent e1f99f6d61
commit efd2ebc91a
87 changed files with 1528 additions and 327 deletions
+6 -8
View File
@@ -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>
);
+6 -4
View File
@@ -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;
+8 -5
View File
@@ -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;
+4 -2
View File
@@ -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(
() =>
+9 -5
View File
@@ -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;
+4 -3
View File
@@ -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}`}
/>
);
+5 -4
View File
@@ -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;
+5 -5
View File
@@ -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 -9
View File
@@ -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;
+6 -3
View File
@@ -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);
+2 -1
View File
@@ -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);
+12 -9
View File
@@ -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;
+5 -5
View File
@@ -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;
+6 -4
View File
@@ -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;
+6 -4
View File
@@ -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;
+6 -4
View File
@@ -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;
+7 -5
View File
@@ -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;
+5 -3
View File
@@ -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;
+5 -4
View File
@@ -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;
+6 -4
View File
@@ -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;
+7 -4
View File
@@ -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;
+2 -1
View File
@@ -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;
+3 -1
View File
@@ -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);
+6 -4
View File
@@ -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;
+6 -4
View File
@@ -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;
+2 -2
View File
@@ -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;
+5 -4
View File
@@ -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,
},
});
}));
+6 -4
View File
@@ -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,
},
});
}));
+7 -4
View File
@@ -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,
},
});
}));
+6 -4
View File
@@ -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,
},
});
}));
+6 -4
View File
@@ -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,
},
});
}));
+8 -4
View File
@@ -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',
},
});
}));
+6 -5
View File
@@ -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',
},
});
}));
+10 -4
View File
@@ -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,
},
});
}));
+6 -4
View File
@@ -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,
},
});
}));
+6 -4
View File
@@ -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,
},
});
}));
+7 -4
View File
@@ -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,
},
});
}));
+6 -4
View File
@@ -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,
},
});
}));
+18 -8
View File
@@ -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;
+8 -5
View File
@@ -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',
},
});
}));
+6 -4
View File
@@ -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,
},
});
}));
+16 -7
View File
@@ -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,
},
}));
+7 -5
View File
@@ -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,
},
});
}));
+10 -5
View File
@@ -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,
},
});
}));
+7 -5
View File
@@ -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,
},
});
}));
+10 -3
View File
@@ -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,
},
});
}));
+7 -4
View File
@@ -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,
},
});
}));