nkbmusic/lib/app/router.dart
metatroncubeswdev 178d2d00ab Nanga Kathai: Flutter app, self-hosted backend and song tools
- Flutter app (Android/iOS): home, explore, albums, search, player with
  equalizer, mini player, sign up/sign in with email codes, profile,
  encrypted offline downloads, About page
- server/: one-command deploy of self-hosted Supabase behind HestiaCP/nginx
  (or Caddy), migrations, backups, email templates
- supabase/migrations: schema, row-level security, profiles
- tools/: song scanner and importer

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

152 lines
4.8 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/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()),
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,
),
),
),
],
);
});