mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 20:49:47 +02:00
performance improvements
This commit is contained in:
+27
-11
@@ -1,3 +1,4 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Tabs } from 'expo-router';
|
||||
// RN's Easing (not reanimated): the bottom-tabs scene transition runs on legacy
|
||||
// Animated.timing and may use the native driver, so the easing must be serializable.
|
||||
@@ -6,23 +7,32 @@ import { TabBar, type TabItem } from '@/components/TabBar';
|
||||
import { useColors } from '@/theme/themed';
|
||||
|
||||
const TAB_TRANSITION_MS = 160;
|
||||
const TAB_EASING = Easing.out(Easing.cubic);
|
||||
|
||||
export default function TabsLayout() {
|
||||
const colors = useColors();
|
||||
const lastSwitchAt = useRef(0);
|
||||
// Stable screenOptions identity: handing the navigator a fresh options object
|
||||
// mid-transition (e.g. on a Material You palette change) re-runs the scene
|
||||
// animation effect and can strand the incoming scene at opacity 0.
|
||||
const screenOptions = useMemo(
|
||||
() => ({
|
||||
headerShown: false,
|
||||
freezeOnBlur: false,
|
||||
sceneStyle: { backgroundColor: colors.bgPrimary },
|
||||
// Directional slide + cross-fade between tabs, following tab order.
|
||||
animation: 'shift' as const,
|
||||
transitionSpec: {
|
||||
animation: 'timing' as const,
|
||||
config: { duration: TAB_TRANSITION_MS, easing: TAB_EASING },
|
||||
},
|
||||
}),
|
||||
[colors.bgPrimary]
|
||||
);
|
||||
return (
|
||||
<Tabs
|
||||
detachInactiveScreens={false}
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
freezeOnBlur: false,
|
||||
sceneStyle: { backgroundColor: colors.bgPrimary },
|
||||
// Directional slide + cross-fade between tabs, following tab order.
|
||||
animation: 'shift',
|
||||
transitionSpec: {
|
||||
animation: 'timing',
|
||||
config: { duration: TAB_TRANSITION_MS, easing: Easing.out(Easing.cubic) },
|
||||
},
|
||||
}}
|
||||
screenOptions={screenOptions}
|
||||
tabBar={({ state, navigation }) => {
|
||||
const items: TabItem[] = state.routes.map((route, index) => ({
|
||||
key: route.key,
|
||||
@@ -31,12 +41,18 @@ export default function TabsLayout() {
|
||||
}));
|
||||
|
||||
const handlePress = (item: TabItem) => {
|
||||
// Interrupting the native-driver shift animation can drop its
|
||||
// completion frame and leave the incoming scene invisible; swallow
|
||||
// taps until the current transition has finished.
|
||||
const now = Date.now();
|
||||
if (now - lastSwitchAt.current < TAB_TRANSITION_MS + 30) return;
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: item.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
if (!item.focused && !event.defaultPrevented) {
|
||||
lastSwitchAt.current = now;
|
||||
navigation.navigate(item.name);
|
||||
}
|
||||
};
|
||||
|
||||
+14
-15
@@ -136,24 +136,32 @@ function AlbumCover({ album, size }: { album: Album; size: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Progress strip subscribes here so the 2Hz tick skips the card and screen. */
|
||||
function NowPlayingSeekStrip() {
|
||||
const styles = useStyles();
|
||||
const currentTime = usePlayerStore((s) => s.currentTime);
|
||||
const duration = usePlayerStore((s) => s.duration);
|
||||
const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
|
||||
return (
|
||||
<View style={styles.seekTrack}>
|
||||
<View style={[styles.seekFill, { width: `${progress * 100}%` }]} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function NowPlayingCard({
|
||||
track,
|
||||
playbackState,
|
||||
currentTime,
|
||||
duration,
|
||||
onOpen,
|
||||
}: {
|
||||
track: Track;
|
||||
playbackState: PlaybackState;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
onOpen: () => void;
|
||||
}) {
|
||||
const styles = useStyles();
|
||||
const colors = useColors();
|
||||
const isPlaying = playbackState === 'playing';
|
||||
const isLoading = playbackState === 'loading';
|
||||
const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
|
||||
const scopeActive = useScopeActive();
|
||||
const [cardSize, setCardSize] = useState({ width: 0, height: PLAYER_CARD_MIN_HEIGHT });
|
||||
|
||||
@@ -171,7 +179,6 @@ function NowPlayingCard({
|
||||
<SpectrumCurve
|
||||
active={scopeActive}
|
||||
pointCount={CURVE_POINTS}
|
||||
analysisFrameMs={0}
|
||||
dbMin={-84}
|
||||
dbMax={-20}
|
||||
width={cardSize.width}
|
||||
@@ -204,9 +211,7 @@ function NowPlayingCard({
|
||||
<Text variant="body" color={colors.textSecondary} numberOfLines={1}>
|
||||
{track.album ? `${track.artist} / ${track.album}` : track.artist}
|
||||
</Text>
|
||||
<View style={styles.seekTrack}>
|
||||
<View style={[styles.seekFill, { width: `${progress * 100}%` }]} />
|
||||
</View>
|
||||
<NowPlayingSeekStrip />
|
||||
<View style={styles.placeholderControls}>
|
||||
<Pressable hitSlop={10} onPress={() => void skipToPrevious()}>
|
||||
<Ionicons name="play-skip-back" size={22} color={colors.textSecondary} />
|
||||
@@ -377,8 +382,6 @@ export default function HomeScreen() {
|
||||
const currentTrack = usePlayerStore((s) => s.currentTrack);
|
||||
const currentPath = currentTrack?.path;
|
||||
const playbackState = usePlayerStore((s) => s.playbackState);
|
||||
const currentTime = usePlayerStore((s) => s.currentTime);
|
||||
const duration = usePlayerStore((s) => s.duration);
|
||||
const openQuickSearch = useSearchStore((s) => s.openQuickSearch);
|
||||
|
||||
const [randomAlbumKey, setRandomAlbumKey] = useState<string | null>(null);
|
||||
@@ -483,8 +486,6 @@ export default function HomeScreen() {
|
||||
<NowPlayingCard
|
||||
track={currentTrack}
|
||||
playbackState={playbackState}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
onOpen={() => router.push('/now-playing')}
|
||||
/>
|
||||
</View>
|
||||
@@ -501,8 +502,6 @@ export default function HomeScreen() {
|
||||
<NowPlayingCard
|
||||
track={currentTrack}
|
||||
playbackState={playbackState}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
onOpen={() => router.push('/now-playing')}
|
||||
/>
|
||||
) : randomAlbum ? (
|
||||
|
||||
+6
-12
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Pressable,
|
||||
@@ -273,6 +273,8 @@ export default function NowPlayingScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||
const [queueOpen, setQueueOpen] = useState(false);
|
||||
// Stable identity: QueueTray is memo'd, so a fresh arrow here would defeat it.
|
||||
const closeQueue = useCallback(() => setQueueOpen(false), []);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [targetPickerOpen, setTargetPickerOpen] = useState(false);
|
||||
const [playlistActionTrack, setPlaylistActionTrack] = useState<DbTrack | null>(null);
|
||||
@@ -286,8 +288,6 @@ export default function NowPlayingScreen() {
|
||||
const libraryTracks = useLibraryStore((s) => s.tracks);
|
||||
const track = usePlayerStore((s) => s.currentTrack);
|
||||
const playbackState = usePlayerStore((s) => s.playbackState);
|
||||
const currentTime = usePlayerStore((s) => s.currentTime);
|
||||
const duration = usePlayerStore((s) => s.duration);
|
||||
const shuffle = usePlayerStore((s) => s.shuffle);
|
||||
const repeat = usePlayerStore((s) => s.repeat);
|
||||
const isFavorite = usePlaylistStore((s) => (track ? s.favoritePaths.has(track.path) : false));
|
||||
@@ -302,8 +302,6 @@ export default function NowPlayingScreen() {
|
||||
const phonePresentation = getPhonePlaybackPresentation({
|
||||
track,
|
||||
playbackState,
|
||||
currentTime,
|
||||
duration,
|
||||
});
|
||||
const desktopPresentation = getDesktopPlaybackPresentation({
|
||||
connection: desktopConnection,
|
||||
@@ -565,8 +563,6 @@ export default function NowPlayingScreen() {
|
||||
{lyricsMode && track ? (
|
||||
<LyricsView
|
||||
track={track}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isPlaying={isPlaying}
|
||||
isLoading={isLoading}
|
||||
isFavorite={isFavorite}
|
||||
@@ -875,6 +871,7 @@ export default function NowPlayingScreen() {
|
||||
showChrome={false}
|
||||
mode={scopeMode}
|
||||
edgeFade
|
||||
paused={queueOpen}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
@@ -945,9 +942,6 @@ export default function NowPlayingScreen() {
|
||||
</View>
|
||||
|
||||
<WaveformSeekBar
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
isPlaying={isPlaying}
|
||||
height={layout.waveformHeight}
|
||||
touchPadding={WAVEFORM_TOUCH_PADDING}
|
||||
trackPath={track.path}
|
||||
@@ -1103,9 +1097,9 @@ export default function NowPlayingScreen() {
|
||||
/>
|
||||
{queueOpen && (
|
||||
isDesktopTarget ? (
|
||||
<RemoteQueueSheet onClose={() => setQueueOpen(false)} />
|
||||
<RemoteQueueSheet onClose={closeQueue} />
|
||||
) : (
|
||||
<QueueTray onClose={() => setQueueOpen(false)} />
|
||||
<QueueTray onClose={closeQueue} />
|
||||
)
|
||||
)}
|
||||
<PlaybackTargetPicker
|
||||
|
||||
Reference in New Issue
Block a user