import 'dart:io' show Platform; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:just_audio_background/just_audio_background.dart'; import 'package:just_audio_media_kit/just_audio_media_kit.dart'; import 'settings/settings_store.dart'; import 'shell/app_shell.dart'; import 'state/providers.dart'; import 'theme/accent.dart'; import 'theme/app_theme.dart'; import 'theme/tokens.dart'; import 'widgets/splash_screen.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); // Desktop (Linux/Windows) has no native just_audio backend; route playback // through libmpv via media_kit. Audio then lands on the system mixer // (PipeWire/PulseAudio), where a system EQ (e.g. EasyEffects) can shape it. // macOS/iOS/Android keep just_audio's own native backends. if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) { JustAudioMediaKit.ensureInitialized(linux: true, windows: true); } // Lock-screen / notification transport is only available where audio_service // has a backend — Android/iOS. Skipping init elsewhere keeps the Linux dev // target (and tests) running. if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { // Never let background-audio setup block the first frame: on failure or // hang we still start the app (playback just loses lock-screen controls). try { await JustAudioBackground.init( androidNotificationChannelId: 'com.laforrestchurch.timbre.audio', androidNotificationChannelName: 'Timbre playback', androidNotificationOngoing: true, ).timeout(const Duration(seconds: 8)); } catch (e, st) { debugPrint('JustAudioBackground.init failed: $e\n$st'); } } runApp(const ProviderScope(child: TimbreApp())); } class TimbreApp extends ConsumerWidget { const TimbreApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(settingsProvider); // Point the token getters at the selected theme's palette before the theme // (and the widget tree) read them. final palette = settings.appTheme == AppTheme.lavender ? TimbrePalette.lavender : TimbrePalette.dark; TimbreColors.applyPalette(palette); // The accent either tracks the album art (default theme, dynamic mode) or // is pinned — to the user's static color, or to the theme's own accent when // the theme locks it (Lavender). The theme rebuilds whenever it changes. final Color accent; if (settings.appTheme == AppTheme.lavender) { accent = palette.accentDefault; // locked lavender primary } else if (settings.useStaticAccent) { accent = settings.staticAccentColor; } else { accent = ref.watch(accentProvider); } // Keep the accent synced with the remote track's art while acting as a // remote (bug #6). Alive as long as the app is. ref.watch(remoteAccentSyncProvider); return MaterialApp( title: 'Timbre', debugShowCheckedModeBanner: false, theme: buildTimbreTheme(accent), // AppShell mounts under the splash and boots behind it; the splash fades // out after a couple of seconds. Keying AppShell by the theme forces a // remount on theme switch so every widget re-reads the (now runtime) // TimbreColors getters; providers live above TimbreApp, so playback and // library state survive the remount. home: SplashGate(child: AppShell(key: ValueKey(settings.appTheme))), ); } }