import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:provider/provider.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import '../common.dart'; import '../models/model.dart'; import '../widgets/dialog.dart'; class FileManagerPage extends StatefulWidget { FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @override State createState() => _FileManagerPageState(); } class _FileManagerPageState extends State { final model = FFI.fileModel; final _selectedItems = SelectedItems(); Timer? _interval; Timer? _timer; var _reconnects = 1; final _breadCrumbScroller = ScrollController(); @override void initState() { super.initState(); showLoading(translate('Connecting...')); FFI.connect(widget.id, isFileTransfer: true); _interval = Timer.periodic(Duration(milliseconds: 30), (timer) => FFI.ffiModel.update(widget.id, handleMsgBox)); } @override void dispose() { model.onClose(); _interval?.cancel(); FFI.close(); EasyLoading.dismiss(); super.dispose(); } @override Widget build(BuildContext context) => Consumer(builder: (_context, _model, _child) { return WillPopScope( onWillPop: () async { if (model.selectMode) { model.toggleSelectMode(); } else { goBack(); } return false; }, child: Scaffold( backgroundColor: MyTheme.grayBg, appBar: AppBar( leading: Row(children: [ IconButton(icon: Icon(Icons.arrow_back), onPressed: goBack), IconButton(icon: Icon(Icons.close), onPressed: clientClose), ]), leadingWidth: 200, centerTitle: true, title: Text(translate(model.isLocal ? "Local" : "Remote")), actions: [ IconButton( icon: Icon(Icons.change_circle), onPressed: () => model.togglePage(), ) ], ), body: body(), bottomSheet: bottomSheet(), )); }); bool needShowCheckBox() { if (!model.selectMode) { return false; } return !_selectedItems.isOtherPage(model.isLocal); } Widget body() { final isLocal = model.isLocal; final fd = model.currentDir; final entries = fd.entries; return Column(children: [ headTools(), Expanded( child: ListView.builder( itemCount: entries.length + 1, itemBuilder: (context, index) { if (index >= entries.length) { return listTail(); } var selected = false; if (model.selectMode) { selected = _selectedItems.contains(entries[index]); } final sizeStr = entries[index].isFile ? readableFileSize(entries[index].size.toDouble()) : ""; return Card( child: ListTile( leading: Icon( entries[index].isFile ? Icons.feed_outlined : Icons.folder, size: 40), title: Text(entries[index].name), selected: selected, subtitle: Text( entries[index] .lastModified() .toString() .replaceAll(".000", "") + " " + sizeStr, style: TextStyle(fontSize: 12, color: MyTheme.darkGray), ), trailing: needShowCheckBox() ? Checkbox( value: selected, onChanged: (v) { if (v == null) return; if (v && !selected) { _selectedItems.add(isLocal, entries[index]); } else if (!v && selected) { _selectedItems.remove(entries[index]); } setState(() {}); }) : PopupMenuButton( icon: Icon(Icons.more_vert), itemBuilder: (context) { return [ PopupMenuItem( child: Text("删除"), value: "delete", ), PopupMenuItem( child: Text("详细信息"), value: "delete", enabled: false, ) ]; }, onSelected: (v) { if (v == "delete") { final items = SelectedItems(); items.add(isLocal, entries[index]); model.removeAction(items); } }), onTap: () { if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { if (selected) { _selectedItems.remove(entries[index]); } else { _selectedItems.add(isLocal, entries[index]); } setState(() {}); return; } if (entries[index].isDirectory) { model.openDirectory(entries[index].path); breadCrumbScrollToEnd(); } else { // Perform file-related tasks. } }, onLongPress: () { _selectedItems.clear(); model.toggleSelectMode(); if (model.selectMode) { _selectedItems.add(isLocal, entries[index]); } setState(() {}); }, ), ); }, )) ]); } goBack() { model.goToParentDirectory(); } void handleMsgBox(Map evt, String id) { var type = evt['type']; var title = evt['title']; var text = evt['text']; if (type == 're-input-password') { wrongPasswordDialog(id); } else if (type == 'input-password') { enterPasswordDialog(id); } else { var hasRetry = evt['hasRetry'] == 'true'; print(evt); showMsgBox(type, title, text, hasRetry); } } void showMsgBox(String type, String title, String text, bool hasRetry) { msgBox(type, title, text); if (hasRetry) { _timer?.cancel(); _timer = Timer(Duration(seconds: _reconnects), () { FFI.reconnect(); showLoading(translate('Connecting...')); }); _reconnects *= 2; } else { _reconnects = 1; } } breadCrumbScrollToEnd() { Future.delayed(Duration(milliseconds: 200), () { _breadCrumbScroller.animateTo( _breadCrumbScroller.position.maxScrollExtent, duration: Duration(milliseconds: 200), curve: Curves.fastLinearToSlowEaseIn); }); } Widget headTools() => Container( child: Row( children: [ Expanded( child: BreadCrumb( items: getPathBreadCrumbItems(() => model.goHome(), (list) { var path = ""; if (model.currentHome.startsWith(list[0])) { // absolute path for (var item in list) { path = PathUtil.join(path, item, model.currentIsWindows); } } else { path += model.currentHome; for (var item in list) { path = PathUtil.join(path, item, model.currentIsWindows); } } model.openDirectory(path); }), divider: Icon(Icons.chevron_right), overflow: ScrollableOverflow(controller: _breadCrumbScroller), )), Row( children: [ // IconButton(onPressed: () {}, icon: Icon(Icons.sort)), PopupMenuButton( icon: Icon(Icons.sort), itemBuilder: (context) { return SortBy.values .map((e) => PopupMenuItem( child: Text( e.toString().split(".").last.toUpperCase()), value: e, )) .toList(); }, onSelected: model.changeSortStyle), PopupMenuButton( icon: Icon(Icons.more_vert), itemBuilder: (context) { return [ PopupMenuItem( child: Row( children: [ Icon(Icons.refresh), SizedBox(width: 5), Text("刷新") ], ), value: "refresh", ), PopupMenuItem( child: Row( children: [ Icon(Icons.check), SizedBox(width: 5), Text("多选") ], ), value: "select", ), PopupMenuItem( child: Row( children: [ Icon(Icons.folder_outlined), SizedBox(width: 5), Text(translate("Create Folder")) ], ), value: "folder", ), PopupMenuItem( child: Row( children: [ Icon(model.currentShowHidden ? Icons.check_box_outlined : Icons.check_box_outline_blank), SizedBox(width: 5), Text(translate("Toggle Hidden")) ], ), value: "hidden", ) ]; }, onSelected: (v) { if (v == "refresh") { model.refresh(); } else if (v == "select") { _selectedItems.clear(); model.toggleSelectMode(); } else if (v == "folder") { final name = TextEditingController(); DialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate("Create Folder")), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( decoration: InputDecoration( labelText: translate( "Please enter the folder name"), ), controller: name, ), ], ), actions: [ TextButton( style: flatButtonStyle, onPressed: () { if (name.value.text.isNotEmpty) { model.createDir(PathUtil.join( model.currentDir.path, name.value.text, model.currentIsWindows)); close(); } }, child: Text(translate("OK"))), TextButton( style: flatButtonStyle, onPressed: () => close(false), child: Text(translate("Cancel"))) ])); } else if (v == "hidden") { model.toggleShowHidden(); } }), ], ) ], )); Widget emptyPage() { return Column( children: [ headTools(), Expanded(child: Center(child: Text("Empty Directory"))) ], ); } Widget listTail() { return Container( height: 100, child: Column( children: [ Padding( padding: EdgeInsets.all(2), child: Text( "总计: ${model.currentDir.entries.length}个项目", style: TextStyle(color: MyTheme.darkGray), ), ) ], ), ); } Widget? bottomSheet() { final state = model.jobState; final isOtherPage = _selectedItems.isOtherPage(model.isLocal); final selectedItemsLength = "${_selectedItems.length} 个项目"; final local = _selectedItems.isLocal == null ? "" : " [${_selectedItems.isLocal! ? '本地' : '远程'}]"; if (model.selectMode) { if (_selectedItems.length == 0 || !isOtherPage) { // 选择模式 当前选择页面 return BottomSheetBody( leading: Icon(Icons.check), title: "已选择", text: selectedItemsLength + local, onCanceled: () => model.toggleSelectMode(), actions: [ IconButton( icon: Icon(Icons.delete_forever), onPressed: () { if (_selectedItems.length > 0) { model.removeAction(_selectedItems); } }, ) ]); } else { // 选择模式 复制目标页面 return BottomSheetBody( leading: Icon(Icons.input), title: "粘贴到这里?", text: selectedItemsLength + local, onCanceled: () => model.toggleSelectMode(), actions: [ IconButton( icon: Icon(Icons.paste), onPressed: () { model.toggleSelectMode(); model.sendFiles(_selectedItems); }, ) ]); } } switch (state) { case JobState.inProgress: return BottomSheetBody( leading: CircularProgressIndicator(), title: "正在发送文件...", text: "速度: ${readableFileSize(model.jobProgress.speed)}/s", onCanceled: null, ); case JobState.done: return BottomSheetBody( leading: Icon(Icons.check), title: "操作成功!", text: "", onCanceled: () => model.jobReset(), ); case JobState.error: return BottomSheetBody( leading: Icon(Icons.error), title: "错误!", text: "", onCanceled: () => model.jobReset(), ); case JobState.none: break; } return null; } List getPathBreadCrumbItems( void Function() onHome, void Function(List) onPressed) { final path = model.currentShortPath; final list = PathUtil.split(path, model.currentIsWindows); final breadCrumbList = [ BreadCrumbItem( content: IconButton( icon: Icon(Icons.home_filled), onPressed: onHome, )) ]; breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( content: TextButton( child: Text(e.value), style: ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))), onPressed: () => onPressed(list.sublist(0, e.key + 1)))))); return breadCrumbList; } } class BottomSheetBody extends StatelessWidget { BottomSheetBody( {required this.leading, required this.title, required this.text, this.onCanceled, this.actions}); final Widget leading; final String title; final String text; final VoidCallback? onCanceled; final List? actions; @override BottomSheet build(BuildContext context) { final _actions = actions ?? []; return BottomSheet( builder: (BuildContext context) { return Container( height: 65, alignment: Alignment.centerLeft, decoration: BoxDecoration( color: MyTheme.accent50, borderRadius: BorderRadius.vertical(top: Radius.circular(10))), child: Padding( padding: EdgeInsets.symmetric(horizontal: 15), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ leading, SizedBox(width: 16), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(fontSize: 18)), Text(text, style: TextStyle( fontSize: 14, color: MyTheme.grayBg)) ], ) ], ), Row(children: () { _actions.add(IconButton( icon: Icon(Icons.cancel_outlined), onPressed: onCanceled, )); return _actions; }()) ], ), )); }, onClosing: () {}, backgroundColor: MyTheme.grayBg, enableDrag: false, ); } } class SelectedItems { bool? _isLocal; final List _items = []; List get items => _items; int get length => _items.length; bool? get isLocal => _isLocal; add(bool isLocal, Entry e) { if (_isLocal == null) { _isLocal = isLocal; } if (_isLocal != null && _isLocal != isLocal) { return; } if (!_items.contains(e)) { _items.add(e); } } bool contains(Entry e) { return _items.contains(e); } remove(Entry e) { _items.remove(e); if (_items.length == 0) { _isLocal = null; } } bool isOtherPage(bool currentIsLocal) { if (_isLocal == null) { return false; } else { return _isLocal != currentIsLocal; } } clear() { _items.clear(); _isLocal = null; } }