440 lines
14 KiB
Dart
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'),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|