mobile-music/lib/main.dart
2026-08-06 21:32:28 -04:00

85 lines
3.5 KiB
Dart

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<void> 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))),
);
}
}