mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-18 19:54:26 +02:00
fix layout issues
This commit is contained in:
+132
-65
@@ -1,7 +1,8 @@
|
||||
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 { useFocusEffect } from 'expo-router';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import { readAsStringAsync } from 'expo-file-system/legacy';
|
||||
import { Screen } from '@/components/Screen';
|
||||
@@ -15,6 +16,7 @@ import { EQValueEditSheet } from '@/components/eq/EQValueEditSheet';
|
||||
import { PresetSheet } from '@/components/eq/PresetSheet';
|
||||
import { SavePresetSheet } from '@/components/eq/SavePresetSheet';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { isWideWindow } from '@/theme/adaptive';
|
||||
import { useEQStore } from '@/stores/eqStore';
|
||||
import { useScopeActive } from '@/scope/scopeStore';
|
||||
import { setActivePostEqNative } from '@/audio/eqNative';
|
||||
@@ -44,6 +46,12 @@ export default function EQScreen() {
|
||||
const [sheet, setSheet] = useState<SheetKind>('none');
|
||||
const [editingValue, setEditingValue] = useState<EQEditableValue | null>(null);
|
||||
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.
|
||||
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 (
|
||||
<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>
|
||||
<View style={styles.headerActions}>
|
||||
<Pressable style={styles.iconButton} onPress={() => setSheet('save')} hitSlop={8}>
|
||||
@@ -91,69 +175,31 @@ export default function EQScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Pressable style={styles.presetRow} onPress={() => setSheet('preset')}>
|
||||
<Text variant="body" color={colors.textPrimary}>
|
||||
{presetName}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
|
||||
</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}
|
||||
{isWide ? (
|
||||
<View
|
||||
style={[
|
||||
styles.wideBody,
|
||||
{ paddingLeft: spacing.lg + insets.left, paddingRight: spacing.lg + insets.right },
|
||||
]}
|
||||
>
|
||||
<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>
|
||||
<View style={styles.wideGraphPane}>{graphEl}</View>
|
||||
<View style={{ width: sidePaneWidth }}>
|
||||
{presetRowEl}
|
||||
{stripEl}
|
||||
<View style={styles.sideDetail}>{detailEl}</View>
|
||||
<View style={styles.wideSpacer} />
|
||||
{bottomBarEl}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{presetRowEl}
|
||||
<View style={styles.graphWrap}>{graphEl}</View>
|
||||
<View style={styles.section}>{stripEl}</View>
|
||||
<View style={styles.section}>{detailEl}</View>
|
||||
{bottomBarEl}
|
||||
</>
|
||||
)}
|
||||
|
||||
{sheet === 'preset' ? (
|
||||
<PresetSheet
|
||||
@@ -311,7 +357,6 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.md,
|
||||
paddingBottom: spacing.sm,
|
||||
},
|
||||
@@ -349,6 +394,28 @@ const styles = StyleSheet.create({
|
||||
marginHorizontal: spacing.lg,
|
||||
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: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
+136
-48
@@ -23,6 +23,7 @@ import { Visualizer } from '@/components/Visualizer';
|
||||
import { TrackActionsSheet } from '@/components/library/TrackActionsSheet';
|
||||
import { QueueTray } from '@/components/queue/QueueTray';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { WIDE_MIN_WIDTH, isWideWindow } from '@/theme/adaptive';
|
||||
import { motion } from '@/theme/motion';
|
||||
import { resolveCanonicalBrowseArtist, resolveStrictBrowseArtist } from '@/library/artistGrouping';
|
||||
import { useLibraryStore } from '@/stores/libraryStore';
|
||||
@@ -46,10 +47,17 @@ const MAX_CONTENT_WIDTH = 408;
|
||||
const CONTENT_SIDE_PADDING = spacing.lg;
|
||||
const NARROW_CONTENT_SIDE_PADDING = spacing.md;
|
||||
const MEDIA_AREA_MIN = 220;
|
||||
const MEDIA_AREA_MAX = 360;
|
||||
const ART_SIZE_MAX = 340;
|
||||
const VISUALIZER_ART_SCALE = 0.8;
|
||||
const VISUALIZER_ART_SIZE_MIN = 176;
|
||||
// Tablet-portrait tier: tall windows >= WIDE_MIN_WIDTH keep the single column but grow it.
|
||||
const TABLET_MAX_CONTENT_WIDTH = 520;
|
||||
const TABLET_ART_SIZE_MAX = 440;
|
||||
// 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_SIDE_PADDING = spacing.md;
|
||||
const VISUALIZER_TOP_GAP = spacing.lg;
|
||||
@@ -61,7 +69,6 @@ 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;
|
||||
@@ -80,8 +87,15 @@ const MENU_ANIMATION_OUT_MS = 100;
|
||||
const MENU_ENTER_OFFSET_Y = -8;
|
||||
|
||||
interface NowPlayingLayout {
|
||||
isWide: boolean;
|
||||
contentPadding: number;
|
||||
contentWidth: number;
|
||||
leftPaneWidth: number;
|
||||
rightPaneWidth: number;
|
||||
controlsGap: number;
|
||||
trackInfoGap: number;
|
||||
waveformHeight: number;
|
||||
mediaStackHeight: number;
|
||||
artSize: number;
|
||||
scopeWidth: number;
|
||||
scopeHeight: number;
|
||||
@@ -109,29 +123,76 @@ function getScopeHeight(scopeWidth: number): number {
|
||||
}
|
||||
|
||||
function getNowPlayingLayout(
|
||||
windowWidth: number,
|
||||
availableWidth: number,
|
||||
availableHeight: number,
|
||||
showVisualizer: boolean
|
||||
): 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 =
|
||||
windowWidth < 360 ? NARROW_CONTENT_SIDE_PADDING : CONTENT_SIDE_PADDING;
|
||||
const contentWidth = Math.max(0, Math.min(windowWidth - contentPadding * 2, MAX_CONTENT_WIDTH));
|
||||
availableWidth < 360 ? NARROW_CONTENT_SIDE_PADDING : CONTENT_SIDE_PADDING;
|
||||
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(
|
||||
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 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 mediaTopMargin = availableHeight < 680 ? spacing.md : MEDIA_TOP_MARGIN;
|
||||
const defaultMediaBottomGap = availableHeight < 680 ? spacing.lg : MEDIA_BOTTOM_GAP;
|
||||
const mediaBottomGap = showVisualizer ? VISUALIZER_MEDIA_BOTTOM_GAP : defaultMediaBottomGap;
|
||||
const fixedHeight =
|
||||
const mediaBottomGap = defaultMediaBottomGap;
|
||||
const fixedHeightBase =
|
||||
CONTENT_TOP_PADDING +
|
||||
CONTENT_BOTTOM_PADDING +
|
||||
HEADER_HEIGHT +
|
||||
mediaTopMargin +
|
||||
mediaBottomGap +
|
||||
TRACK_INFO_ESTIMATE +
|
||||
WAVEFORM_BLOCK_ESTIMATE +
|
||||
TRANSPORT_TOP_MARGIN +
|
||||
@@ -139,27 +200,38 @@ function getNowPlayingLayout(
|
||||
SUB_TOP_MARGIN +
|
||||
SUB_BUTTON_SIZE +
|
||||
MIN_FLOATING_SPACE;
|
||||
const heightBoundMedia = availableHeight - fixedHeight;
|
||||
const baseArtSize = Math.min(
|
||||
Math.round(clamp(heightBoundMedia, mediaMin, mediaMax)),
|
||||
ART_SIZE_MAX
|
||||
// The Math.max(96, ...) floor lets art shrink below MEDIA_AREA_MIN in squat
|
||||
// windows (split-screen halves) instead of pushing the controls off-screen.
|
||||
const bound = availableHeight - fixedHeightBase - mediaBottomGap;
|
||||
const scopeOffArt = Math.round(
|
||||
clamp(bound, Math.min(mediaMin, Math.max(96, bound)), mediaMax)
|
||||
);
|
||||
const artSize = showVisualizer
|
||||
? Math.round(
|
||||
clamp(
|
||||
baseArtSize * VISUALIZER_ART_SCALE,
|
||||
Math.min(VISUALIZER_ART_SIZE_MIN, mediaMax),
|
||||
mediaMax
|
||||
)
|
||||
)
|
||||
: baseArtSize;
|
||||
// Roomy screens get a taller waveform; the rest of the spare space is
|
||||
// distributed between the control rows by flex (space-between), so no
|
||||
// height estimate error can pool as one gap above the controls.
|
||||
const offSurplus = Math.max(0, bound - scopeOffArt);
|
||||
const stretchUnit = Math.min(Math.floor(offSurplus / 5), spacing.md);
|
||||
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.
|
||||
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 visualizerBottomGap = showVisualizer ? VISUALIZER_BOTTOM_GAP : 0;
|
||||
|
||||
return {
|
||||
isWide: false,
|
||||
contentPadding,
|
||||
contentWidth,
|
||||
leftPaneWidth: contentWidth,
|
||||
rightPaneWidth: contentWidth,
|
||||
controlsGap: TRANSPORT_TOP_MARGIN,
|
||||
trackInfoGap: spacing.md,
|
||||
waveformHeight,
|
||||
mediaStackHeight,
|
||||
artSize,
|
||||
scopeWidth,
|
||||
scopeHeight,
|
||||
@@ -194,9 +266,13 @@ export default function NowPlayingScreen() {
|
||||
const isPlaying = playbackState === 'playing';
|
||||
const isLoading = playbackState === 'loading';
|
||||
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 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 libraryTrack = useMemo(
|
||||
() => (track ? libraryTracks.find((entry) => entry.path === track.path) ?? null : null),
|
||||
@@ -350,7 +426,8 @@ export default function NowPlayingScreen() {
|
||||
styles.content,
|
||||
contentStyle,
|
||||
{
|
||||
paddingHorizontal: layout.contentPadding,
|
||||
paddingLeft: insets.left + layout.contentPadding,
|
||||
paddingRight: insets.right + layout.contentPadding,
|
||||
paddingTop: insets.top + CONTENT_TOP_PADDING,
|
||||
paddingBottom: insets.bottom + CONTENT_BOTTOM_PADDING,
|
||||
},
|
||||
@@ -381,14 +458,17 @@ export default function NowPlayingScreen() {
|
||||
</View>
|
||||
|
||||
{track ? (
|
||||
<View style={styles.player}>
|
||||
<View style={[styles.player, layout.isWide && styles.playerWide]}>
|
||||
<View
|
||||
style={[
|
||||
styles.middleStack,
|
||||
{
|
||||
marginTop: layout.mediaTopMargin,
|
||||
marginBottom: layout.mediaBottomGap,
|
||||
},
|
||||
layout.isWide
|
||||
? { width: layout.leftPaneWidth, justifyContent: 'center' }
|
||||
: {
|
||||
height: layout.mediaStackHeight,
|
||||
marginTop: layout.mediaTopMargin,
|
||||
marginBottom: layout.mediaBottomGap,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View
|
||||
@@ -464,10 +544,15 @@ export default function NowPlayingScreen() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
|
||||
<View style={styles.playerControls}>
|
||||
<View style={styles.trackInfo}>
|
||||
<View
|
||||
style={[
|
||||
styles.playerControls,
|
||||
layout.isWide
|
||||
? { width: layout.rightPaneWidth }
|
||||
: styles.playerControlsFill,
|
||||
]}
|
||||
>
|
||||
<View style={[styles.trackInfo, { marginBottom: layout.trackInfoGap }]}>
|
||||
<View style={styles.trackTextStack}>
|
||||
<MarqueeText
|
||||
variant="heading"
|
||||
@@ -508,14 +593,14 @@ export default function NowPlayingScreen() {
|
||||
<WaveformSeekBar
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
height={WAVEFORM_HEIGHT}
|
||||
height={layout.waveformHeight}
|
||||
touchPadding={WAVEFORM_TOUCH_PADDING}
|
||||
trackKey={track.id}
|
||||
trackPath={track.path}
|
||||
onSeek={(seconds) => void seekTo(seconds)}
|
||||
/>
|
||||
|
||||
<View style={styles.transport}>
|
||||
<View style={[styles.transport, { marginTop: layout.controlsGap }]}>
|
||||
<Pressable
|
||||
hitSlop={10}
|
||||
style={styles.transportSideBtn}
|
||||
@@ -581,7 +666,7 @@ export default function NowPlayingScreen() {
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.subRow}>
|
||||
<View style={[styles.subRow, { marginTop: layout.controlsGap }]}>
|
||||
<View style={styles.subBadges}>
|
||||
<RemoteSourceBadge sourceType={track.sourceType} />
|
||||
<FormatBadges track={track} wrap={false} />
|
||||
@@ -749,6 +834,12 @@ const styles = StyleSheet.create({
|
||||
player: {
|
||||
flex: 1,
|
||||
},
|
||||
playerWide: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
columnGap: WIDE_PANE_GAP,
|
||||
},
|
||||
middleStack: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
@@ -791,7 +882,6 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
trackTextStack: {
|
||||
flex: 1,
|
||||
@@ -828,18 +918,17 @@ const styles = StyleSheet.create({
|
||||
artist: {
|
||||
color: colors.accentText,
|
||||
},
|
||||
spacer: {
|
||||
flex: 1,
|
||||
minHeight: MIN_FLOATING_SPACE,
|
||||
},
|
||||
playerControls: {
|
||||
width: '100%',
|
||||
},
|
||||
playerControlsFill: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
transport: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: TRANSPORT_TOP_MARGIN,
|
||||
},
|
||||
transportMainBtn: {
|
||||
width: 48,
|
||||
@@ -866,7 +955,6 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: spacing.md,
|
||||
marginTop: SUB_TOP_MARGIN,
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
subBadges: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user