388 lines
12 KiB
Dart
388 lines
12 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 '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');
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// 路由管理器(控制 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'),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|