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 createState() => _WebDAVFileListPageState(); } class _WebDAVFileListPageState extends BaseState { List _items = []; bool _isLoading = true; String _errorMessage = ''; String _currentPath = '/'; /// path -> 元数据行(songs 表),用于把文件名替换成标签里的标题/艺术家 Map> _metaByPath = {}; String? _lastSyncedSongPath; StreamSubscription? _metaSub; /// 已收藏的歌曲路径 Set _favoritePaths = {}; // 安全解码 String _safeDecode(String input) { try { return Uri.decodeComponent(input); } catch (_) { return input; } } @override void initState() { super.initState(); _currentPath = widget.currentPath; AudioService().addListener(_onPlaybackChanged); _loadDirectory(); } @override void dispose() { 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 _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 _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 _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 = >{}; try { meta = await SongDatabase().getSongsByPaths(paths); } catch (e) { debugPrint('⚠️ [WebDAVList] 批量读取元数据失败: $e'); } var favorites = {}; try { final favRows = await SongDatabase().getFavorites(); favorites = favRows .map((row) => row['song_id'] as String?) .whereType() .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), ); } } }