feat(ui): WebDAV 文件列表改用元数据标题/艺术家显示
之前列表只显示文件名,即使 DB 里已经有标签。 - SongDatabase 新增 getSongsByPaths():一次批量按路径查(分块 400,避开变量上限) - WebDAVFileListPage: * 加载目录后批量取元数据,title/artist 有值就替换文件名 * 副标题显示 "艺术家 · 大小",没有标签时仍显示大小 * 监听 AudioService,正在播放的歌补全元数据后立即刷新对应行 * 没有标签的新歌自然回退文件名,不影响使用
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user