diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index f51b6aa..76000f7 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -8,6 +8,8 @@ import '../widgets/mini_player_bar.dart'; import '../widgets/magnetic_scroll_physics.dart'; import 'webdav_setup_page.dart'; import 'webdav_file_list_page.dart'; +import 'dart:async'; +import 'dart:convert'; // ================================================== // 歌曲数据模型 @@ -174,8 +176,6 @@ class _HomePageState extends State { // 媒体库透明度(快速淡出) double get _mediaOpacity { final p = _progress; - // 0% ~ 30%: 1.0 → 0.05 - // 30% ~ 100%: 0.05 → 0 if (p <= 0.3) { return 1.0 - (p / 0.3) * 0.95; } else { @@ -251,17 +251,37 @@ class _HomePageState extends State { } } - // ========== 播放 ========== + // ============================================================ + // ⭐ 核心:播放方法(UI 优先响应,然后真正播放) + // ============================================================ void _playSong(SongItem song) async { try { final url = WebDAVService.instance.getFileUrl(song.path); - await PlaybackService().play(url); - context.read().playSong(Song( - id: song.path, - title: song.displayTitle, - artist: song.displaySubtitle, - url: url, - )); + + // ✅ 硬编码认证头测试 + final testHeaders = { + 'Authorization': + 'Basic ' + base64Encode(utf8.encode('test-user-1:Lxh10020328?')), + }; + + print('🎵 测试 URL: $url'); + print('📋 测试认证头: ${testHeaders.keys}'); + + final audioService = context.read(); + audioService.playSong(Song( + id: song.path, + title: song.displayTitle, + artist: song.displaySubtitle, + url: url, + )); + + // 获取认证头 + final headers = await WebDAVService.instance.getAuthHeaders(); + print('📋 认证头数量: ${headers.length}'); + print('📋 认证头内容: $headers'); + + // 播放 + await PlaybackService().play(url, headers: headers); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart new file mode 100644 index 0000000..d76eef2 --- /dev/null +++ b/lib/pages/player_page.dart @@ -0,0 +1,210 @@ +// lib/pages/player_page.dart +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../services/audio_service.dart'; + +class PlayerPage extends StatefulWidget { + const PlayerPage({super.key}); + + @override + State createState() => _PlayerPageState(); +} + +class _PlayerPageState extends State { + // ✅ 使用 late,但不赋值,在 didChangeDependencies 中初始化 + late AudioService _audioService; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // ✅ 只在第一次或 service 变化时赋值 + _audioService = context.watch(); + } + + String _formatDuration(Duration d) { + final minutes = d.inMinutes; + final seconds = d.inSeconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + // ✅ 使用 watch 实时获取状态 + final service = context.watch(); + final song = service.currentSong; + + if (song == null) { + return Scaffold( + backgroundColor: const Color(0xFF0E1211), + body: const Center( + child: Text( + '没有正在播放的歌曲', + style: TextStyle(color: Colors.grey), + ), + ), + ); + } + + final position = service.position; + final duration = service.duration; + final isPlaying = service.isPlaying; + final progress = duration.inMilliseconds > 0 + ? position.inMilliseconds / duration.inMilliseconds + : 0.0; + + return Scaffold( + backgroundColor: const Color(0xFF0E1211), + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + actions: [ + IconButton( + icon: const Icon(Icons.more_vert, color: Colors.white54), + onPressed: () {}, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 280, + height: 280, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFF2A3332), + boxShadow: [ + BoxShadow( + color: const Color(0xFF7C9A9E).withOpacity(0.15), + blurRadius: 60, + spreadRadius: 10, + ), + ], + ), + child: const Icon( + Icons.music_note, + size: 80, + color: Colors.white24, + ), + ), + const SizedBox(height: 48), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + song.title, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + song.artist, + style: TextStyle( + fontSize: 16, + color: Colors.grey[400], + ), + textAlign: TextAlign.center, + ), + ], + ), + const SizedBox(height: 40), + Column( + children: [ + Slider( + value: progress.clamp(0.0, 1.0), + onChanged: (value) { + final newPosition = Duration( + milliseconds: (value * duration.inMilliseconds).round(), + ); + service.seekTo(newPosition); + }, + activeColor: const Color(0xFFB8D4D0), + inactiveColor: Colors.grey[800], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(position), + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + ), + Text( + _formatDuration(duration), + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + ), + ], + ), + ], + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.skip_previous, size: 32), + color: Colors.white60, + padding: const EdgeInsets.all(16), + ), + const SizedBox(width: 24), + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: const Color(0xFFB8D4D0), + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.black87, + size: 32, + ), + padding: EdgeInsets.zero, + onPressed: () { + service.togglePlay(); + }, + ), + ), + const SizedBox(width: 24), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.skip_next, size: 32), + color: Colors.white60, + padding: const EdgeInsets.all(16), + ), + ], + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.volume_up_outlined), + color: Colors.white38, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/webdav_file_list_page.dart b/lib/pages/webdav_file_list_page.dart index f5da2e1..c29f7c7 100644 --- a/lib/pages/webdav_file_list_page.dart +++ b/lib/pages/webdav_file_list_page.dart @@ -4,6 +4,8 @@ import 'package:provider/provider.dart'; import '../services/webdav_service.dart'; import '../services/playback_service.dart'; import '../services/audio_service.dart'; +import 'dart:async'; +import 'dart:convert'; class WebDAVFileListPage extends StatefulWidget { final String currentPath; @@ -69,15 +71,25 @@ class _WebDAVFileListPageState extends State { void _playSong(WebDAVItem file) async { try { final url = WebDAVService.instance.getFileUrl(file.path); - await PlaybackService().play(url); + + // ✅ 硬编码认证头(与 home_page 保持一致) + final testHeaders = { + 'Authorization': + 'Basic ' + base64Encode(utf8.encode('test-user-1:Lxh10020328?')), + }; + + print('🎵 [文件列表] 播放 URL: $url'); + print('📋 [文件列表] 认证头已设置'); final song = Song( id: file.path, - title: _safeDecode(file.name).replaceAll(RegExp(r'\.[^.]*$'), ''), + title: file.name.replaceAll(RegExp(r'\.[^.]*$'), ''), artist: '未知艺术家', url: url, ); context.read().playSong(song); + + await PlaybackService().play(url, headers: testHeaders); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 089ef19..ba3153d 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -1,4 +1,7 @@ +// lib/services/audio_service.dart +import 'dart:async'; // ⬅️ 添加这个导入 import 'package:flutter/material.dart'; +import 'playback_service.dart'; class Song { final String id; @@ -17,24 +20,101 @@ class Song { class AudioService extends ChangeNotifier { Song? _currentSong; bool _isPlaying = false; + Duration _position = Duration.zero; + Duration _duration = Duration.zero; + + bool _listening = false; + final List _subscriptions = []; Song? get currentSong => _currentSong; bool get isPlaying => _isPlaying; + Duration get position => _position; + Duration get duration => _duration; - void playSong(Song song) { + // ✅ 完整的播放入口 + Future playSong(Song song) async { _currentSong = song; - _isPlaying = true; + _position = Duration.zero; + _duration = Duration.zero; + + _startListening(); notifyListeners(); + + if (song.url == null || song.url!.isEmpty) { + return; + } + + // 播放由 PlaybackService 执行,状态由 stream 更新 + await PlaybackService().play(song.url!); } + // ✅ 只调用播放器方法,不手动修改 _isPlaying void togglePlay() { - _isPlaying = !_isPlaying; - notifyListeners(); + if (_currentSong == null) return; + + if (_isPlaying) { + PlaybackService().pause(); + } else { + PlaybackService().resume(); + } + // _isPlaying 由 player.stream.playing 更新 } void stopPlay() { _currentSong = null; _isPlaying = false; + _position = Duration.zero; + _duration = Duration.zero; + _stopListening(); notifyListeners(); } + + void seekTo(Duration position) { + PlaybackService().seek(position); + _position = position; + notifyListeners(); + } + + // ✅ 直接监听 media_kit 的三个独立 Stream + void _startListening() { + if (_listening) return; + _listening = true; + + final player = PlaybackService().player; + + _subscriptions.add( + player.stream.playing.listen((playing) { + if (_isPlaying != playing) { + _isPlaying = playing; + notifyListeners(); + } + }), + ); + + _subscriptions.add( + player.stream.position.listen((position) { + if (_position != position) { + _position = position; + notifyListeners(); + } + }), + ); + + _subscriptions.add( + player.stream.duration.listen((duration) { + if (_duration != duration) { + _duration = duration; + notifyListeners(); + } + }), + ); + } + + void _stopListening() { + _listening = false; + for (final subscription in _subscriptions) { + subscription.cancel(); + } + _subscriptions.clear(); + } } diff --git a/lib/services/playback_service.dart b/lib/services/playback_service.dart index 94d23b9..d47b8eb 100644 --- a/lib/services/playback_service.dart +++ b/lib/services/playback_service.dart @@ -1,3 +1,4 @@ +// lib/services/playback_service.dart import 'package:media_kit/media_kit.dart'; class PlaybackService { @@ -8,34 +9,43 @@ class PlaybackService { late final Player _player; bool _initialized = false; + // 获取 Player 实例(供 AudioService 直接监听) + Player get player { + if (!_initialized) init(); + return _player; + } + void init() { if (_initialized) return; _player = Player(); _initialized = true; } - Future play(String url) async { + Future play(String url, {Map? headers}) async { if (!_initialized) init(); - await _player.open(Media(url)); + print('🎵 播放 URL: $url'); + print('📋 认证头: ${headers?.keys}'); + final media = headers != null && headers.isNotEmpty + ? Media(url, httpHeaders: headers) + : Media(url); + await _player.open(media); await _player.play(); } void pause() { - if (_initialized) { - _player.pause(); - } + if (_initialized) _player.pause(); } void resume() { - if (_initialized) { - _player.play(); - } + if (_initialized) _player.play(); } void stop() { - if (_initialized) { - _player.stop(); - } + if (_initialized) _player.stop(); + } + + void seek(Duration position) { + if (_initialized) _player.seek(position); } void dispose() { @@ -44,23 +54,4 @@ class PlaybackService { _initialized = false; } } - - // ✅ 直接返回 PlayerStream(它就是播放器的状态流) - PlayerStream get stateStream { - if (!_initialized) init(); - return _player.stream; - } - - // 获取当前播放状态 - PlayerState get currentState { - if (!_initialized) { - return PlayerState( - playing: false, - position: Duration.zero, - duration: Duration.zero, - buffering: false, - ); - } - return _player.state; - } } diff --git a/lib/services/webdav_service.dart b/lib/services/webdav_service.dart index 0c2594b..19239f3 100644 --- a/lib/services/webdav_service.dart +++ b/lib/services/webdav_service.dart @@ -26,10 +26,22 @@ class WebDAVService { Future saveCredentials( String baseUrl, String username, String password) async { final prefs = await SharedPreferences.getInstance(); + + print('💾 [saveCredentials] 开始保存凭据...'); + print('💾 [saveCredentials] baseUrl: $baseUrl'); + print('💾 [saveCredentials] username: $username'); + print('💾 [saveCredentials] password 长度: ${password.length}'); + await prefs.setString(_keyBaseUrl, baseUrl); await prefs.setString(_keyUsername, username); await prefs.setString(_keyPassword, password); + // 立即读取验证 + final verifyUsername = prefs.getString(_keyUsername); + final verifyPassword = prefs.getString(_keyPassword); + print( + '💾 [saveCredentials] 验证读取 - username: $verifyUsername, password 存在: ${verifyPassword != null}'); + _baseUrl = baseUrl; _username = username; _dio = Dio(BaseOptions( @@ -74,6 +86,28 @@ class WebDAVService { _username = null; } +// ✅ 新增:获取认证头(用于 media_kit 播放) + Future> getAuthHeaders() async { + final prefs = await SharedPreferences.getInstance(); + final username = prefs.getString(_keyUsername); + final password = prefs.getString(_keyPassword); + + print('🔑 [getAuthHeaders] 读取到用户名: $username'); + print('🔑 [getAuthHeaders] 密码存在: ${password != null}'); + print('🔑 [getAuthHeaders] 密码长度: ${password?.length ?? 0}'); + + if (username != null && password != null) { + final credentials = '$username:$password'; + final encoded = base64Encode(utf8.encode(credentials)); + print('🔑 [getAuthHeaders] 生成的 Authorization 头: Basic $encoded'); + return { + 'Authorization': 'Basic $encoded', + }; + } + print('⚠️ [getAuthHeaders] 用户名或密码为 null,返回空 Map'); + return {}; + } + // 获取音乐文件列表(PROPFIND) Future> listDirectory({String path = '/'}) async { if (_dio == null) throw Exception('WebDAV 未连接'); diff --git a/lib/widgets/mini_player_bar.dart b/lib/widgets/mini_player_bar.dart index 3468541..be59ce5 100644 --- a/lib/widgets/mini_player_bar.dart +++ b/lib/widgets/mini_player_bar.dart @@ -1,6 +1,8 @@ +// lib/widgets/mini_player_bar.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/audio_service.dart'; +import '../pages/player_page.dart'; class MiniPlayerBar extends StatelessWidget { const MiniPlayerBar({super.key}); @@ -15,82 +17,99 @@ class MiniPlayerBar extends StatelessWidget { return SafeArea( top: false, bottom: true, - child: Container( - height: 64, - decoration: BoxDecoration( - color: const Color(0xFF1A1F1E), // 不透明背景 - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.4), - blurRadius: 12, - offset: const Offset(0, -4), + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const PlayerPage(), ), - ], - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black.withOpacity(0.1), + ); + }, + child: Container( + height: 64, + decoration: BoxDecoration( + color: const Color(0xFF1A1F1E), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, -4), + ), ], - stops: const [0.0, 1.0], + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.1), + ], + stops: const [0.0, 1.0], + ), ), - ), - child: Row( - children: [ - const SizedBox(width: 12), - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: const Color(0xFF2A3332), - borderRadius: BorderRadius.circular(4), + child: Row( + children: [ + const SizedBox(width: 12), + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFF2A3332), + borderRadius: BorderRadius.circular(4), + ), + child: const Icon( + Icons.music_note, + color: Colors.white38, + size: 24, + ), ), - child: - const Icon(Icons.music_note, color: Colors.white38, size: 24), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - song.title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Colors.white, + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + song.title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - song.artist, - style: TextStyle( - fontSize: 13, - color: Colors.grey[400], + Text( + song.artist, + style: TextStyle( + fontSize: 13, + color: Colors.grey[400], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], + ], + ), ), - ), - IconButton( - icon: Icon( - service.isPlaying ? Icons.pause : Icons.play_arrow, - color: Colors.white, + IconButton( + icon: Icon( + service.isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + ), + onPressed: () { + service.togglePlay(); + }, ), - onPressed: () => context.read().togglePlay(), - ), - IconButton( - icon: const Icon(Icons.playlist_play_outlined, - color: Colors.white54), - onPressed: () {}, - ), - const SizedBox(width: 4), - ], + IconButton( + icon: const Icon( + Icons.playlist_play_outlined, + color: Colors.white54, + ), + onPressed: () {}, + ), + const SizedBox(width: 4), + ], + ), ), ), );