mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
better now playing UI consistency
This commit is contained in:
+256
-140
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Pressable, StyleSheet, useWindowDimensions } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@@ -32,13 +33,86 @@ const SUB_CONTROLS: { icon: IconName; label: string }[] = [
|
||||
|
||||
const DISMISS_DISTANCE = 140;
|
||||
const DISMISS_VELOCITY = 1000;
|
||||
const COMPACT_HEIGHT = 780;
|
||||
const TIGHT_HEIGHT = 700;
|
||||
|
||||
const MAX_CONTENT_WIDTH = 390;
|
||||
const MEDIA_AREA_MIN = 220;
|
||||
const MEDIA_AREA_MAX = 360;
|
||||
const ART_SIZE_MAX = 340;
|
||||
const HEADER_HEIGHT = 32;
|
||||
const CONTENT_TOP_PADDING = spacing.sm;
|
||||
const CONTENT_BOTTOM_PADDING = spacing.lg;
|
||||
const MEDIA_TOP_MARGIN = spacing.lg;
|
||||
const MEDIA_BOTTOM_GAP = spacing.xl;
|
||||
const TRACK_INFO_ESTIMATE = 96;
|
||||
const WAVEFORM_HEIGHT = 58;
|
||||
const WAVEFORM_TOUCH_PADDING = spacing.md;
|
||||
const WAVEFORM_BLOCK_ESTIMATE = WAVEFORM_HEIGHT + WAVEFORM_TOUCH_PADDING * 2 + 24;
|
||||
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;
|
||||
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 {
|
||||
const contentPadding = windowWidth < 360 ? spacing.lg : spacing.xl;
|
||||
const contentWidth = Math.max(0, Math.min(windowWidth - contentPadding * 2, MAX_CONTENT_WIDTH));
|
||||
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 fixedHeight =
|
||||
CONTENT_TOP_PADDING +
|
||||
CONTENT_BOTTOM_PADDING +
|
||||
HEADER_HEIGHT +
|
||||
mediaTopMargin +
|
||||
mediaBottomGap +
|
||||
TRACK_INFO_ESTIMATE +
|
||||
WAVEFORM_BLOCK_ESTIMATE +
|
||||
TRANSPORT_TOP_MARGIN +
|
||||
PLAY_BUTTON_SIZE +
|
||||
secondaryHeight +
|
||||
MIN_FLOATING_SPACE;
|
||||
const heightBoundMedia = availableHeight - fixedHeight;
|
||||
const mediaHeight = Math.round(clamp(heightBoundMedia, mediaMin, mediaMax));
|
||||
const artSize = Math.min(mediaHeight, ART_SIZE_MAX);
|
||||
|
||||
return {
|
||||
contentPadding,
|
||||
contentWidth,
|
||||
mediaHeight,
|
||||
artSize,
|
||||
mediaTopMargin,
|
||||
mediaBottomGap,
|
||||
showSecondaryControls,
|
||||
};
|
||||
}
|
||||
|
||||
export default function NowPlayingScreen() {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||
const [showScopeStage, setShowScopeStage] = useState(false);
|
||||
const track = usePlayerStore((s) => s.currentTrack);
|
||||
const playbackState = usePlayerStore((s) => s.playbackState);
|
||||
const currentTime = usePlayerStore((s) => s.currentTime);
|
||||
@@ -47,25 +121,7 @@ export default function NowPlayingScreen() {
|
||||
const isPlaying = playbackState === 'playing';
|
||||
const isLoading = playbackState === 'loading';
|
||||
const availableHeight = windowHeight - insets.top - insets.bottom;
|
||||
const isCompact = availableHeight < COMPACT_HEIGHT;
|
||||
const isTight = availableHeight < TIGHT_HEIGHT;
|
||||
const contentPadding = isTight ? spacing.lg : spacing.xl;
|
||||
const contentWidth = windowWidth - contentPadding * 2;
|
||||
const artSize = Math.round(
|
||||
Math.min(
|
||||
contentWidth,
|
||||
isTight ? 128 : isCompact ? 200 : 296,
|
||||
Math.max(isTight ? 96 : 144, availableHeight * (isTight ? 0.18 : isCompact ? 0.24 : 0.32))
|
||||
)
|
||||
);
|
||||
const visualizerHeight = isTight ? 58 : isCompact ? 76 : 96;
|
||||
const waveformHeight = isTight ? 42 : isCompact ? 50 : 58;
|
||||
const waveformTouchPadding = isTight ? spacing.xs : spacing.md;
|
||||
const playButtonSize = isTight ? 58 : isCompact ? 62 : 68;
|
||||
const skipIconSize = isTight ? 28 : 32;
|
||||
const playIconSize = isTight ? 30 : 34;
|
||||
const subButtonSize = isTight ? 36 : 40;
|
||||
const subIconSize = isTight ? 18 : 20;
|
||||
const layout = getNowPlayingLayout(windowWidth, availableHeight);
|
||||
const source = track?.album?.trim() ? track.album : 'Library';
|
||||
|
||||
// Swipe down to minimize. The stack transition is disabled for this route, so
|
||||
@@ -128,134 +184,167 @@ export default function NowPlayingScreen() {
|
||||
styles.content,
|
||||
contentStyle,
|
||||
{
|
||||
paddingHorizontal: contentPadding,
|
||||
paddingTop: insets.top + (isTight ? spacing.xs : spacing.sm),
|
||||
paddingBottom: insets.bottom + (isTight ? spacing.sm : spacing.lg),
|
||||
paddingHorizontal: layout.contentPadding,
|
||||
paddingTop: insets.top + CONTENT_TOP_PADDING,
|
||||
paddingBottom: insets.bottom + CONTENT_BOTTOM_PADDING,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<Pressable style={styles.headerBtn} onPress={() => dismissSheet()} hitSlop={12}>
|
||||
<Ionicons name="chevron-down" size={26} color={colors.textSecondary} />
|
||||
</Pressable>
|
||||
<View style={styles.headerMid}>
|
||||
<Text variant="caption" style={styles.eyebrow}>
|
||||
PLAYING FROM
|
||||
</Text>
|
||||
<Text variant="label" numberOfLines={1} style={styles.source}>
|
||||
{source}
|
||||
</Text>
|
||||
<View style={[styles.shell, { width: layout.contentWidth }]}>
|
||||
<View style={styles.header}>
|
||||
<Pressable style={styles.headerBtn} onPress={() => dismissSheet()} hitSlop={12}>
|
||||
<Ionicons name="chevron-down" size={26} color={colors.textSecondary} />
|
||||
</Pressable>
|
||||
<View style={styles.headerMid}>
|
||||
<Text variant="caption" style={styles.eyebrow}>
|
||||
PLAYING FROM
|
||||
</Text>
|
||||
<Text variant="label" numberOfLines={1} style={styles.source}>
|
||||
{source}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable style={styles.headerBtn} hitSlop={12} accessibilityLabel="More options">
|
||||
<Ionicons name="ellipsis-vertical" size={20} color={colors.textSecondary} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable style={styles.headerBtn} hitSlop={12} accessibilityLabel="More options">
|
||||
<Ionicons name="ellipsis-vertical" size={20} color={colors.textSecondary} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{track ? (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
styles.artWrap,
|
||||
{
|
||||
marginTop: isTight ? spacing.xs : spacing.lg,
|
||||
marginBottom: isTight ? spacing.xs : spacing.lg,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={[styles.art, { width: artSize, height: artSize }]}>
|
||||
{track.artworkData ? (
|
||||
<Image
|
||||
source={{ uri: track.artworkData }}
|
||||
style={styles.artImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
{track ? (
|
||||
<View style={styles.player}>
|
||||
<Pressable
|
||||
onPress={() => setShowScopeStage((visible) => !visible)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={showScopeStage ? 'Show artwork' : 'Show visualizer'}
|
||||
style={[
|
||||
styles.mediaArea,
|
||||
{
|
||||
height: layout.mediaHeight,
|
||||
marginTop: layout.mediaTopMargin,
|
||||
marginBottom: layout.mediaBottomGap,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{showScopeStage ? (
|
||||
<View
|
||||
style={[
|
||||
styles.scopeSurface,
|
||||
{
|
||||
width: layout.contentWidth,
|
||||
height: layout.mediaHeight,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Visualizer
|
||||
width={layout.contentWidth}
|
||||
height={layout.mediaHeight}
|
||||
interactive={false}
|
||||
showChrome={false}
|
||||
mode="spectrum"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<AstraLogo size={Math.round(artSize * 0.4)} />
|
||||
<View
|
||||
style={[
|
||||
styles.artCard,
|
||||
{
|
||||
width: layout.artSize,
|
||||
height: layout.artSize,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{track.artworkData ? (
|
||||
<Image
|
||||
key={track.id}
|
||||
source={{ uri: track.artworkData }}
|
||||
style={styles.artImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<View style={styles.transport}>
|
||||
<Pressable onPress={skipToPrevious} hitSlop={12}>
|
||||
<Ionicons
|
||||
name="play-skip-back"
|
||||
size={SKIP_ICON_SIZE}
|
||||
color={colors.textPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable onPress={togglePlay} hitSlop={12} style={styles.playButton}>
|
||||
<Ionicons
|
||||
name={isLoading ? 'ellipsis-horizontal' : isPlaying ? 'pause' : 'play'}
|
||||
size={PLAY_ICON_SIZE}
|
||||
color={colors.bgPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable onPress={skipToNext} hitSlop={12}>
|
||||
<Ionicons
|
||||
name="play-skip-forward"
|
||||
size={SKIP_ICON_SIZE}
|
||||
color={colors.textPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{layout.showSecondaryControls && (
|
||||
<View style={styles.subRow}>
|
||||
{SUB_CONTROLS.map((c) => (
|
||||
<Pressable
|
||||
key={c.label}
|
||||
hitSlop={10}
|
||||
style={styles.subBtn}
|
||||
accessibilityLabel={c.label}
|
||||
>
|
||||
<Ionicons
|
||||
name={c.icon}
|
||||
size={SUB_ICON_SIZE}
|
||||
color={colors.textTertiary}
|
||||
/>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Visualizer width={contentWidth} height={visualizerHeight} />
|
||||
|
||||
<View style={[styles.trackInfo, { marginTop: isTight ? spacing.xs : spacing.md }]}>
|
||||
<Text variant="heading" numberOfLines={2} style={styles.centered}>
|
||||
{track.title}
|
||||
) : (
|
||||
<View style={styles.empty}>
|
||||
<Text variant="heading">Nothing playing</Text>
|
||||
<Text variant="body" color={colors.textSecondary} style={styles.centered}>
|
||||
Start a track from Home.
|
||||
</Text>
|
||||
<Text variant="body" numberOfLines={1} style={[styles.centered, styles.artist]}>
|
||||
{track.artist}
|
||||
</Text>
|
||||
<View style={[styles.badges, { marginTop: isTight ? spacing.sm : spacing.md }]}>
|
||||
<FormatBadges track={track} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.progressBlock, { marginTop: isTight ? spacing.sm : spacing.lg }]}>
|
||||
<WaveformSeekBar
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
height={waveformHeight}
|
||||
touchPadding={waveformTouchPadding}
|
||||
trackKey={track.id}
|
||||
trackPath={track.path}
|
||||
onSeek={(seconds) => void seekTo(seconds)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[styles.spacer, { minHeight: isTight ? spacing.xs : spacing.md }]} />
|
||||
|
||||
<View style={[styles.transport, { gap: isTight ? spacing.xl : spacing.xxl }]}>
|
||||
<Pressable onPress={skipToPrevious} hitSlop={12}>
|
||||
<Ionicons name="play-skip-back" size={skipIconSize} color={colors.textPrimary} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={togglePlay}
|
||||
hitSlop={12}
|
||||
style={[styles.playButton, { width: playButtonSize, height: playButtonSize }]}
|
||||
>
|
||||
<Ionicons
|
||||
name={isLoading ? 'ellipsis-horizontal' : isPlaying ? 'pause' : 'play'}
|
||||
size={playIconSize}
|
||||
color={colors.bgPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable onPress={skipToNext} hitSlop={12}>
|
||||
<Ionicons
|
||||
name="play-skip-forward"
|
||||
size={skipIconSize}
|
||||
color={colors.textPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.subRow,
|
||||
{
|
||||
marginTop: isTight ? spacing.sm : spacing.lg,
|
||||
paddingHorizontal: isTight ? 0 : spacing.sm,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{SUB_CONTROLS.map((c) => (
|
||||
<Pressable
|
||||
key={c.label}
|
||||
hitSlop={10}
|
||||
style={[styles.subBtn, { width: subButtonSize, height: subButtonSize }]}
|
||||
accessibilityLabel={c.label}
|
||||
>
|
||||
<Ionicons name={c.icon} size={subIconSize} color={colors.textTertiary} />
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.empty}>
|
||||
<Text variant="heading">Nothing playing</Text>
|
||||
<Text variant="body" color={colors.textSecondary} style={styles.centered}>
|
||||
Start a track from Home.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
@@ -270,8 +359,13 @@ const styles = StyleSheet.create({
|
||||
content: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.bgPrimary,
|
||||
alignItems: 'center',
|
||||
},
|
||||
shell: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
height: HEADER_HEIGHT,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
@@ -295,23 +389,32 @@ const styles = StyleSheet.create({
|
||||
color: colors.textSecondary,
|
||||
marginTop: 1,
|
||||
},
|
||||
artWrap: {
|
||||
player: {
|
||||
flex: 1,
|
||||
},
|
||||
mediaArea: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
art: {
|
||||
artCard: {
|
||||
borderRadius: radius.lg,
|
||||
backgroundColor: colors.bgTertiary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
scopeSurface: {
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
artImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
trackInfo: {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
},
|
||||
centered: {
|
||||
textAlign: 'center',
|
||||
@@ -321,18 +424,27 @@ const styles = StyleSheet.create({
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
badges: {
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
progressBlock: {
|
||||
},
|
||||
spacer: {
|
||||
flex: 1,
|
||||
minHeight: MIN_FLOATING_SPACE,
|
||||
},
|
||||
playerControls: {
|
||||
width: '100%',
|
||||
},
|
||||
transport: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: TRANSPORT_GAP,
|
||||
marginTop: TRANSPORT_TOP_MARGIN,
|
||||
},
|
||||
playButton: {
|
||||
width: PLAY_BUTTON_SIZE,
|
||||
height: PLAY_BUTTON_SIZE,
|
||||
borderRadius: radius.pill,
|
||||
backgroundColor: colors.accent,
|
||||
alignItems: 'center',
|
||||
@@ -342,8 +454,12 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: SUB_TOP_MARGIN,
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
subBtn: {
|
||||
width: SUB_BUTTON_SIZE,
|
||||
height: SUB_BUTTON_SIZE,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
@@ -12,32 +12,47 @@ const POINTS = 120;
|
||||
|
||||
type Mode = 'spectrum' | 'scope';
|
||||
|
||||
interface VisualizerProps {
|
||||
width: number;
|
||||
height?: number;
|
||||
interactive?: boolean;
|
||||
showChrome?: boolean;
|
||||
mode?: Mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline visualizer for the now-playing screen — no card chrome, it just lives
|
||||
* in the layout. Tap anywhere on it to switch between the live filled-line
|
||||
* Spectrum and the Scope (oscilloscope, placeholder until its native path lands).
|
||||
* Visualizer for the now-playing screen. By default it remains an inline
|
||||
* interactive component; inside the media stage it can render chrome-free so
|
||||
* the parent owns artwork/scope switching without moving the surrounding UI.
|
||||
*/
|
||||
export function Visualizer({ width, height = CANVAS_HEIGHT }: { width: number; height?: number }) {
|
||||
const [mode, setMode] = useState<Mode>('spectrum');
|
||||
export function Visualizer({
|
||||
width,
|
||||
height = CANVAS_HEIGHT,
|
||||
interactive = true,
|
||||
showChrome = true,
|
||||
mode: controlledMode,
|
||||
}: VisualizerProps) {
|
||||
const [uncontrolledMode, setUncontrolledMode] = useState<Mode>('spectrum');
|
||||
const mode = controlledMode ?? uncontrolledMode;
|
||||
const scopeActive = useScopeActive();
|
||||
const spectrumActive = scopeActive && mode === 'spectrum';
|
||||
const values = useSpectrumCurve(POINTS, spectrumActive);
|
||||
|
||||
const toggle = () => setMode((m) => (m === 'spectrum' ? 'scope' : 'spectrum'));
|
||||
const toggle = () => {
|
||||
if (controlledMode) return;
|
||||
setUncontrolledMode((m) => (m === 'spectrum' ? 'scope' : 'spectrum'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={toggle}
|
||||
style={[styles.wrap, { width }]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Visualizer showing ${mode}. Tap to switch.`}
|
||||
>
|
||||
<View style={styles.caption}>
|
||||
<Text variant="caption" style={styles.captionText}>
|
||||
{mode === 'spectrum' ? 'SPECTRUM' : 'SCOPE'}
|
||||
</Text>
|
||||
<Ionicons name="swap-horizontal" size={14} color={colors.textTertiary} />
|
||||
</View>
|
||||
const content = (
|
||||
<>
|
||||
{showChrome && (
|
||||
<View style={styles.caption}>
|
||||
<Text variant="caption" style={styles.captionText}>
|
||||
{mode === 'spectrum' ? 'SPECTRUM' : 'SCOPE'}
|
||||
</Text>
|
||||
<Ionicons name="swap-horizontal" size={14} color={colors.textTertiary} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={{ width, height }}>
|
||||
{mode === 'spectrum' ? (
|
||||
@@ -51,6 +66,25 @@ export function Visualizer({ width, height = CANVAS_HEIGHT }: { width: number; h
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!interactive) {
|
||||
return (
|
||||
<View pointerEvents="none" style={[styles.stageWrap, { width, height }]}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={toggle}
|
||||
style={[styles.wrap, { width }]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Visualizer showing ${mode}. Tap to switch.`}
|
||||
>
|
||||
{content}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
@@ -59,6 +93,9 @@ const styles = StyleSheet.create({
|
||||
wrap: {
|
||||
paddingVertical: spacing.xs,
|
||||
},
|
||||
stageWrap: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
caption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
Reference in New Issue
Block a user