Merge pull request #801 from Kingtous/flutter_desktop

fix: FFI id assignment & keep Remote Page state for multi tabs & add file transfer multi tab support
This commit is contained in:
RustDesk 2022-06-17 23:02:03 +08:00 committed by GitHub
commit 97509127d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 811 additions and 50 deletions

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
@ -79,21 +78,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
}
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage(id: id),
),
);
await rustDeskWinManager.new_file_transfer(id);
} else {
// single window
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (BuildContext context) => RemotePage(id: id),
// ),
// );
// multi window
await rustDeskWinManager.new_remote_desktop(id);
}
FocusScopeNode currentFocus = FocusScope.of(context);
@ -307,12 +293,10 @@ class _ConnectionPageState extends State<ConnectionPage> {
PopupMenuItem<String>(
child: Text(translate('Remove')), value: 'remove')
] +
(!isAndroid
? []
: [
PopupMenuItem<String>(
child: Text(translate('File transfer')), value: 'file')
]),
([
PopupMenuItem<String>(
child: Text(translate('File transfer')), value: 'file')
]),
elevation: 8,
);
if (value == 'remove') {

View File

@ -0,0 +1,572 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:flutter_hbb/mobile/pages/file_manager_page.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:toggle_switch/toggle_switch.dart';
import 'package:wakelock/wakelock.dart';
import '../../common.dart';
import '../../mobile/widgets/dialog.dart';
import '../../models/model.dart';
class FileManagerPage extends StatefulWidget {
FileManagerPage({Key? key, required this.id}) : super(key: key);
final String id;
@override
State<StatefulWidget> createState() => _FileManagerPageState();
}
class _FileManagerPageState extends State<FileManagerPage>
with AutomaticKeepAliveClientMixin {
final _selectedItems = SelectedItems();
final _breadCrumbScroller = ScrollController();
/// FFI with name file_transfer_id
FFI get _ffi => ffi('ft_${widget.id}');
FileModel get model => _ffi.fileModel;
@override
void initState() {
super.initState();
Get.put(FFI(), tag: 'ft_${widget.id}');
_ffi.ffiModel.platformFFI = gFFI.ffiModel.platformFFI;
_ffi.connect(widget.id, isFileTransfer: true);
_ffi.ffiModel.updateEventListener(widget.id);
if (!Platform.isLinux) {
Wakelock.enable();
}
}
@override
void dispose() {
model.onClose();
_ffi.close();
SmartDialog.dismiss();
if (!Platform.isLinux) {
Wakelock.disable();
}
Get.delete<FFI>(tag: 'ft_${widget.id}');
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return ChangeNotifierProvider.value(
value: _ffi.fileModel,
child: Consumer<FileModel>(builder: (_context, _model, _child) {
return WillPopScope(
onWillPop: () async {
if (model.selectMode) {
model.toggleSelectMode();
} else {
goBack();
}
return false;
},
child: Scaffold(
backgroundColor: MyTheme.grayBg,
appBar: AppBar(
leading: Row(children: [
IconButton(icon: Icon(Icons.close), onPressed: clientClose),
]),
centerTitle: true,
title: ToggleSwitch(
initialLabelIndex: model.isLocal ? 0 : 1,
activeBgColor: [MyTheme.idColor],
inactiveBgColor: MyTheme.grayBg,
inactiveFgColor: Colors.black54,
totalSwitches: 2,
minWidth: 100,
fontSize: 15,
iconSize: 18,
labels: [translate("Local"), translate("Remote")],
icons: [Icons.phone_android_sharp, Icons.screen_share],
onToggle: (index) {
final current = model.isLocal ? 0 : 1;
if (index != current) {
model.togglePage();
}
},
),
actions: [
PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Row(
children: [
Icon(Icons.refresh, color: Colors.black),
SizedBox(width: 5),
Text(translate("Refresh File"))
],
),
value: "refresh",
),
PopupMenuItem(
child: Row(
children: [
Icon(Icons.check, color: Colors.black),
SizedBox(width: 5),
Text(translate("Multi Select"))
],
),
value: "select",
),
PopupMenuItem(
child: Row(
children: [
Icon(Icons.folder_outlined,
color: Colors.black),
SizedBox(width: 5),
Text(translate("Create Folder"))
],
),
value: "folder",
),
PopupMenuItem(
child: Row(
children: [
Icon(
model.currentShowHidden
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
color: Colors.black),
SizedBox(width: 5),
Text(translate("Show Hidden Files"))
],
),
value: "hidden",
)
];
},
onSelected: (v) {
if (v == "refresh") {
model.refresh();
} else if (v == "select") {
_selectedItems.clear();
model.toggleSelectMode();
} else if (v == "folder") {
final name = TextEditingController();
DialogManager.show((setState, close) =>
CustomAlertDialog(
title: Text(translate("Create Folder")),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
labelText: translate(
"Please enter the folder name"),
),
controller: name,
),
],
),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () => close(false),
child: Text(translate("Cancel"))),
ElevatedButton(
style: flatButtonStyle,
onPressed: () {
if (name.value.text.isNotEmpty) {
model.createDir(PathUtil.join(
model.currentDir.path,
name.value.text,
model.currentIsWindows));
close();
}
},
child: Text(translate("OK")))
]));
} else if (v == "hidden") {
model.toggleShowHidden();
}
}),
],
),
body: body(),
bottomSheet: bottomSheet(),
));
}));
}
bool needShowCheckBox() {
if (!model.selectMode) {
return false;
}
return !_selectedItems.isOtherPage(model.isLocal);
}
Widget body() {
final isLocal = model.isLocal;
final fd = model.currentDir;
final entries = fd.entries;
return Column(children: [
headTools(),
Expanded(
child: ListView.builder(
itemCount: entries.length + 1,
itemBuilder: (context, index) {
if (index >= entries.length) {
return listTail();
}
var selected = false;
if (model.selectMode) {
selected = _selectedItems.contains(entries[index]);
}
final sizeStr = entries[index].isFile
? readableFileSize(entries[index].size.toDouble())
: "";
return Card(
child: ListTile(
leading: Icon(
entries[index].isFile ? Icons.feed_outlined : Icons.folder,
size: 40),
title: Text(entries[index].name),
selected: selected,
subtitle: Text(
entries[index]
.lastModified()
.toString()
.replaceAll(".000", "") +
" " +
sizeStr,
style: TextStyle(fontSize: 12, color: MyTheme.darkGray),
),
trailing: needShowCheckBox()
? Checkbox(
value: selected,
onChanged: (v) {
if (v == null) return;
if (v && !selected) {
_selectedItems.add(isLocal, entries[index]);
} else if (!v && selected) {
_selectedItems.remove(entries[index]);
}
setState(() {});
})
: PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Text(translate("Delete")),
value: "delete",
),
PopupMenuItem(
child: Text(translate("Multi Select")),
value: "multi_select",
),
PopupMenuItem(
child: Text(translate("Properties")),
value: "properties",
enabled: false,
)
];
},
onSelected: (v) {
if (v == "delete") {
final items = SelectedItems();
items.add(isLocal, entries[index]);
model.removeAction(items);
} else if (v == "multi_select") {
_selectedItems.clear();
model.toggleSelectMode();
}
}),
onTap: () {
if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) {
if (selected) {
_selectedItems.remove(entries[index]);
} else {
_selectedItems.add(isLocal, entries[index]);
}
setState(() {});
return;
}
if (entries[index].isDirectory) {
model.openDirectory(entries[index].path);
breadCrumbScrollToEnd();
} else {
// Perform file-related tasks.
}
},
onLongPress: () {
_selectedItems.clear();
model.toggleSelectMode();
if (model.selectMode) {
_selectedItems.add(isLocal, entries[index]);
}
setState(() {});
},
),
);
},
))
]);
}
goBack() {
model.goToParentDirectory();
}
breadCrumbScrollToEnd() {
Future.delayed(Duration(milliseconds: 200), () {
_breadCrumbScroller.animateTo(
_breadCrumbScroller.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.fastLinearToSlowEaseIn);
});
}
Widget headTools() => Container(
child: Row(
children: [
Expanded(
child: BreadCrumb(
items: getPathBreadCrumbItems(() => model.goHome(), (list) {
var path = "";
if (model.currentHome.startsWith(list[0])) {
// absolute path
for (var item in list) {
path = PathUtil.join(path, item, model.currentIsWindows);
}
} else {
path += model.currentHome;
for (var item in list) {
path = PathUtil.join(path, item, model.currentIsWindows);
}
}
model.openDirectory(path);
}),
divider: Icon(Icons.chevron_right),
overflow: ScrollableOverflow(controller: _breadCrumbScroller),
)),
Row(
children: [
IconButton(
icon: Icon(Icons.arrow_upward),
onPressed: goBack,
),
PopupMenuButton<SortBy>(
icon: Icon(Icons.sort),
itemBuilder: (context) {
return SortBy.values
.map((e) => PopupMenuItem(
child:
Text(translate(e.toString().split(".").last)),
value: e,
))
.toList();
},
onSelected: model.changeSortStyle),
],
)
],
));
Widget listTail() {
return Container(
height: 100,
child: Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(30, 5, 30, 0),
child: Text(
model.currentDir.path,
style: TextStyle(color: MyTheme.darkGray),
),
),
Padding(
padding: EdgeInsets.all(2),
child: Text(
"${translate("Total")}: ${model.currentDir.entries.length} ${translate("items")}",
style: TextStyle(color: MyTheme.darkGray),
),
)
],
),
);
}
Widget? bottomSheet() {
final state = model.jobState;
final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
final selectedItemsLen = "${_selectedItems.length} ${translate("items")}";
final local = _selectedItems.isLocal == null
? ""
: " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]";
if (model.selectMode) {
if (_selectedItems.length == 0 || !isOtherPage) {
return BottomSheetBody(
leading: Icon(Icons.check),
title: translate("Selected"),
text: selectedItemsLen + local,
onCanceled: () => model.toggleSelectMode(),
actions: [
IconButton(
icon: Icon(Icons.compare_arrows),
onPressed: model.togglePage,
),
IconButton(
icon: Icon(Icons.delete_forever),
onPressed: () {
if (_selectedItems.length > 0) {
model.removeAction(_selectedItems);
}
},
)
]);
} else {
return BottomSheetBody(
leading: Icon(Icons.input),
title: translate("Paste here?"),
text: selectedItemsLen + local,
onCanceled: () => model.toggleSelectMode(),
actions: [
IconButton(
icon: Icon(Icons.compare_arrows),
onPressed: model.togglePage,
),
IconButton(
icon: Icon(Icons.paste),
onPressed: () {
model.toggleSelectMode();
model.sendFiles(_selectedItems);
},
)
]);
}
}
switch (state) {
case JobState.inProgress:
return BottomSheetBody(
leading: CircularProgressIndicator(),
title: translate("Waiting"),
text:
"${translate("Speed")}: ${readableFileSize(model.jobProgress.speed)}/s",
onCanceled: () => model.cancelJob(model.jobProgress.id),
);
case JobState.done:
return BottomSheetBody(
leading: Icon(Icons.check),
title: "${translate("Successful")}!",
text: "",
onCanceled: () => model.jobReset(),
);
case JobState.error:
return BottomSheetBody(
leading: Icon(Icons.error),
title: "${translate("Error")}!",
text: "",
onCanceled: () => model.jobReset(),
);
case JobState.none:
break;
}
return null;
}
List<BreadCrumbItem> getPathBreadCrumbItems(
void Function() onHome, void Function(List<String>) onPressed) {
final path = model.currentShortPath;
final list = PathUtil.split(path, model.currentIsWindows);
final breadCrumbList = [
BreadCrumbItem(
content: IconButton(
icon: Icon(Icons.home_filled),
onPressed: onHome,
))
];
breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
content: TextButton(
child: Text(e.value),
style:
ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))),
onPressed: () => onPressed(list.sublist(0, e.key + 1))))));
return breadCrumbList;
}
@override
bool get wantKeepAlive => true;
}
class BottomSheetBody extends StatelessWidget {
BottomSheetBody(
{required this.leading,
required this.title,
required this.text,
this.onCanceled,
this.actions});
final Widget leading;
final String title;
final String text;
final VoidCallback? onCanceled;
final List<IconButton>? actions;
@override
BottomSheet build(BuildContext context) {
final _actions = actions ?? [];
return BottomSheet(
builder: (BuildContext context) {
return Container(
height: 65,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: MyTheme.accent50,
borderRadius: BorderRadius.vertical(top: Radius.circular(10))),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
leading,
SizedBox(width: 16),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 18)),
Text(text,
style: TextStyle(
fontSize: 14, color: MyTheme.grayBg))
],
)
],
),
Row(children: () {
_actions.add(IconButton(
icon: Icon(Icons.cancel_outlined),
onPressed: onCanceled,
));
return _actions;
}())
],
),
));
},
onClosing: () {},
backgroundColor: MyTheme.grayBg,
enableDrag: false,
);
}
}

View File

@ -0,0 +1,122 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
/// File Transfer for multi tabs
class FileManagerTabPage extends StatefulWidget {
final Map<String, dynamic> params;
const FileManagerTabPage({Key? key, required this.params}) : super(key: key);
@override
State<FileManagerTabPage> createState() => _FileManagerTabPageState(params);
}
class _FileManagerTabPageState extends State<FileManagerTabPage>
with SingleTickerProviderStateMixin {
// refactor List<int> when using multi-tab
// this singleton is only for test
List<String> connectionIds = List.empty(growable: true);
var initialIndex = 0;
_FileManagerTabPageState(Map<String, dynamic> params) {
if (params['id'] != null) {
connectionIds.add(params['id']);
}
}
@override
void initState() {
super.initState();
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print(
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
// for simplify, just replace connectionId
if (call.method == "new_file_transfer") {
setState(() {
final args = jsonDecode(call.arguments);
final id = args['id'];
final indexOf = connectionIds.indexOf(id);
if (indexOf >= 0) {
setState(() {
initialIndex = indexOf;
});
} else {
connectionIds.add(id);
setState(() {
initialIndex = connectionIds.length - 1;
});
}
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTabController(
initialIndex: initialIndex,
length: connectionIds.length,
animationDuration: Duration.zero,
child: Column(
children: [
DesktopTitleBar(
child: TabBar(
isScrollable: true,
labelColor: Colors.white,
physics: NeverScrollableScrollPhysics(),
indicatorColor: Colors.white,
tabs: connectionIds
.map((e) => Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(e),
SizedBox(
width: 4,
),
InkWell(
onTap: () {
onRemoveId(e);
},
child: Icon(
Icons.highlight_remove,
size: 20,
))
],
),
))
.toList()),
),
Expanded(
child: TabBarView(
children: connectionIds
.map((e) => Container(
child: FileManagerPage(
key: ValueKey(e),
id: e))) //RemotePage(key: ValueKey(e), id: e))
.toList()),
)
],
),
),
);
}
void onRemoveId(String id) {
final indexOf = connectionIds.indexOf(id);
if (indexOf == -1) {
return;
}
setState(() {
connectionIds.removeAt(indexOf);
initialIndex = max(0, initialIndex - 1);
});
}
}

View File

@ -31,7 +31,8 @@ class RemotePage extends StatefulWidget {
_RemotePageState createState() => _RemotePageState();
}
class _RemotePageState extends State<RemotePage> with WindowListener {
class _RemotePageState extends State<RemotePage>
with WindowListener, AutomaticKeepAliveClientMixin {
Timer? _interval;
Timer? _timer;
bool _showBar = !isWebDesktop;
@ -234,13 +235,14 @@ class _RemotePageState extends State<RemotePage> with WindowListener {
@override
Widget build(BuildContext context) {
super.build(context);
final pi = Provider.of<FfiModel>(context).pi;
final hideKeyboard = isKeyboardShown() && _showEdit;
final showActionButton = !_showBar || hideKeyboard;
final keyboard = _ffi.ffiModel.permissions['keyboard'] != false;
return WillPopScope(
onWillPop: () async {
onWillPop: () async {
clientClose();
return false;
},
@ -254,28 +256,28 @@ class _RemotePageState extends State<RemotePage> with WindowListener {
child: getRawPointerAndKeyBody(
keyboard,
Scaffold(
// resizeToAvoidBottomInset: true,
// resizeToAvoidBottomInset: true,
floatingActionButton: !showActionButton
? null
: FloatingActionButton(
mini: !hideKeyboard,
child: Icon(hideKeyboard
? Icons.expand_more
: Icons.expand_less),
backgroundColor: MyTheme.accent,
onPressed: () {
setState(() {
if (hideKeyboard) {
_showEdit = false;
_ffi.invokeMethod(
"enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
_physicalFocusNode.requestFocus();
} else {
_showBar = !_showBar;
}
});
}),
mini: !hideKeyboard,
child: Icon(hideKeyboard
? Icons.expand_more
: Icons.expand_less),
backgroundColor: MyTheme.accent,
onPressed: () {
setState(() {
if (hideKeyboard) {
_showEdit = false;
_ffi.invokeMethod(
"enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
_physicalFocusNode.requestFocus();
} else {
_showBar = !_showBar;
}
});
}),
bottomNavigationBar: _showBar && pi.displays.length > 0
? getBottomAppBar(keyboard)
: null,
@ -287,11 +289,11 @@ class _RemotePageState extends State<RemotePage> with WindowListener {
child: isWebDesktop
? getBodyForDesktopWithListener(keyboard)
: SafeArea(
child: Container(
color: MyTheme.canvasColor,
child: _isPhysicalMouse
? getBodyForMobile()
: getBodyForMobileWithGesture())));
child: Container(
color: MyTheme.canvasColor,
child: _isPhysicalMouse
? getBodyForMobile()
: getBodyForMobileWithGesture())));
})
],
))),
@ -916,6 +918,9 @@ class _RemotePageState extends State<RemotePage> with WindowListener {
break;
}
}
@override
bool get wantKeepAlive => true;
}
class ImagePaint extends StatelessWidget {

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/file_manager_tab_page.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:provider/provider.dart';
/// multi-tab file transfer remote screen
class DesktopFileTransferScreen extends StatelessWidget {
final Map<String, dynamic> params;
const DesktopFileTransferScreen({Key? key, required this.params})
: super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: gFFI.ffiModel),
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),
ChangeNotifierProvider.value(value: gFFI.canvasModel),
],
child: MaterialApp(
navigatorKey: globalKey,
debugShowCheckedModeBanner: false,
title: 'RustDesk - File Transfer',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: FileManagerTabPage(
params: params,
),
navigatorObservers: [
// FirebaseAnalyticsObserver(analytics: analytics),
FlutterSmartDialog.observer
],
builder: FlutterSmartDialog.init(
builder: isAndroid
? (_, child) => AccessibilityListener(
child: child,
)
: null)),
);
}
}

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -22,7 +23,7 @@ Future<Null> main(List<String> args) async {
// global FFI, use this **ONLY** for global configuration
// for convenience, use global FFI on mobile platform
// focus on multi-ffi on desktop first
initGlobalFFI();
await initGlobalFFI();
// await Firebase.initializeApp();
if (isAndroid) {
toAndroidChannelInit();
@ -51,6 +52,9 @@ void runRustDeskApp(List<String> args) async {
params: argument,
));
break;
case WindowType.FileTransfer:
runApp(DesktopFileTransferScreen(params: argument));
break;
default:
break;
}
@ -75,7 +79,8 @@ class App extends StatelessWidget {
// final analytics = FirebaseAnalytics.instance;
return MultiProvider(
providers: [
// TODO remove it, only for compile
// global configuration
// use session related FFI when in remote control or file transfer page
ChangeNotifierProvider.value(value: gFFI.ffiModel),
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),

View File

@ -913,7 +913,7 @@ class FFI {
}();
// every instance will bind a stream
}
id = id;
this.id = id;
}
/// Login with [password], choose if the client should [remember] it.
@ -935,6 +935,7 @@ class FFI {
ffiModel.clear();
canvasModel.clear();
resetModifiers();
print("model closed");
}
/// Send **get** command to the Rust core based on [name] and [arg].

View File

@ -33,6 +33,7 @@ class RustDeskMultiWindowManager {
static final instance = RustDeskMultiWindowManager._();
int? _remoteDesktopWindowId;
int? _fileTransferWindowId;
Future<dynamic> new_remote_desktop(String remote_id) async {
final msg =
@ -60,6 +61,31 @@ class RustDeskMultiWindowManager {
}
}
Future<dynamic> new_file_transfer(String remote_id) async {
final msg =
jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id});
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
if (!ids.contains(_fileTransferWindowId)) {
_fileTransferWindowId = null;
}
} on Error {
_fileTransferWindowId = null;
}
if (_fileTransferWindowId == null) {
final fileTransferController = await DesktopMultiWindow.createWindow(msg);
fileTransferController
..setFrame(const Offset(0, 0) & const Size(1280, 720))
..center()
..setTitle("rustdesk - file transfer")
..show();
_fileTransferWindowId = fileTransferController.windowId;
} else {
return call(WindowType.FileTransfer, "new_file_transfer", msg);
}
}
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
int? windowId = findWindowByType(type);
if (windowId == null) {