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
135 lines
5.9 KiB
TypeScript
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'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 },
|
|
});
|