Files
QingTing-Player/lib/main.dart
T

440 lines
14 KiB
Dart

// lib/main.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 'package:flutter/services.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';
import 'utils/navigation.dart';
final stopwatch = Stopwatch();
bool _appInitialized = false;
AudioPlayerHandler? _audioHandler;
final AppLifecycleManager lifecycleManager = AppLifecycleManager();
void main() async {
if (_appInitialized) {
debugPrint('⏱️ main 已初始化,跳过重复执行');
return;
}
_appInitialized = true;
stopwatch.start();
debugPrint('⏱️ T0 main: ${stopwatch.elapsedMilliseconds}ms');
WidgetsFlutterBinding.ensureInitialized();
debugPrint('⏱️ T0.5 ensureInitialized: ${stopwatch.elapsedMilliseconds}ms');
debugPrint('⏱️ T0.55 before MediaKit.ensureInitialized');
try {
MediaKit.ensureInitialized();
debugPrint('⏱️ T0.6 MediaKit.ensureInitialized returned');
} catch (e, st) {
debugPrint('❌ MediaKit.ensureInitialized failed: $e');
debugPrint('$st');
rethrow;
}
debugPrint('⏱️ T0.61 before AudioPlayerHandler');
try {
_audioHandler = AudioPlayerHandler();
debugPrint('⏱️ T0.62 AudioPlayerHandler created');
// ⭐ 将 Handler 注入到 AudioService
AudioService().setHandler(_audioHandler!);
} catch (e, st) {
debugPrint('❌ AudioPlayerHandler failed: $e');
debugPrint('$st');
rethrow;
}
debugPrint('⏱️ T0.63 before setOnSongChanged');
AudioService().setOnSongChanged((song) {
debugPrint(
'📢 [main] song.artwork is '
'${song.artwork != null ? 'not null' : 'null'}',
);
_audioHandler!.updateNotification(
id: song.id,
title: song.title,
artist: song.artist,
artwork: song.artwork,
);
});
debugPrint('⏱️ T0.7 before runApp');
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',
);
WidgetsBinding.instance.addPostFrameCallback((_) {
debugPrint('⏱️ T1 postFrame');
AudioService().restorePlaybackState();
});
}
// ════════════════════════════════════════════════════════════
// 路由管理器(控制 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) {
if (route == null) return;
if (route is PopupRoute) {
return;
}
final name = route.settings.name ?? '/';
RouteManager().updateRoute(name);
}
@override
void didPush(Route route, Route? previousRoute) {
_updateRoute(route);
}
@override
void didPop(Route route, Route? previousRoute) {
if (previousRoute != null && previousRoute is! PopupRoute) {
final name = previousRoute.settings.name ?? '/';
RouteManager().updateRoute(name);
} else if (previousRoute == null) {
RouteManager().updateRoute('/');
}
}
@override
void didReplace({Route? newRoute, Route? oldRoute}) {
if (newRoute != null && newRoute is! PopupRoute) {
final name = newRoute.settings.name ?? '/';
RouteManager().updateRoute(name);
}
}
}
// ════════════════════════════════════════════════════════════
// 主应用
// ════════════════════════════════════════════════════════════
class QTPlayerApp extends StatefulWidget {
const QTPlayerApp({super.key});
@override
State<QTPlayerApp> createState() => _QTPlayerAppState();
}
class _QTPlayerAppState extends State<QTPlayerApp> {
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');
lifecycleManager.init();
_bindLifecycleToPlayback();
_startBackgroundInitialization();
});
}
@override
void dispose() {
if (_lifecycleListener != null) {
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
}
super.dispose();
}
void _bindLifecycleToPlayback() {
if (_lifecycleListener != null) {
lifecycleManager.removeListenerFromManager(_lifecycleListener!);
}
_lifecycleListener = (status) {
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 后台初始化开始');
final t1 = DateTime.now();
try {
PlaybackService().init();
debugPrint('⏱️ T3.1 PlaybackService 完成: ${_elapsedMs(t1)}ms');
} catch (e) {
debugPrint('❌ PlaybackService 失败: $e');
}
final t2 = DateTime.now();
try {
final hasCred = await WebDAVService.instance.loadCredentials();
debugPrint('⏱️ T3.2 WebDAV 凭证加载完成: ${_elapsedMs(t2)}ms, 有凭证: $hasCred');
} catch (e) {
debugPrint('❌ WebDAV 凭证加载失败: $e');
}
await _initAudioService();
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');
}
Future<void> _initAudioService() async {
try {
await audio_service.AudioService.init(
builder: () => _audioHandler!,
config: const audio_service.AudioServiceConfig(
androidNotificationChannelId: 'com.lxh.qingting_player.music',
androidNotificationChannelName: '清听',
androidNotificationOngoing: false,
androidStopForegroundOnPause: false,
),
);
debugPrint('✅ AudioService 初始化完成');
} catch (e) {
debugPrint('❌ AudioService 初始化失败: $e');
}
}
String _elapsedMs(DateTime start) {
return '${DateTime.now().difference(start).inMilliseconds}';
}
// ════════════════════════════════════════════════════════════
// ⭐ 路由生成(播放页 & 播放列表页底部滑入)
// ════════════════════════════════════════════════════════════
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
// ⭐ 播放页:底部滑入
if (settings.name == '/player') {
return PageRouteBuilder(
settings: settings,
pageBuilder: (context, animation, secondaryAnimation) {
return const PlayerPage();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
);
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
final tween = Tween(begin: begin, end: end);
final offsetAnimation = tween.animate(curvedAnimation);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
transitionDuration: const Duration(milliseconds: 250),
);
}
// ⭐ 播放列表页:底部滑入(与播放页一致)
if (settings.name == '/playlist') {
return PageRouteBuilder(
settings: settings,
pageBuilder: (context, animation, secondaryAnimation) {
return const PlaylistPage();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
);
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
final tween = Tween(begin: begin, end: end);
final offsetAnimation = tween.animate(curvedAnimation);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
transitionDuration: const Duration(milliseconds: 250),
);
}
// 其他页面使用默认
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
return const HomePage();
default:
return const HomePage();
}
},
);
}
@override
Widget build(BuildContext context) {
_buildCount++;
debugPrint(
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
// ⭐ 使用 AnnotatedRegion 包裹 MaterialApp,统一控制系统栏样式
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: const Color(0xFF1A1F1E),
systemNavigationBarIconBrightness: Brightness.light,
),
child: 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()],
onGenerateRoute: _onGenerateRoute,
builder: (context, child) {
return Stack(
fit: StackFit.expand,
children: [
if (child != null) child,
AnimatedBuilder(
animation: RouteManager(),
builder: (context, _) {
final routeManager = RouteManager();
final shouldShow = routeManager.currentRoute != '/player' &&
routeManager.currentRoute != '/playlist';
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: shouldShow
? const Align(
key: ValueKey('mini_player_visible'),
alignment: Alignment.bottomCenter,
child: GlobalMiniPlayer(),
)
: const SizedBox.shrink(
key: ValueKey('mini_player_hidden'),
),
);
},
),
],
);
},
),
);
}
}