Files
QingTing-Player/lib/pages/webdav_file_list_page.dart
T

403 lines
12 KiB
Dart

// 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';
class WebDAVFileListPage extends StatefulWidget {
final String currentPath;
const WebDAVFileListPage({super.key, this.currentPath = '/'});
@override
State<WebDAVFileListPage> createState() => _WebDAVFileListPageState();
}
class _WebDAVFileListPageState extends State<WebDAVFileListPage> {
List<WebDAVItem> _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<void> _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<AudioService>().setQueue([song], startIndex: 0);
await PlaybackService().play(url, headers: headers);
return;
}
// 5. 设置队列并播放
final audioService = context.read<AudioService>();
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>();
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.symmetric(
horizontal: 16, vertical: 8),
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),
);
}
}
}