From 7d701c5dfc085a4cb0241b89c06272d23627d2b3 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Sun, 1 Nov 2026 14:55:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(search):=20WebDAV=20=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=B9=E5=86=85=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 列表页右上角「全部播放」换成搜索(刷新保留) - 新页面 WebDAVSearchPage:输入关键字实时过滤当前文件夹的歌曲 - 匹配范围:文件名、元数据标题、艺术家(大小写不敏感) - 结果显示封面/标题/艺术家,点击从该首开始播放并返回列表 - 空状态分两种:未输入提示、无结果提示 真机验证:搜索 "forza" 命中 2 首,一条显示标签标题、一条显示文件名。 --- lib/pages/webdav_file_list_page.dart | 15 +- lib/pages/webdav_search_page.dart | 215 +++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 lib/pages/webdav_search_page.dart diff --git a/lib/pages/webdav_file_list_page.dart b/lib/pages/webdav_file_list_page.dart index f65d5a3..3ac004d 100644 --- a/lib/pages/webdav_file_list_page.dart +++ b/lib/pages/webdav_file_list_page.dart @@ -10,6 +10,7 @@ import '../services/audio_service.dart'; import '../services/metadata_events.dart'; import '../widgets/song_artwork.dart'; import '../widgets/add_to_playlist_sheet.dart'; +import 'webdav_search_page.dart'; class WebDAVFileListPage extends StatefulWidget { final String currentPath; @@ -278,9 +279,17 @@ class _WebDAVFileListPageState extends BaseState { ), actions: [ IconButton( - icon: const Icon(Icons.playlist_play), - onPressed: _playAll, - tooltip: '全部播放', + icon: const Icon(Icons.search), + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => WebDAVSearchPage( + directoryPath: _currentPath, + items: _items, + ), + ), + ), + tooltip: '搜索本文件夹', ), IconButton( icon: const Icon(Icons.refresh), diff --git a/lib/pages/webdav_search_page.dart b/lib/pages/webdav_search_page.dart new file mode 100644 index 0000000..e0b6a28 --- /dev/null +++ b/lib/pages/webdav_search_page.dart @@ -0,0 +1,215 @@ +// 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 items; + + const WebDAVSearchPage({ + super.key, + required this.directoryPath, + required this.items, + }); + + @override + State createState() => _WebDAVSearchPageState(); +} + +class _WebDAVSearchPageState extends State { + final TextEditingController _controller = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + + /// path -> songs 表行 + Map> _meta = {}; + String _query = ''; + + List 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 _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 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 _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), + ), + ); + } +}