252 lines
7.0 KiB
Dart
252 lines
7.0 KiB
Dart
// lib/main.dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:media_kit/media_kit.dart';
|
|
import 'services/audio_service.dart';
|
|
import 'services/playback_service.dart';
|
|
import 'pages/home_page.dart';
|
|
import 'pages/player_page.dart';
|
|
import 'pages/playlist_page.dart';
|
|
import 'widgets/global_mini_player.dart';
|
|
|
|
void main() {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
bool initSuccess = false;
|
|
String? initError;
|
|
|
|
try {
|
|
MediaKit.ensureInitialized();
|
|
PlaybackService().init();
|
|
initSuccess = true;
|
|
} catch (e, stack) {
|
|
initError = '初始化失败: $e';
|
|
print('❌ $initError');
|
|
print(stack);
|
|
}
|
|
|
|
runApp(
|
|
ChangeNotifierProvider(
|
|
create: (_) => AudioService(),
|
|
child: QTPlayerApp(
|
|
initSuccess: initSuccess,
|
|
initError: initError,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
class QTPlayerApp extends StatefulWidget {
|
|
final bool initSuccess;
|
|
final String? initError;
|
|
|
|
const QTPlayerApp({
|
|
super.key,
|
|
required this.initSuccess,
|
|
required this.initError,
|
|
});
|
|
|
|
@override
|
|
State<QTPlayerApp> createState() => _QTPlayerAppState();
|
|
}
|
|
|
|
class _QTPlayerAppState extends State<QTPlayerApp>
|
|
with SingleTickerProviderStateMixin {
|
|
bool _showPlayerPage = false;
|
|
bool _showPlaylist = false;
|
|
|
|
late final AnimationController _animationController;
|
|
late final Animation<double> _slideAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
GlobalMiniPlayer.onOpenPlayerPage = _openPlayerPage;
|
|
GlobalMiniPlayer.onOpenPlaylistPage = _openPlaylist;
|
|
|
|
_animationController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 300),
|
|
);
|
|
|
|
_slideAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
|
CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: Curves.easeOutCubic,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _openPlayerPage() {
|
|
setState(() {
|
|
_showPlayerPage = true;
|
|
_showPlaylist = false;
|
|
});
|
|
_animationController.forward(from: 0.0);
|
|
}
|
|
|
|
void _closePlayerPage() {
|
|
_animationController.reverse().then((_) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_showPlayerPage = false;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
void _openPlaylist() {
|
|
setState(() {
|
|
_showPlaylist = true;
|
|
});
|
|
}
|
|
|
|
void _closePlaylist() {
|
|
setState(() {
|
|
_showPlaylist = false;
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// ✅ PopScope 的 onPopInvokedWithResult
|
|
// ============================================================
|
|
void _onPopInvokedWithResult(bool didPop, Object? result) {
|
|
if (didPop) return;
|
|
|
|
// 1️⃣ 优先关闭播放列表(最高层级)
|
|
if (_showPlaylist) {
|
|
_closePlaylist();
|
|
return;
|
|
}
|
|
|
|
// 2️⃣ 其次关闭播放页
|
|
if (_showPlayerPage) {
|
|
_closePlayerPage();
|
|
return;
|
|
}
|
|
|
|
// 3️⃣ 没有覆盖层,允许退出(由 Navigator 处理)
|
|
// 注意:这里不能直接退出,需要让 Navigator 处理
|
|
// 但 canPop 会根据状态返回 true/false
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!widget.initSuccess) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
backgroundColor: const Color(0xFF0E1211),
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 64, color: Colors.red[300]),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'应用初始化失败',
|
|
style: TextStyle(color: Colors.white, fontSize: 20),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
widget.initError ?? '未知错误',
|
|
style: TextStyle(color: Colors.grey[400], fontSize: 14),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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,
|
|
),
|
|
home: const HomePage(),
|
|
builder: (context, child) {
|
|
// ✅ 暴力测试:canPop 写死为 false
|
|
return PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, result) {
|
|
debugPrint('🔥🔥🔥 BACK FIRED: didPop=$didPop');
|
|
debugPrint('🔥 playlist=$_showPlaylist, player=$_showPlayerPage');
|
|
|
|
if (_showPlaylist) {
|
|
debugPrint('🔥 closing playlist');
|
|
_closePlaylist();
|
|
} else if (_showPlayerPage) {
|
|
debugPrint('🔥 closing player');
|
|
_closePlayerPage();
|
|
} else {
|
|
debugPrint('🔥 no overlay, allowing exit');
|
|
// 注意:这里 canPop=false,需要手动退出
|
|
// 用 SystemNavigator.pop() 或 Navigator.pop(context)
|
|
}
|
|
},
|
|
child: Overlay(
|
|
initialEntries: [
|
|
OverlayEntry(
|
|
builder: (context) => Stack(
|
|
children: [
|
|
Positioned.fill(child: child!),
|
|
const Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
child: GlobalMiniPlayer(),
|
|
),
|
|
if (_showPlayerPage)
|
|
Positioned.fill(
|
|
child: AnimatedBuilder(
|
|
animation: _animationController,
|
|
builder: (context, child) {
|
|
final progress = _slideAnimation.value;
|
|
final offsetY =
|
|
MediaQuery.of(context).size.height * progress;
|
|
return Transform.translate(
|
|
offset: Offset(0, offsetY),
|
|
child: Opacity(
|
|
opacity: 1.0 - progress * 0.3,
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
child: PlayerPage(
|
|
onClose: _closePlayerPage,
|
|
onOpenPlaylist: _openPlaylist,
|
|
),
|
|
),
|
|
),
|
|
if (_showPlaylist)
|
|
Positioned.fill(
|
|
child: PlaylistPage(
|
|
onClose: _closePlaylist,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|