Files
QingTing-Player/lib/main.dart
T

392 lines
13 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.
// 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 '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
_audioHandler = AudioPlayerHandler();
// ⭐ 注册切歌回调
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,
);
});
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((_) {
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();
}
}
}
// ════════════════════════════════════════════════════════════
// 导航观察者(监听路由变化)
// ════════════════════════════════════════════════════════════
// lib/main.dart
class MiniPlayerNavigatorObserver extends NavigatorObserver {
void _updateRoute(Route? route) {
if (route == null) return;
// ⭐ 忽略 PopupRoute(菜单、对话框等)
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) {
// ⭐ 返回时,使用上一个路由(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}';
}
// ⭐ 路由生成(播放页滑入动画,速度 200ms)
// lib/main.dart - _onGenerateRoute 方法
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, // 平滑减速
// curve: Curves.easeOutBack, // 轻微回弹,更灵动
// curve: Curves.fastOutSlowIn, // Material 风格
);
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: 300),
);
}
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
return const HomePage();
case '/playlist':
return const PlaylistPage();
default:
return const HomePage();
}
},
);
}
@override
Widget build(BuildContext context) {
_buildCount++;
debugPrint(
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
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()],
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'),
),
);
},
),
],
);
},
);
}
}