mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-18 19:54:26 +02:00
update nowplaying UI/UX
This commit is contained in:
+263
-166
@@ -42,13 +42,20 @@ const NARROW_CONTENT_SIDE_PADDING = spacing.md;
|
||||
const MEDIA_AREA_MIN = 220;
|
||||
const MEDIA_AREA_MAX = 360;
|
||||
const ART_SIZE_MAX = 340;
|
||||
const SCOPE_HEIGHT_MIN = 150;
|
||||
const SCOPE_HEIGHT_MAX = 220;
|
||||
const SCOPE_HEIGHT_RATIO = 9 / 16;
|
||||
const VISUALIZER_ART_SCALE = 0.8;
|
||||
const VISUALIZER_ART_SIZE_MIN = 176;
|
||||
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 CONTENT_TOP_PADDING = spacing.sm;
|
||||
const CONTENT_BOTTOM_PADDING = spacing.lg;
|
||||
const MEDIA_TOP_MARGIN = spacing.lg;
|
||||
const VISUALIZER_MEDIA_BOTTOM_GAP = spacing.sm;
|
||||
const MEDIA_BOTTOM_GAP = spacing.xl;
|
||||
const TRACK_INFO_ESTIMATE = 96;
|
||||
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 SKIP_ICON_SIZE = 32;
|
||||
const PLAY_ICON_SIZE = 34;
|
||||
const TRANSPORT_GAP = spacing.xxl;
|
||||
const TRANSPORT_TOP_MARGIN = spacing.lg;
|
||||
const SUB_BUTTON_SIZE = 40;
|
||||
const SUB_ICON_SIZE = 20;
|
||||
const SUB_TOP_MARGIN = spacing.lg;
|
||||
const MIN_FLOATING_SPACE = spacing.sm;
|
||||
const SECONDARY_CONTROLS_MIN_HEIGHT = 660;
|
||||
|
||||
interface NowPlayingLayout {
|
||||
contentPadding: number;
|
||||
contentWidth: number;
|
||||
mediaHeight: number;
|
||||
artSize: number;
|
||||
scopeWidth: number;
|
||||
scopeHeight: number;
|
||||
visualizerTopGap: number;
|
||||
visualizerBottomGap: number;
|
||||
mediaTopMargin: number;
|
||||
mediaBottomGap: number;
|
||||
showSecondaryControls: boolean;
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
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 =
|
||||
windowWidth < 360 ? NARROW_CONTENT_SIDE_PADDING : CONTENT_SIDE_PADDING;
|
||||
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 mediaMin = Math.min(mediaMax, MEDIA_AREA_MIN);
|
||||
const showSecondaryControls = availableHeight >= SECONDARY_CONTROLS_MIN_HEIGHT;
|
||||
const mediaTopMargin = availableHeight < 680 ? spacing.md : MEDIA_TOP_MARGIN;
|
||||
const mediaBottomGap = availableHeight < 680 ? spacing.lg : MEDIA_BOTTOM_GAP;
|
||||
const secondaryHeight = showSecondaryControls ? SUB_TOP_MARGIN + SUB_BUTTON_SIZE : 0;
|
||||
const defaultMediaBottomGap = availableHeight < 680 ? spacing.lg : MEDIA_BOTTOM_GAP;
|
||||
const mediaBottomGap = showVisualizer ? VISUALIZER_MEDIA_BOTTOM_GAP : defaultMediaBottomGap;
|
||||
const fixedHeight =
|
||||
CONTENT_TOP_PADDING +
|
||||
CONTENT_BOTTOM_PADDING +
|
||||
@@ -101,27 +120,37 @@ function getNowPlayingLayout(windowWidth: number, availableHeight: number): NowP
|
||||
WAVEFORM_BLOCK_ESTIMATE +
|
||||
TRANSPORT_TOP_MARGIN +
|
||||
PLAY_BUTTON_SIZE +
|
||||
secondaryHeight +
|
||||
SUB_TOP_MARGIN +
|
||||
SUB_BUTTON_SIZE +
|
||||
MIN_FLOATING_SPACE;
|
||||
const heightBoundMedia = availableHeight - fixedHeight;
|
||||
const mediaHeight = Math.round(clamp(heightBoundMedia, mediaMin, mediaMax));
|
||||
const artSize = Math.min(mediaHeight, ART_SIZE_MAX);
|
||||
const scopeWidth = contentWidth;
|
||||
const scopeHeight = Math.min(
|
||||
mediaHeight,
|
||||
Math.round(clamp(scopeWidth * SCOPE_HEIGHT_RATIO, SCOPE_HEIGHT_MIN, SCOPE_HEIGHT_MAX))
|
||||
const baseArtSize = Math.min(
|
||||
Math.round(clamp(heightBoundMedia, mediaMin, mediaMax)),
|
||||
ART_SIZE_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 {
|
||||
contentPadding,
|
||||
contentWidth,
|
||||
mediaHeight,
|
||||
artSize,
|
||||
scopeWidth,
|
||||
scopeHeight,
|
||||
visualizerTopGap,
|
||||
visualizerBottomGap,
|
||||
mediaTopMargin,
|
||||
mediaBottomGap,
|
||||
showSecondaryControls,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,9 +158,10 @@ export default function NowPlayingScreen() {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||
const [showScopeStage, setShowScopeStage] = useState(false);
|
||||
const [queueOpen, setQueueOpen] = useState(false);
|
||||
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 playbackState = usePlayerStore((s) => s.playbackState);
|
||||
const currentTime = usePlayerStore((s) => s.currentTime);
|
||||
@@ -144,7 +174,7 @@ export default function NowPlayingScreen() {
|
||||
const isPlaying = playbackState === 'playing';
|
||||
const isLoading = playbackState === 'loading';
|
||||
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';
|
||||
|
||||
// Swipe down to minimize. The stack transition is disabled for this route, so
|
||||
@@ -233,60 +263,24 @@ export default function NowPlayingScreen() {
|
||||
|
||||
{track ? (
|
||||
<View style={styles.player}>
|
||||
<Pressable
|
||||
onPress={() => setShowScopeStage((visible) => !visible)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={showScopeStage ? 'Show artwork' : 'Show visualizer'}
|
||||
<View
|
||||
style={[
|
||||
styles.mediaArea,
|
||||
styles.middleStack,
|
||||
{
|
||||
height: layout.mediaHeight,
|
||||
marginTop: layout.mediaTopMargin,
|
||||
marginBottom: layout.mediaBottomGap,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{showScopeStage ? (
|
||||
<View
|
||||
style={[
|
||||
styles.scopeSurface,
|
||||
{
|
||||
width: layout.scopeWidth,
|
||||
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
|
||||
style={[
|
||||
styles.artButton,
|
||||
{
|
||||
width: layout.artSize,
|
||||
height: layout.artSize,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.artCard,
|
||||
@@ -307,38 +301,111 @@ export default function NowPlayingScreen() {
|
||||
<AstraLogo size={Math.round(layout.artSize * 0.4)} />
|
||||
)}
|
||||
</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>
|
||||
|
||||
{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 style={styles.spacer} />
|
||||
|
||||
<View style={styles.playerControls}>
|
||||
<View style={styles.progressBlock}>
|
||||
<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.trackInfo}>
|
||||
<View style={styles.trackTextStack}>
|
||||
<Text variant="heading" numberOfLines={1} style={styles.trackTitle}>
|
||||
{track.title}
|
||||
</Text>
|
||||
<View style={styles.trackMetaRow}>
|
||||
<Text variant="body" numberOfLines={1} style={styles.artist}>
|
||||
{track.artist}
|
||||
</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>
|
||||
|
||||
<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}>
|
||||
<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
|
||||
name="play-skip-back"
|
||||
size={SKIP_ICON_SIZE}
|
||||
@@ -352,78 +419,67 @@ export default function NowPlayingScreen() {
|
||||
color={colors.bgPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable onPress={skipToNext} hitSlop={12}>
|
||||
<Pressable
|
||||
onPress={skipToNext}
|
||||
hitSlop={12}
|
||||
style={styles.transportMainBtn}
|
||||
>
|
||||
<Ionicons
|
||||
name="play-skip-forward"
|
||||
size={SKIP_ICON_SIZE}
|
||||
color={colors.textPrimary}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{layout.showSecondaryControls && (
|
||||
<View style={styles.subRow}>
|
||||
<Pressable
|
||||
hitSlop={10}
|
||||
style={styles.subBtn}
|
||||
onPress={() => void toggleShuffle()}
|
||||
accessibilityLabel="Shuffle"
|
||||
accessibilityState={{ selected: shuffle }}
|
||||
>
|
||||
<Ionicons
|
||||
name="shuffle"
|
||||
size={SUB_ICON_SIZE}
|
||||
color={shuffle ? colors.accent : colors.textTertiary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
hitSlop={10}
|
||||
style={styles.subBtn}
|
||||
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}
|
||||
color={isFavorite ? colors.accent : colors.textTertiary}
|
||||
/>
|
||||
</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 style={styles.subRow}>
|
||||
<Pressable
|
||||
hitSlop={10}
|
||||
style={styles.subBtn}
|
||||
onPress={() => void setScopeStageVisible(!scopeStageVisible)}
|
||||
accessibilityLabel={scopeStageVisible ? 'Hide visualizer' : 'Show visualizer'}
|
||||
accessibilityState={{ selected: scopeStageVisible }}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="sine-wave"
|
||||
size={SUB_ICON_SIZE + 2}
|
||||
color={scopeStageVisible ? colors.accent : colors.textTertiary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
hitSlop={10}
|
||||
style={styles.subBtn}
|
||||
onPress={() => setQueueOpen(true)}
|
||||
accessibilityLabel="Queue"
|
||||
>
|
||||
<Ionicons
|
||||
name="list-outline"
|
||||
size={SUB_ICON_SIZE + 2}
|
||||
color={colors.textTertiary}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
@@ -483,9 +539,12 @@ const styles = StyleSheet.create({
|
||||
player: {
|
||||
flex: 1,
|
||||
},
|
||||
mediaArea: {
|
||||
middleStack: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
artButton: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
artCard: {
|
||||
@@ -495,7 +554,8 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
scopeSurface: {
|
||||
scopeRail: {
|
||||
alignSelf: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
@@ -517,20 +577,45 @@ const styles = StyleSheet.create({
|
||||
height: '100%',
|
||||
},
|
||||
trackInfo: {
|
||||
alignItems: 'center',
|
||||
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: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
artist: {
|
||||
color: colors.accentText,
|
||||
marginTop: spacing.xs,
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
badges: {
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
progressBlock: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
spacer: {
|
||||
flex: 1,
|
||||
@@ -542,10 +627,21 @@ const styles = StyleSheet.create({
|
||||
transport: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: TRANSPORT_GAP,
|
||||
justifyContent: 'space-between',
|
||||
marginTop: TRANSPORT_TOP_MARGIN,
|
||||
},
|
||||
transportMainBtn: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
transportSideBtn: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
playButton: {
|
||||
width: PLAY_BUTTON_SIZE,
|
||||
height: PLAY_BUTTON_SIZE,
|
||||
@@ -557,7 +653,8 @@ const styles = StyleSheet.create({
|
||||
subRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'flex-end',
|
||||
gap: spacing.lg,
|
||||
marginTop: SUB_TOP_MARGIN,
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ArtistGroupingMode } from '@/library/artistGrouping';
|
||||
*/
|
||||
const ARTIST_GROUPING_KEY = 'artist_grouping_mode';
|
||||
const SCOPE_MODE_KEY = 'scope_mode';
|
||||
const SCOPE_STAGE_VISIBLE_KEY = 'scope_stage_visible';
|
||||
|
||||
/** Which visualizer the now-playing scope stage shows. */
|
||||
export type ScopeMode = 'spectrum' | 'scope';
|
||||
@@ -22,30 +23,39 @@ function parseScopeMode(value: string | null): ScopeMode {
|
||||
return value === 'scope' ? 'scope' : 'spectrum';
|
||||
}
|
||||
|
||||
function parseBoolean(value: string | null): boolean {
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
interface SettingsStore {
|
||||
artistGroupingMode: ArtistGroupingMode;
|
||||
scopeMode: ScopeMode;
|
||||
scopeStageVisible: boolean;
|
||||
loaded: boolean;
|
||||
load: () => Promise<void>;
|
||||
setArtistGroupingMode: (mode: ArtistGroupingMode) => Promise<void>;
|
||||
setScopeMode: (mode: ScopeMode) => Promise<void>;
|
||||
setScopeStageVisible: (visible: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
artistGroupingMode: 'astra',
|
||||
scopeMode: 'spectrum',
|
||||
scopeStageVisible: false,
|
||||
loaded: false,
|
||||
|
||||
load: async () => {
|
||||
if (get().loaded) return;
|
||||
const db = await openLibraryDb();
|
||||
const [grouping, scope] = await Promise.all([
|
||||
const [grouping, scope, scopeStageVisible] = await Promise.all([
|
||||
getSetting(db, ARTIST_GROUPING_KEY),
|
||||
getSetting(db, SCOPE_MODE_KEY),
|
||||
getSetting(db, SCOPE_STAGE_VISIBLE_KEY),
|
||||
]);
|
||||
set({
|
||||
artistGroupingMode: parseGroupingMode(grouping),
|
||||
scopeMode: parseScopeMode(scope),
|
||||
scopeStageVisible: parseBoolean(scopeStageVisible),
|
||||
loaded: true,
|
||||
});
|
||||
},
|
||||
@@ -63,4 +73,11 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
const db = await openLibraryDb();
|
||||
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');
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user