fix layout issues

This commit is contained in:
Boof2015
2026-07-01 17:56:15 -04:00
parent 76a5183dfb
commit 7f00e87823
4 changed files with 280 additions and 114 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Astra", "name": "Astra",
"slug": "astra-mobile", "slug": "astra-mobile",
"version": "0.1.0", "version": "0.1.0",
"orientation": "portrait", "orientation": "default",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "astra", "scheme": "astra",
"userInterfaceStyle": "dark", "userInterfaceStyle": "dark",
+132 -65
View File
@@ -1,7 +1,8 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { Pressable, StyleSheet, View } from 'react-native'; import { Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect } from 'expo-router'; import { useFocusEffect } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as DocumentPicker from 'expo-document-picker'; import * as DocumentPicker from 'expo-document-picker';
import { readAsStringAsync } from 'expo-file-system/legacy'; import { readAsStringAsync } from 'expo-file-system/legacy';
import { Screen } from '@/components/Screen'; import { Screen } from '@/components/Screen';
@@ -15,6 +16,7 @@ import { EQValueEditSheet } from '@/components/eq/EQValueEditSheet';
import { PresetSheet } from '@/components/eq/PresetSheet'; import { PresetSheet } from '@/components/eq/PresetSheet';
import { SavePresetSheet } from '@/components/eq/SavePresetSheet'; import { SavePresetSheet } from '@/components/eq/SavePresetSheet';
import { colors, radius, spacing } from '@/theme'; import { colors, radius, spacing } from '@/theme';
import { isWideWindow } from '@/theme/adaptive';
import { useEQStore } from '@/stores/eqStore'; import { useEQStore } from '@/stores/eqStore';
import { useScopeActive } from '@/scope/scopeStore'; import { useScopeActive } from '@/scope/scopeStore';
import { setActivePostEqNative } from '@/audio/eqNative'; import { setActivePostEqNative } from '@/audio/eqNative';
@@ -44,6 +46,12 @@ export default function EQScreen() {
const [sheet, setSheet] = useState<SheetKind>('none'); const [sheet, setSheet] = useState<SheetKind>('none');
const [editingValue, setEditingValue] = useState<EQEditableValue | null>(null); const [editingValue, setEditingValue] = useState<EQEditableValue | null>(null);
const closeSheet = useCallback(() => setSheet('none'), []); const closeSheet = useCallback(() => setSheet('none'), []);
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const insets = useSafeAreaInsets();
const availableWidth = windowWidth - insets.left - insets.right;
const isWide = isWideWindow(availableWidth, windowHeight - insets.top - insets.bottom);
// Editing pane keeps a phone-ish width; the graph gets everything else.
const sidePaneWidth = Math.min(360, Math.max(280, Math.round(availableWidth * 0.4)));
// Gate the post-EQ tap to while this screen is visible. // Gate the post-EQ tap to while this screen is visible.
useFocusEffect( useFocusEffect(
@@ -77,9 +85,85 @@ export default function EQScreen() {
} }
}; };
const presetRowEl = (
<Pressable
style={[styles.presetRow, isWide && styles.sideItem]}
onPress={() => setSheet('preset')}
>
<Text variant="body" color={colors.textPrimary}>
{presetName}
</Text>
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
</Pressable>
);
const graphEl = (
<EQGraph
bands={eq.bands}
activeBandId={eq.activeBandId}
enabled={eq.enabled}
spectrumActive={scopeActive && focused}
onSelectBand={eq.selectBand}
onChangeBand={(id, updates) => eq.updateBand(id, updates)}
/>
);
const stripEl = (
<BandStrip
bands={eq.bands}
activeBandId={eq.activeBandId}
canAdd={eq.bands.length < EQ_MAX_BANDS}
onSelect={eq.selectBand}
onAdd={() => eq.addBand()}
/>
);
const detailEl = (
<BandDetailPanel
band={activeBand}
bandNumber={activeBandNumber > 0 ? activeBandNumber : 1}
onUpdate={(updates) => activeBand && eq.updateBand(activeBand.id, updates)}
onEditType={() => setSheet('type')}
onEditValue={setEditingValue}
/>
);
const bottomBarEl = (
<View style={[styles.bottomBar, isWide && styles.bottomBarWide]}>
<View style={styles.preamp}>
<EQSlider
label="Preamp"
value={eq.preamp}
min={EQ_MIN_PREAMP_DB}
max={EQ_MAX_PREAMP_DB}
format={(v) => `${formatGain(v)} dB`}
onChange={eq.setPreamp}
/>
</View>
<Pressable
style={[styles.eqToggle, eq.enabled && styles.eqToggleOn]}
onPress={eq.toggleEnabled}
>
<Ionicons
name="power"
size={16}
color={eq.enabled ? colors.accentTextStrong : colors.textSecondary}
/>
<Text variant="label" color={eq.enabled ? colors.accentTextStrong : colors.textSecondary}>
{eq.enabled ? 'EQ on' : 'EQ off'}
</Text>
</Pressable>
</View>
);
return ( return (
<Screen padded={false}> <Screen padded={false}>
<View style={styles.header}> <View
style={[
styles.header,
{ paddingLeft: spacing.lg + insets.left, paddingRight: spacing.lg + insets.right },
]}
>
<Text variant="heading">Equalizer</Text> <Text variant="heading">Equalizer</Text>
<View style={styles.headerActions}> <View style={styles.headerActions}>
<Pressable style={styles.iconButton} onPress={() => setSheet('save')} hitSlop={8}> <Pressable style={styles.iconButton} onPress={() => setSheet('save')} hitSlop={8}>
@@ -91,69 +175,31 @@ export default function EQScreen() {
</View> </View>
</View> </View>
<Pressable style={styles.presetRow} onPress={() => setSheet('preset')}> {isWide ? (
<Text variant="body" color={colors.textPrimary}> <View
{presetName} style={[
</Text> styles.wideBody,
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} /> { paddingLeft: spacing.lg + insets.left, paddingRight: spacing.lg + insets.right },
</Pressable> ]}
<View style={styles.graphWrap}>
<EQGraph
bands={eq.bands}
activeBandId={eq.activeBandId}
enabled={eq.enabled}
spectrumActive={scopeActive && focused}
onSelectBand={eq.selectBand}
onChangeBand={(id, updates) => eq.updateBand(id, updates)}
/>
</View>
<View style={styles.section}>
<BandStrip
bands={eq.bands}
activeBandId={eq.activeBandId}
canAdd={eq.bands.length < EQ_MAX_BANDS}
onSelect={eq.selectBand}
onAdd={() => eq.addBand()}
/>
</View>
<View style={styles.section}>
<BandDetailPanel
band={activeBand}
bandNumber={activeBandNumber > 0 ? activeBandNumber : 1}
onUpdate={(updates) => activeBand && eq.updateBand(activeBand.id, updates)}
onEditType={() => setSheet('type')}
onEditValue={setEditingValue}
/>
</View>
<View style={styles.bottomBar}>
<View style={styles.preamp}>
<EQSlider
label="Preamp"
value={eq.preamp}
min={EQ_MIN_PREAMP_DB}
max={EQ_MAX_PREAMP_DB}
format={(v) => `${formatGain(v)} dB`}
onChange={eq.setPreamp}
/>
</View>
<Pressable
style={[styles.eqToggle, eq.enabled && styles.eqToggleOn]}
onPress={eq.toggleEnabled}
> >
<Ionicons <View style={styles.wideGraphPane}>{graphEl}</View>
name="power" <View style={{ width: sidePaneWidth }}>
size={16} {presetRowEl}
color={eq.enabled ? colors.accentTextStrong : colors.textSecondary} {stripEl}
/> <View style={styles.sideDetail}>{detailEl}</View>
<Text variant="label" color={eq.enabled ? colors.accentTextStrong : colors.textSecondary}> <View style={styles.wideSpacer} />
{eq.enabled ? 'EQ on' : 'EQ off'} {bottomBarEl}
</Text> </View>
</Pressable> </View>
</View> ) : (
<>
{presetRowEl}
<View style={styles.graphWrap}>{graphEl}</View>
<View style={styles.section}>{stripEl}</View>
<View style={styles.section}>{detailEl}</View>
{bottomBarEl}
</>
)}
{sheet === 'preset' ? ( {sheet === 'preset' ? (
<PresetSheet <PresetSheet
@@ -311,7 +357,6 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: spacing.lg,
paddingTop: spacing.md, paddingTop: spacing.md,
paddingBottom: spacing.sm, paddingBottom: spacing.sm,
}, },
@@ -349,6 +394,28 @@ const styles = StyleSheet.create({
marginHorizontal: spacing.lg, marginHorizontal: spacing.lg,
marginTop: spacing.md, marginTop: spacing.md,
}, },
wideBody: {
flex: 1,
flexDirection: 'row',
gap: spacing.lg,
paddingBottom: spacing.sm,
},
wideGraphPane: {
flex: 1,
minWidth: 0,
},
wideSpacer: {
flex: 1,
},
sideItem: {
marginHorizontal: 0,
},
sideDetail: {
marginTop: spacing.md,
},
bottomBarWide: {
paddingHorizontal: 0,
},
bottomBar: { bottomBar: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
+136 -48
View File
@@ -23,6 +23,7 @@ import { Visualizer } from '@/components/Visualizer';
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet'; import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
import { QueueTray } from '@/components/queue/QueueTray'; import { QueueTray } from '@/components/queue/QueueTray';
import { colors, radius, spacing } from '@/theme'; import { colors, radius, spacing } from '@/theme';
import { WIDE_MIN_WIDTH, isWideWindow } from '@/theme/adaptive';
import { motion } from '@/theme/motion'; import { motion } from '@/theme/motion';
import { resolveCanonicalBrowseArtist, resolveStrictBrowseArtist } from '@/library/artistGrouping'; import { resolveCanonicalBrowseArtist, resolveStrictBrowseArtist } from '@/library/artistGrouping';
import { useLibraryStore } from '@/stores/libraryStore'; import { useLibraryStore } from '@/stores/libraryStore';
@@ -46,10 +47,17 @@ const MAX_CONTENT_WIDTH = 408;
const CONTENT_SIDE_PADDING = spacing.lg; const CONTENT_SIDE_PADDING = spacing.lg;
const NARROW_CONTENT_SIDE_PADDING = spacing.md; const NARROW_CONTENT_SIDE_PADDING = spacing.md;
const MEDIA_AREA_MIN = 220; const MEDIA_AREA_MIN = 220;
const MEDIA_AREA_MAX = 360; // Tablet-portrait tier: tall windows >= WIDE_MIN_WIDTH keep the single column but grow it.
const ART_SIZE_MAX = 340; const TABLET_MAX_CONTENT_WIDTH = 520;
const VISUALIZER_ART_SCALE = 0.8; const TABLET_ART_SIZE_MAX = 440;
const VISUALIZER_ART_SIZE_MIN = 176; // Wide (landscape/desktop) tier: two panes, art left, controls right.
const WIDE_MAX_CONTENT_WIDTH = 960;
const WIDE_PANE_GAP = spacing.xxl;
const WIDE_RIGHT_PANE_MIN = 300;
const WIDE_RIGHT_PANE_MAX = MAX_CONTENT_WIDTH;
const WIDE_ART_SIZE_MAX = 400;
const WIDE_ART_SIZE_MIN = 160;
const WIDE_COMPACT_HEIGHT = 480;
const VISUALIZER_WIDTH_MAX = 448; const VISUALIZER_WIDTH_MAX = 448;
const VISUALIZER_SIDE_PADDING = spacing.md; const VISUALIZER_SIDE_PADDING = spacing.md;
const VISUALIZER_TOP_GAP = spacing.lg; const VISUALIZER_TOP_GAP = spacing.lg;
@@ -61,7 +69,6 @@ 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;
@@ -80,8 +87,15 @@ const MENU_ANIMATION_OUT_MS = 100;
const MENU_ENTER_OFFSET_Y = -8; const MENU_ENTER_OFFSET_Y = -8;
interface NowPlayingLayout { interface NowPlayingLayout {
isWide: boolean;
contentPadding: number; contentPadding: number;
contentWidth: number; contentWidth: number;
leftPaneWidth: number;
rightPaneWidth: number;
controlsGap: number;
trackInfoGap: number;
waveformHeight: number;
mediaStackHeight: number;
artSize: number; artSize: number;
scopeWidth: number; scopeWidth: number;
scopeHeight: number; scopeHeight: number;
@@ -109,29 +123,76 @@ function getScopeHeight(scopeWidth: number): number {
} }
function getNowPlayingLayout( function getNowPlayingLayout(
windowWidth: number, availableWidth: number,
availableHeight: number, availableHeight: number,
showVisualizer: boolean showVisualizer: boolean
): NowPlayingLayout { ): NowPlayingLayout {
const isWide = isWideWindow(availableWidth, availableHeight);
if (isWide) {
const contentPadding = CONTENT_SIDE_PADDING;
const contentWidth = Math.max(
0,
Math.min(availableWidth - contentPadding * 2, WIDE_MAX_CONTENT_WIDTH)
);
const rightPaneWidth = Math.round(
clamp(contentWidth * 0.46, WIDE_RIGHT_PANE_MIN, WIDE_RIGHT_PANE_MAX)
);
const leftPaneWidth = Math.max(0, contentWidth - WIDE_PANE_GAP - rightPaneWidth);
const scopeWidth = Math.min(leftPaneWidth, VISUALIZER_WIDTH_MAX);
const scopeHeight = getScopeHeight(scopeWidth);
const visualizerTopGap = showVisualizer ? VISUALIZER_TOP_GAP : 0;
const verticalBudget =
availableHeight - CONTENT_TOP_PADDING - CONTENT_BOTTOM_PADDING - HEADER_HEIGHT - spacing.md;
const artHeightBudget = verticalBudget - (showVisualizer ? scopeHeight + visualizerTopGap : 0);
const artSize = Math.round(
clamp(Math.min(leftPaneWidth, artHeightBudget), WIDE_ART_SIZE_MIN, WIDE_ART_SIZE_MAX)
);
const controlsGap = availableHeight < WIDE_COMPACT_HEIGHT ? spacing.sm : spacing.lg;
return {
isWide: true,
contentPadding,
contentWidth,
leftPaneWidth,
rightPaneWidth,
controlsGap,
trackInfoGap: spacing.md,
waveformHeight: WAVEFORM_HEIGHT,
mediaStackHeight: showVisualizer
? artSize + visualizerTopGap + scopeHeight
: artSize,
artSize,
scopeWidth,
scopeHeight,
visualizerTopGap,
visualizerBottomGap: 0,
mediaTopMargin: 0,
mediaBottomGap: 0,
};
}
// Tall windows: single column. Tablet-width ones get a larger column and art cap.
const isTabletColumn = availableWidth >= WIDE_MIN_WIDTH;
const contentPadding = const contentPadding =
windowWidth < 360 ? NARROW_CONTENT_SIDE_PADDING : CONTENT_SIDE_PADDING; availableWidth < 360 ? NARROW_CONTENT_SIDE_PADDING : CONTENT_SIDE_PADDING;
const contentWidth = Math.max(0, Math.min(windowWidth - contentPadding * 2, MAX_CONTENT_WIDTH)); const maxContentWidth = isTabletColumn ? TABLET_MAX_CONTENT_WIDTH : MAX_CONTENT_WIDTH;
const contentWidth = Math.max(0, Math.min(availableWidth - contentPadding * 2, maxContentWidth));
const scopeWidth = Math.max( const scopeWidth = Math.max(
0, 0,
Math.min(windowWidth - VISUALIZER_SIDE_PADDING * 2, VISUALIZER_WIDTH_MAX) Math.min(availableWidth - VISUALIZER_SIDE_PADDING * 2, VISUALIZER_WIDTH_MAX)
); );
const scopeHeight = getScopeHeight(scopeWidth); const scopeHeight = getScopeHeight(scopeWidth);
const mediaMax = Math.min(contentWidth, MEDIA_AREA_MAX); // Art may grow to the full column width when the height budget allows it.
const mediaMax = Math.min(contentWidth, isTabletColumn ? TABLET_ART_SIZE_MAX : contentWidth);
const mediaMin = Math.min(mediaMax, MEDIA_AREA_MIN); const mediaMin = Math.min(mediaMax, MEDIA_AREA_MIN);
const mediaTopMargin = availableHeight < 680 ? spacing.md : MEDIA_TOP_MARGIN; const mediaTopMargin = availableHeight < 680 ? spacing.md : MEDIA_TOP_MARGIN;
const defaultMediaBottomGap = availableHeight < 680 ? spacing.lg : MEDIA_BOTTOM_GAP; const defaultMediaBottomGap = availableHeight < 680 ? spacing.lg : MEDIA_BOTTOM_GAP;
const mediaBottomGap = showVisualizer ? VISUALIZER_MEDIA_BOTTOM_GAP : defaultMediaBottomGap; const mediaBottomGap = defaultMediaBottomGap;
const fixedHeight = const fixedHeightBase =
CONTENT_TOP_PADDING + CONTENT_TOP_PADDING +
CONTENT_BOTTOM_PADDING + CONTENT_BOTTOM_PADDING +
HEADER_HEIGHT + HEADER_HEIGHT +
mediaTopMargin + mediaTopMargin +
mediaBottomGap +
TRACK_INFO_ESTIMATE + TRACK_INFO_ESTIMATE +
WAVEFORM_BLOCK_ESTIMATE + WAVEFORM_BLOCK_ESTIMATE +
TRANSPORT_TOP_MARGIN + TRANSPORT_TOP_MARGIN +
@@ -139,27 +200,38 @@ function getNowPlayingLayout(
SUB_TOP_MARGIN + SUB_TOP_MARGIN +
SUB_BUTTON_SIZE + SUB_BUTTON_SIZE +
MIN_FLOATING_SPACE; MIN_FLOATING_SPACE;
const heightBoundMedia = availableHeight - fixedHeight; // The Math.max(96, ...) floor lets art shrink below MEDIA_AREA_MIN in squat
const baseArtSize = Math.min( // windows (split-screen halves) instead of pushing the controls off-screen.
Math.round(clamp(heightBoundMedia, mediaMin, mediaMax)), const bound = availableHeight - fixedHeightBase - mediaBottomGap;
ART_SIZE_MAX const scopeOffArt = Math.round(
clamp(bound, Math.min(mediaMin, Math.max(96, bound)), mediaMax)
); );
const artSize = showVisualizer // Roomy screens get a taller waveform; the rest of the spare space is
? Math.round( // distributed between the control rows by flex (space-between), so no
clamp( // height estimate error can pool as one gap above the controls.
baseArtSize * VISUALIZER_ART_SCALE, const offSurplus = Math.max(0, bound - scopeOffArt);
Math.min(VISUALIZER_ART_SIZE_MIN, mediaMax), const stretchUnit = Math.min(Math.floor(offSurplus / 5), spacing.md);
mediaMax const waveformHeight = WAVEFORM_HEIGHT + stretchUnit * 2;
) // The media stack keeps one locked height in both scope states — the scope
) // steals its space from the art alone, so toggling it moves nothing else.
: baseArtSize; const scopeBlockHeight = VISUALIZER_TOP_GAP + scopeHeight + VISUALIZER_BOTTOM_GAP;
const mediaStackHeight = Math.max(scopeOffArt, 96 + scopeBlockHeight);
const scopeOnArt = mediaStackHeight - scopeBlockHeight;
const artSize = showVisualizer ? scopeOnArt : scopeOffArt;
const visualizerTopGap = showVisualizer ? VISUALIZER_TOP_GAP : 0; const visualizerTopGap = showVisualizer ? VISUALIZER_TOP_GAP : 0;
const visualizerBottomGap = showVisualizer ? VISUALIZER_BOTTOM_GAP : 0; const visualizerBottomGap = showVisualizer ? VISUALIZER_BOTTOM_GAP : 0;
return { return {
isWide: false,
contentPadding, contentPadding,
contentWidth, contentWidth,
leftPaneWidth: contentWidth,
rightPaneWidth: contentWidth,
controlsGap: TRANSPORT_TOP_MARGIN,
trackInfoGap: spacing.md,
waveformHeight,
mediaStackHeight,
artSize, artSize,
scopeWidth, scopeWidth,
scopeHeight, scopeHeight,
@@ -194,9 +266,13 @@ 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, scopeStageVisible); const effectiveWidth = windowWidth - insets.left - insets.right;
const layout = getNowPlayingLayout(effectiveWidth, availableHeight, scopeStageVisible);
const source = track?.album?.trim() ? track.album : 'Library'; const source = track?.album?.trim() ? track.album : 'Library';
const shellRight = Math.max(layout.contentPadding, (windowWidth - layout.contentWidth) / 2); const shellRight =
insets.right +
layout.contentPadding +
Math.max(0, (effectiveWidth - layout.contentPadding * 2 - layout.contentWidth) / 2);
const menuTop = insets.top + CONTENT_TOP_PADDING + HEADER_HEIGHT + spacing.xs; const menuTop = insets.top + CONTENT_TOP_PADDING + HEADER_HEIGHT + spacing.xs;
const libraryTrack = useMemo( const libraryTrack = useMemo(
() => (track ? libraryTracks.find((entry) => entry.path === track.path) ?? null : null), () => (track ? libraryTracks.find((entry) => entry.path === track.path) ?? null : null),
@@ -350,7 +426,8 @@ export default function NowPlayingScreen() {
styles.content, styles.content,
contentStyle, contentStyle,
{ {
paddingHorizontal: layout.contentPadding, paddingLeft: insets.left + layout.contentPadding,
paddingRight: insets.right + layout.contentPadding,
paddingTop: insets.top + CONTENT_TOP_PADDING, paddingTop: insets.top + CONTENT_TOP_PADDING,
paddingBottom: insets.bottom + CONTENT_BOTTOM_PADDING, paddingBottom: insets.bottom + CONTENT_BOTTOM_PADDING,
}, },
@@ -381,14 +458,17 @@ export default function NowPlayingScreen() {
</View> </View>
{track ? ( {track ? (
<View style={styles.player}> <View style={[styles.player, layout.isWide && styles.playerWide]}>
<View <View
style={[ style={[
styles.middleStack, styles.middleStack,
{ layout.isWide
marginTop: layout.mediaTopMargin, ? { width: layout.leftPaneWidth, justifyContent: 'center' }
marginBottom: layout.mediaBottomGap, : {
}, height: layout.mediaStackHeight,
marginTop: layout.mediaTopMargin,
marginBottom: layout.mediaBottomGap,
},
]} ]}
> >
<View <View
@@ -464,10 +544,15 @@ export default function NowPlayingScreen() {
)} )}
</View> </View>
<View style={styles.spacer} /> <View
style={[
<View style={styles.playerControls}> styles.playerControls,
<View style={styles.trackInfo}> layout.isWide
? { width: layout.rightPaneWidth }
: styles.playerControlsFill,
]}
>
<View style={[styles.trackInfo, { marginBottom: layout.trackInfoGap }]}>
<View style={styles.trackTextStack}> <View style={styles.trackTextStack}>
<MarqueeText <MarqueeText
variant="heading" variant="heading"
@@ -508,14 +593,14 @@ export default function NowPlayingScreen() {
<WaveformSeekBar <WaveformSeekBar
currentTime={currentTime} currentTime={currentTime}
duration={duration} duration={duration}
height={WAVEFORM_HEIGHT} height={layout.waveformHeight}
touchPadding={WAVEFORM_TOUCH_PADDING} touchPadding={WAVEFORM_TOUCH_PADDING}
trackKey={track.id} trackKey={track.id}
trackPath={track.path} trackPath={track.path}
onSeek={(seconds) => void seekTo(seconds)} onSeek={(seconds) => void seekTo(seconds)}
/> />
<View style={styles.transport}> <View style={[styles.transport, { marginTop: layout.controlsGap }]}>
<Pressable <Pressable
hitSlop={10} hitSlop={10}
style={styles.transportSideBtn} style={styles.transportSideBtn}
@@ -581,7 +666,7 @@ export default function NowPlayingScreen() {
</Pressable> </Pressable>
</View> </View>
<View style={styles.subRow}> <View style={[styles.subRow, { marginTop: layout.controlsGap }]}>
<View style={styles.subBadges}> <View style={styles.subBadges}>
<RemoteSourceBadge sourceType={track.sourceType} /> <RemoteSourceBadge sourceType={track.sourceType} />
<FormatBadges track={track} wrap={false} /> <FormatBadges track={track} wrap={false} />
@@ -749,6 +834,12 @@ const styles = StyleSheet.create({
player: { player: {
flex: 1, flex: 1,
}, },
playerWide: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
columnGap: WIDE_PANE_GAP,
},
middleStack: { middleStack: {
width: '100%', width: '100%',
alignItems: 'center', alignItems: 'center',
@@ -791,7 +882,6 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: spacing.md, gap: spacing.md,
marginBottom: spacing.md,
}, },
trackTextStack: { trackTextStack: {
flex: 1, flex: 1,
@@ -828,18 +918,17 @@ const styles = StyleSheet.create({
artist: { artist: {
color: colors.accentText, color: colors.accentText,
}, },
spacer: {
flex: 1,
minHeight: MIN_FLOATING_SPACE,
},
playerControls: { playerControls: {
width: '100%', width: '100%',
}, },
playerControlsFill: {
flex: 1,
justifyContent: 'space-between',
},
transport: { transport: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
marginTop: TRANSPORT_TOP_MARGIN,
}, },
transportMainBtn: { transportMainBtn: {
width: 48, width: 48,
@@ -866,7 +955,6 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
gap: spacing.md, gap: spacing.md,
marginTop: SUB_TOP_MARGIN,
paddingHorizontal: spacing.sm, paddingHorizontal: spacing.sm,
}, },
subBadges: { subBadges: {
+11
View File
@@ -0,0 +1,11 @@
/** Minimum width for two-pane layouts — below this, panes get too cramped to use. */
export const WIDE_MIN_WIDTH = 600;
/**
* True when a window should use a two-pane / wide layout: wide enough for two
* useful panes and wider than it is tall (single columns overflow short windows).
* Pass inset-adjusted dimensions (window minus safe areas).
*/
export function isWideWindow(availableWidth: number, availableHeight: number): boolean {
return availableWidth >= WIDE_MIN_WIDTH && availableWidth > availableHeight;
}