nkbmusic/lib/app/router.dart
metatroncubeswdev ad8c4f1c6b App: speaker/Bluetooth picker, Settings & help, Privacy Policy, Terms, FAQ, song reports
- Now Playing shows where music plays and opens the phone's output switcher
- Settings hub: Wi-Fi-only downloads (default on), storage, sound, help, legal
- Privacy Policy and Terms bundled as Markdown (assets/legal), shown in-app
- 'Is this your song?' report form feeding takedown_requests (+ kind, user_id)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:00:16 -04:00

170 lines
5.6 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../core/env.dart';
import '../core/models.dart';
import '../data/user_library.dart';
import '../features/artist/artist_screen.dart';
import '../features/auth/login_screen.dart';
import '../features/downloads/downloads_screen.dart';
import '../features/explore/explore_screen.dart';
import '../features/explore/occasion_screen.dart';
import '../features/home/home_screen.dart';
import '../features/library/library_screen.dart';
import '../features/profile/profile_screen.dart';
import '../features/player/now_playing_screen.dart';
import '../features/about/about_screen.dart';
import '../features/album/album_screen.dart';
import '../features/search/search_screen.dart';
import '../features/settings/document_screen.dart';
import '../features/settings/help_screen.dart';
import '../features/settings/report_screen.dart';
import '../features/settings/settings_screen.dart';
import '../features/welcome/splash_screen.dart';
import '../shared/widgets/app_shell.dart';
/// Whether the welcome + sign-in screens have been seen on this device.
class Onboarding extends Notifier<bool> {
static const _key = 'onboarding_done';
@override
bool build() => ref.watch(sharedPrefsProvider).getBool(_key) ?? false;
void complete() {
state = true;
ref.read(sharedPrefsProvider).setBool(_key, true);
}
}
final onboardingProvider = NotifierProvider<Onboarding, bool>(Onboarding.new);
/// Re-runs the router redirect when someone signs in or out.
class _AuthRefresh extends ChangeNotifier {
_AuthRefresh() {
if (Env.hasSupabase) {
_sub = Supabase.instance.client.auth.onAuthStateChange.listen((_) => notifyListeners());
}
}
StreamSubscription<AuthState>? _sub;
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
}
/// Signed in to the server. Guests (and demo builds with no server) are not.
bool get isSignedIn => Env.hasSupabase && Supabase.instance.client.auth.currentSession != null;
final routerProvider = Provider<GoRouter>((ref) {
final onboarded = ref.read(onboardingProvider);
final authRefresh = _AuthRefresh();
ref.onDispose(authRefresh.dispose);
return GoRouter(
initialLocation: onboarded ? '/home' : '/welcome',
refreshListenable: authRefresh,
// Guests can browse (with 30-second previews); the profile needs an account.
redirect: (_, state) {
final at = state.matchedLocation;
if (!isSignedIn && at == '/profile') return '/login';
if (isSignedIn && at == '/login') return '/home';
return null;
},
routes: [
GoRoute(path: '/welcome', builder: (_, _) => const SplashScreen()),
GoRoute(
path: '/login',
builder: (_, state) => LoginScreen(startWithSignUp: state.uri.queryParameters['mode'] == 'signup'),
),
GoRoute(path: '/profile', builder: (_, _) => const ProfileScreen()),
GoRoute(path: '/about', builder: (_, _) => const AboutScreen()),
GoRoute(path: '/settings', builder: (_, _) => const SettingsScreen()),
GoRoute(path: '/help', builder: (_, _) => const HelpScreen()),
GoRoute(
path: '/privacy',
builder: (_, _) => const DocumentScreen(title: 'Privacy Policy', asset: 'assets/legal/privacy.md'),
),
GoRoute(
path: '/terms',
builder: (_, _) => const DocumentScreen(title: 'Terms of Use', asset: 'assets/legal/terms.md'),
),
GoRoute(
path: '/report',
builder: (_, state) => ReportScreen(trackId: state.uri.queryParameters['track']),
),
StatefulShellRoute.indexedStack(
builder: (_, _, shell) => AppShell(shell: shell),
branches: [
StatefulShellBranch(
routes: [
GoRoute(path: '/home', builder: (_, _) => const HomeScreen()),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/explore',
builder: (_, _) => const ExploreScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/library',
builder: (_, _) => const LibraryScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/downloads',
builder: (_, _) => const DownloadsScreen(),
),
],
),
],
),
GoRoute(path: '/search', builder: (_, _) => const SearchScreen()),
GoRoute(
path: '/artist/:id',
builder: (_, state) =>
ArtistScreen(artistId: state.pathParameters['id']!),
),
GoRoute(
path: '/album/:id',
builder: (_, state) =>
AlbumScreen(albumId: state.pathParameters['id']!),
),
GoRoute(
path: '/occasion/:id',
builder: (_, state) => OccasionScreen(
occasion: Occasion.fromId(state.pathParameters['id']),
),
),
GoRoute(
path: '/player',
pageBuilder: (_, state) => CustomTransitionPage(
key: state.pageKey,
fullscreenDialog: true,
child: const NowPlayingScreen(),
transitionsBuilder: (_, animation, _, child) => SlideTransition(
position: Tween(
begin: const Offset(0, 1),
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeOutCubic)).animate(animation),
child: child,
),
),
),
],
);
});