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;
|
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 {
|
Future<Map<String, dynamic>?> getSongByHash(String contentHash) async {
|
||||||
_logQuery('songs', 'getByHash', args: {'content_hash': contentHash});
|
_logQuery('songs', 'getByHash', args: {'content_hash': contentHash});
|
||||||
final db = await database;
|
final db = await database;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import '../services/webdav_service.dart';
|
|||||||
import '../constants/ui_constants.dart';
|
import '../constants/ui_constants.dart';
|
||||||
import '../base/base_state.dart';
|
import '../base/base_state.dart';
|
||||||
import '../controllers/playback_controller.dart';
|
import '../controllers/playback_controller.dart';
|
||||||
|
import '../database/song_database.dart';
|
||||||
|
import '../services/audio_service.dart';
|
||||||
|
|
||||||
class WebDAVFileListPage extends StatefulWidget {
|
class WebDAVFileListPage extends StatefulWidget {
|
||||||
final String currentPath;
|
final String currentPath;
|
||||||
@@ -20,6 +22,10 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
String _errorMessage = '';
|
String _errorMessage = '';
|
||||||
String _currentPath = '/';
|
String _currentPath = '/';
|
||||||
|
|
||||||
|
/// path -> 元数据行(songs 表),用于把文件名替换成标签里的标题/艺术家
|
||||||
|
Map<String, Map<String, dynamic>> _metaByPath = {};
|
||||||
|
String? _lastSyncedSongPath;
|
||||||
|
|
||||||
// 安全解码
|
// 安全解码
|
||||||
String _safeDecode(String input) {
|
String _safeDecode(String input) {
|
||||||
try {
|
try {
|
||||||
@@ -33,9 +39,35 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_currentPath = widget.currentPath;
|
_currentPath = widget.currentPath;
|
||||||
|
AudioService().addListener(_onPlaybackChanged);
|
||||||
_loadDirectory();
|
_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 {
|
try {
|
||||||
final items =
|
final items =
|
||||||
await WebDAVService.instance.listDirectory(path: _currentPath);
|
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) {
|
if (mounted) {
|
||||||
safeSetState(() {
|
safeSetState(() {
|
||||||
_items = items;
|
_items = items;
|
||||||
|
_metaByPath = meta;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
// 如果当前正在播的歌就在这个目录里,补一次同步
|
||||||
|
_lastSyncedSongPath = null;
|
||||||
|
_onPlaybackChanged();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -295,6 +344,18 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
final sizeStr = item.size != null
|
final sizeStr = item.size != null
|
||||||
? '${(item.size! / 1024 / 1024).toStringAsFixed(1)} MB'
|
? '${(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(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||||
leading: const Icon(
|
leading: const Icon(
|
||||||
@@ -303,7 +364,7 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
size: 28,
|
size: 28,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
displayName,
|
titleText,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
@@ -313,7 +374,7 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
sizeStr,
|
subtitleText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
|
|||||||
Reference in New Issue
Block a user