- 列表页右上角「全部播放」换成搜索(刷新保留) - 新页面 WebDAVSearchPage:输入关键字实时过滤当前文件夹的歌曲 - 匹配范围:文件名、元数据标题、艺术家(大小写不敏感) - 结果显示封面/标题/艺术家,点击从该首开始播放并返回列表 - 空状态分两种:未输入提示、无结果提示 真机验证:搜索 "forza" 命中 2 首,一条显示标签标题、一条显示文件名。
216 lines
6.6 KiB
Dart
216 lines
6.6 KiB
Dart
// lib/pages/webdav_search_page.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../controllers/playback_controller.dart';
|
|
import '../database/song_database.dart';
|
|
import '../services/webdav_service.dart';
|
|
import '../widgets/song_artwork.dart';
|
|
|
|
/// 在当前 WebDAV 文件夹内搜索歌曲
|
|
class WebDAVSearchPage extends StatefulWidget {
|
|
final String directoryPath;
|
|
final List<WebDAVItem> items;
|
|
|
|
const WebDAVSearchPage({
|
|
super.key,
|
|
required this.directoryPath,
|
|
required this.items,
|
|
});
|
|
|
|
@override
|
|
State<WebDAVSearchPage> createState() => _WebDAVSearchPageState();
|
|
}
|
|
|
|
class _WebDAVSearchPageState extends State<WebDAVSearchPage> {
|
|
final TextEditingController _controller = TextEditingController();
|
|
final FocusNode _focusNode = FocusNode();
|
|
|
|
/// path -> songs 表行
|
|
Map<String, Map<String, dynamic>> _meta = {};
|
|
String _query = '';
|
|
|
|
List<WebDAVItem> get _files =>
|
|
widget.items.where((item) => !item.isDirectory).toList();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadMeta();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadMeta() async {
|
|
try {
|
|
final paths = _files.map((item) => item.path).toList();
|
|
final meta = await SongDatabase().getSongsByPaths(paths);
|
|
if (!mounted) return;
|
|
setState(() => _meta = meta);
|
|
} catch (e) {
|
|
debugPrint('⚠️ [Search] 读取元数据失败: $e');
|
|
}
|
|
}
|
|
|
|
String _decode(String input) {
|
|
try {
|
|
return Uri.decodeComponent(input);
|
|
} catch (_) {
|
|
return input;
|
|
}
|
|
}
|
|
|
|
List<WebDAVItem> get _results {
|
|
final keyword = _query.trim().toLowerCase();
|
|
if (keyword.isEmpty) return const [];
|
|
return _files.where((item) {
|
|
final name = _decode(item.name).toLowerCase();
|
|
final meta = _meta[item.path];
|
|
final title = (meta?['title'] as String? ?? '').toLowerCase();
|
|
final artist = (meta?['artist'] as String? ?? '').toLowerCase();
|
|
return name.contains(keyword) ||
|
|
title.contains(keyword) ||
|
|
artist.contains(keyword);
|
|
}).toList();
|
|
}
|
|
|
|
Future<void> _play(WebDAVItem item) async {
|
|
try {
|
|
await PlaybackController().playFromWebDAV(
|
|
directoryPath: widget.directoryPath,
|
|
clickedSongPath: item.path,
|
|
allItems: widget.items,
|
|
);
|
|
if (mounted) Navigator.pop(context);
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('播放失败: $e')),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final results = _results;
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFF0E1211),
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
foregroundColor: Colors.white,
|
|
titleSpacing: 0,
|
|
title: TextField(
|
|
controller: _controller,
|
|
focusNode: _focusNode,
|
|
autofocus: true,
|
|
style: const TextStyle(color: Colors.white, fontSize: 16),
|
|
cursorColor: const Color(0xFFB8D4D0),
|
|
decoration: const InputDecoration(
|
|
hintText: '搜索本文件夹的歌曲',
|
|
hintStyle: TextStyle(color: Colors.white38, fontSize: 15),
|
|
border: InputBorder.none,
|
|
),
|
|
onChanged: (value) => setState(() => _query = value),
|
|
),
|
|
actions: [
|
|
if (_query.isNotEmpty)
|
|
IconButton(
|
|
icon: const Icon(Icons.close, color: Colors.white54),
|
|
tooltip: '清除',
|
|
onPressed: () {
|
|
_controller.clear();
|
|
setState(() => _query = '');
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: _query.trim().isEmpty
|
|
? _buildHint()
|
|
: results.isEmpty
|
|
? _buildEmpty()
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16, vertical: 8),
|
|
itemCount: results.length,
|
|
itemBuilder: (context, index) {
|
|
final item = results[index];
|
|
final meta = _meta[item.path];
|
|
final metaTitle =
|
|
(meta?['title'] as String?)?.trim() ?? '';
|
|
final metaArtist =
|
|
(meta?['artist'] as String?)?.trim() ?? '';
|
|
final artwork =
|
|
(meta?['artwork_path'] as String?)?.trim() ?? '';
|
|
final name = _decode(item.name);
|
|
|
|
return ListTile(
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 4),
|
|
visualDensity: VisualDensity.compact,
|
|
leading: SongArtwork(
|
|
filePath: artwork.isNotEmpty ? artwork : null,
|
|
size: 44,
|
|
fallbackIcon: Icons.audiotrack,
|
|
),
|
|
title: Text(
|
|
metaTitle.isNotEmpty ? metaTitle : name,
|
|
style: const TextStyle(
|
|
color: Colors.white, fontSize: 16),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
subtitle: Text(
|
|
metaArtist.isNotEmpty
|
|
? metaArtist
|
|
: (item.size != null
|
|
? '${(item.size! / 1024 / 1024).toStringAsFixed(1)} MB'
|
|
: ''),
|
|
style: const TextStyle(
|
|
color: Colors.grey, fontSize: 12),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
onTap: () => _play(item),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHint() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.search, size: 48, color: Colors.grey[700]),
|
|
const SizedBox(height: 14),
|
|
Text(
|
|
'输入歌名、歌手或文件名',
|
|
style: TextStyle(color: Colors.grey[500], fontSize: 14),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'只在当前文件夹内搜索',
|
|
style: TextStyle(color: Colors.grey[700], fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmpty() {
|
|
return Center(
|
|
child: Text(
|
|
'没有找到「${_query.trim()}」',
|
|
style: TextStyle(color: Colors.grey[500], fontSize: 14),
|
|
),
|
|
);
|
|
}
|
|
}
|