Files
musicshare/apps/mobile/src/app/_layout.tsx
T
chrisandClaude Opus 5.5 864ab88627 New look: slate-indigo panels, pink accent, Poppins, and a gradient card for your song
Inspired by a dashboard design the user shared.
- App: new palette and fonts, borderless rounded panels with soft shadows, a pink-to-blue
  gradient card for your own song (text and controls switch to white on it), flat cards for
  friends, a 'Hi Chris, welcome back' header, an outline bell, and a pink pill on the active tab.
- Website: the same look, plus a wide-screen layout with a sidebar, the page in a rounded panel,
  and a right column with your profile, recent activity and recent plays. Phones keep the top
  bar and bottom tabs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
2026-10-15 15:01:20 -04:00

135 lines
5.9 KiB
TypeScript

import { Poppins_400Regular, Poppins_500Medium, Poppins_600SemiBold, useFonts } from '@expo-google-fonts/poppins';
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 useSWR, { SWRConfig } from 'swr';
import type { ConnectionDTO } from '@musicshare/shared';
import { Body, Button, Title } from '@/components/ui';
import { api } from '@/lib/api';
import { appleMusicAvailable, useAppleMusicLive } from '@/lib/appleMusic';
import { syncDevice } from '@/lib/nowPlayingReader';
import { syncPush } from '@/lib/push';
import { SessionProvider, useSession } from '@/lib/session';
import { startUpdater } from '@/lib/updates';
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';
const onboarded = Boolean(me?.onboardedAt);
const [fontsLoaded, fontError] = useFonts({ Poppins_400Regular, Poppins_500Medium, Poppins_600SemiBold });
const ready = status !== 'loading' && (fontsLoaded || Boolean(fontError));
useEffect(() => {
if (ready) SplashScreen.hideAsync();
}, [ready]);
// Look for a newer version of the app (our own APKs only; see lib/updates.ts).
useEffect(() => {
if (ready) startUpdater();
}, [ready]);
// Keep the Android listener pointed at this account (no-op elsewhere).
useEffect(() => {
if (signedIn) syncDevice().catch(() => {});
if (signedIn) syncPush().catch(() => {});
}, [signedIn]);
if (!ready) return null;
if (status === 'offline') {
return (
<View style={styles.offline}>
<Title>Can&apos;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();
},
}}>
{signedIn && appleMusicAvailable && <AppleMusicLive />}
<Stack
screenOptions={{
headerStyle: { backgroundColor: colors.bg },
headerTintColor: colors.text,
headerShadowVisible: false,
contentStyle: { backgroundColor: colors.bg },
}}>
<Stack.Protected guard={signedIn && onboarded}>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="share-setup" options={{ headerShown: false, presentation: 'modal' }} />
<Stack.Screen name="qr" options={{ title: 'My code', presentation: 'modal' }} />
<Stack.Screen name="spotify-setup" options={{ headerShown: false, presentation: 'modal' }} />
</Stack.Protected>
{/* New accounts go through onboarding first (finishing flips onboardedAt and lands on the feed). */}
<Stack.Protected guard={signedIn && !onboarded}>
<Stack.Screen name="onboarding" options={{ headerShown: false, gestureEnabled: false }} />
</Stack.Protected>
<Stack.Protected guard={signedIn}>
{/* Reachable from onboarding too (scanning a friend's code, viewing a profile). */}
<Stack.Screen name="u/[username]/index" options={{ title: '' }} />
<Stack.Screen name="u/[username]/followers" options={{ title: 'Followers' }} />
<Stack.Screen name="u/[username]/following" options={{ title: 'Following' }} />
<Stack.Screen name="report/[username]" options={{ title: 'Report', presentation: 'modal' }} />
<Stack.Screen name="blocked" options={{ title: 'Blocked accounts' }} />
<Stack.Screen name="change-password" options={{ title: 'Change password', presentation: 'modal' }} />
<Stack.Screen name="change-email" options={{ title: 'Change email', presentation: 'modal' }} />
<Stack.Screen name="scan" options={{ title: 'Scan a code', presentation: 'modal' }} />
<Stack.Screen name="notifications" options={{ title: 'Notifications' }} />
<Stack.Screen name="t/[id]" options={{ title: '' }} />
<Stack.Screen name="play/[id]" options={{ title: 'Comments' }} />
<Stack.Screen name="send/[trackId]" options={{ title: 'Send to friends', presentation: 'modal' }} />
</Stack.Protected>
<Stack.Protected guard={!signedIn}>
<Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="register" options={{ title: 'Create account' }} />
<Stack.Screen name="forgot-password" options={{ title: 'Reset password' }} />
</Stack.Protected>
</Stack>
</SWRConfig>
);
}
/** iOS: mirror the Music app live while the app is open, once Apple Music is connected. */
function AppleMusicLive() {
const { data: connections } = useSWR<ConnectionDTO[]>('/connections');
useAppleMusicLive(Boolean(connections?.some((c) => c.provider === 'apple_music' && c.status === 'active')));
return null;
}
const styles = StyleSheet.create({
offline: { flex: 1, backgroundColor: colors.bg, justifyContent: 'center', padding: space.xl, gap: space.md },
});