update nowplaying UI/UX

This commit is contained in:
Boof2015
2026-06-18 17:18:18 -04:00
parent 9fd37c93d7
commit 4b2382e3cb
2 changed files with 281 additions and 167 deletions
+263 -166
View File
@@ -42,13 +42,20 @@ const NARROW_CONTENT_SIDE_PADDING = spacing.md;
const MEDIA_AREA_MIN = 220; const MEDIA_AREA_MIN = 220;
const MEDIA_AREA_MAX = 360; const MEDIA_AREA_MAX = 360;
const ART_SIZE_MAX = 340; const ART_SIZE_MAX = 340;
const SCOPE_HEIGHT_MIN = 150; const VISUALIZER_ART_SCALE = 0.8;
const SCOPE_HEIGHT_MAX = 220; const VISUALIZER_ART_SIZE_MIN = 176;
const SCOPE_HEIGHT_RATIO = 9 / 16; const VISUALIZER_WIDTH_MAX = 448;
const VISUALIZER_SIDE_PADDING = spacing.md;
const VISUALIZER_TOP_GAP = spacing.lg;
const VISUALIZER_BOTTOM_GAP = spacing.sm;
const VISUALIZER_HEIGHT_MIN = 84;
const VISUALIZER_HEIGHT_MAX = 108;
const VISUALIZER_HEIGHT_RATIO = 0.28;
const HEADER_HEIGHT = 32; const HEADER_HEIGHT = 32;
const CONTENT_TOP_PADDING = spacing.sm; const CONTENT_TOP_PADDING = spacing.sm;
const CONTENT_BOTTOM_PADDING = spacing.lg; const CONTENT_BOTTOM_PADDING = spacing.lg;
const MEDIA_TOP_MARGIN = spacing.lg; const MEDIA_TOP_MARGIN = spacing.lg;
const VISUALIZER_MEDIA_BOTTOM_GAP = spacing.sm;
const MEDIA_BOTTOM_GAP = spacing.xl; const MEDIA_BOTTOM_GAP = spacing.xl;
const TRACK_INFO_ESTIMATE = 96; const TRACK_INFO_ESTIMATE = 96;
const WAVEFORM_HEIGHT = 58; const WAVEFORM_HEIGHT = 58;
@@ -57,40 +64,52 @@ const WAVEFORM_BLOCK_ESTIMATE = WAVEFORM_HEIGHT + WAVEFORM_TOUCH_PADDING * 2 + 2
const PLAY_BUTTON_SIZE = 68; const PLAY_BUTTON_SIZE = 68;
const SKIP_ICON_SIZE = 32; const SKIP_ICON_SIZE = 32;
const PLAY_ICON_SIZE = 34; const PLAY_ICON_SIZE = 34;
const TRANSPORT_GAP = spacing.xxl;
const TRANSPORT_TOP_MARGIN = spacing.lg; const TRANSPORT_TOP_MARGIN = spacing.lg;
const SUB_BUTTON_SIZE = 40; const SUB_BUTTON_SIZE = 40;
const SUB_ICON_SIZE = 20; const SUB_ICON_SIZE = 20;
const SUB_TOP_MARGIN = spacing.lg; const SUB_TOP_MARGIN = spacing.lg;
const MIN_FLOATING_SPACE = spacing.sm; const MIN_FLOATING_SPACE = spacing.sm;
const SECONDARY_CONTROLS_MIN_HEIGHT = 660;
interface NowPlayingLayout { interface NowPlayingLayout {
contentPadding: number; contentPadding: number;
contentWidth: number; contentWidth: number;
mediaHeight: number;
artSize: number; artSize: number;
scopeWidth: number; scopeWidth: number;
scopeHeight: number; scopeHeight: number;
visualizerTopGap: number;
visualizerBottomGap: number;
mediaTopMargin: number; mediaTopMargin: number;
mediaBottomGap: number; mediaBottomGap: number;
showSecondaryControls: boolean;
} }
function clamp(value: number, min: number, max: number): number { function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value)); return Math.min(max, Math.max(min, value));
} }
function getNowPlayingLayout(windowWidth: number, availableHeight: number): NowPlayingLayout { function getScopeHeight(scopeWidth: number): number {
return Math.round(
clamp(scopeWidth * VISUALIZER_HEIGHT_RATIO, VISUALIZER_HEIGHT_MIN, VISUALIZER_HEIGHT_MAX)
);
}
function getNowPlayingLayout(
windowWidth: number,
availableHeight: number,
showVisualizer: boolean
): NowPlayingLayout {
const contentPadding = const contentPadding =
windowWidth < 360 ? NARROW_CONTENT_SIDE_PADDING : CONTENT_SIDE_PADDING; windowWidth < 360 ? NARROW_CONTENT_SIDE_PADDING : CONTENT_SIDE_PADDING;
const contentWidth = Math.max(0, Math.min(windowWidth - contentPadding * 2, MAX_CONTENT_WIDTH)); const contentWidth = Math.max(0, Math.min(windowWidth - contentPadding * 2, MAX_CONTENT_WIDTH));
const scopeWidth = Math.max(
0,
Math.min(windowWidth - VISUALIZER_SIDE_PADDING * 2, VISUALIZER_WIDTH_MAX)
);
const scopeHeight = getScopeHeight(scopeWidth);
const mediaMax = Math.min(contentWidth, MEDIA_AREA_MAX); const mediaMax = Math.min(contentWidth, MEDIA_AREA_MAX);
const mediaMin = Math.min(mediaMax, MEDIA_AREA_MIN); const mediaMin = Math.min(mediaMax, MEDIA_AREA_MIN);
const showSecondaryControls = availableHeight >= SECONDARY_CONTROLS_MIN_HEIGHT;
const mediaTopMargin = availableHeight < 680 ? spacing.md : MEDIA_TOP_MARGIN; const mediaTopMargin = availableHeight < 680 ? spacing.md : MEDIA_TOP_MARGIN;
const mediaBottomGap = availableHeight < 680 ? spacing.lg : MEDIA_BOTTOM_GAP; const defaultMediaBottomGap = availableHeight < 680 ? spacing.lg : MEDIA_BOTTOM_GAP;
const secondaryHeight = showSecondaryControls ? SUB_TOP_MARGIN + SUB_BUTTON_SIZE : 0; const mediaBottomGap = showVisualizer ? VISUALIZER_MEDIA_BOTTOM_GAP : defaultMediaBottomGap;
const fixedHeight = const fixedHeight =
CONTENT_TOP_PADDING + CONTENT_TOP_PADDING +
CONTENT_BOTTOM_PADDING + CONTENT_BOTTOM_PADDING +
@@ -101,27 +120,37 @@ function getNowPlayingLayout(windowWidth: number, availableHeight: number): NowP
WAVEFORM_BLOCK_ESTIMATE + WAVEFORM_BLOCK_ESTIMATE +
TRANSPORT_TOP_MARGIN + TRANSPORT_TOP_MARGIN +
PLAY_BUTTON_SIZE + PLAY_BUTTON_SIZE +
secondaryHeight + SUB_TOP_MARGIN +
SUB_BUTTON_SIZE +
MIN_FLOATING_SPACE; MIN_FLOATING_SPACE;
const heightBoundMedia = availableHeight - fixedHeight; const heightBoundMedia = availableHeight - fixedHeight;
const mediaHeight = Math.round(clamp(heightBoundMedia, mediaMin, mediaMax)); const baseArtSize = Math.min(
const artSize = Math.min(mediaHeight, ART_SIZE_MAX); Math.round(clamp(heightBoundMedia, mediaMin, mediaMax)),
const scopeWidth = contentWidth; ART_SIZE_MAX
const scopeHeight = Math.min(
mediaHeight,
Math.round(clamp(scopeWidth * SCOPE_HEIGHT_RATIO, SCOPE_HEIGHT_MIN, SCOPE_HEIGHT_MAX))
); );
const artSize = showVisualizer
? Math.round(
clamp(
baseArtSize * VISUALIZER_ART_SCALE,
Math.min(VISUALIZER_ART_SIZE_MIN, mediaMax),
mediaMax
)
)
: baseArtSize;
const visualizerTopGap = showVisualizer ? VISUALIZER_TOP_GAP : 0;
const visualizerBottomGap = showVisualizer ? VISUALIZER_BOTTOM_GAP : 0;
return { return {
contentPadding, contentPadding,
contentWidth, contentWidth,
mediaHeight,
artSize, artSize,
scopeWidth, scopeWidth,
scopeHeight, scopeHeight,
visualizerTopGap,
visualizerBottomGap,
mediaTopMargin, mediaTopMargin,
mediaBottomGap, mediaBottomGap,
showSecondaryControls,
}; };
} }
@@ -129,9 +158,10 @@ export default function NowPlayingScreen() {
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { width: windowWidth, height: windowHeight } = useWindowDimensions(); const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const [showScopeStage, setShowScopeStage] = useState(false);
const [queueOpen, setQueueOpen] = useState(false); const [queueOpen, setQueueOpen] = useState(false);
const scopeMode = useSettingsStore((s) => s.scopeMode); const scopeMode = useSettingsStore((s) => s.scopeMode);
const scopeStageVisible = useSettingsStore((s) => s.scopeStageVisible);
const setScopeStageVisible = useSettingsStore((s) => s.setScopeStageVisible);
const track = usePlayerStore((s) => s.currentTrack); const track = usePlayerStore((s) => s.currentTrack);
const playbackState = usePlayerStore((s) => s.playbackState); const playbackState = usePlayerStore((s) => s.playbackState);
const currentTime = usePlayerStore((s) => s.currentTime); const currentTime = usePlayerStore((s) => s.currentTime);
@@ -144,7 +174,7 @@ export default function NowPlayingScreen() {
const isPlaying = playbackState === 'playing'; const isPlaying = playbackState === 'playing';
const isLoading = playbackState === 'loading'; const isLoading = playbackState === 'loading';
const availableHeight = windowHeight - insets.top - insets.bottom; const availableHeight = windowHeight - insets.top - insets.bottom;
const layout = getNowPlayingLayout(windowWidth, availableHeight); const layout = getNowPlayingLayout(windowWidth, availableHeight, scopeStageVisible);
const source = track?.album?.trim() ? track.album : 'Library'; const source = track?.album?.trim() ? track.album : 'Library';
// Swipe down to minimize. The stack transition is disabled for this route, so // Swipe down to minimize. The stack transition is disabled for this route, so
@@ -233,60 +263,24 @@ export default function NowPlayingScreen() {
{track ? ( {track ? (
<View style={styles.player}> <View style={styles.player}>
<Pressable <View
onPress={() => setShowScopeStage((visible) => !visible)}
accessibilityRole="button"
accessibilityLabel={showScopeStage ? 'Show artwork' : 'Show visualizer'}
style={[ style={[
styles.mediaArea, styles.middleStack,
{ {
height: layout.mediaHeight,
marginTop: layout.mediaTopMargin, marginTop: layout.mediaTopMargin,
marginBottom: layout.mediaBottomGap, marginBottom: layout.mediaBottomGap,
}, },
]} ]}
> >
{showScopeStage ? ( <View
<View style={[
style={[ styles.artButton,
styles.scopeSurface, {
{ width: layout.artSize,
width: layout.scopeWidth, height: layout.artSize,
height: layout.scopeHeight, },
}, ]}
]} >
>
<Visualizer
width={layout.scopeWidth}
height={layout.scopeHeight}
interactive={false}
showChrome={false}
mode={scopeMode}
edgeFade
/>
{/* Nested Pressable: RN grants the responder to this
button, so a swap tap does not also collapse the
stage (the outer Pressable's onPress won't fire). */}
<Pressable
onPress={() =>
useSettingsStore
.getState()
.setScopeMode(scopeMode === 'spectrum' ? 'scope' : 'spectrum')
}
hitSlop={12}
style={styles.scopeSwap}
accessibilityRole="button"
accessibilityLabel={`Showing ${
scopeMode === 'spectrum' ? 'spectrum' : 'oscilloscope'
}. Tap to switch.`}
>
<Text variant="caption" style={styles.scopeSwapLabel}>
{scopeMode === 'spectrum' ? 'SPECTRUM' : 'SCOPE'}
</Text>
<Ionicons name="swap-horizontal" size={14} color={colors.textTertiary} />
</Pressable>
</View>
) : (
<View <View
style={[ style={[
styles.artCard, styles.artCard,
@@ -307,38 +301,111 @@ export default function NowPlayingScreen() {
<AstraLogo size={Math.round(layout.artSize * 0.4)} /> <AstraLogo size={Math.round(layout.artSize * 0.4)} />
)} )}
</View> </View>
)}
</Pressable>
<View style={styles.trackInfo}>
<Text variant="heading" numberOfLines={2} style={styles.centered}>
{track.title}
</Text>
<Text variant="body" numberOfLines={1} style={[styles.centered, styles.artist]}>
{track.artist}
</Text>
<View style={styles.badges}>
<FormatBadges track={track} />
</View> </View>
{scopeStageVisible && (
<View
style={[
styles.scopeRail,
{
width: layout.scopeWidth,
height: layout.scopeHeight,
marginTop: layout.visualizerTopGap,
marginBottom: layout.visualizerBottomGap,
},
]}
>
<Visualizer
width={layout.scopeWidth}
height={layout.scopeHeight}
interactive={false}
showChrome={false}
mode={scopeMode}
edgeFade
/>
<Pressable
onPress={() =>
useSettingsStore
.getState()
.setScopeMode(scopeMode === 'spectrum' ? 'scope' : 'spectrum')
}
hitSlop={12}
style={styles.scopeSwap}
accessibilityRole="button"
accessibilityLabel={`Showing ${
scopeMode === 'spectrum' ? 'spectrum' : 'oscilloscope'
}. Tap to switch.`}
>
<Text variant="caption" style={styles.scopeSwapLabel}>
{scopeMode === 'spectrum' ? 'SPECTRUM' : 'SCOPE'}
</Text>
<Ionicons name="swap-horizontal" size={14} color={colors.textTertiary} />
</Pressable>
</View>
)}
</View> </View>
<View style={styles.spacer} /> <View style={styles.spacer} />
<View style={styles.playerControls}> <View style={styles.playerControls}>
<View style={styles.progressBlock}> <View style={styles.trackInfo}>
<WaveformSeekBar <View style={styles.trackTextStack}>
currentTime={currentTime} <Text variant="heading" numberOfLines={1} style={styles.trackTitle}>
duration={duration} {track.title}
height={WAVEFORM_HEIGHT} </Text>
touchPadding={WAVEFORM_TOUCH_PADDING} <View style={styles.trackMetaRow}>
trackKey={track.id} <Text variant="body" numberOfLines={1} style={styles.artist}>
trackPath={track.path} {track.artist}
onSeek={(seconds) => void seekTo(seconds)} </Text>
/> <View style={styles.badges}>
<FormatBadges track={track} />
</View>
</View>
</View>
<Pressable
hitSlop={10}
style={styles.inlineActionBtn}
onPress={() => void toggleFavorite(track)}
accessibilityLabel={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
accessibilityState={{ selected: isFavorite }}
>
<Ionicons
name={isFavorite ? 'heart' : 'heart-outline'}
size={SUB_ICON_SIZE + 4}
color={isFavorite ? colors.accent : colors.textTertiary}
/>
</Pressable>
</View> </View>
<WaveformSeekBar
currentTime={currentTime}
duration={duration}
height={WAVEFORM_HEIGHT}
touchPadding={WAVEFORM_TOUCH_PADDING}
trackKey={track.id}
trackPath={track.path}
onSeek={(seconds) => void seekTo(seconds)}
/>
<View style={styles.transport}> <View style={styles.transport}>
<Pressable onPress={skipToPrevious} hitSlop={12}> <Pressable
hitSlop={10}
style={styles.transportSideBtn}
onPress={() => void toggleShuffle()}
accessibilityLabel="Shuffle"
accessibilityState={{ selected: shuffle }}
>
<Ionicons
name="shuffle"
size={SUB_ICON_SIZE + 2}
color={shuffle ? colors.accent : colors.textTertiary}
/>
</Pressable>
<Pressable
onPress={skipToPrevious}
hitSlop={12}
style={styles.transportMainBtn}
>
<Ionicons <Ionicons
name="play-skip-back" name="play-skip-back"
size={SKIP_ICON_SIZE} size={SKIP_ICON_SIZE}
@@ -352,78 +419,67 @@ export default function NowPlayingScreen() {
color={colors.bgPrimary} color={colors.bgPrimary}
/> />
</Pressable> </Pressable>
<Pressable onPress={skipToNext} hitSlop={12}> <Pressable
onPress={skipToNext}
hitSlop={12}
style={styles.transportMainBtn}
>
<Ionicons <Ionicons
name="play-skip-forward" name="play-skip-forward"
size={SKIP_ICON_SIZE} size={SKIP_ICON_SIZE}
color={colors.textPrimary} color={colors.textPrimary}
/> />
</Pressable> </Pressable>
<Pressable
hitSlop={10}
style={styles.transportSideBtn}
onPress={() => void cycleRepeat()}
accessibilityLabel="Repeat"
accessibilityState={{ selected: repeat !== 'none' }}
>
{repeat === 'one' ? (
<MaterialCommunityIcons
name="repeat-once"
size={SUB_ICON_SIZE + 2}
color={colors.accent}
/>
) : (
<Ionicons
name="repeat"
size={SUB_ICON_SIZE + 2}
color={repeat === 'all' ? colors.accent : colors.textTertiary}
/>
)}
</Pressable>
</View> </View>
{layout.showSecondaryControls && ( <View style={styles.subRow}>
<View style={styles.subRow}> <Pressable
<Pressable hitSlop={10}
hitSlop={10} style={styles.subBtn}
style={styles.subBtn} onPress={() => void setScopeStageVisible(!scopeStageVisible)}
onPress={() => void toggleShuffle()} accessibilityLabel={scopeStageVisible ? 'Hide visualizer' : 'Show visualizer'}
accessibilityLabel="Shuffle" accessibilityState={{ selected: scopeStageVisible }}
accessibilityState={{ selected: shuffle }} >
> <MaterialCommunityIcons
<Ionicons name="sine-wave"
name="shuffle" size={SUB_ICON_SIZE + 2}
size={SUB_ICON_SIZE} color={scopeStageVisible ? colors.accent : colors.textTertiary}
color={shuffle ? colors.accent : colors.textTertiary} />
/> </Pressable>
</Pressable> <Pressable
<Pressable hitSlop={10}
hitSlop={10} style={styles.subBtn}
style={styles.subBtn} onPress={() => setQueueOpen(true)}
onPress={() => void toggleFavorite(track)} accessibilityLabel="Queue"
accessibilityLabel={isFavorite ? 'Remove from favorites' : 'Add to favorites'} >
accessibilityState={{ selected: isFavorite }} <Ionicons
> name="list-outline"
<Ionicons size={SUB_ICON_SIZE + 2}
name={isFavorite ? 'heart' : 'heart-outline'} color={colors.textTertiary}
size={SUB_ICON_SIZE} />
color={isFavorite ? colors.accent : colors.textTertiary} </Pressable>
/> </View>
</Pressable>
<Pressable
hitSlop={10}
style={styles.subBtn}
onPress={() => setQueueOpen(true)}
accessibilityLabel="Queue"
>
<Ionicons
name="list-outline"
size={SUB_ICON_SIZE}
color={colors.textTertiary}
/>
</Pressable>
<Pressable
hitSlop={10}
style={styles.subBtn}
onPress={() => void cycleRepeat()}
accessibilityLabel="Repeat"
accessibilityState={{ selected: repeat !== 'none' }}
>
{repeat === 'one' ? (
<MaterialCommunityIcons
name="repeat-once"
size={SUB_ICON_SIZE}
color={colors.accent}
/>
) : (
<Ionicons
name="repeat"
size={SUB_ICON_SIZE}
color={repeat === 'all' ? colors.accent : colors.textTertiary}
/>
)}
</Pressable>
</View>
)}
</View> </View>
</View> </View>
) : ( ) : (
@@ -483,9 +539,12 @@ const styles = StyleSheet.create({
player: { player: {
flex: 1, flex: 1,
}, },
mediaArea: { middleStack: {
width: '100%', width: '100%',
alignItems: 'center', alignItems: 'center',
},
artButton: {
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
artCard: { artCard: {
@@ -495,7 +554,8 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', overflow: 'hidden',
}, },
scopeSurface: { scopeRail: {
alignSelf: 'center',
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', overflow: 'hidden',
}, },
@@ -517,20 +577,45 @@ const styles = StyleSheet.create({
height: '100%', height: '100%',
}, },
trackInfo: { trackInfo: {
alignItems: 'center',
alignSelf: 'stretch', alignSelf: 'stretch',
flexDirection: 'row',
alignItems: 'center',
gap: spacing.md,
marginBottom: spacing.md,
},
trackTextStack: {
flex: 1,
minWidth: 0,
alignItems: 'flex-start',
},
trackTitle: {
alignSelf: 'stretch',
textAlign: 'left',
},
inlineActionBtn: {
width: SUB_BUTTON_SIZE,
height: SUB_BUTTON_SIZE,
alignItems: 'center',
justifyContent: 'center',
},
trackMetaRow: {
alignSelf: 'stretch',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
gap: spacing.sm,
marginTop: spacing.xs,
}, },
centered: { centered: {
textAlign: 'center', textAlign: 'center',
}, },
artist: { artist: {
color: colors.accentText, color: colors.accentText,
marginTop: spacing.xs, flexShrink: 1,
minWidth: 0,
}, },
badges: { badges: {
marginTop: spacing.md, flexShrink: 0,
},
progressBlock: {
}, },
spacer: { spacer: {
flex: 1, flex: 1,
@@ -542,10 +627,21 @@ const styles = StyleSheet.create({
transport: { transport: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'space-between',
gap: TRANSPORT_GAP,
marginTop: TRANSPORT_TOP_MARGIN, marginTop: TRANSPORT_TOP_MARGIN,
}, },
transportMainBtn: {
width: 48,
height: 48,
alignItems: 'center',
justifyContent: 'center',
},
transportSideBtn: {
width: 48,
height: 48,
alignItems: 'center',
justifyContent: 'center',
},
playButton: { playButton: {
width: PLAY_BUTTON_SIZE, width: PLAY_BUTTON_SIZE,
height: PLAY_BUTTON_SIZE, height: PLAY_BUTTON_SIZE,
@@ -557,7 +653,8 @@ const styles = StyleSheet.create({
subRow: { subRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'flex-end',
gap: spacing.lg,
marginTop: SUB_TOP_MARGIN, marginTop: SUB_TOP_MARGIN,
paddingHorizontal: spacing.sm, paddingHorizontal: spacing.sm,
}, },
+18 -1
View File
@@ -10,6 +10,7 @@ import type { ArtistGroupingMode } from '@/library/artistGrouping';
*/ */
const ARTIST_GROUPING_KEY = 'artist_grouping_mode'; const ARTIST_GROUPING_KEY = 'artist_grouping_mode';
const SCOPE_MODE_KEY = 'scope_mode'; const SCOPE_MODE_KEY = 'scope_mode';
const SCOPE_STAGE_VISIBLE_KEY = 'scope_stage_visible';
/** Which visualizer the now-playing scope stage shows. */ /** Which visualizer the now-playing scope stage shows. */
export type ScopeMode = 'spectrum' | 'scope'; export type ScopeMode = 'spectrum' | 'scope';
@@ -22,30 +23,39 @@ function parseScopeMode(value: string | null): ScopeMode {
return value === 'scope' ? 'scope' : 'spectrum'; return value === 'scope' ? 'scope' : 'spectrum';
} }
function parseBoolean(value: string | null): boolean {
return value === 'true';
}
interface SettingsStore { interface SettingsStore {
artistGroupingMode: ArtistGroupingMode; artistGroupingMode: ArtistGroupingMode;
scopeMode: ScopeMode; scopeMode: ScopeMode;
scopeStageVisible: boolean;
loaded: boolean; loaded: boolean;
load: () => Promise<void>; load: () => Promise<void>;
setArtistGroupingMode: (mode: ArtistGroupingMode) => Promise<void>; setArtistGroupingMode: (mode: ArtistGroupingMode) => Promise<void>;
setScopeMode: (mode: ScopeMode) => Promise<void>; setScopeMode: (mode: ScopeMode) => Promise<void>;
setScopeStageVisible: (visible: boolean) => Promise<void>;
} }
export const useSettingsStore = create<SettingsStore>((set, get) => ({ export const useSettingsStore = create<SettingsStore>((set, get) => ({
artistGroupingMode: 'astra', artistGroupingMode: 'astra',
scopeMode: 'spectrum', scopeMode: 'spectrum',
scopeStageVisible: false,
loaded: false, loaded: false,
load: async () => { load: async () => {
if (get().loaded) return; if (get().loaded) return;
const db = await openLibraryDb(); const db = await openLibraryDb();
const [grouping, scope] = await Promise.all([ const [grouping, scope, scopeStageVisible] = await Promise.all([
getSetting(db, ARTIST_GROUPING_KEY), getSetting(db, ARTIST_GROUPING_KEY),
getSetting(db, SCOPE_MODE_KEY), getSetting(db, SCOPE_MODE_KEY),
getSetting(db, SCOPE_STAGE_VISIBLE_KEY),
]); ]);
set({ set({
artistGroupingMode: parseGroupingMode(grouping), artistGroupingMode: parseGroupingMode(grouping),
scopeMode: parseScopeMode(scope), scopeMode: parseScopeMode(scope),
scopeStageVisible: parseBoolean(scopeStageVisible),
loaded: true, loaded: true,
}); });
}, },
@@ -63,4 +73,11 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
const db = await openLibraryDb(); const db = await openLibraryDb();
await setSetting(db, SCOPE_MODE_KEY, mode); await setSetting(db, SCOPE_MODE_KEY, mode);
}, },
setScopeStageVisible: async (visible) => {
if (get().scopeStageVisible === visible) return;
set({ scopeStageVisible: visible });
const db = await openLibraryDb();
await setSetting(db, SCOPE_STAGE_VISIBLE_KEY, visible ? 'true' : 'false');
},
})); }));