Files
QingTing-Player/lib/pages/webdav_file_list_page.dart
T
lxh2875931338 83ab47d8fc fix(metadata): 补上事件订阅,缓存命中也推送一次
上一条只加了字段和回调,listen 没插进去(列表不会刷新)。
另外歌曲元数据在 SQLite 命中时直接 return,也不会发事件——
导致"标签其实早就在库里、列表却还显示文件名"。

- HomePage / WebDAVFileListPage 补上 stream.listen 与 cancel
- MetadataService 的 SQLite 命中分支也 publish 一次(幂等,按 path 覆盖)

验证方式:播放一首收藏里显示文件名的歌 -> 返回主页 -> 该行应变为标签标题。
2026-11-01 12:22:46 +08:00

457 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
// lib/pages/webdav_file_list_page.dart
import 'package:flutter/material.dart';
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';
import '../services/metadata_events.dart';
import '../widgets/song_artwork.dart';
import '../widgets/add_to_playlist_sheet.dart';
class WebDAVFileListPage extends StatefulWidget {
final String currentPath;
const WebDAVFileListPage({super.key, this.currentPath = '/'});
@override
State<WebDAVFileListPage> createState() => _WebDAVFileListPageState();
}
class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
List<WebDAVItem> _items = [];
bool _isLoading = true;
String _errorMessage = '';
String _currentPath = '/';
/// path -> 元数据行(songs 表),用于把文件名替换成标签里的标题/艺术家
Map<String, Map<String, dynamic>> _metaByPath = {};
String? _lastSyncedSongPath;
StreamSubscription<MetadataUpdate>? _metaSub;
/// 已收藏的歌曲路径
Set<String> _favoritePaths = {};
// 安全解码
String _safeDecode(String input) {
try {
return Uri.decodeComponent(input);
} catch (_) {
return input;
}
}
@override
void initState() {
super.initState();
_currentPath = widget.currentPath;
AudioService().addListener(_onPlaybackChanged);
_metaSub = MetadataEvents().stream.listen(_onMetadataUpdate);
_loadDirectory();
}
@override
void dispose() {
_metaSub?.cancel();
AudioService().removeListener(_onPlaybackChanged);
super.dispose();
}
/// 元数据补全后,按 path 更新列表行的标题/艺术家/封面
void _onMetadataUpdate(MetadataUpdate update) {
if (!_items.any((item) => item.path == update.path)) return;
if (!mounted) return;
safeSetState(() {
_metaByPath = {..._metaByPath, update.path: update.row};
});
}
/// 播放中的歌一旦补全了元数据,立刻刷新列表里对应的那一行
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};
});
}
/// 收藏 / 取消收藏:整行点击是播放,右侧红心专门做收藏
Future<void> _toggleFavorite(String path) async {
final wasFavorite = _favoritePaths.contains(path);
try {
await SongDatabase().toggleFavorite(path);
if (!mounted) return;
safeSetState(() {
final next = {..._favoritePaths};
if (wasFavorite) {
next.remove(path);
} else {
next.add(path);
}
_favoritePaths = next;
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('收藏失败: $e')),
);
}
}
}
// ============================================================
// 加载目录
// ============================================================
Future<void> _loadDirectory() async {
safeSetState(() {
_isLoading = true;
_errorMessage = '';
});
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');
}
var favorites = <String>{};
try {
final favRows = await SongDatabase().getFavorites();
favorites = favRows
.map((row) => row['song_id'] as String?)
.whereType<String>()
.toSet();
} catch (e) {
debugPrint('⚠️ [WebDAVList] 读取收藏失败: $e');
}
if (mounted) {
safeSetState(() {
_items = items;
_metaByPath = meta;
_favoritePaths = favorites;
_isLoading = false;
});
// 如果当前正在播的歌就在这个目录里,补一次同步
_lastSyncedSongPath = null;
_onPlaybackChanged();
}
} catch (e) {
if (mounted) {
safeSetState(() {
_errorMessage = '加载失败: $e';
_isLoading = false;
});
}
}
}
// ============================================================
// 进入子目录
// ============================================================
void _enterDirectory(WebDAVItem dir) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => WebDAVFileListPage(currentPath: dir.path),
),
);
}
// ============================================================
// ⭐ 播放单首(使用 PlaybackController)
// ============================================================
void _playSong(WebDAVItem file) async {
try {
await PlaybackController().playFromWebDAV(
directoryPath: _currentPath,
clickedSongPath: file.path,
allItems: _items,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('正在播放: ${_safeDecode(file.name)}'),
backgroundColor: const Color(0xFF4CAF50),
duration: const Duration(seconds: 1),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('播放失败: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
// ============================================================
// ⭐ 全部播放(使用 PlaybackController)
// ============================================================
void _playAll() async {
try {
await PlaybackController().playAllFromWebDAV(
directoryPath: _currentPath,
allItems: _items,
);
if (mounted) {
final musicCount = _items.where((item) => !item.isDirectory).length;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('开始播放全部 ($musicCount 首)'),
backgroundColor: const Color(0xFF4CAF50),
duration: const Duration(seconds: 1),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('播放失败: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
// ============================================================
// 显示路径
// ============================================================
String _getDisplayPath() {
if (_currentPath == '/' || _currentPath.isEmpty) return '根目录';
final decoded = _safeDecode(_currentPath);
final parts = decoded.split('/').where((s) => s.isNotEmpty).toList();
if (parts.isEmpty) return '根目录';
if (parts.length <= 2) return parts.join(' / ');
return '... / ${parts.sublist(parts.length - 2).join(' / ')}';
}
// ============================================================
// Build
// ============================================================
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0E1211),
appBar: AppBar(
title: Text(
_getDisplayPath(),
style: const TextStyle(fontSize: 16),
),
backgroundColor: Colors.transparent,
elevation: 0,
foregroundColor: Colors.white,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.playlist_play),
onPressed: _playAll,
tooltip: '全部播放',
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadDirectory,
tooltip: '刷新',
),
],
),
body: _isLoading
? const Center(
child: CircularProgressIndicator(
color: Color(0xFFB8D4D0),
),
)
: _errorMessage.isNotEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline,
size: 48, color: Colors.grey[600]),
const SizedBox(height: 16),
Text(
_errorMessage,
style: TextStyle(color: Colors.grey[400]),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadDirectory,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFB8D4D0),
foregroundColor: Colors.black87,
),
child: const Text('重试'),
),
],
),
)
: _items.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.folder_open,
size: 48, color: Colors.grey[600]),
const SizedBox(height: 16),
Text(
'此目录为空',
style: TextStyle(color: Colors.grey[400]),
),
const SizedBox(height: 8),
Text(
'支持格式: MP3, FLAC, M4A, APE, WAV, OPUS',
style: TextStyle(
color: Colors.grey[600], fontSize: 12),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
bottom: UIConstants.miniPlayerBottomSpace,
),
itemCount: _items.length,
itemBuilder: (context, index) {
final item = _items[index];
return _buildListItem(item);
},
),
);
}
// ============================================================
// 构建列表项
// ============================================================
Widget _buildListItem(WebDAVItem item) {
final displayName = _safeDecode(item.name);
if (item.isDirectory) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
visualDensity: VisualDensity.compact,
leading: const Icon(
Icons.folder_outlined,
color: Color(0xFFB8D4D0),
size: 32,
),
title: Text(
displayName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'文件夹',
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
trailing: const Icon(
Icons.chevron_right,
color: Colors.grey,
),
onTap: () => _enterDirectory(item),
);
} else {
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),
visualDensity: VisualDensity.compact,
leading: SongArtwork(
filePath: meta?['artwork_path'] as String?,
size: 44,
fallbackIcon: Icons.audiotrack,
),
title: Text(
titleText,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
subtitleText,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
trailing: IconButton(
tooltip: '收藏',
icon: Icon(
_favoritePaths.contains(item.path)
? Icons.favorite
: Icons.favorite_border,
color: _favoritePaths.contains(item.path)
? const Color(0xFFE05C5C)
: Colors.grey,
),
onPressed: () => _toggleFavorite(item.path),
),
onTap: () => _playSong(item),
onLongPress: () => showAddToPlaylistSheet(context, songPath: item.path),
);
}
}
}