From 699ab7ce86e8f32cddd657f3cea6c70d2a97f734 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Sat, 15 Aug 2026 23:20:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9A=82=E6=97=B6=E6=8A=8Abug=E5=85=88?= =?UTF-8?q?=E5=A4=84=E7=90=86=E6=8E=89=E4=BA=86=EF=BC=8C=E5=87=86=E5=A4=87?= =?UTF-8?q?=E5=B0=9D=E8=AF=95=E5=AE=9E=E7=8E=B0=E6=9C=80=E5=B0=8F=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E7=9B=AE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.kotlin/errors/errors-1786806960565.log | 3 + lib/main.dart | 9 +- lib/models/song_model.dart | 16 -- lib/pages/playlist_page.dart | 53 ------- lib/pages/webdav_setup_page.dart | 2 +- lib/services/audio_service.dart | 26 +++- lib/services/playback_service.dart | 54 ++++++- lib/services/webdav_service.dart | 145 ++++++++++++++---- lib/widgets/mini_player_bar.dart | 8 +- pubspec.lock | 123 ++++++++++++++- pubspec.yaml | 4 +- 11 files changed, 313 insertions(+), 130 deletions(-) create mode 100644 android/.kotlin/errors/errors-1786806960565.log delete mode 100644 lib/models/song_model.dart delete mode 100644 lib/pages/playlist_page.dart diff --git a/android/.kotlin/errors/errors-1786806960565.log b/android/.kotlin/errors/errors-1786806960565.log new file mode 100644 index 0000000..88185a8 --- /dev/null +++ b/android/.kotlin/errors/errors-1786806960565.log @@ -0,0 +1,3 @@ +kotlin version: 2.3.20 +error message: Daemon compilation failed + diff --git a/lib/main.dart b/lib/main.dart index 2abf198..dbeaa62 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,9 +7,7 @@ import 'pages/home_page.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - // 初始化 media_kit - Player.init(); - // 初始化播放服务 + // media_kit 不需要静态 init,PlaybackService 中会初始化 Player PlaybackService().init(); runApp( ChangeNotifierProvider( @@ -32,8 +30,9 @@ class QTPlayerApp extends StatelessWidget { colorScheme: const ColorScheme.dark( primary: Color(0xFF7C9A9E), secondary: Color(0xFFB8D4D0), - surface: Color(0xFF1A1F1E), - background: Color(0xFF0E1211), + surface: Color(0xFF1A1F1E), // 替代 background + // 用 onSurface 控制文字颜色 + onSurface: Colors.white, ), appBarTheme: const AppBarTheme( backgroundColor: Color(0xFF1A1F1E), diff --git a/lib/models/song_model.dart b/lib/models/song_model.dart deleted file mode 100644 index c3ef9d7..0000000 --- a/lib/models/song_model.dart +++ /dev/null @@ -1,16 +0,0 @@ -class Song { - final String id; - final String title; - final String artist; - final String? url; // 以后放 content:// 或 https:// - - Song({required this.id, required this.title, required this.artist, this.url}); - - // 示例占位数据 - static List get placeholderSongs => [ - Song(id: '1', title: '晴天', artist: '周杰伦'), - Song(id: '2', title: '起风了', artist: '买辣椒也用券'), - Song(id: '3', title: 'Flower Dance', artist: 'DJ Okawari'), - Song(id: '4', title: '夜曲', artist: '周杰伦'), - ]; -} diff --git a/lib/pages/playlist_page.dart b/lib/pages/playlist_page.dart deleted file mode 100644 index 5f770d3..0000000 --- a/lib/pages/playlist_page.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../models/song_model.dart'; -import '../services/audio_service.dart'; -import '../widgets/full_player_page.dart'; - -class PlaylistPage extends StatelessWidget { - const PlaylistPage({super.key}); - - @override - Widget build(BuildContext context) { - final songs = Song.placeholderSongs; // 后续替换成 WebDAV/SAF 列表 - - return Scaffold( - appBar: AppBar( - title: const Text('我的清听'), - actions: [ - IconButton( - icon: const Icon(Icons.shuffle), - onPressed: () {}, // 以后实现随机 - ), - ], - ), - body: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: songs.length, - itemBuilder: (context, index) { - final song = songs[index]; - return ListTile( - leading: const CircleAvatar( - backgroundColor: Color(0xFF2A3332), - child: Icon(Icons.audiotrack, size: 20, color: Colors.white54), - ), - title: Text(song.title, - style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Text(song.artist, - style: TextStyle(color: Colors.grey[400], fontSize: 13)), - trailing: const Icon(Icons.more_vert, color: Colors.grey), - onTap: () { - // 1. 先更新服务,显示迷你条 - context.read().playSong(song); - // 2. 再跳转全屏播放器 - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const FullPlayerPage()), - ); - }, - ); - }, - ), - ); - } -} diff --git a/lib/pages/webdav_setup_page.dart b/lib/pages/webdav_setup_page.dart index e4cf4a8..018eaf6 100644 --- a/lib/pages/webdav_setup_page.dart +++ b/lib/pages/webdav_setup_page.dart @@ -37,7 +37,7 @@ class _WebDAVSetupPageState extends State { _isConnected = hasCred; _statusText = hasCred ? '已连接' : '未连接'; if (hasCred) { - _baseUrlController.text = WebDAVService.instance._baseUrl ?? ''; + _baseUrlController.text = WebDAVService.instance.baseUrl ?? ''; // 用户名不显示,保持隐私 } }); diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 321dc04..089ef19 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -1,7 +1,19 @@ import 'package:flutter/material.dart'; -import '../models/song_model.dart'; -// 这是一个 ChangeNotifier,以后用来刷新底部的迷你播放条 +class Song { + final String id; + final String title; + final String artist; + final String? url; + + Song({ + required this.id, + required this.title, + required this.artist, + this.url, + }); +} + class AudioService extends ChangeNotifier { Song? _currentSong; bool _isPlaying = false; @@ -9,16 +21,20 @@ class AudioService extends ChangeNotifier { Song? get currentSong => _currentSong; bool get isPlaying => _isPlaying; - // 占位方法:以后这里会调用 media_kit void playSong(Song song) { _currentSong = song; _isPlaying = true; - notifyListeners(); // 刷新 UI - print('🎵 准备播放: ${song.title} - ${song.artist}'); + notifyListeners(); } void togglePlay() { _isPlaying = !_isPlaying; notifyListeners(); } + + void stopPlay() { + _currentSong = null; + _isPlaying = false; + notifyListeners(); + } } diff --git a/lib/services/playback_service.dart b/lib/services/playback_service.dart index 1a8590b..94d23b9 100644 --- a/lib/services/playback_service.dart +++ b/lib/services/playback_service.dart @@ -1,5 +1,4 @@ import 'package:media_kit/media_kit.dart'; -import 'package:media_kit/media_kit.dart' as media_kit; class PlaybackService { static final PlaybackService _instance = PlaybackService._(); @@ -7,22 +6,61 @@ class PlaybackService { PlaybackService._(); late final Player _player; - bool get isInitialized => _player.state.playing; + bool _initialized = false; void init() { + if (_initialized) return; _player = Player(); + _initialized = true; } Future play(String url) async { + if (!_initialized) init(); await _player.open(Media(url)); await _player.play(); } - void pause() => _player.pause(); - void resume() => _player.play(); - void stop() => _player.stop(); - void dispose() => _player.dispose(); + void pause() { + if (_initialized) { + _player.pause(); + } + } - // 状态流 - Stream get stateStream => _player.stream; + void resume() { + if (_initialized) { + _player.play(); + } + } + + void stop() { + if (_initialized) { + _player.stop(); + } + } + + void dispose() { + if (_initialized) { + _player.dispose(); + _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 0058420..575624a 100644 --- a/lib/services/webdav_service.dart +++ b/lib/services/webdav_service.dart @@ -1,5 +1,7 @@ -import 'package:webdav_client/webdav_client.dart'; +import 'dart:convert'; +import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:xml/xml.dart'; class WebDAVService { static const String _keyBaseUrl = 'webdav_base_url'; @@ -11,10 +13,13 @@ class WebDAVService { WebDAVService._(); - WebDAVClient? _client; + Dio? _dio; String? _baseUrl; + String? _username; - bool get isConnected => _client != null; + bool get isConnected => _dio != null; + String? get baseUrl => _baseUrl; + String? get username => _username; // 保存凭据 Future saveCredentials( @@ -23,11 +28,16 @@ class WebDAVService { await prefs.setString(_keyBaseUrl, baseUrl); await prefs.setString(_keyUsername, username); await prefs.setString(_keyPassword, password); + _baseUrl = baseUrl; - _client = WebDAVClient( - baseUri: Uri.parse(baseUrl), - credentials: '$username:$password', - ); + _username = username; + _dio = Dio(BaseOptions( + baseUrl: baseUrl, + headers: { + 'Authorization': + 'Basic ${base64Encode(utf8.encode('$username:$password'))}', + }, + )); } // 加载已保存的凭据 @@ -36,12 +46,17 @@ class WebDAVService { final baseUrl = prefs.getString(_keyBaseUrl); final username = prefs.getString(_keyUsername); final password = prefs.getString(_keyPassword); + if (baseUrl != null && username != null && password != null) { _baseUrl = baseUrl; - _client = WebDAVClient( - baseUri: Uri.parse(baseUrl), - credentials: '$username:$password', - ); + _username = username; + _dio = Dio(BaseOptions( + baseUrl: baseUrl, + headers: { + 'Authorization': + 'Basic ${base64Encode(utf8.encode('$username:$password'))}', + }, + )); return true; } return false; @@ -53,36 +68,108 @@ class WebDAVService { await prefs.remove(_keyBaseUrl); await prefs.remove(_keyUsername); await prefs.remove(_keyPassword); - _client = null; + _dio = null; _baseUrl = null; + _username = null; } - // 获取音乐文件列表(仅 .mp3 .flac .m4a .ape .wav) + // 获取音乐文件列表(通过 PROPFIND) Future> getMusicFiles({String path = '/'}) async { - if (_client == null) throw Exception('WebDAV 未连接'); + if (_dio == null) throw Exception('WebDAV 未连接'); - final items = await _client!.listAll(recursive: true); + // PROPFIND 请求体(Depth: 1 表示获取子项) + final body = ''' + + + + + + + + + '''; + + final response = await _dio!.request( + path, + options: Options( + method: 'PROPFIND', + headers: { + 'Depth': '1', + 'Content-Type': 'application/xml; charset=utf-8', + }, + ), + data: body, + ); + + if (response.statusCode != 207) { + throw Exception('WebDAV 响应异常: ${response.statusCode}'); + } + + final xml = XmlDocument.parse(response.data as String); final musicExtensions = ['.mp3', '.flac', '.m4a', '.ape', '.wav', '.opus']; - return items - .where((item) => - item.isFile && - musicExtensions.any((ext) => item.path.toLowerCase().endsWith(ext))) - .map((item) => WebDAVFileItem( - path: item.path, - name: item.path.split('/').last, - size: item.size, - modified: item.modified, - )) - .toList(); + final items = []; + + // 查找所有响应项 + for (final responseNode in xml.findAllElements('response')) { + final hrefNode = responseNode.findElements('href').firstOrNull; + if (hrefNode == null) continue; + + String fullPath = Uri.decodeComponent(hrefNode.text.trim()); + // 去掉 baseUrl 前缀,得到相对路径 + String relativePath = fullPath; + if (_baseUrl != null) { + final baseUri = Uri.parse(_baseUrl!); + final fullUri = Uri.parse(fullPath); + relativePath = fullUri.path; + if (baseUri.path.isNotEmpty && relativePath.startsWith(baseUri.path)) { + relativePath = relativePath.substring(baseUri.path.length); + } + } + if (relativePath.isEmpty || relativePath == '/') continue; + + // 检查是否是文件(非集合) + final resTypeNode = responseNode.findElements('resourcetype').firstOrNull; + final isCollection = + resTypeNode?.findElements('collection').isNotEmpty ?? false; + if (isCollection) continue; + + // 检查扩展名 + final fileName = relativePath.split('/').last; + if (!musicExtensions.any((ext) => fileName.toLowerCase().endsWith(ext))) + continue; + + // 获取文件大小和修改时间 + int? size; + DateTime? modified; + final sizeNode = + responseNode.findElements('getcontentlength').firstOrNull; + if (sizeNode != null) { + size = int.tryParse(sizeNode.text); + } + final modifiedNode = + responseNode.findElements('getlastmodified').firstOrNull; + if (modifiedNode != null) { + try { + modified = DateTime.parse(modifiedNode.text); + } catch (_) {} + } + + items.add(WebDAVFileItem( + path: relativePath, + name: fileName, + size: size, + modified: modified, + )); + } + + return items; } - // 获取文件的完整下载 URL(用于播放) + // 获取文件的完整 URL String getFileUrl(String path) { if (_baseUrl == null) throw Exception('WebDAV 未配置'); - // 确保 baseUrl 末尾有 '/' final base = _baseUrl!.endsWith('/') ? _baseUrl! : '$_baseUrl/'; - // 去掉路径开头的 '/' final cleanPath = path.startsWith('/') ? path.substring(1) : path; return '$base$cleanPath'; } diff --git a/lib/widgets/mini_player_bar.dart b/lib/widgets/mini_player_bar.dart index 8485a49..c4b4f5b 100644 --- a/lib/widgets/mini_player_bar.dart +++ b/lib/widgets/mini_player_bar.dart @@ -8,7 +8,9 @@ class MiniPlayerBar extends StatelessWidget { @override Widget build(BuildContext context) { final service = context.watch(); - final song = service.currentSong!; + final song = service.currentSong; + + if (song == null) return const SizedBox.shrink(); return Container( height: 64, @@ -21,7 +23,6 @@ class MiniPlayerBar extends StatelessWidget { offset: const Offset(0, -4), ), ], - // 顶部微渐变(轻阴影) gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, @@ -35,7 +36,6 @@ class MiniPlayerBar extends StatelessWidget { child: Row( children: [ const SizedBox(width: 12), - // 封面占位(56px) Container( width: 48, height: 48, @@ -47,7 +47,6 @@ class MiniPlayerBar extends StatelessWidget { const Icon(Icons.music_note, color: Colors.white38, size: 24), ), const SizedBox(width: 12), - // 歌曲信息 Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -75,7 +74,6 @@ class MiniPlayerBar extends StatelessWidget { ], ), ), - // 控制按钮 IconButton( icon: Icon( service.isPlaying ? Icons.pause : Icons.play_arrow, diff --git a/pubspec.lock b/pubspec.lock index ef991b8..40b91f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" fixnum: dependency: transitive description: @@ -123,6 +131,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" http: dependency: transitive description: @@ -143,10 +156,10 @@ packages: dependency: transitive description: name: image - sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.flutter-io.cn" source: hosted - version: "4.9.1" + version: "4.8.0" leak_tracker: dependency: transitive description: @@ -283,6 +296,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -291,6 +328,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -323,6 +368,62 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.27" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -440,14 +541,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" - xml: + xdg_directories: dependency: transitive description: - name: xml - sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.1" + version: "1.1.0" + xml: + dependency: "direct main" + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.6.1" sdks: dart: ">=3.12.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4266cae..7eea464 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,8 +32,10 @@ dependencies: sdk: flutter media_kit: ^1.1.10 media_kit_libs_audio: ^1.0.7 + shared_preferences: ^2.2.2 provider: ^6.1.2 - dio: ^5.4.0 + dio: ^5.4.0 # HTTP 客户端 + xml: ^6.5.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.