feat(ui): WebDAV 文件列表改用元数据标题/艺术家显示

之前列表只显示文件名,即使 DB 里已经有标签。
- SongDatabase 新增 getSongsByPaths():一次批量按路径查(分块 400,避开变量上限)
- WebDAVFileListPage:
  * 加载目录后批量取元数据,title/artist 有值就替换文件名
  * 副标题显示 "艺术家 · 大小",没有标签时仍显示大小
  * 监听 AudioService,正在播放的歌补全元数据后立即刷新对应行
  * 没有标签的新歌自然回退文件名,不影响使用
This commit is contained in:
2026-10-24 20:41:21 +08:00
parent 18308d9d4e
commit 0a1ebfa5df
2 changed files with 90 additions and 2 deletions
+27
View File
@@ -256,6 +256,33 @@ class SongDatabase {
return result.isNotEmpty ? result.first : null;
}
/// 批量按路径查元数据,返回 path -> row。
/// 文件列表页一次要显示几十上百首,逐条查会太慢。
Future<Map<String, Map<String, dynamic>>> getSongsByPaths(
List<String> paths) async {
final result = <String, Map<String, dynamic>>{};
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<Map<String, dynamic>?> getSongByHash(String contentHash) async {
_logQuery('songs', 'getByHash', args: {'content_hash': contentHash});
final db = await database;
+63 -2
View File
@@ -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<WebDAVFileListPage> {
String _errorMessage = '';
String _currentPath = '/';
/// path -> 元数据行(songs 表),用于把文件名替换成标签里的标题/艺术家
Map<String, Map<String, dynamic>> _metaByPath = {};
String? _lastSyncedSongPath;
// 安全解码
String _safeDecode(String input) {
try {
@@ -33,9 +39,35 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
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<void> _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<WebDAVFileListPage> {
try {
final items =
await WebDAVService.instance.listDirectory(path: _currentPath);
// 一次性把本目录下所有文件的元数据查出来,列表直接用标签显示
final paths = items
.where((item) => !item.isDirectory)
.map((item) => item.path)
.toList();
var meta = <String, Map<String, dynamic>>{};
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<WebDAVFileListPage> {
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<WebDAVFileListPage> {
size: 28,
),
title: Text(
displayName,
titleText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
@@ -313,7 +374,7 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
sizeStr,
subtitleText,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],