diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 78d73daee..be415eb80 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -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 { } } } - 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 { PopupMenuItem( child: Text(translate('Remove')), value: 'remove') ] + - (!isAndroid - ? [] - : [ - PopupMenuItem( - child: Text(translate('File transfer')), value: 'file') - ]), + ([ + PopupMenuItem( + child: Text(translate('File transfer')), value: 'file') + ]), elevation: 8, ); if (value == 'remove') { diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart new file mode 100644 index 000000000..162e9d720 --- /dev/null +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -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 createState() => _FileManagerPageState(); +} + +class _FileManagerPageState extends State + 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(tag: 'ft_${widget.id}'); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return ChangeNotifierProvider.value( + value: _ffi.fileModel, + child: Consumer(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( + 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( + 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( + 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 getPathBreadCrumbItems( + void Function() onHome, void Function(List) 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? 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, + ); + } +} diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart new file mode 100644 index 000000000..6c945aede --- /dev/null +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -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 params; + + const FileManagerTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _FileManagerTabPageState(params); +} + +class _FileManagerTabPageState extends State + with SingleTickerProviderStateMixin { + // refactor List when using multi-tab + // this singleton is only for test + List connectionIds = List.empty(growable: true); + var initialIndex = 0; + + _FileManagerTabPageState(Map 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); + }); + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 30e647593..9412e03c5 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -31,7 +31,8 @@ class RemotePage extends StatefulWidget { _RemotePageState createState() => _RemotePageState(); } -class _RemotePageState extends State with WindowListener { +class _RemotePageState extends State + with WindowListener, AutomaticKeepAliveClientMixin { Timer? _interval; Timer? _timer; bool _showBar = !isWebDesktop; @@ -234,13 +235,14 @@ class _RemotePageState extends State with WindowListener { @override Widget build(BuildContext context) { + super.build(context); final pi = Provider.of(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 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 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 with WindowListener { break; } } + + @override + bool get wantKeepAlive => true; } class ImagePaint extends StatelessWidget { diff --git a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart new file mode 100644 index 000000000..06a71981e --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart @@ -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 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)), + ); + } +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2707d9535..898274337 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -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 main(List 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 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), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 85bdc13b7..5c383f774 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -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]. diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 81944e648..5c522f3a5 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -33,6 +33,7 @@ class RustDeskMultiWindowManager { static final instance = RustDeskMultiWindowManager._(); int? _remoteDesktopWindowId; + int? _fileTransferWindowId; Future new_remote_desktop(String remote_id) async { final msg = @@ -60,6 +61,31 @@ class RustDeskMultiWindowManager { } } + Future 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 call(WindowType type, String methodName, dynamic args) async { int? windowId = findWindowByType(type); if (windowId == null) {