diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index e07f345c..6bb8586a 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -17,6 +17,7 @@ import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/view/widget/omit_start_text.dart'; import 'package:toolbox/view/widget/cardx.dart'; +import 'package:toolbox/view/widget/search.dart'; import 'package:toolbox/view/widget/val_builder.dart'; import '../../../core/extension/numx.dart'; @@ -129,8 +130,10 @@ class _SftpPageState extends State with AfterLayoutMixin { final children = widget.isSelect ? [ IconButton( - onPressed: () => context.pop(_status.path?.path), - icon: const Icon(Icons.done)) + onPressed: () => context.pop(_status.path?.path), + icon: const Icon(Icons.done), + ), + _buildSearchBtn(), ] : [ IconButton( @@ -143,6 +146,7 @@ class _SftpPageState extends State with AfterLayoutMixin { _buildAddBtn(), _buildGotoBtn(), _buildUploadBtn(), + _buildSearchBtn(), ]; if (isDesktop) children.add(_buildRefreshBtn()); return SafeArea( @@ -162,6 +166,28 @@ class _SftpPageState extends State with AfterLayoutMixin { ); } + Widget _buildSearchBtn() { + return IconButton( + onPressed: () async { + Stream find(String query) async* { + final fs = _status.files; + if (fs == null) return; + for (final f in fs) { + if (f.filename.contains(query)) yield f; + } + } + + final search = SearchPage( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), + future: (q) => find(q).toList(), + builder: (ctx, e) => _buildItem(e, beforeTap: () => ctx.pop()), + ); + await showSearch(context: context, delegate: search); + }, + icon: const Icon(Icons.search), + ); + } + Widget _buildUploadBtn() { return IconButton( onPressed: () async { @@ -314,7 +340,7 @@ class _SftpPageState extends State with AfterLayoutMixin { ); } - Widget _buildItem(SftpName file) { + Widget _buildItem(SftpName file, {VoidCallback? beforeTap}) { final isDir = file.attr.isDirectory; final trailing = Text( '${_getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}', @@ -333,6 +359,7 @@ class _SftpPageState extends State with AfterLayoutMixin { style: UIs.textGrey, ), onTap: () { + beforeTap?.call(); if (isDir) { _status.path?.update(file.filename); _listDir(); @@ -340,7 +367,10 @@ class _SftpPageState extends State with AfterLayoutMixin { _onItemPress(file, true); } }, - onLongPress: () => _onItemPress(file, !isDir), + onLongPress: () { + beforeTap?.call(); + _onItemPress(file, !isDir); + }, ), ); } diff --git a/lib/view/widget/search.dart b/lib/view/widget/search.dart new file mode 100644 index 00000000..9dda8a9a --- /dev/null +++ b/lib/view/widget/search.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:toolbox/core/extension/context/common.dart'; +import 'package:toolbox/core/extension/context/locale.dart'; +import 'package:toolbox/data/res/ui.dart'; +import 'package:toolbox/view/widget/future_widget.dart'; + +final class SearchPage extends SearchDelegate { + final Future> Function(String) future; + final Widget Function(BuildContext, T) builder; + final EdgeInsetsGeometry? padding; + final Duration throttleInterval; + + List _cache = []; + DateTime? _lastSearch; + + SearchPage({ + required this.future, + required this.builder, + this.padding, + this.throttleInterval = const Duration(milliseconds: 200), + }); + + @override + List? buildActions(BuildContext context) { + return [ + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + query = ''; + }, + ), + ]; + } + + @override + Widget? buildLeading(BuildContext context) { + return IconButton( + onPressed: context.pop, + icon: const Icon(Icons.arrow_back), + ); + } + + @override + Widget buildResults(BuildContext context) { + return _buildList(context); + } + + @override + Widget buildSuggestions(BuildContext context) { + return _buildList(context); + } + + Widget _buildList(BuildContext context) { + return FutureWidget( + future: _search(query), + loading: const Center(child: UIs.centerSizedLoading), + error: (error, trace) { + return Center( + child: Text('$error\n$trace'), + ); + }, + success: (list) { + if (list == null || list.isEmpty) { + return Center(child: Text(l10n.noResult)); + } + + return ListView.builder( + padding: padding, + itemCount: list.length, + itemBuilder: (_, index) => builder(context, list[index]), + ); + }, + ); + } + + Future> _search(String query) async { + final lastSearch = _lastSearch; + if (lastSearch != null) { + final now = DateTime.now(); + if (now.difference(lastSearch) < throttleInterval) { + return _cache; + } + } + _cache = await future(query); + return _cache; + } +}