new: sftp search (#345)
This commit is contained in:
@@ -17,6 +17,7 @@ import 'package:toolbox/data/res/provider.dart';
|
|||||||
import 'package:toolbox/data/res/store.dart';
|
import 'package:toolbox/data/res/store.dart';
|
||||||
import 'package:toolbox/view/widget/omit_start_text.dart';
|
import 'package:toolbox/view/widget/omit_start_text.dart';
|
||||||
import 'package:toolbox/view/widget/cardx.dart';
|
import 'package:toolbox/view/widget/cardx.dart';
|
||||||
|
import 'package:toolbox/view/widget/search.dart';
|
||||||
import 'package:toolbox/view/widget/val_builder.dart';
|
import 'package:toolbox/view/widget/val_builder.dart';
|
||||||
|
|
||||||
import '../../../core/extension/numx.dart';
|
import '../../../core/extension/numx.dart';
|
||||||
@@ -129,8 +130,10 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
final children = widget.isSelect
|
final children = widget.isSelect
|
||||||
? [
|
? [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => context.pop(_status.path?.path),
|
onPressed: () => context.pop(_status.path?.path),
|
||||||
icon: const Icon(Icons.done))
|
icon: const Icon(Icons.done),
|
||||||
|
),
|
||||||
|
_buildSearchBtn(),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -143,6 +146,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
_buildAddBtn(),
|
_buildAddBtn(),
|
||||||
_buildGotoBtn(),
|
_buildGotoBtn(),
|
||||||
_buildUploadBtn(),
|
_buildUploadBtn(),
|
||||||
|
_buildSearchBtn(),
|
||||||
];
|
];
|
||||||
if (isDesktop) children.add(_buildRefreshBtn());
|
if (isDesktop) children.add(_buildRefreshBtn());
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
@@ -162,6 +166,28 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchBtn() {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Stream<SftpName> 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() {
|
Widget _buildUploadBtn() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -314,7 +340,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildItem(SftpName file) {
|
Widget _buildItem(SftpName file, {VoidCallback? beforeTap}) {
|
||||||
final isDir = file.attr.isDirectory;
|
final isDir = file.attr.isDirectory;
|
||||||
final trailing = Text(
|
final trailing = Text(
|
||||||
'${_getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}',
|
'${_getTime(file.attr.modifyTime)}\n${file.attr.mode?.str ?? ''}',
|
||||||
@@ -333,6 +359,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
style: UIs.textGrey,
|
style: UIs.textGrey,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
beforeTap?.call();
|
||||||
if (isDir) {
|
if (isDir) {
|
||||||
_status.path?.update(file.filename);
|
_status.path?.update(file.filename);
|
||||||
_listDir();
|
_listDir();
|
||||||
@@ -340,7 +367,10 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
_onItemPress(file, true);
|
_onItemPress(file, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () => _onItemPress(file, !isDir),
|
onLongPress: () {
|
||||||
|
beforeTap?.call();
|
||||||
|
_onItemPress(file, !isDir);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
87
lib/view/widget/search.dart
Normal file
87
lib/view/widget/search.dart
Normal file
@@ -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<T> extends SearchDelegate<T> {
|
||||||
|
final Future<List<T>> Function(String) future;
|
||||||
|
final Widget Function(BuildContext, T) builder;
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
final Duration throttleInterval;
|
||||||
|
|
||||||
|
List<T> _cache = [];
|
||||||
|
DateTime? _lastSearch;
|
||||||
|
|
||||||
|
SearchPage({
|
||||||
|
required this.future,
|
||||||
|
required this.builder,
|
||||||
|
this.padding,
|
||||||
|
this.throttleInterval = const Duration(milliseconds: 200),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget>? 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<List<T>> _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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user