From 0a1ebfa5df8cac487bcfbff5de39a9f50b789102 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Sat, 24 Oct 2026 20:41:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20WebDAV=20=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=94=B9=E7=94=A8=E5=85=83=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=A0=87=E9=A2=98/=E8=89=BA=E6=9C=AF=E5=AE=B6=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前列表只显示文件名,即使 DB 里已经有标签。 - SongDatabase 新增 getSongsByPaths():一次批量按路径查(分块 400,避开变量上限) - WebDAVFileListPage: * 加载目录后批量取元数据,title/artist 有值就替换文件名 * 副标题显示 "艺术家 · 大小",没有标签时仍显示大小 * 监听 AudioService,正在播放的歌补全元数据后立即刷新对应行 * 没有标签的新歌自然回退文件名,不影响使用 --- lib/database/song_database.dart | 27 ++++++++++++ lib/pages/webdav_file_list_page.dart | 65 +++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/lib/database/song_database.dart b/lib/database/song_database.dart index d13185d..6567655 100644 --- a/lib/database/song_database.dart +++ b/lib/database/song_database.dart @@ -256,6 +256,33 @@ class SongDatabase { return result.isNotEmpty ? result.first : null; } + /// 批量按路径查元数据,返回 path -> row。 + /// 文件列表页一次要显示几十上百首,逐条查会太慢。 + Future>> getSongsByPaths( + List paths) async { + final result = >{}; + if (paths.isEmpty) return result; + + final db = await database; + const chunkSize = 400; // 避免 SQLite 变量数量上限 + for (var i = 0; i < paths.length; i += chunkSize) { + final end = (i + chunkSize) > paths.length ? paths.length : i + chunkSize; + final slice = paths.sublist(i, end); + final placeholders = List.filled(slice.length, '?').join(','); + final rows = await db.query( + 'songs', + where: 'remote_path IN ($placeholders)', + whereArgs: slice, + ); + for (final row in rows) { + final path = row['remote_path'] as String?; + if (path != null) result[path] = row; + } + } + _logQuery('songs', 'getByPaths', args: {'count': result.length}); + return result; + } + Future?> getSongByHash(String contentHash) async { _logQuery('songs', 'getByHash', args: {'content_hash': contentHash}); final db = await database; diff --git a/lib/pages/webdav_file_list_page.dart b/lib/pages/webdav_file_list_page.dart index 5c5e024..05f5679 100644 --- a/lib/pages/webdav_file_list_page.dart +++ b/lib/pages/webdav_file_list_page.dart @@ -4,6 +4,8 @@ import '../services/webdav_service.dart'; import '../constants/ui_constants.dart'; import '../base/base_state.dart'; import '../controllers/playback_controller.dart'; +import '../database/song_database.dart'; +import '../services/audio_service.dart'; class WebDAVFileListPage extends StatefulWidget { final String currentPath; @@ -20,6 +22,10 @@ class _WebDAVFileListPageState extends BaseState { String _errorMessage = ''; String _currentPath = '/'; + /// path -> 元数据行(songs 表),用于把文件名替换成标签里的标题/艺术家 + Map> _metaByPath = {}; + String? _lastSyncedSongPath; + // 安全解码 String _safeDecode(String input) { try { @@ -33,9 +39,35 @@ class _WebDAVFileListPageState extends BaseState { void initState() { super.initState(); _currentPath = widget.currentPath; + AudioService().addListener(_onPlaybackChanged); _loadDirectory(); } + @override + void dispose() { + AudioService().removeListener(_onPlaybackChanged); + super.dispose(); + } + + /// 播放中的歌一旦补全了元数据,立刻刷新列表里对应的那一行 + void _onPlaybackChanged() { + final path = AudioService().currentSong?.id; + if (path == null || path == _lastSyncedSongPath) return; + _lastSyncedSongPath = path; + _syncMetadataFor(path); + } + + Future _syncMetadataFor(String path) async { + if (!_items.any((item) => item.path == path)) return; + final row = await SongDatabase().getSongByPath(path); + if (row == null || !mounted) return; + final title = (row['title'] as String?)?.trim() ?? ''; + if (title.isEmpty) return; + safeSetState(() { + _metaByPath = {..._metaByPath, path: row}; + }); + } + // ============================================================ // 加载目录 // ============================================================ @@ -48,11 +80,28 @@ class _WebDAVFileListPageState extends BaseState { try { final items = await WebDAVService.instance.listDirectory(path: _currentPath); + + // 一次性把本目录下所有文件的元数据查出来,列表直接用标签显示 + final paths = items + .where((item) => !item.isDirectory) + .map((item) => item.path) + .toList(); + var meta = >{}; + try { + meta = await SongDatabase().getSongsByPaths(paths); + } catch (e) { + debugPrint('⚠️ [WebDAVList] 批量读取元数据失败: $e'); + } + if (mounted) { safeSetState(() { _items = items; + _metaByPath = meta; _isLoading = false; }); + // 如果当前正在播的歌就在这个目录里,补一次同步 + _lastSyncedSongPath = null; + _onPlaybackChanged(); } } catch (e) { if (mounted) { @@ -295,6 +344,18 @@ class _WebDAVFileListPageState extends BaseState { final sizeStr = item.size != null ? '${(item.size! / 1024 / 1024).toStringAsFixed(1)} MB' : ''; + // 有标签就用标签,没有才退回文件名(例如新歌尚未抓取元数据) + final meta = _metaByPath[item.path]; + final metaTitle = (meta?['title'] as String?)?.trim() ?? ''; + final metaArtist = (meta?['artist'] as String?)?.trim() ?? ''; + final hasMeta = metaTitle.isNotEmpty; + final titleText = hasMeta ? metaTitle : displayName; + final subtitleText = [ + if (hasMeta && metaArtist.isNotEmpty && metaArtist != '未知艺术家') + metaArtist, + if (sizeStr.isNotEmpty) sizeStr, + ].join(' · '); + return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), leading: const Icon( @@ -303,7 +364,7 @@ class _WebDAVFileListPageState extends BaseState { size: 28, ), title: Text( - displayName, + titleText, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w400, @@ -313,7 +374,7 @@ class _WebDAVFileListPageState extends BaseState { overflow: TextOverflow.ellipsis, ), subtitle: Text( - sizeStr, + subtitleText, style: TextStyle( fontSize: 12, color: Colors.grey[500],