mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-11-24 04:12:20 +08:00
Merge branch 'master' of https://github.com/rustdesk/rustdesk
This commit is contained in:
commit
bc7611ae0d
@ -32,10 +32,10 @@ RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTI
|
|||||||
Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow.
|
Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow.
|
||||||
| Location | Vendor | Specification |
|
| Location | Vendor | Specification |
|
||||||
| --------- | ------------- | ------------------ |
|
| --------- | ------------- | ------------------ |
|
||||||
| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM |
|
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
|
||||||
| Singapore | Vultr | 1 VCPU / 1GB RAM |
|
| Singapore | Vultr | 1 vCPU / 1GB RAM |
|
||||||
| Germany | Hetzner | 2 VCPU / 4GB RAM |
|
| Germany | Hetzner | 2 vCPU / 4GB RAM |
|
||||||
| Germany | Codext | 4 VCPU / 8GB RAM |
|
| Germany | Codext | 4 vCPU / 8GB RAM |
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
2
flutter/.gitignore
vendored
2
flutter/.gitignore
vendored
@ -45,7 +45,7 @@ jniLibs
|
|||||||
# flutter rust bridge
|
# flutter rust bridge
|
||||||
|
|
||||||
# Flutter Generated Files
|
# Flutter Generated Files
|
||||||
**/flutter/GeneratedPluginRegistrant.swift
|
**/GeneratedPluginRegistrant.swift
|
||||||
**/flutter/generated_plugin_registrant.cc
|
**/flutter/generated_plugin_registrant.cc
|
||||||
**/flutter/generated_plugin_registrant.h
|
**/flutter/generated_plugin_registrant.h
|
||||||
**/flutter/generated_plugins.cmake
|
**/flutter/generated_plugins.cmake
|
||||||
|
@ -427,7 +427,45 @@ class CustomAlertDialog extends StatelessWidget {
|
|||||||
void msgBox(
|
void msgBox(
|
||||||
String type, String title, String text, OverlayDialogManager dialogManager,
|
String type, String title, String text, OverlayDialogManager dialogManager,
|
||||||
{bool? hasCancel}) {
|
{bool? hasCancel}) {
|
||||||
var wrap = (String text, void Function() onPressed) => ButtonTheme(
|
dialogManager.dismissAll();
|
||||||
|
List<Widget> buttons = [];
|
||||||
|
if (type != "connecting" && type != "success" && !type.contains("nook")) {
|
||||||
|
buttons.insert(
|
||||||
|
0,
|
||||||
|
msgBoxButton(translate('OK'), () {
|
||||||
|
dialogManager.dismissAll();
|
||||||
|
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||||
|
if (!type.contains("custom")) {
|
||||||
|
closeConnection();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
hasCancel ??= !type.contains("error") &&
|
||||||
|
!type.contains("nocancel") &&
|
||||||
|
type != "restarting";
|
||||||
|
if (hasCancel) {
|
||||||
|
buttons.insert(
|
||||||
|
0,
|
||||||
|
msgBoxButton(translate('Cancel'), () {
|
||||||
|
dialogManager.dismissAll();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// TODO: test this button
|
||||||
|
if (type.contains("hasclose")) {
|
||||||
|
buttons.insert(
|
||||||
|
0,
|
||||||
|
msgBoxButton(translate('Close'), () {
|
||||||
|
dialogManager.dismissAll();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||||
|
title: _msgBoxTitle(title),
|
||||||
|
content: Text(translate(text), style: TextStyle(fontSize: 15)),
|
||||||
|
actions: buttons));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget msgBoxButton(String text, void Function() onPressed) {
|
||||||
|
return ButtonTheme(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
//limits the touch area to the button area
|
//limits the touch area to the button area
|
||||||
@ -439,41 +477,16 @@ void msgBox(
|
|||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child:
|
child:
|
||||||
Text(translate(text), style: TextStyle(color: MyTheme.accent))));
|
Text(translate(text), style: TextStyle(color: MyTheme.accent))));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21));
|
||||||
|
|
||||||
|
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
|
||||||
|
Widget content, List<Widget> buttons) {
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
List<Widget> buttons = [];
|
|
||||||
if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) {
|
|
||||||
buttons.insert(
|
|
||||||
0,
|
|
||||||
wrap(translate('OK'), () {
|
|
||||||
dialogManager.dismissAll();
|
|
||||||
closeConnection();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (hasCancel == null) {
|
|
||||||
// hasCancel = type != 'error';
|
|
||||||
hasCancel = type.indexOf("error") < 0 &&
|
|
||||||
type.indexOf("nocancel") < 0 &&
|
|
||||||
type != "restarting";
|
|
||||||
}
|
|
||||||
if (hasCancel) {
|
|
||||||
buttons.insert(
|
|
||||||
0,
|
|
||||||
wrap(translate('Cancel'), () {
|
|
||||||
dialogManager.dismissAll();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
// TODO: test this button
|
|
||||||
if (type.indexOf("hasclose") >= 0) {
|
|
||||||
buttons.insert(
|
|
||||||
0,
|
|
||||||
wrap(translate('Close'), () {
|
|
||||||
dialogManager.dismissAll();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||||
title: Text(translate(title), style: TextStyle(fontSize: 21)),
|
title: _msgBoxTitle(title),
|
||||||
content: Text(translate(text), style: TextStyle(fontSize: 15)),
|
content: content,
|
||||||
actions: buttons));
|
actions: buttons));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,13 +505,13 @@ const G = M * K;
|
|||||||
|
|
||||||
String readableFileSize(double size) {
|
String readableFileSize(double size) {
|
||||||
if (size < K) {
|
if (size < K) {
|
||||||
return size.toStringAsFixed(2) + " B";
|
return "${size.toStringAsFixed(2)} B";
|
||||||
} else if (size < M) {
|
} else if (size < M) {
|
||||||
return (size / K).toStringAsFixed(2) + " KB";
|
return "${(size / K).toStringAsFixed(2)} KB";
|
||||||
} else if (size < G) {
|
} else if (size < G) {
|
||||||
return (size / M).toStringAsFixed(2) + " MB";
|
return "${(size / M).toStringAsFixed(2)} MB";
|
||||||
} else {
|
} else {
|
||||||
return (size / G).toStringAsFixed(2) + " GB";
|
return "${(size / G).toStringAsFixed(2)} GB";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -661,8 +674,6 @@ Future<void> initGlobalFFI() async {
|
|||||||
debugPrint("_globalFFI init end");
|
debugPrint("_globalFFI init end");
|
||||||
// after `put`, can also be globally found by Get.find<FFI>();
|
// after `put`, can also be globally found by Get.find<FFI>();
|
||||||
Get.put(_globalFFI, permanent: true);
|
Get.put(_globalFFI, permanent: true);
|
||||||
// trigger connection status updater
|
|
||||||
await bind.mainCheckConnectStatus();
|
|
||||||
// global shared preference
|
// global shared preference
|
||||||
await Get.putAsync(() => SharedPreferences.getInstance());
|
await Get.putAsync(() => SharedPreferences.getInstance());
|
||||||
}
|
}
|
||||||
|
87
flutter/lib/common/shared_state.dart
Normal file
87
flutter/lib/common/shared_state.dart
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../consts.dart';
|
||||||
|
|
||||||
|
class PrivacyModeState {
|
||||||
|
static String tag(String id) => 'privacy_mode_$id';
|
||||||
|
|
||||||
|
static void init(String id) {
|
||||||
|
final RxBool state = false.obs;
|
||||||
|
Get.put(state, tag: tag(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) => Get.delete(tag: tag(id));
|
||||||
|
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockInputState {
|
||||||
|
static String tag(String id) => 'block_input_$id';
|
||||||
|
|
||||||
|
static void init(String id) {
|
||||||
|
final RxBool state = false.obs;
|
||||||
|
Get.put(state, tag: tag(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) => Get.delete(tag: tag(id));
|
||||||
|
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
class CurrentDisplayState {
|
||||||
|
static String tag(String id) => 'current_display_$id';
|
||||||
|
|
||||||
|
static void init(String id) {
|
||||||
|
final RxInt state = RxInt(0);
|
||||||
|
Get.put(state, tag: tag(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) => Get.delete(tag: tag(id));
|
||||||
|
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionType {
|
||||||
|
final Rx<String> _secure = kInvalidValueStr.obs;
|
||||||
|
final Rx<String> _direct = kInvalidValueStr.obs;
|
||||||
|
|
||||||
|
Rx<String> get secure => _secure;
|
||||||
|
Rx<String> get direct => _direct;
|
||||||
|
|
||||||
|
static String get strSecure => 'secure';
|
||||||
|
static String get strInsecure => 'insecure';
|
||||||
|
static String get strDirect => '';
|
||||||
|
static String get strIndirect => '_relay';
|
||||||
|
|
||||||
|
void setSecure(bool v) {
|
||||||
|
_secure.value = v ? strSecure : strInsecure;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDirect(bool v) {
|
||||||
|
_direct.value = v ? strDirect : strIndirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() {
|
||||||
|
return _secure.value != kInvalidValueStr &&
|
||||||
|
_direct.value != kInvalidValueStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionTypeState {
|
||||||
|
static String tag(String id) => 'connection_type_$id';
|
||||||
|
|
||||||
|
static void init(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (!Get.isRegistered(tag: key)) {
|
||||||
|
final ConnectionType collectionType = ConnectionType();
|
||||||
|
Get.put(collectionType, tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (Get.isRegistered(tag: key)) {
|
||||||
|
Get.delete(tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConnectionType find(String id) =>
|
||||||
|
Get.find<ConnectionType>(tag: tag(id));
|
||||||
|
}
|
@ -4,8 +4,14 @@ const double kDesktopRemoteTabBarHeight = 28.0;
|
|||||||
const String kAppTypeMain = "main";
|
const String kAppTypeMain = "main";
|
||||||
const String kAppTypeDesktopRemote = "remote";
|
const String kAppTypeDesktopRemote = "remote";
|
||||||
const String kAppTypeDesktopFileTransfer = "file transfer";
|
const String kAppTypeDesktopFileTransfer = "file transfer";
|
||||||
|
const String kAppTypeDesktopPortForward = "port forward";
|
||||||
const String kTabLabelHomePage = "Home";
|
const String kTabLabelHomePage = "Home";
|
||||||
const String kTabLabelSettingPage = "Settings";
|
const String kTabLabelSettingPage = "Settings";
|
||||||
|
|
||||||
const int kDefaultDisplayWidth = 1280;
|
const int kMobileDefaultDisplayWidth = 720;
|
||||||
const int kDefaultDisplayHeight = 720;
|
const int kMobileDefaultDisplayHeight = 1280;
|
||||||
|
|
||||||
|
const int kDesktopDefaultDisplayWidth = 1080;
|
||||||
|
const int kDesktopDefaultDisplayHeight = 720;
|
||||||
|
|
||||||
|
const kInvalidValueStr = "InvalidValueStr";
|
||||||
|
@ -33,7 +33,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
final _idController = TextEditingController();
|
final _idController = TextEditingController();
|
||||||
|
|
||||||
/// Update url. If it's not null, means an update is available.
|
/// Update url. If it's not null, means an update is available.
|
||||||
var _updateUrl = '';
|
final _updateUrl = '';
|
||||||
|
|
||||||
Timer? _updateTimer;
|
Timer? _updateTimer;
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
return snapshot.data!;
|
return snapshot.data!;
|
||||||
} else {
|
} else {
|
||||||
return Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@ -110,7 +110,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
/// Callback for the connect button.
|
/// Callback for the connect button.
|
||||||
/// Connects to the selected peer.
|
/// Connects to the selected peer.
|
||||||
void onConnect({bool isFileTransfer = false}) {
|
void onConnect({bool isFileTransfer = false}) {
|
||||||
var id = _idController.text.trim();
|
final id = _idController.text.trim();
|
||||||
connect(id, isFileTransfer: isFileTransfer);
|
connect(id, isFileTransfer: isFileTransfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,9 +120,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
if (id == '') return;
|
if (id == '') return;
|
||||||
id = id.replaceAll(' ', '');
|
id = id.replaceAll(' ', '');
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
await rustDeskWinManager.new_file_transfer(id);
|
await rustDeskWinManager.newFileTransfer(id);
|
||||||
} else {
|
} else {
|
||||||
await rustDeskWinManager.new_remote_desktop(id);
|
await rustDeskWinManager.newRemoteDesktop(id);
|
||||||
}
|
}
|
||||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||||
if (!currentFocus.hasPrimaryFocus) {
|
if (!currentFocus.hasPrimaryFocus) {
|
||||||
@ -233,7 +233,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 72,
|
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ftPressed.value
|
color: ftPressed.value
|
||||||
@ -257,7 +256,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
color: ftPressed.value
|
color: ftPressed.value
|
||||||
? MyTheme.color(context).bg
|
? MyTheme.color(context).bg
|
||||||
: MyTheme.color(context).text),
|
: MyTheme.color(context).text),
|
||||||
),
|
).marginSymmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@ -272,7 +271,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
onTap: onConnect,
|
onTap: onConnect,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 65,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: connPressed.value
|
color: connPressed.value
|
||||||
? MyTheme.accent
|
? MyTheme.accent
|
||||||
@ -289,12 +287,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
translate(
|
translate(
|
||||||
"Connection",
|
"Connect",
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12, color: MyTheme.color(context).bg),
|
fontSize: 12, color: MyTheme.color(context).bg),
|
||||||
),
|
),
|
||||||
),
|
).marginSymmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -3,14 +3,13 @@ import 'dart:convert';
|
|||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../models/model.dart';
|
|
||||||
|
|
||||||
class ConnectionTabPage extends StatefulWidget {
|
class ConnectionTabPage extends StatefulWidget {
|
||||||
final Map<String, dynamic> params;
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
@ -22,26 +21,27 @@ class ConnectionTabPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||||
final tabController = Get.put(DesktopTabController());
|
final tabController = Get.put(DesktopTabController());
|
||||||
static final Rx<String> _fullscreenID = "".obs;
|
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||||
static final IconData selectedIcon = Icons.desktop_windows_sharp;
|
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||||
static final IconData unselectedIcon = Icons.desktop_windows_outlined;
|
|
||||||
|
|
||||||
var connectionMap = RxList<Widget>.empty(growable: true);
|
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||||
|
|
||||||
_ConnectionTabPageState(Map<String, dynamic> params) {
|
_ConnectionTabPageState(Map<String, dynamic> params) {
|
||||||
if (params['id'] != null) {
|
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||||
|
final peerId = params['id'];
|
||||||
|
if (peerId != null) {
|
||||||
|
ConnectionTypeState.init(peerId);
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: params['id'],
|
key: peerId,
|
||||||
label: params['id'],
|
label: peerId,
|
||||||
selectedIcon: selectedIcon,
|
selectedIcon: selectedIcon,
|
||||||
unselectedIcon: unselectedIcon,
|
unselectedIcon: unselectedIcon,
|
||||||
page: RemotePage(
|
page: Obx(() => RemotePage(
|
||||||
key: ValueKey(params['id']),
|
key: ValueKey(peerId),
|
||||||
id: params['id'],
|
id: peerId,
|
||||||
tabBarHeight:
|
tabBarHeight:
|
||||||
_fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight,
|
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
|
||||||
fullscreenID: _fullscreenID,
|
))));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,33 +54,27 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
print(
|
print(
|
||||||
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||||
|
|
||||||
|
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||||
// for simplify, just replace connectionId
|
// for simplify, just replace connectionId
|
||||||
if (call.method == "new_remote_desktop") {
|
if (call.method == "new_remote_desktop") {
|
||||||
final args = jsonDecode(call.arguments);
|
final args = jsonDecode(call.arguments);
|
||||||
final id = args['id'];
|
final id = args['id'];
|
||||||
window_on_top(windowId());
|
window_on_top(windowId());
|
||||||
|
ConnectionTypeState.init(id);
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: id,
|
key: id,
|
||||||
label: id,
|
label: id,
|
||||||
selectedIcon: selectedIcon,
|
selectedIcon: selectedIcon,
|
||||||
unselectedIcon: unselectedIcon,
|
unselectedIcon: unselectedIcon,
|
||||||
page: RemotePage(
|
page: Obx(() => RemotePage(
|
||||||
key: ValueKey(id),
|
key: ValueKey(id),
|
||||||
id: id,
|
id: id,
|
||||||
tabBarHeight: _fullscreenID.value.isNotEmpty
|
tabBarHeight:
|
||||||
? 0
|
fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight,
|
||||||
: kDesktopRemoteTabBarHeight,
|
))));
|
||||||
fullscreenID: _fullscreenID,
|
|
||||||
)));
|
|
||||||
} else if (call.method == "onDestroy") {
|
} else if (call.method == "onDestroy") {
|
||||||
tabController.state.value.tabs.forEach((tab) {
|
tabController.clear();
|
||||||
print("executing onDestroy hook, closing ${tab.label}}");
|
|
||||||
final tag = tab.label;
|
|
||||||
ffi(tag).close().then((_) {
|
|
||||||
Get.delete<FFI>(tag: tag);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Get.back();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -88,36 +82,79 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||||
return SubWindowDragToResizeArea(
|
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||||
windowId: windowId(),
|
return Obx(() => SubWindowDragToResizeArea(
|
||||||
child: Container(
|
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||||
decoration: BoxDecoration(
|
windowId: windowId(),
|
||||||
border: Border.all(color: MyTheme.color(context).border!)),
|
child: Container(
|
||||||
child: Scaffold(
|
decoration: BoxDecoration(
|
||||||
backgroundColor: MyTheme.color(context).bg,
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
body: Obx(() => DesktopTab(
|
child: Scaffold(
|
||||||
controller: tabController,
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
theme: theme,
|
body: Obx(() => DesktopTab(
|
||||||
isMainWindow: false,
|
controller: tabController,
|
||||||
showTabBar: _fullscreenID.value.isEmpty,
|
theme: theme,
|
||||||
tail: AddButton(
|
tabType: DesktopTabType.remoteScreen,
|
||||||
theme: theme,
|
showTabBar: fullscreen.isFalse,
|
||||||
).paddingOnly(left: 10),
|
onClose: () {
|
||||||
pageViewBuilder: (pageView) {
|
tabController.clear();
|
||||||
WindowController.fromWindowId(windowId())
|
},
|
||||||
.setFullscreen(_fullscreenID.value.isNotEmpty);
|
tail: AddButton(
|
||||||
return pageView;
|
theme: theme,
|
||||||
},
|
).paddingOnly(left: 10),
|
||||||
))),
|
pageViewBuilder: (pageView) {
|
||||||
),
|
WindowController.fromWindowId(windowId())
|
||||||
);
|
.setFullscreen(fullscreen.isTrue);
|
||||||
|
return pageView;
|
||||||
|
},
|
||||||
|
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||||
|
final connectionType = ConnectionTypeState.find(key);
|
||||||
|
if (!connectionType.isValid()) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final msgDirect = translate(
|
||||||
|
connectionType.direct.value ==
|
||||||
|
ConnectionType.strDirect
|
||||||
|
? 'Direct Connection'
|
||||||
|
: 'Relay Connection');
|
||||||
|
final msgSecure = translate(
|
||||||
|
connectionType.secure.value ==
|
||||||
|
ConnectionType.strSecure
|
||||||
|
? 'Secure Connection'
|
||||||
|
: 'Insecure Connection');
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
Tooltip(
|
||||||
|
message: '$msgDirect\n$msgSecure',
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/${connectionType.secure.value}${connectionType.direct.value}.png',
|
||||||
|
width: themeConf.iconSize,
|
||||||
|
height: themeConf.iconSize,
|
||||||
|
).paddingOnly(right: 5),
|
||||||
|
),
|
||||||
|
label,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onRemoveId(String id) {
|
void onRemoveId(String id) {
|
||||||
ffi(id).close();
|
if (tabController.state.value.tabs.isEmpty) {
|
||||||
if (tabController.state.value.tabs.length == 0) {
|
WindowController.fromWindowId(windowId()).hide();
|
||||||
WindowController.fromWindowId(windowId()).close();
|
|
||||||
}
|
}
|
||||||
|
ConnectionTypeState.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
int windowId() {
|
int windowId() {
|
||||||
|
@ -806,6 +806,8 @@ Future<bool> loginDialog() async {
|
|||||||
var userNameMsg = "";
|
var userNameMsg = "";
|
||||||
String pass = "";
|
String pass = "";
|
||||||
var passMsg = "";
|
var passMsg = "";
|
||||||
|
var userContontroller = TextEditingController(text: userName);
|
||||||
|
var pwdController = TextEditingController(text: pass);
|
||||||
|
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
var completer = Completer<bool>();
|
var completer = Completer<bool>();
|
||||||
@ -833,13 +835,10 @@ Future<bool> loginDialog() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
userName = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
||||||
controller: TextEditingController(text: userName),
|
controller: userContontroller,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -859,13 +858,10 @@ Future<bool> loginDialog() async {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
onChanged: (s) {
|
|
||||||
pass = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: passMsg.isNotEmpty ? passMsg : null),
|
errorText: passMsg.isNotEmpty ? passMsg : null),
|
||||||
controller: TextEditingController(text: pass),
|
controller: pwdController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -896,8 +892,8 @@ Future<bool> loginDialog() async {
|
|||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
userName = userName;
|
userName = userContontroller.text;
|
||||||
pass = pass;
|
pass = pwdController.text;
|
||||||
if (userName.isEmpty) {
|
if (userName.isEmpty) {
|
||||||
userNameMsg = translate("Username missed");
|
userNameMsg = translate("Username missed");
|
||||||
cancel();
|
cancel();
|
||||||
|
@ -1025,7 +1025,6 @@ class _ComboBox extends StatelessWidget {
|
|||||||
|
|
||||||
void changeServer() async {
|
void changeServer() async {
|
||||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||||
print("${oldOptions}");
|
|
||||||
String idServer = oldOptions['custom-rendezvous-server'] ?? "";
|
String idServer = oldOptions['custom-rendezvous-server'] ?? "";
|
||||||
var idServerMsg = "";
|
var idServerMsg = "";
|
||||||
String relayServer = oldOptions['relay-server'] ?? "";
|
String relayServer = oldOptions['relay-server'] ?? "";
|
||||||
@ -1033,6 +1032,10 @@ void changeServer() async {
|
|||||||
String apiServer = oldOptions['api-server'] ?? "";
|
String apiServer = oldOptions['api-server'] ?? "";
|
||||||
var apiServerMsg = "";
|
var apiServerMsg = "";
|
||||||
var key = oldOptions['key'] ?? "";
|
var key = oldOptions['key'] ?? "";
|
||||||
|
var idController = TextEditingController(text: idServer);
|
||||||
|
var relayController = TextEditingController(text: relayServer);
|
||||||
|
var apiController = TextEditingController(text: apiServer);
|
||||||
|
var keyController = TextEditingController(text: key);
|
||||||
|
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
@ -1057,13 +1060,10 @@ void changeServer() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
idServer = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
||||||
controller: TextEditingController(text: idServer),
|
controller: idController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1082,14 +1082,11 @@ void changeServer() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
relayServer = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText:
|
errorText:
|
||||||
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
||||||
controller: TextEditingController(text: relayServer),
|
controller: relayController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1108,14 +1105,11 @@ void changeServer() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
apiServer = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText:
|
errorText:
|
||||||
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
||||||
controller: TextEditingController(text: apiServer),
|
controller: apiController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1134,13 +1128,10 @@ void changeServer() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
key = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: key),
|
controller: keyController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1171,10 +1162,10 @@ void changeServer() async {
|
|||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
idServer = idServer.trim();
|
idServer = idController.text.trim();
|
||||||
relayServer = relayServer.trim();
|
relayServer = relayController.text.trim();
|
||||||
apiServer = apiServer.trim();
|
apiServer = apiController.text.trim().toLowerCase();
|
||||||
key = key.trim();
|
key = keyController.text.trim();
|
||||||
|
|
||||||
if (idServer.isNotEmpty) {
|
if (idServer.isNotEmpty) {
|
||||||
idServerMsg = translate(
|
idServerMsg = translate(
|
||||||
@ -1230,6 +1221,7 @@ void changeWhiteList() async {
|
|||||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||||
var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(',');
|
var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(',');
|
||||||
var newWhiteListField = newWhiteList.join('\n');
|
var newWhiteListField = newWhiteList.join('\n');
|
||||||
|
var controller = TextEditingController(text: newWhiteListField);
|
||||||
var msg = "";
|
var msg = "";
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
@ -1246,15 +1238,12 @@ void changeWhiteList() async {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
newWhiteListField = s;
|
|
||||||
},
|
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: msg.isEmpty ? null : translate(msg),
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: newWhiteListField),
|
controller: controller,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1277,7 +1266,7 @@ void changeWhiteList() async {
|
|||||||
msg = "";
|
msg = "";
|
||||||
isInProgress = true;
|
isInProgress = true;
|
||||||
});
|
});
|
||||||
newWhiteListField = newWhiteListField.trim();
|
newWhiteListField = controller.text.trim();
|
||||||
var newWhiteList = "";
|
var newWhiteList = "";
|
||||||
if (newWhiteListField.isEmpty) {
|
if (newWhiteListField.isEmpty) {
|
||||||
// pass
|
// pass
|
||||||
@ -1319,6 +1308,9 @@ void changeSocks5Proxy() async {
|
|||||||
username = socks[1];
|
username = socks[1];
|
||||||
password = socks[2];
|
password = socks[2];
|
||||||
}
|
}
|
||||||
|
var proxyController = TextEditingController(text: proxy);
|
||||||
|
var userController = TextEditingController(text: username);
|
||||||
|
var pwdController = TextEditingController(text: password);
|
||||||
|
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
@ -1343,13 +1335,10 @@ void changeSocks5Proxy() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
proxy = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
||||||
controller: TextEditingController(text: proxy),
|
controller: proxyController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1368,13 +1357,10 @@ void changeSocks5Proxy() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
username = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: username),
|
controller: userController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1393,13 +1379,10 @@ void changeSocks5Proxy() async {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (s) {
|
|
||||||
password = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
controller: TextEditingController(text: password),
|
controller: pwdController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1428,9 +1411,9 @@ void changeSocks5Proxy() async {
|
|||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
proxy = proxy.trim();
|
proxy = proxyController.text.trim();
|
||||||
username = username.trim();
|
username = userController.text.trim();
|
||||||
password = password.trim();
|
password = pwdController.text.trim();
|
||||||
|
|
||||||
if (proxy.isNotEmpty) {
|
if (proxy.isNotEmpty) {
|
||||||
proxyMsg =
|
proxyMsg =
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter_hbb/consts.dart';
|
|||||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class DesktopTabPage extends StatefulWidget {
|
class DesktopTabPage extends StatefulWidget {
|
||||||
@ -33,26 +34,29 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final dark = isDarkTheme();
|
final dark = isDarkTheme();
|
||||||
return DragToResizeArea(
|
RxBool fullscreen = false.obs;
|
||||||
child: Container(
|
Get.put(fullscreen, tag: 'fullscreen');
|
||||||
decoration: BoxDecoration(
|
return Obx(() => DragToResizeArea(
|
||||||
border: Border.all(color: MyTheme.color(context).border!)),
|
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||||
child: Scaffold(
|
child: Container(
|
||||||
backgroundColor: MyTheme.color(context).bg,
|
decoration: BoxDecoration(
|
||||||
body: DesktopTab(
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
controller: tabController,
|
child: Scaffold(
|
||||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
isMainWindow: true,
|
body: DesktopTab(
|
||||||
tail: ActionIcon(
|
controller: tabController,
|
||||||
message: 'Settings',
|
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||||
icon: IconFont.menu,
|
tabType: DesktopTabType.main,
|
||||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
tail: ActionIcon(
|
||||||
onTap: onAddSetting,
|
message: 'Settings',
|
||||||
is_close: false,
|
icon: IconFont.menu,
|
||||||
),
|
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||||
)),
|
onTap: onAddSetting,
|
||||||
),
|
is_close: false,
|
||||||
);
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAddSetting() {
|
void onAddSetting() {
|
||||||
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
|
import 'package:flutter_hbb/desktop/pages/file_manager_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
@ -20,12 +19,13 @@ class FileManagerTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||||
final tabController = Get.put(DesktopTabController());
|
DesktopTabController get tabController => Get.find<DesktopTabController>();
|
||||||
|
|
||||||
static final IconData selectedIcon = Icons.file_copy_sharp;
|
static final IconData selectedIcon = Icons.file_copy_sharp;
|
||||||
static final IconData unselectedIcon = Icons.file_copy_outlined;
|
static final IconData unselectedIcon = Icons.file_copy_outlined;
|
||||||
|
|
||||||
_FileManagerTabPageState(Map<String, dynamic> params) {
|
_FileManagerTabPageState(Map<String, dynamic> params) {
|
||||||
|
Get.put(DesktopTabController());
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: params['id'],
|
key: params['id'],
|
||||||
label: params['id'],
|
label: params['id'],
|
||||||
@ -42,7 +42,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
|
|
||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
print(
|
print(
|
||||||
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
"call ${call.method} with args ${call.arguments} from window ${fromWindowId} to ${windowId()}");
|
||||||
// for simplify, just replace connectionId
|
// for simplify, just replace connectionId
|
||||||
if (call.method == "new_file_transfer") {
|
if (call.method == "new_file_transfer") {
|
||||||
final args = jsonDecode(call.arguments);
|
final args = jsonDecode(call.arguments);
|
||||||
@ -55,21 +55,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
unselectedIcon: unselectedIcon,
|
unselectedIcon: unselectedIcon,
|
||||||
page: FileManagerPage(key: ValueKey(id), id: id)));
|
page: FileManagerPage(key: ValueKey(id), id: id)));
|
||||||
} else if (call.method == "onDestroy") {
|
} else if (call.method == "onDestroy") {
|
||||||
tabController.state.value.tabs.forEach((tab) {
|
tabController.clear();
|
||||||
print("executing onDestroy hook, closing ${tab.label}}");
|
|
||||||
final tag = tab.label;
|
|
||||||
ffi(tag).close().then((_) {
|
|
||||||
Get.delete<FFI>(tag: tag);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Get.back();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
final theme =
|
||||||
|
isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light();
|
||||||
return SubWindowDragToResizeArea(
|
return SubWindowDragToResizeArea(
|
||||||
windowId: windowId(),
|
windowId: windowId(),
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -80,7 +74,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
body: DesktopTab(
|
body: DesktopTab(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
isMainWindow: false,
|
tabType: DesktopTabType.fileTransfer,
|
||||||
|
onClose: () {
|
||||||
|
tabController.clear();
|
||||||
|
},
|
||||||
tail: AddButton(
|
tail: AddButton(
|
||||||
theme: theme,
|
theme: theme,
|
||||||
).paddingOnly(left: 10),
|
).paddingOnly(left: 10),
|
||||||
@ -90,9 +87,8 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onRemoveId(String id) {
|
void onRemoveId(String id) {
|
||||||
ffi("ft_$id").close();
|
if (tabController.state.value.tabs.isEmpty) {
|
||||||
if (tabController.state.value.tabs.length == 0) {
|
WindowController.fromWindowId(windowId()).hide();
|
||||||
WindowController.fromWindowId(windowId()).close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
348
flutter/lib/desktop/pages/port_forward_page.dart
Normal file
348
flutter/lib/desktop/pages/port_forward_page.dart
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:wakelock/wakelock.dart';
|
||||||
|
|
||||||
|
const double _kColumn1Width = 30;
|
||||||
|
const double _kColumn4Width = 100;
|
||||||
|
const double _kRowHeight = 50;
|
||||||
|
const double _kTextLeftMargin = 20;
|
||||||
|
|
||||||
|
class _PortForward {
|
||||||
|
int localPort;
|
||||||
|
String remoteHost;
|
||||||
|
int remotePort;
|
||||||
|
|
||||||
|
_PortForward.fromJson(List<dynamic> json)
|
||||||
|
: localPort = json[0] as int,
|
||||||
|
remoteHost = json[1] as String,
|
||||||
|
remotePort = json[2] as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PortForwardPage extends StatefulWidget {
|
||||||
|
const PortForwardPage({Key? key, required this.id, required this.isRDP})
|
||||||
|
: super(key: key);
|
||||||
|
final String id;
|
||||||
|
final bool isRDP;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PortForwardPage> createState() => _PortForwardPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PortForwardPageState extends State<PortForwardPage>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
final bool isRdp = false;
|
||||||
|
final TextEditingController localPortController = TextEditingController();
|
||||||
|
final TextEditingController remoteHostController = TextEditingController();
|
||||||
|
final TextEditingController remotePortController = TextEditingController();
|
||||||
|
RxList<_PortForward> pfs = RxList.empty(growable: true);
|
||||||
|
late FFI _ffi;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ffi = FFI();
|
||||||
|
_ffi.connect(widget.id, isPortForward: true);
|
||||||
|
Get.put(_ffi, tag: 'pf_${widget.id}');
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
Wakelock.enable();
|
||||||
|
}
|
||||||
|
print("init success with id ${widget.id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ffi.close();
|
||||||
|
_ffi.dialogManager.dismissAll();
|
||||||
|
if (!Platform.isLinux) {
|
||||||
|
Wakelock.disable();
|
||||||
|
}
|
||||||
|
Get.delete<FFI>(tag: 'pf_${widget.id}');
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: MyTheme.color(context).grayBg,
|
||||||
|
body: FutureBuilder(future: () async {
|
||||||
|
if (!isRdp) {
|
||||||
|
refreshTunnelConfig();
|
||||||
|
}
|
||||||
|
}(), builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
width: 20, color: MyTheme.color(context).grayBg!)),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
buildPrompt(context),
|
||||||
|
Flexible(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: MyTheme.color(context).bg,
|
||||||
|
border: Border.all(width: 1, color: MyTheme.border)),
|
||||||
|
child:
|
||||||
|
widget.isRDP ? buildRdp(context) : buildTunnel(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const Offstage();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPrompt(BuildContext context) {
|
||||||
|
return Obx(() => Offstage(
|
||||||
|
offstage: pfs.isEmpty && !widget.isRDP,
|
||||||
|
child: Container(
|
||||||
|
height: 45,
|
||||||
|
color: const Color(0xFF007F00),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
translate('Listening ...'),
|
||||||
|
style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
translate('not_close_tcp_tip'),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10, color: Color(0xFFDDDDDD), height: 1.2),
|
||||||
|
)
|
||||||
|
])).marginOnly(bottom: 8),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTunnel(BuildContext context) {
|
||||||
|
text(String lable) => Expanded(
|
||||||
|
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
|
||||||
|
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context)
|
||||||
|
.copyWith(backgroundColor: MyTheme.color(context).bg),
|
||||||
|
child: Obx(() => ListView.builder(
|
||||||
|
itemCount: pfs.length + 2,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return Container(
|
||||||
|
height: 25,
|
||||||
|
color: MyTheme.color(context).grayBg,
|
||||||
|
child: Row(children: [
|
||||||
|
text('Local Port'),
|
||||||
|
const SizedBox(width: _kColumn1Width),
|
||||||
|
text('Remote Host'),
|
||||||
|
text('Remote Port'),
|
||||||
|
SizedBox(
|
||||||
|
width: _kColumn4Width, child: Text(translate('Action')))
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
} else if (index == 1) {
|
||||||
|
return buildTunnelAddRow(context);
|
||||||
|
} else {
|
||||||
|
return buildTunnelDataRow(context, pfs[index - 2], index - 2);
|
||||||
|
}
|
||||||
|
}))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTunnelAddRow(BuildContext context) {
|
||||||
|
var portInputFormatter = [
|
||||||
|
FilteringTextInputFormatter.allow(RegExp(
|
||||||
|
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$'))
|
||||||
|
];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: _kRowHeight,
|
||||||
|
decoration: BoxDecoration(color: MyTheme.color(context).bg),
|
||||||
|
child: Row(children: [
|
||||||
|
buildTunnelInputCell(context,
|
||||||
|
controller: localPortController,
|
||||||
|
inputFormatters: portInputFormatter),
|
||||||
|
const SizedBox(
|
||||||
|
width: _kColumn1Width, child: Icon(Icons.arrow_forward_sharp)),
|
||||||
|
buildTunnelInputCell(context,
|
||||||
|
controller: remoteHostController, hint: 'localhost'),
|
||||||
|
buildTunnelInputCell(context,
|
||||||
|
controller: remotePortController,
|
||||||
|
inputFormatters: portInputFormatter),
|
||||||
|
SizedBox(
|
||||||
|
width: _kColumn4Width,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
elevation: 0, side: const BorderSide(color: MyTheme.border)),
|
||||||
|
onPressed: () async {
|
||||||
|
int? localPort = int.tryParse(localPortController.text);
|
||||||
|
int? remotePort = int.tryParse(remotePortController.text);
|
||||||
|
if (localPort != null &&
|
||||||
|
remotePort != null &&
|
||||||
|
(remoteHostController.text.isEmpty ||
|
||||||
|
remoteHostController.text.trim().isNotEmpty)) {
|
||||||
|
await bind.sessionAddPortForward(
|
||||||
|
id: 'pf_${widget.id}',
|
||||||
|
localPort: localPort,
|
||||||
|
remoteHost: remoteHostController.text.trim().isEmpty
|
||||||
|
? 'localhost'
|
||||||
|
: remoteHostController.text.trim(),
|
||||||
|
remotePort: remotePort);
|
||||||
|
localPortController.clear();
|
||||||
|
remoteHostController.clear();
|
||||||
|
remotePortController.clear();
|
||||||
|
refreshTunnelConfig();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
translate('Add'),
|
||||||
|
),
|
||||||
|
).marginAll(10),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTunnelInputCell(BuildContext context,
|
||||||
|
{required TextEditingController controller,
|
||||||
|
List<TextInputFormatter>? inputFormatters,
|
||||||
|
String? hint}) {
|
||||||
|
return Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
inputFormatters: inputFormatters,
|
||||||
|
cursorColor: MyTheme.color(context).text,
|
||||||
|
cursorHeight: 20,
|
||||||
|
cursorWidth: 1,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: MyTheme.color(context).border!)),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: MyTheme.color(context).border!)),
|
||||||
|
fillColor: MyTheme.color(context).bg,
|
||||||
|
contentPadding: const EdgeInsets.all(10),
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: MyTheme.color(context).placeholder, fontSize: 16)),
|
||||||
|
style: TextStyle(color: MyTheme.color(context).text, fontSize: 16),
|
||||||
|
).marginAll(10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) {
|
||||||
|
text(String lable) => Expanded(
|
||||||
|
child: Text(lable, style: const TextStyle(fontSize: 20))
|
||||||
|
.marginOnly(left: _kTextLeftMargin));
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: _kRowHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: index % 2 == 0
|
||||||
|
? isDarkTheme()
|
||||||
|
? const Color(0xFF202020)
|
||||||
|
: const Color(0xFFF4F5F6)
|
||||||
|
: MyTheme.color(context).bg),
|
||||||
|
child: Row(children: [
|
||||||
|
text(pf.localPort.toString()),
|
||||||
|
const SizedBox(width: _kColumn1Width),
|
||||||
|
text(pf.remoteHost),
|
||||||
|
text(pf.remotePort.toString()),
|
||||||
|
SizedBox(
|
||||||
|
width: _kColumn4Width,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () async {
|
||||||
|
await bind.sessionRemovePortForward(
|
||||||
|
id: 'pf_${widget.id}', localPort: pf.localPort);
|
||||||
|
refreshTunnelConfig();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshTunnelConfig() async {
|
||||||
|
String peer = await bind.mainGetPeer(id: widget.id);
|
||||||
|
Map<String, dynamic> config = jsonDecode(peer);
|
||||||
|
List<dynamic> infos = config['port_forwards'] as List;
|
||||||
|
List<_PortForward> result = List.empty(growable: true);
|
||||||
|
for (var e in infos) {
|
||||||
|
result.add(_PortForward.fromJson(e));
|
||||||
|
}
|
||||||
|
pfs.value = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildRdp(BuildContext context) {
|
||||||
|
text1(String lable) =>
|
||||||
|
Expanded(child: Text(lable).marginOnly(left: _kTextLeftMargin));
|
||||||
|
text2(String lable) => Expanded(
|
||||||
|
child: Text(
|
||||||
|
lable,
|
||||||
|
style: TextStyle(fontSize: 20),
|
||||||
|
).marginOnly(left: _kTextLeftMargin));
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context)
|
||||||
|
.copyWith(backgroundColor: MyTheme.color(context).bg),
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: 2,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return Container(
|
||||||
|
height: 25,
|
||||||
|
color: MyTheme.color(context).grayBg,
|
||||||
|
child: Row(children: [
|
||||||
|
text1('Local Port'),
|
||||||
|
const SizedBox(width: _kColumn1Width),
|
||||||
|
text1('Remote Host'),
|
||||||
|
text1('Remote Port'),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
height: _kRowHeight,
|
||||||
|
decoration: BoxDecoration(color: MyTheme.color(context).bg),
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
elevation: 0,
|
||||||
|
side: const BorderSide(color: MyTheme.border)),
|
||||||
|
onPressed: () {},
|
||||||
|
child: Text(
|
||||||
|
translate('New RDP'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w300, fontSize: 14),
|
||||||
|
),
|
||||||
|
).marginSymmetric(vertical: 10),
|
||||||
|
).marginOnly(left: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: _kColumn1Width,
|
||||||
|
child: Icon(Icons.arrow_forward_sharp)),
|
||||||
|
text2('localhost'),
|
||||||
|
text2('RDP'),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
103
flutter/lib/desktop/pages/port_forward_tab_page.dart
Normal file
103
flutter/lib/desktop/pages/port_forward_tab_page.dart
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/port_forward_page.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class PortForwardTabPage extends StatefulWidget {
|
||||||
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
|
const PortForwardTabPage({Key? key, required this.params}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PortForwardTabPage> createState() => _PortForwardTabPageState(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||||
|
final tabController = Get.put(DesktopTabController());
|
||||||
|
late final bool isRDP;
|
||||||
|
|
||||||
|
static const IconData selectedIcon = Icons.forward_sharp;
|
||||||
|
static const IconData unselectedIcon = Icons.forward_outlined;
|
||||||
|
|
||||||
|
_PortForwardTabPageState(Map<String, dynamic> params) {
|
||||||
|
isRDP = params['isRDP'];
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: params['id'],
|
||||||
|
label: params['id'],
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon,
|
||||||
|
page: PortForwardPage(
|
||||||
|
key: ValueKey(params['id']),
|
||||||
|
id: params['id'],
|
||||||
|
isRDP: isRDP,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
tabController.onRemove = (_, id) => onRemoveId(id);
|
||||||
|
|
||||||
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
|
debugPrint(
|
||||||
|
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||||
|
// for simplify, just replace connectionId
|
||||||
|
if (call.method == "new_port_forward") {
|
||||||
|
final args = jsonDecode(call.arguments);
|
||||||
|
final id = args['id'];
|
||||||
|
final isRDP = args['isRDP'];
|
||||||
|
window_on_top(windowId());
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: id,
|
||||||
|
label: id,
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon,
|
||||||
|
page: PortForwardPage(id: id, isRDP: isRDP)));
|
||||||
|
} else if (call.method == "onDestroy") {
|
||||||
|
tabController.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||||
|
return SubWindowDragToResizeArea(
|
||||||
|
windowId: windowId(),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
|
body: DesktopTab(
|
||||||
|
controller: tabController,
|
||||||
|
theme: theme,
|
||||||
|
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward,
|
||||||
|
onClose: () {
|
||||||
|
tabController.clear();
|
||||||
|
},
|
||||||
|
tail: AddButton(
|
||||||
|
theme: theme,
|
||||||
|
).paddingOnly(left: 10),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRemoveId(String id) {
|
||||||
|
ffi("pf_$id").close();
|
||||||
|
if (tabController.state.value.tabs.isEmpty) {
|
||||||
|
WindowController.fromWindowId(windowId()).hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int windowId() {
|
||||||
|
return widget.params["windowId"];
|
||||||
|
}
|
||||||
|
}
|
@ -5,32 +5,32 @@ import 'dart:ui' as ui;
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock/wakelock.dart';
|
import 'package:wakelock/wakelock.dart';
|
||||||
|
|
||||||
// import 'package:window_manager/window_manager.dart';
|
// import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
import '../widgets/remote_menubar.dart';
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../mobile/widgets/dialog.dart';
|
import '../../mobile/widgets/dialog.dart';
|
||||||
import '../../mobile/widgets/overlay.dart';
|
import '../../mobile/widgets/overlay.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/chat_model.dart';
|
||||||
|
import '../../common/shared_state.dart';
|
||||||
|
|
||||||
final initText = '\1' * 1024;
|
final initText = '\1' * 1024;
|
||||||
|
|
||||||
class RemotePage extends StatefulWidget {
|
class RemotePage extends StatefulWidget {
|
||||||
RemotePage(
|
RemotePage({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.tabBarHeight,
|
required this.tabBarHeight,
|
||||||
required this.fullscreenID})
|
}) : super(key: key);
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final double tabBarHeight;
|
final double tabBarHeight;
|
||||||
final Rx<String> fullscreenID;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_RemotePageState createState() => _RemotePageState();
|
_RemotePageState createState() => _RemotePageState();
|
||||||
@ -41,7 +41,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
bool _showBar = !isWebDesktop;
|
bool _showBar = !isWebDesktop;
|
||||||
String _value = '';
|
String _value = '';
|
||||||
var _cursorOverImage = false.obs;
|
final _cursorOverImage = false.obs;
|
||||||
|
|
||||||
final FocusNode _mobileFocusNode = FocusNode();
|
final FocusNode _mobileFocusNode = FocusNode();
|
||||||
final FocusNode _physicalFocusNode = FocusNode();
|
final FocusNode _physicalFocusNode = FocusNode();
|
||||||
@ -50,11 +50,27 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
|
|
||||||
|
void _updateTabBarHeight() {
|
||||||
|
_ffi.canvasModel.tabBarHeight = widget.tabBarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initStates(String id) {
|
||||||
|
PrivacyModeState.init(id);
|
||||||
|
BlockInputState.init(id);
|
||||||
|
CurrentDisplayState.init(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeStates(String id) {
|
||||||
|
PrivacyModeState.delete(id);
|
||||||
|
BlockInputState.delete(id);
|
||||||
|
CurrentDisplayState.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_ffi = FFI();
|
_ffi = FFI();
|
||||||
_ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight;
|
_updateTabBarHeight();
|
||||||
Get.put(_ffi, tag: widget.id);
|
Get.put(_ffi, tag: widget.id);
|
||||||
_ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight);
|
_ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@ -70,11 +86,12 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.listenToMouse(true);
|
_ffi.listenToMouse(true);
|
||||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||||
// WindowManager.instance.addListener(this);
|
// WindowManager.instance.addListener(this);
|
||||||
|
_initStates(widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
print("REMOTE PAGE dispose ${widget.id}");
|
debugPrint("REMOTE PAGE dispose ${widget.id}");
|
||||||
hideMobileActionsOverlay();
|
hideMobileActionsOverlay();
|
||||||
_ffi.listenToMouse(false);
|
_ffi.listenToMouse(false);
|
||||||
_mobileFocusNode.dispose();
|
_mobileFocusNode.dispose();
|
||||||
@ -90,6 +107,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// WindowManager.instance.removeListener(this);
|
// WindowManager.instance.removeListener(this);
|
||||||
Get.delete<FFI>(tag: widget.id);
|
Get.delete<FFI>(tag: widget.id);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
_removeStates(widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetTool() {
|
void resetTool() {
|
||||||
@ -187,19 +205,19 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: MyTheme.color(context).bg,
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
// resizeToAvoidBottomInset: true,
|
// resizeToAvoidBottomInset: true,
|
||||||
floatingActionButton: _showBar
|
// floatingActionButton: _showBar
|
||||||
? null
|
// ? null
|
||||||
: FloatingActionButton(
|
// : FloatingActionButton(
|
||||||
mini: true,
|
// mini: true,
|
||||||
child: Icon(Icons.expand_less),
|
// child: Icon(Icons.expand_less),
|
||||||
backgroundColor: MyTheme.accent,
|
// backgroundColor: MyTheme.accent,
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
setState(() {
|
// setState(() {
|
||||||
_showBar = !_showBar;
|
// _showBar = !_showBar;
|
||||||
});
|
// });
|
||||||
}),
|
// }),
|
||||||
bottomNavigationBar:
|
// bottomNavigationBar:
|
||||||
_showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
|
// _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
|
||||||
body: Overlay(
|
body: Overlay(
|
||||||
initialEntries: [
|
initialEntries: [
|
||||||
OverlayEntry(builder: (context) {
|
OverlayEntry(builder: (context) {
|
||||||
@ -217,6 +235,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
_updateTabBarHeight();
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
clientClose(_ffi.dialogManager);
|
clientClose(_ffi.dialogManager);
|
||||||
@ -337,6 +356,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget? getBottomAppBar(FfiModel ffiModel) {
|
Widget? getBottomAppBar(FfiModel ffiModel) {
|
||||||
|
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
cursor: SystemMouseCursors.basic,
|
cursor: SystemMouseCursors.basic,
|
||||||
child: BottomAppBar(
|
child: BottomAppBar(
|
||||||
@ -371,15 +391,11 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
: <Widget>[
|
: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(widget.fullscreenID.value.isEmpty
|
icon: Icon(fullscreen.isTrue
|
||||||
? Icons.fullscreen
|
? Icons.fullscreen
|
||||||
: Icons.close_fullscreen),
|
: Icons.close_fullscreen),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (widget.fullscreenID.value.isEmpty) {
|
fullscreen.value = !fullscreen.value;
|
||||||
widget.fullscreenID.value = widget.id;
|
|
||||||
} else {
|
|
||||||
widget.fullscreenID.value = "";
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]) +
|
]) +
|
||||||
@ -452,7 +468,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
if (_isPhysicalMouse) {
|
if (_isPhysicalMouse) {
|
||||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||||
tabBarHeight: super.widget.tabBarHeight);
|
tabBarHeight: widget.tabBarHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,7 +482,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
if (_isPhysicalMouse) {
|
if (_isPhysicalMouse) {
|
||||||
_ffi.handleMouse(getEvent(e, 'mousedown'),
|
_ffi.handleMouse(getEvent(e, 'mousedown'),
|
||||||
tabBarHeight: super.widget.tabBarHeight);
|
tabBarHeight: widget.tabBarHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -474,7 +490,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
if (_isPhysicalMouse) {
|
if (_isPhysicalMouse) {
|
||||||
_ffi.handleMouse(getEvent(e, 'mouseup'),
|
_ffi.handleMouse(getEvent(e, 'mouseup'),
|
||||||
tabBarHeight: super.widget.tabBarHeight);
|
tabBarHeight: widget.tabBarHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -482,7 +498,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
if (_isPhysicalMouse) {
|
if (_isPhysicalMouse) {
|
||||||
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
_ffi.handleMouse(getEvent(e, 'mousemove'),
|
||||||
tabBarHeight: super.widget.tabBarHeight);
|
tabBarHeight: widget.tabBarHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,6 +564,10 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
|
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
|
||||||
|
paints.add(RemoteMenubar(
|
||||||
|
id: widget.id,
|
||||||
|
ffi: _ffi,
|
||||||
|
));
|
||||||
return Stack(
|
return Stack(
|
||||||
children: paints,
|
children: paints,
|
||||||
);
|
);
|
||||||
@ -717,11 +737,11 @@ class ImagePaint extends StatelessWidget {
|
|||||||
width: c.getDisplayWidth() * s,
|
width: c.getDisplayWidth() * s,
|
||||||
height: c.getDisplayHeight() * s,
|
height: c.getDisplayHeight() * s,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: new ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||||
));
|
));
|
||||||
return Center(
|
return Center(
|
||||||
child: NotificationListener<ScrollNotification>(
|
child: NotificationListener<ScrollNotification>(
|
||||||
onNotification: (_notification) {
|
onNotification: (notification) {
|
||||||
final percentX = _horizontal.position.extentBefore /
|
final percentX = _horizontal.position.extentBefore /
|
||||||
(_horizontal.position.extentBefore +
|
(_horizontal.position.extentBefore +
|
||||||
_horizontal.position.extentInside +
|
_horizontal.position.extentInside +
|
||||||
@ -744,8 +764,8 @@ class ImagePaint extends StatelessWidget {
|
|||||||
width: c.size.width,
|
width: c.size.width,
|
||||||
height: c.size.height,
|
height: c.size.height,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: new ImagePainter(
|
painter:
|
||||||
image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
||||||
));
|
));
|
||||||
return _buildListener(imageWidget);
|
return _buildListener(imageWidget);
|
||||||
}
|
}
|
||||||
@ -799,7 +819,7 @@ class CursorPaint extends StatelessWidget {
|
|||||||
// final adjust = m.adjustForKeyboard();
|
// final adjust = m.adjustForKeyboard();
|
||||||
var s = c.scale;
|
var s = c.scale;
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: new ImagePainter(
|
painter: ImagePainter(
|
||||||
image: m.image,
|
image: m.image,
|
||||||
x: m.x * s - m.hotx + c.x,
|
x: m.x * s - m.hotx + c.x,
|
||||||
y: m.y * s - m.hoty + c.y,
|
y: m.y * s - m.hoty + c.y,
|
||||||
@ -824,15 +844,16 @@ class ImagePainter extends CustomPainter {
|
|||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
if (x.isNaN || y.isNaN) return;
|
||||||
canvas.scale(scale, scale);
|
canvas.scale(scale, scale);
|
||||||
// https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
|
// https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
|
||||||
// https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
|
// https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
|
||||||
var paint = new Paint();
|
var paint = Paint();
|
||||||
paint.filterQuality = FilterQuality.medium;
|
paint.filterQuality = FilterQuality.medium;
|
||||||
if (scale > 10.00000) {
|
if (scale > 10.00000) {
|
||||||
paint.filterQuality = FilterQuality.high;
|
paint.filterQuality = FilterQuality.high;
|
||||||
}
|
}
|
||||||
canvas.drawImage(image!, new Offset(x, y), paint);
|
canvas.drawImage(image!, Offset(x, y), paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -111,7 +111,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
showMaximize: false,
|
showMaximize: false,
|
||||||
showMinimize: false,
|
showMinimize: false,
|
||||||
controller: serverModel.tabController,
|
controller: serverModel.tabController,
|
||||||
isMainWindow: true,
|
tabType: DesktopTabType.cm,
|
||||||
pageViewBuilder: (pageView) => Row(children: [
|
pageViewBuilder: (pageView) => Row(children: [
|
||||||
Expanded(child: pageView),
|
Expanded(child: pageView),
|
||||||
Consumer<ChatModel>(
|
Consumer<ChatModel>(
|
||||||
@ -294,7 +294,8 @@ class _CmHeaderState extends State<_CmHeader>
|
|||||||
Offstage(
|
Offstage(
|
||||||
offstage: client.isFileTransfer,
|
offstage: client.isFileTransfer,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id),
|
onPressed: () => checkClickTime(
|
||||||
|
client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)),
|
||||||
icon: Icon(Icons.message_outlined),
|
icon: Icon(Icons.message_outlined),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -326,7 +327,8 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|||||||
BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey),
|
BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey),
|
||||||
padding: EdgeInsets.all(4.0),
|
padding: EdgeInsets.all(4.0),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => onTap?.call(!enabled),
|
onTap: () =>
|
||||||
|
checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
|
||||||
child: Image(
|
child: Image(
|
||||||
image: icon,
|
image: icon,
|
||||||
width: 50,
|
width: 50,
|
||||||
@ -422,7 +424,8 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.redAccent, borderRadius: BorderRadius.circular(10)),
|
color: Colors.redAccent, borderRadius: BorderRadius.circular(10)),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => handleDisconnect(context),
|
onTap: () =>
|
||||||
|
checkClickTime(client.id, () => handleDisconnect(context)),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -447,7 +450,8 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: MyTheme.accent, borderRadius: BorderRadius.circular(10)),
|
color: MyTheme.accent, borderRadius: BorderRadius.circular(10)),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => handleAccept(context),
|
onTap: () =>
|
||||||
|
checkClickTime(client.id, () => handleAccept(context)),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -469,7 +473,8 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: Colors.grey)),
|
border: Border.all(color: Colors.grey)),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => handleDisconnect(context),
|
onTap: () =>
|
||||||
|
checkClickTime(client.id, () => handleDisconnect(context)),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -572,3 +577,12 @@ Widget clientInfo(Client client) {
|
|||||||
),
|
),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void checkClickTime(int id, Function() callback) async {
|
||||||
|
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
await bind.cmCheckClickTime(connId: id);
|
||||||
|
Timer(const Duration(milliseconds: 120), () async {
|
||||||
|
var d = clickCallbackTime - await bind.cmGetClickTime();
|
||||||
|
if (d > 120) callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
26
flutter/lib/desktop/screen/desktop_port_forward_screen.dart
Normal file
26
flutter/lib/desktop/screen/desktop_port_forward_screen.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/port_forward_tab_page.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// multi-tab file port forward screen
|
||||||
|
class DesktopPortForwardScreen extends StatelessWidget {
|
||||||
|
final Map<String, dynamic> params;
|
||||||
|
|
||||||
|
const DesktopPortForwardScreen({Key? key, required this.params})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||||
|
],
|
||||||
|
child: Scaffold(
|
||||||
|
body: PortForwardTabPage(
|
||||||
|
params: params,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart';
|
import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
/// multi-tab desktop remote screen
|
/// multi-tab desktop remote screen
|
||||||
@ -11,6 +12,8 @@ class DesktopRemoteScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
RxBool fullscreen = false.obs;
|
||||||
|
Get.put(fullscreen, tag: 'fullscreen');
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||||
|
1321
flutter/lib/desktop/widgets/material_mod_popup_menu.dart
Normal file
1321
flutter/lib/desktop/widgets/material_mod_popup_menu.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -21,18 +21,16 @@ final peerSearchTextController =
|
|||||||
TextEditingController(text: peerSearchText.value);
|
TextEditingController(text: peerSearchText.value);
|
||||||
|
|
||||||
class _PeerWidget extends StatefulWidget {
|
class _PeerWidget extends StatefulWidget {
|
||||||
late final _peers;
|
final Peers peers;
|
||||||
late final OffstageFunc _offstageFunc;
|
final OffstageFunc offstageFunc;
|
||||||
late final PeerCardWidgetFunc _peerCardWidgetFunc;
|
final PeerCardWidgetFunc peerCardWidgetFunc;
|
||||||
|
|
||||||
_PeerWidget(Peers peers, OffstageFunc offstageFunc,
|
const _PeerWidget(
|
||||||
PeerCardWidgetFunc peerCardWidgetFunc,
|
{required this.peers,
|
||||||
{Key? key})
|
required this.offstageFunc,
|
||||||
: super(key: key) {
|
required this.peerCardWidgetFunc,
|
||||||
_peers = peers;
|
Key? key})
|
||||||
_offstageFunc = offstageFunc;
|
: super(key: key);
|
||||||
_peerCardWidgetFunc = peerCardWidgetFunc;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PeerWidgetState createState() => _PeerWidgetState();
|
_PeerWidgetState createState() => _PeerWidgetState();
|
||||||
@ -42,9 +40,9 @@ class _PeerWidget extends StatefulWidget {
|
|||||||
class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
|
class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
|
||||||
static const int _maxQueryCount = 3;
|
static const int _maxQueryCount = 3;
|
||||||
|
|
||||||
var _curPeers = Set<String>();
|
final _curPeers = <String>{};
|
||||||
var _lastChangeTime = DateTime.now();
|
var _lastChangeTime = DateTime.now();
|
||||||
var _lastQueryPeers = Set<String>();
|
var _lastQueryPeers = <String>{};
|
||||||
var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1));
|
var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1));
|
||||||
var _queryCoun = 0;
|
var _queryCoun = 0;
|
||||||
var _exit = false;
|
var _exit = false;
|
||||||
@ -78,65 +76,62 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final space = 12.0;
|
const space = 12.0;
|
||||||
return ChangeNotifierProvider<Peers>(
|
return ChangeNotifierProvider<Peers>(
|
||||||
create: (context) => super.widget._peers,
|
create: (context) => widget.peers,
|
||||||
child: Consumer<Peers>(
|
child: Consumer<Peers>(
|
||||||
builder: (context, peers, child) => peers.peers.isEmpty
|
builder: (context, peers, child) => peers.peers.isEmpty
|
||||||
? Center(
|
? Center(
|
||||||
child: Text(translate("Empty")),
|
child: Text(translate("Empty")),
|
||||||
)
|
)
|
||||||
: SingleChildScrollView(
|
: SingleChildScrollView(
|
||||||
child: ObxValue<RxString>((searchText) {
|
child: ObxValue<RxString>((searchText) {
|
||||||
return FutureBuilder<List<Peer>>(
|
return FutureBuilder<List<Peer>>(
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
final peers = snapshot.data!;
|
final peers = snapshot.data!;
|
||||||
final cards = <Widget>[];
|
final cards = <Widget>[];
|
||||||
for (final peer in peers) {
|
for (final peer in peers) {
|
||||||
cards.add(Offstage(
|
cards.add(Offstage(
|
||||||
key: ValueKey("off${peer.id}"),
|
key: ValueKey("off${peer.id}"),
|
||||||
offstage: super.widget._offstageFunc(peer),
|
offstage: widget.offstageFunc(peer),
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => SizedBox(
|
() => SizedBox(
|
||||||
width: 220,
|
width: 220,
|
||||||
height:
|
height:
|
||||||
peerCardUiType.value == PeerUiType.grid
|
peerCardUiType.value == PeerUiType.grid
|
||||||
? 140
|
? 140
|
||||||
: 42,
|
: 42,
|
||||||
child: VisibilityDetector(
|
child: VisibilityDetector(
|
||||||
key: ValueKey(peer.id),
|
key: ValueKey(peer.id),
|
||||||
onVisibilityChanged: (info) {
|
onVisibilityChanged: (info) {
|
||||||
final peerId =
|
final peerId =
|
||||||
(info.key as ValueKey).value;
|
(info.key as ValueKey).value;
|
||||||
if (info.visibleFraction > 0.00001) {
|
if (info.visibleFraction > 0.00001) {
|
||||||
_curPeers.add(peerId);
|
_curPeers.add(peerId);
|
||||||
} else {
|
} else {
|
||||||
_curPeers.remove(peerId);
|
_curPeers.remove(peerId);
|
||||||
}
|
}
|
||||||
_lastChangeTime = DateTime.now();
|
_lastChangeTime = DateTime.now();
|
||||||
},
|
},
|
||||||
child: super
|
child: widget.peerCardWidgetFunc(peer),
|
||||||
.widget
|
|
||||||
._peerCardWidgetFunc(peer),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)));
|
),
|
||||||
}
|
)));
|
||||||
return Wrap(
|
|
||||||
spacing: space,
|
|
||||||
runSpacing: space,
|
|
||||||
children: cards);
|
|
||||||
} else {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
return Wrap(
|
||||||
future: matchPeers(searchText.value, peers.peers),
|
spacing: space, runSpacing: space, children: cards);
|
||||||
);
|
} else {
|
||||||
}, peerSearchText),
|
return const Center(
|
||||||
)),
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
future: matchPeers(searchText.value, peers.peers),
|
||||||
|
);
|
||||||
|
}, peerSearchText),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,31 +170,42 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class BasePeerWidget extends StatelessWidget {
|
abstract class BasePeerWidget extends StatelessWidget {
|
||||||
late final _name;
|
final String name;
|
||||||
late final _loadEvent;
|
final String loadEvent;
|
||||||
late final OffstageFunc _offstageFunc;
|
final OffstageFunc offstageFunc;
|
||||||
late final PeerCardWidgetFunc _peerCardWidgetFunc;
|
final PeerCardWidgetFunc peerCardWidgetFunc;
|
||||||
late final List<Peer> _initPeers;
|
final List<Peer> initPeers;
|
||||||
|
|
||||||
BasePeerWidget({Key? key}) : super(key: key) {}
|
const BasePeerWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.name,
|
||||||
|
required this.loadEvent,
|
||||||
|
required this.offstageFunc,
|
||||||
|
required this.peerCardWidgetFunc,
|
||||||
|
required this.initPeers,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc,
|
return _PeerWidget(
|
||||||
_peerCardWidgetFunc);
|
peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers),
|
||||||
|
offstageFunc: offstageFunc,
|
||||||
|
peerCardWidgetFunc: peerCardWidgetFunc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RecentPeerWidget extends BasePeerWidget {
|
class RecentPeerWidget extends BasePeerWidget {
|
||||||
RecentPeerWidget({Key? key}) : super(key: key) {
|
RecentPeerWidget({Key? key})
|
||||||
super._name = "recent peer";
|
: super(
|
||||||
super._loadEvent = "load_recent_peers";
|
key: key,
|
||||||
super._offstageFunc = (Peer _peer) => false;
|
name: 'recent peer',
|
||||||
super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard(
|
loadEvent: 'load_recent_peers',
|
||||||
peer: peer,
|
offstageFunc: (Peer peer) => false,
|
||||||
|
peerCardWidgetFunc: (Peer peer) => RecentPeerCard(
|
||||||
|
peer: peer,
|
||||||
|
),
|
||||||
|
initPeers: [],
|
||||||
);
|
);
|
||||||
super._initPeers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -210,13 +216,17 @@ class RecentPeerWidget extends BasePeerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FavoritePeerWidget extends BasePeerWidget {
|
class FavoritePeerWidget extends BasePeerWidget {
|
||||||
FavoritePeerWidget({Key? key}) : super(key: key) {
|
FavoritePeerWidget({Key? key})
|
||||||
super._name = "favorite peer";
|
: super(
|
||||||
super._loadEvent = "load_fav_peers";
|
key: key,
|
||||||
super._offstageFunc = (Peer _peer) => false;
|
name: 'favorite peer',
|
||||||
super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer);
|
loadEvent: 'load_fav_peers',
|
||||||
super._initPeers = [];
|
offstageFunc: (Peer peer) => false,
|
||||||
}
|
peerCardWidgetFunc: (Peer peer) => FavoritePeerCard(
|
||||||
|
peer: peer,
|
||||||
|
),
|
||||||
|
initPeers: [],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -227,13 +237,17 @@ class FavoritePeerWidget extends BasePeerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DiscoveredPeerWidget extends BasePeerWidget {
|
class DiscoveredPeerWidget extends BasePeerWidget {
|
||||||
DiscoveredPeerWidget({Key? key}) : super(key: key) {
|
DiscoveredPeerWidget({Key? key})
|
||||||
super._name = "discovered peer";
|
: super(
|
||||||
super._loadEvent = "load_lan_peers";
|
key: key,
|
||||||
super._offstageFunc = (Peer _peer) => false;
|
name: 'discovered peer',
|
||||||
super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer);
|
loadEvent: 'load_lan_peers',
|
||||||
super._initPeers = [];
|
offstageFunc: (Peer peer) => false,
|
||||||
}
|
peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard(
|
||||||
|
peer: peer,
|
||||||
|
),
|
||||||
|
initPeers: [],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -244,21 +258,26 @@ class DiscoveredPeerWidget extends BasePeerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AddressBookPeerWidget extends BasePeerWidget {
|
class AddressBookPeerWidget extends BasePeerWidget {
|
||||||
AddressBookPeerWidget({Key? key}) : super(key: key) {
|
AddressBookPeerWidget({Key? key})
|
||||||
super._name = "address book peer";
|
: super(
|
||||||
super._offstageFunc =
|
key: key,
|
||||||
(Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags);
|
name: 'address book peer',
|
||||||
super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer);
|
loadEvent: 'load_address_book_peers',
|
||||||
super._initPeers = _loadPeers();
|
offstageFunc: (Peer peer) =>
|
||||||
}
|
!_hitTag(gFFI.abModel.selectedTags, peer.tags),
|
||||||
|
peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard(
|
||||||
|
peer: peer,
|
||||||
|
),
|
||||||
|
initPeers: _loadPeers(),
|
||||||
|
);
|
||||||
|
|
||||||
List<Peer> _loadPeers() {
|
static List<Peer> _loadPeers() {
|
||||||
return gFFI.abModel.peers.map((e) {
|
return gFFI.abModel.peers.map((e) {
|
||||||
return Peer.fromJson(e['id'], e);
|
return Peer.fromJson(e['id'], e);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
static bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
|
||||||
if (selectedTags.isEmpty) {
|
if (selectedTags.isEmpty) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
509
flutter/lib/desktop/widgets/popup_menu.dart
Normal file
509
flutter/lib/desktop/widgets/popup_menu.dart
Normal file
@ -0,0 +1,509 @@
|
|||||||
|
import 'dart:core';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import './material_mod_popup_menu.dart' as mod_menu;
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu
|
||||||
|
class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||||
|
const PopupMenuChildrenItem({
|
||||||
|
key,
|
||||||
|
this.height = kMinInteractiveDimension,
|
||||||
|
this.padding,
|
||||||
|
this.enable = true,
|
||||||
|
this.textStyle,
|
||||||
|
this.onTap,
|
||||||
|
this.position = mod_menu.PopupMenuPosition.overSide,
|
||||||
|
this.offset = Offset.zero,
|
||||||
|
required this.itemBuilder,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final mod_menu.PopupMenuPosition position;
|
||||||
|
final Offset offset;
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final bool enable;
|
||||||
|
final void Function()? onTap;
|
||||||
|
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool represents(T? value) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
||||||
|
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||||
|
extends State<W> {
|
||||||
|
@protected
|
||||||
|
void handleTap(T value) {
|
||||||
|
widget.onTap?.call();
|
||||||
|
Navigator.pop<T>(context, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
|
TextStyle style = widget.textStyle ??
|
||||||
|
popupMenuTheme.textStyle ??
|
||||||
|
theme.textTheme.subtitle1!;
|
||||||
|
|
||||||
|
return mod_menu.PopupMenuButton<T>(
|
||||||
|
enabled: widget.enable,
|
||||||
|
position: widget.position,
|
||||||
|
offset: widget.offset,
|
||||||
|
onSelected: handleTap,
|
||||||
|
itemBuilder: widget.itemBuilder,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: AnimatedDefaultTextStyle(
|
||||||
|
style: style,
|
||||||
|
duration: kThemeChangeDuration,
|
||||||
|
child: Container(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
constraints: BoxConstraints(minHeight: widget.height),
|
||||||
|
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuConfig {
|
||||||
|
// adapt to the screen height
|
||||||
|
static const fontSize = 14.0;
|
||||||
|
static const midPadding = 10.0;
|
||||||
|
static const iconScale = 0.8;
|
||||||
|
static const iconWidth = 12.0;
|
||||||
|
static const iconHeight = 12.0;
|
||||||
|
|
||||||
|
final double height;
|
||||||
|
final double dividerHeight;
|
||||||
|
final Color commonColor;
|
||||||
|
|
||||||
|
const MenuConfig(
|
||||||
|
{required this.commonColor,
|
||||||
|
this.height = kMinInteractiveDimension,
|
||||||
|
this.dividerHeight = 16.0});
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class MenuEntryBase<T> {
|
||||||
|
bool dismissOnClicked;
|
||||||
|
|
||||||
|
MenuEntryBase({this.dismissOnClicked = false});
|
||||||
|
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuEntryDivider<T> extends MenuEntryBase<T> {
|
||||||
|
@override
|
||||||
|
List<mod_menu.PopupMenuEntry<T>> build(
|
||||||
|
BuildContext context, MenuConfig conf) {
|
||||||
|
return [
|
||||||
|
mod_menu.PopupMenuDivider(
|
||||||
|
height: conf.dividerHeight,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuEntryRadioOption {
|
||||||
|
String text;
|
||||||
|
String value;
|
||||||
|
bool dismissOnClicked;
|
||||||
|
|
||||||
|
MenuEntryRadioOption(
|
||||||
|
{required this.text, required this.value, this.dismissOnClicked = false});
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef RadioOptionsGetter = List<MenuEntryRadioOption> Function();
|
||||||
|
typedef RadioCurOptionGetter = Future<String> Function();
|
||||||
|
typedef RadioOptionSetter = Future<void> Function(
|
||||||
|
String oldValue, String newValue);
|
||||||
|
|
||||||
|
class MenuEntryRadioUtils<T> {}
|
||||||
|
|
||||||
|
class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
||||||
|
final String text;
|
||||||
|
final RadioOptionsGetter optionsGetter;
|
||||||
|
final RadioCurOptionGetter curOptionGetter;
|
||||||
|
final RadioOptionSetter optionSetter;
|
||||||
|
final RxString _curOption = "".obs;
|
||||||
|
|
||||||
|
MenuEntryRadios(
|
||||||
|
{required this.text,
|
||||||
|
required this.optionsGetter,
|
||||||
|
required this.curOptionGetter,
|
||||||
|
required this.optionSetter,
|
||||||
|
dismissOnClicked = false})
|
||||||
|
: super(dismissOnClicked: dismissOnClicked) {
|
||||||
|
() async {
|
||||||
|
_curOption.value = await curOptionGetter();
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MenuEntryRadioOption> get options => optionsGetter();
|
||||||
|
RxString get curOption => _curOption;
|
||||||
|
setOption(String option) async {
|
||||||
|
await optionSetter(_curOption.value, option);
|
||||||
|
if (_curOption.value != option) {
|
||||||
|
final opt = await curOptionGetter();
|
||||||
|
if (_curOption.value != opt) {
|
||||||
|
_curOption.value = opt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod_menu.PopupMenuEntry<T> _buildMenuItem(
|
||||||
|
BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
|
||||||
|
return mod_menu.PopupMenuItem(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
height: conf.height,
|
||||||
|
child: TextButton(
|
||||||
|
child: Container(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
constraints: BoxConstraints(minHeight: conf.height),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
opt.text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: MenuConfig.fontSize,
|
||||||
|
fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20.0,
|
||||||
|
height: 20.0,
|
||||||
|
child: Obx(() => opt.value == curOption.value
|
||||||
|
? Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: conf.commonColor,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink())),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (opt.dismissOnClicked && Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
setOption(opt.value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<mod_menu.PopupMenuEntry<T>> build(
|
||||||
|
BuildContext context, MenuConfig conf) {
|
||||||
|
return options.map((opt) => _buildMenuItem(context, conf, opt)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||||
|
final String text;
|
||||||
|
final RadioOptionsGetter optionsGetter;
|
||||||
|
final RadioCurOptionGetter curOptionGetter;
|
||||||
|
final RadioOptionSetter optionSetter;
|
||||||
|
final RxString _curOption = "".obs;
|
||||||
|
|
||||||
|
MenuEntrySubRadios(
|
||||||
|
{required this.text,
|
||||||
|
required this.optionsGetter,
|
||||||
|
required this.curOptionGetter,
|
||||||
|
required this.optionSetter,
|
||||||
|
dismissOnClicked = false})
|
||||||
|
: super(dismissOnClicked: dismissOnClicked) {
|
||||||
|
() async {
|
||||||
|
_curOption.value = await curOptionGetter();
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MenuEntryRadioOption> get options => optionsGetter();
|
||||||
|
RxString get curOption => _curOption;
|
||||||
|
setOption(String option) async {
|
||||||
|
await optionSetter(_curOption.value, option);
|
||||||
|
if (_curOption.value != option) {
|
||||||
|
final opt = await curOptionGetter();
|
||||||
|
if (_curOption.value != opt) {
|
||||||
|
_curOption.value = opt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod_menu.PopupMenuEntry<T> _buildSecondMenu(
|
||||||
|
BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
|
||||||
|
return mod_menu.PopupMenuItem(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
height: conf.height,
|
||||||
|
child: TextButton(
|
||||||
|
child: Container(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
constraints: BoxConstraints(minHeight: conf.height),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
opt.text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: MenuConfig.fontSize,
|
||||||
|
fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20.0,
|
||||||
|
height: 20.0,
|
||||||
|
child: Obx(() => opt.value == curOption.value
|
||||||
|
? Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: conf.commonColor,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink())),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (opt.dismissOnClicked && Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
setOption(opt.value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<mod_menu.PopupMenuEntry<T>> build(
|
||||||
|
BuildContext context, MenuConfig conf) {
|
||||||
|
return [
|
||||||
|
PopupMenuChildrenItem(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
height: conf.height,
|
||||||
|
itemBuilder: (BuildContext context) =>
|
||||||
|
options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(),
|
||||||
|
child: Row(children: [
|
||||||
|
const SizedBox(width: MenuConfig.midPadding),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: MenuConfig.fontSize,
|
||||||
|
fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Icon(
|
||||||
|
Icons.keyboard_arrow_right,
|
||||||
|
color: conf.commonColor,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef SwitchGetter = Future<bool> Function();
|
||||||
|
typedef SwitchSetter = Future<void> Function(bool);
|
||||||
|
|
||||||
|
abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
MenuEntrySwitchBase({required this.text, required dismissOnClicked})
|
||||||
|
: super(dismissOnClicked: dismissOnClicked);
|
||||||
|
|
||||||
|
RxBool get curOption;
|
||||||
|
Future<void> setOption(bool option);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<mod_menu.PopupMenuEntry<T>> build(
|
||||||
|
BuildContext context, MenuConfig conf) {
|
||||||
|
return [
|
||||||
|
mod_menu.PopupMenuItem(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
height: conf.height,
|
||||||
|
child: TextButton(
|
||||||
|
child: Container(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
height: conf.height,
|
||||||
|
child: Row(children: [
|
||||||
|
// const SizedBox(width: MenuConfig.midPadding),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: MenuConfig.fontSize,
|
||||||
|
fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Obx(() => Switch(
|
||||||
|
value: curOption.value,
|
||||||
|
onChanged: (v) {
|
||||||
|
if (super.dismissOnClicked &&
|
||||||
|
Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
setOption(v);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
])),
|
||||||
|
onPressed: () {
|
||||||
|
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
setOption(!curOption.value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
|
||||||
|
final SwitchGetter getter;
|
||||||
|
final SwitchSetter setter;
|
||||||
|
final RxBool _curOption = false.obs;
|
||||||
|
|
||||||
|
MenuEntrySwitch(
|
||||||
|
{required String text,
|
||||||
|
required this.getter,
|
||||||
|
required this.setter,
|
||||||
|
dismissOnClicked = false})
|
||||||
|
: super(text: text, dismissOnClicked: dismissOnClicked) {
|
||||||
|
() async {
|
||||||
|
_curOption.value = await getter();
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RxBool get curOption => _curOption;
|
||||||
|
@override
|
||||||
|
setOption(bool option) async {
|
||||||
|
await setter(option);
|
||||||
|
final opt = await getter();
|
||||||
|
if (_curOption.value != opt) {
|
||||||
|
_curOption.value = opt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef Switch2Getter = RxBool Function();
|
||||||
|
typedef Switch2Setter = Future<void> Function(bool);
|
||||||
|
|
||||||
|
class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
|
||||||
|
final Switch2Getter getter;
|
||||||
|
final SwitchSetter setter;
|
||||||
|
|
||||||
|
MenuEntrySwitch2(
|
||||||
|
{required String text,
|
||||||
|
required this.getter,
|
||||||
|
required this.setter,
|
||||||
|
dismissOnClicked = false})
|
||||||
|
: super(text: text, dismissOnClicked: dismissOnClicked);
|
||||||
|
|
||||||
|
@override
|
||||||
|
RxBool get curOption => getter();
|
||||||
|
@override
|
||||||
|
setOption(bool option) async {
|
||||||
|
await setter(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||||
|
final String text;
|
||||||
|
final List<MenuEntryBase<T>> entries;
|
||||||
|
|
||||||
|
MenuEntrySubMenu({required this.text, required this.entries});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<mod_menu.PopupMenuEntry<T>> build(
|
||||||
|
BuildContext context, MenuConfig conf) {
|
||||||
|
return [
|
||||||
|
PopupMenuChildrenItem(
|
||||||
|
height: conf.height,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
position: mod_menu.PopupMenuPosition.overSide,
|
||||||
|
itemBuilder: (BuildContext context) => entries
|
||||||
|
.map((entry) => entry.build(context, conf))
|
||||||
|
.expand((i) => i)
|
||||||
|
.toList(),
|
||||||
|
child: Row(children: [
|
||||||
|
const SizedBox(width: MenuConfig.midPadding),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: MenuConfig.fontSize,
|
||||||
|
fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Icon(
|
||||||
|
Icons.keyboard_arrow_right,
|
||||||
|
color: conf.commonColor,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||||
|
final Widget Function(TextStyle? style) childBuilder;
|
||||||
|
Function() proc;
|
||||||
|
|
||||||
|
MenuEntryButton(
|
||||||
|
{required this.childBuilder,
|
||||||
|
required this.proc,
|
||||||
|
dismissOnClicked = false})
|
||||||
|
: super(dismissOnClicked: dismissOnClicked);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<mod_menu.PopupMenuEntry<T>> build(
|
||||||
|
BuildContext context, MenuConfig conf) {
|
||||||
|
return [
|
||||||
|
mod_menu.PopupMenuItem(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
height: conf.height,
|
||||||
|
child: TextButton(
|
||||||
|
child: Container(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
constraints: BoxConstraints(minHeight: conf.height),
|
||||||
|
child: childBuilder(
|
||||||
|
const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: MenuConfig.fontSize,
|
||||||
|
fontWeight: FontWeight.normal),
|
||||||
|
)),
|
||||||
|
onPressed: () {
|
||||||
|
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
proc();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
619
flutter/lib/desktop/widgets/remote_menubar.dart
Normal file
619
flutter/lib/desktop/widgets/remote_menubar.dart
Normal file
@ -0,0 +1,619 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart' as rxdart;
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../mobile/widgets/dialog.dart';
|
||||||
|
import '../../mobile/widgets/overlay.dart';
|
||||||
|
import '../../models/model.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../common/shared_state.dart';
|
||||||
|
import './popup_menu.dart';
|
||||||
|
import './material_mod_popup_menu.dart' as mod_menu;
|
||||||
|
|
||||||
|
class _MenubarTheme {
|
||||||
|
static const Color commonColor = MyTheme.accent;
|
||||||
|
// kMinInteractiveDimension
|
||||||
|
static const double height = 25.0;
|
||||||
|
static const double dividerHeight = 12.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteMenubar extends StatefulWidget {
|
||||||
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
|
|
||||||
|
const RemoteMenubar({
|
||||||
|
Key? key,
|
||||||
|
required this.id,
|
||||||
|
required this.ffi,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RemoteMenubar> createState() => _RemoteMenubarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||||
|
final RxBool _show = false.obs;
|
||||||
|
final Rx<Color> _hideColor = Colors.white12.obs;
|
||||||
|
|
||||||
|
bool get isFullscreen => Get.find<RxBool>(tag: 'fullscreen').isTrue;
|
||||||
|
void setFullscreen(bool v) {
|
||||||
|
Get.find<RxBool>(tag: 'fullscreen').value = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Obx(
|
||||||
|
() => _show.value ? _buildMenubar(context) : _buildShowHide(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShowHide(BuildContext context) {
|
||||||
|
return Obx(() => Tooltip(
|
||||||
|
message: translate(_show.value ? "Hide Menubar" : "Show Menubar"),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 100,
|
||||||
|
height: 5,
|
||||||
|
child: TextButton(
|
||||||
|
onHover: (bool v) {
|
||||||
|
_hideColor.value = v ? Colors.white60 : Colors.white24;
|
||||||
|
},
|
||||||
|
onPressed: () {
|
||||||
|
_show.value = !_show.value;
|
||||||
|
},
|
||||||
|
child: Obx(() => Container(
|
||||||
|
color: _hideColor.value,
|
||||||
|
)))),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMenubar(BuildContext context) {
|
||||||
|
final List<Widget> menubarItems = [];
|
||||||
|
if (!isWebDesktop) {
|
||||||
|
menubarItems.add(_buildFullscreen(context));
|
||||||
|
if (widget.ffi.ffiModel.isPeerAndroid) {
|
||||||
|
menubarItems.add(IconButton(
|
||||||
|
tooltip: translate('Mobile Actions'),
|
||||||
|
color: _MenubarTheme.commonColor,
|
||||||
|
icon: const Icon(Icons.build),
|
||||||
|
onPressed: () {
|
||||||
|
if (mobileActionsOverlayEntry == null) {
|
||||||
|
showMobileActionsOverlay();
|
||||||
|
} else {
|
||||||
|
hideMobileActionsOverlay();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menubarItems.add(_buildMonitor(context));
|
||||||
|
menubarItems.add(_buildControl(context));
|
||||||
|
menubarItems.add(_buildDisplay(context));
|
||||||
|
if (!isWeb) {
|
||||||
|
menubarItems.add(_buildChat(context));
|
||||||
|
}
|
||||||
|
menubarItems.add(_buildClose(context));
|
||||||
|
return PopupMenuTheme(
|
||||||
|
data: const PopupMenuThemeData(
|
||||||
|
textStyle: TextStyle(color: _MenubarTheme.commonColor)),
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Container(
|
||||||
|
color: Colors.white,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: menubarItems,
|
||||||
|
)),
|
||||||
|
_buildShowHide(context),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFullscreen(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'),
|
||||||
|
onPressed: () {
|
||||||
|
setFullscreen(!isFullscreen);
|
||||||
|
},
|
||||||
|
icon: Obx(() => isFullscreen
|
||||||
|
? const Icon(
|
||||||
|
Icons.fullscreen_exit,
|
||||||
|
color: _MenubarTheme.commonColor,
|
||||||
|
)
|
||||||
|
: const Icon(
|
||||||
|
Icons.fullscreen,
|
||||||
|
color: _MenubarTheme.commonColor,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildChat(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
tooltip: translate('Chat'),
|
||||||
|
onPressed: () {
|
||||||
|
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
|
||||||
|
widget.ffi.chatModel.toggleChatOverlay();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.message,
|
||||||
|
color: _MenubarTheme.commonColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMonitor(BuildContext context) {
|
||||||
|
final pi = widget.ffi.ffiModel.pi;
|
||||||
|
return mod_menu.PopupMenuButton(
|
||||||
|
tooltip: translate('Select Monitor'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
position: mod_menu.PopupMenuPosition.under,
|
||||||
|
icon: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.personal_video,
|
||||||
|
color: _MenubarTheme.commonColor,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 3.9),
|
||||||
|
child: Obx(() {
|
||||||
|
RxInt display = CurrentDisplayState.find(widget.id);
|
||||||
|
return Text(
|
||||||
|
"${display.value + 1}/${pi.displays.length}",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: _MenubarTheme.commonColor, fontSize: 8),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
itemBuilder: (BuildContext context) {
|
||||||
|
final List<Widget> rowChildren = [];
|
||||||
|
for (int i = 0; i < pi.displays.length; i++) {
|
||||||
|
rowChildren.add(
|
||||||
|
Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.personal_video,
|
||||||
|
color: _MenubarTheme.commonColor,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: Container(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
constraints:
|
||||||
|
const BoxConstraints(minHeight: _MenubarTheme.height),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2.5),
|
||||||
|
child: Text(
|
||||||
|
(i + 1).toString(),
|
||||||
|
style:
|
||||||
|
const TextStyle(color: _MenubarTheme.commonColor),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
onPressed: () {
|
||||||
|
RxInt display = CurrentDisplayState.find(widget.id);
|
||||||
|
if (display.value != i) {
|
||||||
|
bind.sessionSwitchDisplay(id: widget.id, value: i);
|
||||||
|
pi.currentDisplay = i;
|
||||||
|
display.value = i;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <mod_menu.PopupMenuEntry<String>>[
|
||||||
|
mod_menu.PopupMenuItem<String>(
|
||||||
|
height: _MenubarTheme.height,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: rowChildren),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildControl(BuildContext context) {
|
||||||
|
return mod_menu.PopupMenuButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.bolt,
|
||||||
|
color: _MenubarTheme.commonColor,
|
||||||
|
),
|
||||||
|
tooltip: translate('Control Actions'),
|
||||||
|
position: mod_menu.PopupMenuPosition.under,
|
||||||
|
itemBuilder: (BuildContext context) => _getControlMenu()
|
||||||
|
.map((entry) => entry.build(
|
||||||
|
context,
|
||||||
|
const MenuConfig(
|
||||||
|
commonColor: _MenubarTheme.commonColor,
|
||||||
|
height: _MenubarTheme.height,
|
||||||
|
dividerHeight: _MenubarTheme.dividerHeight,
|
||||||
|
)))
|
||||||
|
.expand((i) => i)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDisplay(BuildContext context) {
|
||||||
|
return mod_menu.PopupMenuButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.tv,
|
||||||
|
color: _MenubarTheme.commonColor,
|
||||||
|
),
|
||||||
|
tooltip: translate('Display Settings'),
|
||||||
|
position: mod_menu.PopupMenuPosition.under,
|
||||||
|
onSelected: (String item) {},
|
||||||
|
itemBuilder: (BuildContext context) => _getDisplayMenu()
|
||||||
|
.map((entry) => entry.build(
|
||||||
|
context,
|
||||||
|
const MenuConfig(
|
||||||
|
commonColor: _MenubarTheme.commonColor,
|
||||||
|
height: _MenubarTheme.height,
|
||||||
|
dividerHeight: _MenubarTheme.dividerHeight,
|
||||||
|
)))
|
||||||
|
.expand((i) => i)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildClose(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
tooltip: translate('Close'),
|
||||||
|
onPressed: () {
|
||||||
|
clientClose(widget.ffi.dialogManager);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: _MenubarTheme.commonColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MenuEntryBase<String>> _getControlMenu() {
|
||||||
|
final pi = widget.ffi.ffiModel.pi;
|
||||||
|
final perms = widget.ffi.ffiModel.permissions;
|
||||||
|
|
||||||
|
final List<MenuEntryBase<String>> displayMenu = [];
|
||||||
|
|
||||||
|
if (pi.version.isNotEmpty) {
|
||||||
|
displayMenu.add(MenuEntryButton<String>(
|
||||||
|
childBuilder: (TextStyle? style) => Text(
|
||||||
|
translate('Refresh'),
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
proc: () {
|
||||||
|
bind.sessionRefresh(id: widget.id);
|
||||||
|
},
|
||||||
|
dismissOnClicked: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
displayMenu.add(MenuEntryButton<String>(
|
||||||
|
childBuilder: (TextStyle? style) => Text(
|
||||||
|
translate('OS Password'),
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
proc: () {
|
||||||
|
showSetOSPassword(widget.id, false, widget.ffi.dialogManager);
|
||||||
|
},
|
||||||
|
dismissOnClicked: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!isWebDesktop) {
|
||||||
|
if (perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||||
|
displayMenu.add(MenuEntryButton<String>(
|
||||||
|
childBuilder: (TextStyle? style) => Text(
|
||||||
|
translate('Paste'),
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
proc: () {
|
||||||
|
() async {
|
||||||
|
ClipboardData? data =
|
||||||
|
await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
|
if (data != null && data.text != null) {
|
||||||
|
bind.sessionInputString(id: widget.id, value: data.text ?? "");
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
},
|
||||||
|
dismissOnClicked: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
displayMenu.add(MenuEntryButton<String>(
|
||||||
|
childBuilder: (TextStyle? style) => Text(
|
||||||
|
translate('Reset canvas'),
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
proc: () {
|
||||||
|
widget.ffi.cursorModel.reset();
|
||||||
|
},
|
||||||
|
dismissOnClicked: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (perms['keyboard'] != false) {
|
||||||
|
if (pi.platform == 'Linux' || pi.sasEnabled) {
|
||||||
|
displayMenu.add(MenuEntryButton<String>(
|
||||||
|
childBuilder: (TextStyle? style) => Text(
|
||||||
|
'${translate("Insert")} Ctrl + Alt + Del',
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
proc: () {
|
||||||
|
bind.sessionCtrlAltDel(id: widget.id);
|
||||||
|
},
|
||||||
|
dismissOnClicked: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
displayMenu.add(MenuEntryButton<String>(
|
||||||
|
childBuilder: (TextStyle? style) => Text(
|
||||||
|
translate('Insert Lock'),
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
proc: () {
|
||||||
|
bind.sessionLockScreen(id: widget.id);
|
||||||
|
},
|
||||||
|
dismissOnClicked: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (pi.platform == 'Windows') {
|
||||||
|
displayMenu.add(MenuEntryButton<String>(
|
||||||
|
childBuilder: (TextStyle? style) => Obx(() => Text(
|
||||||
|
translate(
|
||||||
|
'${BlockInputState.find(widget.id).value ? "Unb" : "B"}lock user input'),
|
||||||
|
style: style,
|
||||||
|
)),
|
||||||
|
proc: () {
|
||||||
|
RxBool blockInput = BlockInputState.find(widget.id);
|
||||||
|
bind.sessionToggleOption(
|
||||||
|
id: widget.id,
|
||||||
|
value: '${blockInput.value ? "un" : ""}block-input');
|
||||||
|
blockInput.value = !blockInput.value;
|
||||||
|
},
|
||||||
|
dismissOnClicked: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gFFI.ffiModel.permissions["restart"] != false &&
|
||||||
|
(pi.platform == "Linux" ||
|
||||||
|
pi.platform == "Windows" ||
|
||||||
|
pi.platform == "Mac OS")) {
|
||||||
|
displayMenu.add(MenuEntryButton<String>(
|
||||||
|
childBuilder: (TextStyle? style) => Text(
|
||||||
|
translate('Restart Remote Device'),
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
proc: () {
|
||||||
|
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
|
||||||
|
},
|
||||||
|
dismissOnClicked: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MenuEntryBase<String>> _getDisplayMenu() {
|
||||||
|
final displayMenu = [
|
||||||
|
MenuEntryRadios<String>(
|
||||||
|
text: translate('Ratio'),
|
||||||
|
optionsGetter: () => [
|
||||||
|
MenuEntryRadioOption(
|
||||||
|
text: translate('Scale original'), value: 'original'),
|
||||||
|
MenuEntryRadioOption(
|
||||||
|
text: translate('Scale adaptive'), value: 'adaptive'),
|
||||||
|
],
|
||||||
|
curOptionGetter: () async {
|
||||||
|
return await bind.sessionGetOption(
|
||||||
|
id: widget.id, arg: 'view-style') ??
|
||||||
|
'adaptive';
|
||||||
|
},
|
||||||
|
optionSetter: (String oldValue, String newValue) async {
|
||||||
|
await bind.sessionPeerOption(
|
||||||
|
id: widget.id, name: "view-style", value: newValue);
|
||||||
|
widget.ffi.canvasModel.updateViewStyle();
|
||||||
|
}),
|
||||||
|
MenuEntryDivider<String>(),
|
||||||
|
MenuEntryRadios<String>(
|
||||||
|
text: translate('Scroll Style'),
|
||||||
|
optionsGetter: () => [
|
||||||
|
MenuEntryRadioOption(
|
||||||
|
text: translate('ScrollAuto'), value: 'scrollauto'),
|
||||||
|
MenuEntryRadioOption(
|
||||||
|
text: translate('Scrollbar'), value: 'scrollbar'),
|
||||||
|
],
|
||||||
|
curOptionGetter: () async {
|
||||||
|
return await bind.sessionGetOption(
|
||||||
|
id: widget.id, arg: 'scroll-style') ??
|
||||||
|
'';
|
||||||
|
},
|
||||||
|
optionSetter: (String oldValue, String newValue) async {
|
||||||
|
await bind.sessionPeerOption(
|
||||||
|
id: widget.id, name: "scroll-style", value: newValue);
|
||||||
|
widget.ffi.canvasModel.updateScrollStyle();
|
||||||
|
}),
|
||||||
|
MenuEntryDivider<String>(),
|
||||||
|
MenuEntryRadios<String>(
|
||||||
|
text: translate('Image Quality'),
|
||||||
|
optionsGetter: () => [
|
||||||
|
MenuEntryRadioOption(
|
||||||
|
text: translate('Good image quality'), value: 'best'),
|
||||||
|
MenuEntryRadioOption(
|
||||||
|
text: translate('Balanced'), value: 'balanced'),
|
||||||
|
MenuEntryRadioOption(
|
||||||
|
text: translate('Optimize reaction time'), value: 'low'),
|
||||||
|
MenuEntryRadioOption(
|
||||||
|
text: translate('Custom'),
|
||||||
|
value: 'custom',
|
||||||
|
dismissOnClicked: true),
|
||||||
|
],
|
||||||
|
curOptionGetter: () async {
|
||||||
|
String quality =
|
||||||
|
await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced';
|
||||||
|
if (quality == '') quality = 'balanced';
|
||||||
|
return quality;
|
||||||
|
},
|
||||||
|
optionSetter: (String oldValue, String newValue) async {
|
||||||
|
if (oldValue != newValue) {
|
||||||
|
await bind.sessionSetImageQuality(id: widget.id, value: newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue == 'custom') {
|
||||||
|
final btnCancel = msgBoxButton(translate('Close'), () {
|
||||||
|
widget.ffi.dialogManager.dismissAll();
|
||||||
|
});
|
||||||
|
final quality =
|
||||||
|
await bind.sessionGetCustomImageQuality(id: widget.id);
|
||||||
|
final double initValue = quality != null && quality.isNotEmpty
|
||||||
|
? quality[0].toDouble()
|
||||||
|
: 50.0;
|
||||||
|
final RxDouble sliderValue = RxDouble(initValue);
|
||||||
|
final rxReplay = rxdart.ReplaySubject<double>();
|
||||||
|
rxReplay
|
||||||
|
.throttleTime(const Duration(milliseconds: 1000),
|
||||||
|
trailing: true, leading: false)
|
||||||
|
.listen((double v) {
|
||||||
|
() async {
|
||||||
|
await bind.sessionSetCustomImageQuality(
|
||||||
|
id: widget.id, value: v.toInt());
|
||||||
|
}();
|
||||||
|
});
|
||||||
|
final slider = Obx(() {
|
||||||
|
return Slider(
|
||||||
|
value: sliderValue.value,
|
||||||
|
max: 100,
|
||||||
|
divisions: 100,
|
||||||
|
label: sliderValue.value.round().toString(),
|
||||||
|
onChanged: (double value) {
|
||||||
|
sliderValue.value = value;
|
||||||
|
rxReplay.add(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
|
||||||
|
slider, [btnCancel]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
MenuEntryDivider<String>(),
|
||||||
|
MenuEntrySwitch<String>(
|
||||||
|
text: translate('Show remote cursor'),
|
||||||
|
getter: () async {
|
||||||
|
return bind.sessionGetToggleOptionSync(
|
||||||
|
id: widget.id, arg: 'show-remote-cursor');
|
||||||
|
},
|
||||||
|
setter: (bool v) async {
|
||||||
|
await bind.sessionToggleOption(
|
||||||
|
id: widget.id, value: 'show-remote-cursor');
|
||||||
|
}),
|
||||||
|
MenuEntrySwitch<String>(
|
||||||
|
text: translate('Show quality monitor'),
|
||||||
|
getter: () async {
|
||||||
|
return bind.sessionGetToggleOptionSync(
|
||||||
|
id: widget.id, arg: 'show-quality-monitor');
|
||||||
|
},
|
||||||
|
setter: (bool v) async {
|
||||||
|
await bind.sessionToggleOption(
|
||||||
|
id: widget.id, value: 'show-quality-monitor');
|
||||||
|
widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
final perms = widget.ffi.ffiModel.permissions;
|
||||||
|
final pi = widget.ffi.ffiModel.pi;
|
||||||
|
|
||||||
|
if (perms['audio'] != false) {
|
||||||
|
displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio'));
|
||||||
|
}
|
||||||
|
if (perms['keyboard'] != false) {
|
||||||
|
if (perms['clipboard'] != false) {
|
||||||
|
displayMenu.add(
|
||||||
|
_createSwitchMenuEntry('Disable clipboard', 'disable-clipboard'));
|
||||||
|
}
|
||||||
|
displayMenu.add(_createSwitchMenuEntry(
|
||||||
|
'Lock after session end', 'lock-after-session-end'));
|
||||||
|
if (pi.platform == 'Windows') {
|
||||||
|
displayMenu.add(MenuEntrySwitch2<String>(
|
||||||
|
text: translate('Privacy mode'),
|
||||||
|
getter: () {
|
||||||
|
return PrivacyModeState.find(widget.id);
|
||||||
|
},
|
||||||
|
setter: (bool v) async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await bind.sessionToggleOption(
|
||||||
|
id: widget.id, value: 'privacy-mode');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return displayMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuEntrySwitch<String> _createSwitchMenuEntry(String text, String option) {
|
||||||
|
return MenuEntrySwitch<String>(
|
||||||
|
text: translate(text),
|
||||||
|
getter: () async {
|
||||||
|
return bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
||||||
|
},
|
||||||
|
setter: (bool v) async {
|
||||||
|
await bind.sessionToggleOption(id: widget.id, value: option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showSetOSPassword(
|
||||||
|
String id, bool login, OverlayDialogManager dialogManager) async {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
|
||||||
|
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
||||||
|
controller.text = password;
|
||||||
|
dialogManager.show((setState, close) {
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate('OS Password')),
|
||||||
|
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
PasswordWidget(controller: controller),
|
||||||
|
CheckboxListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
title: Text(
|
||||||
|
translate('Auto Login'),
|
||||||
|
),
|
||||||
|
value: autoLogin,
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null) return;
|
||||||
|
setState(() => autoLogin = v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
style: flatButtonStyle,
|
||||||
|
onPressed: () {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate('Cancel')),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
style: flatButtonStyle,
|
||||||
|
onPressed: () {
|
||||||
|
var text = controller.text.trim();
|
||||||
|
bind.sessionPeerOption(id: id, name: "os-password", value: text);
|
||||||
|
bind.sessionPeerOption(
|
||||||
|
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
|
||||||
|
if (text != "" && login) {
|
||||||
|
bind.sessionInputOsPassword(id: id, value: text);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
child: Text(translate('OK')),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
@ -5,9 +7,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/main.dart';
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
import 'package:scroll_pos/scroll_pos.dart';
|
import 'package:scroll_pos/scroll_pos.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import '../../utils/multi_window_manager.dart';
|
import '../../utils/multi_window_manager.dart';
|
||||||
|
|
||||||
@ -33,6 +36,15 @@ class TabInfo {
|
|||||||
required this.page});
|
required this.page});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DesktopTabType {
|
||||||
|
main,
|
||||||
|
cm,
|
||||||
|
remoteScreen,
|
||||||
|
fileTransfer,
|
||||||
|
portForward,
|
||||||
|
rdp,
|
||||||
|
}
|
||||||
|
|
||||||
class DesktopTabState {
|
class DesktopTabState {
|
||||||
final List<TabInfo> tabs = [];
|
final List<TabInfo> tabs = [];
|
||||||
final ScrollPosController scrollController =
|
final ScrollPosController scrollController =
|
||||||
@ -63,6 +75,7 @@ class DesktopTabController {
|
|||||||
state.update((val) {
|
state.update((val) {
|
||||||
val!.tabs.add(tab);
|
val!.tabs.add(tab);
|
||||||
});
|
});
|
||||||
|
state.value.scrollController.itemCount = state.value.tabs.length;
|
||||||
toIndex = state.value.tabs.length - 1;
|
toIndex = state.value.tabs.length - 1;
|
||||||
assert(toIndex >= 0);
|
assert(toIndex >= 0);
|
||||||
}
|
}
|
||||||
@ -95,8 +108,16 @@ class DesktopTabController {
|
|||||||
void jumpTo(int index) {
|
void jumpTo(int index) {
|
||||||
state.update((val) {
|
state.update((val) {
|
||||||
val!.selected = index;
|
val!.selected = index;
|
||||||
val.pageController.jumpToPage(index);
|
Future.delayed(Duration.zero, (() {
|
||||||
val.scrollController.scrollToItem(index, center: true, animate: true);
|
if (val.pageController.hasClients) {
|
||||||
|
val.pageController.jumpToPage(index);
|
||||||
|
}
|
||||||
|
if (val.scrollController.hasClients &&
|
||||||
|
val.scrollController.canScroll &&
|
||||||
|
val.scrollController.itemCount >= index) {
|
||||||
|
val.scrollController.scrollToItem(index, center: true, animate: true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
onSelected?.call(index);
|
onSelected?.call(index);
|
||||||
}
|
}
|
||||||
@ -113,11 +134,27 @@ class DesktopTabController {
|
|||||||
remove(state.value.selected);
|
remove(state.value.selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
state.value.tabs.clear();
|
||||||
|
state.refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TabThemeConf {
|
||||||
|
double iconSize;
|
||||||
|
TarBarTheme theme;
|
||||||
|
TabThemeConf({required this.iconSize, required this.theme});
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef TabBuilder = Widget Function(
|
||||||
|
String key, Widget icon, Widget label, TabThemeConf themeConf);
|
||||||
|
typedef LabelGetter = Rx<String> Function(String key);
|
||||||
|
|
||||||
class DesktopTab extends StatelessWidget {
|
class DesktopTab extends StatelessWidget {
|
||||||
final Function(String)? onTabClose;
|
final Function(String)? onTabClose;
|
||||||
final TarBarTheme theme;
|
final TarBarTheme theme;
|
||||||
|
final DesktopTabType tabType;
|
||||||
final bool isMainWindow;
|
final bool isMainWindow;
|
||||||
final bool showTabBar;
|
final bool showTabBar;
|
||||||
final bool showLogo;
|
final bool showLogo;
|
||||||
@ -127,23 +164,31 @@ class DesktopTab extends StatelessWidget {
|
|||||||
final bool showClose;
|
final bool showClose;
|
||||||
final Widget Function(Widget pageView)? pageViewBuilder;
|
final Widget Function(Widget pageView)? pageViewBuilder;
|
||||||
final Widget? tail;
|
final Widget? tail;
|
||||||
|
final VoidCallback? onClose;
|
||||||
|
final TabBuilder? tabBuilder;
|
||||||
|
final LabelGetter? labelGetter;
|
||||||
|
|
||||||
final DesktopTabController controller;
|
final DesktopTabController controller;
|
||||||
late final state = controller.state;
|
Rx<DesktopTabState> get state => controller.state;
|
||||||
|
|
||||||
DesktopTab(
|
const DesktopTab({
|
||||||
{required this.controller,
|
required this.controller,
|
||||||
required this.isMainWindow,
|
required this.tabType,
|
||||||
this.theme = const TarBarTheme.light(),
|
this.theme = const TarBarTheme.light(),
|
||||||
this.onTabClose,
|
this.onTabClose,
|
||||||
this.showTabBar = true,
|
this.showTabBar = true,
|
||||||
this.showLogo = true,
|
this.showLogo = true,
|
||||||
this.showTitle = true,
|
this.showTitle = true,
|
||||||
this.showMinimize = true,
|
this.showMinimize = true,
|
||||||
this.showMaximize = true,
|
this.showMaximize = true,
|
||||||
this.showClose = true,
|
this.showClose = true,
|
||||||
this.pageViewBuilder,
|
this.pageViewBuilder,
|
||||||
this.tail});
|
this.tail,
|
||||||
|
this.onClose,
|
||||||
|
this.tabBuilder,
|
||||||
|
this.labelGetter,
|
||||||
|
}) : isMainWindow =
|
||||||
|
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -172,11 +217,48 @@ class DesktopTab extends StatelessWidget {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBlock({required Widget child}) {
|
||||||
|
if (tabType != DesktopTabType.main) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
var block = false.obs;
|
||||||
|
return Obx(() => MouseRegion(
|
||||||
|
onEnter: (_) async {
|
||||||
|
if (!option2bool(
|
||||||
|
'allow-remote-config-modification',
|
||||||
|
await bind.mainGetOption(
|
||||||
|
key: 'allow-remote-config-modification'))) {
|
||||||
|
var time0 = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
await bind.mainCheckMouseTime();
|
||||||
|
Timer(const Duration(milliseconds: 120), () async {
|
||||||
|
var d = time0 - await bind.mainGetMouseTime();
|
||||||
|
if (d < 120) {
|
||||||
|
block.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onExit: (_) => block.value = false,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
Offstage(
|
||||||
|
offstage: !block.value,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildPageView() {
|
Widget _buildPageView() {
|
||||||
return Obx(() => PageView(
|
return _buildBlock(
|
||||||
controller: state.value.pageController,
|
child: Obx(() => PageView(
|
||||||
children:
|
controller: state.value.pageController,
|
||||||
state.value.tabs.map((tab) => tab.page).toList(growable: false)));
|
children: state.value.tabs
|
||||||
|
.map((tab) => tab.page)
|
||||||
|
.toList(growable: false))));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBar() {
|
Widget _buildBar() {
|
||||||
@ -185,6 +267,11 @@ class DesktopTab extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
Offstage(
|
||||||
|
offstage: !Platform.isMacOS,
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 78,
|
||||||
|
)),
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !showLogo,
|
offstage: !showLogo,
|
||||||
@ -217,6 +304,8 @@ class DesktopTab extends StatelessWidget {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
onTabClose: onTabClose,
|
onTabClose: onTabClose,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
|
tabBuilder: tabBuilder,
|
||||||
|
labelGetter: labelGetter,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -229,6 +318,7 @@ class DesktopTab extends StatelessWidget {
|
|||||||
showMinimize: showMinimize,
|
showMinimize: showMinimize,
|
||||||
showMaximize: showMaximize,
|
showMaximize: showMaximize,
|
||||||
showClose: showClose,
|
showClose: showClose,
|
||||||
|
onClose: onClose,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -242,6 +332,7 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
final bool showMinimize;
|
final bool showMinimize;
|
||||||
final bool showMaximize;
|
final bool showMaximize;
|
||||||
final bool showClose;
|
final bool showClose;
|
||||||
|
final VoidCallback? onClose;
|
||||||
|
|
||||||
const WindowActionPanel(
|
const WindowActionPanel(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
@ -249,7 +340,8 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
required this.theme,
|
required this.theme,
|
||||||
this.showMinimize = true,
|
this.showMinimize = true,
|
||||||
this.showMaximize = true,
|
this.showMaximize = true,
|
||||||
this.showClose = true})
|
this.showClose = true,
|
||||||
|
this.onClose})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -323,8 +415,12 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
if (mainTab) {
|
if (mainTab) {
|
||||||
windowManager.close();
|
windowManager.close();
|
||||||
} else {
|
} else {
|
||||||
WindowController.fromWindowId(windowId!).close();
|
// only hide for multi window, not close
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
WindowController.fromWindowId(windowId!).hide();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
onClose?.call();
|
||||||
},
|
},
|
||||||
is_close: true,
|
is_close: true,
|
||||||
)),
|
)),
|
||||||
@ -336,13 +432,20 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class _ListView extends StatelessWidget {
|
class _ListView extends StatelessWidget {
|
||||||
final DesktopTabController controller;
|
final DesktopTabController controller;
|
||||||
late final Rx<DesktopTabState> state;
|
|
||||||
final Function(String key)? onTabClose;
|
final Function(String key)? onTabClose;
|
||||||
final TarBarTheme theme;
|
final TarBarTheme theme;
|
||||||
|
|
||||||
|
final TabBuilder? tabBuilder;
|
||||||
|
final LabelGetter? labelGetter;
|
||||||
|
|
||||||
|
Rx<DesktopTabState> get state => controller.state;
|
||||||
|
|
||||||
_ListView(
|
_ListView(
|
||||||
{required this.controller, required this.onTabClose, required this.theme})
|
{required this.controller,
|
||||||
: this.state = controller.state;
|
required this.onTabClose,
|
||||||
|
required this.theme,
|
||||||
|
this.tabBuilder,
|
||||||
|
this.labelGetter});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -356,7 +459,9 @@ class _ListView extends StatelessWidget {
|
|||||||
final tab = e.value;
|
final tab = e.value;
|
||||||
return _Tab(
|
return _Tab(
|
||||||
index: index,
|
index: index,
|
||||||
label: tab.label,
|
label: labelGetter == null
|
||||||
|
? Rx<String>(tab.label)
|
||||||
|
: labelGetter!(tab.label),
|
||||||
selectedIcon: tab.selectedIcon,
|
selectedIcon: tab.selectedIcon,
|
||||||
unselectedIcon: tab.unselectedIcon,
|
unselectedIcon: tab.unselectedIcon,
|
||||||
closable: tab.closable,
|
closable: tab.closable,
|
||||||
@ -364,22 +469,33 @@ class _ListView extends StatelessWidget {
|
|||||||
onClose: () => controller.remove(index),
|
onClose: () => controller.remove(index),
|
||||||
onSelected: () => controller.jumpTo(index),
|
onSelected: () => controller.jumpTo(index),
|
||||||
theme: theme,
|
theme: theme,
|
||||||
|
tabBuilder: tabBuilder == null
|
||||||
|
? null
|
||||||
|
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
|
||||||
|
return tabBuilder!(
|
||||||
|
tab.label,
|
||||||
|
icon,
|
||||||
|
labelWidget,
|
||||||
|
themeConf,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}).toList()));
|
}).toList()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Tab extends StatelessWidget {
|
class _Tab extends StatefulWidget {
|
||||||
late final int index;
|
late final int index;
|
||||||
late final String label;
|
late final Rx<String> label;
|
||||||
late final IconData? selectedIcon;
|
late final IconData? selectedIcon;
|
||||||
late final IconData? unselectedIcon;
|
late final IconData? unselectedIcon;
|
||||||
late final bool closable;
|
late final bool closable;
|
||||||
late final int selected;
|
late final int selected;
|
||||||
late final Function() onClose;
|
late final Function() onClose;
|
||||||
late final Function() onSelected;
|
late final Function() onSelected;
|
||||||
final RxBool _hover = false.obs;
|
|
||||||
late final TarBarTheme theme;
|
late final TarBarTheme theme;
|
||||||
|
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
|
||||||
|
tabBuilder;
|
||||||
|
|
||||||
_Tab(
|
_Tab(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
@ -387,6 +503,7 @@ class _Tab extends StatelessWidget {
|
|||||||
required this.label,
|
required this.label,
|
||||||
this.selectedIcon,
|
this.selectedIcon,
|
||||||
this.unselectedIcon,
|
this.unselectedIcon,
|
||||||
|
this.tabBuilder,
|
||||||
required this.closable,
|
required this.closable,
|
||||||
required this.selected,
|
required this.selected,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
@ -394,61 +511,87 @@ class _Tab extends StatelessWidget {
|
|||||||
required this.theme})
|
required this.theme})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_Tab> createState() => _TabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabState extends State<_Tab> with RestorationMixin {
|
||||||
|
final RestorableBool restoreHover = RestorableBool(false);
|
||||||
|
|
||||||
|
Widget _buildTabContent() {
|
||||||
|
bool showIcon =
|
||||||
|
widget.selectedIcon != null && widget.unselectedIcon != null;
|
||||||
|
bool isSelected = widget.index == widget.selected;
|
||||||
|
|
||||||
|
final icon = Offstage(
|
||||||
|
offstage: !showIcon,
|
||||||
|
child: Icon(
|
||||||
|
isSelected ? widget.selectedIcon : widget.unselectedIcon,
|
||||||
|
size: _kIconSize,
|
||||||
|
color: isSelected
|
||||||
|
? widget.theme.selectedtabIconColor
|
||||||
|
: widget.theme.unSelectedtabIconColor,
|
||||||
|
).paddingOnly(right: 5));
|
||||||
|
final labelWidget = Obx(() {
|
||||||
|
return Text(
|
||||||
|
translate(widget.label.value),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected
|
||||||
|
? widget.theme.selectedTextColor
|
||||||
|
: widget.theme.unSelectedTextColor),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (widget.tabBuilder == null) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
labelWidget,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return widget.tabBuilder!(icon, labelWidget,
|
||||||
|
TabThemeConf(iconSize: _kIconSize, theme: widget.theme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool show_icon = selectedIcon != null && unselectedIcon != null;
|
bool isSelected = widget.index == widget.selected;
|
||||||
bool is_selected = index == selected;
|
bool showDivider =
|
||||||
bool show_divider = index != selected - 1 && index != selected;
|
widget.index != widget.selected - 1 && widget.index != widget.selected;
|
||||||
|
RxBool hover = restoreHover.value.obs;
|
||||||
return Ink(
|
return Ink(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onHover: (hover) => _hover.value = hover,
|
onHover: (value) {
|
||||||
onTap: () => onSelected(),
|
hover.value = value;
|
||||||
|
restoreHover.value = value;
|
||||||
|
},
|
||||||
|
onTap: () => widget.onSelected(),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
SizedBox(
|
||||||
height: _kTabBarHeight,
|
height: _kTabBarHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
_buildTabContent(),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Obx((() => _CloseButton(
|
||||||
children: [
|
visiable: hover.value && widget.closable,
|
||||||
Offstage(
|
tabSelected: isSelected,
|
||||||
offstage: !show_icon,
|
onClose: () => widget.onClose(),
|
||||||
child: Icon(
|
theme: widget.theme,
|
||||||
is_selected ? selectedIcon : unselectedIcon,
|
)))
|
||||||
size: _kIconSize,
|
|
||||||
color: is_selected
|
|
||||||
? theme.selectedtabIconColor
|
|
||||||
: theme.unSelectedtabIconColor,
|
|
||||||
).paddingOnly(right: 5)),
|
|
||||||
Text(
|
|
||||||
translate(label),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
color: is_selected
|
|
||||||
? theme.selectedTextColor
|
|
||||||
: theme.unSelectedTextColor),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Offstage(
|
|
||||||
offstage: !closable,
|
|
||||||
child: Obx((() => _CloseButton(
|
|
||||||
visiable: _hover.value,
|
|
||||||
tabSelected: is_selected,
|
|
||||||
onClose: () => onClose(),
|
|
||||||
theme: theme,
|
|
||||||
))),
|
|
||||||
)
|
|
||||||
])).paddingSymmetric(horizontal: 10),
|
])).paddingSymmetric(horizontal: 10),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !show_divider,
|
offstage: !showDivider,
|
||||||
child: VerticalDivider(
|
child: VerticalDivider(
|
||||||
width: 1,
|
width: 1,
|
||||||
indent: _kDividerIndent,
|
indent: _kDividerIndent,
|
||||||
endIndent: _kDividerIndent,
|
endIndent: _kDividerIndent,
|
||||||
color: theme.dividerColor,
|
color: widget.theme.dividerColor,
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -457,6 +600,14 @@ class _Tab extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get restorationId => "_Tab${widget.label.value}";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||||
|
registerForRestoration(restoreHover, 'restoreHover');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CloseButton extends StatelessWidget {
|
class _CloseButton extends StatelessWidget {
|
||||||
@ -480,7 +631,7 @@ class _CloseButton extends StatelessWidget {
|
|||||||
child: Offstage(
|
child: Offstage(
|
||||||
offstage: !visiable,
|
offstage: !visiable,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
customBorder: RoundedRectangleBorder(),
|
customBorder: const RoundedRectangleBorder(),
|
||||||
onTap: () => onClose(),
|
onTap: () => onClose(),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.close,
|
Icons.close,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
|
import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
|
||||||
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
|
import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -47,6 +48,9 @@ Future<Null> main(List<String> args) async {
|
|||||||
case WindowType.FileTransfer:
|
case WindowType.FileTransfer:
|
||||||
runFileTransferScreen(argument);
|
runFileTransferScreen(argument);
|
||||||
break;
|
break;
|
||||||
|
case WindowType.PortForward:
|
||||||
|
runPortForwardScreen(argument);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -76,14 +80,9 @@ Future<void> initEnv(String appType) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void runMainApp(bool startService) async {
|
void runMainApp(bool startService) async {
|
||||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(1280, 720));
|
await initEnv(kAppTypeMain);
|
||||||
await Future.wait([
|
// trigger connection status updater
|
||||||
initEnv(kAppTypeMain),
|
await bind.mainCheckConnectStatus();
|
||||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
|
||||||
await windowManager.show();
|
|
||||||
await windowManager.focus();
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
if (startService) {
|
if (startService) {
|
||||||
// await windowManager.ensureInitialized();
|
// await windowManager.ensureInitialized();
|
||||||
// disable tray
|
// disable tray
|
||||||
@ -91,6 +90,13 @@ void runMainApp(bool startService) async {
|
|||||||
gFFI.serverModel.startService();
|
gFFI.serverModel.startService();
|
||||||
}
|
}
|
||||||
runApp(App());
|
runApp(App());
|
||||||
|
// set window option
|
||||||
|
WindowOptions windowOptions =
|
||||||
|
getHiddenTitleBarWindowOptions(const Size(1280, 720));
|
||||||
|
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
|
await windowManager.show();
|
||||||
|
await windowManager.focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void runMobileApp() async {
|
void runMobileApp() async {
|
||||||
@ -133,6 +139,23 @@ void runFileTransferScreen(Map<String, dynamic> argument) async {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void runPortForwardScreen(Map<String, dynamic> argument) async {
|
||||||
|
await initEnv(kAppTypeDesktopPortForward);
|
||||||
|
runApp(
|
||||||
|
GetMaterialApp(
|
||||||
|
navigatorKey: globalKey,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
title: 'RustDesk - Port Forward',
|
||||||
|
theme: getCurrentTheme(),
|
||||||
|
home: DesktopPortForwardScreen(params: argument),
|
||||||
|
navigatorObservers: [
|
||||||
|
// FirebaseAnalyticsObserver(analytics: analytics),
|
||||||
|
],
|
||||||
|
builder: _keepScaleBuilder(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void runConnectionManagerScreen() async {
|
void runConnectionManagerScreen() async {
|
||||||
// initialize window
|
// initialize window
|
||||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400));
|
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400));
|
||||||
@ -182,7 +205,7 @@ class App extends StatelessWidget {
|
|||||||
title: 'RustDesk',
|
title: 'RustDesk',
|
||||||
theme: getCurrentTheme(),
|
theme: getCurrentTheme(),
|
||||||
home: isDesktop
|
home: isDesktop
|
||||||
? DesktopTabPage()
|
? const DesktopTabPage()
|
||||||
: !isAndroid
|
: !isAndroid
|
||||||
? WebHomePage()
|
? WebHomePage()
|
||||||
: HomePage(),
|
: HomePage(),
|
||||||
@ -190,8 +213,13 @@ class App extends StatelessWidget {
|
|||||||
// FirebaseAnalyticsObserver(analytics: analytics),
|
// FirebaseAnalyticsObserver(analytics: analytics),
|
||||||
],
|
],
|
||||||
builder: isAndroid
|
builder: isAndroid
|
||||||
? (_, child) => AccessibilityListener(
|
? (context, child) => AccessibilityListener(
|
||||||
child: child,
|
child: MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
textScaleFactor: 1.0,
|
||||||
|
),
|
||||||
|
child: child ?? Container(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: _keepScaleBuilder(),
|
: _keepScaleBuilder(),
|
||||||
),
|
),
|
||||||
|
@ -7,6 +7,7 @@ import 'dart:ui' as ui;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/generated_bridge.dart';
|
import 'package:flutter_hbb/generated_bridge.dart';
|
||||||
import 'package:flutter_hbb/models/ab_model.dart';
|
import 'package:flutter_hbb/models/ab_model.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
@ -17,6 +18,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
|
import '../common/shared_state.dart';
|
||||||
import '../mobile/widgets/dialog.dart';
|
import '../mobile/widgets/dialog.dart';
|
||||||
import '../mobile/widgets/overlay.dart';
|
import '../mobile/widgets/overlay.dart';
|
||||||
import 'peer_model.dart';
|
import 'peer_model.dart';
|
||||||
@ -96,25 +98,26 @@ class FfiModel with ChangeNotifier {
|
|||||||
clearPermissions();
|
clearPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setConnectionType(bool secure, bool direct) {
|
void setConnectionType(String peerId, bool secure, bool direct) {
|
||||||
_secure = secure;
|
_secure = secure;
|
||||||
_direct = direct;
|
_direct = direct;
|
||||||
|
try {
|
||||||
|
var connectionType = ConnectionTypeState.find(peerId);
|
||||||
|
connectionType.setSecure(secure);
|
||||||
|
connectionType.setDirect(direct);
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Image? getConnectionImage() {
|
Image? getConnectionImage() {
|
||||||
String? icon;
|
if (secure == null || direct == null) {
|
||||||
if (secure == true && direct == true) {
|
return null;
|
||||||
icon = 'secure';
|
} else {
|
||||||
} else if (secure == false && direct == true) {
|
final icon =
|
||||||
icon = 'insecure';
|
'${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}';
|
||||||
} else if (secure == false && direct == false) {
|
return Image.asset('assets/$icon.png', width: 48, height: 48);
|
||||||
icon = 'insecure_relay';
|
|
||||||
} else if (secure == true && direct == false) {
|
|
||||||
icon = 'secure_relay';
|
|
||||||
}
|
}
|
||||||
return icon == null
|
|
||||||
? null
|
|
||||||
: Image.asset('assets/$icon.png', width: 48, height: 48);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearPermissions() {
|
void clearPermissions() {
|
||||||
@ -130,7 +133,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'peer_info') {
|
} else if (name == 'peer_info') {
|
||||||
handlePeerInfo(evt, peerId);
|
handlePeerInfo(evt, peerId);
|
||||||
} else if (name == 'connection_ready') {
|
} else if (name == 'connection_ready') {
|
||||||
setConnectionType(evt['secure'] == 'true', evt['direct'] == 'true');
|
setConnectionType(
|
||||||
|
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
|
||||||
} else if (name == 'switch_display') {
|
} else if (name == 'switch_display') {
|
||||||
handleSwitchDisplay(evt);
|
handleSwitchDisplay(evt);
|
||||||
} else if (name == 'cursor_data') {
|
} else if (name == 'cursor_data') {
|
||||||
@ -172,9 +176,9 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'update_quality_status') {
|
} else if (name == 'update_quality_status') {
|
||||||
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
|
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
|
||||||
} else if (name == 'update_block_input_state') {
|
} else if (name == 'update_block_input_state') {
|
||||||
updateBlockInputState(evt);
|
updateBlockInputState(evt, peerId);
|
||||||
} else if (name == 'update_privacy_mode') {
|
} else if (name == 'update_privacy_mode') {
|
||||||
updatePrivacyMode(evt);
|
updatePrivacyMode(evt, peerId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -189,7 +193,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
handlePeerInfo(evt, peerId);
|
handlePeerInfo(evt, peerId);
|
||||||
} else if (name == 'connection_ready') {
|
} else if (name == 'connection_ready') {
|
||||||
parent.target?.ffiModel.setConnectionType(
|
parent.target?.ffiModel.setConnectionType(
|
||||||
evt['secure'] == 'true', evt['direct'] == 'true');
|
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
|
||||||
} else if (name == 'switch_display') {
|
} else if (name == 'switch_display') {
|
||||||
handleSwitchDisplay(evt);
|
handleSwitchDisplay(evt);
|
||||||
} else if (name == 'cursor_data') {
|
} else if (name == 'cursor_data') {
|
||||||
@ -231,9 +235,9 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'update_quality_status') {
|
} else if (name == 'update_quality_status') {
|
||||||
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
|
parent.target?.qualityMonitorModel.updateQualityStatus(evt);
|
||||||
} else if (name == 'update_block_input_state') {
|
} else if (name == 'update_block_input_state') {
|
||||||
updateBlockInputState(evt);
|
updateBlockInputState(evt, peerId);
|
||||||
} else if (name == 'update_privacy_mode') {
|
} else if (name == 'update_privacy_mode') {
|
||||||
updatePrivacyMode(evt);
|
updatePrivacyMode(evt, peerId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
platformFFI.setEventCallback(cb);
|
platformFFI.setEventCallback(cb);
|
||||||
@ -297,6 +301,9 @@ class FfiModel with ChangeNotifier {
|
|||||||
|
|
||||||
/// Handle the peer info event based on [evt].
|
/// Handle the peer info event based on [evt].
|
||||||
void handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
|
void handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
|
||||||
|
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
|
||||||
|
bind.mainLoadRecentPeers();
|
||||||
|
|
||||||
parent.target?.dialogManager.dismissAll();
|
parent.target?.dialogManager.dismissAll();
|
||||||
_pi.version = evt['version'];
|
_pi.version = evt['version'];
|
||||||
_pi.username = evt['username'];
|
_pi.username = evt['username'];
|
||||||
@ -305,6 +312,12 @@ class FfiModel with ChangeNotifier {
|
|||||||
_pi.sasEnabled = evt['sas_enabled'] == "true";
|
_pi.sasEnabled = evt['sas_enabled'] == "true";
|
||||||
_pi.currentDisplay = int.parse(evt['current_display']);
|
_pi.currentDisplay = int.parse(evt['current_display']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
if (isPeerAndroid) {
|
if (isPeerAndroid) {
|
||||||
_touchMode = true;
|
_touchMode = true;
|
||||||
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
|
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
|
||||||
@ -316,6 +329,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (evt['is_file_transfer'] == "true") {
|
if (evt['is_file_transfer'] == "true") {
|
||||||
|
// TODO is file transfer
|
||||||
parent.target?.fileModel.onReady();
|
parent.target?.fileModel.onReady();
|
||||||
} else {
|
} else {
|
||||||
_pi.displays = [];
|
_pi.displays = [];
|
||||||
@ -343,13 +357,24 @@ class FfiModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBlockInputState(Map<String, dynamic> evt) {
|
updateBlockInputState(Map<String, dynamic> evt, String peerId) {
|
||||||
_inputBlocked = evt['input_state'] == 'on';
|
_inputBlocked = evt['input_state'] == 'on';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
BlockInputState.find(peerId).value = evt['input_state'] == 'on';
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePrivacyMode(Map<String, dynamic> evt) {
|
updatePrivacyMode(Map<String, dynamic> evt, String peerId) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
PrivacyModeState.find(peerId).value =
|
||||||
|
bind.sessionGetToggleOptionSync(id: peerId, arg: 'privacy-mode');
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -476,39 +501,11 @@ class CanvasModel with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720);
|
_scale = 1.0;
|
||||||
final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280);
|
if (style == 'adaptive') {
|
||||||
|
final s1 = size.width / getDisplayWidth();
|
||||||
// Closure to perform shrink operation.
|
final s2 = size.height / getDisplayHeight();
|
||||||
final shrinkOp = () {
|
_scale = s1 < s2 ? s1 : s2;
|
||||||
final s = s1 < s2 ? s1 : s2;
|
|
||||||
if (s < 1) {
|
|
||||||
_scale = s;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Closure to perform stretch operation.
|
|
||||||
final stretchOp = () {
|
|
||||||
final s = s1 < s2 ? s1 : s2;
|
|
||||||
if (s > 1) {
|
|
||||||
_scale = s;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Closure to perform default operation(set the scale to 1.0).
|
|
||||||
final defaultOp = () {
|
|
||||||
_scale = 1.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// // On desktop, shrink is the default behavior.
|
|
||||||
// if (isDesktop) {
|
|
||||||
// shrinkOp();
|
|
||||||
// } else {
|
|
||||||
defaultOp();
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (style == 'shrink') {
|
|
||||||
shrinkOp();
|
|
||||||
} else if (style == 'stretch') {
|
|
||||||
stretchOp();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_x = (size.width - getDisplayWidth() * _scale) / 2;
|
_x = (size.width - getDisplayWidth() * _scale) / 2;
|
||||||
@ -536,11 +533,17 @@ class CanvasModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int getDisplayWidth() {
|
int getDisplayWidth() {
|
||||||
return parent.target?.ffiModel.display.width ?? 1080;
|
final defaultWidth = (isDesktop || isWebDesktop)
|
||||||
|
? kDesktopDefaultDisplayWidth
|
||||||
|
: kMobileDefaultDisplayWidth;
|
||||||
|
return parent.target?.ffiModel.display.width ?? defaultWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
int getDisplayHeight() {
|
int getDisplayHeight() {
|
||||||
return parent.target?.ffiModel.display.height ?? 720;
|
final defaultHeight = (isDesktop || isWebDesktop)
|
||||||
|
? kDesktopDefaultDisplayHeight
|
||||||
|
: kMobileDefaultDisplayHeight;
|
||||||
|
return parent.target?.ffiModel.display.height ?? defaultHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
Size get size {
|
Size get size {
|
||||||
@ -556,9 +559,19 @@ class CanvasModel with ChangeNotifier {
|
|||||||
var dxOffset = 0;
|
var dxOffset = 0;
|
||||||
var dyOffset = 0;
|
var dyOffset = 0;
|
||||||
if (dw > size.width) {
|
if (dw > size.width) {
|
||||||
|
final X_debugNanOrInfinite = x - dw * (x / size.width) - _x;
|
||||||
|
if (X_debugNanOrInfinite.isInfinite || X_debugNanOrInfinite.isNaN) {
|
||||||
|
debugPrint(
|
||||||
|
'REMOVE ME ============================ X_debugNanOrInfinite $x,$dw,$_scale,${size.width},$_x');
|
||||||
|
}
|
||||||
dxOffset = (x - dw * (x / size.width) - _x).toInt();
|
dxOffset = (x - dw * (x / size.width) - _x).toInt();
|
||||||
}
|
}
|
||||||
if (dh > size.height) {
|
if (dh > size.height) {
|
||||||
|
final Y_debugNanOrInfinite = y - dh * (y / size.height) - _y;
|
||||||
|
if (Y_debugNanOrInfinite.isInfinite || Y_debugNanOrInfinite.isNaN) {
|
||||||
|
debugPrint(
|
||||||
|
'REMOVE ME ============================ Y_debugNanOrInfinite $y,$dh,$_scale,${size.height},$_y');
|
||||||
|
}
|
||||||
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
||||||
}
|
}
|
||||||
_x += dxOffset;
|
_x += dxOffset;
|
||||||
@ -926,16 +939,16 @@ class FFI {
|
|||||||
late final QualityMonitorModel qualityMonitorModel; // session
|
late final QualityMonitorModel qualityMonitorModel; // session
|
||||||
|
|
||||||
FFI() {
|
FFI() {
|
||||||
this.imageModel = ImageModel(WeakReference(this));
|
imageModel = ImageModel(WeakReference(this));
|
||||||
this.ffiModel = FfiModel(WeakReference(this));
|
ffiModel = FfiModel(WeakReference(this));
|
||||||
this.cursorModel = CursorModel(WeakReference(this));
|
cursorModel = CursorModel(WeakReference(this));
|
||||||
this.canvasModel = CanvasModel(WeakReference(this));
|
canvasModel = CanvasModel(WeakReference(this));
|
||||||
this.serverModel = ServerModel(WeakReference(this)); // use global FFI
|
serverModel = ServerModel(WeakReference(this)); // use global FFI
|
||||||
this.chatModel = ChatModel(WeakReference(this));
|
chatModel = ChatModel(WeakReference(this));
|
||||||
this.fileModel = FileModel(WeakReference(this));
|
fileModel = FileModel(WeakReference(this));
|
||||||
this.abModel = AbModel(WeakReference(this));
|
abModel = AbModel(WeakReference(this));
|
||||||
this.userModel = UserModel(WeakReference(this));
|
userModel = UserModel(WeakReference(this));
|
||||||
this.qualityMonitorModel = QualityMonitorModel(WeakReference(this));
|
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a mouse tap event(down and up).
|
/// Send a mouse tap event(down and up).
|
||||||
@ -983,7 +996,7 @@ class FFI {
|
|||||||
// Raw Key
|
// Raw Key
|
||||||
void inputRawKey(int keyCode, int scanCode, bool down){
|
void inputRawKey(int keyCode, int scanCode, bool down){
|
||||||
debugPrint(scanCode.toString());
|
debugPrint(scanCode.toString());
|
||||||
bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down);
|
// bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send key stroke event.
|
/// Send key stroke event.
|
||||||
@ -1040,17 +1053,26 @@ class FFI {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect with the given [id]. Only transfer file if [isFileTransfer].
|
/// Connect with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
|
||||||
void connect(String id,
|
void connect(String id,
|
||||||
{bool isFileTransfer = false, double tabBarHeight = 0.0}) {
|
{bool isFileTransfer = false,
|
||||||
if (!isFileTransfer) {
|
bool isPortForward = false,
|
||||||
|
double tabBarHeight = 0.0}) {
|
||||||
|
assert(!(isFileTransfer && isPortForward), "more than one connect type");
|
||||||
|
if (isFileTransfer) {
|
||||||
|
id = 'ft_${id}';
|
||||||
|
} else if (isPortForward) {
|
||||||
|
id = 'pf_${id}';
|
||||||
|
} else {
|
||||||
chatModel.resetClientMode();
|
chatModel.resetClientMode();
|
||||||
canvasModel.id = id;
|
canvasModel.id = id;
|
||||||
imageModel._id = id;
|
imageModel._id = id;
|
||||||
cursorModel.id = id;
|
cursorModel.id = id;
|
||||||
}
|
}
|
||||||
id = isFileTransfer ? 'ft_${id}' : id;
|
// ignore: unused_local_variable
|
||||||
final stream = bind.sessionConnect(id: id, isFileTransfer: isFileTransfer);
|
final addRes = bind.sessionAddSync(
|
||||||
|
id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward);
|
||||||
|
final stream = bind.sessionStart(id: id);
|
||||||
final cb = ffiModel.startEventListener(id);
|
final cb = ffiModel.startEventListener(id);
|
||||||
() async {
|
() async {
|
||||||
await for (final message in stream) {
|
await for (final message in stream) {
|
||||||
@ -1092,7 +1114,7 @@ class FFI {
|
|||||||
ffiModel.clear();
|
ffiModel.clear();
|
||||||
canvasModel.clear();
|
canvasModel.clear();
|
||||||
resetModifiers();
|
resetModifiers();
|
||||||
print("model closed");
|
debugPrint("model $id closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send **get** command to the Rust core based on [name] and [arg].
|
/// Send **get** command to the Rust core based on [name] and [arg].
|
||||||
@ -1236,20 +1258,20 @@ class PeerInfo {
|
|||||||
Future<void> savePreference(String id, double xCursor, double yCursor,
|
Future<void> savePreference(String id, double xCursor, double yCursor,
|
||||||
double xCanvas, double yCanvas, double scale, int currentDisplay) async {
|
double xCanvas, double yCanvas, double scale, int currentDisplay) async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
final p = Map<String, dynamic>();
|
final p = <String, dynamic>{};
|
||||||
p['xCursor'] = xCursor;
|
p['xCursor'] = xCursor;
|
||||||
p['yCursor'] = yCursor;
|
p['yCursor'] = yCursor;
|
||||||
p['xCanvas'] = xCanvas;
|
p['xCanvas'] = xCanvas;
|
||||||
p['yCanvas'] = yCanvas;
|
p['yCanvas'] = yCanvas;
|
||||||
p['scale'] = scale;
|
p['scale'] = scale;
|
||||||
p['currentDisplay'] = currentDisplay;
|
p['currentDisplay'] = currentDisplay;
|
||||||
prefs.setString('peer' + id, json.encode(p));
|
prefs.setString('peer$id', json.encode(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> getPreference(String id) async {
|
Future<Map<String, dynamic>?> getPreference(String id) async {
|
||||||
if (!isWebDesktop) return null;
|
if (!isWebDesktop) return null;
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
var p = prefs.getString('peer' + id);
|
var p = prefs.getString('peer$id');
|
||||||
if (p == null) return null;
|
if (p == null) return null;
|
||||||
Map<String, dynamic> m = json.decode(p);
|
Map<String, dynamic> m = json.decode(p);
|
||||||
return m;
|
return m;
|
||||||
@ -1257,7 +1279,7 @@ Future<Map<String, dynamic>?> getPreference(String id) async {
|
|||||||
|
|
||||||
void removePreference(String id) async {
|
void removePreference(String id) async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
prefs.remove('peer' + id);
|
prefs.remove('peer$id');
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeCursorAndCanvas(FFI ffi) async {
|
void initializeCursorAndCanvas(FFI ffi) async {
|
||||||
|
@ -30,7 +30,7 @@ class PlatformFFI {
|
|||||||
String _dir = '';
|
String _dir = '';
|
||||||
String _homeDir = '';
|
String _homeDir = '';
|
||||||
F2? _translate;
|
F2? _translate;
|
||||||
var _eventHandlers = Map<String, Map<String, HandleEvent>>();
|
final _eventHandlers = Map<String, Map<String, HandleEvent>>();
|
||||||
late RustdeskImpl _ffiBind;
|
late RustdeskImpl _ffiBind;
|
||||||
late String _appType;
|
late String _appType;
|
||||||
void Function(Map<String, dynamic>)? _eventCallback;
|
void Function(Map<String, dynamic>)? _eventCallback;
|
||||||
@ -50,27 +50,27 @@ class PlatformFFI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool registerEventHandler(
|
bool registerEventHandler(
|
||||||
String event_name, String handler_name, HandleEvent handler) {
|
String eventName, String handlerName, HandleEvent handler) {
|
||||||
debugPrint('registerEventHandler $event_name $handler_name');
|
debugPrint('registerEventHandler $eventName $handlerName');
|
||||||
var handlers = _eventHandlers[event_name];
|
var handlers = _eventHandlers[eventName];
|
||||||
if (handlers == null) {
|
if (handlers == null) {
|
||||||
_eventHandlers[event_name] = {handler_name: handler};
|
_eventHandlers[eventName] = {handlerName: handler};
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (handlers.containsKey(handler_name)) {
|
if (handlers.containsKey(handlerName)) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
handlers[handler_name] = handler;
|
handlers[handlerName] = handler;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void unregisterEventHandler(String event_name, String handler_name) {
|
void unregisterEventHandler(String eventName, String handlerName) {
|
||||||
debugPrint('unregisterEventHandler $event_name $handler_name');
|
debugPrint('unregisterEventHandler $eventName $handlerName');
|
||||||
var handlers = _eventHandlers[event_name];
|
var handlers = _eventHandlers[eventName];
|
||||||
if (handlers != null) {
|
if (handlers != null) {
|
||||||
handlers.remove(handler_name);
|
handlers.remove(handlerName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ class PlatformFFI {
|
|||||||
_homeDir = (await getDownloadsDirectory())?.path ?? "";
|
_homeDir = (await getDownloadsDirectory())?.path ?? "";
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
print("initialize failed: $e");
|
||||||
}
|
}
|
||||||
String id = 'NA';
|
String id = 'NA';
|
||||||
String name = 'Flutter';
|
String name = 'Flutter';
|
||||||
@ -151,7 +151,7 @@ class PlatformFFI {
|
|||||||
await _ffiBind.mainSetHomeDir(home: _homeDir);
|
await _ffiBind.mainSetHomeDir(home: _homeDir);
|
||||||
await _ffiBind.mainInit(appDir: _dir);
|
await _ffiBind.mainInit(appDir: _dir);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
print("initialize failed: $e");
|
||||||
}
|
}
|
||||||
version = await getVersion();
|
version = await getVersion();
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,8 @@ class Peer {
|
|||||||
final List<dynamic> tags;
|
final List<dynamic> tags;
|
||||||
bool online = false;
|
bool online = false;
|
||||||
|
|
||||||
Peer.fromJson(String id, Map<String, dynamic> json)
|
Peer.fromJson(this.id, Map<String, dynamic> json)
|
||||||
: id = id,
|
: username = json['username'] ?? '',
|
||||||
username = json['username'] ?? '',
|
|
||||||
hostname = json['hostname'] ?? '',
|
hostname = json['hostname'] ?? '',
|
||||||
platform = json['platform'] ?? '',
|
platform = json['platform'] ?? '',
|
||||||
tags = json['tags'] ?? [];
|
tags = json['tags'] ?? [];
|
||||||
@ -35,57 +34,52 @@ class Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Peers extends ChangeNotifier {
|
class Peers extends ChangeNotifier {
|
||||||
late String _name;
|
final String name;
|
||||||
late List<Peer> _peers;
|
final String loadEvent;
|
||||||
late final _loadEvent;
|
List<Peer> peers;
|
||||||
static const _cbQueryOnlines = 'callback_query_onlines';
|
static const _cbQueryOnlines = 'callback_query_onlines';
|
||||||
|
|
||||||
Peers(String name, String loadEvent, List<Peer> _initPeers) {
|
Peers({required this.name, required this.peers, required this.loadEvent}) {
|
||||||
_name = name;
|
platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) {
|
||||||
_loadEvent = loadEvent;
|
|
||||||
_peers = _initPeers;
|
|
||||||
platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) {
|
|
||||||
_updateOnlineState(evt);
|
_updateOnlineState(evt);
|
||||||
});
|
});
|
||||||
platformFFI.registerEventHandler(_loadEvent, _name, (evt) {
|
platformFFI.registerEventHandler(loadEvent, name, (evt) {
|
||||||
_updatePeers(evt);
|
_updatePeers(evt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Peer> get peers => _peers;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
platformFFI.unregisterEventHandler(_cbQueryOnlines, _name);
|
platformFFI.unregisterEventHandler(_cbQueryOnlines, name);
|
||||||
platformFFI.unregisterEventHandler(_loadEvent, _name);
|
platformFFI.unregisterEventHandler(loadEvent, name);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Peer getByIndex(int index) {
|
Peer getByIndex(int index) {
|
||||||
if (index < _peers.length) {
|
if (index < peers.length) {
|
||||||
return _peers[index];
|
return peers[index];
|
||||||
} else {
|
} else {
|
||||||
return Peer.loading();
|
return Peer.loading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int getPeersCount() {
|
int getPeersCount() {
|
||||||
return _peers.length;
|
return peers.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateOnlineState(Map<String, dynamic> evt) {
|
void _updateOnlineState(Map<String, dynamic> evt) {
|
||||||
evt['onlines'].split(',').forEach((online) {
|
evt['onlines'].split(',').forEach((online) {
|
||||||
for (var i = 0; i < _peers.length; i++) {
|
for (var i = 0; i < peers.length; i++) {
|
||||||
if (_peers[i].id == online) {
|
if (peers[i].id == online) {
|
||||||
_peers[i].online = true;
|
peers[i].online = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
evt['offlines'].split(',').forEach((offline) {
|
evt['offlines'].split(',').forEach((offline) {
|
||||||
for (var i = 0; i < _peers.length; i++) {
|
for (var i = 0; i < peers.length; i++) {
|
||||||
if (_peers[i].id == offline) {
|
if (peers[i].id == offline) {
|
||||||
_peers[i].online = false;
|
peers[i].online = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -95,19 +89,19 @@ class Peers extends ChangeNotifier {
|
|||||||
|
|
||||||
void _updatePeers(Map<String, dynamic> evt) {
|
void _updatePeers(Map<String, dynamic> evt) {
|
||||||
final onlineStates = _getOnlineStates();
|
final onlineStates = _getOnlineStates();
|
||||||
_peers = _decodePeers(evt['peers']);
|
peers = _decodePeers(evt['peers']);
|
||||||
_peers.forEach((peer) {
|
for (var peer in peers) {
|
||||||
final state = onlineStates[peer.id];
|
final state = onlineStates[peer.id];
|
||||||
peer.online = state != null && state != false;
|
peer.online = state != null && state != false;
|
||||||
});
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, bool> _getOnlineStates() {
|
Map<String, bool> _getOnlineStates() {
|
||||||
var onlineStates = new Map<String, bool>();
|
var onlineStates = <String, bool>{};
|
||||||
_peers.forEach((peer) {
|
for (var peer in peers) {
|
||||||
onlineStates[peer.id] = peer.online;
|
onlineStates[peer.id] = peer.online;
|
||||||
});
|
}
|
||||||
return onlineStates;
|
return onlineStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +115,7 @@ class Peers extends ChangeNotifier {
|
|||||||
Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
|
Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('peers(): $e');
|
debugPrint('peers(): $e');
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -35,10 +34,11 @@ class RustDeskMultiWindowManager {
|
|||||||
|
|
||||||
int? _remoteDesktopWindowId;
|
int? _remoteDesktopWindowId;
|
||||||
int? _fileTransferWindowId;
|
int? _fileTransferWindowId;
|
||||||
|
int? _portForwardWindowId;
|
||||||
|
|
||||||
Future<dynamic> new_remote_desktop(String remote_id) async {
|
Future<dynamic> newRemoteDesktop(String remoteId) async {
|
||||||
final msg =
|
final msg =
|
||||||
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id});
|
jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||||
@ -62,9 +62,9 @@ class RustDeskMultiWindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> new_file_transfer(String remote_id) async {
|
Future<dynamic> newFileTransfer(String remoteId) async {
|
||||||
final msg =
|
final msg =
|
||||||
jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id});
|
jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||||
@ -87,6 +87,31 @@ class RustDeskMultiWindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<dynamic> newPortForward(String remoteId, bool isRDP) async {
|
||||||
|
final msg = jsonEncode(
|
||||||
|
{"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
||||||
|
if (!ids.contains(_portForwardWindowId)) {
|
||||||
|
_portForwardWindowId = null;
|
||||||
|
}
|
||||||
|
} on Error {
|
||||||
|
_portForwardWindowId = null;
|
||||||
|
}
|
||||||
|
if (_portForwardWindowId == null) {
|
||||||
|
final portForwardController = await DesktopMultiWindow.createWindow(msg);
|
||||||
|
portForwardController
|
||||||
|
..setFrame(const Offset(0, 0) & const Size(1280, 720))
|
||||||
|
..center()
|
||||||
|
..setTitle("rustdesk - port forward")
|
||||||
|
..show();
|
||||||
|
_portForwardWindowId = portForwardController.windowId;
|
||||||
|
} else {
|
||||||
|
return call(WindowType.PortForward, "new_port_forward", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
|
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
|
||||||
int? windowId = findWindowByType(type);
|
int? windowId = findWindowByType(type);
|
||||||
if (windowId == null) {
|
if (windowId == null) {
|
||||||
@ -104,7 +129,7 @@ class RustDeskMultiWindowManager {
|
|||||||
case WindowType.FileTransfer:
|
case WindowType.FileTransfer:
|
||||||
return _fileTransferWindowId;
|
return _fileTransferWindowId;
|
||||||
case WindowType.PortForward:
|
case WindowType.PortForward:
|
||||||
break;
|
return _portForwardWindowId;
|
||||||
case WindowType.Unknown:
|
case WindowType.Unknown:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -120,7 +145,7 @@ class RustDeskMultiWindowManager {
|
|||||||
await Future.wait(WindowType.values.map((e) => closeWindows(e)));
|
await Future.wait(WindowType.values.map((e) => closeWindows(e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeWindows(WindowType type) async {
|
Future<void> closeWindows(WindowType type) async {
|
||||||
if (type == WindowType.Main) {
|
if (type == WindowType.Main) {
|
||||||
// skip main window, use window manager instead
|
// skip main window, use window manager instead
|
||||||
return;
|
return;
|
||||||
|
@ -235,12 +235,10 @@ packages:
|
|||||||
dash_chat_2:
|
dash_chat_2:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
name: dash_chat_2
|
||||||
ref: feat_maxWidth
|
url: "https://pub.flutter-io.cn"
|
||||||
resolved-ref: "3946ecf86d3600b54632fd80d0eb0ef0e74f2d6a"
|
source: hosted
|
||||||
url: "https://github.com/fufesou/Dash-Chat-2"
|
version: "0.0.14"
|
||||||
source: git
|
|
||||||
version: "0.0.12"
|
|
||||||
desktop_drop:
|
desktop_drop:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -252,8 +250,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: e013c81d75320bbf28adddeaadf462264ee6039d
|
ref: e0368a023ba195462acc00d33ab361b499f0e413
|
||||||
resolved-ref: e013c81d75320bbf28adddeaadf462264ee6039d
|
resolved-ref: e0368a023ba195462acc00d33ab361b499f0e413
|
||||||
url: "https://github.com/Kingtous/rustdesk_desktop_multi_window"
|
url: "https://github.com/Kingtous/rustdesk_desktop_multi_window"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
@ -849,7 +847,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: rxdart
|
name: rxdart
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
@ -1244,8 +1242,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: a25f1776ccc1119cbb2a8541174293aa36d532ed
|
ref: "799ef079e87938c3f4340591b4330c2598f38bb9"
|
||||||
resolved-ref: a25f1776ccc1119cbb2a8541174293aa36d532ed
|
resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9"
|
||||||
url: "https://github.com/Kingtous/rustdesk_window_manager"
|
url: "https://github.com/Kingtous/rustdesk_window_manager"
|
||||||
source: git
|
source: git
|
||||||
version: "0.2.6"
|
version: "0.2.6"
|
||||||
|
@ -34,16 +34,13 @@ dependencies:
|
|||||||
provider: ^6.0.3
|
provider: ^6.0.3
|
||||||
tuple: ^2.0.0
|
tuple: ^2.0.0
|
||||||
wakelock: ^0.5.2
|
wakelock: ^0.5.2
|
||||||
device_info_plus: ^4.0.2
|
device_info_plus: ^4.1.2
|
||||||
firebase_analytics: ^9.1.5
|
firebase_analytics: ^9.1.5
|
||||||
package_info_plus: ^1.4.2
|
package_info_plus: ^1.4.2
|
||||||
url_launcher: ^6.0.9
|
url_launcher: ^6.0.9
|
||||||
shared_preferences: ^2.0.6
|
shared_preferences: ^2.0.6
|
||||||
toggle_switch: ^1.4.0
|
toggle_switch: ^1.4.0
|
||||||
dash_chat_2:
|
dash_chat_2: ^0.0.14
|
||||||
git:
|
|
||||||
url: https://github.com/fufesou/Dash-Chat-2
|
|
||||||
ref: feat_maxWidth
|
|
||||||
draggable_float_widget: ^0.0.2
|
draggable_float_widget: ^0.0.2
|
||||||
settings_ui: ^2.0.2
|
settings_ui: ^2.0.2
|
||||||
flutter_breadcrumb: ^1.0.1
|
flutter_breadcrumb: ^1.0.1
|
||||||
@ -61,11 +58,11 @@ dependencies:
|
|||||||
window_manager:
|
window_manager:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Kingtous/rustdesk_window_manager
|
url: https://github.com/Kingtous/rustdesk_window_manager
|
||||||
ref: a25f1776ccc1119cbb2a8541174293aa36d532ed
|
ref: 799ef079e87938c3f4340591b4330c2598f38bb9
|
||||||
desktop_multi_window:
|
desktop_multi_window:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
||||||
ref: e013c81d75320bbf28adddeaadf462264ee6039d
|
ref: e0368a023ba195462acc00d33ab361b499f0e413
|
||||||
freezed_annotation: ^2.0.3
|
freezed_annotation: ^2.0.3
|
||||||
tray_manager:
|
tray_manager:
|
||||||
git:
|
git:
|
||||||
@ -76,6 +73,7 @@ dependencies:
|
|||||||
contextmenu: ^3.0.0
|
contextmenu: ^3.0.0
|
||||||
desktop_drop: ^0.3.3
|
desktop_drop: ^0.3.3
|
||||||
scroll_pos: ^0.3.0
|
scroll_pos: ^0.3.0
|
||||||
|
rxdart: ^0.27.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_launcher_icons: ^0.9.1
|
flutter_launcher_icons: ^0.9.1
|
||||||
|
@ -5,25 +5,36 @@ import os
|
|||||||
import glob
|
import glob
|
||||||
from tabnanny import check
|
from tabnanny import check
|
||||||
|
|
||||||
|
def pad_start(s, n, c = ' '):
|
||||||
|
if len(s) >= n:
|
||||||
|
return s
|
||||||
|
return c * (n - len(s)) + s
|
||||||
|
|
||||||
|
def safe_unicode(s):
|
||||||
|
res = ""
|
||||||
|
for c in s:
|
||||||
|
res += r"\u{}".format(pad_start(hex(ord(c))[2:], 4, '0'))
|
||||||
|
return res
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print('export const LANGS = {')
|
print('export const LANGS = {')
|
||||||
for fn in glob.glob('../../../src/lang/*'):
|
for fn in glob.glob('../../../src/lang/*'):
|
||||||
lang = os.path.basename(fn)[:-3]
|
lang = os.path.basename(fn)[:-3]
|
||||||
if lang == 'template': continue
|
if lang == 'template': continue
|
||||||
print(' %s: {'%lang)
|
print(' %s: {'%lang)
|
||||||
for ln in open(fn):
|
for ln in open(fn, encoding='utf-8'):
|
||||||
ln = ln.strip()
|
ln = ln.strip()
|
||||||
if ln.startswith('("'):
|
if ln.startswith('("'):
|
||||||
toks = ln.split('", "')
|
toks = ln.split('", "')
|
||||||
assert(len(toks) == 2)
|
assert(len(toks) == 2)
|
||||||
a = toks[0][2:]
|
a = toks[0][2:]
|
||||||
b = toks[1][:-3]
|
b = toks[1][:-3]
|
||||||
print(' "%s": "%s",'%(a, b))
|
print(' "%s": "%s",'%(safe_unicode(a), safe_unicode(b)))
|
||||||
print(' },')
|
print(' },')
|
||||||
print('}')
|
print('}')
|
||||||
check_if_retry = ['', False]
|
check_if_retry = ['', False]
|
||||||
KEY_MAP = ['', False]
|
KEY_MAP = ['', False]
|
||||||
for ln in open('../../../src/client.rs'):
|
for ln in open('../../../src/client.rs', encoding='utf-8'):
|
||||||
ln = ln.strip()
|
ln = ln.strip()
|
||||||
if 'check_if_retry' in ln:
|
if 'check_if_retry' in ln:
|
||||||
check_if_retry[1] = True
|
check_if_retry[1] = True
|
||||||
@ -55,7 +66,7 @@ def main():
|
|||||||
print('export const KEY_MAP: any = {')
|
print('export const KEY_MAP: any = {')
|
||||||
print(KEY_MAP[0])
|
print(KEY_MAP[0])
|
||||||
print('}')
|
print('}')
|
||||||
for ln in open('../../../Cargo.toml'):
|
for ln in open('../../../Cargo.toml', encoding='utf-8'):
|
||||||
if ln.startswith('version ='):
|
if ln.startswith('version ='):
|
||||||
print('export const ' + ln)
|
print('export const ' + ln)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
ops::{Deref, Not},
|
ops::{Deref, Not},
|
||||||
sync::{mpsc, Arc, Mutex, RwLock},
|
sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use async_trait::async_trait;
|
pub use async_trait::async_trait;
|
||||||
@ -37,7 +37,6 @@ use hbb_common::{
|
|||||||
};
|
};
|
||||||
pub use helper::LatencyController;
|
pub use helper::LatencyController;
|
||||||
pub use helper::*;
|
pub use helper::*;
|
||||||
use scrap::Image;
|
|
||||||
use scrap::{
|
use scrap::{
|
||||||
codec::{Decoder, DecoderCfg},
|
codec::{Decoder, DecoderCfg},
|
||||||
VpxDecoderConfig, VpxVideoCodecId,
|
VpxDecoderConfig, VpxVideoCodecId,
|
||||||
@ -47,7 +46,12 @@ pub use super::lang::*;
|
|||||||
|
|
||||||
pub mod file_trait;
|
pub mod file_trait;
|
||||||
pub mod helper;
|
pub mod helper;
|
||||||
|
pub mod io_loop;
|
||||||
|
|
||||||
|
pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||||
|
pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||||
|
pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||||
|
pub const MILLI1: Duration = Duration::from_millis(1);
|
||||||
pub const SEC30: Duration = Duration::from_secs(30);
|
pub const SEC30: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
/// Client of the remote desktop.
|
/// Client of the remote desktop.
|
||||||
@ -55,7 +59,23 @@ pub struct Client;
|
|||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref AUDIO_HOST: Host = cpal::default_host();
|
static ref AUDIO_HOST: Host = cpal::default_host();
|
||||||
|
}
|
||||||
|
use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState};
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref ENIGO: Arc<Mutex<enigo::Enigo>> = Arc::new(Mutex::new(enigo::Enigo::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn get_key_state(key: enigo::Key) -> bool {
|
||||||
|
use enigo::KeyboardControllable;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if key == enigo::Key::NumLock {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ENIGO.lock().unwrap().get_key_state(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
@ -846,8 +866,7 @@ impl VideoHandler {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct LoginConfigHandler {
|
pub struct LoginConfigHandler {
|
||||||
id: String,
|
id: String,
|
||||||
pub is_file_transfer: bool,
|
pub conn_type: ConnType,
|
||||||
is_port_forward: bool,
|
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
password: Vec<u8>, // remember password for reconnect
|
password: Vec<u8>, // remember password for reconnect
|
||||||
pub remember: bool,
|
pub remember: bool,
|
||||||
@ -886,12 +905,10 @@ impl LoginConfigHandler {
|
|||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `id` - id of peer
|
/// * `id` - id of peer
|
||||||
/// * `is_file_transfer` - Whether the connection is file transfer.
|
/// * `conn_type` - Connection type enum.
|
||||||
/// * `is_port_forward` - Whether the connection is port forward.
|
pub fn initialize(&mut self, id: String, conn_type: ConnType) {
|
||||||
pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) {
|
|
||||||
self.id = id;
|
self.id = id;
|
||||||
self.is_file_transfer = is_file_transfer;
|
self.conn_type = conn_type;
|
||||||
self.is_port_forward = is_port_forward;
|
|
||||||
let config = self.load_config();
|
let config = self.load_config();
|
||||||
self.remember = !config.password.is_empty();
|
self.remember = !config.password.is_empty();
|
||||||
self.config = config;
|
self.config = config;
|
||||||
@ -1048,7 +1065,8 @@ impl LoginConfigHandler {
|
|||||||
///
|
///
|
||||||
/// * `ignore_default` - If `true`, ignore the default value of the option.
|
/// * `ignore_default` - If `true`, ignore the default value of the option.
|
||||||
fn get_option_message(&self, ignore_default: bool) -> Option<OptionMessage> {
|
fn get_option_message(&self, ignore_default: bool) -> Option<OptionMessage> {
|
||||||
if self.is_port_forward || self.is_file_transfer {
|
if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD)
|
||||||
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
@ -1094,7 +1112,8 @@ impl LoginConfigHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> {
|
pub fn get_option_message_after_login(&self) -> Option<OptionMessage> {
|
||||||
if self.is_port_forward || self.is_file_transfer {
|
if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD)
|
||||||
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
@ -1260,13 +1279,13 @@ impl LoginConfigHandler {
|
|||||||
///
|
///
|
||||||
/// * `username` - The name of the peer.
|
/// * `username` - The name of the peer.
|
||||||
/// * `pi` - The peer info.
|
/// * `pi` - The peer info.
|
||||||
pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) {
|
pub fn handle_peer_info(&mut self, pi: &PeerInfo) {
|
||||||
if !pi.version.is_empty() {
|
if !pi.version.is_empty() {
|
||||||
self.version = hbb_common::get_version_number(&pi.version);
|
self.version = hbb_common::get_version_number(&pi.version);
|
||||||
}
|
}
|
||||||
self.features = pi.features.into_option();
|
self.features = pi.features.clone().into_option();
|
||||||
let serde = PeerInfoSerde {
|
let serde = PeerInfoSerde {
|
||||||
username,
|
username: pi.username.clone(),
|
||||||
hostname: pi.hostname.clone(),
|
hostname: pi.hostname.clone(),
|
||||||
platform: pi.platform.clone(),
|
platform: pi.platform.clone(),
|
||||||
};
|
};
|
||||||
@ -1330,19 +1349,20 @@ impl LoginConfigHandler {
|
|||||||
version: crate::VERSION.to_string(),
|
version: crate::VERSION.to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
if self.is_file_transfer {
|
match self.conn_type {
|
||||||
lr.set_file_transfer(FileTransfer {
|
ConnType::FILE_TRANSFER => lr.set_file_transfer(FileTransfer {
|
||||||
dir: self.get_remote_dir(),
|
dir: self.get_remote_dir(),
|
||||||
show_hidden: !self.get_option("remote_show_hidden").is_empty(),
|
show_hidden: !self.get_option("remote_show_hidden").is_empty(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}),
|
||||||
} else if self.is_port_forward {
|
ConnType::PORT_FORWARD => lr.set_port_forward(PortForward {
|
||||||
lr.set_port_forward(PortForward {
|
|
||||||
host: self.port_forward.0.clone(),
|
host: self.port_forward.0.clone(),
|
||||||
port: self.port_forward.1,
|
port: self.port_forward.1,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut msg_out = Message::new();
|
let mut msg_out = Message::new();
|
||||||
msg_out.set_login_request(lr);
|
msg_out.set_login_request(lr);
|
||||||
msg_out
|
msg_out
|
||||||
@ -1651,6 +1671,12 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
|||||||
fn handle_login_error(&mut self, err: &str) -> bool;
|
fn handle_login_error(&mut self, err: &str) -> bool;
|
||||||
fn handle_peer_info(&mut self, pi: PeerInfo);
|
fn handle_peer_info(&mut self, pi: PeerInfo);
|
||||||
fn set_force_relay(&mut self, direct: bool, received: bool);
|
fn set_force_relay(&mut self, direct: bool, received: bool);
|
||||||
|
fn is_file_transfer(&self) -> bool;
|
||||||
|
fn is_port_forward(&self) -> bool;
|
||||||
|
fn is_rdp(&self) -> bool;
|
||||||
|
fn on_error(&self, err: &str) {
|
||||||
|
self.msgbox("error", "Error", err);
|
||||||
|
}
|
||||||
fn is_force_relay(&self) -> bool;
|
fn is_force_relay(&self) -> bool;
|
||||||
async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream);
|
async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream);
|
||||||
async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream);
|
async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use hbb_common::{fs, message_proto::*};
|
use hbb_common::{fs, message_proto::*, log};
|
||||||
|
|
||||||
use super::{Data, Interface};
|
use super::{Data, Interface};
|
||||||
|
|
||||||
@ -114,4 +114,26 @@ pub trait FileManager: Interface {
|
|||||||
fn resume_job(&self, id: i32, is_remote: bool) {
|
fn resume_job(&self, id: i32, is_remote: bool) {
|
||||||
self.send(Data::ResumeJob((id, is_remote)));
|
self.send(Data::ResumeJob((id, is_remote)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_confirm_override_file(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
file_num: i32,
|
||||||
|
need_override: bool,
|
||||||
|
remember: bool,
|
||||||
|
is_upload: bool,
|
||||||
|
) {
|
||||||
|
log::info!(
|
||||||
|
"confirm file transfer, job: {}, need_override: {}",
|
||||||
|
id,
|
||||||
|
need_override
|
||||||
|
);
|
||||||
|
self.send(Data::SetConfirmOverrideFile((
|
||||||
|
id,
|
||||||
|
file_num,
|
||||||
|
need_override,
|
||||||
|
remember,
|
||||||
|
is_upload,
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1208
src/client/io_loop.rs
Normal file
1208
src/client/io_loop.rs
Normal file
File diff suppressed because it is too large
Load Diff
1875
src/flutter.rs
1875
src/flutter.rs
File diff suppressed because it is too large
Load Diff
@ -13,21 +13,22 @@ use hbb_common::{
|
|||||||
};
|
};
|
||||||
use hbb_common::{password_security, ResultType};
|
use hbb_common::{password_security, ResultType};
|
||||||
|
|
||||||
use crate::client::file_trait::FileManager;
|
use crate::{client::file_trait::FileManager, flutter::{session_add, session_start_}};
|
||||||
use crate::common::make_fd_to_json;
|
use crate::common::make_fd_to_json;
|
||||||
use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state};
|
use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state};
|
||||||
use crate::flutter::{self, Session, SESSIONS};
|
use crate::flutter::{self, SESSIONS};
|
||||||
use crate::start_server;
|
use crate::start_server;
|
||||||
use crate::ui_interface;
|
use crate::ui_interface;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id};
|
use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id};
|
||||||
use crate::ui_interface::{
|
use crate::ui_interface::{
|
||||||
check_super_user_permission, discover, forget_password, get_api_server, get_app_name,
|
check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server,
|
||||||
get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs,
|
get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers,
|
||||||
get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks,
|
get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer,
|
||||||
get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request,
|
get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_hwcodec,
|
||||||
set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks,
|
has_rendezvous_service, post_request, set_local_option, set_option, set_options,
|
||||||
store_fav, test_if_valid_server, update_temporary_password, using_public_server,
|
set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server,
|
||||||
|
update_temporary_password, using_public_server,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn initialize(app_dir: &str) {
|
fn initialize(app_dir: &str) {
|
||||||
@ -107,13 +108,18 @@ pub fn host_stop_system_key_propagate(stopped: bool) {
|
|||||||
crate::platform::windows::stop_system_key_propagate(stopped);
|
crate::platform::windows::stop_system_key_propagate(stopped);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_connect(
|
// FIXME: -> ResultType<()> cannot be parsed by frb_codegen
|
||||||
events2ui: StreamSink<EventToUI>,
|
// thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25
|
||||||
id: String,
|
pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn<String> {
|
||||||
is_file_transfer: bool,
|
if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) {
|
||||||
) -> ResultType<()> {
|
SyncReturn(format!("Failed to add session with id {}, {}", &id, e))
|
||||||
Session::start(&id, is_file_transfer, events2ui);
|
} else {
|
||||||
Ok(())
|
SyncReturn("".to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_start(events2ui: StreamSink<EventToUI>, id: String) -> ResultType<()> {
|
||||||
|
session_start_(&id, events2ui)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_get_remember(id: String) -> Option<bool> {
|
pub fn session_get_remember(id: String) -> Option<bool> {
|
||||||
@ -126,7 +132,7 @@ pub fn session_get_remember(id: String) -> Option<bool> {
|
|||||||
|
|
||||||
pub fn session_get_toggle_option(id: String, arg: String) -> Option<bool> {
|
pub fn session_get_toggle_option(id: String, arg: String) -> Option<bool> {
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||||
Some(session.get_toggle_option(&arg))
|
Some(session.get_toggle_option(arg))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -137,17 +143,9 @@ pub fn session_get_toggle_option_sync(id: String, arg: String) -> SyncReturn<boo
|
|||||||
SyncReturn(res)
|
SyncReturn(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_get_image_quality(id: String) -> Option<String> {
|
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
|
||||||
Some(session.get_image_quality())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session_get_option(id: String, arg: String) -> Option<String> {
|
pub fn session_get_option(id: String, arg: String) -> Option<String> {
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||||
Some(session.get_option(&arg))
|
Some(session.get_option(arg))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -155,7 +153,7 @@ pub fn session_get_option(id: String, arg: String) -> Option<String> {
|
|||||||
|
|
||||||
pub fn session_login(id: String, password: String, remember: bool) {
|
pub fn session_login(id: String, password: String, remember: bool) {
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||||
session.login(&password, remember);
|
session.login(password, remember);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +166,7 @@ pub fn session_close(id: String) {
|
|||||||
|
|
||||||
pub fn session_refresh(id: String) {
|
pub fn session_refresh(id: String) {
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||||
session.refresh();
|
session.refresh_video();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,14 +177,36 @@ pub fn session_reconnect(id: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_toggle_option(id: String, value: String) {
|
pub fn session_toggle_option(id: String, value: String) {
|
||||||
|
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||||
|
session.toggle_option(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_get_image_quality(id: String) -> Option<String> {
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||||
session.toggle_option(&value);
|
Some(session.get_image_quality())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_set_image_quality(id: String, value: String) {
|
pub fn session_set_image_quality(id: String, value: String) {
|
||||||
|
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||||
|
session.save_image_quality(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_get_custom_image_quality(id: String) -> Option<Vec<i32>> {
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||||
session.set_image_quality(&value);
|
Some(session.get_custom_image_quality())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_set_custom_image_quality(id: String, value: i32) {
|
||||||
|
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||||
|
session.save_custom_image_quality(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,11 +228,11 @@ pub fn session_switch_display(id: String, value: i32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){
|
// pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
// if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||||
session.input_raw_key(keycode, scancode, down);
|
// session.input_raw_key(keycode, scancode, down);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub fn session_input_key(
|
pub fn session_input_key(
|
||||||
id: String,
|
id: String,
|
||||||
@ -250,7 +270,7 @@ pub fn session_peer_option(id: String, name: String, value: String) {
|
|||||||
|
|
||||||
pub fn session_get_peer_option(id: String, name: String) -> String {
|
pub fn session_get_peer_option(id: String, name: String) -> String {
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||||
return session.get_option(&name);
|
return session.get_option(name);
|
||||||
}
|
}
|
||||||
"".to_string()
|
"".to_string()
|
||||||
}
|
}
|
||||||
@ -349,7 +369,7 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String {
|
|||||||
|
|
||||||
pub fn session_load_last_transfer_jobs(id: String) {
|
pub fn session_load_last_transfer_jobs(id: String) {
|
||||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||||
return session.load_last_jobs();
|
// return session.load_last_jobs();
|
||||||
} else {
|
} else {
|
||||||
// a tip for flutter dev
|
// a tip for flutter dev
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@ -473,7 +493,7 @@ pub fn main_get_connect_status() -> String {
|
|||||||
|
|
||||||
pub fn main_check_connect_status() {
|
pub fn main_check_connect_status() {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
check_connect_status(true);
|
check_mouse_time(); // avoid multi calls
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_is_using_public_server() -> bool {
|
pub fn main_is_using_public_server() -> bool {
|
||||||
@ -598,12 +618,32 @@ pub fn main_load_lan_peers() {
|
|||||||
{
|
{
|
||||||
let data = HashMap::from([
|
let data = HashMap::from([
|
||||||
("name", "load_lan_peers".to_owned()),
|
("name", "load_lan_peers".to_owned()),
|
||||||
("peers", serde_json::to_string(&get_lan_peers()).unwrap_or_default()),
|
(
|
||||||
|
"peers",
|
||||||
|
serde_json::to_string(&get_lan_peers()).unwrap_or_default(),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned()));
|
s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned()));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn session_add_port_forward(
|
||||||
|
id: String,
|
||||||
|
local_port: i32,
|
||||||
|
remote_host: String,
|
||||||
|
remote_port: i32,
|
||||||
|
) {
|
||||||
|
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||||
|
session.add_port_forward(local_port, remote_host, remote_port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_remove_port_forward(id: String, local_port: i32) {
|
||||||
|
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||||
|
session.remove_port_forward(local_port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main_get_last_remote_id() -> String {
|
pub fn main_get_last_remote_id() -> String {
|
||||||
// if !config::APP_DIR.read().unwrap().is_empty() {
|
// if !config::APP_DIR.read().unwrap().is_empty() {
|
||||||
// res = LocalConfig::get_remote_id();
|
// res = LocalConfig::get_remote_id();
|
||||||
@ -667,7 +707,6 @@ pub fn main_has_hwcodec() -> bool {
|
|||||||
has_hwcodec()
|
has_hwcodec()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
pub fn session_send_mouse(id: String, msg: String) {
|
pub fn session_send_mouse(id: String, msg: String) {
|
||||||
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
|
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
|
||||||
let alt = m.get("alt").is_some();
|
let alt = m.get("alt").is_some();
|
||||||
@ -745,6 +784,14 @@ pub fn main_check_super_user_permission() -> bool {
|
|||||||
check_super_user_permission()
|
check_super_user_permission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn main_check_mouse_time() {
|
||||||
|
check_mouse_time();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main_get_mouse_time() -> f64 {
|
||||||
|
get_mouse_time()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cm_send_chat(conn_id: i32, msg: String) {
|
pub fn cm_send_chat(conn_id: i32, msg: String) {
|
||||||
connection_manager::send_chat(conn_id, msg);
|
connection_manager::send_chat(conn_id, msg);
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", "正在重启远程设备"),
|
("Restarting Remote Device", "正在重启远程设备"),
|
||||||
("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"),
|
("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"),
|
||||||
("Copied", "已复制"),
|
("Copied", "已复制"),
|
||||||
|
("Exit Fullscreen", "退出全屏"),
|
||||||
|
("Fullscreen", "全屏"),
|
||||||
|
("Mobile Actions", "移动端操作"),
|
||||||
|
("Select Monitor", "选择监视器"),
|
||||||
|
("Control Actions", "控制操作"),
|
||||||
|
("Display Settings", "显示设置"),
|
||||||
|
("Ratio", "比例"),
|
||||||
|
("Image Quality", "画质"),
|
||||||
|
("Scroll Style", "滚屏方式"),
|
||||||
|
("Show Menubar", "显示菜单栏"),
|
||||||
|
("Hide Menubar", "隐藏菜单栏"),
|
||||||
|
("Direct Connection", "直接连接"),
|
||||||
|
("Relay Connection", "中继连接"),
|
||||||
|
("Secure Connection", "安全连接"),
|
||||||
|
("Insecure Connection", "非安全连接"),
|
||||||
|
("Scale original", "原始尺寸"),
|
||||||
|
("Scale adaptive", "适应窗口"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", ""),
|
("Restarting Remote Device", ""),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Ukončete celou obrazovku"),
|
||||||
|
("Fullscreen", "Celá obrazovka"),
|
||||||
|
("Mobile Actions", "Mobilní akce"),
|
||||||
|
("Select Monitor", "Vyberte možnost Monitor"),
|
||||||
|
("Control Actions", "Ovládací akce"),
|
||||||
|
("Display Settings", "Nastavení obrazovky"),
|
||||||
|
("Ratio", "Poměr"),
|
||||||
|
("Image Quality", "Kvalita obrazu"),
|
||||||
|
("Scroll Style", "Štýl posúvania"),
|
||||||
|
("Show Menubar", "Zobrazit panel nabídek"),
|
||||||
|
("Hide Menubar", "skrýt panel nabídek"),
|
||||||
|
("Direct Connection", "Přímé spojení"),
|
||||||
|
("Relay Connection", "Připojení relé"),
|
||||||
|
("Secure Connection", "Zabezpečené připojení"),
|
||||||
|
("Insecure Connection", "Nezabezpečené připojení"),
|
||||||
|
("Scale original", "Měřítko původní"),
|
||||||
|
("Scale adaptive", "Měřítko adaptivní"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", ""),
|
("Restarting Remote Device", ""),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Afslut fuldskærm"),
|
||||||
|
("Fullscreen", "Fuld skærm"),
|
||||||
|
("Mobile Actions", "Mobile handlinger"),
|
||||||
|
("Select Monitor", "Vælg Monitor"),
|
||||||
|
("Control Actions", "Kontrolhandlinger"),
|
||||||
|
("Display Settings", "Skærmindstillinger"),
|
||||||
|
("Ratio", "Forhold"),
|
||||||
|
("Image Quality", "Billede kvalitet"),
|
||||||
|
("Scroll Style", "Rulstil"),
|
||||||
|
("Show Menubar", "Vis menulinje"),
|
||||||
|
("Hide Menubar", "skjul menulinjen"),
|
||||||
|
("Direct Connection", "Direkte forbindelse"),
|
||||||
|
("Relay Connection", "Relæforbindelse"),
|
||||||
|
("Secure Connection", "Sikker forbindelse"),
|
||||||
|
("Insecure Connection", "Usikker forbindelse"),
|
||||||
|
("Scale original", "Skala original"),
|
||||||
|
("Scale adaptive", "Skala adaptiv"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"),
|
("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"),
|
||||||
("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."),
|
("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Vollbild beenden"),
|
||||||
|
("Fullscreen", "Ganzer Bildschirm"),
|
||||||
|
("Mobile Actions", "Mobile Aktionen"),
|
||||||
|
("Select Monitor", "Wählen Sie Überwachen aus"),
|
||||||
|
("Control Actions", "Kontrollaktionen"),
|
||||||
|
("Display Settings", "Bildschirmeinstellungen"),
|
||||||
|
("Ratio", "Verhältnis"),
|
||||||
|
("Image Quality", "Bildqualität"),
|
||||||
|
("Scroll Style", "Scroll-Stil"),
|
||||||
|
("Show Menubar", "Menüleiste anzeigen"),
|
||||||
|
("Hide Menubar", "Menüleiste ausblenden"),
|
||||||
|
("Direct Connection", "Direkte Verbindung"),
|
||||||
|
("Relay Connection", "Relaisverbindung"),
|
||||||
|
("Secure Connection", "Sichere Verbindung"),
|
||||||
|
("Insecure Connection", "Unsichere Verbindung"),
|
||||||
|
("Scale original", "Original skalieren"),
|
||||||
|
("Scale adaptive", "Adaptiv skalieren"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", ""),
|
("Restarting Remote Device", ""),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Eliru Plenekranon"),
|
||||||
|
("Fullscreen", "Plenekrane"),
|
||||||
|
("Mobile Actions", "Poŝtelefonaj Agoj"),
|
||||||
|
("Select Monitor", "Elektu Monitoron"),
|
||||||
|
("Control Actions", "Kontrolaj Agoj"),
|
||||||
|
("Display Settings", "Montraj Agordoj"),
|
||||||
|
("Ratio", "Proporcio"),
|
||||||
|
("Image Quality", "Bilda Kvalito"),
|
||||||
|
("Scroll Style", "Ruluma Stilo"),
|
||||||
|
("Show Menubar", "Montru menubreton"),
|
||||||
|
("Hide Menubar", "kaŝi menubreton"),
|
||||||
|
("Direct Connection", "Rekta Konekto"),
|
||||||
|
("Relay Connection", "Relajsa Konekto"),
|
||||||
|
("Secure Connection", "Sekura Konekto"),
|
||||||
|
("Insecure Connection", "Nesekura Konekto"),
|
||||||
|
("Scale original", "Skalo originalo"),
|
||||||
|
("Scale adaptive", "Skalo adapta"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", "Reiniciando dispositivo remoto"),
|
("Restarting Remote Device", "Reiniciando dispositivo remoto"),
|
||||||
("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."),
|
("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Salir de pantalla completa"),
|
||||||
|
("Fullscreen", "Pantalla completa"),
|
||||||
|
("Mobile Actions", "Acciones móviles"),
|
||||||
|
("Select Monitor", "Seleccionar monitor"),
|
||||||
|
("Control Actions", "Acciones de control"),
|
||||||
|
("Display Settings", "Configuración de pantalla"),
|
||||||
|
("Ratio", "Relación"),
|
||||||
|
("Image Quality", "La calidad de imagen"),
|
||||||
|
("Scroll Style", "Estilo de desplazamiento"),
|
||||||
|
("Show Menubar", "ajustes de pantalla"),
|
||||||
|
("Hide Menubar", "ocultar barra de menú"),
|
||||||
|
("Direct Connection", "Conexión directa"),
|
||||||
|
("Relay Connection", "Conexión de relé"),
|
||||||
|
("Secure Connection", "Conexión segura"),
|
||||||
|
("Insecure Connection", "Conexión insegura"),
|
||||||
|
("Scale original", "escala originales"),
|
||||||
|
("Scale adaptive", "Adaptable a escala"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", ""),
|
("Restarting Remote Device", ""),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Quitter le mode plein écran"),
|
||||||
|
("Fullscreen", "Plein écran"),
|
||||||
|
("Mobile Actions", "Actions mobiles"),
|
||||||
|
("Select Monitor", "Sélectionnez Moniteur"),
|
||||||
|
("Control Actions", "Actions de contrôle"),
|
||||||
|
("Display Settings", "Paramètres d'affichage"),
|
||||||
|
("Ratio", "Rapport"),
|
||||||
|
("Image Quality", "Qualité d'image"),
|
||||||
|
("Scroll Style", "Style de défilement"),
|
||||||
|
("Show Menubar", "Afficher la barre de menus"),
|
||||||
|
("Hide Menubar", "masquer la barre de menus"),
|
||||||
|
("Direct Connection", "Connexion directe"),
|
||||||
|
("Relay Connection", "Connexion relais"),
|
||||||
|
("Secure Connection", "Connexion sécurisée"),
|
||||||
|
("Insecure Connection", "Connexion non sécurisée"),
|
||||||
|
("Scale original", "Échelle d'origine"),
|
||||||
|
("Scale adaptive", "Échelle adaptative"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", ""),
|
("Restarting Remote Device", ""),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Lépjen ki a teljes képernyőről"),
|
||||||
|
("Fullscreen", "Teljes képernyő"),
|
||||||
|
("Mobile Actions", "mobil műveletek"),
|
||||||
|
("Select Monitor", "Válassza a Monitor lehetőséget"),
|
||||||
|
("Control Actions", "Irányítási műveletek"),
|
||||||
|
("Display Settings", "Megjelenítési beállítások"),
|
||||||
|
("Ratio", "Hányados"),
|
||||||
|
("Image Quality", "Képminőség"),
|
||||||
|
("Scroll Style", "Görgetési stílus"),
|
||||||
|
("Show Menubar", "Menüsor megjelenítése"),
|
||||||
|
("Hide Menubar", "menüsor elrejtése"),
|
||||||
|
("Direct Connection", "Közvetlen kapcsolat"),
|
||||||
|
("Relay Connection", "Relé csatlakozás"),
|
||||||
|
("Secure Connection", "Biztonságos kapcsolat"),
|
||||||
|
("Insecure Connection", "Nem biztonságos kapcsolat"),
|
||||||
|
("Scale original", "Eredeti méretarány"),
|
||||||
|
("Scale adaptive", "Skála adaptív"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"),
|
("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Keluar dari Layar Penuh"),
|
||||||
|
("Fullscreen", "Layar penuh"),
|
||||||
|
("Mobile Actions", "Tindakan Seluler"),
|
||||||
|
("Select Monitor", "Pilih Monitor"),
|
||||||
|
("Control Actions", "Tindakan Kontrol"),
|
||||||
|
("Display Settings", "Pengaturan tampilan"),
|
||||||
|
("Ratio", "Perbandingan"),
|
||||||
|
("Image Quality", "Kualitas gambar"),
|
||||||
|
("Scroll Style", "Gaya Gulir"),
|
||||||
|
("Show Menubar", "Tampilkan bilah menu"),
|
||||||
|
("Hide Menubar", "sembunyikan bilah menu"),
|
||||||
|
("Direct Connection", "Koneksi langsung"),
|
||||||
|
("Relay Connection", "Koneksi Relay"),
|
||||||
|
("Secure Connection", "Koneksi aman"),
|
||||||
|
("Insecure Connection", "Koneksi Tidak Aman"),
|
||||||
|
("Scale original", "Skala asli"),
|
||||||
|
("Scale adaptive", "Skala adaptif"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -301,6 +301,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Are you sure you want to restart", "Sei sicuro di voler riavviare?"),
|
("Are you sure you want to restart", "Sei sicuro di voler riavviare?"),
|
||||||
("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"),
|
("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"),
|
||||||
("remote_restarting_tip", "Riavviare il dispositivo remoto"),
|
("remote_restarting_tip", "Riavviare il dispositivo remoto"),
|
||||||
|
("Exit Fullscreen", "Esci dalla modalità schermo intero"),
|
||||||
|
("Fullscreen", "A schermo intero"),
|
||||||
|
("Mobile Actions", "Azioni mobili"),
|
||||||
|
("Select Monitor", "Seleziona Monitora"),
|
||||||
|
("Control Actions", "Azioni di controllo"),
|
||||||
|
("Display Settings", "Impostazioni di visualizzazione"),
|
||||||
|
("Ratio", "Rapporto"),
|
||||||
|
("Image Quality", "Qualità dell'immagine"),
|
||||||
|
("Scroll Style", "Stile di scorrimento"),
|
||||||
|
("Show Menubar", "Mostra la barra dei menu"),
|
||||||
|
("Hide Menubar", "nascondi la barra dei menu"),
|
||||||
|
("Direct Connection", "Connessione diretta"),
|
||||||
|
("Relay Connection", "Collegamento a relè"),
|
||||||
|
("Secure Connection", "Connessione sicura"),
|
||||||
|
("Insecure Connection", "Connessione insicura"),
|
||||||
|
("Scale original", "Scala originale"),
|
||||||
|
("Scale adaptive", "Scala adattiva"),
|
||||||
("Legacy mode", ""),
|
("Legacy mode", ""),
|
||||||
("Map mode", ""),
|
("Map mode", ""),
|
||||||
("Translate mode", ""),
|
("Translate mode", ""),
|
||||||
|
@ -302,5 +302,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Are you sure you want to restart", "本当に再起動しますか"),
|
("Are you sure you want to restart", "本当に再起動しますか"),
|
||||||
("Restarting Remote Device", "リモート端末を再起動中"),
|
("Restarting Remote Device", "リモート端末を再起動中"),
|
||||||
("remote_restarting_tip", "リモート端末は再起動中です。このメッセージボックスを閉じて、しばらくした後に固定のパスワードを使用して再接続してください。"),
|
("remote_restarting_tip", "リモート端末は再起動中です。このメッセージボックスを閉じて、しばらくした後に固定のパスワードを使用して再接続してください。"),
|
||||||
|
("Exit Fullscreen", "全画面表示を終了"),
|
||||||
|
("Fullscreen", "全画面表示"),
|
||||||
|
("Mobile Actions", "モバイル アクション"),
|
||||||
|
("Select Monitor", "モニターを選択"),
|
||||||
|
("Control Actions", "コントロール アクション"),
|
||||||
|
("Display Settings", "ディスプレイの設定"),
|
||||||
|
("Ratio", "比率"),
|
||||||
|
("Image Quality", "画質"),
|
||||||
|
("Scroll Style", "スクロール スタイル"),
|
||||||
|
("Show Menubar", "メニューバーを表示"),
|
||||||
|
("Hide Menubar", "メニューバーを隠す"),
|
||||||
|
("Direct Connection", "直接接続"),
|
||||||
|
("Relay Connection", "リレー接続"),
|
||||||
|
("Secure Connection", "安全な接続"),
|
||||||
|
("Insecure Connection", "安全でない接続"),
|
||||||
|
("Scale original", "オリジナルサイズ"),
|
||||||
|
("Scale adaptive", "フィットウィンドウ"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -299,5 +299,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Are you sure you want to restart", "정말로 재시작 하시겠습니까"),
|
("Are you sure you want to restart", "정말로 재시작 하시겠습니까"),
|
||||||
("Restarting Remote Device", "원격 기기를 다시 시작하는중"),
|
("Restarting Remote Device", "원격 기기를 다시 시작하는중"),
|
||||||
("remote_restarting_tip", "원격 장치를 다시 시작하는 중입니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결하십시오."),
|
("remote_restarting_tip", "원격 장치를 다시 시작하는 중입니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결하십시오."),
|
||||||
|
("Exit Fullscreen", "전체 화면 종료"),
|
||||||
|
("Fullscreen", "전체화면"),
|
||||||
|
("Mobile Actions", "모바일 액션"),
|
||||||
|
("Select Monitor", "모니터 선택"),
|
||||||
|
("Control Actions", "제어 작업"),
|
||||||
|
("Display Settings", "화면 설정"),
|
||||||
|
("Ratio", "비율"),
|
||||||
|
("Image Quality", "이미지 품질"),
|
||||||
|
("Scroll Style", "스크롤 스타일"),
|
||||||
|
("Show Menubar", "메뉴 표시줄 표시"),
|
||||||
|
("Hide Menubar", "메뉴 표시줄 숨기기"),
|
||||||
|
("Direct Connection", "직접 연결"),
|
||||||
|
("Relay Connection", "릴레이 연결"),
|
||||||
|
("Secure Connection", "보안 연결"),
|
||||||
|
("Insecure Connection", "안전하지 않은 연결"),
|
||||||
|
("Scale original", "원래 크기"),
|
||||||
|
("Scale adaptive", "맞는 창"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
@ -303,5 +303,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Set security password", "Ustaw hasło zabezpieczające"),
|
("Set security password", "Ustaw hasło zabezpieczające"),
|
||||||
("Connection not allowed", "Połączenie niedozwolone"),
|
("Connection not allowed", "Połączenie niedozwolone"),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"),
|
||||||
|
("Fullscreen", "Pełny ekran"),
|
||||||
|
("Mobile Actions", "Działania mobilne"),
|
||||||
|
("Select Monitor", "Wybierz Monitor"),
|
||||||
|
("Control Actions", "Działania kontrolne"),
|
||||||
|
("Display Settings", "Ustawienia wyświetlania"),
|
||||||
|
("Ratio", "Stosunek"),
|
||||||
|
("Image Quality", "Jakość obrazu"),
|
||||||
|
("Scroll Style", "Styl przewijania"),
|
||||||
|
("Show Menubar", "Pokaż pasek menu"),
|
||||||
|
("Hide Menubar", "ukryj pasek menu"),
|
||||||
|
("Direct Connection", "Bezpośrednie połączenie"),
|
||||||
|
("Relay Connection", "Połączenie przekaźnika"),
|
||||||
|
("Secure Connection", "Bezpieczne połączenie"),
|
||||||
|
("Insecure Connection", "Niepewne połączenie"),
|
||||||
|
("Scale original", "Skala oryginalna"),
|
||||||
|
("Scale adaptive", "Skala adaptacyjna"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -299,5 +299,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"),
|
("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"),
|
||||||
("Restarting Remote Device", "A reiniciar sistema remoto"),
|
("Restarting Remote Device", "A reiniciar sistema remoto"),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
|
("Exit Fullscreen", "Sair da tela cheia"),
|
||||||
|
("Fullscreen", "Tela cheia"),
|
||||||
|
("Mobile Actions", "Ações para celular"),
|
||||||
|
("Select Monitor", "Selecionar monitor"),
|
||||||
|
("Control Actions", "Ações de controle"),
|
||||||
|
("Display Settings", "Configurações do visor"),
|
||||||
|
("Ratio", "Razão"),
|
||||||
|
("Image Quality", "Qualidade da imagem"),
|
||||||
|
("Scroll Style", "Estilo de rolagem"),
|
||||||
|
("Show Menubar", "Mostrar barra de menus"),
|
||||||
|
("Hide Menubar", "ocultar barra de menu"),
|
||||||
|
("Direct Connection", "Conexão direta"),
|
||||||
|
("Relay Connection", "Conexão de relé"),
|
||||||
|
("Secure Connection", "Conexão segura"),
|
||||||
|
("Insecure Connection", "Conexão insegura"),
|
||||||
|
("Scale original", "Escala original"),
|
||||||
|
("Scale adaptive", "Escala adaptável"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", ""),
|
("Restarting Remote Device", ""),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", ""),
|
||||||
|
("Fullscreen", ""),
|
||||||
|
("Mobile Actions", ""),
|
||||||
|
("Select Monitor", ""),
|
||||||
|
("Control Actions", ""),
|
||||||
|
("Display Settings", ""),
|
||||||
|
("Ratio", ""),
|
||||||
|
("Image Quality", ""),
|
||||||
|
("Scroll Style", ""),
|
||||||
|
("Show Menubar", ""),
|
||||||
|
("Hide Menubar", ""),
|
||||||
|
("Direct Connection", ""),
|
||||||
|
("Relay Connection", ""),
|
||||||
|
("Secure Connection", ""),
|
||||||
|
("Insecure Connection", ""),
|
||||||
|
("Scale original", ""),
|
||||||
|
("Scale adaptive", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", "Перезагрузка удаленного устройства"),
|
("Restarting Remote Device", "Перезагрузка удаленного устройства"),
|
||||||
("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."),
|
("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Выйти из полноэкранного режима"),
|
||||||
|
("Fullscreen", "Полноэкранный"),
|
||||||
|
("Mobile Actions", "Мобильные действия"),
|
||||||
|
("Select Monitor", "Выберите монитор"),
|
||||||
|
("Control Actions", "Действия по управлению"),
|
||||||
|
("Display Settings", "Настройки отображения"),
|
||||||
|
("Ratio", "Соотношение"),
|
||||||
|
("Image Quality", "Качество изображения"),
|
||||||
|
("Scroll Style", "Стиль прокрутки"),
|
||||||
|
("Show Menubar", "Показать строку меню"),
|
||||||
|
("Hide Menubar", "скрыть строку меню"),
|
||||||
|
("Direct Connection", "Прямая связь"),
|
||||||
|
("Relay Connection", "Релейное соединение"),
|
||||||
|
("Secure Connection", "Безопасное соединение"),
|
||||||
|
("Insecure Connection", "Небезопасное соединение"),
|
||||||
|
("Scale original", "Оригинал масштаба"),
|
||||||
|
("Scale adaptive", "Масштаб адаптивный"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", ""),
|
("Restarting Remote Device", ""),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Ukončiť celú obrazovku"),
|
||||||
|
("Fullscreen", "Celá obrazovka"),
|
||||||
|
("Mobile Actions", "Mobilné akcie"),
|
||||||
|
("Select Monitor", "Vyberte možnosť Monitor"),
|
||||||
|
("Control Actions", "Kontrolné akcie"),
|
||||||
|
("Display Settings", "Nastavenia displeja"),
|
||||||
|
("Ratio", "Pomer"),
|
||||||
|
("Image Quality", "Kvalita obrazu"),
|
||||||
|
("Scroll Style", "Štýl posúvania"),
|
||||||
|
("Show Menubar", "Zobraziť panel s ponukami"),
|
||||||
|
("Hide Menubar", "skryť panel s ponukami"),
|
||||||
|
("Direct Connection", "Priame pripojenie"),
|
||||||
|
("Relay Connection", "Reléové pripojenie"),
|
||||||
|
("Secure Connection", "Zabezpečené pripojenie"),
|
||||||
|
("Insecure Connection", "Nezabezpečené pripojenie"),
|
||||||
|
("Scale original", "Pôvodná mierka"),
|
||||||
|
("Scale adaptive", "Prispôsobivá mierka"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", ""),
|
("Restarting Remote Device", ""),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", ""),
|
||||||
|
("Fullscreen", ""),
|
||||||
|
("Mobile Actions", ""),
|
||||||
|
("Select Monitor", ""),
|
||||||
|
("Control Actions", ""),
|
||||||
|
("Display Settings", ""),
|
||||||
|
("Ratio", ""),
|
||||||
|
("Image Quality", ""),
|
||||||
|
("Scroll Style", ""),
|
||||||
|
("Show Menubar", ""),
|
||||||
|
("Hide Menubar", ""),
|
||||||
|
("Direct Connection", ""),
|
||||||
|
("Relay Connection", ""),
|
||||||
|
("Secure Connection", ""),
|
||||||
|
("Insecure Connection", ""),
|
||||||
|
("Scale original", ""),
|
||||||
|
("Scale adaptive", ""),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -318,5 +318,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"),
|
("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"),
|
||||||
("remote_restarting_tip", ""),
|
("remote_restarting_tip", ""),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Tam ekrandan çık"),
|
||||||
|
("Fullscreen", "Tam ekran"),
|
||||||
|
("Mobile Actions", "Mobil İşlemler"),
|
||||||
|
("Select Monitor", "Monitörü Seç"),
|
||||||
|
("Control Actions", "Kontrol Eylemleri"),
|
||||||
|
("Display Settings", "Görüntü ayarları"),
|
||||||
|
("Ratio", "Oran"),
|
||||||
|
("Image Quality", "Görüntü kalitesi"),
|
||||||
|
("Scroll Style", "Kaydırma Stili"),
|
||||||
|
("Show Menubar", "Menü çubuğunu göster"),
|
||||||
|
("Hide Menubar", "menü çubuğunu gizle"),
|
||||||
|
("Direct Connection", "Doğrudan Bağlantı"),
|
||||||
|
("Relay Connection", "Röle Bağlantısı"),
|
||||||
|
("Secure Connection", "Güvenli bağlantı"),
|
||||||
|
("Insecure Connection", "Güvenli Bağlantı"),
|
||||||
|
("Scale original", "Orijinali ölçeklendir"),
|
||||||
|
("Scale adaptive", "Ölçek uyarlanabilir"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", "正在重啓遠程設備"),
|
("Restarting Remote Device", "正在重啓遠程設備"),
|
||||||
("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"),
|
("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"),
|
||||||
("Copied", "已複製"),
|
("Copied", "已複製"),
|
||||||
|
("Exit Fullscreen", "退出全屏"),
|
||||||
|
("Fullscreen", "全屏"),
|
||||||
|
("Mobile Actions", "移動端操作"),
|
||||||
|
("Select Monitor", "選擇監視器"),
|
||||||
|
("Control Actions", "控制操作"),
|
||||||
|
("Display Settings", "顯示設置"),
|
||||||
|
("Ratio", "比例"),
|
||||||
|
("Image Quality", "畫質"),
|
||||||
|
("Scroll Style", "滾動樣式"),
|
||||||
|
("Show Menubar", "顯示菜單欄"),
|
||||||
|
("Hide Menubar", "隱藏菜單欄"),
|
||||||
|
("Direct Connection", "直接連接"),
|
||||||
|
("Relay Connection", "中繼連接"),
|
||||||
|
("Secure Connection", "安全連接"),
|
||||||
|
("Insecure Connection", "非安全連接"),
|
||||||
|
("Scale original", "原始尺寸"),
|
||||||
|
("Scale adaptive", "適應窗口"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -305,5 +305,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"),
|
("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"),
|
||||||
("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"),
|
("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"),
|
||||||
("Copied", ""),
|
("Copied", ""),
|
||||||
|
("Exit Fullscreen", "Thoát toàn màn hình"),
|
||||||
|
("Fullscreen", "Toàn màn hình"),
|
||||||
|
("Mobile Actions", "Hành động trên thiết bị di động"),
|
||||||
|
("Select Monitor", "Chọn màn hình"),
|
||||||
|
("Control Actions", "Kiểm soát hành động"),
|
||||||
|
("Display Settings", "Thiết lập hiển thị"),
|
||||||
|
("Ratio", "Tỉ lệ"),
|
||||||
|
("Image Quality", "Chất lượng hình ảnh"),
|
||||||
|
("Scroll Style", "Kiểu cuộn"),
|
||||||
|
("Show Menubar", "Hiển thị thanh menu"),
|
||||||
|
("Hide Menubar", "ẩn thanh menu"),
|
||||||
|
("Direct Connection", "Kết nối trực tiếp"),
|
||||||
|
("Relay Connection", "Kết nối chuyển tiếp"),
|
||||||
|
("Secure Connection", "Kết nối an toàn"),
|
||||||
|
("Insecure Connection", "Kết nối không an toàn"),
|
||||||
|
("Scale original", "Quy mô gốc"),
|
||||||
|
("Scale adaptive", "Quy mô thích ứng"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ mod port_forward;
|
|||||||
mod tray;
|
mod tray;
|
||||||
|
|
||||||
mod ui_interface;
|
mod ui_interface;
|
||||||
|
mod ui_session_interface;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub mod clipboard_file;
|
pub mod clipboard_file;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::client::*;
|
use crate::client::*;
|
||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
allow_err, bail,
|
allow_err, bail,
|
||||||
@ -48,6 +50,9 @@ pub async fn listen(
|
|||||||
ui_receiver: mpsc::UnboundedReceiver<Data>,
|
ui_receiver: mpsc::UnboundedReceiver<Data>,
|
||||||
key: &str,
|
key: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
|
lc: Arc<RwLock<LoginConfigHandler>>,
|
||||||
|
remote_host: String,
|
||||||
|
remote_port: i32,
|
||||||
) -> ResultType<()> {
|
) -> ResultType<()> {
|
||||||
let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?;
|
let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?;
|
||||||
let addr = listener.local_addr()?;
|
let addr = listener.local_addr()?;
|
||||||
@ -61,6 +66,7 @@ pub async fn listen(
|
|||||||
tokio::select! {
|
tokio::select! {
|
||||||
Ok((forward, addr)) = listener.accept() => {
|
Ok((forward, addr)) = listener.accept() => {
|
||||||
log::info!("new connection from {:?}", addr);
|
log::info!("new connection from {:?}", addr);
|
||||||
|
lc.write().unwrap().port_forward = (remote_host.clone(), remote_port);
|
||||||
let id = id.clone();
|
let id = id.clone();
|
||||||
let password = password.clone();
|
let password = password.clone();
|
||||||
let mut forward = Framed::new(forward, BytesCodec::new());
|
let mut forward = Framed::new(forward, BytesCodec::new());
|
||||||
|
@ -950,6 +950,7 @@ impl Connection {
|
|||||||
addr
|
addr
|
||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@ pub fn start(args: &mut [String]) {
|
|||||||
let args: Vec<String> = iter.map(|x| x.clone()).collect();
|
let args: Vec<String> = iter.map(|x| x.clone()).collect();
|
||||||
frame.set_title(&id);
|
frame.set_title(&id);
|
||||||
frame.register_behavior("native-remote", move || {
|
frame.register_behavior("native-remote", move || {
|
||||||
Box::new(remote::Handler::new(
|
Box::new(remote::SciterSession::new(
|
||||||
cmd.clone(),
|
cmd.clone(),
|
||||||
id.clone(),
|
id.clone(),
|
||||||
pass.clone(),
|
pass.clone(),
|
||||||
|
2688
src/ui/remote.rs
2688
src/ui/remote.rs
File diff suppressed because it is too large
Load Diff
1305
src/ui_session_interface.rs
Normal file
1305
src/ui_session_interface.rs
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user