284 lines
9.3 KiB
Dart
284 lines
9.3 KiB
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 '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;
|
||
|
||
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(AudioService 稍后初始化)
|
||
_audioHandler = AudioPlayerHandler();
|
||
|
||
runApp(
|
||
MultiProvider(
|
||
providers: [
|
||
ChangeNotifierProvider(create: (_) => AudioService()),
|
||
ChangeNotifierProvider(create: (_) => AppLifecycleService()),
|
||
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> {
|
||
bool _isInitializing = false;
|
||
|
||
// ============================================================
|
||
// 生命周期
|
||
// ============================================================
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
debugPrint('⏱️ T1.5 initState: ${stopwatch.elapsedMilliseconds}ms');
|
||
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
debugPrint('⏱️ T2 首帧回调: ${stopwatch.elapsedMilliseconds}ms');
|
||
_startBackgroundInitialization();
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
super.dispose();
|
||
}
|
||
|
||
// ============================================================
|
||
// 后台初始化(不阻塞首帧)
|
||
// ============================================================
|
||
Future<void> _startBackgroundInitialization() async {
|
||
if (_isInitializing) return;
|
||
_isInitializing = true;
|
||
|
||
debugPrint('⏱️ T3 后台初始化开始');
|
||
|
||
// 1. PlaybackService(轻量级)
|
||
try {
|
||
PlaybackService().init();
|
||
debugPrint('⏱️ T3.1 PlaybackService 初始化完成');
|
||
} catch (e) {
|
||
debugPrint('❌ PlaybackService 初始化失败: $e');
|
||
}
|
||
|
||
// 2. ⭐ AudioService 暂时禁用(等 MainActivity 改为 AudioServiceActivity 后启用)
|
||
// await _initAudioService();
|
||
|
||
// 3. WebDAV
|
||
await _initWebDAV();
|
||
|
||
// 4. 通知权限
|
||
await _requestPermissions();
|
||
|
||
debugPrint('⏱️ T4 全部初始化完成');
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// 子初始化方法
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
// ⭐ 暂时禁用,等 MainActivity 改为 AudioServiceActivity 后启用
|
||
// 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');
|
||
// }
|
||
// }
|
||
|
||
Future<void> _initWebDAV() async {
|
||
try {
|
||
final hasCred = await WebDAVService.instance.loadCredentials();
|
||
debugPrint('✅ WebDAV 加载完成, 已连接: $hasCred');
|
||
} catch (e) {
|
||
debugPrint('❌ WebDAV 加载失败: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _requestPermissions() async {
|
||
try {
|
||
if (await Permission.notification.isDenied) {
|
||
final status = await Permission.notification.request();
|
||
debugPrint('📢 通知权限状态: $status');
|
||
}
|
||
} catch (e) {
|
||
debugPrint('❌ 权限申请失败: $e');
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Build
|
||
// ============================================================
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
debugPrint('⏱️ T1 build: ${stopwatch.elapsedMilliseconds}ms');
|
||
|
||
// 监听歌曲变化,更新通知栏
|
||
final audioService = context.watch<AudioService>();
|
||
final handler = context.read<AudioPlayerHandler>();
|
||
final song = audioService.currentSong;
|
||
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (song != null) {
|
||
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(),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|