页面层级与返回逻辑实现完成,并且去掉web相关代码支持

This commit is contained in:
2026-08-19 23:27:47 +08:00
parent d116814bb6
commit 81990385f4
13 changed files with 242 additions and 526 deletions
+91 -162
View File
@@ -1,4 +1,3 @@
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:media_kit/media_kit.dart';
@@ -9,6 +8,8 @@ import 'pages/player_page.dart';
import 'pages/playlist_page.dart';
import 'widgets/global_mini_player.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() {
WidgetsFlutterBinding.ensureInitialized();
@@ -21,13 +22,15 @@ void main() {
initSuccess = true;
} catch (e, stack) {
initError = '初始化失败: $e';
print('$initError');
print(stack);
debugPrint('$initError');
debugPrint(stack.toString());
}
runApp(
ChangeNotifierProvider(
create: (_) => AudioService(),
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AudioService()),
],
child: QTPlayerApp(
initSuccess: initSuccess,
initError: initError,
@@ -36,7 +39,60 @@ void main() {
);
}
class QTPlayerApp extends StatefulWidget {
// ═══════════════════════════════════════════════════
// RouteManager - 路由状态管理
// ═══════════════════════════════════════════════════
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) {
// ⭐ 关键:用 previousRoute 而不是 navigatorKey.currentContext
_updateRoute(previousRoute);
}
@override
void didReplace({Route? newRoute, Route? oldRoute}) {
_updateRoute(newRoute);
}
}
// ═══════════════════════════════════════════════════
// 主应用
// ═══════════════════════════════════════════════════
class QTPlayerApp extends StatelessWidget {
final bool initSuccess;
final String? initError;
@@ -46,99 +102,9 @@ class QTPlayerApp extends StatefulWidget {
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) {
if (!initSuccess) {
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF0E1211),
@@ -154,7 +120,7 @@ class _QTPlayerAppState extends State<QTPlayerApp>
),
const SizedBox(height: 8),
Text(
widget.initError ?? '未知错误',
initError ?? '未知错误',
style: TextStyle(color: Colors.grey[400], fontSize: 14),
textAlign: TextAlign.center,
),
@@ -178,72 +144,35 @@ class _QTPlayerAppState extends State<QTPlayerApp>
),
useMaterial3: true,
),
home: const HomePage(),
navigatorKey: navigatorKey,
navigatorObservers: [MiniPlayerNavigatorObserver()],
routes: {
'/': (context) => const HomePage(),
'/player': (context) => const PlayerPage(),
'/playlist': (context) => const PlaylistPage(),
},
// ⭐ 核心改动:删除手写 Overlay,直接用 Stack
builder: (context, child) {
// ✅ 暴力测试:canPop 写死为 false
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
debugPrint('🔥🔥🔥 BACK FIRED: didPop=$didPop');
debugPrint('🔥 playlist=$_showPlaylist, player=$_showPlayerPage');
return Stack(
fit: StackFit.expand,
children: [
// 1. Navigator(所有路由页面)
if (child != null) child,
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,
),
),
],
),
),
],
),
// 2. MiniPlayer(由路由状态控制显隐)
AnimatedBuilder(
animation: RouteManager(),
builder: (context, _) {
if (!RouteManager().showMiniPlayer) {
return const SizedBox.shrink();
}
return const Align(
alignment: Alignment.bottomCenter,
child: GlobalMiniPlayer(),
);
},
),
],
);
},
);
+2 -33
View File
@@ -1,4 +1,3 @@
// lib/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
@@ -7,7 +6,6 @@ import '../services/playback_service.dart';
import '../widgets/magnetic_scroll_physics.dart';
import 'webdav_setup_page.dart';
import 'webdav_file_list_page.dart';
import 'dart:async';
// ==================================================
// 歌曲数据模型
@@ -171,7 +169,6 @@ class _HomePageState extends State<HomePage> {
return (offset / _mediaLibraryHeight).clamp(0.0, 1.0);
}
// 媒体库透明度(快速淡出)
double get _mediaOpacity {
final p = _progress;
if (p <= 0.3) {
@@ -181,7 +178,6 @@ class _HomePageState extends State<HomePage> {
}
}
// 列表展开程度
double get _listReveal {
if (_progress <= 0.7) return 0.0;
return (_progress - 0.7) / 0.3;
@@ -204,7 +200,6 @@ class _HomePageState extends State<HomePage> {
super.dispose();
}
// ========== 高度测量(带重试限制) ==========
void _measureMediaLibraryHeight() {
final renderBox =
_mediaLibraryKey.currentContext?.findRenderObject() as RenderBox?;
@@ -231,12 +226,10 @@ class _HomePageState extends State<HomePage> {
}
}
// ========== 滚动监听 ==========
void _onScroll() {
if (mounted) setState(() {});
}
// ========== 初始化 WebDAV ==========
Future<void> _initWebDAV() async {
setState(() => _isLoading = true);
try {
@@ -249,14 +242,10 @@ class _HomePageState extends State<HomePage> {
}
}
// ============================================================
// ⭐ 核心:播放方法(UI 优先响应,然后真正播放)
// ============================================================
void _playSong(SongItem song) async {
try {
final url = WebDAVService.instance.getFileUrl(song.path);
// 1️⃣ 先更新 UI(立即响应)
final audioService = context.read<AudioService>();
audioService.playSong(Song(
id: song.path,
@@ -265,11 +254,9 @@ class _HomePageState extends State<HomePage> {
url: url,
));
// 2️⃣ 动态获取认证头
final headers = await WebDAVService.instance.getAuthHeaders();
if (headers.isEmpty) {
print('⚠️ 未获取到认证头,请检查 WebDAV 登录状态');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
@@ -281,10 +268,6 @@ class _HomePageState extends State<HomePage> {
return;
}
print('🎵 [收藏列表] 播放 URL: $url');
print('📋 [收藏列表] 认证头已设置: ${headers.keys}');
// 3️⃣ 播放
await PlaybackService().play(url, headers: headers);
} catch (e) {
if (mounted) {
@@ -298,7 +281,6 @@ class _HomePageState extends State<HomePage> {
}
}
// ========== 重置磁吸状态 ==========
void _resetSnapState() {
if (_scrollController.hasClients) {
_scrollController.jumpTo(0.0);
@@ -306,21 +288,15 @@ class _HomePageState extends State<HomePage> {
setState(() {});
}
// ================================================================
// Build
// ================================================================
@override
Widget build(BuildContext context) {
final isConnected = WebDAVService.instance.isConnected;
final username = WebDAVService.instance.username ?? '点击连接';
final audioService = context.watch<AudioService>();
final showMiniBar = audioService.currentSong != null;
return Scaffold(
backgroundColor: const Color(0xFF0E1211),
body: Column(
children: [
// ---- 固定标题 "清听" ----
SafeArea(
bottom: false,
child: Padding(
@@ -344,8 +320,6 @@ class _HomePageState extends State<HomePage> {
),
),
),
// ---- 滚动区域 ----
Expanded(
child: CustomScrollView(
controller: _scrollController,
@@ -356,7 +330,6 @@ class _HomePageState extends State<HomePage> {
)
: const ClampingScrollPhysics(),
slivers: [
// ---- 媒体库 ----
SliverToBoxAdapter(
child: Opacity(
opacity: _mediaOpacity,
@@ -551,8 +524,6 @@ class _HomePageState extends State<HomePage> {
),
),
),
// ---- "我的收藏" Sticky Header ----
SliverPersistentHeader(
pinned: true,
delegate: _StickyHeaderDelegate(
@@ -578,13 +549,11 @@ class _HomePageState extends State<HomePage> {
),
),
),
// ---- 收藏列表 ----
SliverPadding(
padding: EdgeInsets.only(
padding: const EdgeInsets.only(
left: 20,
right: 20,
bottom: showMiniBar ? 80.0 : 20.0,
bottom: 20,
),
sliver: _isLoading
? const SliverFillRemaining(
+28 -24
View File
@@ -1,19 +1,10 @@
// lib/pages/player_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
import '../main.dart';
import 'playlist_page.dart';
class PlayerPage extends StatefulWidget {
final VoidCallback onClose;
final VoidCallback onOpenPlaylist;
const PlayerPage({
super.key,
required this.onClose,
required this.onOpenPlaylist,
});
const PlayerPage({super.key});
@override
State<PlayerPage> createState() => _PlayerPageState();
@@ -37,22 +28,33 @@ class _PlayerPageState extends State<PlayerPage> {
if (song == null) {
return Scaffold(
backgroundColor: const Color(0xFF0E1211),
body: Center(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'正在播放',
style: TextStyle(color: Colors.white, fontSize: 16),
),
centerTitle: true,
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
Icon(Icons.music_off, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'没有正在播放的歌曲',
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: widget.onClose,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFB8D4D0),
foregroundColor: Colors.black87,
),
child: const Text('返回'),
SizedBox(height: 16),
Text(
'请先在首页点击一首歌曲',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
@@ -83,7 +85,7 @@ class _PlayerPageState extends State<PlayerPage> {
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
onPressed: widget.onClose,
onPressed: () => Navigator.pop(context),
),
title: const Text(
'正在播放',
@@ -279,8 +281,8 @@ class _PlayerPageState extends State<PlayerPage> {
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: const Color(0xFFB8D4D0),
decoration: const BoxDecoration(
color: Color(0xFFB8D4D0),
shape: BoxShape.circle,
),
child: IconButton(
@@ -301,7 +303,9 @@ class _PlayerPageState extends State<PlayerPage> {
padding: const EdgeInsets.all(12),
),
IconButton(
onPressed: widget.onOpenPlaylist,
onPressed: () {
navigatorKey.currentState?.pushNamed('/playlist');
},
icon: const Icon(
Icons.playlist_play_outlined,
color: Color(0xFFB8D4D0),
+4 -10
View File
@@ -1,15 +1,9 @@
// lib/pages/playlist_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
class PlaylistPage extends StatelessWidget {
final VoidCallback onClose;
const PlaylistPage({
super.key,
required this.onClose,
});
const PlaylistPage({super.key});
@override
Widget build(BuildContext context) {
@@ -33,7 +27,7 @@ class PlaylistPage extends StatelessWidget {
foregroundColor: Colors.white,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new),
onPressed: onClose,
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
@@ -60,7 +54,7 @@ class PlaylistPage extends StatelessWidget {
onPressed: () {
service.clearQueue();
Navigator.pop(context);
onClose();
Navigator.pop(context);
},
child: const Text(
'清空',
@@ -146,7 +140,7 @@ class PlaylistPage extends StatelessWidget {
: null,
onTap: () {
service.setQueue(queue, startIndex: index);
onClose();
Navigator.pop(context);
},
);
},
-114
View File
@@ -1,114 +0,0 @@
// lib/pages/shell_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
import '../widgets/global_mini_player.dart';
import 'home_page.dart';
import 'player_page.dart';
import 'playlist_page.dart';
class ShellPage extends StatefulWidget {
const ShellPage({super.key});
@override
State<ShellPage> createState() => _ShellPageState();
}
class _ShellPageState extends State<ShellPage> {
// ✅ 从 PlayerPage 打开 PlaylistPage
void _openPlaylistFromPlayer() {
Navigator.pushNamed(context, '/playlist');
}
// ✅ 关闭 PlayerPage
void _closePlayer() {
Navigator.pop(context);
}
// ✅ 关闭 PlaylistPage
void _closePlaylist() {
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0E1211),
body: Stack(
children: [
// ---- 页面内容(Navigator ----
Navigator(
initialRoute: '/',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return _buildPageRoute(
const HomePage(),
settings: settings,
);
case '/player':
return _buildPageRoute(
PlayerPage(
onClose: _closePlayer,
onOpenPlaylist: _openPlaylistFromPlayer,
),
settings: settings,
fromBottom: true,
);
case '/playlist':
return _buildPageRoute(
PlaylistPage(
onClose: _closePlaylist,
),
settings: settings,
);
default:
return null;
}
},
),
// ---- MiniPlayer(覆盖在最上层) ----
const Positioned(
left: 0,
right: 0,
bottom: 0,
child: GlobalMiniPlayer(),
),
],
),
);
}
PageRouteBuilder _buildPageRoute(
Widget page, {
required RouteSettings settings,
bool fromBottom = false,
}) {
return PageRouteBuilder(
settings: settings,
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
if (fromBottom) {
// 播放页:从底部滑入
final offset = Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
));
return SlideTransition(position: offset, child: child);
} else {
// 其他页面:淡入
return FadeTransition(
opacity: animation,
child: child,
);
}
},
transitionDuration: const Duration(milliseconds: 300),
reverseTransitionDuration: const Duration(milliseconds: 300),
);
}
}
+117 -102
View File
@@ -1,128 +1,143 @@
// lib/widgets/global_mini_player.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
import '../main.dart';
class GlobalMiniPlayer extends StatelessWidget {
const GlobalMiniPlayer({super.key});
// ✅ 保留静态回调
static VoidCallback? onOpenPlayerPage;
static VoidCallback? onOpenPlaylistPage;
@override
Widget build(BuildContext context) {
final service = context.watch<AudioService>();
final song = service.currentSong;
final audioService = context.watch<AudioService>();
final song = audioService.currentSong;
final bottomPadding = MediaQuery.of(context).padding.bottom;
return GestureDetector(
onTap: () {
if (song != null && onOpenPlayerPage != null) {
onOpenPlayerPage!();
}
},
child: Container(
height: 56 + bottomPadding,
decoration: BoxDecoration(
color: const Color(0xFF1A1F1E),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 12,
offset: const Offset(0, -4),
),
],
return Stack(
clipBehavior: Clip.none,
children: [
// 底层遮罩
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: 56 + bottomPadding,
color: const Color(0xFF1A1F1E),
),
),
child: SafeArea(
top: false,
bottom: true,
child: Padding(
padding: const EdgeInsets.only(bottom: 0),
child: Row(
children: [
const SizedBox(width: 12),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: song != null
? const Color(0xFF2A3332)
: Colors.grey[800],
borderRadius: BorderRadius.circular(4),
),
child: Icon(
song != null ? Icons.music_note : Icons.music_off,
color: song != null ? Colors.white38 : Colors.grey[600],
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
// 上层主体
Positioned(
left: 0,
right: 0,
bottom: bottomPadding,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(16),
),
child: Container(
height: 56,
color: const Color(0xFF1A1F1E),
// ⭐ 用 Material 包裹 InkWell,获得更好的点击反馈
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
if (song != null) {
navigatorKey.currentState?.pushNamed('/player');
}
},
// ⭐ 让整个区域都响应点击,包括空白部分
highlightColor: Colors.white.withOpacity(0.05),
splashColor: Colors.white.withOpacity(0.1),
child: Row(
children: [
Text(
song != null ? song.title : '清听',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
decoration: TextDecoration.none,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
song != null ? song.artist : '未播放',
style: TextStyle(
fontSize: 12,
const SizedBox(width: 12),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: song != null
? Colors.grey[400]
: Colors.grey[600],
decoration: TextDecoration.none,
? const Color(0xFF2A3332)
: Colors.grey[800],
borderRadius: BorderRadius.circular(4),
),
child: Icon(
song != null ? Icons.music_note : Icons.music_off,
color:
song != null ? Colors.white38 : Colors.grey[600],
size: 20,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
song != null ? song.title : '清听',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
// ⭐ 强制去掉下划线
decoration: TextDecoration.none,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
song != null ? song.artist : '未播放',
style: TextStyle(
fontSize: 12,
color: song != null
? Colors.grey[400]
: Colors.grey[600],
// ⭐ 强制去掉下划线
decoration: TextDecoration.none,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
IconButton(
icon: Icon(
song != null && audioService.isPlaying
? Icons.pause
: Icons.play_arrow,
color: song != null ? Colors.white : Colors.grey[600],
size: 24,
),
onPressed: () {
if (song != null) {
audioService.togglePlay();
}
},
),
IconButton(
icon: const Icon(
Icons.playlist_play_outlined,
color: Colors.grey,
size: 24,
),
onPressed: () {
if (song != null) {
navigatorKey.currentState?.pushNamed('/playlist');
}
},
),
const SizedBox(width: 4),
],
),
),
IconButton(
icon: Icon(
song != null && service.isPlaying
? Icons.pause
: Icons.play_arrow,
color: song != null ? Colors.white : Colors.grey[600],
size: 24,
),
onPressed: () {
if (song != null) {
service.togglePlay();
}
},
),
IconButton(
icon: Icon(
Icons.playlist_play_outlined,
color: Colors.grey[500],
size: 24,
),
onPressed: () {
// ✅ 使用静态回调,而不是 Navigator.pushNamed
if (onOpenPlaylistPage != null) {
onOpenPlaylistPage!();
}
},
),
const SizedBox(width: 4),
],
),
),
),
),
),
],
);
}
}