mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-11-27 14:59:02 +08:00
add file log page to cm
* Only send and receive logs are shown * For older version, client send to server doesn't have size information, because server side doesn't know the total_size * Not switch tabs automatically when new files are transferred * If cm side page is open, not pop up automatically when new files are transferred * Show unread message count * The cm tab remains open when closed if a file transfer has previously occurred Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
parent
e88e17a4b0
commit
2afce3f1f4
1
flutter/assets/file_transfer.svg
Normal file
1
flutter/assets/file_transfer.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694049173782" class="icon" viewBox="0 0 1024 1024" width="24" height="24" fill="#fff" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="992" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M891.64 184.73H620.41c-27.41 0-54.41-7.77-77.32-22.5L428.13 87.36C402.77 71 372.91 62 342.64 62H131.95C93.5 62 62 93.5 62 132.36v759.68C62 930.91 93.5 962 131.95 962h759.68c38.86 0 70.36-31.09 70.36-69.96V255.09c0.01-38.86-31.49-70.36-70.35-70.36zM480.5 753.77c0 16.77-13.5 30.68-30.68 30.68-16.77 0-30.68-13.91-30.68-30.68V523.04l-31.91 55.64c-8.59 14.32-27.41 19.64-42.14 11.04-14.32-8.59-19.64-27.41-11.05-41.73l89.18-154.64c6.96-12.27 21.27-18 34.77-14.32 13.09 3.27 22.5 15.55 22.5 29.45v345.29z m209.04-139.5l-89.18 154.64c-5.32 9.82-15.55 15.55-26.59 15.55-2.46 0-5.32-0.41-7.77-1.23-13.5-3.68-22.91-15.55-22.91-29.46V408.5c0-16.77 13.91-30.68 30.68-30.68 17.18 0 30.68 13.91 30.68 30.68v230.73l31.91-55.64c8.59-14.73 27.41-19.64 42.14-11.05 14.73 8.6 19.64 27.01 11.04 41.73z" p-id="993"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -2,6 +2,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
@ -9,12 +10,14 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/chat_page.dart';
|
||||
import '../../models/file_model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/server_model.dart';
|
||||
|
||||
@ -32,6 +35,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
void initState() {
|
||||
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
||||
windowManager.addListener(this);
|
||||
Get.put(tabController);
|
||||
tabController.onRemoved = (_, id) {
|
||||
onRemoveId(id);
|
||||
};
|
||||
@ -111,6 +115,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
});
|
||||
}
|
||||
windowManager.setTitle(getWindowNameWithId(client.peerId));
|
||||
gFFI.cmFileModel.updateCurrentClientId(client.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -173,7 +178,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
pageViewBuilder: (pageView) => Row(
|
||||
children: [
|
||||
Consumer<ChatModel>(
|
||||
builder: (_, model, child) => model.isShowCMChatPage
|
||||
builder: (_, model, child) => model.isShowCMSidePage
|
||||
? Expanded(
|
||||
child: buildRemoteBlock(
|
||||
child: Container(
|
||||
@ -182,8 +187,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
right: BorderSide(
|
||||
color: Theme.of(context)
|
||||
.dividerColor))),
|
||||
child:
|
||||
ChatPage(type: ChatPageType.desktopCM)),
|
||||
child: buildSidePage()),
|
||||
),
|
||||
flex: (kConnectionManagerWindowSizeOpenChat.width -
|
||||
kConnectionManagerWindowSizeClosedChat
|
||||
@ -204,6 +208,19 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSidePage() {
|
||||
final selected = gFFI.serverModel.tabController.state.value.selected;
|
||||
if (selected < 0 || selected >= gFFI.serverModel.clients.length) {
|
||||
return Offstage();
|
||||
}
|
||||
final clientType = gFFI.serverModel.clients[selected].type_();
|
||||
if (clientType == ClientType.file) {
|
||||
return _FileTransferLogPage();
|
||||
} else {
|
||||
return ChatPage(type: ChatPageType.desktopCM);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildTitleBar() {
|
||||
return SizedBox(
|
||||
height: kDesktopRemoteTabBarHeight,
|
||||
@ -447,14 +464,21 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !client.authorized || client.type_() != ClientType.remote,
|
||||
offstage: !client.authorized ||
|
||||
(client.type_() != ClientType.remote &&
|
||||
client.type_() != ClientType.file),
|
||||
child: IconButton(
|
||||
onPressed: () => checkClickTime(
|
||||
client.id,
|
||||
() => gFFI.chatModel
|
||||
.toggleCMChatPage(MessageKey(client.peerId, client.id)),
|
||||
),
|
||||
icon: SvgPicture.asset('assets/chat2.svg'),
|
||||
onPressed: () => checkClickTime(client.id, () {
|
||||
if (client.type_() != ClientType.file) {
|
||||
gFFI.chatModel.toggleCMSidePage();
|
||||
} else {
|
||||
gFFI.chatModel
|
||||
.toggleCMChatPage(MessageKey(client.peerId, client.id));
|
||||
}
|
||||
}),
|
||||
icon: SvgPicture.asset(client.type_() == ClientType.file
|
||||
? 'assets/file_transfer.svg'
|
||||
: 'assets/chat2.svg'),
|
||||
splashRadius: kDesktopIconButtonSplashRadius,
|
||||
),
|
||||
)
|
||||
@ -912,3 +936,182 @@ void checkClickTime(int id, Function() callback) async {
|
||||
if (d > 120) callback();
|
||||
});
|
||||
}
|
||||
|
||||
class _FileTransferLogPage extends StatefulWidget {
|
||||
_FileTransferLogPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_FileTransferLogPage> createState() => __FileTransferLogPageState();
|
||||
}
|
||||
|
||||
class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return statusList();
|
||||
}
|
||||
|
||||
Widget generateCard(Widget child) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(15.0),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget statusList() {
|
||||
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(
|
||||
() {
|
||||
final jobTable = gFFI.cmFileModel.currentJobTable;
|
||||
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: [
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Column(
|
||||
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),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.fileName,
|
||||
).paddingSymmetric(vertical: 10),
|
||||
if (item.totalSize > 0)
|
||||
Text(
|
||||
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
if (item.totalSize > 0)
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (item.totalSize > 0)
|
||||
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: [],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(vertical: 10),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: jobTable.length,
|
||||
);
|
||||
|
||||
return 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(jobTable);
|
||||
},
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -86,13 +86,13 @@ class ChatModel with ChangeNotifier {
|
||||
late final Map<MessageKey, MessageBody> _messages = {};
|
||||
|
||||
MessageKey _currentKey = MessageKey('', -2); // -2 is invalid value
|
||||
late bool _isShowCMChatPage = false;
|
||||
late bool _isShowCMSidePage = false;
|
||||
|
||||
Map<MessageKey, MessageBody> get messages => _messages;
|
||||
|
||||
MessageKey get currentKey => _currentKey;
|
||||
|
||||
bool get isShowCMChatPage => _isShowCMChatPage;
|
||||
bool get isShowCMSidePage => _isShowCMSidePage;
|
||||
|
||||
void setOverlayState(BlockableOverlayState blockableOverlayState) {
|
||||
_blockableOverlayState = blockableOverlayState;
|
||||
@ -255,7 +255,7 @@ class ChatModel with ChangeNotifier {
|
||||
showChatPage(MessageKey key) async {
|
||||
if (isDesktop) {
|
||||
if (isConnManager) {
|
||||
if (!_isShowCMChatPage) {
|
||||
if (!_isShowCMSidePage) {
|
||||
await toggleCMChatPage(key);
|
||||
}
|
||||
} else {
|
||||
@ -272,12 +272,26 @@ class ChatModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
showSidePage() async {
|
||||
if (isDesktop) {
|
||||
if (isConnManager) {
|
||||
if (!_isShowCMSidePage) {
|
||||
await toggleCMSidePage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleCMChatPage(MessageKey key) async {
|
||||
if (gFFI.chatModel.currentKey != key) {
|
||||
gFFI.chatModel.changeCurrentKey(key);
|
||||
}
|
||||
if (_isShowCMChatPage) {
|
||||
_isShowCMChatPage = !_isShowCMChatPage;
|
||||
await toggleCMSidePage();
|
||||
}
|
||||
|
||||
toggleCMSidePage() async {
|
||||
if (_isShowCMSidePage) {
|
||||
_isShowCMSidePage = !_isShowCMSidePage;
|
||||
notifyListeners();
|
||||
await windowManager.show();
|
||||
await windowManager.setSizeAlignment(
|
||||
@ -287,7 +301,7 @@ class ChatModel with ChangeNotifier {
|
||||
await windowManager.show();
|
||||
await windowManager.setSizeAlignment(
|
||||
kConnectionManagerWindowSizeOpenChat, Alignment.topRight);
|
||||
_isShowCMChatPage = !_isShowCMChatPage;
|
||||
_isShowCMSidePage = !_isShowCMSidePage;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
154
flutter/lib/models/cm_file_model.dart
Normal file
154
flutter/lib/models/cm_file_model.dart
Normal file
@ -0,0 +1,154 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'file_model.dart';
|
||||
|
||||
class CmFileModel {
|
||||
final WeakReference<FFI> parent;
|
||||
final currentJobTable = RxList<JobProgress>();
|
||||
final _jobTables = HashMap<int, RxList<JobProgress>>.fromEntries([]);
|
||||
Stopwatch stopwatch = Stopwatch();
|
||||
int _lastElapsed = 0;
|
||||
bool _jobAdded = false;
|
||||
bool _showing = false;
|
||||
|
||||
CmFileModel(this.parent);
|
||||
|
||||
void updateCurrentClientId(int id) {
|
||||
if (_jobTables[id] == null) {
|
||||
_jobTables[id] = RxList<JobProgress>();
|
||||
}
|
||||
Future.delayed(Duration.zero, () {
|
||||
currentJobTable.value = _jobTables[id]!;
|
||||
});
|
||||
}
|
||||
|
||||
onFileTransferLog(dynamic log) {
|
||||
try {
|
||||
dynamic d = jsonDecode(log);
|
||||
if (!stopwatch.isRunning) stopwatch.start();
|
||||
bool calcSpeed = stopwatch.elapsedMilliseconds - _lastElapsed >= 1000;
|
||||
if (calcSpeed) {
|
||||
_lastElapsed = stopwatch.elapsedMilliseconds;
|
||||
}
|
||||
if (d is List<dynamic>) {
|
||||
for (var l in d) {
|
||||
_dealOneJob(l, calcSpeed);
|
||||
}
|
||||
} else {
|
||||
_dealOneJob(d, calcSpeed);
|
||||
}
|
||||
currentJobTable.refresh();
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (_jobAdded) {
|
||||
_jobAdded = false;
|
||||
if (!_showing) {
|
||||
_showing = true;
|
||||
await gFFI.chatModel.showSidePage();
|
||||
_showing = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint("onFileTransferLog:$e");
|
||||
}
|
||||
}
|
||||
|
||||
_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");
|
||||
return;
|
||||
}
|
||||
JobProgress? job = jobTable.firstWhereOrNull((e) => e.id == data.id);
|
||||
if (job == null) {
|
||||
job = JobProgress();
|
||||
jobTable.add(job);
|
||||
_jobAdded = true;
|
||||
final currentSelectedTab =
|
||||
gFFI.serverModel.tabController.state.value.selectedTabInfo;
|
||||
if (currentSelectedTab.key != data.connId.toString()) {
|
||||
client?.unreadChatMessageCount.value += 1;
|
||||
}
|
||||
}
|
||||
job.id = data.id;
|
||||
job.isRemoteToLocal = data.isRemote;
|
||||
job.fileName = data.path;
|
||||
job.totalSize = data.totalSize;
|
||||
job.finishedSize = data.finishedSize;
|
||||
if (job.finishedSize > data.totalSize) {
|
||||
job.finishedSize = data.totalSize;
|
||||
}
|
||||
job.isRemoteToLocal = data.isRemote;
|
||||
|
||||
if (job.finishedSize > 0) {
|
||||
if (job.finishedSize < job.totalSize) {
|
||||
job.state = JobState.inProgress;
|
||||
} else {
|
||||
job.state = JobState.done;
|
||||
}
|
||||
}
|
||||
if (data.done) {
|
||||
job.state = JobState.done;
|
||||
} else if (data.cancel || data.error == 'skipped') {
|
||||
job.state = JobState.done;
|
||||
job.err = 'skipped';
|
||||
} else if (data.error.isNotEmpty) {
|
||||
job.state = JobState.error;
|
||||
job.err = data.error;
|
||||
}
|
||||
if (calcSpeed) {
|
||||
job.speed = (data.transferred - job.lastTransferredSize) * 1.0;
|
||||
job.lastTransferredSize = data.transferred;
|
||||
}
|
||||
jobTable.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
class TransferJobSerdeData {
|
||||
int connId;
|
||||
int id;
|
||||
String path;
|
||||
bool isRemote;
|
||||
int totalSize;
|
||||
int finishedSize;
|
||||
int transferred;
|
||||
bool done;
|
||||
bool cancel;
|
||||
String error;
|
||||
|
||||
TransferJobSerdeData({
|
||||
required this.connId,
|
||||
required this.id,
|
||||
required this.path,
|
||||
required this.isRemote,
|
||||
required this.totalSize,
|
||||
required this.finishedSize,
|
||||
required this.transferred,
|
||||
required this.done,
|
||||
required this.cancel,
|
||||
required this.error,
|
||||
});
|
||||
|
||||
TransferJobSerdeData.fromJson(dynamic d)
|
||||
: this(
|
||||
connId: d['connId'] ?? 0,
|
||||
id: int.tryParse(d['id'].toString()) ?? 0,
|
||||
path: d['path'] ?? '',
|
||||
isRemote: d['isRemote'] ?? false,
|
||||
totalSize: d['totalSize'] ?? 0,
|
||||
finishedSize: d['finishedSize'] ?? 0,
|
||||
transferred: d['transferred'] ?? 0,
|
||||
done: d['done'] ?? false,
|
||||
cancel: d['cancel'] ?? false,
|
||||
error: d['error'] ?? '',
|
||||
);
|
||||
}
|
@ -1029,6 +1029,7 @@ class JobProgress {
|
||||
var to = "";
|
||||
var showHidden = false;
|
||||
var err = "";
|
||||
int lastTransferredSize = 0;
|
||||
|
||||
clear() {
|
||||
state = JobState.none;
|
||||
|
@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/generated_bridge.dart';
|
||||
import 'package:flutter_hbb/models/ab_model.dart';
|
||||
import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/cm_file_model.dart';
|
||||
import 'package:flutter_hbb/models/file_model.dart';
|
||||
import 'package:flutter_hbb/models/group_model.dart';
|
||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
||||
@ -317,6 +318,10 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (name == "cm_file_transfer_log") {
|
||||
if (isDesktop) {
|
||||
gFFI.cmFileModel.onFileTransferLog(evt['log']);
|
||||
}
|
||||
} else {
|
||||
debugPrint('Unknown event name: $name');
|
||||
}
|
||||
@ -1696,6 +1701,7 @@ class FFI {
|
||||
late final RecordingModel recordingModel; // session
|
||||
late final InputModel inputModel; // session
|
||||
late final ElevationModel elevationModel; // session
|
||||
late final CmFileModel cmFileModel; // cm
|
||||
|
||||
FFI(SessionID? sId) {
|
||||
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
|
||||
@ -1714,6 +1720,7 @@ class FFI {
|
||||
recordingModel = RecordingModel(WeakReference(this));
|
||||
inputModel = InputModel(WeakReference(this));
|
||||
elevationModel = ElevationModel(WeakReference(this));
|
||||
cmFileModel = CmFileModel(WeakReference(this));
|
||||
}
|
||||
|
||||
/// Mobile reuse FFI
|
||||
|
@ -397,6 +397,7 @@ message FileTransferReceiveRequest {
|
||||
string path = 2; // path written to
|
||||
repeated FileEntry files = 3;
|
||||
int32 file_num = 4;
|
||||
uint64 total_size = 5;
|
||||
}
|
||||
|
||||
message FileRemoveDir {
|
||||
|
@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tokio::{fs::File, io::*};
|
||||
|
||||
use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream};
|
||||
@ -194,7 +195,8 @@ pub fn can_enable_overwrite_detection(version: i64) -> bool {
|
||||
version >= get_version_number("1.1.10")
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransferJob {
|
||||
pub id: i32,
|
||||
pub remote: String,
|
||||
@ -203,10 +205,13 @@ pub struct TransferJob {
|
||||
pub is_remote: bool,
|
||||
pub is_last_job: bool,
|
||||
pub file_num: i32,
|
||||
#[serde(skip_serializing)]
|
||||
pub files: Vec<FileEntry>,
|
||||
pub conn_id: i32, // server only
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
file: Option<File>,
|
||||
total_size: u64,
|
||||
pub total_size: u64,
|
||||
finished_size: u64,
|
||||
transferred: u64,
|
||||
enable_overwrite_detection: bool,
|
||||
@ -695,13 +700,20 @@ pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec<FileEntry>) -> Message {
|
||||
pub fn new_receive(
|
||||
id: i32,
|
||||
path: String,
|
||||
file_num: i32,
|
||||
files: Vec<FileEntry>,
|
||||
total_size: u64,
|
||||
) -> Message {
|
||||
let mut action = FileAction::new();
|
||||
action.set_receive(FileTransferReceiveRequest {
|
||||
id,
|
||||
path,
|
||||
files,
|
||||
file_num,
|
||||
total_size,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
@ -748,10 +760,16 @@ pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> {
|
||||
jobs.iter_mut().find(|x| x.id() == id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> {
|
||||
jobs.iter().find(|x| x.id() == id)
|
||||
}
|
||||
|
||||
pub async fn handle_read_jobs(
|
||||
jobs: &mut Vec<TransferJob>,
|
||||
stream: &mut crate::Stream,
|
||||
) -> ResultType<()> {
|
||||
) -> ResultType<String> {
|
||||
let mut job_log = Default::default();
|
||||
let mut finished = Vec::new();
|
||||
for job in jobs.iter_mut() {
|
||||
if job.is_last_job {
|
||||
@ -768,9 +786,11 @@ pub async fn handle_read_jobs(
|
||||
}
|
||||
Ok(None) => {
|
||||
if job.job_completed() {
|
||||
job_log = serialize_transfer_job(job, true, false, "");
|
||||
finished.push(job.id());
|
||||
match job.job_error() {
|
||||
Some(err) => {
|
||||
job_log = serialize_transfer_job(job, false, false, &err);
|
||||
stream
|
||||
.send(&new_error(job.id(), err, job.file_num()))
|
||||
.await?
|
||||
@ -786,7 +806,7 @@ pub async fn handle_read_jobs(
|
||||
for id in finished {
|
||||
remove_job(id, jobs);
|
||||
}
|
||||
Ok(())
|
||||
Ok(job_log)
|
||||
}
|
||||
|
||||
pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
|
||||
@ -861,3 +881,20 @@ pub fn is_write_need_confirmation(
|
||||
Ok(DigestCheckResult::NoSuchFile)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String {
|
||||
let mut v = vec![];
|
||||
for job in jobs {
|
||||
let value = serde_json::to_value(job).unwrap_or_default();
|
||||
v.push(value);
|
||||
}
|
||||
serde_json::to_string(&v).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String {
|
||||
let mut value = serde_json::to_value(job).unwrap_or_default();
|
||||
value["done"] = json!(done);
|
||||
value["cancel"] = json!(cancel);
|
||||
value["error"] = json!(error);
|
||||
serde_json::to_string(&value).unwrap_or_default()
|
||||
}
|
||||
|
@ -483,9 +483,13 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
// peer is not windows, need transform \ to /
|
||||
fs::transform_windows_path(&mut files);
|
||||
}
|
||||
let total_size = job.total_size();
|
||||
self.read_jobs.push(job);
|
||||
self.timer = time::interval(MILLI1);
|
||||
allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await);
|
||||
allow_err!(
|
||||
peer.send(&fs::new_receive(id, to, file_num, files, total_size))
|
||||
.await
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -568,7 +572,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
id,
|
||||
job.path.to_string_lossy().to_string(),
|
||||
job.file_num,
|
||||
job.files.clone()
|
||||
job.files.clone(),
|
||||
job.total_size(),
|
||||
))
|
||||
.await
|
||||
);
|
||||
|
@ -907,6 +907,10 @@ pub mod connection_manager {
|
||||
let client_json = serde_json::to_string(&client).unwrap_or("".into());
|
||||
self.push_event("update_voice_call_state", vec![("client", &client_json)]);
|
||||
}
|
||||
|
||||
fn file_transfer_log(&self, log: String) {
|
||||
self.push_event("cm_file_transfer_log", vec![("log", &log.to_string())]);
|
||||
}
|
||||
}
|
||||
|
||||
impl FlutterHandler {
|
||||
|
@ -70,6 +70,8 @@ pub enum FS {
|
||||
file_num: i32,
|
||||
files: Vec<(String, u64)>,
|
||||
overwrite_detection: bool,
|
||||
total_size: u64,
|
||||
conn_id: i32,
|
||||
},
|
||||
CancelWrite {
|
||||
id: i32,
|
||||
@ -231,6 +233,7 @@ pub enum Data {
|
||||
Plugin(Plugin),
|
||||
#[cfg(windows)]
|
||||
SyncWinCpuUsage(Option<f64>),
|
||||
FileTransferLog(String),
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
|
@ -188,6 +188,7 @@ pub struct Connection {
|
||||
lr: LoginRequest,
|
||||
last_recv_time: Arc<Mutex<Instant>>,
|
||||
chat_unanswered: bool,
|
||||
file_transferred: bool,
|
||||
#[cfg(windows)]
|
||||
portable: PortableState,
|
||||
from_switch: bool,
|
||||
@ -320,6 +321,7 @@ impl Connection {
|
||||
lr: Default::default(),
|
||||
last_recv_time: Arc::new(Mutex::new(Instant::now())),
|
||||
chat_unanswered: false,
|
||||
file_transferred: false,
|
||||
#[cfg(windows)]
|
||||
portable: Default::default(),
|
||||
from_switch: false,
|
||||
@ -399,6 +401,7 @@ impl Connection {
|
||||
}
|
||||
ipc::Data::Close => {
|
||||
conn.chat_unanswered = false; // seen
|
||||
conn.file_transferred = false; //seen
|
||||
conn.send_close_reason_no_retry("").await;
|
||||
conn.on_close("connection manager", true).await;
|
||||
break;
|
||||
@ -536,9 +539,17 @@ impl Connection {
|
||||
},
|
||||
_ = conn.file_timer.tick() => {
|
||||
if !conn.read_jobs.is_empty() {
|
||||
if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await {
|
||||
conn.on_close(&err.to_string(), false).await;
|
||||
break;
|
||||
conn.send_to_cm(ipc::Data::FileTransferLog(fs::serialize_transfer_jobs(&conn.read_jobs)));
|
||||
match fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await {
|
||||
Ok(log) => {
|
||||
if !log.is_empty() {
|
||||
conn.send_to_cm(ipc::Data::FileTransferLog(log));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
conn.on_close(&err.to_string(), false).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30);
|
||||
@ -1717,6 +1728,7 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
Some(file_action::Union::Send(s)) => {
|
||||
// server to client
|
||||
let id = s.id;
|
||||
let od = can_enable_overwrite_detection(get_version_number(
|
||||
&self.lr.version,
|
||||
@ -1734,10 +1746,12 @@ impl Connection {
|
||||
Err(err) => {
|
||||
self.send(fs::new_error(id, err, 0)).await;
|
||||
}
|
||||
Ok(job) => {
|
||||
Ok(mut job) => {
|
||||
self.send(fs::new_dir(id, path, job.files().to_vec()))
|
||||
.await;
|
||||
let mut files = job.files().to_owned();
|
||||
job.is_remote = true;
|
||||
job.conn_id = self.inner.id();
|
||||
self.read_jobs.push(job);
|
||||
self.file_timer = time::interval(MILLI1);
|
||||
self.post_file_audit(
|
||||
@ -1751,8 +1765,10 @@ impl Connection {
|
||||
);
|
||||
}
|
||||
}
|
||||
self.file_transferred = true;
|
||||
}
|
||||
Some(file_action::Union::Receive(r)) => {
|
||||
// client to server
|
||||
// note: 1.1.10 introduced identical file detection, which breaks original logic of send/recv files
|
||||
// whenever got send/recv request, check peer version to ensure old version of rustdesk
|
||||
let od = can_enable_overwrite_detection(get_version_number(
|
||||
@ -1769,6 +1785,8 @@ impl Connection {
|
||||
.map(|f| (f.name, f.modified_time))
|
||||
.collect(),
|
||||
overwrite_detection: od,
|
||||
total_size: r.total_size,
|
||||
conn_id: self.inner.id(),
|
||||
});
|
||||
self.post_file_audit(
|
||||
FileAuditType::RemoteReceive,
|
||||
@ -1780,6 +1798,7 @@ impl Connection {
|
||||
.collect(),
|
||||
json!({}),
|
||||
);
|
||||
self.file_transferred = true;
|
||||
}
|
||||
Some(file_action::Union::RemoveDir(d)) => {
|
||||
self.send_fs(ipc::FS::RemoveDir {
|
||||
@ -1803,6 +1822,11 @@ impl Connection {
|
||||
}
|
||||
Some(file_action::Union::Cancel(c)) => {
|
||||
self.send_fs(ipc::FS::CancelWrite { id: c.id });
|
||||
if let Some(job) = fs::get_job_immutable(c.id, &self.read_jobs) {
|
||||
self.send_to_cm(ipc::Data::FileTransferLog(
|
||||
fs::serialize_transfer_job(job, false, true, ""),
|
||||
));
|
||||
}
|
||||
fs::remove_job(c.id, &mut self.read_jobs);
|
||||
}
|
||||
Some(file_action::Union::SendConfirm(r)) => {
|
||||
@ -2254,7 +2278,7 @@ impl Connection {
|
||||
lock_screen().await;
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let data = if self.chat_unanswered {
|
||||
let data = if self.chat_unanswered || self.file_transferred {
|
||||
ipc::Data::Disconnected
|
||||
} else {
|
||||
ipc::Data::Close
|
||||
|
@ -59,13 +59,11 @@ impl InvokeUiCM for SciterHandler {
|
||||
fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) {
|
||||
self.call(
|
||||
"updateVoiceCallState",
|
||||
&make_args!(
|
||||
client.id,
|
||||
client.in_voice_call,
|
||||
client.incoming_voice_call
|
||||
),
|
||||
&make_args!(client.id, client.in_voice_call, client.incoming_voice_call),
|
||||
);
|
||||
}
|
||||
|
||||
fn file_transfer_log(&self, _log: String) {}
|
||||
}
|
||||
|
||||
impl SciterHandler {
|
||||
|
@ -99,6 +99,8 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized {
|
||||
fn show_elevation(&self, show: bool);
|
||||
|
||||
fn update_voice_call_state(&self, client: &Client);
|
||||
|
||||
fn file_transfer_log(&self, log: String);
|
||||
}
|
||||
|
||||
impl<T: InvokeUiCM> Deref for ConnectionManager<T> {
|
||||
@ -357,6 +359,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
);
|
||||
}
|
||||
}
|
||||
let (tx_log, mut rx_log) = mpsc::unbounded_channel::<String>();
|
||||
|
||||
self.running = false;
|
||||
loop {
|
||||
@ -403,11 +406,16 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs {
|
||||
if let Ok(bytes) = self.stream.next_raw().await {
|
||||
fs = ipc::FS::WriteBlock{id, file_num, data:bytes.into(), compressed};
|
||||
handle_fs(fs, &mut write_jobs, &self.tx).await;
|
||||
handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await;
|
||||
}
|
||||
} else {
|
||||
handle_fs(fs, &mut write_jobs, &self.tx).await;
|
||||
handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await;
|
||||
}
|
||||
let log = fs::serialize_transfer_jobs(&write_jobs);
|
||||
self.cm.ui_handler.file_transfer_log(log);
|
||||
}
|
||||
Data::FileTransferLog(log) => {
|
||||
self.cm.ui_handler.file_transfer_log(log);
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
Data::ClipboardFile(_clip) => {
|
||||
@ -509,6 +517,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
//
|
||||
}
|
||||
},
|
||||
Some(job_log) = rx_log.recv() => {
|
||||
self.cm.ui_handler.file_transfer_log(job_log);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -632,7 +643,7 @@ pub async fn start_listen<T: InvokeUiCM>(
|
||||
cm.new_message(current_id, text);
|
||||
}
|
||||
Some(Data::FS(fs)) => {
|
||||
handle_fs(fs, &mut write_jobs, &tx).await;
|
||||
handle_fs(fs, &mut write_jobs, &tx, None).await;
|
||||
}
|
||||
Some(Data::Close) => {
|
||||
break;
|
||||
@ -647,7 +658,14 @@ pub async fn start_listen<T: InvokeUiCM>(
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec<fs::TransferJob>, tx: &UnboundedSender<Data>) {
|
||||
async fn handle_fs(
|
||||
fs: ipc::FS,
|
||||
write_jobs: &mut Vec<fs::TransferJob>,
|
||||
tx: &UnboundedSender<Data>,
|
||||
tx_log: Option<&UnboundedSender<String>>,
|
||||
) {
|
||||
use hbb_common::fs::serialize_transfer_job;
|
||||
|
||||
match fs {
|
||||
ipc::FS::ReadDir {
|
||||
dir,
|
||||
@ -674,10 +692,12 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec<fs::TransferJob>, tx: &Unbo
|
||||
file_num,
|
||||
mut files,
|
||||
overwrite_detection,
|
||||
total_size,
|
||||
conn_id,
|
||||
} => {
|
||||
// cm has no show_hidden context
|
||||
// dummy remote, show_hidden, is_remote
|
||||
write_jobs.push(fs::TransferJob::new_write(
|
||||
let mut job = fs::TransferJob::new_write(
|
||||
id,
|
||||
"".to_string(),
|
||||
path,
|
||||
@ -693,11 +713,17 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec<fs::TransferJob>, tx: &Unbo
|
||||
})
|
||||
.collect(),
|
||||
overwrite_detection,
|
||||
));
|
||||
);
|
||||
job.total_size = total_size;
|
||||
job.conn_id = conn_id;
|
||||
write_jobs.push(job);
|
||||
}
|
||||
ipc::FS::CancelWrite { id } => {
|
||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||
job.remove_download_file();
|
||||
tx_log.map(|tx: &UnboundedSender<String>| {
|
||||
tx.send(serialize_transfer_job(job, false, true, ""))
|
||||
});
|
||||
fs::remove_job(id, write_jobs);
|
||||
}
|
||||
}
|
||||
@ -705,11 +731,13 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec<fs::TransferJob>, tx: &Unbo
|
||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||
job.modify_time();
|
||||
send_raw(fs::new_done(id, file_num), tx);
|
||||
tx_log.map(|tx| tx.send(serialize_transfer_job(job, true, false, "")));
|
||||
fs::remove_job(id, write_jobs);
|
||||
}
|
||||
}
|
||||
ipc::FS::WriteError { id, file_num, err } => {
|
||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||
tx_log.map(|tx| tx.send(serialize_transfer_job(job, false, false, &err)));
|
||||
send_raw(fs::new_error(job.id(), err, file_num), tx);
|
||||
fs::remove_job(job.id(), write_jobs);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user