mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
@@ -262,6 +262,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Offstage(
|
||||
offstage: item.state != JobState.paused,
|
||||
child: MenuButton(
|
||||
tooltip: translate("Resume"),
|
||||
onPressed: () {
|
||||
jobController.resumeJob(item.id);
|
||||
},
|
||||
@@ -274,6 +275,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
),
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate("Delete"),
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
child: SvgPicture.asset(
|
||||
"assets/close.svg",
|
||||
@@ -521,6 +523,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
Row(
|
||||
children: [
|
||||
MenuButton(
|
||||
tooltip: translate('Back'),
|
||||
padding: EdgeInsets.only(
|
||||
right: 3,
|
||||
),
|
||||
@@ -540,6 +543,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
},
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate('Parent directory'),
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: SvgPicture.asset(
|
||||
@@ -604,6 +608,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
switch (_locationStatus.value) {
|
||||
case LocationStatus.bread:
|
||||
return MenuButton(
|
||||
tooltip: translate('Search'),
|
||||
onPressed: () {
|
||||
_locationStatus.value = LocationStatus.fileSearchBar;
|
||||
Future.delayed(
|
||||
@@ -630,6 +635,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
);
|
||||
case LocationStatus.fileSearchBar:
|
||||
return MenuButton(
|
||||
tooltip: translate('Clear'),
|
||||
onPressed: () {
|
||||
onSearchText("", isLocal);
|
||||
_locationStatus.value = LocationStatus.bread;
|
||||
@@ -645,6 +651,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
}
|
||||
}),
|
||||
MenuButton(
|
||||
tooltip: translate('Refresh File'),
|
||||
padding: EdgeInsets.only(
|
||||
left: 3,
|
||||
),
|
||||
@@ -670,6 +677,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||
children: [
|
||||
MenuButton(
|
||||
tooltip: translate('Home'),
|
||||
padding: EdgeInsets.only(
|
||||
right: 3,
|
||||
),
|
||||
@@ -685,11 +693,27 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
),
|
||||
MenuButton(
|
||||
tooltip: translate('Create Folder'),
|
||||
onPressed: () {
|
||||
final name = TextEditingController();
|
||||
String? errorText;
|
||||
_ffi.dialogManager.show((setState, close, context) {
|
||||
name.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
submit() {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
if (!PathUtil.validName(name.value.text,
|
||||
controller.options.value.isWindows)) {
|
||||
setState(() {
|
||||
errorText = translate("Invalid folder name");
|
||||
});
|
||||
return;
|
||||
}
|
||||
controller.createDir(PathUtil.join(
|
||||
controller.directory.value.path,
|
||||
name.value.text,
|
||||
@@ -721,6 +745,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
labelText: translate(
|
||||
"Please enter the folder name",
|
||||
),
|
||||
errorText: errorText,
|
||||
),
|
||||
controller: name,
|
||||
autofocus: true,
|
||||
@@ -754,6 +779,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
hoverColor: Theme.of(context).hoverColor,
|
||||
),
|
||||
Obx(() => MenuButton(
|
||||
tooltip: translate('Delete'),
|
||||
onPressed: SelectedItems.valid(selectedItems.items)
|
||||
? () async {
|
||||
await (controller
|
||||
@@ -885,6 +911,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
child: MenuButton(
|
||||
tooltip: translate('More'),
|
||||
onPressed: () => mod_menu.showMenu(
|
||||
context: context,
|
||||
position: menuPos,
|
||||
@@ -974,6 +1001,7 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
final lastModifiedStr = entry.isDrive
|
||||
? " "
|
||||
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
|
||||
var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 1),
|
||||
child: Obx(() => Container(
|
||||
@@ -1038,6 +1066,35 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
_onSelectedChanged(
|
||||
items, filteredEntries, entry, isLocal);
|
||||
},
|
||||
onSecondaryTap: () {
|
||||
final items = [
|
||||
if (!entry.isDrive &&
|
||||
versionCmp(_ffi.ffiModel.pi.version,
|
||||
"1.3.0") >=
|
||||
0)
|
||||
mod_menu.PopupMenuItem(
|
||||
child: Text("Rename"),
|
||||
height: CustomPopupMenuTheme.height,
|
||||
onTap: () {
|
||||
controller.renameAction(entry, isLocal);
|
||||
},
|
||||
)
|
||||
];
|
||||
if (items.isNotEmpty) {
|
||||
mod_menu.showMenu(
|
||||
context: context,
|
||||
position: secondaryPosition,
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
},
|
||||
onSecondaryTapDown: (details) {
|
||||
secondaryPosition = RelativeRect.fromLTRB(
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy);
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 2.0,
|
||||
|
||||
@@ -1157,6 +1157,16 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
Text(translate('Create Folder'))
|
||||
],
|
||||
);
|
||||
case CmFileAction.rename:
|
||||
return Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.drive_file_move_outlined,
|
||||
color: Theme.of(context).tabBarTheme.labelColor,
|
||||
),
|
||||
Text(translate('Rename'))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class _MenuButtonState extends State<MenuButton> {
|
||||
return Padding(
|
||||
padding: widget.padding,
|
||||
child: Tooltip(
|
||||
waitDuration: Duration(milliseconds: 300),
|
||||
message: widget.tooltip,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
|
||||
@@ -204,36 +204,54 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
setState(() {});
|
||||
} else if (v == "folder") {
|
||||
final name = TextEditingController();
|
||||
gFFI.dialogManager
|
||||
.show((setState, close, context) => CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate(
|
||||
"Please enter the folder name"),
|
||||
),
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
String? errorText;
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
name.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
translate("Please enter the folder name"),
|
||||
errorText: errorText,
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel",
|
||||
onPressed: () => close(false),
|
||||
isOutline: true),
|
||||
dialogButton("OK", onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
currentFileController.createDir(
|
||||
PathUtil.join(
|
||||
currentDir.path,
|
||||
name.value.text,
|
||||
currentOptions.isWindows));
|
||||
close();
|
||||
}
|
||||
})
|
||||
]));
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton("Cancel",
|
||||
onPressed: () => close(false), isOutline: true),
|
||||
dialogButton("OK", onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
if (!PathUtil.validName(
|
||||
name.value.text,
|
||||
currentFileController
|
||||
.options.value.isWindows)) {
|
||||
setState(() {
|
||||
errorText =
|
||||
translate("Invalid folder name");
|
||||
});
|
||||
return;
|
||||
}
|
||||
currentFileController.createDir(PathUtil.join(
|
||||
currentDir.path,
|
||||
name.value.text,
|
||||
currentOptions.isWindows));
|
||||
close();
|
||||
}
|
||||
})
|
||||
]);
|
||||
});
|
||||
} else if (v == "hidden") {
|
||||
currentFileController.toggleShowHidden();
|
||||
}
|
||||
@@ -497,7 +515,15 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
child: Text(translate("Properties")),
|
||||
value: "properties",
|
||||
enabled: false,
|
||||
)
|
||||
),
|
||||
if (!entries[index].isDrive &&
|
||||
versionCmp(gFFI.ffiModel.pi.version,
|
||||
"1.3.0") >=
|
||||
0)
|
||||
PopupMenuItem(
|
||||
child: Text(translate("Rename")),
|
||||
value: "rename",
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (v) {
|
||||
@@ -509,6 +535,9 @@ class _FileManagerViewState extends State<FileManagerView> {
|
||||
_selectedItems.clear();
|
||||
widget.selectMode.toggle(isLocal);
|
||||
setState(() {});
|
||||
} else if (v == "rename") {
|
||||
controller.renameAction(
|
||||
entries[index], isLocal);
|
||||
}
|
||||
}),
|
||||
onTap: () {
|
||||
|
||||
@@ -291,7 +291,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate('Enable trusted devices')),
|
||||
Text(translate('enable-trusted-devices-tip'),
|
||||
Text('* ${translate('enable-trusted-devices-tip')}',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -33,6 +33,8 @@ class CmFileModel {
|
||||
_onFileRemove(evt['remove']);
|
||||
} else if (evt['create_dir'] != null) {
|
||||
_onDirCreate(evt['create_dir']);
|
||||
} else if (evt['rename'] != null) {
|
||||
_onRename(evt['rename']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +61,6 @@ class CmFileModel {
|
||||
|
||||
_dealOneJob(dynamic l, bool calcSpeed) {
|
||||
final data = TransferJobSerdeData.fromJson(l);
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
@@ -70,12 +70,7 @@ class CmFileModel {
|
||||
if (job == null) {
|
||||
job = CmFileLog();
|
||||
jobTable.add(job);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == data.connId.toString())) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
_addUnread(data.connId);
|
||||
}
|
||||
job.id = data.id;
|
||||
job.action =
|
||||
@@ -167,8 +162,6 @@ class CmFileModel {
|
||||
try {
|
||||
dynamic d = jsonDecode(log);
|
||||
FileActionLog data = FileActionLog.fromJson(d);
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
@@ -179,17 +172,45 @@ class CmFileModel {
|
||||
..fileName = data.path
|
||||
..action = CmFileAction.createDir
|
||||
..state = JobState.done);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == data.connId.toString())) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
_addUnread(data.connId);
|
||||
jobTable.refresh();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
_onRename(dynamic log) {
|
||||
try {
|
||||
dynamic d = jsonDecode(log);
|
||||
FileRenamenLog data = FileRenamenLog.fromJson(d);
|
||||
var jobTable = _jobTables[data.connId];
|
||||
if (jobTable == null) {
|
||||
debugPrint("jobTable should not be null");
|
||||
return;
|
||||
}
|
||||
final fileName = '${data.path} -> ${data.newName}';
|
||||
jobTable.add(CmFileLog()
|
||||
..id = 0
|
||||
..fileName = fileName
|
||||
..action = CmFileAction.rename
|
||||
..state = JobState.done);
|
||||
_addUnread(data.connId);
|
||||
jobTable.refresh();
|
||||
} catch (e) {
|
||||
debugPrint('$e');
|
||||
}
|
||||
}
|
||||
|
||||
_addUnread(int connId) {
|
||||
Client? client =
|
||||
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == connId);
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (!(gFFI.chatModel.isShowCMSidePage &&
|
||||
currentSelectedTab.key == connId.toString())) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CmFileAction {
|
||||
@@ -198,6 +219,7 @@ enum CmFileAction {
|
||||
localToRemote,
|
||||
remove,
|
||||
createDir,
|
||||
rename,
|
||||
}
|
||||
|
||||
class CmFileLog {
|
||||
@@ -285,3 +307,22 @@ class FileActionLog {
|
||||
dir: d['dir'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
class FileRenamenLog {
|
||||
int connId = 0;
|
||||
String path = '';
|
||||
String newName = '';
|
||||
|
||||
FileRenamenLog({
|
||||
required this.connId,
|
||||
required this.path,
|
||||
required this.newName,
|
||||
});
|
||||
|
||||
FileRenamenLog.fromJson(dynamic d)
|
||||
: this(
|
||||
connId: d['connId'] ?? 0,
|
||||
path: d['path'] ?? '',
|
||||
newName: d['newName'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/utils/event_loop.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
@@ -642,6 +643,77 @@ class FileController {
|
||||
path: path,
|
||||
isRemote: !isLocal);
|
||||
}
|
||||
|
||||
Future<void> renameAction(Entry item, bool isLocal) async {
|
||||
final textEditingController = TextEditingController(text: item.name);
|
||||
String? errorText;
|
||||
dialogManager?.show((setState, close, context) {
|
||||
textEditingController.addListener(() {
|
||||
if (errorText != null) {
|
||||
setState(() {
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
submit() async {
|
||||
final newName = textEditingController.text;
|
||||
if (newName.isEmpty || newName == item.name) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (directory.value.entries.any((e) => e.name == newName)) {
|
||||
setState(() {
|
||||
errorText = translate("Already exists");
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!PathUtil.validName(newName, options.value.isWindows)) {
|
||||
setState(() {
|
||||
if (item.isDirectory) {
|
||||
errorText = translate("Invalid folder name");
|
||||
} else {
|
||||
errorText = translate("Invalid file name");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
await bind.sessionRenameFile(
|
||||
sessionId: sessionId,
|
||||
actId: JobController.jobID.next(),
|
||||
path: item.path,
|
||||
newName: newName,
|
||||
isRemote: !isLocal);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Column(
|
||||
children: [
|
||||
DialogTextField(
|
||||
title: '${translate('Rename')} ${item.name}',
|
||||
controller: textEditingController,
|
||||
errorText: errorText,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton(
|
||||
"Cancel",
|
||||
icon: Icon(Icons.close_rounded),
|
||||
onPressed: close,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
"OK",
|
||||
icon: Icon(Icons.done_rounded),
|
||||
onPressed: submit,
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class JobController {
|
||||
@@ -1083,6 +1155,13 @@ class PathUtil {
|
||||
final pathUtil = isWindows ? windowsContext : posixContext;
|
||||
return pathUtil.dirname(path);
|
||||
}
|
||||
|
||||
static bool validName(String name, bool isWindows) {
|
||||
final unixFileNamePattern = RegExp(r'^[^/\0]+$');
|
||||
final windowsFileNamePattern = RegExp(r'^[^<>:"/\\|?*]+$');
|
||||
final reg = isWindows ? windowsFileNamePattern : unixFileNamePattern;
|
||||
return reg.hasMatch(name);
|
||||
}
|
||||
}
|
||||
|
||||
class DirectoryOptions {
|
||||
|
||||
Reference in New Issue
Block a user