- 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>
119 lines
3.9 KiB
Dart
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'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|