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],