349 lines
12 KiB
Dart
349 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:media_kit/media_kit.dart';
|
||
import 'package:audio_service/audio_service.dart' as audio_service;
|
||
import 'package:permission_handler/permission_handler.dart';
|
||
|
||
import 'services/audio_service.dart';
|
||
import 'services/playback_service.dart';
|
||
import 'services/app_lifecycle_service.dart';
|
||
import 'services/webdav_service.dart';
|
||
import 'services/audio_player_handler.dart';
|
||
import 'services/app_lifecycle_manager.dart';
|
||
import 'pages/home_page.dart';
|
||
import 'pages/player_page.dart';
|
||
import 'pages/playlist_page.dart';
|
||
import 'widgets/global_mini_player.dart';
|
||
|
||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||
final stopwatch = Stopwatch();
|
||
|
||
// ⭐ 防止 Hot Restart / 热重载时重复初始化
|
||
bool _appInitialized = false;
|
||
|
||
// ⭐ 全局缓存 Handler
|
||
AudioPlayerHandler? _audioHandler;
|
||
|
||
// ⭐ 全局缓存 LifecycleManager
|
||
final AppLifecycleManager lifecycleManager = AppLifecycleManager();
|
||
|
||
void main() async {
|
||
// ⭐ P0:防止重复初始化
|
||
if (_appInitialized) {
|
||
debugPrint('⏱️ main 已初始化,跳过重复执行');
|
||
return;
|
||
}
|
||
_appInitialized = true;
|
||
|
||
stopwatch.start();
|
||
debugPrint('⏱️ T0 main: ${stopwatch.elapsedMilliseconds}ms');
|
||
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
debugPrint('⏱️ T0.5 ensureInitialized: ${stopwatch.elapsedMilliseconds}ms');
|
||
|
||
// ⭐ MediaKit 在 runApp 前初始化(官方要求)
|
||
try {
|
||
MediaKit.ensureInitialized();
|
||
debugPrint('⏱️ T0.6 MediaKit 初始化完成');
|
||
} catch (e) {
|
||
debugPrint('❌ MediaKit 初始化失败: $e');
|
||
}
|
||
|
||
// ⭐ 创建 Handler(AudioService 稍后初始化)
|
||
_audioHandler = AudioPlayerHandler();
|
||
|
||
runApp(
|
||
MultiProvider(
|
||
providers: [
|
||
ChangeNotifierProvider(create: (_) => AudioService()),
|
||
ChangeNotifierProvider(create: (_) => AppLifecycleService()),
|
||
// ⭐ 生命周期管理器
|
||
ChangeNotifierProvider<AppLifecycleManager>(
|
||
create: (_) => lifecycleManager,
|
||
),
|
||
Provider<AudioPlayerHandler>(create: (_) => _audioHandler!),
|
||
],
|
||
child: const QTPlayerApp(),
|
||
),
|
||
);
|
||
|
||
debugPrint('⏱️ T0.8 runApp 完成: ${stopwatch.elapsedMilliseconds}ms');
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// 路由管理器(控制 MiniPlayer 显示/隐藏)
|
||
// ════════════════════════════════════════════════════════════
|
||
class RouteManager extends ChangeNotifier {
|
||
static final RouteManager _instance = RouteManager._internal();
|
||
factory RouteManager() => _instance;
|
||
RouteManager._internal();
|
||
|
||
String _currentRoute = '/';
|
||
String get currentRoute => _currentRoute;
|
||
|
||
bool get showMiniPlayer =>
|
||
_currentRoute != '/player' && _currentRoute != '/playlist';
|
||
|
||
void updateRoute(String route) {
|
||
if (_currentRoute != route) {
|
||
_currentRoute = route;
|
||
debugPrint('📌 路由更新: $route');
|
||
notifyListeners();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// 导航观察者(监听路由变化)
|
||
// ════════════════════════════════════════════════════════════
|
||
class MiniPlayerNavigatorObserver extends NavigatorObserver {
|
||
void _updateRoute(Route? route) {
|
||
final name = route?.settings.name ?? '/';
|
||
RouteManager().updateRoute(name);
|
||
}
|
||
|
||
@override
|
||
void didPush(Route route, Route? previousRoute) {
|
||
_updateRoute(route);
|
||
}
|
||
|
||
@override
|
||
void didPop(Route route, Route? previousRoute) {
|
||
_updateRoute(previousRoute);
|
||
}
|
||
|
||
@override
|
||
void didReplace({Route? newRoute, Route? oldRoute}) {
|
||
_updateRoute(newRoute);
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// 主应用
|
||
// ════════════════════════════════════════════════════════════
|
||
class QTPlayerApp extends StatefulWidget {
|
||
const QTPlayerApp({super.key});
|
||
|
||
@override
|
||
State<QTPlayerApp> createState() => _QTPlayerAppState();
|
||
}
|
||
|
||
class _QTPlayerAppState extends State<QTPlayerApp> {
|
||
// ⭐ build 计数器(诊断用)
|
||
static int _buildCount = 0;
|
||
|
||
bool _isInitializing = false;
|
||
|
||
// ⭐ 保存生命周期监听器引用
|
||
void Function(AppLifecycleStatus)? _lifecycleListener;
|
||
|
||
// ============================================================
|
||
// 生命周期
|
||
// ============================================================
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
debugPrint('⏱️ T1.5 initState: ${stopwatch.elapsedMilliseconds}ms');
|
||
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
debugPrint('⏱️ T2 首帧回调: ${stopwatch.elapsedMilliseconds}ms');
|
||
|
||
// ⭐ 1. 初始化生命周期管理器
|
||
lifecycleManager.init();
|
||
|
||
// ⭐ 2. 绑定生命周期到播放器
|
||
_bindLifecycleToPlayback();
|
||
|
||
// ⭐ 3. 后台初始化
|
||
_startBackgroundInitialization();
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
// ⭐ 移除生命周期监听器
|
||
if (_lifecycleListener != null) {
|
||
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
|
||
}
|
||
super.dispose();
|
||
}
|
||
|
||
// ============================================================
|
||
// 生命周期 → 播放器绑定
|
||
// ============================================================
|
||
void _bindLifecycleToPlayback() {
|
||
if (_lifecycleListener != null) {
|
||
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
|
||
}
|
||
|
||
_lifecycleListener = (status) {
|
||
final audioService = context.read<AudioService>();
|
||
|
||
switch (status) {
|
||
case AppLifecycleStatus.paused:
|
||
case AppLifecycleStatus.inactive:
|
||
debugPrint('🎵 App 进入后台,播放继续');
|
||
break;
|
||
|
||
case AppLifecycleStatus.resumed:
|
||
debugPrint('🎵 App 回到前台');
|
||
break;
|
||
|
||
case AppLifecycleStatus.backgroundIdle:
|
||
debugPrint('🎵 长后台唤醒,检查播放状态');
|
||
break;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
};
|
||
|
||
lifecycleManager.addListenerToManager(_lifecycleListener!);
|
||
}
|
||
|
||
// ============================================================
|
||
// 后台初始化(精细时间切片)
|
||
// ============================================================
|
||
Future<void> _startBackgroundInitialization() async {
|
||
if (_isInitializing) return;
|
||
_isInitializing = true;
|
||
|
||
final tStart = DateTime.now();
|
||
debugPrint('⏱️ T3 后台初始化开始');
|
||
|
||
// ----- 1. PlaybackService -----
|
||
final t1 = DateTime.now();
|
||
try {
|
||
PlaybackService().init();
|
||
debugPrint('⏱️ T3.1 PlaybackService 完成: ${_elapsedMs(t1)}ms');
|
||
} catch (e) {
|
||
debugPrint('❌ PlaybackService 失败: $e');
|
||
}
|
||
|
||
// ----- 2. WebDAV(只加载凭证) -----
|
||
final t2 = DateTime.now();
|
||
try {
|
||
final hasCred = await WebDAVService.instance.loadCredentials();
|
||
debugPrint('⏱️ T3.2 WebDAV 凭证加载完成: ${_elapsedMs(t2)}ms, 有凭证: $hasCred');
|
||
} catch (e) {
|
||
debugPrint('❌ WebDAV 凭证加载失败: $e');
|
||
}
|
||
|
||
// ----- 3. ⭐ AudioService(后台播放核心) -----
|
||
await _initAudioService();
|
||
|
||
// ----- 4. 通知权限 -----
|
||
final t3 = DateTime.now();
|
||
try {
|
||
if (await Permission.notification.isDenied) {
|
||
await Permission.notification.request();
|
||
debugPrint('⏱️ T3.3 通知权限完成: ${_elapsedMs(t3)}ms');
|
||
} else {
|
||
debugPrint('⏱️ T3.3 通知权限已授予: ${_elapsedMs(t3)}ms');
|
||
}
|
||
} catch (e) {
|
||
debugPrint('❌ 权限申请失败: $e');
|
||
}
|
||
|
||
final tEnd = DateTime.now();
|
||
debugPrint('⏱️ T4 全部初始化完成: ${tEnd.difference(tStart).inMilliseconds}ms');
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// ⭐ AudioService 初始化(后台播放核心)
|
||
// ════════════════════════════════════════════════════════════
|
||
Future<void> _initAudioService() async {
|
||
try {
|
||
await audio_service.AudioService.init(
|
||
builder: () => _audioHandler!,
|
||
config: const audio_service.AudioServiceConfig(
|
||
androidNotificationChannelId: 'com.example.qt_player.music',
|
||
androidNotificationChannelName: '清听音乐播放',
|
||
androidNotificationOngoing: true,
|
||
androidStopForegroundOnPause: true,
|
||
),
|
||
);
|
||
debugPrint('✅ AudioService 初始化完成');
|
||
} catch (e) {
|
||
debugPrint('❌ AudioService 初始化失败: $e');
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// 辅助方法
|
||
// ════════════════════════════════════════════════════════════
|
||
String _elapsedMs(DateTime start) {
|
||
return '${DateTime.now().difference(start).inMilliseconds}';
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// Build
|
||
// ════════════════════════════════════════════════════════════
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
_buildCount++;
|
||
debugPrint(
|
||
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
|
||
|
||
// 监听歌曲变化,更新通知栏
|
||
final audioService = context.watch<AudioService>();
|
||
final handler = context.read<AudioPlayerHandler>();
|
||
final song = audioService.currentSong;
|
||
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (song != null) {
|
||
handler.updateNotification(
|
||
id: song.id,
|
||
title: song.title,
|
||
artist: song.artist,
|
||
);
|
||
} else {
|
||
handler.mediaItem.add(null);
|
||
}
|
||
});
|
||
|
||
return MaterialApp(
|
||
title: '清听',
|
||
theme: ThemeData(
|
||
brightness: Brightness.dark,
|
||
primaryColor: const Color(0xFF7C9A9E),
|
||
colorScheme: const ColorScheme.dark(
|
||
primary: Color(0xFF7C9A9E),
|
||
secondary: Color(0xFFB8D4D0),
|
||
surface: Color(0xFF1A1F1E),
|
||
onSurface: Colors.white,
|
||
),
|
||
useMaterial3: true,
|
||
),
|
||
navigatorKey: navigatorKey,
|
||
navigatorObservers: [MiniPlayerNavigatorObserver()],
|
||
routes: {
|
||
'/': (context) => const HomePage(),
|
||
'/player': (context) => const PlayerPage(),
|
||
'/playlist': (context) => const PlaylistPage(),
|
||
},
|
||
builder: (context, child) {
|
||
return Stack(
|
||
fit: StackFit.expand,
|
||
children: [
|
||
if (child != null) child,
|
||
AnimatedBuilder(
|
||
animation: RouteManager(),
|
||
builder: (context, _) {
|
||
if (!RouteManager().showMiniPlayer) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
return const Align(
|
||
alignment: Alignment.bottomCenter,
|
||
child: GlobalMiniPlayer(),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|