From ed18e3c7860edca81d07794773bd86ea51e14e18 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 16 Aug 2024 12:55:58 +0800 Subject: [PATCH] file rename (#9089) Signed-off-by: 21pages --- .../lib/desktop/pages/file_manager_page.dart | 57 ++++++++++++ flutter/lib/desktop/pages/server_page.dart | 10 +++ flutter/lib/desktop/widgets/menu_button.dart | 1 + .../lib/mobile/pages/file_manager_page.dart | 89 ++++++++++++------- flutter/lib/mobile/pages/settings_page.dart | 2 +- flutter/lib/models/cm_file_model.dart | 73 +++++++++++---- flutter/lib/models/file_model.dart | 79 ++++++++++++++++ libs/hbb_common/protos/message.proto | 7 ++ libs/hbb_common/src/fs.rs | 15 ++++ src/client.rs | 1 + src/client/file_trait.rs | 20 ++++- src/client/io_loop.rs | 19 ++++ src/flutter_ffi.rs | 12 +++ src/ipc.rs | 5 ++ src/lang/ar.rs | 3 + src/lang/be.rs | 3 + src/lang/bg.rs | 3 + src/lang/ca.rs | 3 + src/lang/cn.rs | 3 + src/lang/cs.rs | 3 + src/lang/da.rs | 3 + src/lang/de.rs | 3 + src/lang/el.rs | 3 + src/lang/eo.rs | 3 + src/lang/es.rs | 3 + src/lang/et.rs | 3 + src/lang/eu.rs | 3 + src/lang/fa.rs | 3 + src/lang/fr.rs | 3 + src/lang/he.rs | 3 + src/lang/hr.rs | 3 + src/lang/hu.rs | 3 + src/lang/id.rs | 3 + src/lang/it.rs | 3 + src/lang/ja.rs | 3 + src/lang/ko.rs | 3 + src/lang/kz.rs | 3 + src/lang/lt.rs | 3 + src/lang/lv.rs | 3 + src/lang/nb.rs | 3 + src/lang/nl.rs | 3 + src/lang/pl.rs | 3 + src/lang/pt_PT.rs | 3 + src/lang/ptbr.rs | 3 + src/lang/ro.rs | 3 + src/lang/ru.rs | 3 + src/lang/sk.rs | 3 + src/lang/sl.rs | 3 + src/lang/sq.rs | 3 + src/lang/sr.rs | 3 + src/lang/sv.rs | 3 + src/lang/template.rs | 3 + src/lang/th.rs | 3 + src/lang/tr.rs | 3 + src/lang/tw.rs | 3 + src/lang/uk.rs | 3 + src/lang/vn.rs | 3 + src/server/connection.rs | 24 +++++ src/ui_cm_interface.rs | 14 +++ 59 files changed, 507 insertions(+), 50 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 3b4428f99..c9e565fd7 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -262,6 +262,7 @@ class _FileManagerPageState extends State Offstage( offstage: item.state != JobState.paused, child: MenuButton( + tooltip: translate("Resume"), onPressed: () { jobController.resumeJob(item.id); }, @@ -274,6 +275,7 @@ class _FileManagerPageState extends State ), ), MenuButton( + tooltip: translate("Delete"), padding: EdgeInsets.only(right: 15), child: SvgPicture.asset( "assets/close.svg", @@ -521,6 +523,7 @@ class _FileManagerViewState extends State { Row( children: [ MenuButton( + tooltip: translate('Back'), padding: EdgeInsets.only( right: 3, ), @@ -540,6 +543,7 @@ class _FileManagerViewState extends State { }, ), MenuButton( + tooltip: translate('Parent directory'), child: RotatedBox( quarterTurns: 3, child: SvgPicture.asset( @@ -604,6 +608,7 @@ class _FileManagerViewState extends State { 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 { ); case LocationStatus.fileSearchBar: return MenuButton( + tooltip: translate('Clear'), onPressed: () { onSearchText("", isLocal); _locationStatus.value = LocationStatus.bread; @@ -645,6 +651,7 @@ class _FileManagerViewState extends State { } }), MenuButton( + tooltip: translate('Refresh File'), padding: EdgeInsets.only( left: 3, ), @@ -670,6 +677,7 @@ class _FileManagerViewState extends State { isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ MenuButton( + tooltip: translate('Home'), padding: EdgeInsets.only( right: 3, ), @@ -685,11 +693,27 @@ class _FileManagerViewState extends State { 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 { labelText: translate( "Please enter the folder name", ), + errorText: errorText, ), controller: name, autofocus: true, @@ -754,6 +779,7 @@ class _FileManagerViewState extends State { 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 { 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 { 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 { _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, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index d2588a286..53ba73c97 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -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')) + ], + ); } } diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 17b160fed..8fc90de11 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -34,6 +34,7 @@ class _MenuButtonState extends State { return Padding( padding: widget.padding, child: Tooltip( + waitDuration: Duration(milliseconds: 300), message: widget.tooltip, child: Material( type: MaterialType.transparency, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index b74d44484..e017b5b6f 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -204,36 +204,54 @@ class _FileManagerPageState extends State { 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 { 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 { _selectedItems.clear(); widget.selectMode.toggle(isLocal); setState(() {}); + } else if (v == "rename") { + controller.renameAction( + entries[index], isLocal); } }), onTap: () { diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index ca1ca1afa..8fac2ea2a 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -291,7 +291,7 @@ class _SettingsState extends State 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), ], ), diff --git a/flutter/lib/models/cm_file_model.dart b/flutter/lib/models/cm_file_model.dart index ce9b711a2..6609f1191 100644 --- a/flutter/lib/models/cm_file_model.dart +++ b/flutter/lib/models/cm_file_model.dart @@ -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'] ?? '', + ); +} diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index c32ec5405..0838c8b06 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -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 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 { diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 497bdee9b..4554617a7 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -371,6 +371,12 @@ message ReadAllFiles { bool include_hidden = 3; } +message FileRename { + int32 id = 1; + string path = 2; + string new_name = 3; +} + message FileAction { oneof union { ReadDir read_dir = 1; @@ -382,6 +388,7 @@ message FileAction { ReadAllFiles all_files = 7; FileTransferCancel cancel = 8; FileTransferSendConfirmRequest send_confirm = 9; + FileRename rename = 10; } } diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 235cb4837..3f236fd3a 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -838,6 +838,21 @@ pub fn create_dir(dir: &str) -> ResultType<()> { Ok(()) } +#[inline] +pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> { + let path = std::path::Path::new(&path); + if path.exists() { + let dir = path + .parent() + .ok_or(anyhow!("Parent directoy of {path:?} not exists"))?; + let new_path = dir.join(&new_name); + std::fs::rename(&path, &new_path)?; + Ok(()) + } else { + bail!("{path:?} not exists"); + } +} + #[inline] pub fn transform_windows_path(entries: &mut Vec) { for entry in entries { diff --git a/src/client.rs b/src/client.rs index 2c5d0a339..665560d62 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3180,6 +3180,7 @@ pub enum Data { NewVoiceCall, CloseVoiceCall, ResetDecoder(Option), + RenameFile((i32, String, String, bool)), } /// Keycode for key events. diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 49e3f2358..71ddfb09c 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -1,4 +1,4 @@ -use hbb_common::{fs, message_proto::*, log}; +use hbb_common::{fs, log, message_proto::*}; use super::{Data, Interface}; @@ -7,7 +7,12 @@ pub trait FileManager: Interface { fs::get_home_as_string() } - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] + #[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + )))] fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { match fs::read_dir(&fs::get_path(&path), include_hidden) { Err(_) => sciter::Value::null(), @@ -20,7 +25,12 @@ pub trait FileManager: Interface { } } - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter"))] + #[cfg(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + ))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { @@ -136,4 +146,8 @@ pub trait FileManager: Interface { is_upload, ))); } + + fn rename_file(&self, act_id: i32, path: String, new_name: String, is_remote: bool) { + self.send(Data::RenameFile((act_id, path, new_name, is_remote))); + } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 15c6c24b5..b222e4118 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -817,6 +817,25 @@ impl Remote { } } } + Data::RenameFile((id, path, new_name, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_rename(FileRename { + id, + path, + new_name, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + let err = fs::rename_file(&path, &new_name) + .err() + .map(|e| e.to_string()); + self.handle_job_status(id, -1, err); + } + } Data::RecordScreen(start, display, w, h, id) => { let _ = self .video_sender diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b64fd6044..9b9914cfd 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -710,6 +710,18 @@ pub fn session_resume_job(session_id: SessionID, act_id: i32, is_remote: bool) { } } +pub fn session_rename_file( + session_id: SessionID, + act_id: i32, + path: String, + new_name: String, + is_remote: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.rename_file(act_id, path, new_name, is_remote); + } +} + pub fn session_elevate_direct(session_id: SessionID) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.elevate_direct(); diff --git a/src/ipc.rs b/src/ipc.rs index 7903a942b..3f093c758 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -102,6 +102,11 @@ pub enum FS { last_modified: u64, is_upload: bool, }, + Rename { + id: i32, + path: String, + new_name: String, + }, } #[cfg(target_os = "windows")] diff --git a/src/lang/ar.rs b/src/lang/ar.rs index d1f6d35e3..68c041481 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 6bf449df2..136ccf9fe 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index dfdfaa85d..9a5b320a8 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 4a98333fd..0aa64ec28 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 0768bd25e..62cb5452c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "平台"), ("Days remaining", "剩余天数"), ("enable-trusted-devices-tip", "允许受信任的设备跳过 2FA 验证"), + ("Parent directory", "父目录"), + ("Resume", "继续"), + ("Invalid file name", "无效文件名"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 7ec337b1a..41d94d978 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Platforma"), ("Days remaining", "Zbývajících dnů"), ("enable-trusted-devices-tip", "Přeskočte 2FA ověření na důvěryhodných zařízeních"), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 11d6516b5..a9e286600 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 9732fb70f..677f58552 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Plattform"), ("Days remaining", "Verbleibende Tage"), ("enable-trusted-devices-tip", "2FA-Verifizierung auf vertrauenswürdigen Geräten überspringen"), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index e5331c104..b3ce7dcaf 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index ffa4d3b7d..144bf7bc3 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ca73b8f1b..afac3f558 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Plataforma"), ("Days remaining", "Días restantes"), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 70c834a6c..cc3f3afc3 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 3bb756859..412b4e740 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 898596a15..b6949aa50 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c0eefab36..31d786408 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 45fefe666..07d9aa977 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 599b87c7b..1dca1c7e0 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index ef8d0d193..be089347a 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f814a5c21..42c36b5c7 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index db224085b..281704f5e 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "Piattaforma"), ("Days remaining", "Giorni rimanenti"), ("enable-trusted-devices-tip", "Salta verifica 2FA nei dispositivi attendibili"), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 27736008c..ffb93e379 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 192845b98..3e84ca262 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", "플랫폼"), ("Days remaining", "일 남음"), ("enable-trusted-devices-tip", "신뢰할 수 있는 기기에서 2FA 검증 건너뛰기"), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 7fe5b8b38..c47764c81 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 922bb1881..b1d0317f8 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 8b00c360e..df2324def 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 0ce11a845..d23191ef2 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 2873fb474..a6b681a14 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5b8061c59..246de02f4 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 1ec66daee..6afa4c528 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8983413f5..83ee1e0d2 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b913fdac3..0f11e5449 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 965bc0ce6..dc4b2133f 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 5c3247374..d1f5467af 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 0973fdac8..7c8d97494 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index bf89fd05f..0a73e0217 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 987b88e4f..4d3654ea9 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index d079cdfbd..9d7956545 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index e11bb0663..76e491c91 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 0ab796348..acd14c8f9 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 98f07122f..f926e9454 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index d935b2dae..99c808320 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 6465299f7..6a177059c 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index b6c2c66c1..88f70a8e2 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -641,5 +641,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Platform", ""), ("Days remaining", ""), ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 929d040e2..3d05a938b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2274,6 +2274,22 @@ impl Connection { job.confirm(&r); } } + Some(file_action::Union::Rename(r)) => { + self.send_fs(ipc::FS::Rename { + id: r.id, + path: r.path.clone(), + new_name: r.new_name.clone(), + }); + self.send_to_cm(ipc::Data::FileTransferLog(( + "rename".to_string(), + serde_json::to_string(&FileRenameLog { + conn_id: self.inner.id(), + path: r.path, + new_name: r.new_name, + }) + .unwrap_or_default(), + ))); + } _ => {} } } @@ -3451,6 +3467,14 @@ struct FileActionLog { dir: bool, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct FileRenameLog { + conn_id: i32, + path: String, + new_name: String, +} + struct FileRemoveLogControl { conn_id: i32, instant: Instant, diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 451de6b23..89e9ceabb 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -881,6 +881,9 @@ async fn handle_fs( } } } + ipc::FS::Rename { id, path, new_name } => { + rename_file(path, new_name, id, tx).await; + } _ => {} } } @@ -945,6 +948,17 @@ async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { .await; } +#[cfg(not(any(target_os = "ios")))] +async fn rename_file(path: String, new_name: String, id: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::rename_file(&path, &new_name)).await, + id, + 0, + tx, + ) + .await; +} + #[cfg(not(any(target_os = "ios")))] async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { let path = fs::get_path(&path);