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

331 lines
10 KiB
Dart
Raw Permalink 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.
// 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';
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 = '/';
// 安全解码
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 {
safeSetState(() {
_isLoading = true;
_errorMessage = '';
});
try {
final items =
await WebDAVService.instance.listDirectory(path: _currentPath);
if (mounted) {
safeSetState(() {
_items = items;
_isLoading = false;
});
}
} 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, 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),
);
}
}
}