- 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>
170 lines
5.6 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
});
|