nkbmusic/lib/main.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

119 lines
3.9 KiB
Dart

import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'app/router.dart';
import 'app/theme.dart';
import 'audio/nk_audio_handler.dart';
import 'audio/player_providers.dart';
import 'audio/sound_settings.dart';
import 'core/env.dart';
import 'data/user_library.dart';
import 'offline/download_store.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Env.hasSupabase) {
await Supabase.initialize(url: Env.supabaseUrl, publishableKey: Env.supabasePublishableKey);
}
final prefs = await SharedPreferences.getInstance();
final audioHandler = await AudioService.init(
builder: NkAudioHandler.new,
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.metatroncubesolutions.nanga_kathai.playback',
androidNotificationChannelName: 'Nanga Kathai playback',
androidNotificationOngoing: true,
androidStopForegroundOnPause: true,
),
);
final downloads = await DownloadStore.open();
audioHandler.localSource = (item) => downloads.sourceFor(item.id, tag: item);
runApp(
ProviderScope(
overrides: [
sharedPrefsProvider.overrideWithValue(prefs),
audioHandlerProvider.overrideWithValue(audioHandler),
downloadStoreProvider.overrideWithValue(downloads),
],
child: const NangaKathaiApp(),
),
);
}
class NangaKathaiApp extends ConsumerWidget {
const NangaKathaiApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(soundSettingsProvider); // apply the saved equalizer and boost at startup
final router = ref.watch(routerProvider);
// Guests hear a short preview of each song, then are invited to sign up.
ref.listen(positionProvider, (_, next) {
final position = next.value ?? Duration.zero;
if (!Env.hasSupabase || isSignedIn || position < guestPreview) return;
final handler = ref.read(audioHandlerProvider);
if (!handler.playbackState.value.playing) return;
handler
..pause()
..seek(Duration.zero);
final context = router.routerDelegate.navigatorKey.currentContext;
if (context != null) _showPreviewEnded(context);
});
return MaterialApp.router(
title: 'Nanga Kathai',
debugShowCheckedModeBanner: false,
theme: buildNkTheme(),
routerConfig: router,
);
}
}
const guestPreview = Duration(seconds: 30);
void _showPreviewEnded(BuildContext context) {
showModalBottomSheet<void>(
context: context,
backgroundColor: NkColors.surface,
showDragHandle: true,
builder: (sheet) => SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Enjoying the song?', style: Theme.of(sheet).textTheme.headlineSmall),
const SizedBox(height: 8),
const Text(
'Previews are 30 seconds. Create a free account to listen to every song in full.',
style: TextStyle(color: NkColors.muted, height: 1.5),
),
const SizedBox(height: 20),
FilledButton(
onPressed: () {
Navigator.pop(sheet);
GoRouter.of(context).push('/login?mode=signup');
},
child: const Text('Create free account'),
),
TextButton(
onPressed: () {
Navigator.pop(sheet);
GoRouter.of(context).push('/login');
},
child: const Text('I already have an account'),
),
],
),
),
),
);
}