mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 20:49:47 +02:00
fix internal routing, and remove softlocking
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
TAB_TRANSITION_SETTLE_MS,
|
||||
TAB_TRANSITION_SPEC,
|
||||
} from '@/navigation/tabTransition';
|
||||
import { popToTop } from '@/navigation/stackActions';
|
||||
import { useColors } from '@/theme/themed';
|
||||
|
||||
export default function TabsLayout() {
|
||||
@@ -46,10 +47,21 @@ export default function TabsLayout() {
|
||||
target: item.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
if (!item.focused && !event.defaultPrevented) {
|
||||
lastSwitchAt.current = now;
|
||||
navigation.navigate(item.name);
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
if (item.focused) {
|
||||
// Re-tapping the active tab resets its nested stack. This is the
|
||||
// one-tap escape from a deep library chain (artist → album →
|
||||
// another artist), which is why back itself only pops one level.
|
||||
const nested = state.routes[state.index]?.state;
|
||||
if (nested?.key && (nested.index ?? 0) > 0) {
|
||||
navigation.dispatch({ ...popToTop(), target: nested.key });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lastSwitchAt.current = now;
|
||||
navigation.navigate(item.name);
|
||||
};
|
||||
|
||||
return <TabBar items={items} onPress={handlePress} />;
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { useColors } from '@/theme/themed';
|
||||
|
||||
/**
|
||||
* Anchor the library stack at its list. Without this, navigating straight to a
|
||||
* detail route (from Home, quick search, or the now-playing overlay) builds a
|
||||
* stack of just `[album]` with nothing beneath it — so popping one level had
|
||||
* nowhere to go and back escaped the tab entirely.
|
||||
*/
|
||||
export const unstable_settings = {
|
||||
initialRouteName: 'index',
|
||||
};
|
||||
|
||||
/**
|
||||
* Nested stack inside the Library tab so album/artist detail screens keep the
|
||||
* tab bar + mini-player visible.
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function AlbumScreen() {
|
||||
const { key } = useLocalSearchParams<{ key: string }>();
|
||||
const { items: tracks, summary: album, totalCount, loadMore } = useNativeAlbumDetail(key);
|
||||
const currentPath = usePlayerStore((s) => s.currentTrack?.path);
|
||||
const handleBack = useLibraryDetailBack();
|
||||
const { goBack, backLabel } = useLibraryDetailBack();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { scrollY, heroFaded, collapsed, onScroll, scrollEventThrottle, expandedHeight, onHeroBlockLayout } =
|
||||
useDetailCollapse();
|
||||
@@ -154,7 +154,8 @@ export default function AlbumScreen() {
|
||||
</>
|
||||
}
|
||||
disabled={tracks.length === 0}
|
||||
onBack={handleBack}
|
||||
onBack={goBack}
|
||||
backLabel={backLabel}
|
||||
onPlay={() => playFrom(0)}
|
||||
onShuffle={() => void playLibraryQuery({ kind: 'album', albumKey: key }, {
|
||||
shuffle: true,
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function ArtistScreen() {
|
||||
name: string;
|
||||
credit?: string;
|
||||
}>();
|
||||
const handleBack = useLibraryDetailBack();
|
||||
const { goBack, backLabel } = useLibraryDetailBack();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { scrollY, heroFaded, collapsed, onScroll, scrollEventThrottle, expandedHeight, onHeroBlockLayout } =
|
||||
useDetailCollapse();
|
||||
@@ -246,7 +246,8 @@ export default function ArtistScreen() {
|
||||
</View>
|
||||
}
|
||||
disabled={disabled}
|
||||
onBack={handleBack}
|
||||
onBack={goBack}
|
||||
backLabel={backLabel}
|
||||
onPlay={playArtist}
|
||||
onShuffle={shuffleArtist}
|
||||
scrollY={scrollY}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useFocusEffect, useRouter } from 'expo-router';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { ViewModeSwitcher } from '@/components/library/ViewModeSwitcher';
|
||||
@@ -165,14 +165,20 @@ export default function LibraryScreen() {
|
||||
setPlaylistPickerOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectMode) return;
|
||||
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
exitSelection();
|
||||
return true;
|
||||
});
|
||||
return () => sub.remove();
|
||||
}, [selectMode]);
|
||||
// Focus-gated, not a plain effect: the tabs layout keeps this screen mounted
|
||||
// while blurred (`detachInactiveScreens={false}` + `freezeOnBlur: false`), so
|
||||
// an ungated handler swallowed one back press anywhere in the app — any other
|
||||
// tab, any settings screen — whenever selection happened to be active.
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (!selectMode) return undefined;
|
||||
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
exitSelection();
|
||||
return true;
|
||||
});
|
||||
return () => sub.remove();
|
||||
}, [selectMode])
|
||||
);
|
||||
|
||||
const selectedDbTracks = () => sortedTracks.filter((track) => selectedIds.has(track.id));
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function PlaylistScreen() {
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const handleBack = useLibraryDetailBack();
|
||||
const { goBack, backLabel } = useLibraryDetailBack();
|
||||
const isFavorites = id === 'favorites';
|
||||
const playlistId = isFavorites ? null : Number(id);
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function PlaylistScreen() {
|
||||
void (async () => {
|
||||
try {
|
||||
await deletePlaylist(target.id);
|
||||
handleBack();
|
||||
goBack();
|
||||
} catch (err) {
|
||||
Alert.alert('Delete failed', errorMessage(err));
|
||||
}
|
||||
@@ -340,7 +340,8 @@ export default function PlaylistScreen() {
|
||||
) : null
|
||||
}
|
||||
disabled={playable.length === 0}
|
||||
onBack={handleBack}
|
||||
onBack={goBack}
|
||||
backLabel={backLabel}
|
||||
onMore={() => setOptionsOpen(true)}
|
||||
onPlay={() => startPlayback(0)}
|
||||
onShuffle={startShuffle}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { Image } from 'expo-image';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useReturnToTabs } from '@/navigation/returnToTabs';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { AstraLogo } from '@/components/AstraLogo';
|
||||
import { Screen } from '@/components/Screen';
|
||||
@@ -225,6 +226,7 @@ export default function DesktopRemoteScreen() {
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const returnToTabs = useReturnToTabs();
|
||||
const pairingParams = useLocalSearchParams<{
|
||||
pair?: string;
|
||||
baseUrl?: string;
|
||||
@@ -620,7 +622,7 @@ export default function DesktopRemoteScreen() {
|
||||
// screen so it slides in above wherever the user came from.
|
||||
usePlayerUiStore.getState().openPlayer();
|
||||
if (router.canGoBack()) router.back();
|
||||
else router.replace('/');
|
||||
else returnToTabs('/');
|
||||
}}
|
||||
>
|
||||
<Ionicons name="musical-notes-outline" size={18} color={colors.accentTextStrong} />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Pressable, StyleSheet, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useLocalSearchParams } from 'expo-router';
|
||||
import { useReturnToTabs } from '@/navigation/returnToTabs';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { EQPresetPreviewSheet } from '@/components/eq/EQPresetPreviewSheet';
|
||||
@@ -17,7 +18,7 @@ export default function EQPresetImportScreen() {
|
||||
const styles = useStyles();
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const returnToTabs = useReturnToTabs();
|
||||
const importPreset = useEQStore((state) => state.importPreset);
|
||||
const { data } = useLocalSearchParams<{ data?: string }>();
|
||||
|
||||
@@ -30,7 +31,9 @@ export default function EQPresetImportScreen() {
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const goToEq = () => router.replace('/eq' as never);
|
||||
// `/eq` is a tab, and this screen is a root-stack sibling of `(tabs)`, so a
|
||||
// bare replace here mints a second copy of the whole tab tree.
|
||||
const goToEq = () => returnToTabs('/eq' as never);
|
||||
|
||||
if (!preset) {
|
||||
return (
|
||||
|
||||
+4
-1
@@ -11,6 +11,7 @@ import {
|
||||
} from 'expo-camera';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useReturnToTabs } from '@/navigation/returnToTabs';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Text } from '@/components/Text';
|
||||
import { EQPresetPreviewSheet } from '@/components/eq/EQPresetPreviewSheet';
|
||||
@@ -30,6 +31,7 @@ export default function EQPresetScanScreen() {
|
||||
const ripple = useRipple();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
const returnToTabs = useReturnToTabs();
|
||||
const importPreset = useEQStore((state) => state.importPreset);
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
const [pendingPreset, setPendingPreset] = useState<EQPreset | null>(null);
|
||||
@@ -109,7 +111,8 @@ export default function EQPresetScanScreen() {
|
||||
onConfirm={() => {
|
||||
importPreset(pendingPreset);
|
||||
setPendingPreset(null);
|
||||
router.replace('/eq' as never);
|
||||
// `/eq` is a tab; see useReturnToTabs.
|
||||
returnToTabs('/eq' as never);
|
||||
}}
|
||||
onClose={() => setPendingPreset(null)}
|
||||
/>
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import { useEffect } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { resolveNotificationClick } from '@/audio/notificationIntent';
|
||||
import { useReturnToTabs } from '@/navigation/returnToTabs';
|
||||
import { createThemedStyles } from '@/theme/themed';
|
||||
|
||||
export default function NotificationClickRoute() {
|
||||
const styles = useStyles();
|
||||
const router = useRouter();
|
||||
const returnToTabs = useReturnToTabs();
|
||||
|
||||
// This route is a root-stack sibling of `(tabs)`, and it is entered often
|
||||
// (media-notification and widget taps). A bare replace toward a tab route
|
||||
// therefore left a duplicate `(tabs)` behind on every single tap.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
resolveNotificationClick()
|
||||
.then((href) => {
|
||||
if (!cancelled) router.replace(href);
|
||||
if (!cancelled) returnToTabs(href);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) router.replace('/');
|
||||
if (!cancelled) returnToTabs('/');
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [router]);
|
||||
}, [returnToTabs]);
|
||||
|
||||
return <View style={styles.root} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user