mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 20:49:47 +02:00
theming + material you
This commit is contained in:
@@ -3,11 +3,12 @@ import { Tabs } from 'expo-router';
|
||||
// Animated.timing and may use the native driver, so the easing must be serializable.
|
||||
import { Easing } from 'react-native';
|
||||
import { TabBar, type TabItem } from '@/components/TabBar';
|
||||
import { colors } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
|
||||
const TAB_TRANSITION_MS = 160;
|
||||
|
||||
export default function TabsLayout() {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Tabs
|
||||
detachInactiveScreens
|
||||
|
||||
@@ -24,10 +24,10 @@ import { GraphicEQPanel } from '@/components/eq/GraphicEQPanel';
|
||||
import { PresetSheet } from '@/components/eq/PresetSheet';
|
||||
import { SavePresetSheet } from '@/components/eq/SavePresetSheet';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { isWideWindow } from '@/theme/adaptive';
|
||||
import { useEQStore } from '@/stores/eqStore';
|
||||
import { useScopeActive } from '@/scope/scopeStore';
|
||||
@@ -52,6 +52,8 @@ type SheetKind = 'none' | 'preset' | 'save' | 'overflow' | 'type';
|
||||
const BAND_TYPES: EQBandType[] = ['lowshelf', 'peaking', 'highshelf', 'highpass', 'lowpass'];
|
||||
|
||||
export default function EQScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const eq = useEQStore();
|
||||
const scopeActive = useScopeActive();
|
||||
const [focused, setFocused] = useState(false);
|
||||
@@ -394,7 +396,7 @@ function parsePlainNumber(value: string): number | null {
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -501,4 +503,4 @@ const styles = StyleSheet.create({
|
||||
marginTop: spacing.xs,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -23,11 +23,12 @@ import {
|
||||
useScrollTopGate
|
||||
} from '@/components/search/PullSearchGesture';
|
||||
import {
|
||||
colors,
|
||||
fonts,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { rgbaFromHex } from '@/theme/colorUtils';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
@@ -90,6 +91,8 @@ function SectionHeader({
|
||||
actionLabel?: string;
|
||||
onActionPress?: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={styles.sectionTitleGroup}>
|
||||
@@ -115,6 +118,7 @@ function SectionHeader({
|
||||
}
|
||||
|
||||
function AlbumCover({ album, size }: { album: Album; size: number }) {
|
||||
const styles = useStyles();
|
||||
const artUri = albumArtworkSource(album);
|
||||
return (
|
||||
<View style={[styles.albumArt, { width: size, height: size }]}>
|
||||
@@ -145,6 +149,8 @@ function NowPlayingCard({
|
||||
duration: number;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const isPlaying = playbackState === 'playing';
|
||||
const isLoading = playbackState === 'loading';
|
||||
const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
|
||||
@@ -231,6 +237,7 @@ function RecentlyAddedAlbum({
|
||||
album: Album;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<Pressable style={styles.recentAlbum} onPress={onPress} accessibilityRole="button">
|
||||
<AlbumCover album={album} size={112} />
|
||||
@@ -259,6 +266,8 @@ function RandomAlbumCard({
|
||||
onReroll: () => void;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const disabled = tracks.length === 0;
|
||||
|
||||
return (
|
||||
@@ -325,6 +334,8 @@ function EmptyHomeCard({
|
||||
scanError: string | null;
|
||||
onManageFolders: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.emptyCard}>
|
||||
<Ionicons name="folder-open-outline" size={34} color={colors.textTertiary} />
|
||||
@@ -354,6 +365,8 @@ function EmptyHomeCard({
|
||||
}
|
||||
|
||||
export default function HomeScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const tracks = useLibraryStore((s) => s.tracks);
|
||||
const albums = useLibraryStore((s) => s.albums);
|
||||
@@ -588,7 +601,7 @@ export default function HomeScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
content: {
|
||||
paddingBottom: spacing.xxl,
|
||||
},
|
||||
@@ -636,7 +649,7 @@ const styles = StyleSheet.create({
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(8, 10, 15, 0.28)',
|
||||
backgroundColor: rgbaFromHex(colors.bgPrimary, 0.28),
|
||||
},
|
||||
playerArt: {
|
||||
width: 112,
|
||||
@@ -813,4 +826,4 @@ const styles = StyleSheet.create({
|
||||
emptyCopy: {
|
||||
gap: spacing.xs,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { colors } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
|
||||
/**
|
||||
* Nested stack inside the Library tab so album/artist detail screens keep the
|
||||
* tab bar + mini-player visible.
|
||||
*/
|
||||
export default function LibraryLayout() {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
|
||||
@@ -11,7 +11,8 @@ import { AstraLogo } from '@/components/AstraLogo';
|
||||
import { TrackRow } from '@/components/library/TrackRow';
|
||||
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
|
||||
import { CollapsingHeader, useDetailCollapse } from '@/components/library/CollapsingDetail';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { playTracks, shuffleTracks } from '@/audio/playbackController';
|
||||
@@ -27,6 +28,7 @@ type AlbumRow =
|
||||
| { kind: 'disc'; disc: number };
|
||||
|
||||
function DiscHeader({ disc }: { disc: number }) {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.discHeader}>
|
||||
<Ionicons name="disc-outline" size={16} color={colors.textSecondary} />
|
||||
@@ -36,6 +38,7 @@ function DiscHeader({ disc }: { disc: number }) {
|
||||
}
|
||||
|
||||
export default function AlbumScreen() {
|
||||
const colors = useColors();
|
||||
const { key, from } = useLocalSearchParams<{ key: string; from?: string }>();
|
||||
const albums = useLibraryStore((s) => s.albums);
|
||||
const allTracks = useLibraryStore((s) => s.tracks);
|
||||
|
||||
@@ -21,11 +21,12 @@ import { TrackRow } from '@/components/library/TrackRow';
|
||||
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
|
||||
import { CollapsingHeader, useDetailCollapse } from '@/components/library/CollapsingDetail';
|
||||
import {
|
||||
colors,
|
||||
fontSize,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import type { Palette } from '@/theme/palettes';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
@@ -60,6 +61,8 @@ type ArtistPageItem =
|
||||
| { key: 'empty'; type: 'empty' };
|
||||
|
||||
export default function ArtistScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { name = 'Artist', from } = useLocalSearchParams<{ name: string; from?: string }>();
|
||||
const handleBack = useLibraryDetailBack(from);
|
||||
@@ -165,7 +168,7 @@ export default function ArtistScreen() {
|
||||
}}
|
||||
/>
|
||||
<CollapsingHeader
|
||||
artwork={artistArtwork(detail.artworkHashes)}
|
||||
artwork={artistArtwork(detail.artworkHashes, colors, styles)}
|
||||
backdropUri={backdropHash ? artworkThumbUri(backdropHash) : null}
|
||||
title={name}
|
||||
heroMeta={
|
||||
@@ -247,7 +250,11 @@ function buildListItems(detail: ArtistDetail): ArtistPageItem[] {
|
||||
}
|
||||
|
||||
/** Inner artwork for the collapsing header: 2x2 album mosaic, single cover, or fallback. */
|
||||
function artistArtwork(hashes: string[]) {
|
||||
function artistArtwork(
|
||||
hashes: string[],
|
||||
colors: Palette,
|
||||
styles: ReturnType<typeof useStyles>,
|
||||
) {
|
||||
const useMosaic = hashes.length >= 4;
|
||||
const display = useMosaic ? hashes.slice(0, 4) : hashes.slice(0, 1);
|
||||
|
||||
@@ -283,6 +290,8 @@ function SectionHeader({
|
||||
trailing: string;
|
||||
onPress?: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={styles.sectionTitleGroup}>
|
||||
@@ -312,6 +321,7 @@ function AlbumRail({
|
||||
albums: ArtistAlbum[];
|
||||
onAlbumPress: (album: ArtistAlbum) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal
|
||||
@@ -352,6 +362,8 @@ function AlbumRail({
|
||||
}
|
||||
|
||||
function StatChip({ icon, label }: { icon: IconName; label: string }) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.statChip}>
|
||||
<Ionicons name={icon} size={13} color={colors.accentText} />
|
||||
@@ -380,7 +392,7 @@ function trackSubtitle(track: DbTrack, section: 'appearances' | 'songs'): string
|
||||
return track.album;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
// The backdrop runs behind the status bar; content pads itself instead.
|
||||
screen: {
|
||||
paddingTop: 0,
|
||||
@@ -482,4 +494,4 @@ const styles = StyleSheet.create({
|
||||
emptyText: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -10,12 +10,14 @@ import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { AlbumGridItem } from '@/components/library/AlbumGridItem';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
import { buildArtistDetail } from '@/library/artistDetail';
|
||||
|
||||
export default function ArtistAlbumsScreen() {
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { name = 'Artist' } = useLocalSearchParams<{ name: string }>();
|
||||
const allTracks = useLibraryStore((s) => s.tracks);
|
||||
@@ -68,6 +70,7 @@ export default function ArtistAlbumsScreen() {
|
||||
}
|
||||
|
||||
function EmptyList({ label }: { label: string }) {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<Ionicons name="albums-outline" size={24} color={colors.textTertiary} />
|
||||
|
||||
@@ -11,7 +11,8 @@ import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { TrackRow } from '@/components/library/TrackRow';
|
||||
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
@@ -21,6 +22,7 @@ import { buildArtistDetail } from '@/library/artistDetail';
|
||||
import type { DbTrack } from '@/types/library';
|
||||
|
||||
export default function ArtistAppearancesScreen() {
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { name = 'Artist' } = useLocalSearchParams<{ name: string }>();
|
||||
const allTracks = useLibraryStore((s) => s.tracks);
|
||||
@@ -79,6 +81,7 @@ export default function ArtistAppearancesScreen() {
|
||||
}
|
||||
|
||||
function EmptyList({ label }: { label: string }) {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<Ionicons name="people-outline" size={24} color={colors.textTertiary} />
|
||||
|
||||
@@ -11,7 +11,8 @@ import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { TrackRow } from '@/components/library/TrackRow';
|
||||
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
@@ -21,6 +22,7 @@ import { buildArtistDetail } from '@/library/artistDetail';
|
||||
import type { DbTrack } from '@/types/library';
|
||||
|
||||
export default function ArtistSongsScreen() {
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { name = 'Artist' } = useLocalSearchParams<{ name: string }>();
|
||||
const allTracks = useLibraryStore((s) => s.tracks);
|
||||
@@ -79,6 +81,7 @@ export default function ArtistSongsScreen() {
|
||||
}
|
||||
|
||||
function EmptyList({ label }: { label: string }) {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<Ionicons name="musical-notes" size={24} color={colors.textTertiary} />
|
||||
|
||||
@@ -37,7 +37,8 @@ import {
|
||||
PullSearchScrollView,
|
||||
useScrollTopGate
|
||||
} from '@/components/search/PullSearchGesture';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useSearchStore } from '@/stores/searchStore';
|
||||
@@ -75,6 +76,7 @@ const ALBUM_SORT_OPTIONS: AlbumSort[] = ['artist', 'name', 'recently_added', 'ye
|
||||
const ARTIST_SORT_OPTIONS: ArtistSort[] = ['name', 'track_count'];
|
||||
|
||||
export default function LibraryScreen() {
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const viewMode = useLibraryStore((s) => s.viewMode);
|
||||
const setViewMode = useLibraryStore((s) => s.setViewMode);
|
||||
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
} from '@/components/sheets/AppSheet';
|
||||
import { TextPromptModal } from '@/components/sheets/TextPromptModal';
|
||||
import { CollapsingHeader, useDetailCollapse } from '@/components/library/CollapsingDetail';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { playTracks, shuffleTracks } from '@/audio/playbackController';
|
||||
@@ -52,6 +53,8 @@ function basename(path: string): string {
|
||||
}
|
||||
|
||||
function MissingRow({ entry, onLongPress }: { entry: PlaylistTrackEntry; onLongPress: () => void }) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Pressable style={styles.missingRow} onLongPress={onLongPress} accessibilityRole="button">
|
||||
<View style={styles.missingMeta}>
|
||||
@@ -70,6 +73,8 @@ function MissingRow({ entry, onLongPress }: { entry: PlaylistTrackEntry; onLongP
|
||||
type Prompt = { kind: 'rename'; playlist: Playlist } | null;
|
||||
|
||||
export default function PlaylistScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { id, from } = useLocalSearchParams<{ id: string; from?: string }>();
|
||||
const handleBack = useLibraryDetailBack(from);
|
||||
@@ -404,7 +409,7 @@ export default function PlaylistScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
// The backdrop runs behind the status bar; content pads itself instead.
|
||||
screen: {
|
||||
paddingTop: 0,
|
||||
@@ -438,4 +443,4 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.xs,
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
AppSheetSection,
|
||||
AppSheetTitle,
|
||||
} from '@/components/sheets/AppSheet';
|
||||
import { colors, fonts, fontSize, radius, spacing } from '@/theme';
|
||||
import { fonts, fontSize, radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { usePlaylistStore } from '@/stores/playlistStore';
|
||||
import {
|
||||
DYNAMIC_PLAYLIST_PRESETS,
|
||||
@@ -325,6 +326,8 @@ function DraftActions({
|
||||
onCancel: () => void;
|
||||
onApply: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.sheetActions}>
|
||||
<Pressable style={[styles.sheetButton, styles.cancelButton]} onPress={onCancel} accessibilityRole="button">
|
||||
@@ -427,6 +430,8 @@ function ConditionValueEditor({
|
||||
condition: DynamicPlaylistCondition;
|
||||
onChange: (condition: DynamicPlaylistCondition) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
if (condition.kind === 'text') {
|
||||
return (
|
||||
<BottomSheetTextInput
|
||||
@@ -523,6 +528,8 @@ function ConditionEditorSheet({
|
||||
onCancel: () => void;
|
||||
onApply: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const draft = target.draft;
|
||||
const error = validateCondition(draft);
|
||||
|
||||
@@ -591,6 +598,8 @@ function SortLimitSheet({
|
||||
onCancel: () => void;
|
||||
onApply: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const trimmedLimit = limitText.trim();
|
||||
const parsedLimit = trimmedLimit ? Number(trimmedLimit) : null;
|
||||
const limitValid =
|
||||
@@ -661,6 +670,8 @@ function PreviewSheet({
|
||||
isLoading: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const count = preview?.track_count ?? 0;
|
||||
|
||||
return (
|
||||
@@ -701,6 +712,8 @@ function FilterCard({
|
||||
onPress: () => void;
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const option = fieldOptionForKey(getConditionFieldKey(condition));
|
||||
|
||||
return (
|
||||
@@ -730,6 +743,8 @@ function FilterCard({
|
||||
}
|
||||
|
||||
export default function DynamicPlaylistEditorScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { id } = useLocalSearchParams<{ id?: string }>();
|
||||
const playlistId = id ? Number(id) : null;
|
||||
@@ -1175,7 +1190,7 @@ export default function DynamicPlaylistEditorScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
keyboardRoot: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.bgPrimary,
|
||||
@@ -1449,4 +1464,4 @@ const styles = StyleSheet.create({
|
||||
previewMessage: {
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
+111
-3
@@ -14,8 +14,13 @@ import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { EQSlider } from '@/components/eq/EQSlider';
|
||||
import { ScanProgress } from '@/components/library/ScanProgress';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { SegmentedControl } from '@/components/SegmentedControl';
|
||||
import { AccentSwatchRow } from '@/components/settings/AccentSwatchRow';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import type { BaseThemeId, PreferredDark } from '@/theme/resolve';
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
import { useLibraryStore, type FolderWithCount } from '@/stores/libraryStore';
|
||||
import { useAudioSettingsStore } from '@/stores/audioSettingsStore';
|
||||
import { useRemoteSourcesStore } from '@/stores/remoteSourcesStore';
|
||||
@@ -34,6 +39,93 @@ function lastFmScrobbleSubtitle(status: LastFmStatus | null): string {
|
||||
return status?.enabled ? `${base}.` : `${base} · paused.`;
|
||||
}
|
||||
|
||||
const THEME_OPTIONS: { id: BaseThemeId; title: string; description: string }[] = [
|
||||
{ id: 'system', title: 'System', description: 'Follow the Android dark/light setting.' },
|
||||
{ id: 'midnight', title: 'Midnight', description: 'Deep navy. The classic Astra look.' },
|
||||
{ id: 'dark', title: 'Dark', description: 'Neutral dark gray, no navy cast.' },
|
||||
{ id: 'amoled', title: 'AMOLED', description: 'True black. Easy on OLED screens and batteries.' },
|
||||
{ id: 'light', title: 'Light', description: 'Cool near-white with navy ink.' },
|
||||
{ id: 'materialYou', title: 'Material You', description: 'Colors from your wallpaper.' },
|
||||
];
|
||||
|
||||
const DARK_STYLE_SEGMENTS = [
|
||||
{ key: 'midnight', label: 'Midnight' },
|
||||
{ key: 'dark', label: 'Dark' },
|
||||
{ key: 'amoled', label: 'AMOLED' },
|
||||
];
|
||||
|
||||
/** Theme picker + accent swatches. Lives first in settings — it demos live switching. */
|
||||
function AppearanceSettings() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const baseTheme = useThemeStore((s) => s.baseTheme);
|
||||
const preferredDark = useThemeStore((s) => s.preferredDark);
|
||||
const accentId = useThemeStore((s) => s.accentId);
|
||||
const materialYouAvailable = useThemeStore((s) => s.materialYouAvailable);
|
||||
const resolvedId = useThemeStore((s) => s.theme.id);
|
||||
const setBaseTheme = useThemeStore((s) => s.setBaseTheme);
|
||||
const setPreferredDark = useThemeStore((s) => s.setPreferredDark);
|
||||
const setAccent = useThemeStore((s) => s.setAccent);
|
||||
|
||||
const options = THEME_OPTIONS.filter(
|
||||
(option) => option.id !== 'materialYou' || materialYouAvailable
|
||||
);
|
||||
// Material You picks its own accent from the wallpaper — hide the swatches.
|
||||
const accentApplies = !resolvedId.startsWith('materialYou');
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.options}>
|
||||
{options.map((option) => {
|
||||
const selected = option.id === baseTheme;
|
||||
return (
|
||||
<Pressable
|
||||
key={option.id}
|
||||
style={[styles.option, selected && styles.optionSelected]}
|
||||
onPress={() => void setBaseTheme(option.id)}
|
||||
accessibilityRole="radio"
|
||||
accessibilityState={{ selected }}
|
||||
>
|
||||
<View style={styles.optionText}>
|
||||
<Text variant="body" color={selected ? colors.accentTextStrong : colors.textPrimary}>
|
||||
{option.title}
|
||||
</Text>
|
||||
<Text variant="caption" color={colors.textSecondary} style={styles.optionDescription}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</View>
|
||||
{selected ? (
|
||||
<Ionicons name="checkmark-circle" size={20} color={colors.accent} />
|
||||
) : (
|
||||
<Ionicons name="ellipse-outline" size={20} color={colors.textTertiary} />
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{baseTheme === 'system' ? (
|
||||
<View style={styles.appearanceBlock}>
|
||||
<Text variant="caption" color={colors.textSecondary} style={styles.settingNote}>
|
||||
Dark style used when the system is dark.
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
segments={DARK_STYLE_SEGMENTS}
|
||||
value={preferredDark}
|
||||
onChange={(key) => void setPreferredDark(key as PreferredDark)}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{accentApplies ? (
|
||||
<View style={styles.appearanceBlock}>
|
||||
<AccentSwatchRow value={accentId} onChange={(id) => void setAccent(id)} />
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ARTIST_GROUPING_OPTIONS: { mode: ArtistGroupingMode; title: string; description: string }[] = [
|
||||
{
|
||||
mode: 'astra',
|
||||
@@ -64,6 +156,8 @@ function ToggleRow({
|
||||
value: boolean;
|
||||
onValueChange: (v: boolean) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.toggleRow}>
|
||||
<View style={styles.toggleText}>
|
||||
@@ -99,6 +193,8 @@ function LibraryFolderSettingsRow({
|
||||
disabled: boolean;
|
||||
onRemove: (folder: FolderWithCount) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.folderSettingsRow}>
|
||||
<Ionicons
|
||||
@@ -135,6 +231,8 @@ function LibraryFolderSettingsRow({
|
||||
}
|
||||
|
||||
function LibraryFoldersSettings() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const folders = useLibraryStore((s) => s.folders);
|
||||
const isScanning = useLibraryStore((s) => s.isScanning);
|
||||
const scanError = useLibraryStore((s) => s.scanError);
|
||||
@@ -227,6 +325,8 @@ function LibraryFoldersSettings() {
|
||||
}
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const remoteSources = useRemoteSourcesStore((s) => s.sources);
|
||||
const lastFmStatus = useLastFmSettingsStore((s) => s.status);
|
||||
@@ -279,6 +379,11 @@ export default function SettingsScreen() {
|
||||
</Text>
|
||||
|
||||
<Text variant="label" color={colors.textTertiary} style={styles.sectionLabel}>
|
||||
APPEARANCE
|
||||
</Text>
|
||||
<AppearanceSettings />
|
||||
|
||||
<Text variant="label" color={colors.textTertiary} style={[styles.sectionLabel, styles.sectionSpacing]}>
|
||||
LIBRARY FOLDERS
|
||||
</Text>
|
||||
<LibraryFoldersSettings />
|
||||
@@ -462,7 +567,7 @@ export default function SettingsScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
content: {
|
||||
paddingBottom: spacing.xxl,
|
||||
},
|
||||
@@ -580,6 +685,9 @@ const styles = StyleSheet.create({
|
||||
settingTitle: {
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
appearanceBlock: {
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
settingNote: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
@@ -607,4 +715,4 @@ const styles = StyleSheet.create({
|
||||
optionDescription: {
|
||||
lineHeight: 16,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
+31
-9
@@ -40,7 +40,8 @@ import {
|
||||
import { fetchDesktopRemoteIdentity } from '@/services/desktopRemoteClient';
|
||||
import { useDesktopSyncStore } from '@/stores/desktopSyncStore';
|
||||
import { SyncConflictPrompt } from '@/components/sync/SyncConflictPrompt';
|
||||
import { colors } from '@/theme';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
import { useTheme } from '@/theme/themed';
|
||||
|
||||
// Anchor the root stack at the tabs so a deep link straight to a top-level route (the
|
||||
// widget/notification opening `now-playing`, or `recently-played`) builds `[(tabs), route]`
|
||||
@@ -58,6 +59,21 @@ function PlaybackSync() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-reads system theme inputs (OS scheme + monet wallpaper ramps) on each
|
||||
* return to foreground — wallpaper changes can only happen while backgrounded.
|
||||
* OS dark/light toggles are covered by the Appearance listener in themeStore.
|
||||
*/
|
||||
function ThemeSystemSync() {
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener('change', (state) => {
|
||||
if (state === 'active') useThemeStore.getState().refreshSystemInputs();
|
||||
});
|
||||
return () => subscription.remove();
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Owns the visualizer on/off gate (foreground + playing + motion). Renders nothing. */
|
||||
function ScopeLifecycle() {
|
||||
useScopeLifecycle();
|
||||
@@ -260,7 +276,12 @@ export default function RootLayout() {
|
||||
const timer = setTimeout(() => setSplashTimedOut(true), 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
const ready = fontsLoaded || splashTimedOut;
|
||||
// Theme joins the gate so the first painted frame is already in the
|
||||
// persisted theme (no flash). The failsafe path paints the default theme
|
||||
// and snaps once the SQLite read lands — accepted degradation.
|
||||
const themeLoaded = useThemeStore((s) => s.loaded);
|
||||
const theme = useTheme();
|
||||
const ready = (fontsLoaded && themeLoaded) || splashTimedOut;
|
||||
|
||||
useEffect(() => {
|
||||
if (ready) {
|
||||
@@ -272,6 +293,10 @@ export default function RootLayout() {
|
||||
// Library tab + playback adapters get data immediately. EQ + audio settings load
|
||||
// alongside so the native EQ/gain reflect persisted prefs from the first play.
|
||||
useEffect(() => {
|
||||
useThemeStore
|
||||
.getState()
|
||||
.load()
|
||||
.catch((err) => console.error('[theme] load failed', err));
|
||||
useLibraryStore
|
||||
.getState()
|
||||
.initialize()
|
||||
@@ -301,9 +326,10 @@ export default function RootLayout() {
|
||||
if (!ready) return null;
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={styles.root}>
|
||||
<GestureHandlerRootView style={{ flex: 1, backgroundColor: theme.colors.bgPrimary }}>
|
||||
<SafeAreaProvider>
|
||||
<StatusBar style="light" />
|
||||
<StatusBar style={theme.statusBarStyle} />
|
||||
<ThemeSystemSync />
|
||||
<PlaybackSync />
|
||||
<ScopeLifecycle />
|
||||
<NormalizationSync />
|
||||
@@ -314,7 +340,7 @@ export default function RootLayout() {
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
contentStyle: { backgroundColor: colors.bgPrimary },
|
||||
contentStyle: { backgroundColor: theme.colors.bgPrimary },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="(tabs)" />
|
||||
@@ -334,7 +360,3 @@ export default function RootLayout() {
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
root: { flex: 1, backgroundColor: colors.bgPrimary },
|
||||
} as const;
|
||||
|
||||
@@ -23,10 +23,10 @@ import { AstraLogo } from '@/components/AstraLogo';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { isWideWindow, WIDE_MIN_WIDTH } from '@/theme/adaptive';
|
||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
import type { DesktopRemoteDiscoveredDesktop } from '@/types/desktopRemote';
|
||||
@@ -188,6 +188,8 @@ function DiscoveredDesktopRow({ desktop, onPair, disabled }: {
|
||||
onPair: (desktop: DesktopRemoteDiscoveredDesktop) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.discoveredRow, disabled && styles.buttonDisabled]}
|
||||
@@ -216,6 +218,8 @@ function DiscoveredDesktopRow({ desktop, onPair, disabled }: {
|
||||
}
|
||||
|
||||
export default function DesktopRemoteScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { pair } = useLocalSearchParams<{ pair?: string }>();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -641,7 +645,7 @@ export default function DesktopRemoteScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -993,4 +997,4 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: CONTENT_SIDE_PADDING,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -15,13 +15,15 @@ import { useRouter } from 'expo-router';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
|
||||
|
||||
export default function DesktopRemoteScanScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const pairFromInput = useDesktopRemoteStore((s) => s.pairFromInput);
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
@@ -89,7 +91,7 @@ export default function DesktopRemoteScanScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
header: {
|
||||
marginTop: spacing.md,
|
||||
marginBottom: spacing.lg,
|
||||
@@ -156,4 +158,4 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -18,7 +18,8 @@ import { useRouter } from 'expo-router';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { SyncConflictDetails } from '@/components/sync/SyncConflictDetails';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { radius, spacing } from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { formatRelativeTime } from '@/lib/format';
|
||||
import { getDesktopRemoteConnection } from '@/services/desktopRemoteCredentials';
|
||||
import { useDesktopSyncStore } from '@/stores/desktopSyncStore';
|
||||
@@ -65,6 +66,8 @@ function ConflictCard({
|
||||
busy: boolean;
|
||||
onResolve: (resolution: DesktopSyncConflictResolution) => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const [selectedResolution, setSelectedResolution] = useState<DesktopSyncConflictResolution | null>(null);
|
||||
const desktopSnapshot = syncPlaylistToSnapshot(conflict.remote);
|
||||
const phoneSnapshot = syncPlaylistToSnapshot(conflict.local);
|
||||
@@ -126,6 +129,8 @@ function ConflictCard({
|
||||
}
|
||||
|
||||
export default function DesktopSyncScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const status = useDesktopSyncStore((s) => s.status);
|
||||
const lastSyncAt = useDesktopSyncStore((s) => s.lastSyncAt);
|
||||
@@ -295,7 +300,7 @@ export default function DesktopSyncScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
topBar: {
|
||||
marginTop: spacing.md,
|
||||
marginBottom: spacing.sm,
|
||||
@@ -405,4 +410,4 @@ const styles = StyleSheet.create({
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -15,10 +15,10 @@ import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useLastFmSettingsStore } from '@/stores/lastFmSettingsStore';
|
||||
import type { LastFmScrobbleProtocol } from '@/types/lastFm';
|
||||
|
||||
@@ -79,6 +79,8 @@ function Field({
|
||||
autoCapitalize = 'none',
|
||||
keyboardType = 'default',
|
||||
}: FieldProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.field}>
|
||||
<Text variant="label" color={colors.textTertiary} style={styles.fieldLabel}>
|
||||
@@ -100,6 +102,8 @@ function Field({
|
||||
}
|
||||
|
||||
export default function LastFmEditScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { id } = useLocalSearchParams<{ id?: string }>();
|
||||
const status = useLastFmSettingsStore((s) => s.status);
|
||||
@@ -318,7 +322,7 @@ export default function LastFmEditScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
flex: { flex: 1 },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
@@ -409,4 +413,4 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: spacing.md,
|
||||
marginTop: spacing.lg,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -12,10 +12,10 @@ import { useRouter } from 'expo-router';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useLastFmSettingsStore } from '@/stores/lastFmSettingsStore';
|
||||
import { requestLastFmFlush } from '@/services/lastfm';
|
||||
import type { LastFmProfileStatus } from '@/types/lastFm';
|
||||
@@ -44,6 +44,8 @@ function customStatusLine(profile: LastFmProfileStatus): { text: string; tone: '
|
||||
}
|
||||
|
||||
export default function LastFmScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const status = useLastFmSettingsStore((s) => s.status);
|
||||
const authHint = useLastFmSettingsStore((s) => s.authHint);
|
||||
@@ -285,7 +287,7 @@ export default function LastFmScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -416,4 +418,4 @@ const styles = StyleSheet.create({
|
||||
marginTop: spacing.xl,
|
||||
lineHeight: 16,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { getNotificationClickRedirectPath } from '@/audio/notificationIntent';
|
||||
import { colors } from '@/theme';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
|
||||
export default function NotificationClickRoute() {
|
||||
const styles = useStyles();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -26,9 +27,9 @@ export default function NotificationClickRoute() {
|
||||
return <View style={styles.root} />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
root: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.bgPrimary,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -31,10 +31,10 @@ import { PlaybackTargetPicker } from '@/components/PlaybackTargetPicker';
|
||||
import { QueueTray } from '@/components/queue/QueueTray';
|
||||
import { RemoteQueueSheet } from '@/components/queue/RemoteQueueSheet';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { WIDE_MIN_WIDTH, isWideWindow } from '@/theme/adaptive';
|
||||
import { motion } from '@/theme/motion';
|
||||
import { resolveNavigationArtist } from '@/library/artistGrouping';
|
||||
@@ -264,6 +264,8 @@ function getNowPlayingLayout(
|
||||
}
|
||||
|
||||
export default function NowPlayingScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||
@@ -1059,7 +1061,7 @@ export default function NowPlayingScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'transparent',
|
||||
@@ -1330,4 +1332,4 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: spacing.lg,
|
||||
backgroundColor: colors.glassBg,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -11,7 +11,8 @@ import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { TrackRow } from '@/components/library/TrackRow';
|
||||
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { spacing } from '@/theme';
|
||||
import { useColors } from '@/theme/themed';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { playTracks } from '@/audio/playbackController';
|
||||
@@ -23,6 +24,7 @@ function formatCount(count: number, noun: string): string {
|
||||
}
|
||||
|
||||
function EmptyList() {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<Ionicons name="time-outline" size={24} color={colors.textTertiary} />
|
||||
@@ -34,6 +36,7 @@ function EmptyList() {
|
||||
}
|
||||
|
||||
export default function RecentlyPlayedScreen() {
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const tracks = useLibraryStore((s) => s.recentlyPlayedTracks);
|
||||
const currentPath = usePlayerStore((s) => s.currentTrack?.path);
|
||||
|
||||
@@ -14,10 +14,10 @@ import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRemoteSourcesStore } from '@/stores/remoteSourcesStore';
|
||||
import type { RemoteSourceType } from '@/types/remote';
|
||||
|
||||
@@ -60,6 +60,8 @@ function Field({
|
||||
autoCapitalize = 'none',
|
||||
keyboardType = 'default',
|
||||
}: FieldProps) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
return (
|
||||
<View style={styles.field}>
|
||||
<Text variant="label" color={colors.textTertiary} style={styles.fieldLabel}>
|
||||
@@ -81,6 +83,8 @@ function Field({
|
||||
}
|
||||
|
||||
export default function SourceEditScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { id } = useLocalSearchParams<{ id?: string }>();
|
||||
const sources = useRemoteSourcesStore((s) => s.sources);
|
||||
@@ -308,7 +312,7 @@ export default function SourceEditScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
flex: { flex: 1 },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
@@ -403,4 +407,4 @@ const styles = StyleSheet.create({
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -12,10 +12,10 @@ import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { ActionSheet, type ActionSheetItem } from '@/components/sheets/ActionSheet';
|
||||
import {
|
||||
colors,
|
||||
radius,
|
||||
spacing
|
||||
spacing,
|
||||
} from '@/theme';
|
||||
import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRemoteSourcesStore } from '@/stores/remoteSourcesStore';
|
||||
import type { RemoteSourceRow, RemoteSyncProgress } from '@/types/remote';
|
||||
|
||||
@@ -38,6 +38,8 @@ function statusLine(
|
||||
}
|
||||
|
||||
export default function SourcesScreen() {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const sources = useRemoteSourcesStore((s) => s.sources);
|
||||
const progressById = useRemoteSourcesStore((s) => s.progressById);
|
||||
@@ -196,7 +198,7 @@ export default function SourcesScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const useStyles = createThemedStyles((colors) => ({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -274,4 +276,4 @@ const styles = StyleSheet.create({
|
||||
rowName: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user