android auto support

This commit is contained in:
Boof2015
2026-06-29 14:07:58 -04:00
parent 3558ea30e4
commit de7ad39d62
31 changed files with 2259 additions and 39 deletions
+25 -5
View File
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
@@ -27,6 +27,14 @@ import { useNormalizationSync } from '@/audio/useNormalizationSync';
import { useLastFmScrobbler } from '@/audio/useLastFmScrobbler';
import { colors } from '@/theme';
// Anchor the root stack at the tabs so a deep link straight to a top-level route (the
// widget/notification opening `now-playing`, or `recently-played`) builds `[(tabs), route]`
// instead of just `[route]`. Without this, dismissing the now-playing modal pops to an empty
// stack → blank screen. Only affects deep-link/launch ordering; normal nav is unchanged.
export const unstable_settings = {
initialRouteName: '(tabs)',
};
SplashScreen.preventAutoHideAsync();
/** Mirrors RNTP state into the player store. Renders nothing. */
@@ -63,11 +71,23 @@ export default function RootLayout() {
JetBrainsMono_500Medium,
});
// Failsafe so the splash can never hang the UI blank. `preventAutoHideAsync` runs at
// module scope — including in the headless JS context Android Auto spins up — so when
// the process is started from the car first and the app is opened later, the normal
// "hide once fonts load" path can get stuck. Render (and hide the splash) anyway after
// a short timeout even if fonts haven't reported in.
const [splashTimedOut, setSplashTimedOut] = useState(false);
useEffect(() => {
if (fontsLoaded) {
void SplashScreen.hideAsync();
const timer = setTimeout(() => setSplashTimedOut(true), 2000);
return () => clearTimeout(timer);
}, []);
const ready = fontsLoaded || splashTimedOut;
useEffect(() => {
if (ready) {
void SplashScreen.hideAsync().catch(() => {});
}
}, [fontsLoaded]);
}, [ready]);
// Eager library init: SQLite open + initial reads are tens of ms, and the
// Library tab + playback adapters get data immediately. EQ + audio settings load
@@ -99,7 +119,7 @@ export default function RootLayout() {
.catch((err) => console.error('[lastfm] init failed', err));
}, []);
if (!fontsLoaded) return null;
if (!ready) return null;
return (
<GestureHandlerRootView style={styles.root}>
+8 -1
View File
@@ -265,7 +265,14 @@ export default function NowPlayingScreen() {
// a second native modal animation after release.
const translateY = useSharedValue(0);
const menuProgress = useSharedValue(0);
const dismiss = () => router.back();
// Belt-and-suspenders for deep-link entry (widget/notification → now-playing with no
// history): `(tabs)` is the stack anchor (see root _layout unstable_settings), so back()
// returns there; if somehow there's nothing to go back to, replace to the tabs home so
// dismissing can never land on a blank screen.
const dismiss = () => {
if (router.canGoBack()) router.back();
else router.replace('/');
};
const finishCloseMenu = () => setMenuOpen(false);
function openMenu() {