Add Expo mobile app for iOS and Android
Native tabs for feed, friend search, profile and settings, with feature parity with the web app. Sessions are stored in SecureStore, realtime disconnects in the background, and Spotify/Plex sign-in returns to the app via the URL scheme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { DarkTheme, Stack, ThemeProvider } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useEffect } from 'react';
|
||||
import { AppState, StyleSheet, View } from 'react-native';
|
||||
import { SWRConfig } from 'swr';
|
||||
import { Body, Button, Title } from '@/components/ui';
|
||||
import { api } from '@/lib/api';
|
||||
import { SessionProvider, useSession } from '@/lib/session';
|
||||
import { colors, space } from '@/lib/theme';
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
const theme = {
|
||||
...DarkTheme,
|
||||
colors: { ...DarkTheme.colors, background: colors.bg, card: colors.bg, text: colors.text, primary: colors.accent, border: colors.border },
|
||||
};
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<ThemeProvider value={theme}>
|
||||
<StatusBar style="light" />
|
||||
<SessionProvider>
|
||||
<AppNavigator />
|
||||
</SessionProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppNavigator() {
|
||||
const { status, me, retry } = useSession();
|
||||
const signedIn = status === 'signedIn';
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'loading') SplashScreen.hideAsync();
|
||||
}, [status]);
|
||||
|
||||
if (status === 'loading') return null;
|
||||
if (status === 'offline') {
|
||||
return (
|
||||
<View style={styles.offline}>
|
||||
<Title>Can't reach musicshare</Title>
|
||||
<Body muted>Check your connection and try again.</Body>
|
||||
<Button title="Retry" variant="primary" onPress={retry} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// Keyed by user so signing out (or in as someone else) starts from an empty cache.
|
||||
<SWRConfig
|
||||
key={me?.id ?? 'anon'}
|
||||
value={{
|
||||
fetcher: (path: string) => api.get(path),
|
||||
provider: () => new Map(),
|
||||
isVisible: () => AppState.currentState === 'active',
|
||||
initFocus(callback) {
|
||||
const sub = AppState.addEventListener('change', (s) => s === 'active' && callback());
|
||||
return () => sub.remove();
|
||||
},
|
||||
}}>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: colors.bg },
|
||||
headerTintColor: colors.text,
|
||||
headerShadowVisible: false,
|
||||
contentStyle: { backgroundColor: colors.bg },
|
||||
}}>
|
||||
<Stack.Protected guard={signedIn}>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="u/[username]" options={{ title: '' }} />
|
||||
</Stack.Protected>
|
||||
<Stack.Protected guard={!signedIn}>
|
||||
<Stack.Screen name="login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="register" options={{ title: 'Create account' }} />
|
||||
</Stack.Protected>
|
||||
</Stack>
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
offline: { flex: 1, backgroundColor: colors.bg, justifyContent: 'center', padding: space.xl, gap: space.md },
|
||||
});
|
||||
Reference in New Issue
Block a user