Files
QingTing-Player/lib/main.dart
T

352 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
}
// ⭐ 创建 HandlerAudioService 稍后初始化)
_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');
// 监听歌曲变化,更新通知栏
// main.dart 中 build 方法里的这部分
final audioService = context.watch<AudioService>();
final handler = context.read<AudioPlayerHandler>();
final song = audioService.currentSong;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (song != null) {
// ⭐ song 不为空时才调用
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(),
);
},
),
],
);
},
);
}
}