file rename (#9089)

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2024-08-16 12:55:58 +08:00
committed by GitHub
parent 579e0fac36
commit ed18e3c786
59 changed files with 507 additions and 50 deletions

View File

@@ -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,

View File

@@ -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'))
],
);
}
}

View File

@@ -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,

View File

@@ -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: () {

View File

@@ -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),
],
),

View File

@@ -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'] ?? '',
);
}

View File

@@ -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 {