// lib/pages/webdav_file_list_page.dart // ignore: unused_import import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/webdav_service.dart'; import '../services/playback_service.dart'; import '../services/audio_service.dart'; import '../constants/ui_constants.dart'; class WebDAVFileListPage extends StatefulWidget { final String currentPath; const WebDAVFileListPage({super.key, this.currentPath = '/'}); @override State createState() => _WebDAVFileListPageState(); } class _WebDAVFileListPageState extends State { List _items = []; bool _isLoading = true; String _errorMessage = ''; String _currentPath = '/'; // 安全解码 String _safeDecode(String input) { try { return Uri.decodeComponent(input); } catch (_) { return input; } } @override void initState() { super.initState(); _currentPath = widget.currentPath; _loadDirectory(); } // ============================================================ // 加载目录 // ============================================================ Future _loadDirectory() async { setState(() { _isLoading = true; _errorMessage = ''; }); try { final items = await WebDAVService.instance.listDirectory(path: _currentPath); setState(() { _items = items; _isLoading = false; }); } catch (e) { setState(() { _errorMessage = '加载失败: $e'; _isLoading = false; }); } } // ============================================================ // 进入子目录 // ============================================================ void _enterDirectory(WebDAVItem dir) { Navigator.push( context, MaterialPageRoute( builder: (_) => WebDAVFileListPage(currentPath: dir.path), ), ); } // ============================================================ // ⭐ 核心:播放歌曲 + 自动构建队列 // ============================================================ void _playSong(WebDAVItem file) async { try { // 1. 获取当前目录所有音乐文件(过滤掉目录) final musicFiles = _items.where((item) => !item.isDirectory).toList(); if (musicFiles.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('当前目录没有音乐文件'), backgroundColor: Colors.orange, ), ); } return; } // 2. 获取认证头 final headers = await WebDAVService.instance.getAuthHeaders(); // 3. 构建播放队列(所有音乐文件) final queue = musicFiles.map((item) { final itemUrl = WebDAVService.instance.getFileUrl(item.path); return Song( id: item.path, title: item.name.replaceAll(RegExp(r'\.[^.]*$'), ''), artist: '未知艺术家', url: itemUrl, ); }).toList(); // 4. 找到当前点击歌曲在队列中的位置 final startIndex = queue.indexWhere((s) => s.id == file.path); if (startIndex == -1) { // 极端情况:队列构建有问题,退化为单曲播放 final url = WebDAVService.instance.getFileUrl(file.path); final song = Song( id: file.path, title: file.name.replaceAll(RegExp(r'\.[^.]*$'), ''), artist: '未知艺术家', url: url, ); context.read().setQueue([song], startIndex: 0); await PlaybackService().play(url, headers: headers); return; } // 5. 设置队列并播放 final audioService = context.read(); audioService.setQueue(queue, startIndex: startIndex); // 6. 播放(AudioService 内部已经调用了 PlaybackService,但为了确保认证头传递) // 这里再显式调用一下,确保认证头正确 final targetUrl = queue[startIndex].url!; await PlaybackService().play(targetUrl, headers: headers); // 7. 更新 AudioService 的播放状态(确保 UI 同步) // setQueue 已经调用了 _playCurrent(),所以这里不需要重复调用 if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('正在播放: ${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, ), ); } } } // ============================================================ // 全部播放(从第一首开始) // ============================================================ void _playAll() async { try { final musicFiles = _items.where((item) => !item.isDirectory).toList(); if (musicFiles.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('当前目录没有音乐文件'), backgroundColor: Colors.orange, ), ); } return; } final headers = await WebDAVService.instance.getAuthHeaders(); final queue = musicFiles.map((item) { final itemUrl = WebDAVService.instance.getFileUrl(item.path); return Song( id: item.path, title: item.name.replaceAll(RegExp(r'\.[^.]*$'), ''), artist: '未知艺术家', url: itemUrl, ); }).toList(); final audioService = context.read(); audioService.setQueue(queue, startIndex: 0); final targetUrl = queue[0].url!; await PlaybackService().play(targetUrl, headers: headers); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('开始播放全部 (${queue.length}首)'), 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, vertical: 2), 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' : ''; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), leading: const Icon( Icons.audiotrack, color: Color(0xFFB8D4D0), size: 28, ), title: Text( displayName, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w400, color: Colors.white, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text( sizeStr, style: TextStyle( fontSize: 12, color: Colors.grey[500], ), ), trailing: IconButton( icon: const Icon(Icons.play_arrow, color: Color(0xFFB8D4D0)), onPressed: () => _playSong(item), ), onTap: () => _playSong(item), ); } } }