mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-12-25 04:37:49 +08:00
1485 lines
56 KiB
Dart
1485 lines
56 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
|
|
import 'package:percent_indicator/percent_indicator.dart';
|
|
import 'package:desktop_drop/desktop_drop.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
|
|
import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart';
|
|
import 'package:flutter_hbb/desktop/widgets/menu_button.dart';
|
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
|
import 'package:flutter_hbb/models/file_model.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:wakelock/wakelock.dart';
|
|
|
|
import '../../consts.dart';
|
|
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
|
import '../../common.dart';
|
|
import '../../models/model.dart';
|
|
import '../../models/platform_model.dart';
|
|
import '../widgets/popup_menu.dart';
|
|
|
|
/// status of location bar
|
|
enum LocationStatus {
|
|
/// normal bread crumb bar
|
|
bread,
|
|
|
|
/// show path text field
|
|
pathLocation,
|
|
|
|
/// show file search bar text field
|
|
fileSearchBar
|
|
}
|
|
|
|
/// The status of currently focused scope of the mouse
|
|
enum MouseFocusScope {
|
|
/// Mouse is in local field.
|
|
local,
|
|
|
|
/// Mouse is in remote field.
|
|
remote,
|
|
|
|
/// Mouse is not in local field, remote neither.
|
|
none
|
|
}
|
|
|
|
class FileManagerPage extends StatefulWidget {
|
|
const FileManagerPage(
|
|
{Key? key,
|
|
required this.id,
|
|
required this.password,
|
|
required this.tabController,
|
|
this.forceRelay})
|
|
: super(key: key);
|
|
final String id;
|
|
final String? password;
|
|
final bool? forceRelay;
|
|
final DesktopTabController tabController;
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _FileManagerPageState();
|
|
}
|
|
|
|
class _FileManagerPageState extends State<FileManagerPage>
|
|
with AutomaticKeepAliveClientMixin {
|
|
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
|
|
|
|
final _dropMaskVisible = false.obs; // TODO impl drop mask
|
|
final _overlayKeyState = OverlayKeyState();
|
|
|
|
late FFI _ffi;
|
|
|
|
FileModel get model => _ffi.fileModel;
|
|
JobController get jobController => model.jobController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_ffi = FFI(null);
|
|
_ffi.start(widget.id,
|
|
isFileTransfer: true,
|
|
password: widget.password,
|
|
forceRelay: widget.forceRelay);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_ffi.dialogManager
|
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
|
});
|
|
Get.put(_ffi, tag: 'ft_${widget.id}');
|
|
if (!Platform.isLinux) {
|
|
Wakelock.enable();
|
|
}
|
|
debugPrint("File manager page init success with id ${widget.id}");
|
|
_ffi.dialogManager.setOverlayState(_overlayKeyState);
|
|
widget.tabController.onSelected?.call(widget.id);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
model.close().whenComplete(() {
|
|
_ffi.close();
|
|
_ffi.dialogManager.dismissAll();
|
|
if (!Platform.isLinux) {
|
|
Wakelock.disable();
|
|
}
|
|
Get.delete<FFI>(tag: 'ft_${widget.id}');
|
|
});
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return Overlay(key: _overlayKeyState.key, initialEntries: [
|
|
OverlayEntry(builder: (_) {
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
body: Row(
|
|
children: [
|
|
Flexible(
|
|
flex: 3,
|
|
child: dropArea(FileManagerView(
|
|
model.localController, _ffi, _mouseFocusScope))),
|
|
Flexible(
|
|
flex: 3,
|
|
child: dropArea(FileManagerView(
|
|
model.remoteController, _ffi, _mouseFocusScope))),
|
|
Flexible(flex: 2, child: statusList())
|
|
],
|
|
),
|
|
);
|
|
})
|
|
]);
|
|
}
|
|
|
|
Widget dropArea(FileManagerView fileView) {
|
|
return DropTarget(
|
|
onDragDone: (detail) =>
|
|
handleDragDone(detail, fileView.controller.isLocal),
|
|
onDragEntered: (enter) {
|
|
_dropMaskVisible.value = true;
|
|
},
|
|
onDragExited: (exit) {
|
|
_dropMaskVisible.value = false;
|
|
},
|
|
child: fileView);
|
|
}
|
|
|
|
Widget generateCard(Widget child) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(15.0),
|
|
),
|
|
),
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
/// transfer status list
|
|
/// watch transfer status
|
|
Widget statusList() {
|
|
statusListView(List<JobProgress> jobs) => ListView.builder(
|
|
controller: ScrollController(),
|
|
itemBuilder: (BuildContext context, int index) {
|
|
final item = jobs[index];
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 5),
|
|
child: generateCard(
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Transform.rotate(
|
|
angle: item.isRemoteToLocal ? pi : 0,
|
|
child: SvgPicture.asset(
|
|
"assets/arrow.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
).paddingOnly(left: 15),
|
|
const SizedBox(
|
|
width: 16.0,
|
|
),
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Tooltip(
|
|
waitDuration: Duration(milliseconds: 500),
|
|
message: item.jobName,
|
|
child: Text(
|
|
item.fileName,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
).paddingSymmetric(vertical: 10),
|
|
),
|
|
Text(
|
|
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: MyTheme.darkGray,
|
|
),
|
|
),
|
|
Offstage(
|
|
offstage: item.state != JobState.inProgress,
|
|
child: Text(
|
|
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: MyTheme.darkGray,
|
|
),
|
|
),
|
|
),
|
|
Offstage(
|
|
offstage: item.state == JobState.inProgress,
|
|
child: Text(
|
|
translate(
|
|
item.display(),
|
|
),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: MyTheme.darkGray,
|
|
),
|
|
),
|
|
),
|
|
Offstage(
|
|
offstage: item.state != JobState.inProgress,
|
|
child: LinearPercentIndicator(
|
|
padding: EdgeInsets.only(right: 15),
|
|
animateFromLastPercent: true,
|
|
center: Text(
|
|
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
|
),
|
|
barRadius: Radius.circular(15),
|
|
percent: item.finishedSize / item.totalSize,
|
|
progressColor: MyTheme.accent,
|
|
backgroundColor: Theme.of(context).hoverColor,
|
|
lineHeight: kDesktopFileTransferRowHeight,
|
|
).paddingSymmetric(vertical: 15),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Offstage(
|
|
offstage: item.state != JobState.paused,
|
|
child: MenuButton(
|
|
onPressed: () {
|
|
jobController.resumeJob(item.id);
|
|
},
|
|
child: SvgPicture.asset(
|
|
"assets/refresh.svg",
|
|
color: Colors.white,
|
|
),
|
|
color: MyTheme.accent,
|
|
hoverColor: MyTheme.accent80,
|
|
),
|
|
),
|
|
MenuButton(
|
|
padding: EdgeInsets.only(right: 15),
|
|
child: SvgPicture.asset(
|
|
"assets/close.svg",
|
|
color: Colors.white,
|
|
),
|
|
onPressed: () {
|
|
jobController.jobTable.removeAt(index);
|
|
jobController.cancelJob(item.id);
|
|
},
|
|
color: MyTheme.accent,
|
|
hoverColor: MyTheme.accent80,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
).paddingSymmetric(vertical: 10),
|
|
),
|
|
);
|
|
},
|
|
itemCount: jobController.jobTable.length,
|
|
);
|
|
|
|
return PreferredSize(
|
|
preferredSize: const Size(200, double.infinity),
|
|
child: Container(
|
|
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Obx(
|
|
() => jobController.jobTable.isEmpty
|
|
? generateCard(
|
|
Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
SvgPicture.asset(
|
|
"assets/transfer.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
height: 40,
|
|
).paddingOnly(bottom: 10),
|
|
Text(
|
|
translate("No transfers in progress"),
|
|
textAlign: TextAlign.center,
|
|
textScaleFactor: 1.20,
|
|
style: TextStyle(
|
|
color:
|
|
Theme.of(context).tabBarTheme.labelColor),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
: statusListView(jobController.jobTable),
|
|
)),
|
|
);
|
|
}
|
|
|
|
void handleDragDone(DropDoneDetails details, bool isLocal) {
|
|
if (isLocal) {
|
|
// ignore local
|
|
return;
|
|
}
|
|
final items = SelectedItems(isLocal: false);
|
|
for (var file in details.files) {
|
|
final f = File(file.path);
|
|
items.add(Entry()
|
|
..path = file.path
|
|
..name = file.name
|
|
..size = FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync());
|
|
}
|
|
final otherSideData = model.localController.directoryData();
|
|
model.remoteController.sendFiles(items, otherSideData);
|
|
}
|
|
}
|
|
|
|
class FileManagerView extends StatefulWidget {
|
|
final FileController controller;
|
|
final FFI _ffi;
|
|
final Rx<MouseFocusScope> _mouseFocusScope;
|
|
|
|
FileManagerView(this.controller, this._ffi, this._mouseFocusScope);
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _FileManagerViewState();
|
|
}
|
|
|
|
class _FileManagerViewState extends State<FileManagerView> {
|
|
final _locationStatus = LocationStatus.bread.obs;
|
|
final _locationNode = FocusNode();
|
|
final _locationBarKey = GlobalKey();
|
|
final _searchText = "".obs;
|
|
final _breadCrumbScroller = ScrollController();
|
|
final _keyboardNode = FocusNode();
|
|
final _listSearchBuffer = TimeoutStringBuffer();
|
|
final _nameColWidth = 0.0.obs;
|
|
final _modifiedColWidth = 0.0.obs;
|
|
final _sizeColWidth = 0.0.obs;
|
|
final _fileListScrollController = ScrollController();
|
|
final _globalHeaderKey = GlobalKey();
|
|
|
|
/// [_lastClickTime], [_lastClickEntry] help to handle double click
|
|
var _lastClickTime =
|
|
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
|
|
Entry? _lastClickEntry;
|
|
|
|
double? _windowWidthPrev;
|
|
double _fileTransferMinimumWidth = 0.0;
|
|
|
|
FileController get controller => widget.controller;
|
|
bool get isLocal => widget.controller.isLocal;
|
|
FFI get _ffi => widget._ffi;
|
|
SelectedItems get selectedItems => controller.selectedItems;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// register location listener
|
|
_locationNode.addListener(onLocationFocusChanged);
|
|
controller.directory.listen((e) => breadCrumbScrollToEnd());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_locationNode.removeListener(onLocationFocusChanged);
|
|
_locationNode.dispose();
|
|
_keyboardNode.dispose();
|
|
_breadCrumbScroller.dispose();
|
|
_fileListScrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
_handleColumnPorportions();
|
|
return Container(
|
|
margin: const EdgeInsets.all(16.0),
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
headTools(),
|
|
Expanded(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: MouseRegion(
|
|
onEnter: (evt) {
|
|
widget._mouseFocusScope.value = isLocal
|
|
? MouseFocusScope.local
|
|
: MouseFocusScope.remote;
|
|
_keyboardNode.requestFocus();
|
|
},
|
|
onExit: (evt) =>
|
|
widget._mouseFocusScope.value = MouseFocusScope.none,
|
|
child: _buildFileList(context, _fileListScrollController),
|
|
))
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _handleColumnPorportions() {
|
|
final windowWidthNow = MediaQuery.of(context).size.width;
|
|
if (_windowWidthPrev == null) {
|
|
_windowWidthPrev = windowWidthNow;
|
|
final defaultColumnWidth = windowWidthNow * 0.115;
|
|
_fileTransferMinimumWidth = defaultColumnWidth / 3;
|
|
_nameColWidth.value = defaultColumnWidth;
|
|
_modifiedColWidth.value = defaultColumnWidth;
|
|
_sizeColWidth.value = defaultColumnWidth;
|
|
}
|
|
|
|
if (_windowWidthPrev != windowWidthNow) {
|
|
final difference = windowWidthNow / _windowWidthPrev!;
|
|
_windowWidthPrev = windowWidthNow;
|
|
_fileTransferMinimumWidth *= difference;
|
|
_nameColWidth.value *= difference;
|
|
_modifiedColWidth.value *= difference;
|
|
_sizeColWidth.value *= difference;
|
|
}
|
|
}
|
|
|
|
void onLocationFocusChanged() {
|
|
debugPrint("focus changed on local");
|
|
if (_locationNode.hasFocus) {
|
|
// ignore
|
|
} else {
|
|
// lost focus, change to bread
|
|
if (_locationStatus.value != LocationStatus.fileSearchBar) {
|
|
_locationStatus.value = LocationStatus.bread;
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget headTools() {
|
|
return Container(
|
|
child: Column(
|
|
children: [
|
|
// symbols
|
|
PreferredSize(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 50,
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
|
color: MyTheme.accent,
|
|
),
|
|
padding: EdgeInsets.all(8.0),
|
|
child: FutureBuilder<String>(
|
|
future: bind.sessionGetPlatform(
|
|
sessionId: _ffi.sessionId,
|
|
isRemote: !isLocal),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasData &&
|
|
snapshot.data!.isNotEmpty) {
|
|
return getPlatformImage('${snapshot.data}');
|
|
} else {
|
|
return CircularProgressIndicator(
|
|
color: Theme.of(context)
|
|
.tabBarTheme
|
|
.labelColor,
|
|
);
|
|
}
|
|
})),
|
|
Text(isLocal
|
|
? translate("Local Computer")
|
|
: translate("Remote Computer"))
|
|
.marginOnly(left: 8.0)
|
|
],
|
|
),
|
|
preferredSize: Size(double.infinity, 70))
|
|
.paddingOnly(bottom: 15),
|
|
// buttons
|
|
Row(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
MenuButton(
|
|
padding: EdgeInsets.only(
|
|
right: 3,
|
|
),
|
|
child: RotatedBox(
|
|
quarterTurns: 2,
|
|
child: SvgPicture.asset(
|
|
"assets/arrow.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
),
|
|
color: Theme.of(context).cardColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
onPressed: () {
|
|
selectedItems.clear();
|
|
controller.goBack();
|
|
},
|
|
),
|
|
MenuButton(
|
|
child: RotatedBox(
|
|
quarterTurns: 3,
|
|
child: SvgPicture.asset(
|
|
"assets/arrow.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
),
|
|
color: Theme.of(context).cardColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
onPressed: () {
|
|
selectedItems.clear();
|
|
controller.goToParentDirectory();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 3.0),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(8.0),
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 2.5),
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
_locationStatus.value =
|
|
_locationStatus.value == LocationStatus.bread
|
|
? LocationStatus.pathLocation
|
|
: LocationStatus.bread;
|
|
Future.delayed(Duration.zero, () {
|
|
if (_locationStatus.value ==
|
|
LocationStatus.pathLocation) {
|
|
_locationNode.requestFocus();
|
|
}
|
|
});
|
|
},
|
|
child: Obx(
|
|
() => Container(
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _locationStatus.value ==
|
|
LocationStatus.bread
|
|
? buildBread()
|
|
: buildPathLocation()),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Obx(() {
|
|
switch (_locationStatus.value) {
|
|
case LocationStatus.bread:
|
|
return MenuButton(
|
|
onPressed: () {
|
|
_locationStatus.value = LocationStatus.fileSearchBar;
|
|
Future.delayed(
|
|
Duration.zero, () => _locationNode.requestFocus());
|
|
},
|
|
child: SvgPicture.asset(
|
|
"assets/search.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
color: Theme.of(context).cardColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
);
|
|
case LocationStatus.pathLocation:
|
|
return MenuButton(
|
|
onPressed: null,
|
|
child: SvgPicture.asset(
|
|
"assets/close.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
color: Theme.of(context).disabledColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
);
|
|
case LocationStatus.fileSearchBar:
|
|
return MenuButton(
|
|
onPressed: () {
|
|
onSearchText("", isLocal);
|
|
_locationStatus.value = LocationStatus.bread;
|
|
},
|
|
child: SvgPicture.asset(
|
|
"assets/close.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
color: Theme.of(context).cardColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
);
|
|
}
|
|
}),
|
|
MenuButton(
|
|
padding: EdgeInsets.only(
|
|
left: 3,
|
|
),
|
|
onPressed: () {
|
|
controller.refresh();
|
|
},
|
|
child: SvgPicture.asset(
|
|
"assets/refresh.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
color: Theme.of(context).cardColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl,
|
|
children: [
|
|
Expanded(
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
|
children: [
|
|
MenuButton(
|
|
padding: EdgeInsets.only(
|
|
right: 3,
|
|
),
|
|
onPressed: () {
|
|
controller.goToHomeDirectory();
|
|
},
|
|
child: SvgPicture.asset(
|
|
"assets/home.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
color: Theme.of(context).cardColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
),
|
|
MenuButton(
|
|
onPressed: () {
|
|
final name = TextEditingController();
|
|
_ffi.dialogManager.show((setState, close, context) {
|
|
submit() {
|
|
if (name.value.text.isNotEmpty) {
|
|
controller.createDir(PathUtil.join(
|
|
controller.directory.value.path,
|
|
name.value.text,
|
|
controller.options.value.isWindows,
|
|
));
|
|
close();
|
|
}
|
|
}
|
|
|
|
cancel() => close(false);
|
|
return CustomAlertDialog(
|
|
title: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
SvgPicture.asset("assets/folder_new.svg",
|
|
color: MyTheme.accent),
|
|
Text(
|
|
translate("Create Folder"),
|
|
).paddingOnly(
|
|
left: 10,
|
|
),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextFormField(
|
|
decoration: InputDecoration(
|
|
labelText: translate(
|
|
"Please enter the folder name",
|
|
),
|
|
),
|
|
controller: name,
|
|
autofocus: true,
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
dialogButton(
|
|
"Cancel",
|
|
icon: Icon(Icons.close_rounded),
|
|
onPressed: cancel,
|
|
isOutline: true,
|
|
),
|
|
dialogButton(
|
|
"Ok",
|
|
icon: Icon(Icons.done_rounded),
|
|
onPressed: submit,
|
|
),
|
|
],
|
|
onSubmit: submit,
|
|
onCancel: cancel,
|
|
);
|
|
});
|
|
},
|
|
child: SvgPicture.asset(
|
|
"assets/folder_new.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
color: Theme.of(context).cardColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
),
|
|
Obx(() => MenuButton(
|
|
onPressed: SelectedItems.valid(selectedItems.items)
|
|
? () async {
|
|
await (controller
|
|
.removeAction(selectedItems));
|
|
selectedItems.clear();
|
|
}
|
|
: null,
|
|
child: SvgPicture.asset(
|
|
"assets/trash.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
color: Theme.of(context).cardColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
)),
|
|
menu(isLocal: isLocal),
|
|
],
|
|
),
|
|
),
|
|
Obx(() => ElevatedButton.icon(
|
|
style: ButtonStyle(
|
|
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
|
isLocal
|
|
? EdgeInsets.only(left: 10)
|
|
: EdgeInsets.only(right: 10)),
|
|
backgroundColor: MaterialStateProperty.all(
|
|
selectedItems.items.isEmpty
|
|
? MyTheme.accent80
|
|
: MyTheme.accent,
|
|
),
|
|
),
|
|
onPressed: SelectedItems.valid(selectedItems.items)
|
|
? () {
|
|
final otherSideData =
|
|
controller.getOtherSideDirectoryData();
|
|
controller.sendFiles(selectedItems, otherSideData);
|
|
selectedItems.clear();
|
|
}
|
|
: null,
|
|
icon: isLocal
|
|
? Text(
|
|
translate('Send'),
|
|
textAlign: TextAlign.right,
|
|
style: TextStyle(
|
|
color: selectedItems.items.isEmpty
|
|
? Theme.of(context).brightness ==
|
|
Brightness.light
|
|
? MyTheme.grayBg
|
|
: MyTheme.darkGray
|
|
: Colors.white,
|
|
),
|
|
)
|
|
: RotatedBox(
|
|
quarterTurns: 2,
|
|
child: SvgPicture.asset(
|
|
"assets/arrow.svg",
|
|
color: selectedItems.items.isEmpty
|
|
? Theme.of(context).brightness ==
|
|
Brightness.light
|
|
? MyTheme.grayBg
|
|
: MyTheme.darkGray
|
|
: Colors.white,
|
|
alignment: Alignment.bottomRight,
|
|
),
|
|
),
|
|
label: isLocal
|
|
? SvgPicture.asset(
|
|
"assets/arrow.svg",
|
|
color: selectedItems.items.isEmpty
|
|
? Theme.of(context).brightness ==
|
|
Brightness.light
|
|
? MyTheme.grayBg
|
|
: MyTheme.darkGray
|
|
: Colors.white,
|
|
)
|
|
: Text(
|
|
translate('Receive'),
|
|
style: TextStyle(
|
|
color: selectedItems.items.isEmpty
|
|
? Theme.of(context).brightness ==
|
|
Brightness.light
|
|
? MyTheme.grayBg
|
|
: MyTheme.darkGray
|
|
: Colors.white,
|
|
),
|
|
),
|
|
)),
|
|
],
|
|
).marginOnly(top: 8.0)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget menu({bool isLocal = false}) {
|
|
var menuPos = RelativeRect.fill;
|
|
|
|
final List<MenuEntryBase<String>> items = [
|
|
MenuEntrySwitch<String>(
|
|
switchType: SwitchType.scheckbox,
|
|
text: translate("Show Hidden Files"),
|
|
getter: () async {
|
|
return controller.options.value.showHidden;
|
|
},
|
|
setter: (bool v) async {
|
|
controller.toggleShowHidden();
|
|
},
|
|
padding: kDesktopMenuPadding,
|
|
dismissOnClicked: true,
|
|
),
|
|
MenuEntryButton(
|
|
childBuilder: (style) => Text(translate("Select All"), style: style),
|
|
proc: () => setState(() =>
|
|
selectedItems.selectAll(controller.directory.value.entries)),
|
|
padding: kDesktopMenuPadding,
|
|
dismissOnClicked: true),
|
|
MenuEntryButton(
|
|
childBuilder: (style) =>
|
|
Text(translate("Unselect All"), style: style),
|
|
proc: () => selectedItems.clear(),
|
|
padding: kDesktopMenuPadding,
|
|
dismissOnClicked: true)
|
|
];
|
|
|
|
return Listener(
|
|
onPointerDown: (e) {
|
|
final x = e.position.dx;
|
|
final y = e.position.dy;
|
|
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
|
},
|
|
child: MenuButton(
|
|
onPressed: () => mod_menu.showMenu(
|
|
context: context,
|
|
position: menuPos,
|
|
items: items
|
|
.map(
|
|
(e) => e.build(
|
|
context,
|
|
MenuConfig(
|
|
commonColor: CustomPopupMenuTheme.commonColor,
|
|
height: CustomPopupMenuTheme.height,
|
|
dividerHeight: CustomPopupMenuTheme.dividerHeight),
|
|
),
|
|
)
|
|
.expand((i) => i)
|
|
.toList(),
|
|
elevation: 8,
|
|
),
|
|
child: SvgPicture.asset(
|
|
"assets/dots.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
color: Theme.of(context).cardColor,
|
|
hoverColor: Theme.of(context).hoverColor,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFileList(
|
|
BuildContext context, ScrollController scrollController) {
|
|
final fd = controller.directory.value;
|
|
final entries = fd.entries;
|
|
|
|
return ListSearchActionListener(
|
|
node: _keyboardNode,
|
|
buffer: _listSearchBuffer,
|
|
onNext: (buffer) {
|
|
debugPrint("searching next for $buffer");
|
|
assert(buffer.length == 1);
|
|
assert(selectedItems.items.length <= 1);
|
|
var skipCount = 0;
|
|
if (selectedItems.items.isNotEmpty) {
|
|
final index = entries.indexOf(selectedItems.items.first);
|
|
if (index < 0) {
|
|
return;
|
|
}
|
|
skipCount = index + 1;
|
|
}
|
|
var searchResult = entries
|
|
.skip(skipCount)
|
|
.where((element) => element.name.toLowerCase().startsWith(buffer));
|
|
if (searchResult.isEmpty) {
|
|
// cannot find next, lets restart search from head
|
|
debugPrint("restart search from head");
|
|
searchResult = entries.where(
|
|
(element) => element.name.toLowerCase().startsWith(buffer));
|
|
}
|
|
if (searchResult.isEmpty) {
|
|
selectedItems.clear();
|
|
return;
|
|
}
|
|
_jumpToEntry(isLocal, searchResult.first, scrollController,
|
|
kDesktopFileTransferRowHeight);
|
|
},
|
|
onSearch: (buffer) {
|
|
debugPrint("searching for $buffer");
|
|
final selectedEntries = selectedItems;
|
|
final searchResult = entries
|
|
.where((element) => element.name.toLowerCase().startsWith(buffer));
|
|
selectedEntries.clear();
|
|
if (searchResult.isEmpty) {
|
|
selectedItems.clear();
|
|
return;
|
|
}
|
|
_jumpToEntry(isLocal, searchResult.first, scrollController,
|
|
kDesktopFileTransferRowHeight);
|
|
},
|
|
child: Obx(() {
|
|
final entries = controller.directory.value.entries;
|
|
final filteredEntries = _searchText.isNotEmpty
|
|
? entries.where((element) {
|
|
return element.name.contains(_searchText.value);
|
|
}).toList(growable: false)
|
|
: entries;
|
|
final rows = filteredEntries.map((entry) {
|
|
final sizeStr =
|
|
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
|
|
final lastModifiedStr = entry.isDrive
|
|
? " "
|
|
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 1),
|
|
child: Obx(() => Container(
|
|
decoration: BoxDecoration(
|
|
color: selectedItems.items.contains(entry)
|
|
? Theme.of(context).hoverColor
|
|
: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(5.0),
|
|
),
|
|
),
|
|
key: ValueKey(entry.name),
|
|
height: kDesktopFileTransferRowHeight,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
Expanded(
|
|
child: InkWell(
|
|
child: Row(
|
|
children: [
|
|
GestureDetector(
|
|
child: Obx(
|
|
() => Container(
|
|
width: _nameColWidth.value,
|
|
child: Tooltip(
|
|
waitDuration: Duration(milliseconds: 500),
|
|
message: entry.name,
|
|
child: Row(children: [
|
|
entry.isDrive
|
|
? Image(
|
|
image: iconHardDrive,
|
|
fit: BoxFit.scaleDown,
|
|
color: Theme.of(context)
|
|
.iconTheme
|
|
.color
|
|
?.withOpacity(0.7))
|
|
.paddingAll(4)
|
|
: SvgPicture.asset(
|
|
entry.isFile
|
|
? "assets/file.svg"
|
|
: "assets/folder.svg",
|
|
color: Theme.of(context)
|
|
.tabBarTheme
|
|
.labelColor,
|
|
),
|
|
Expanded(
|
|
child: Text(entry.name.nonBreaking,
|
|
overflow:
|
|
TextOverflow.ellipsis))
|
|
]),
|
|
)),
|
|
),
|
|
onTap: () {
|
|
final items = selectedItems;
|
|
// handle double click
|
|
if (_checkDoubleClick(entry)) {
|
|
controller.openDirectory(entry.path);
|
|
items.clear();
|
|
return;
|
|
}
|
|
_onSelectedChanged(
|
|
items, filteredEntries, entry, isLocal);
|
|
},
|
|
),
|
|
SizedBox(
|
|
width: 2.0,
|
|
),
|
|
GestureDetector(
|
|
child: Obx(
|
|
() => SizedBox(
|
|
width: _modifiedColWidth.value,
|
|
child: Tooltip(
|
|
waitDuration: Duration(milliseconds: 500),
|
|
message: lastModifiedStr,
|
|
child: Text(
|
|
lastModifiedStr,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: MyTheme.darkGray,
|
|
),
|
|
)),
|
|
),
|
|
),
|
|
),
|
|
// Divider from header.
|
|
SizedBox(
|
|
width: 2.0,
|
|
),
|
|
Expanded(
|
|
// width: 100,
|
|
child: GestureDetector(
|
|
child: Tooltip(
|
|
waitDuration: Duration(milliseconds: 500),
|
|
message: sizeStr,
|
|
child: Text(
|
|
sizeStr,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 10, color: MyTheme.darkGray),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
))),
|
|
);
|
|
}).toList(growable: false);
|
|
|
|
return Column(
|
|
children: [
|
|
// Header
|
|
Row(
|
|
children: [
|
|
Expanded(child: _buildFileBrowserHeader(context)),
|
|
],
|
|
),
|
|
// Body
|
|
Expanded(
|
|
child: ListView.builder(
|
|
controller: scrollController,
|
|
itemExtent: kDesktopFileTransferRowHeight,
|
|
itemBuilder: (context, index) {
|
|
return rows[index];
|
|
},
|
|
itemCount: rows.length,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
onSearchText(String searchText, bool isLocal) {
|
|
selectedItems.clear();
|
|
_searchText.value = searchText;
|
|
}
|
|
|
|
void _jumpToEntry(bool isLocal, Entry entry,
|
|
ScrollController scrollController, double rowHeight) {
|
|
final entries = controller.directory.value.entries;
|
|
final index = entries.indexOf(entry);
|
|
if (index == -1) {
|
|
debugPrint("entry is not valid: ${entry.path}");
|
|
}
|
|
final selectedEntries = selectedItems;
|
|
final searchResult = entries.where((element) => element == entry);
|
|
selectedEntries.clear();
|
|
if (searchResult.isEmpty) {
|
|
return;
|
|
}
|
|
final offset = min(
|
|
max(scrollController.position.minScrollExtent,
|
|
entries.indexOf(searchResult.first) * rowHeight),
|
|
scrollController.position.maxScrollExtent);
|
|
scrollController.jumpTo(offset);
|
|
selectedEntries.add(searchResult.first);
|
|
debugPrint("focused on ${searchResult.first.name}");
|
|
}
|
|
|
|
void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
|
|
Entry entry, bool isLocal) {
|
|
final isCtrlDown = RawKeyboard.instance.keysPressed
|
|
.contains(LogicalKeyboardKey.controlLeft);
|
|
final isShiftDown =
|
|
RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft);
|
|
if (isCtrlDown) {
|
|
if (selectedItems.items.contains(entry)) {
|
|
selectedItems.remove(entry);
|
|
} else {
|
|
selectedItems.add(entry);
|
|
}
|
|
} else if (isShiftDown) {
|
|
final List<int> indexGroup = [];
|
|
for (var selected in selectedItems.items) {
|
|
indexGroup.add(entries.indexOf(selected));
|
|
}
|
|
indexGroup.add(entries.indexOf(entry));
|
|
indexGroup.removeWhere((e) => e == -1);
|
|
final maxIndex = indexGroup.reduce(max);
|
|
final minIndex = indexGroup.reduce(min);
|
|
selectedItems.clear();
|
|
entries
|
|
.getRange(minIndex, maxIndex + 1)
|
|
.forEach((e) => selectedItems.add(e));
|
|
} else {
|
|
selectedItems.clear();
|
|
selectedItems.add(entry);
|
|
}
|
|
setState(() {});
|
|
}
|
|
|
|
bool _checkDoubleClick(Entry entry) {
|
|
final current = DateTime.now().millisecondsSinceEpoch;
|
|
final elapsed = current - _lastClickTime;
|
|
_lastClickTime = current;
|
|
if (_lastClickEntry == entry) {
|
|
if (elapsed < bind.getDoubleClickTime()) {
|
|
return true;
|
|
}
|
|
} else {
|
|
_lastClickEntry = entry;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void _onDrag(double dx, RxDouble column1, RxDouble column2) {
|
|
if (column1.value + dx <= _fileTransferMinimumWidth ||
|
|
column2.value - dx <= _fileTransferMinimumWidth) {
|
|
return;
|
|
}
|
|
column1.value += dx;
|
|
column2.value -= dx;
|
|
column1.value = max(_fileTransferMinimumWidth, column1.value);
|
|
column2.value = max(_fileTransferMinimumWidth, column2.value);
|
|
}
|
|
|
|
Widget _buildFileBrowserHeader(BuildContext context) {
|
|
final padding = EdgeInsets.all(1.0);
|
|
return SizedBox(
|
|
key: _globalHeaderKey,
|
|
height: kDesktopFileTransferHeaderHeight,
|
|
child: Row(
|
|
children: [
|
|
Obx(
|
|
() => headerItemFunc(
|
|
_nameColWidth.value, SortBy.name, translate("Name")),
|
|
),
|
|
DraggableDivider(
|
|
axis: Axis.vertical,
|
|
onPointerMove: (dx) =>
|
|
_onDrag(dx, _nameColWidth, _modifiedColWidth),
|
|
padding: padding,
|
|
),
|
|
Obx(
|
|
() => headerItemFunc(_modifiedColWidth.value, SortBy.modified,
|
|
translate("Modified")),
|
|
),
|
|
DraggableDivider(
|
|
axis: Axis.vertical,
|
|
onPointerMove: (dx) =>
|
|
_onDrag(dx, _modifiedColWidth, _sizeColWidth),
|
|
padding: padding),
|
|
Expanded(
|
|
child: headerItemFunc(
|
|
_sizeColWidth.value, SortBy.size, translate("Size")))
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget headerItemFunc(double? width, SortBy sortBy, String name) {
|
|
final headerTextStyle =
|
|
Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle();
|
|
return ObxValue<Rx<bool?>>(
|
|
(ascending) => InkWell(
|
|
onTap: () {
|
|
if (ascending.value == null) {
|
|
ascending.value = true;
|
|
} else {
|
|
ascending.value = !ascending.value!;
|
|
}
|
|
controller.changeSortStyle(sortBy,
|
|
isLocal: isLocal, ascending: ascending.value!);
|
|
},
|
|
child: SizedBox(
|
|
width: width,
|
|
height: kDesktopFileTransferHeaderHeight,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
name,
|
|
style: headerTextStyle,
|
|
overflow: TextOverflow.ellipsis,
|
|
).marginOnly(left: 4),
|
|
),
|
|
ascending.value != null
|
|
? Icon(
|
|
ascending.value!
|
|
? Icons.keyboard_arrow_up_rounded
|
|
: Icons.keyboard_arrow_down_rounded,
|
|
)
|
|
: SizedBox()
|
|
],
|
|
),
|
|
),
|
|
), () {
|
|
if (controller.sortBy.value == sortBy) {
|
|
return controller.sortAscending.obs;
|
|
} else {
|
|
return Rx<bool?>(null);
|
|
}
|
|
}());
|
|
}
|
|
|
|
Widget buildBread() {
|
|
final items = getPathBreadCrumbItems(isLocal, (list) {
|
|
var path = "";
|
|
for (var item in list) {
|
|
path = PathUtil.join(path, item, controller.options.value.isWindows);
|
|
}
|
|
controller.openDirectory(path);
|
|
});
|
|
|
|
return items.isEmpty
|
|
? Offstage()
|
|
: Row(
|
|
key: _locationBarKey,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Listener(
|
|
// handle mouse wheel
|
|
onPointerSignal: (e) {
|
|
if (e is PointerScrollEvent) {
|
|
final sc = _breadCrumbScroller;
|
|
final scale = Platform.isWindows ? 2 : 4;
|
|
sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
|
|
}
|
|
},
|
|
child: BreadCrumb(
|
|
items: items,
|
|
divider: const Icon(Icons.keyboard_arrow_right_rounded),
|
|
overflow: ScrollableOverflow(
|
|
controller: _breadCrumbScroller,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
ActionIcon(
|
|
message: "",
|
|
icon: Icons.keyboard_arrow_down_rounded,
|
|
onTap: () async {
|
|
final renderBox = _locationBarKey.currentContext
|
|
?.findRenderObject() as RenderBox;
|
|
_locationBarKey.currentContext?.size;
|
|
|
|
final size = renderBox.size;
|
|
final offset = renderBox.localToGlobal(Offset.zero);
|
|
|
|
final x = offset.dx;
|
|
final y = offset.dy + size.height + 1;
|
|
|
|
final isPeerWindows = controller.options.value.isWindows;
|
|
final List<MenuEntryBase> menuItems = [
|
|
MenuEntryButton(
|
|
childBuilder: (TextStyle? style) => isPeerWindows
|
|
? buildWindowsThisPC(context, style)
|
|
: Text(
|
|
'/',
|
|
style: style,
|
|
),
|
|
proc: () {
|
|
controller.openDirectory('/');
|
|
},
|
|
dismissOnClicked: true),
|
|
MenuEntryDivider()
|
|
];
|
|
if (isPeerWindows) {
|
|
var loadingTag = "";
|
|
if (!isLocal) {
|
|
loadingTag = _ffi.dialogManager.showLoading("Waiting");
|
|
}
|
|
try {
|
|
final showHidden = controller.options.value.showHidden;
|
|
final fd = await controller.fileFetcher
|
|
.fetchDirectory("/", isLocal, showHidden);
|
|
for (var entry in fd.entries) {
|
|
menuItems.add(MenuEntryButton(
|
|
childBuilder: (TextStyle? style) =>
|
|
Row(children: [
|
|
Image(
|
|
image: iconHardDrive,
|
|
fit: BoxFit.scaleDown,
|
|
color: Theme.of(context)
|
|
.iconTheme
|
|
.color
|
|
?.withOpacity(0.7)),
|
|
SizedBox(width: 10),
|
|
Text(
|
|
entry.name,
|
|
style: style,
|
|
)
|
|
]),
|
|
proc: () {
|
|
controller.openDirectory('${entry.name}\\');
|
|
},
|
|
dismissOnClicked: true));
|
|
}
|
|
menuItems.add(MenuEntryDivider());
|
|
} catch (e) {
|
|
debugPrint("buildBread fetchDirectory err=$e");
|
|
} finally {
|
|
if (!isLocal) {
|
|
_ffi.dialogManager.dismissByTag(loadingTag);
|
|
}
|
|
}
|
|
}
|
|
mod_menu.showMenu(
|
|
context: context,
|
|
position: RelativeRect.fromLTRB(x, y, x, y),
|
|
elevation: 4,
|
|
items: menuItems
|
|
.map((e) => e.build(
|
|
context,
|
|
MenuConfig(
|
|
commonColor:
|
|
CustomPopupMenuTheme.commonColor,
|
|
height: CustomPopupMenuTheme.height,
|
|
dividerHeight:
|
|
CustomPopupMenuTheme.dividerHeight,
|
|
boxWidth: size.width)))
|
|
.expand((i) => i)
|
|
.toList());
|
|
},
|
|
iconSize: 20,
|
|
)
|
|
]);
|
|
}
|
|
|
|
List<BreadCrumbItem> getPathBreadCrumbItems(
|
|
bool isLocal, void Function(List<String>) onPressed) {
|
|
final path = controller.directory.value.path;
|
|
final breadCrumbList = List<BreadCrumbItem>.empty(growable: true);
|
|
final isWindows = controller.options.value.isWindows;
|
|
if (isWindows && path == '/') {
|
|
breadCrumbList.add(BreadCrumbItem(
|
|
content: TextButton(
|
|
child: buildWindowsThisPC(context),
|
|
style: ButtonStyle(
|
|
minimumSize: MaterialStateProperty.all(Size(0, 0))),
|
|
onPressed: () => onPressed(['/']))
|
|
.marginSymmetric(horizontal: 4)));
|
|
} else {
|
|
final list = PathUtil.split(path, isWindows);
|
|
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),
|
|
),
|
|
).marginSymmetric(horizontal: 4),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return breadCrumbList;
|
|
}
|
|
|
|
breadCrumbScrollToEnd() {
|
|
Future.delayed(Duration(milliseconds: 200), () {
|
|
if (_breadCrumbScroller.hasClients) {
|
|
_breadCrumbScroller.animateTo(
|
|
_breadCrumbScroller.position.maxScrollExtent,
|
|
duration: Duration(milliseconds: 200),
|
|
curve: Curves.fastLinearToSlowEaseIn);
|
|
}
|
|
});
|
|
}
|
|
|
|
Widget buildPathLocation() {
|
|
final text = _locationStatus.value == LocationStatus.pathLocation
|
|
? controller.directory.value.path
|
|
: _searchText.value;
|
|
final textController = TextEditingController(text: text)
|
|
..selection = TextSelection.collapsed(offset: text.length);
|
|
return Row(
|
|
children: [
|
|
SvgPicture.asset(
|
|
_locationStatus.value == LocationStatus.pathLocation
|
|
? "assets/folder.svg"
|
|
: "assets/search.svg",
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
Expanded(
|
|
child: TextField(
|
|
focusNode: _locationNode,
|
|
decoration: InputDecoration(
|
|
border: InputBorder.none,
|
|
isDense: true,
|
|
prefix: Padding(
|
|
padding: EdgeInsets.only(left: 4.0),
|
|
),
|
|
),
|
|
controller: textController,
|
|
onSubmitted: (path) {
|
|
controller.openDirectory(path);
|
|
},
|
|
onChanged: _locationStatus.value == LocationStatus.fileSearchBar
|
|
? (searchText) => onSearchText(searchText, isLocal)
|
|
: null,
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
|
|
// openDirectory(String path, {bool isLocal = false}) {
|
|
// model.openDirectory(path, isLocal: isLocal);
|
|
// }
|
|
}
|
|
|
|
Widget buildWindowsThisPC(BuildContext context, [TextStyle? textStyle]) {
|
|
final color = Theme.of(context).iconTheme.color?.withOpacity(0.7);
|
|
return Row(children: [
|
|
Icon(Icons.computer, size: 20, color: color),
|
|
SizedBox(width: 10),
|
|
Text(translate('This PC'), style: textStyle)
|
|
]);
|
|
}
|