cm file delete/create log

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2023-11-05 15:55:09 +08:00
parent 27112e3480
commit 663d355a48
9 changed files with 345 additions and 46 deletions

View File

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/cm_file_model.dart';
import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:percent_indicator/linear_percent_indicator.dart'; import 'package:percent_indicator/linear_percent_indicator.dart';
@ -482,8 +483,8 @@ class _CmHeaderState extends State<_CmHeader>
client.type_() != ClientType.file), client.type_() != ClientType.file),
child: IconButton( child: IconButton(
onPressed: () => checkClickTime(client.id, () { onPressed: () => checkClickTime(client.id, () {
if (client.type_() != ClientType.file) { if (client.type_() == ClientType.file) {
gFFI.chatModel.toggleCMSidePage(); gFFI.chatModel.toggleCMFilePage();
} else { } else {
gFFI.chatModel gFFI.chatModel
.toggleCMChatPage(MessageKey(client.peerId, client.id)); .toggleCMChatPage(MessageKey(client.peerId, client.id));
@ -975,6 +976,49 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
); );
} }
iconLabel(CmFileLog item) {
switch (item.action) {
case CmFileAction.none:
return Container();
case CmFileAction.localToRemote:
case CmFileAction.remoteToLocal:
return Column(
children: [
Transform.rotate(
angle: item.action == CmFileAction.remoteToLocal ? 0 : pi,
child: SvgPicture.asset(
"assets/arrow.svg",
color: Theme.of(context).tabBarTheme.labelColor,
),
),
Text(item.action == CmFileAction.remoteToLocal
? translate('Send')
: translate('Receive'))
],
);
case CmFileAction.remove:
return Column(
children: [
Icon(
Icons.delete,
color: Theme.of(context).tabBarTheme.labelColor,
),
Text(translate('Delete'))
],
);
case CmFileAction.createDir:
return Column(
children: [
Icon(
Icons.create_new_folder,
color: Theme.of(context).tabBarTheme.labelColor,
),
Text(translate('Create Folder'))
],
);
}
}
Widget statusList() { Widget statusList() {
return PreferredSize( return PreferredSize(
preferredSize: const Size(200, double.infinity), preferredSize: const Size(200, double.infinity),
@ -983,7 +1027,7 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
child: Obx( child: Obx(
() { () {
final jobTable = gFFI.cmFileModel.currentJobTable; final jobTable = gFFI.cmFileModel.currentJobTable;
statusListView(List<JobProgress> jobs) => ListView.builder( statusListView(List<CmFileLog> jobs) => ListView.builder(
controller: ScrollController(), controller: ScrollController(),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final item = jobs[index]; final item = jobs[index];
@ -998,22 +1042,7 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
children: [ children: [
SizedBox( SizedBox(
width: 50, width: 50,
child: Column( child: iconLabel(item),
children: [
Transform.rotate(
angle: item.isRemoteToLocal ? 0 : pi,
child: SvgPicture.asset(
"assets/arrow.svg",
color: Theme.of(context)
.tabBarTheme
.labelColor,
),
),
Text(item.isRemoteToLocal
? translate('Send')
: translate('Receive'))
],
),
).paddingOnly(left: 15), ).paddingOnly(left: 15),
const SizedBox( const SizedBox(
width: 16.0, width: 16.0,
@ -1048,8 +1077,9 @@ class __FileTransferLogPageState extends State<_FileTransferLogPage> {
), ),
), ),
Offstage( Offstage(
offstage: offstage: !(item.isTransfer() &&
item.state == JobState.inProgress, item.state !=
JobState.inProgress),
child: Text( child: Text(
translate( translate(
item.display(), item.display(),

View File

@ -285,6 +285,10 @@ class ChatModel with ChangeNotifier {
await toggleCMSidePage(); await toggleCMSidePage();
} }
toggleCMFilePage() async {
await toggleCMSidePage();
}
var _togglingCMSidePage = false; // protect order for await var _togglingCMSidePage = false; // protect order for await
toggleCMSidePage() async { toggleCMSidePage() async {
if (_togglingCMSidePage) return false; if (_togglingCMSidePage) return false;
@ -296,6 +300,13 @@ class ChatModel with ChangeNotifier {
await windowManager.setSizeAlignment( await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeClosedChat, Alignment.topRight); kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
} else { } else {
final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo;
final client = parent.target?.serverModel.clients.firstWhereOrNull(
(client) => client.id.toString() == currentSelectedTab.key);
if (client != null) {
client.unreadChatMessageCount.value = 0;
}
requestChatInputFocus(); requestChatInputFocus();
await windowManager.show(); await windowManager.show();
await windowManager.setSizeAlignment( await windowManager.setSizeAlignment(

View File

@ -10,8 +10,8 @@ import 'file_model.dart';
class CmFileModel { class CmFileModel {
final WeakReference<FFI> parent; final WeakReference<FFI> parent;
final currentJobTable = RxList<JobProgress>(); final currentJobTable = RxList<CmFileLog>();
final _jobTables = HashMap<int, RxList<JobProgress>>.fromEntries([]); final _jobTables = HashMap<int, RxList<CmFileLog>>.fromEntries([]);
Stopwatch stopwatch = Stopwatch(); Stopwatch stopwatch = Stopwatch();
int _lastElapsed = 0; int _lastElapsed = 0;
@ -19,14 +19,24 @@ class CmFileModel {
void updateCurrentClientId(int id) { void updateCurrentClientId(int id) {
if (_jobTables[id] == null) { if (_jobTables[id] == null) {
_jobTables[id] = RxList<JobProgress>(); _jobTables[id] = RxList<CmFileLog>();
} }
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
currentJobTable.value = _jobTables[id]!; currentJobTable.value = _jobTables[id]!;
}); });
} }
onFileTransferLog(dynamic log) { onFileTransferLog(Map<String, dynamic> evt) {
if (evt['transfer'] != null) {
_onFileTransfer(evt['transfer']);
} else if (evt['remove'] != null) {
_onFileRemove(evt['remove']);
} else if (evt['create_dir'] != null) {
_onDirCreate(evt['create_dir']);
}
}
_onFileTransfer(dynamic log) {
try { try {
dynamic d = jsonDecode(log); dynamic d = jsonDecode(log);
if (!stopwatch.isRunning) stopwatch.start(); if (!stopwatch.isRunning) stopwatch.start();
@ -56,9 +66,9 @@ class CmFileModel {
debugPrint("jobTable should not be null"); debugPrint("jobTable should not be null");
return; return;
} }
JobProgress? job = jobTable.firstWhereOrNull((e) => e.id == data.id); CmFileLog? job = jobTable.firstWhereOrNull((e) => e.id == data.id);
if (job == null) { if (job == null) {
job = JobProgress(); job = CmFileLog();
jobTable.add(job); jobTable.add(job);
final currentSelectedTab = final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo; gFFI.serverModel.tabController.state.value.selectedTabInfo;
@ -68,14 +78,14 @@ class CmFileModel {
} }
} }
job.id = data.id; job.id = data.id;
job.isRemoteToLocal = data.isRemote; job.action =
data.isRemote ? CmFileAction.remoteToLocal : CmFileAction.localToRemote;
job.fileName = data.path; job.fileName = data.path;
job.totalSize = data.totalSize; job.totalSize = data.totalSize;
job.finishedSize = data.finishedSize; job.finishedSize = data.finishedSize;
if (job.finishedSize > data.totalSize) { if (job.finishedSize > data.totalSize) {
job.finishedSize = data.totalSize; job.finishedSize = data.totalSize;
} }
job.isRemoteToLocal = data.isRemote;
if (job.finishedSize > 0) { if (job.finishedSize > 0) {
if (job.finishedSize < job.totalSize) { if (job.finishedSize < job.totalSize) {
@ -99,6 +109,112 @@ class CmFileModel {
} }
jobTable.refresh(); jobTable.refresh();
} }
_onFileRemove(dynamic log) {
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");
return;
}
int removeUnreadCount = 0;
if (data.dir) {
removeUnreadCount = jobTable
.where((e) =>
e.action == CmFileAction.remove &&
e.fileName.startsWith(data.path))
.length;
jobTable.removeWhere((e) =>
e.action == CmFileAction.remove &&
e.fileName.startsWith(data.path));
}
jobTable.add(CmFileLog()
..id = data.id
..fileName = data.path
..action = CmFileAction.remove
..state = JobState.done);
final currentSelectedTab =
gFFI.serverModel.tabController.state.value.selectedTabInfo;
if (!(gFFI.chatModel.isShowCMSidePage &&
currentSelectedTab.key == data.connId.toString())) {
// Wrong number if unreadCount changes during deletion, which rarely happens
RxInt? rx = client?.unreadChatMessageCount;
if (rx != null) {
if (rx.value >= removeUnreadCount) {
rx.value -= removeUnreadCount;
}
rx.value += 1;
}
}
jobTable.refresh();
} catch (e) {
debugPrint('$e');
}
}
_onDirCreate(dynamic log) {
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");
return;
}
jobTable.add(CmFileLog()
..id = data.id
..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;
}
jobTable.refresh();
} catch (e) {
debugPrint('$e');
}
}
}
enum CmFileAction {
none,
remoteToLocal,
localToRemote,
remove,
createDir,
}
class CmFileLog {
JobState state = JobState.none;
var id = 0;
var speed = 0.0;
var finishedSize = 0;
var totalSize = 0;
CmFileAction action = CmFileAction.none;
var fileName = "";
var err = "";
int lastTransferredSize = 0;
String display() {
if (state == JobState.done && err == "skipped") {
return translate("Skipped");
}
return state.display();
}
bool isTransfer() {
return action == CmFileAction.remoteToLocal ||
action == CmFileAction.localToRemote;
}
} }
class TransferJobSerdeData { class TransferJobSerdeData {
@ -140,3 +256,25 @@ class TransferJobSerdeData {
error: d['error'] ?? '', error: d['error'] ?? '',
); );
} }
class FileActionLog {
int id = 0;
int connId = 0;
String path = '';
bool dir = false;
FileActionLog({
required this.connId,
required this.id,
required this.path,
required this.dir,
});
FileActionLog.fromJson(dynamic d)
: this(
connId: d['connId'] ?? 0,
id: d['id'] ?? 0,
path: d['path'] ?? '',
dir: d['dir'] ?? false,
);
}

View File

@ -353,7 +353,7 @@ class FfiModel with ChangeNotifier {
} }
} else if (name == "cm_file_transfer_log") { } else if (name == "cm_file_transfer_log") {
if (isDesktop) { if (isDesktop) {
gFFI.cmFileModel.onFileTransferLog(evt['log']); gFFI.cmFileModel.onFileTransferLog(evt);
} }
} else { } else {
debugPrint('Unknown event name: $name'); debugPrint('Unknown event name: $name');

View File

@ -1031,8 +1031,8 @@ pub mod connection_manager {
self.push_event("update_voice_call_state", vec![("client", &client_json)]); self.push_event("update_voice_call_state", vec![("client", &client_json)]);
} }
fn file_transfer_log(&self, log: String) { fn file_transfer_log(&self, action: &str, log: &str) {
self.push_event("cm_file_transfer_log", vec![("log", &log.to_string())]); self.push_event("cm_file_transfer_log", vec![(action, log)]);
} }
} }

View File

@ -232,7 +232,7 @@ pub enum Data {
Plugin(Plugin), Plugin(Plugin),
#[cfg(windows)] #[cfg(windows)]
SyncWinCpuUsage(Option<f64>), SyncWinCpuUsage(Option<f64>),
FileTransferLog(String), FileTransferLog((String, String)),
#[cfg(windows)] #[cfg(windows)]
ControlledSessionCount(usize), ControlledSessionCount(usize),
} }

View File

@ -42,6 +42,7 @@ use hbb_common::{
}; };
#[cfg(any(target_os = "android", target_os = "ios"))] #[cfg(any(target_os = "android", target_os = "ios"))]
use scrap::android::call_main_service_pointer_input; use scrap::android::call_main_service_pointer_input;
use serde_derive::Serialize;
use serde_json::{json, value::Value}; use serde_json::{json, value::Value};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
@ -223,6 +224,7 @@ pub struct Connection {
start_cm_ipc_para: Option<StartCmIpcPara>, start_cm_ipc_para: Option<StartCmIpcPara>,
auto_disconnect_timer: Option<(Instant, u64)>, auto_disconnect_timer: Option<(Instant, u64)>,
authed_conn_id: Option<self::raii::AuthedConnID>, authed_conn_id: Option<self::raii::AuthedConnID>,
file_remove_log_control: FileRemoveLogControl,
} }
impl ConnInner { impl ConnInner {
@ -365,6 +367,7 @@ impl Connection {
}), }),
auto_disconnect_timer: None, auto_disconnect_timer: None,
authed_conn_id: None, authed_conn_id: None,
file_remove_log_control: FileRemoveLogControl::new(id),
}; };
let addr = hbb_common::try_into_v4(addr); let addr = hbb_common::try_into_v4(addr);
if !conn.on_open(addr).await { if !conn.on_open(addr).await {
@ -556,11 +559,11 @@ impl Connection {
}, },
_ = conn.file_timer.tick() => { _ = conn.file_timer.tick() => {
if !conn.read_jobs.is_empty() { if !conn.read_jobs.is_empty() {
conn.send_to_cm(ipc::Data::FileTransferLog(fs::serialize_transfer_jobs(&conn.read_jobs))); conn.send_to_cm(ipc::Data::FileTransferLog(("transfer".to_string(), fs::serialize_transfer_jobs(&conn.read_jobs))));
match fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await { match fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await {
Ok(log) => { Ok(log) => {
if !log.is_empty() { if !log.is_empty() {
conn.send_to_cm(ipc::Data::FileTransferLog(log)); conn.send_to_cm(ipc::Data::FileTransferLog(("transfer".to_string(), log)));
} }
} }
Err(err) => { Err(err) => {
@ -632,6 +635,7 @@ impl Connection {
break; break;
} }
} }
conn.file_remove_log_control.on_timer().drain(..).map(|x| conn.send_to_cm(x)).count();
} }
_ = test_delay_timer.tick() => { _ = test_delay_timer.tick() => {
if last_recv_time.elapsed() >= SEC30 { if last_recv_time.elapsed() >= SEC30 {
@ -1911,30 +1915,43 @@ impl Connection {
} }
Some(file_action::Union::RemoveDir(d)) => { Some(file_action::Union::RemoveDir(d)) => {
self.send_fs(ipc::FS::RemoveDir { self.send_fs(ipc::FS::RemoveDir {
path: d.path, path: d.path.clone(),
id: d.id, id: d.id,
recursive: d.recursive, recursive: d.recursive,
}); });
self.file_remove_log_control.on_remove_dir(d);
} }
Some(file_action::Union::RemoveFile(f)) => { Some(file_action::Union::RemoveFile(f)) => {
self.send_fs(ipc::FS::RemoveFile { self.send_fs(ipc::FS::RemoveFile {
path: f.path, path: f.path.clone(),
id: f.id, id: f.id,
file_num: f.file_num, file_num: f.file_num,
}); });
self.file_remove_log_control.on_remove_file(f);
} }
Some(file_action::Union::Create(c)) => { Some(file_action::Union::Create(c)) => {
self.send_fs(ipc::FS::CreateDir { self.send_fs(ipc::FS::CreateDir {
path: c.path, path: c.path.clone(),
id: c.id, id: c.id,
}); });
self.send_to_cm(ipc::Data::FileTransferLog((
"create_dir".to_string(),
serde_json::to_string(&FileActionLog {
id: c.id,
conn_id: self.inner.id(),
path: c.path,
dir: true,
})
.unwrap_or_default(),
)));
} }
Some(file_action::Union::Cancel(c)) => { Some(file_action::Union::Cancel(c)) => {
self.send_fs(ipc::FS::CancelWrite { id: c.id }); self.send_fs(ipc::FS::CancelWrite { id: c.id });
if let Some(job) = fs::get_job_immutable(c.id, &self.read_jobs) { if let Some(job) = fs::get_job_immutable(c.id, &self.read_jobs) {
self.send_to_cm(ipc::Data::FileTransferLog( self.send_to_cm(ipc::Data::FileTransferLog((
"transfer".to_string(),
fs::serialize_transfer_job(job, false, true, ""), fs::serialize_transfer_job(job, false, true, ""),
)); )));
} }
fs::remove_job(c.id, &mut self.read_jobs); fs::remove_job(c.id, &mut self.read_jobs);
} }
@ -2873,6 +2890,109 @@ pub enum FileAuditType {
RemoteReceive = 1, RemoteReceive = 1,
} }
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct FileActionLog {
id: i32,
conn_id: i32,
path: String,
dir: bool,
}
struct FileRemoveLogControl {
conn_id: i32,
instant: Instant,
removed_files: Vec<FileRemoveFile>,
removed_dirs: Vec<FileRemoveDir>,
}
impl FileRemoveLogControl {
fn new(conn_id: i32) -> Self {
FileRemoveLogControl {
conn_id,
instant: Instant::now(),
removed_files: vec![],
removed_dirs: vec![],
}
}
fn on_remove_file(&mut self, f: FileRemoveFile) -> Option<ipc::Data> {
self.instant = Instant::now();
self.removed_files.push(f.clone());
Some(ipc::Data::FileTransferLog((
"remove".to_string(),
serde_json::to_string(&FileActionLog {
id: f.id,
conn_id: self.conn_id,
path: f.path,
dir: false,
})
.unwrap_or_default(),
)))
}
fn on_remove_dir(&mut self, d: FileRemoveDir) -> Option<ipc::Data> {
self.instant = Instant::now();
self.removed_files.retain(|f| !f.path.starts_with(&d.path));
self.removed_dirs.retain(|x| !x.path.starts_with(&d.path));
if !self
.removed_dirs
.iter()
.any(|x| d.path.starts_with(&x.path))
{
self.removed_dirs.push(d.clone());
}
Some(ipc::Data::FileTransferLog((
"remove".to_string(),
serde_json::to_string(&FileActionLog {
id: d.id,
conn_id: self.conn_id,
path: d.path,
dir: true,
})
.unwrap_or_default(),
)))
}
fn on_timer(&mut self) -> Vec<ipc::Data> {
if self.instant.elapsed().as_secs() < 1 {
return vec![];
}
let mut v: Vec<ipc::Data> = vec![];
self.removed_files
.drain(..)
.map(|f| {
v.push(ipc::Data::FileTransferLog((
"remove".to_string(),
serde_json::to_string(&FileActionLog {
id: f.id,
conn_id: self.conn_id,
path: f.path,
dir: false,
})
.unwrap_or_default(),
)));
})
.count();
self.removed_dirs
.drain(..)
.map(|d| {
v.push(ipc::Data::FileTransferLog((
"remove".to_string(),
serde_json::to_string(&FileActionLog {
id: d.id,
conn_id: self.conn_id,
path: d.path,
dir: true,
})
.unwrap_or_default(),
)));
})
.count();
v
}
}
#[cfg(windows)] #[cfg(windows)]
pub struct PortableState { pub struct PortableState {
pub last_uac: bool, pub last_uac: bool,

View File

@ -63,7 +63,7 @@ impl InvokeUiCM for SciterHandler {
); );
} }
fn file_transfer_log(&self, _log: String) {} fn file_transfer_log(&self, _action: &str, _log: &str) {}
} }
impl SciterHandler { impl SciterHandler {

View File

@ -101,7 +101,7 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized {
fn update_voice_call_state(&self, client: &Client); fn update_voice_call_state(&self, client: &Client);
fn file_transfer_log(&self, log: String); fn file_transfer_log(&self, action: &str, log: &str);
} }
impl<T: InvokeUiCM> Deref for ConnectionManager<T> { impl<T: InvokeUiCM> Deref for ConnectionManager<T> {
@ -418,10 +418,10 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await; handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await;
} }
let log = fs::serialize_transfer_jobs(&write_jobs); let log = fs::serialize_transfer_jobs(&write_jobs);
self.cm.ui_handler.file_transfer_log(log); self.cm.ui_handler.file_transfer_log("transfer", &log);
} }
Data::FileTransferLog(log) => { Data::FileTransferLog((action, log)) => {
self.cm.ui_handler.file_transfer_log(log); self.cm.ui_handler.file_transfer_log(&action, &log);
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
Data::ClipboardFile(_clip) => { Data::ClipboardFile(_clip) => {
@ -526,7 +526,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
} }
}, },
Some(job_log) = rx_log.recv() => { Some(job_log) = rx_log.recv() => {
self.cm.ui_handler.file_transfer_log(job_log); self.cm.ui_handler.file_transfer_log("transfer", &job_log);
} }
} }
} }