mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge branch 'master' of https://github.com/rustdesk/rustdesk
This commit is contained in:
commit
8da4fbabf5
@ -105,43 +105,30 @@ class MainService : Service() {
|
|||||||
@Keep
|
@Keep
|
||||||
fun rustSetByName(name: String, arg1: String, arg2: String) {
|
fun rustSetByName(name: String, arg1: String, arg2: String) {
|
||||||
when (name) {
|
when (name) {
|
||||||
"try_start_without_auth" -> {
|
"add_connection" -> {
|
||||||
try {
|
|
||||||
val jsonObject = JSONObject(arg1)
|
|
||||||
val id = jsonObject["id"] as Int
|
|
||||||
val username = jsonObject["name"] as String
|
|
||||||
val peerId = jsonObject["peer_id"] as String
|
|
||||||
val type = if (jsonObject["is_file_transfer"] as Boolean) {
|
|
||||||
translate("File Connection")
|
|
||||||
} else {
|
|
||||||
translate("Screen Connection")
|
|
||||||
}
|
|
||||||
loginRequestNotification(id, type, username, peerId)
|
|
||||||
} catch (e: JSONException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"on_client_authorized" -> {
|
|
||||||
Log.d(logTag, "from rust:on_client_authorized")
|
|
||||||
try {
|
try {
|
||||||
val jsonObject = JSONObject(arg1)
|
val jsonObject = JSONObject(arg1)
|
||||||
val id = jsonObject["id"] as Int
|
val id = jsonObject["id"] as Int
|
||||||
val username = jsonObject["name"] as String
|
val username = jsonObject["name"] as String
|
||||||
val peerId = jsonObject["peer_id"] as String
|
val peerId = jsonObject["peer_id"] as String
|
||||||
|
val authorized = jsonObject["authorized"] as Boolean
|
||||||
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
|
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
|
||||||
val type = if (isFileTransfer) {
|
val type = if (isFileTransfer) {
|
||||||
translate("File Connection")
|
translate("File Connection")
|
||||||
} else {
|
} else {
|
||||||
translate("Screen Connection")
|
translate("Screen Connection")
|
||||||
}
|
}
|
||||||
|
if (authorized) {
|
||||||
if (!isFileTransfer && !isStart) {
|
if (!isFileTransfer && !isStart) {
|
||||||
startCapture()
|
startCapture()
|
||||||
}
|
}
|
||||||
onClientAuthorizedNotification(id, type, username, peerId)
|
onClientAuthorizedNotification(id, type, username, peerId)
|
||||||
|
} else {
|
||||||
|
loginRequestNotification(id, type, username, peerId)
|
||||||
|
}
|
||||||
} catch (e: JSONException) {
|
} catch (e: JSONException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
"stop_capture" -> {
|
"stop_capture" -> {
|
||||||
Log.d(logTag, "from rust:stop_capture")
|
Log.d(logTag, "from rust:stop_capture")
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import 'package:back_button_interceptor/back_button_interceptor.dart';
|
|||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
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_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -154,7 +155,7 @@ class MyTheme {
|
|||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
primarySwatch: Colors.blue,
|
primarySwatch: Colors.blue,
|
||||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||||
tabBarTheme: TabBarTheme(
|
tabBarTheme: const TabBarTheme(
|
||||||
labelColor: Colors.black87,
|
labelColor: Colors.black87,
|
||||||
),
|
),
|
||||||
splashColor: Colors.transparent,
|
splashColor: Colors.transparent,
|
||||||
@ -162,13 +163,14 @@ class MyTheme {
|
|||||||
).copyWith(
|
).copyWith(
|
||||||
extensions: <ThemeExtension<dynamic>>[
|
extensions: <ThemeExtension<dynamic>>[
|
||||||
ColorThemeExtension.light,
|
ColorThemeExtension.light,
|
||||||
|
TabbarTheme.light,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
static ThemeData darkTheme = ThemeData(
|
static ThemeData darkTheme = ThemeData(
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
primarySwatch: Colors.blue,
|
primarySwatch: Colors.blue,
|
||||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||||
tabBarTheme: TabBarTheme(
|
tabBarTheme: const TabBarTheme(
|
||||||
labelColor: Colors.white70,
|
labelColor: Colors.white70,
|
||||||
),
|
),
|
||||||
splashColor: Colors.transparent,
|
splashColor: Colors.transparent,
|
||||||
@ -176,12 +178,17 @@ class MyTheme {
|
|||||||
).copyWith(
|
).copyWith(
|
||||||
extensions: <ThemeExtension<dynamic>>[
|
extensions: <ThemeExtension<dynamic>>[
|
||||||
ColorThemeExtension.dark,
|
ColorThemeExtension.dark,
|
||||||
|
TabbarTheme.dark,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
static ColorThemeExtension color(BuildContext context) {
|
static ColorThemeExtension color(BuildContext context) {
|
||||||
return Theme.of(context).extension<ColorThemeExtension>()!;
|
return Theme.of(context).extension<ColorThemeExtension>()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static TabbarTheme tabbar(BuildContext context) {
|
||||||
|
return Theme.of(context).extension<TabbarTheme>()!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isDarkTheme() {
|
bool isDarkTheme() {
|
||||||
@ -340,34 +347,41 @@ class OverlayDialogManager {
|
|||||||
{bool clickMaskDismiss = false,
|
{bool clickMaskDismiss = false,
|
||||||
bool showCancel = true,
|
bool showCancel = true,
|
||||||
VoidCallback? onCancel}) {
|
VoidCallback? onCancel}) {
|
||||||
show((setState, close) => CustomAlertDialog(
|
show((setState, close) {
|
||||||
|
cancel() {
|
||||||
|
dismissAll();
|
||||||
|
if (onCancel != null) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
content: Container(
|
content: Container(
|
||||||
constraints: BoxConstraints(maxWidth: 240),
|
constraints: const BoxConstraints(maxWidth: 240),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
Center(child: CircularProgressIndicator()),
|
const Center(child: CircularProgressIndicator()),
|
||||||
SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Center(
|
Center(
|
||||||
child: Text(translate(text),
|
child: Text(translate(text),
|
||||||
style: TextStyle(fontSize: 15))),
|
style: const TextStyle(fontSize: 15))),
|
||||||
SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !showCancel,
|
offstage: !showCancel,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: cancel,
|
||||||
dismissAll();
|
|
||||||
if (onCancel != null) {
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(translate('Cancel'),
|
child: Text(translate('Cancel'),
|
||||||
style: TextStyle(color: MyTheme.accent)))))
|
style:
|
||||||
]))));
|
const TextStyle(color: MyTheme.accent)))))
|
||||||
|
])),
|
||||||
|
onCancel: showCancel ? cancel : null,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,18 +391,18 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
|||||||
final entry = OverlayEntry(builder: (_) {
|
final entry = OverlayEntry(builder: (_) {
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment(0.0, 0.8),
|
alignment: const Alignment(0.0, 0.8),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.6),
|
color: Colors.black.withOpacity(0.6),
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(20),
|
Radius.circular(20),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w300,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@ -403,23 +417,54 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CustomAlertDialog extends StatelessWidget {
|
class CustomAlertDialog extends StatelessWidget {
|
||||||
CustomAlertDialog(
|
const CustomAlertDialog(
|
||||||
{this.title, required this.content, this.actions, this.contentPadding});
|
{Key? key,
|
||||||
|
this.title,
|
||||||
|
required this.content,
|
||||||
|
this.actions,
|
||||||
|
this.contentPadding,
|
||||||
|
this.onSubmit,
|
||||||
|
this.onCancel})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
final Widget? title;
|
final Widget? title;
|
||||||
final Widget content;
|
final Widget content;
|
||||||
final List<Widget>? actions;
|
final List<Widget>? actions;
|
||||||
final double? contentPadding;
|
final double? contentPadding;
|
||||||
|
final Function()? onSubmit;
|
||||||
|
final Function()? onCancel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
FocusNode focusNode = FocusNode();
|
||||||
|
// request focus if there is no focused FocusNode in the dialog
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
if (!focusNode.hasFocus) focusNode.requestFocus();
|
||||||
|
});
|
||||||
|
return Focus(
|
||||||
|
focusNode: focusNode,
|
||||||
|
autofocus: true,
|
||||||
|
onKey: (node, key) {
|
||||||
|
if (key.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
if (key is RawKeyDownEvent) {
|
||||||
|
onCancel?.call();
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled; // avoid TextField exception on escape
|
||||||
|
} else if (onSubmit != null &&
|
||||||
|
key.logicalKey == LogicalKeyboardKey.enter) {
|
||||||
|
if (key is RawKeyDownEvent) onSubmit?.call();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
},
|
||||||
|
child: AlertDialog(
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
title: title,
|
title: title,
|
||||||
contentPadding:
|
contentPadding: EdgeInsets.symmetric(
|
||||||
EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10),
|
horizontal: contentPadding ?? 25, vertical: 10),
|
||||||
content: content,
|
content: content,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -429,26 +474,28 @@ void msgBox(
|
|||||||
{bool? hasCancel}) {
|
{bool? hasCancel}) {
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
List<Widget> buttons = [];
|
List<Widget> buttons = [];
|
||||||
if (type != "connecting" && type != "success" && !type.contains("nook")) {
|
bool hasOk = false;
|
||||||
buttons.insert(
|
submit() {
|
||||||
0,
|
|
||||||
msgBoxButton(translate('OK'), () {
|
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||||
if (!type.contains("custom")) {
|
if (!type.contains("custom")) {
|
||||||
closeConnection();
|
closeConnection();
|
||||||
}
|
}
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
dialogManager.dismissAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != "connecting" && type != "success" && !type.contains("nook")) {
|
||||||
|
hasOk = true;
|
||||||
|
buttons.insert(0, msgBoxButton(translate('OK'), submit));
|
||||||
}
|
}
|
||||||
hasCancel ??= !type.contains("error") &&
|
hasCancel ??= !type.contains("error") &&
|
||||||
!type.contains("nocancel") &&
|
!type.contains("nocancel") &&
|
||||||
type != "restarting";
|
type != "restarting";
|
||||||
if (hasCancel) {
|
if (hasCancel) {
|
||||||
buttons.insert(
|
buttons.insert(0, msgBoxButton(translate('Cancel'), cancel));
|
||||||
0,
|
|
||||||
msgBoxButton(translate('Cancel'), () {
|
|
||||||
dialogManager.dismissAll();
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
// TODO: test this button
|
// TODO: test this button
|
||||||
if (type.contains("hasclose")) {
|
if (type.contains("hasclose")) {
|
||||||
@ -460,8 +507,11 @@ void msgBox(
|
|||||||
}
|
}
|
||||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||||
title: _msgBoxTitle(title),
|
title: _msgBoxTitle(title),
|
||||||
content: Text(translate(text), style: TextStyle(fontSize: 15)),
|
content: Text(translate(text), style: const TextStyle(fontSize: 15)),
|
||||||
actions: buttons));
|
actions: buttons,
|
||||||
|
onSubmit: hasOk ? submit : null,
|
||||||
|
onCancel: hasCancel == true ? cancel : null,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget msgBoxButton(String text, void Function() onPressed) {
|
Widget msgBoxButton(String text, void Function() onPressed) {
|
||||||
@ -479,15 +529,19 @@ Widget msgBoxButton(String text, void Function() onPressed) {
|
|||||||
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));
|
Widget _msgBoxTitle(String title) =>
|
||||||
|
Text(translate(title), style: TextStyle(fontSize: 21));
|
||||||
|
|
||||||
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
|
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
|
||||||
Widget content, List<Widget> buttons) {
|
Widget content, List<Widget> buttons,
|
||||||
|
{bool hasCancel = true}) {
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||||
title: _msgBoxTitle(title),
|
title: _msgBoxTitle(title),
|
||||||
content: content,
|
content: content,
|
||||||
actions: buttons));
|
actions: buttons,
|
||||||
|
onCancel: hasCancel ? close : null,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Color str2color(String str, [alpha = 0xFF]) {
|
Color str2color(String str, [alpha = 0xFF]) {
|
||||||
|
|||||||
@ -1,4 +1,52 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// TODO: Divide every 3 number to display ID
|
class IDTextEditingController extends TextEditingController {
|
||||||
class IdFormController extends TextEditingController {}
|
IDTextEditingController({String? text}) : super(text: text);
|
||||||
|
|
||||||
|
String get id => trimID(value.text);
|
||||||
|
|
||||||
|
set id(String newID) => text = formatID(newID);
|
||||||
|
}
|
||||||
|
|
||||||
|
class IDTextInputFormatter extends TextInputFormatter {
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue, TextEditingValue newValue) {
|
||||||
|
if (newValue.text.isEmpty) {
|
||||||
|
return newValue.copyWith(text: '');
|
||||||
|
} else if (newValue.text.compareTo(oldValue.text) == 0) {
|
||||||
|
return newValue;
|
||||||
|
} else {
|
||||||
|
int selectionIndexFromTheRight =
|
||||||
|
newValue.text.length - newValue.selection.extentOffset;
|
||||||
|
String newID = formatID(newValue.text);
|
||||||
|
return TextEditingValue(
|
||||||
|
text: newID,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: newID.length - selectionIndexFromTheRight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatID(String id) {
|
||||||
|
String id2 = id.replaceAll(' ', '');
|
||||||
|
String newID = '';
|
||||||
|
if (id2.length <= 3) {
|
||||||
|
newID = id2;
|
||||||
|
} else {
|
||||||
|
var n = id2.length;
|
||||||
|
var a = n % 3 != 0 ? n % 3 : 3;
|
||||||
|
newID = id2.substring(0, a);
|
||||||
|
for (var i = a; i < n; i += 3) {
|
||||||
|
newID += " ${id2.substring(i, i + 3)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newID;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimID(String id) {
|
||||||
|
return id.replaceAll(' ', '');
|
||||||
|
}
|
||||||
|
|||||||
@ -1,16 +1,26 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../consts.dart';
|
import '../consts.dart';
|
||||||
|
import '../models/platform_model.dart';
|
||||||
|
|
||||||
class PrivacyModeState {
|
class PrivacyModeState {
|
||||||
static String tag(String id) => 'privacy_mode_$id';
|
static String tag(String id) => 'privacy_mode_$id';
|
||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (!Get.isRegistered(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: tag(id));
|
Get.put(state, tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (Get.isRegistered(tag: key)) {
|
||||||
|
Get.delete(tag: key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(String id) => Get.delete(tag: tag(id));
|
|
||||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,11 +28,20 @@ class BlockInputState {
|
|||||||
static String tag(String id) => 'block_input_$id';
|
static String tag(String id) => 'block_input_$id';
|
||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (!Get.isRegistered(tag: key)) {
|
||||||
final RxBool state = false.obs;
|
final RxBool state = false.obs;
|
||||||
Get.put(state, tag: tag(id));
|
Get.put(state, tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (Get.isRegistered(tag: key)) {
|
||||||
|
Get.delete(tag: key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(String id) => Get.delete(tag: tag(id));
|
|
||||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,11 +49,20 @@ class CurrentDisplayState {
|
|||||||
static String tag(String id) => 'current_display_$id';
|
static String tag(String id) => 'current_display_$id';
|
||||||
|
|
||||||
static void init(String id) {
|
static void init(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (!Get.isRegistered(tag: key)) {
|
||||||
final RxInt state = RxInt(0);
|
final RxInt state = RxInt(0);
|
||||||
Get.put(state, tag: tag(id));
|
Get.put(state, tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (Get.isRegistered(tag: key)) {
|
||||||
|
Get.delete(tag: key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void delete(String id) => Get.delete(tag: tag(id));
|
|
||||||
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
|
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,3 +113,46 @@ class ConnectionTypeState {
|
|||||||
static ConnectionType find(String id) =>
|
static ConnectionType find(String id) =>
|
||||||
Get.find<ConnectionType>(tag: tag(id));
|
Get.find<ConnectionType>(tag: tag(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ShowRemoteCursorState {
|
||||||
|
static String tag(String id) => 'show_remote_cursor_$id';
|
||||||
|
|
||||||
|
static void init(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (!Get.isRegistered(tag: key)) {
|
||||||
|
final RxBool state = false.obs;
|
||||||
|
Get.put(state, tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (Get.isRegistered(tag: key)) {
|
||||||
|
Get.delete(tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
class KeyboardEnabledState {
|
||||||
|
static String tag(String id) => 'keyboard_enabled_$id';
|
||||||
|
|
||||||
|
static void init(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (!Get.isRegistered(tag: key)) {
|
||||||
|
// Server side, default true
|
||||||
|
final RxBool state = true.obs;
|
||||||
|
Get.put(state, tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (Get.isRegistered(tag: key)) {
|
||||||
|
Get.delete(tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||||
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
|
import '../../common/formatter/id_formatter.dart';
|
||||||
import '../../mobile/pages/scan_page.dart';
|
import '../../mobile/pages/scan_page.dart';
|
||||||
import '../../mobile/pages/settings_page.dart';
|
import '../../mobile/pages/settings_page.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
@ -30,7 +31,7 @@ class ConnectionPage extends StatefulWidget {
|
|||||||
/// State for the connection page.
|
/// State for the connection page.
|
||||||
class _ConnectionPageState extends State<ConnectionPage> {
|
class _ConnectionPageState extends State<ConnectionPage> {
|
||||||
/// Controller for the id input bar.
|
/// Controller for the id input bar.
|
||||||
final _idController = TextEditingController();
|
final _idController = IDTextEditingController();
|
||||||
|
|
||||||
/// Update url. If it's not null, means an update is available.
|
/// Update url. If it's not null, means an update is available.
|
||||||
final _updateUrl = '';
|
final _updateUrl = '';
|
||||||
@ -43,9 +44,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
if (_idController.text.isEmpty) {
|
if (_idController.text.isEmpty) {
|
||||||
() async {
|
() async {
|
||||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||||
if (lastRemoteId != _idController.text) {
|
if (lastRemoteId != _idController.id) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_idController.text = lastRemoteId;
|
_idController.id = lastRemoteId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
@ -110,7 +111,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}) {
|
||||||
final id = _idController.text.trim();
|
final id = _idController.id;
|
||||||
connect(id, isFileTransfer: isFileTransfer);
|
connect(id, isFileTransfer: isFileTransfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
});
|
});
|
||||||
var w = Container(
|
var w = Container(
|
||||||
width: 320 + 20 * 2,
|
width: 320 + 20 * 2,
|
||||||
padding: EdgeInsets.fromLTRB(20, 24, 20, 22),
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 22),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: MyTheme.color(context).bg,
|
color: MyTheme.color(context).bg,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(13)),
|
borderRadius: const BorderRadius.all(Radius.circular(13)),
|
||||||
@ -178,44 +179,56 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
translate('Control Remote Desktop'),
|
translate('Control Remote Desktop'),
|
||||||
style: TextStyle(fontSize: 19, height: 1),
|
style: const TextStyle(fontSize: 19, height: 1),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).marginOnly(bottom: 15),
|
).marginOnly(bottom: 15),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: Obx(
|
||||||
|
() => TextField(
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
keyboardType: TextInputType.visiblePassword,
|
keyboardType: TextInputType.visiblePassword,
|
||||||
style: TextStyle(
|
focusNode: focusNode,
|
||||||
|
style: const TextStyle(
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
height: 1,
|
height: 1,
|
||||||
),
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
cursorColor: MyTheme.color(context).text!,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: translate('Enter Remote ID'),
|
hintText: inputFocused.value
|
||||||
|
? null
|
||||||
|
: translate('Enter Remote ID'),
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: MyTheme.color(context).placeholder),
|
color: MyTheme.color(context).placeholder),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: MyTheme.color(context).placeholder!)),
|
color: MyTheme.color(context).border!)),
|
||||||
focusedBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: MyTheme.color(context).border!)),
|
||||||
|
focusedBorder: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
borderSide:
|
borderSide:
|
||||||
BorderSide(color: MyTheme.button, width: 3),
|
BorderSide(color: MyTheme.button, width: 3),
|
||||||
),
|
),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
EdgeInsets.symmetric(horizontal: 10, vertical: 12)),
|
horizontal: 10, vertical: 12)),
|
||||||
controller: _idController,
|
controller: _idController,
|
||||||
|
inputFormatters: [IDTextInputFormatter()],
|
||||||
onSubmitted: (s) {
|
onSubmitted: (s) {
|
||||||
onConnect();
|
onConnect();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@ -259,7 +272,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
).marginSymmetric(horizontal: 12),
|
).marginSymmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 17,
|
width: 17,
|
||||||
),
|
),
|
||||||
Obx(
|
Obx(
|
||||||
@ -304,7 +317,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(constraints: BoxConstraints(maxWidth: 600), child: w));
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 600), child: w));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -654,52 +668,15 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
var field = "";
|
var field = "";
|
||||||
var msg = "";
|
var msg = "";
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
|
TextEditingController controller = TextEditingController(text: field);
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
submit() async {
|
||||||
title: Text(translate("Add ID")),
|
|
||||||
content: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(translate("whitelist_sep")),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
onChanged: (s) {
|
|
||||||
field = s;
|
|
||||||
},
|
|
||||||
maxLines: null,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText: msg.isEmpty ? null : translate(msg),
|
|
||||||
),
|
|
||||||
controller: TextEditingController(text: field),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 4.0,
|
|
||||||
),
|
|
||||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
msg = "";
|
msg = "";
|
||||||
isInProgress = true;
|
isInProgress = true;
|
||||||
});
|
});
|
||||||
field = field.trim();
|
field = controller.text.trim();
|
||||||
if (field.isEmpty) {
|
if (field.isEmpty) {
|
||||||
// pass
|
// pass
|
||||||
} else {
|
} else {
|
||||||
@ -716,9 +693,44 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
// final currentPeers
|
// final currentPeers
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
},
|
}
|
||||||
child: Text(translate("OK"))),
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Add ID")),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(translate("whitelist_sep")),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
maxLines: null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
|
),
|
||||||
|
controller: controller,
|
||||||
|
focusNode: FocusNode()..requestFocus()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4.0,
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -727,52 +739,14 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
var field = "";
|
var field = "";
|
||||||
var msg = "";
|
var msg = "";
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
|
TextEditingController controller = TextEditingController(text: field);
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
submit() async {
|
||||||
title: Text(translate("Add Tag")),
|
|
||||||
content: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(translate("whitelist_sep")),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
onChanged: (s) {
|
|
||||||
field = s;
|
|
||||||
},
|
|
||||||
maxLines: null,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText: msg.isEmpty ? null : translate(msg),
|
|
||||||
),
|
|
||||||
controller: TextEditingController(text: field),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 4.0,
|
|
||||||
),
|
|
||||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
msg = "";
|
msg = "";
|
||||||
isInProgress = true;
|
isInProgress = true;
|
||||||
});
|
});
|
||||||
field = field.trim();
|
field = controller.text.trim();
|
||||||
if (field.isEmpty) {
|
if (field.isEmpty) {
|
||||||
// pass
|
// pass
|
||||||
} else {
|
} else {
|
||||||
@ -785,9 +759,45 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
// final currentPeers
|
// final currentPeers
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
},
|
}
|
||||||
child: Text(translate("OK"))),
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Add Tag")),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(translate("whitelist_sep")),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
maxLines: null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
|
),
|
||||||
|
controller: controller,
|
||||||
|
focusNode: FocusNode()..requestFocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4.0,
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -799,13 +809,23 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
|
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
|
submit() async {
|
||||||
|
setState(() {
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||||
|
await gFFI.abModel.updateAb();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("Edit Tag")),
|
title: Text(translate("Edit Tag")),
|
||||||
content: Column(
|
content: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: tags
|
children: tags
|
||||||
.map((e) => buildTag(e, selectedTag, onTap: () {
|
.map((e) => buildTag(e, selectedTag, onTap: () {
|
||||||
@ -818,26 +838,16 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
.toList(growable: false),
|
.toList(growable: false),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
onPressed: () {
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
|
||||||
isInProgress = true;
|
|
||||||
});
|
|
||||||
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
|
||||||
await gFFI.abModel.updateAb();
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("OK"))),
|
|
||||||
],
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,8 @@ class ConnectionTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||||
final tabController = Get.put(DesktopTabController());
|
final tabController =
|
||||||
|
Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen));
|
||||||
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||||
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
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'];
|
||||||
|
ConnectionTypeState.init(id);
|
||||||
window_on_top(windowId());
|
window_on_top(windowId());
|
||||||
ConnectionTypeState.init(id);
|
ConnectionTypeState.init(id);
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
@ -81,7 +83,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
|
||||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||||
return Obx(() => SubWindowDragToResizeArea(
|
return Obx(() => SubWindowDragToResizeArea(
|
||||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||||
@ -93,15 +94,11 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
backgroundColor: MyTheme.color(context).bg,
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
body: Obx(() => DesktopTab(
|
body: Obx(() => DesktopTab(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
theme: theme,
|
|
||||||
tabType: DesktopTabType.remoteScreen,
|
|
||||||
showTabBar: fullscreen.isFalse,
|
showTabBar: fullscreen.isFalse,
|
||||||
onClose: () {
|
onClose: () {
|
||||||
tabController.clear();
|
tabController.clear();
|
||||||
},
|
},
|
||||||
tail: AddButton(
|
tail: AddButton().paddingOnly(left: 10),
|
||||||
theme: theme,
|
|
||||||
).paddingOnly(left: 10),
|
|
||||||
pageViewBuilder: (pageView) {
|
pageViewBuilder: (pageView) {
|
||||||
WindowController.fromWindowId(windowId())
|
WindowController.fromWindowId(windowId())
|
||||||
.setFullscreen(fullscreen.isTrue);
|
.setFullscreen(fullscreen.isTrue);
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
|
import 'package:flutter_hbb/desktop/pages/connection_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/popup_menu.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
|
||||||
|
as mod_menu;
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/server_model.dart';
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
@ -52,7 +55,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
buildServerInfo(context),
|
buildServerInfo(context),
|
||||||
VerticalDivider(
|
const VerticalDivider(
|
||||||
width: 1,
|
width: 1,
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
),
|
),
|
||||||
@ -90,23 +93,23 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
buildIDBoard(BuildContext context) {
|
buildIDBoard(BuildContext context) {
|
||||||
final model = gFFI.serverModel;
|
final model = gFFI.serverModel;
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.only(left: 20, right: 16),
|
margin: const EdgeInsets.only(left: 20, right: 11),
|
||||||
height: 52,
|
height: 57,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
textBaseline: TextBaseline.alphabetic,
|
textBaseline: TextBaseline.alphabetic,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 2,
|
width: 2,
|
||||||
decoration: BoxDecoration(color: MyTheme.accent),
|
decoration: const BoxDecoration(color: MyTheme.accent),
|
||||||
),
|
).marginOnly(top: 5),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 7),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
SizedBox(
|
||||||
height: 25,
|
height: 25,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@ -117,7 +120,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: MyTheme.color(context).lightText),
|
color: MyTheme.color(context).lightText),
|
||||||
),
|
).marginOnly(top: 5),
|
||||||
buildPopupMenu(context)
|
buildPopupMenu(context)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -132,11 +135,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: model.serverId,
|
controller: model.serverId,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.only(bottom: 18),
|
contentPadding: EdgeInsets.only(bottom: 20),
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -241,7 +244,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
},
|
},
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => CircleAvatar(
|
() => CircleAvatar(
|
||||||
radius: 12,
|
radius: 15,
|
||||||
backgroundColor: hover.value
|
backgroundColor: hover.value
|
||||||
? MyTheme.color(context).grayBg!
|
? MyTheme.color(context).grayBg!
|
||||||
: MyTheme.color(context).bg!,
|
: MyTheme.color(context).bg!,
|
||||||
@ -274,7 +277,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 7),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -300,7 +303,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
readOnly: true,
|
readOnly: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.only(bottom: 8),
|
contentPadding: EdgeInsets.only(bottom: 2),
|
||||||
),
|
),
|
||||||
style: TextStyle(fontSize: 15),
|
style: TextStyle(fontSize: 15),
|
||||||
),
|
),
|
||||||
@ -314,7 +317,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
? MyTheme.color(context).text
|
? MyTheme.color(context).text
|
||||||
: Color(0xFFDDDDDD),
|
: Color(0xFFDDDDDD),
|
||||||
size: 22,
|
size: 22,
|
||||||
).marginOnly(right: 10, bottom: 8),
|
).marginOnly(right: 8, bottom: 2),
|
||||||
),
|
),
|
||||||
onTap: () => bind.mainUpdateTemporaryPassword(),
|
onTap: () => bind.mainUpdateTemporaryPassword(),
|
||||||
onHover: (value) => refreshHover.value = value,
|
onHover: (value) => refreshHover.value = value,
|
||||||
@ -422,13 +425,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
color: editHover.value
|
color: editHover.value
|
||||||
? MyTheme.color(context).text
|
? MyTheme.color(context).text
|
||||||
: Color(0xFFDDDDDD))
|
: Color(0xFFDDDDDD))
|
||||||
.marginOnly(bottom: 8)));
|
.marginOnly(bottom: 2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTip(BuildContext context) {
|
buildTip(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 14),
|
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -639,53 +642,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
var newId = "";
|
var newId = "";
|
||||||
var msg = "";
|
var msg = "";
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
|
TextEditingController controller = TextEditingController();
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
submit() async {
|
||||||
title: Text(translate("Change ID")),
|
newId = controller.text.trim();
|
||||||
content: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(translate("id_change_tip")),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text("ID:").marginOnly(bottom: 16.0),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
onChanged: (s) {
|
|
||||||
newId = s;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText: msg.isEmpty ? null : translate(msg)),
|
|
||||||
inputFormatters: [
|
|
||||||
LengthLimitingTextInputFormatter(16),
|
|
||||||
// FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
|
|
||||||
],
|
|
||||||
maxLength: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 4.0,
|
|
||||||
),
|
|
||||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
msg = "";
|
msg = "";
|
||||||
isInProgress = true;
|
isInProgress = true;
|
||||||
@ -694,7 +654,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
|
|
||||||
var status = await bind.mainGetAsyncStatus();
|
var status = await bind.mainGetAsyncStatus();
|
||||||
while (status == " ") {
|
while (status == " ") {
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
status = await bind.mainGetAsyncStatus();
|
status = await bind.mainGetAsyncStatus();
|
||||||
}
|
}
|
||||||
if (status.isEmpty) {
|
if (status.isEmpty) {
|
||||||
@ -706,9 +666,52 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
msg = translate(status);
|
msg = translate(status);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
child: Text(translate("OK"))),
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Change ID")),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(translate("id_change_tip")),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text("ID:").marginOnly(bottom: 16.0),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: msg.isEmpty ? null : translate(msg)),
|
||||||
|
inputFormatters: [
|
||||||
|
LengthLimitingTextInputFormatter(16),
|
||||||
|
// FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
|
||||||
],
|
],
|
||||||
|
maxLength: 16,
|
||||||
|
controller: controller,
|
||||||
|
focusNode: FocusNode()..requestFocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4.0,
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -717,16 +720,16 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
final appName = await bind.mainGetAppName();
|
final appName = await bind.mainGetAppName();
|
||||||
final license = await bind.mainGetLicense();
|
final license = await bind.mainGetLicense();
|
||||||
final version = await bind.mainGetVersion();
|
final version = await bind.mainGetVersion();
|
||||||
final linkStyle = TextStyle(decoration: TextDecoration.underline);
|
const linkStyle = TextStyle(decoration: TextDecoration.underline);
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text("About $appName"),
|
title: Text("About $appName"),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: BoxConstraints(minWidth: 500),
|
constraints: const BoxConstraints(minWidth: 500),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 8.0,
|
height: 8.0,
|
||||||
),
|
),
|
||||||
Text("Version: $version").marginSymmetric(vertical: 4.0),
|
Text("Version: $version").marginSymmetric(vertical: 4.0),
|
||||||
@ -734,7 +737,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString("https://rustdesk.com/privacy");
|
launchUrlString("https://rustdesk.com/privacy");
|
||||||
},
|
},
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Privacy Statement",
|
"Privacy Statement",
|
||||||
style: linkStyle,
|
style: linkStyle,
|
||||||
).marginSymmetric(vertical: 4.0)),
|
).marginSymmetric(vertical: 4.0)),
|
||||||
@ -742,13 +745,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString("https://rustdesk.com");
|
launchUrlString("https://rustdesk.com");
|
||||||
},
|
},
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Website",
|
"Website",
|
||||||
style: linkStyle,
|
style: linkStyle,
|
||||||
).marginSymmetric(vertical: 4.0)),
|
).marginSymmetric(vertical: 4.0)),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(color: Color(0xFF2c8cff)),
|
decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
|
||||||
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -757,9 +761,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Copyright © 2022 Purslane Ltd.\n$license",
|
"Copyright © 2022 Purslane Ltd.\n$license",
|
||||||
style: TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
Text(
|
const Text(
|
||||||
"Made with heart in this chaotic world!",
|
"Made with heart in this chaotic world!",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
@ -775,12 +779,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(onPressed: close, child: Text(translate("OK"))),
|
||||||
onPressed: () async {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("OK"))),
|
|
||||||
],
|
],
|
||||||
|
onSubmit: close,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -812,86 +814,18 @@ Future<bool> loginDialog() async {
|
|||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
var completer = Completer<bool>();
|
var completer = Completer<bool>();
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
submit() async {
|
||||||
title: Text(translate("Login")),
|
|
||||||
content: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 500),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text(
|
|
||||||
"${translate('Username')}:",
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
).marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
|
||||||
controller: userContontroller,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text("${translate('Password')}:")
|
|
||||||
.marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
obscureText: true,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText: passMsg.isNotEmpty ? passMsg : null),
|
|
||||||
controller: pwdController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 4.0,
|
|
||||||
),
|
|
||||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
completer.complete(false);
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
userNameMsg = "";
|
userNameMsg = "";
|
||||||
passMsg = "";
|
passMsg = "";
|
||||||
isInProgress = true;
|
isInProgress = true;
|
||||||
});
|
});
|
||||||
final cancel = () {
|
cancel() {
|
||||||
setState(() {
|
setState(() {
|
||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
userName = userContontroller.text;
|
userName = userContontroller.text;
|
||||||
pass = pwdController.text;
|
pass = pwdController.text;
|
||||||
if (userName.isEmpty) {
|
if (userName.isEmpty) {
|
||||||
@ -916,14 +850,88 @@ Future<bool> loginDialog() async {
|
|||||||
debugPrint("$resp");
|
debugPrint("$resp");
|
||||||
completer.complete(true);
|
completer.complete(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// ignore: avoid_print
|
||||||
print(err.toString());
|
print(err.toString());
|
||||||
cancel();
|
cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
},
|
}
|
||||||
child: Text(translate("OK"))),
|
|
||||||
|
cancel() {
|
||||||
|
completer.complete(false);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Login")),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 500),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text(
|
||||||
|
"${translate('Username')}:",
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
).marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
||||||
|
controller: userContontroller,
|
||||||
|
focusNode: FocusNode()..requestFocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Password')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: passMsg.isNotEmpty ? passMsg : null),
|
||||||
|
controller: pwdController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4.0,
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: cancel, child: Text(translate("Cancel"))),
|
||||||
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: cancel,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return completer.future;
|
return completer.future;
|
||||||
@ -937,72 +945,7 @@ void setPasswordDialog() async {
|
|||||||
var errMsg1 = "";
|
var errMsg1 = "";
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
submit() {
|
||||||
title: Text(translate("Set Password")),
|
|
||||||
content: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 500),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text(
|
|
||||||
"${translate('Password')}:",
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
).marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
obscureText: true,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
|
|
||||||
controller: p0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text("${translate('Confirmation')}:")
|
|
||||||
.marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
obscureText: true,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
|
|
||||||
controller: p1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
errMsg0 = "";
|
errMsg0 = "";
|
||||||
errMsg1 = "";
|
errMsg1 = "";
|
||||||
@ -1022,9 +965,73 @@ void setPasswordDialog() async {
|
|||||||
}
|
}
|
||||||
bind.mainSetPermanentPassword(password: pass);
|
bind.mainSetPermanentPassword(password: pass);
|
||||||
close();
|
close();
|
||||||
},
|
}
|
||||||
child: Text(translate("OK"))),
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Set Password")),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 500),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text(
|
||||||
|
"${translate('Password')}:",
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
).marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
|
||||||
|
controller: p0,
|
||||||
|
focusNode: FocusNode()..requestFocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Confirmation')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
obscureText: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
|
||||||
|
controller: p1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|||||||
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp),
|
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp),
|
||||||
_TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp),
|
_TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp),
|
||||||
_TabInfo('Connection', Icons.link_outlined, Icons.link_sharp),
|
_TabInfo('Connection', Icons.link_outlined, Icons.link_sharp),
|
||||||
_TabInfo('About RustDesk', Icons.info_outline, Icons.info_sharp)
|
_TabInfo('About', Icons.info_outline, Icons.info_sharp)
|
||||||
];
|
];
|
||||||
|
|
||||||
late PageController controller;
|
late PageController controller;
|
||||||
@ -714,7 +714,7 @@ class _AboutState extends State<_About> {
|
|||||||
],
|
],
|
||||||
).marginOnly(left: _kContentHMargin)
|
).marginOnly(left: _kContentHMargin)
|
||||||
]),
|
]),
|
||||||
]).marginOnly(left: _kCardLeftMargin);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1038,138 +1038,29 @@ void changeServer() async {
|
|||||||
var keyController = TextEditingController(text: key);
|
var keyController = TextEditingController(text: key);
|
||||||
|
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
submit() async {
|
||||||
title: Text(translate("ID/Relay Server")),
|
|
||||||
content: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 500),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text("${translate('ID Server')}:")
|
|
||||||
.marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
|
||||||
controller: idController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text("${translate('Relay Server')}:")
|
|
||||||
.marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText:
|
|
||||||
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
|
||||||
controller: relayController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text("${translate('API Server')}:")
|
|
||||||
.marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText:
|
|
||||||
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
|
||||||
controller: apiController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child:
|
|
||||||
Text("${translate('Key')}:").marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
controller: keyController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 4.0,
|
|
||||||
),
|
|
||||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
[idServerMsg, relayServerMsg, apiServerMsg].forEach((element) {
|
[idServerMsg, relayServerMsg, apiServerMsg].forEach((element) {
|
||||||
element = "";
|
element = "";
|
||||||
});
|
});
|
||||||
isInProgress = true;
|
isInProgress = true;
|
||||||
});
|
});
|
||||||
final cancel = () {
|
cancel() {
|
||||||
setState(() {
|
setState(() {
|
||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
idServer = idController.text.trim();
|
idServer = idController.text.trim();
|
||||||
relayServer = relayController.text.trim();
|
relayServer = relayController.text.trim();
|
||||||
apiServer = apiController.text.trim().toLowerCase();
|
apiServer = apiController.text.trim().toLowerCase();
|
||||||
key = keyController.text.trim();
|
key = keyController.text.trim();
|
||||||
|
|
||||||
if (idServer.isNotEmpty) {
|
if (idServer.isNotEmpty) {
|
||||||
idServerMsg = translate(
|
idServerMsg =
|
||||||
await bind.mainTestIfValidServer(server: idServer));
|
translate(await bind.mainTestIfValidServer(server: idServer));
|
||||||
if (idServerMsg.isEmpty) {
|
if (idServerMsg.isEmpty) {
|
||||||
oldOptions['custom-rendezvous-server'] = idServer;
|
oldOptions['custom-rendezvous-server'] = idServer;
|
||||||
} else {
|
} else {
|
||||||
@ -1181,8 +1072,8 @@ void changeServer() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (relayServer.isNotEmpty) {
|
if (relayServer.isNotEmpty) {
|
||||||
relayServerMsg = translate(
|
relayServerMsg =
|
||||||
await bind.mainTestIfValidServer(server: relayServer));
|
translate(await bind.mainTestIfValidServer(server: relayServer));
|
||||||
if (relayServerMsg.isEmpty) {
|
if (relayServerMsg.isEmpty) {
|
||||||
oldOptions['relay-server'] = relayServer;
|
oldOptions['relay-server'] = relayServer;
|
||||||
} else {
|
} else {
|
||||||
@ -1210,9 +1101,120 @@ void changeServer() async {
|
|||||||
oldOptions['key'] = key;
|
oldOptions['key'] = key;
|
||||||
await bind.mainSetOptions(json: jsonEncode(oldOptions));
|
await bind.mainSetOptions(json: jsonEncode(oldOptions));
|
||||||
close();
|
close();
|
||||||
},
|
}
|
||||||
child: Text(translate("OK"))),
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("ID/Relay Server")),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 500),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('ID Server')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
||||||
|
controller: idController,
|
||||||
|
focusNode: FocusNode()..requestFocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Relay Server')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText:
|
||||||
|
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
||||||
|
controller: relayController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('API Server')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText:
|
||||||
|
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
||||||
|
controller: apiController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child:
|
||||||
|
Text("${translate('Key')}:").marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
controller: keyController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4.0,
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1231,7 +1233,7 @@ void changeWhiteList() async {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(translate("whitelist_sep")),
|
Text(translate("whitelist_sep")),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 8.0,
|
height: 8.0,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@ -1240,18 +1242,19 @@ void changeWhiteList() async {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
errorText: msg.isEmpty ? null : translate(msg),
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
),
|
),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
),
|
focusNode: FocusNode()..requestFocus()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 4.0,
|
height: 4.0,
|
||||||
),
|
),
|
||||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@ -1277,7 +1280,7 @@ void changeWhiteList() async {
|
|||||||
final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$");
|
final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$");
|
||||||
for (final ip in ips) {
|
for (final ip in ips) {
|
||||||
if (!ipMatch.hasMatch(ip)) {
|
if (!ipMatch.hasMatch(ip)) {
|
||||||
msg = translate("Invalid IP") + " $ip";
|
msg = "${translate("Invalid IP")} $ip";
|
||||||
setState(() {
|
setState(() {
|
||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
@ -1292,6 +1295,7 @@ void changeWhiteList() async {
|
|||||||
},
|
},
|
||||||
child: Text(translate("OK"))),
|
child: Text(translate("OK"))),
|
||||||
],
|
],
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1314,110 +1318,23 @@ void changeSocks5Proxy() async {
|
|||||||
|
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
submit() async {
|
||||||
title: Text(translate("Socks5 Proxy")),
|
|
||||||
content: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 500),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text("${translate('Hostname')}:")
|
|
||||||
.marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
|
||||||
controller: proxyController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text("${translate('Username')}:")
|
|
||||||
.marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
controller: userController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
|
||||||
child: Text("${translate('Password')}:")
|
|
||||||
.marginOnly(bottom: 16.0)),
|
|
||||||
SizedBox(
|
|
||||||
width: 24.0,
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
controller: pwdController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 8.0,
|
|
||||||
),
|
|
||||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
proxyMsg = "";
|
proxyMsg = "";
|
||||||
isInProgress = true;
|
isInProgress = true;
|
||||||
});
|
});
|
||||||
final cancel = () {
|
cancel() {
|
||||||
setState(() {
|
setState(() {
|
||||||
isInProgress = false;
|
isInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
proxy = proxyController.text.trim();
|
proxy = proxyController.text.trim();
|
||||||
username = userController.text.trim();
|
username = userController.text.trim();
|
||||||
password = pwdController.text.trim();
|
password = pwdController.text.trim();
|
||||||
|
|
||||||
if (proxy.isNotEmpty) {
|
if (proxy.isNotEmpty) {
|
||||||
proxyMsg =
|
proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy));
|
||||||
translate(await bind.mainTestIfValidServer(server: proxy));
|
|
||||||
if (proxyMsg.isEmpty) {
|
if (proxyMsg.isEmpty) {
|
||||||
// ignore
|
// ignore
|
||||||
} else {
|
} else {
|
||||||
@ -1428,9 +1345,96 @@ void changeSocks5Proxy() async {
|
|||||||
await bind.mainSetSocks(
|
await bind.mainSetSocks(
|
||||||
proxy: proxy, username: username, password: password);
|
proxy: proxy, username: username, password: password);
|
||||||
close();
|
close();
|
||||||
},
|
}
|
||||||
child: Text(translate("OK"))),
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Socks5 Proxy")),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 500),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Hostname')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
||||||
|
controller: proxyController,
|
||||||
|
focusNode: FocusNode()..requestFocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Username')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
controller: userController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
|
child: Text("${translate('Password')}:")
|
||||||
|
.marginOnly(bottom: 16.0)),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
controller: pwdController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8.0,
|
||||||
|
),
|
||||||
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DesktopTabPageState extends State<DesktopTabPage> {
|
class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||||
final tabController = DesktopTabController();
|
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -41,22 +41,23 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: MyTheme.color(context).border!)),
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
child: Scaffold(
|
child: Overlay(initialEntries: [
|
||||||
|
OverlayEntry(builder: (context) {
|
||||||
|
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
||||||
|
return Scaffold(
|
||||||
backgroundColor: MyTheme.color(context).bg,
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
body: DesktopTab(
|
body: DesktopTab(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
|
||||||
tabType: DesktopTabType.main,
|
|
||||||
tail: ActionIcon(
|
tail: ActionIcon(
|
||||||
message: 'Settings',
|
message: 'Settings',
|
||||||
icon: IconFont.menu,
|
icon: IconFont.menu,
|
||||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
|
||||||
onTap: onAddSetting,
|
onTap: onAddSetting,
|
||||||
is_close: false,
|
isClose: false,
|
||||||
),
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAddSetting() {
|
void onAddSetting() {
|
||||||
|
|||||||
@ -59,8 +59,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_ffi = FFI();
|
_ffi = FFI();
|
||||||
_ffi.connect(widget.id, isFileTransfer: true);
|
_ffi.connect(widget.id, isFileTransfer: true);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_ffi.dialogManager
|
||||||
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||||
|
});
|
||||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
Get.put(_ffi, tag: 'ft_${widget.id}');
|
||||||
// _ffi.ffiModel.updateEventListener(widget.id);
|
|
||||||
if (!Platform.isLinux) {
|
if (!Platform.isLinux) {
|
||||||
Wakelock.enable();
|
Wakelock.enable();
|
||||||
}
|
}
|
||||||
@ -117,7 +120,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
|
|
||||||
Widget menu({bool isLocal = false}) {
|
Widget menu({bool isLocal = false}) {
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
icon: Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
|
splashRadius: 20,
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@ -413,6 +417,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
/// watch transfer status
|
/// watch transfer status
|
||||||
Widget statusList() {
|
Widget statusList() {
|
||||||
return PreferredSize(
|
return PreferredSize(
|
||||||
|
preferredSize: const Size(200, double.infinity),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
|
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
@ -429,8 +434,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
children: [
|
children: [
|
||||||
Transform.rotate(
|
Transform.rotate(
|
||||||
angle: item.isRemote ? pi : 0,
|
angle: item.isRemote ? pi : 0,
|
||||||
child: Icon(Icons.send)),
|
child: const Icon(Icons.send)),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 16.0,
|
width: 16.0,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -441,7 +446,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
Tooltip(
|
Tooltip(
|
||||||
message: item.jobName,
|
message: item.jobName,
|
||||||
child: Text(
|
child: Text(
|
||||||
'${item.jobName}',
|
item.jobName,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)),
|
)),
|
||||||
@ -455,7 +460,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
offstage:
|
offstage:
|
||||||
item.state != JobState.inProgress,
|
item.state != JobState.inProgress,
|
||||||
child: Text(
|
child: Text(
|
||||||
'${readableFileSize(item.speed) + "/s"} ')),
|
'${"${readableFileSize(item.speed)}/s"} ')),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: item.totalSize <= 0,
|
offstage: item.totalSize <= 0,
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -475,10 +480,12 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
model.resumeJob(item.id);
|
model.resumeJob(item.id);
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.restart_alt_rounded)),
|
splashRadius: 20,
|
||||||
|
icon: const Icon(Icons.restart_alt_rounded)),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
|
splashRadius: 20,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
model.jobTable.removeAt(index);
|
model.jobTable.removeAt(index);
|
||||||
model.cancelJob(item.id);
|
model.cancelJob(item.id);
|
||||||
@ -500,8 +507,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
itemCount: model.jobTable.length,
|
itemCount: model.jobTable.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
));
|
||||||
preferredSize: Size(200, double.infinity));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
goBack({bool? isLocal}) {
|
goBack({bool? isLocal}) {
|
||||||
@ -554,9 +560,12 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
model.goHome(isLocal: isLocal);
|
model.goHome(isLocal: isLocal);
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.home_outlined)),
|
icon: const Icon(Icons.home_outlined),
|
||||||
|
splashRadius: 20,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.arrow_upward),
|
icon: const Icon(Icons.arrow_upward),
|
||||||
|
splashRadius: 20,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
goBack(isLocal: isLocal);
|
goBack(isLocal: isLocal);
|
||||||
},
|
},
|
||||||
@ -622,13 +631,15 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
),
|
),
|
||||||
))
|
))
|
||||||
],
|
],
|
||||||
child: Icon(Icons.search),
|
splashRadius: 20,
|
||||||
|
child: const Icon(Icons.search),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
model.refresh(isLocal: isLocal);
|
model.refresh(isLocal: isLocal);
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.refresh)),
|
splashRadius: 20,
|
||||||
|
icon: const Icon(Icons.refresh)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@ -642,8 +653,21 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final name = TextEditingController();
|
final name = TextEditingController();
|
||||||
_ffi.dialogManager
|
_ffi.dialogManager.show((setState, close) {
|
||||||
.show((setState, close) => CustomAlertDialog(
|
submit() {
|
||||||
|
if (name.value.text.isNotEmpty) {
|
||||||
|
model.createDir(
|
||||||
|
PathUtil.join(
|
||||||
|
model.getCurrentDir(isLocal).path,
|
||||||
|
name.value.text,
|
||||||
|
model.getCurrentIsWindows(isLocal)),
|
||||||
|
isLocal: isLocal);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() => close(false);
|
||||||
|
return CustomAlertDialog(
|
||||||
title: Text(translate("Create Folder")),
|
title: Text(translate("Create Folder")),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -654,35 +678,27 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
"Please enter the folder name"),
|
"Please enter the folder name"),
|
||||||
),
|
),
|
||||||
controller: name,
|
controller: name,
|
||||||
|
focusNode: FocusNode()..requestFocus(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () => close(false),
|
onPressed: cancel,
|
||||||
child: Text(translate("Cancel"))),
|
child: Text(translate("Cancel"))),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: submit,
|
||||||
if (name.value.text.isNotEmpty) {
|
|
||||||
model.createDir(
|
|
||||||
PathUtil.join(
|
|
||||||
model
|
|
||||||
.getCurrentDir(
|
|
||||||
isLocal)
|
|
||||||
.path,
|
|
||||||
name.value.text,
|
|
||||||
model.getCurrentIsWindows(
|
|
||||||
isLocal)),
|
|
||||||
isLocal: isLocal);
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(translate("OK")))
|
child: Text(translate("OK")))
|
||||||
]));
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: cancel,
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.create_new_folder_outlined)),
|
splashRadius: 20,
|
||||||
|
icon: const Icon(Icons.create_new_folder_outlined)),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final items = isLocal
|
final items = isLocal
|
||||||
@ -691,7 +707,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
await (model.removeAction(items, isLocal: isLocal));
|
await (model.removeAction(items, isLocal: isLocal));
|
||||||
items.clear();
|
items.clear();
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.delete_forever_outlined)),
|
splashRadius: 20,
|
||||||
|
icon: const Icon(Icons.delete_forever_outlined)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -703,7 +720,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
|||||||
},
|
},
|
||||||
icon: Transform.rotate(
|
icon: Transform.rotate(
|
||||||
angle: isLocal ? 0 : pi,
|
angle: isLocal ? 0 : pi,
|
||||||
child: Icon(
|
child: const Icon(
|
||||||
Icons.send,
|
Icons.send,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -25,7 +25,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
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());
|
Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer));
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: params['id'],
|
key: params['id'],
|
||||||
label: params['id'],
|
label: params['id'],
|
||||||
@ -62,8 +62,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme =
|
|
||||||
isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light();
|
|
||||||
return SubWindowDragToResizeArea(
|
return SubWindowDragToResizeArea(
|
||||||
windowId: windowId(),
|
windowId: windowId(),
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -73,14 +71,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
backgroundColor: MyTheme.color(context).bg,
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
body: DesktopTab(
|
body: DesktopTab(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
theme: theme,
|
|
||||||
tabType: DesktopTabType.fileTransfer,
|
|
||||||
onClose: () {
|
onClose: () {
|
||||||
tabController.clear();
|
tabController.clear();
|
||||||
},
|
},
|
||||||
tail: AddButton(
|
tail: AddButton().paddingOnly(left: 10),
|
||||||
theme: theme,
|
|
||||||
).paddingOnly(left: 10),
|
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -70,6 +70,9 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
return Overlay(initialEntries: [
|
||||||
|
OverlayEntry(builder: (context) {
|
||||||
|
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: MyTheme.color(context).grayBg,
|
backgroundColor: MyTheme.color(context).grayBg,
|
||||||
body: FutureBuilder(future: () async {
|
body: FutureBuilder(future: () async {
|
||||||
@ -90,9 +93,11 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: MyTheme.color(context).bg,
|
color: MyTheme.color(context).bg,
|
||||||
border: Border.all(width: 1, color: MyTheme.border)),
|
border:
|
||||||
child:
|
Border.all(width: 1, color: MyTheme.border)),
|
||||||
widget.isRDP ? buildRdp(context) : buildTunnel(context),
|
child: widget.isRDP
|
||||||
|
? buildRdp(context)
|
||||||
|
: buildTunnel(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -102,6 +107,8 @@ class _PortForwardPageState extends State<PortForwardPage>
|
|||||||
return const Offstage();
|
return const Offstage();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPrompt(BuildContext context) {
|
buildPrompt(BuildContext context) {
|
||||||
|
|||||||
@ -18,7 +18,7 @@ class PortForwardTabPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||||
final tabController = Get.put(DesktopTabController());
|
late final DesktopTabController tabController;
|
||||||
late final bool isRDP;
|
late final bool isRDP;
|
||||||
|
|
||||||
static const IconData selectedIcon = Icons.forward_sharp;
|
static const IconData selectedIcon = Icons.forward_sharp;
|
||||||
@ -26,6 +26,8 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
|
|
||||||
_PortForwardTabPageState(Map<String, dynamic> params) {
|
_PortForwardTabPageState(Map<String, dynamic> params) {
|
||||||
isRDP = params['isRDP'];
|
isRDP = params['isRDP'];
|
||||||
|
tabController = Get.put(DesktopTabController(
|
||||||
|
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward));
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
key: params['id'],
|
key: params['id'],
|
||||||
label: params['id'],
|
label: params['id'],
|
||||||
@ -67,7 +69,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
|
||||||
return SubWindowDragToResizeArea(
|
return SubWindowDragToResizeArea(
|
||||||
windowId: windowId(),
|
windowId: windowId(),
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -77,14 +78,10 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
backgroundColor: MyTheme.color(context).bg,
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
body: DesktopTab(
|
body: DesktopTab(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
theme: theme,
|
|
||||||
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward,
|
|
||||||
onClose: () {
|
onClose: () {
|
||||||
tabController.clear();
|
tabController.clear();
|
||||||
},
|
},
|
||||||
tail: AddButton(
|
tail: AddButton().paddingOnly(left: 10),
|
||||||
theme: theme,
|
|
||||||
).paddingOnly(left: 10),
|
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,7 +17,6 @@ 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';
|
import '../../common/shared_state.dart';
|
||||||
|
|
||||||
final initText = '\1' * 1024;
|
final initText = '\1' * 1024;
|
||||||
@ -39,10 +38,11 @@ class RemotePage extends StatefulWidget {
|
|||||||
class _RemotePageState extends State<RemotePage>
|
class _RemotePageState extends State<RemotePage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
bool _showBar = !isWebDesktop;
|
|
||||||
String _value = '';
|
String _value = '';
|
||||||
String keyboardMode = "legacy";
|
String keyboardMode = "legacy";
|
||||||
final _cursorOverImage = false.obs;
|
final _cursorOverImage = false.obs;
|
||||||
|
late RxBool _showRemoteCursor;
|
||||||
|
late RxBool _keyboardEnabled;
|
||||||
|
|
||||||
final FocusNode _mobileFocusNode = FocusNode();
|
final FocusNode _mobileFocusNode = FocusNode();
|
||||||
final FocusNode _physicalFocusNode = FocusNode();
|
final FocusNode _physicalFocusNode = FocusNode();
|
||||||
@ -59,17 +59,24 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
PrivacyModeState.init(id);
|
PrivacyModeState.init(id);
|
||||||
BlockInputState.init(id);
|
BlockInputState.init(id);
|
||||||
CurrentDisplayState.init(id);
|
CurrentDisplayState.init(id);
|
||||||
|
KeyboardEnabledState.init(id);
|
||||||
|
ShowRemoteCursorState.init(id);
|
||||||
|
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||||
|
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeStates(String id) {
|
void _removeStates(String id) {
|
||||||
PrivacyModeState.delete(id);
|
PrivacyModeState.delete(id);
|
||||||
BlockInputState.delete(id);
|
BlockInputState.delete(id);
|
||||||
CurrentDisplayState.delete(id);
|
CurrentDisplayState.delete(id);
|
||||||
|
ShowRemoteCursorState.delete(id);
|
||||||
|
KeyboardEnabledState.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_initStates(widget.id);
|
||||||
_ffi = FFI();
|
_ffi = FFI();
|
||||||
_updateTabBarHeight();
|
_updateTabBarHeight();
|
||||||
Get.put(_ffi, tag: widget.id);
|
Get.put(_ffi, tag: widget.id);
|
||||||
@ -87,7 +94,8 @@ 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);
|
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
|
id: widget.id, arg: 'show-remote-cursor');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -132,7 +140,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
common < oldValue.length &&
|
common < oldValue.length &&
|
||||||
common < newValue.length &&
|
common < newValue.length &&
|
||||||
newValue[common] == oldValue[common];
|
newValue[common] == oldValue[common];
|
||||||
++common);
|
++common) {}
|
||||||
for (i = 0; i < oldValue.length - common; ++i) {
|
for (i = 0; i < oldValue.length - common; ++i) {
|
||||||
_ffi.inputKey('VK_BACK');
|
_ffi.inputKey('VK_BACK');
|
||||||
}
|
}
|
||||||
@ -146,8 +154,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (oldValue.length > 0 &&
|
if (oldValue.isNotEmpty &&
|
||||||
newValue.length > 0 &&
|
newValue.isNotEmpty &&
|
||||||
oldValue[0] == '\1' &&
|
oldValue[0] == '\1' &&
|
||||||
newValue[0] != '\1') {
|
newValue[0] != '\1') {
|
||||||
// clipboard
|
// clipboard
|
||||||
@ -156,7 +164,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
if (newValue.length == oldValue.length) {
|
if (newValue.length == oldValue.length) {
|
||||||
// ?
|
// ?
|
||||||
} else if (newValue.length < oldValue.length) {
|
} else if (newValue.length < oldValue.length) {
|
||||||
final char = 'VK_BACK';
|
const char = 'VK_BACK';
|
||||||
_ffi.inputKey(char);
|
_ffi.inputKey(char);
|
||||||
} else {
|
} else {
|
||||||
final content = newValue.substring(oldValue.length);
|
final content = newValue.substring(oldValue.length);
|
||||||
@ -200,25 +208,9 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.inputKey(label, down: down, press: press ?? false);
|
_ffi.inputKey(label, down: down, press: press ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildBody(BuildContext context, FfiModel ffiModel) {
|
Widget buildBody(BuildContext context) {
|
||||||
final hasDisplays = ffiModel.pi.displays.length > 0;
|
|
||||||
final keyboard = ffiModel.permissions['keyboard'] != false;
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: MyTheme.color(context).bg,
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
// resizeToAvoidBottomInset: true,
|
|
||||||
// floatingActionButton: _showBar
|
|
||||||
// ? null
|
|
||||||
// : FloatingActionButton(
|
|
||||||
// mini: true,
|
|
||||||
// child: Icon(Icons.expand_less),
|
|
||||||
// backgroundColor: MyTheme.accent,
|
|
||||||
// onPressed: () {
|
|
||||||
// setState(() {
|
|
||||||
// _showBar = !_showBar;
|
|
||||||
// });
|
|
||||||
// }),
|
|
||||||
// bottomNavigationBar:
|
|
||||||
// _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
|
|
||||||
body: Overlay(
|
body: Overlay(
|
||||||
initialEntries: [
|
initialEntries: [
|
||||||
OverlayEntry(builder: (context) {
|
OverlayEntry(builder: (context) {
|
||||||
@ -226,8 +218,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: getRawPointerAndKeyBody(
|
child: getRawPointerAndKeyBody(getBodyForDesktop(context)));
|
||||||
getBodyForDesktop(context, keyboard)));
|
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
@ -242,16 +233,12 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
clientClose(_ffi.dialogManager);
|
clientClose(_ffi.dialogManager);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: MultiProvider(
|
child: MultiProvider(providers: [
|
||||||
providers: [
|
|
||||||
ChangeNotifierProvider.value(value: _ffi.ffiModel),
|
ChangeNotifierProvider.value(value: _ffi.ffiModel),
|
||||||
ChangeNotifierProvider.value(value: _ffi.imageModel),
|
ChangeNotifierProvider.value(value: _ffi.imageModel),
|
||||||
ChangeNotifierProvider.value(value: _ffi.cursorModel),
|
ChangeNotifierProvider.value(value: _ffi.cursorModel),
|
||||||
ChangeNotifierProvider.value(value: _ffi.canvasModel),
|
ChangeNotifierProvider.value(value: _ffi.canvasModel),
|
||||||
],
|
], child: buildBody(context)));
|
||||||
child: Consumer<FfiModel>(
|
|
||||||
builder: (context, ffiModel, _child) =>
|
|
||||||
buildBody(context, ffiModel))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
|
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
|
||||||
@ -332,7 +319,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
key == LogicalKeyboardKey.shiftLeft) {
|
key == LogicalKeyboardKey.shiftLeft) {
|
||||||
_ffi.shift = false;
|
_ffi.shift = false;
|
||||||
} else if (key == LogicalKeyboardKey.metaLeft ||
|
} else if (key == LogicalKeyboardKey.metaLeft ||
|
||||||
key == LogicalKeyboardKey.metaRight || key == LogicalKeyboardKey.superKey) {
|
key == LogicalKeyboardKey.metaRight ||
|
||||||
|
key == LogicalKeyboardKey.superKey) {
|
||||||
_ffi.command = false;
|
_ffi.command = false;
|
||||||
}
|
}
|
||||||
sendRawKey(e);
|
sendRawKey(e);
|
||||||
@ -340,12 +328,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget getRawPointerAndKeyBody(Widget child) {
|
Widget getRawPointerAndKeyBody(Widget child) {
|
||||||
return Consumer<FfiModel>(
|
return FocusScope(
|
||||||
builder: (context, FfiModel, _child) => MouseRegion(
|
|
||||||
cursor: FfiModel.permissions['keyboard'] != false
|
|
||||||
? SystemMouseCursors.none
|
|
||||||
: MouseCursor.defer,
|
|
||||||
child: FocusScope(
|
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: Focus(
|
child: Focus(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@ -355,101 +338,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_imageFocused = v;
|
_imageFocused = v;
|
||||||
},
|
},
|
||||||
onKey: handleRawKeyEvent,
|
onKey: handleRawKeyEvent,
|
||||||
child: child))));
|
child: child));
|
||||||
}
|
|
||||||
|
|
||||||
Widget? getBottomAppBar(FfiModel ffiModel) {
|
|
||||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
|
||||||
return MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.basic,
|
|
||||||
child: BottomAppBar(
|
|
||||||
elevation: 10,
|
|
||||||
color: MyTheme.accent,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: Icon(Icons.clear),
|
|
||||||
onPressed: () {
|
|
||||||
clientClose(_ffi.dialogManager);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
] +
|
|
||||||
<Widget>[
|
|
||||||
IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: Icon(Icons.tv),
|
|
||||||
onPressed: () {
|
|
||||||
_ffi.dialogManager.dismissAll();
|
|
||||||
showOptions(widget.id);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
] +
|
|
||||||
(isWebDesktop
|
|
||||||
? []
|
|
||||||
: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: Icon(fullscreen.isTrue
|
|
||||||
? Icons.fullscreen
|
|
||||||
: Icons.close_fullscreen),
|
|
||||||
onPressed: () {
|
|
||||||
fullscreen.value = !fullscreen.value;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]) +
|
|
||||||
(isWebDesktop
|
|
||||||
? []
|
|
||||||
: _ffi.ffiModel.isPeerAndroid
|
|
||||||
? [
|
|
||||||
IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: Icon(Icons.build),
|
|
||||||
onPressed: () {
|
|
||||||
if (mobileActionsOverlayEntry == null) {
|
|
||||||
showMobileActionsOverlay();
|
|
||||||
} else {
|
|
||||||
hideMobileActionsOverlay();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]
|
|
||||||
: []) +
|
|
||||||
(isWeb
|
|
||||||
? []
|
|
||||||
: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: Icon(Icons.message),
|
|
||||||
onPressed: () {
|
|
||||||
_ffi.chatModel
|
|
||||||
.changeCurrentID(ChatModel.clientModeID);
|
|
||||||
_ffi.chatModel.toggleChatOverlay();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]) +
|
|
||||||
[
|
|
||||||
IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: Icon(Icons.more_vert),
|
|
||||||
onPressed: () {
|
|
||||||
showActions(widget.id, ffiModel);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
IconButton(
|
|
||||||
color: Colors.white,
|
|
||||||
icon: Icon(Icons.expand_more),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() => _showBar = !_showBar);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// touchMode only:
|
/// touchMode only:
|
||||||
@ -461,7 +350,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
/// mouseMode only:
|
/// mouseMode only:
|
||||||
/// DoubleFiner -> right click
|
/// DoubleFiner -> right click
|
||||||
/// HoldDrag -> left drag
|
/// HoldDrag -> left drag
|
||||||
|
|
||||||
void _onPointHoverImage(PointerHoverEvent e) {
|
void _onPointHoverImage(PointerHoverEvent e) {
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
if (!_isPhysicalMouse) {
|
if (!_isPhysicalMouse) {
|
||||||
@ -509,12 +397,16 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
if (e is PointerScrollEvent) {
|
if (e is PointerScrollEvent) {
|
||||||
var dx = e.scrollDelta.dx.toInt();
|
var dx = e.scrollDelta.dx.toInt();
|
||||||
var dy = e.scrollDelta.dy.toInt();
|
var dy = e.scrollDelta.dy.toInt();
|
||||||
if (dx > 0)
|
if (dx > 0) {
|
||||||
dx = -1;
|
dx = -1;
|
||||||
else if (dx < 0) dx = 1;
|
} else if (dx < 0) {
|
||||||
if (dy > 0)
|
dx = 1;
|
||||||
|
}
|
||||||
|
if (dy > 0) {
|
||||||
dy = -1;
|
dy = -1;
|
||||||
else if (dy < 0) dy = 1;
|
} else if (dy < 0) {
|
||||||
|
dy = 1;
|
||||||
|
}
|
||||||
bind.sessionSendMouse(
|
bind.sessionSendMouse(
|
||||||
id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}');
|
id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}');
|
||||||
}
|
}
|
||||||
@ -544,32 +436,30 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
MouseRegion(onEnter: enterView, onExit: leaveView, child: child));
|
MouseRegion(onEnter: enterView, onExit: leaveView, child: child));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget getBodyForDesktop(BuildContext context, bool keyboard) {
|
Widget getBodyForDesktop(BuildContext context) {
|
||||||
var paints = <Widget>[
|
var paints = <Widget>[
|
||||||
MouseRegion(onEnter: (evt) {
|
MouseRegion(onEnter: (evt) {
|
||||||
bind.hostStopSystemKeyPropagate(stopped: false);
|
bind.hostStopSystemKeyPropagate(stopped: false);
|
||||||
}, onExit: (evt) {
|
}, onExit: (evt) {
|
||||||
bind.hostStopSystemKeyPropagate(stopped: true);
|
bind.hostStopSystemKeyPropagate(stopped: true);
|
||||||
}, child: Container(
|
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||||
});
|
});
|
||||||
return ImagePaint(
|
return ImagePaint(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
cursorOverImage: _cursorOverImage,
|
cursorOverImage: _cursorOverImage,
|
||||||
|
keyboardEnabled: _keyboardEnabled,
|
||||||
listenerBuilder: _buildImageListener,
|
listenerBuilder: _buildImageListener,
|
||||||
);
|
);
|
||||||
}),
|
}))
|
||||||
))
|
|
||||||
];
|
];
|
||||||
final cursor = bind.sessionGetToggleOptionSync(
|
|
||||||
id: widget.id, arg: 'show-remote-cursor');
|
paints.add(Obx(() => Visibility(
|
||||||
if (keyboard || cursor) {
|
visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue,
|
||||||
paints.add(CursorPaint(
|
child: CursorPaint(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
));
|
))));
|
||||||
}
|
|
||||||
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
|
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
|
||||||
paints.add(RemoteMenubar(
|
paints.add(RemoteMenubar(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
@ -601,106 +491,6 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
void showActions(String id, FfiModel ffiModel) async {
|
|
||||||
final size = MediaQuery.of(context).size;
|
|
||||||
final x = 120.0;
|
|
||||||
final y = size.height - super.widget.tabBarHeight;
|
|
||||||
final more = <PopupMenuItem<String>>[];
|
|
||||||
final pi = _ffi.ffiModel.pi;
|
|
||||||
final perms = _ffi.ffiModel.permissions;
|
|
||||||
if (pi.version.isNotEmpty) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Refresh')), value: 'refresh'));
|
|
||||||
}
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Row(
|
|
||||||
children: ([
|
|
||||||
Text(translate('OS Password')),
|
|
||||||
TextButton(
|
|
||||||
style: flatButtonStyle,
|
|
||||||
onPressed: () {
|
|
||||||
showSetOSPassword(widget.id, false, _ffi.dialogManager);
|
|
||||||
},
|
|
||||||
child: Icon(Icons.edit, color: MyTheme.accent),
|
|
||||||
)
|
|
||||||
])),
|
|
||||||
value: 'enter_os_password'));
|
|
||||||
if (!isWebDesktop) {
|
|
||||||
if (perms['keyboard'] != false && perms['clipboard'] != false) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Paste')), value: 'paste'));
|
|
||||||
}
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
|
|
||||||
}
|
|
||||||
if (perms['keyboard'] != false) {
|
|
||||||
if (pi.platform == 'Linux' || pi.sasEnabled) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Insert') + ' Ctrl + Alt + Del'),
|
|
||||||
value: 'cad'));
|
|
||||||
}
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Insert Lock')), value: 'lock'));
|
|
||||||
if (pi.platform == 'Windows' &&
|
|
||||||
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
|
|
||||||
true) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate(
|
|
||||||
(ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')),
|
|
||||||
value: 'block-input'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (gFFI.ffiModel.permissions["restart"] != false &&
|
|
||||||
(pi.platform == "Linux" ||
|
|
||||||
pi.platform == "Windows" ||
|
|
||||||
pi.platform == "Mac OS")) {
|
|
||||||
more.add(PopupMenuItem<String>(
|
|
||||||
child: Text(translate('Restart Remote Device')), value: 'restart'));
|
|
||||||
}
|
|
||||||
() async {
|
|
||||||
var value = await showMenu(
|
|
||||||
context: context,
|
|
||||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
|
||||||
items: more,
|
|
||||||
elevation: 8,
|
|
||||||
);
|
|
||||||
if (value == 'cad') {
|
|
||||||
bind.sessionCtrlAltDel(id: widget.id);
|
|
||||||
} else if (value == 'lock') {
|
|
||||||
bind.sessionLockScreen(id: widget.id);
|
|
||||||
} else if (value == 'block-input') {
|
|
||||||
bind.sessionToggleOption(
|
|
||||||
id: widget.id,
|
|
||||||
value: (_ffi.ffiModel.inputBlocked ? 'un' : '') + 'block-input');
|
|
||||||
_ffi.ffiModel.inputBlocked = !_ffi.ffiModel.inputBlocked;
|
|
||||||
} else if (value == 'refresh') {
|
|
||||||
bind.sessionRefresh(id: widget.id);
|
|
||||||
} else if (value == 'paste') {
|
|
||||||
() async {
|
|
||||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
|
||||||
if (data != null && data.text != null) {
|
|
||||||
bind.sessionInputString(id: widget.id, value: data.text ?? "");
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
} else if (value == 'enter_os_password') {
|
|
||||||
// FIXME:
|
|
||||||
// TODO icon diff
|
|
||||||
// null means no session of id
|
|
||||||
// empty string means no password
|
|
||||||
var password = await bind.sessionGetOption(id: id, arg: "os-password");
|
|
||||||
if (password != null) {
|
|
||||||
bind.sessionInputOsPassword(id: widget.id, value: password);
|
|
||||||
} else {
|
|
||||||
showSetOSPassword(widget.id, true, _ffi.dialogManager);
|
|
||||||
}
|
|
||||||
} else if (value == 'reset_canvas') {
|
|
||||||
_ffi.cursorModel.reset();
|
|
||||||
} else if (value == 'restart') {
|
|
||||||
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowEvent(String eventName) {
|
void onWindowEvent(String eventName) {
|
||||||
print("window event: $eventName");
|
print("window event: $eventName");
|
||||||
@ -709,7 +499,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.canvasModel.updateViewStyle();
|
_ffi.canvasModel.updateViewStyle();
|
||||||
break;
|
break;
|
||||||
case 'maximize':
|
case 'maximize':
|
||||||
Future.delayed(Duration(milliseconds: 100), () {
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
_ffi.canvasModel.updateViewStyle();
|
_ffi.canvasModel.updateViewStyle();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -723,6 +513,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
class ImagePaint extends StatelessWidget {
|
class ImagePaint extends StatelessWidget {
|
||||||
final String id;
|
final String id;
|
||||||
final Rx<bool> cursorOverImage;
|
final Rx<bool> cursorOverImage;
|
||||||
|
final Rx<bool> keyboardEnabled;
|
||||||
final Widget Function(Widget)? listenerBuilder;
|
final Widget Function(Widget)? listenerBuilder;
|
||||||
final ScrollController _horizontal = ScrollController();
|
final ScrollController _horizontal = ScrollController();
|
||||||
final ScrollController _vertical = ScrollController();
|
final ScrollController _vertical = ScrollController();
|
||||||
@ -731,7 +522,8 @@ class ImagePaint extends StatelessWidget {
|
|||||||
{Key? key,
|
{Key? key,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.cursorOverImage,
|
required this.cursorOverImage,
|
||||||
this.listenerBuilder = null})
|
required this.keyboardEnabled,
|
||||||
|
this.listenerBuilder})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -761,11 +553,12 @@ class ImagePaint extends StatelessWidget {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: Obx(() => MouseRegion(
|
child: Obx(() => MouseRegion(
|
||||||
cursor: cursorOverImage.value
|
cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue)
|
||||||
? SystemMouseCursors.none
|
? SystemMouseCursors.none
|
||||||
: SystemMouseCursors.basic,
|
: MouseCursor.defer,
|
||||||
child: _buildCrossScrollbar(_buildListener(imageWidget)))),
|
child: _buildCrossScrollbar(_buildListener(imageWidget)))),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
final imageWidget = SizedBox(
|
final imageWidget = SizedBox(
|
||||||
width: c.size.width,
|
width: c.size.width,
|
||||||
@ -824,13 +617,12 @@ class CursorPaint extends StatelessWidget {
|
|||||||
final m = Provider.of<CursorModel>(context);
|
final m = Provider.of<CursorModel>(context);
|
||||||
final c = Provider.of<CanvasModel>(context);
|
final c = Provider.of<CanvasModel>(context);
|
||||||
// final adjust = m.adjustForKeyboard();
|
// final adjust = m.adjustForKeyboard();
|
||||||
var s = c.scale;
|
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
painter: ImagePainter(
|
painter: ImagePainter(
|
||||||
image: m.image,
|
image: m.image,
|
||||||
x: m.x * s - m.hotx + c.x,
|
x: m.x - m.hotx + c.x / c.scale,
|
||||||
y: m.y * s - m.hoty + c.y,
|
y: m.y - m.hoty + c.y / c.scale,
|
||||||
scale: 1),
|
scale: c.scale),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -882,205 +674,35 @@ class QualityMonitor extends StatelessWidget {
|
|||||||
right: 10,
|
right: 10,
|
||||||
child: qualityMonitorModel.show
|
child: qualityMonitorModel.show
|
||||||
? Container(
|
? Container(
|
||||||
padding: EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
color: MyTheme.canvasColor.withAlpha(120),
|
color: MyTheme.canvasColor.withAlpha(120),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Speed: ${qualityMonitorModel.data.speed ?? ''}",
|
"Speed: ${qualityMonitorModel.data.speed ?? ''}",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: const TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"FPS: ${qualityMonitorModel.data.fps ?? ''}",
|
"FPS: ${qualityMonitorModel.data.fps ?? ''}",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: const TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"Delay: ${qualityMonitorModel.data.delay ?? ''} ms",
|
"Delay: ${qualityMonitorModel.data.delay ?? ''} ms",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: const TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb",
|
"Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: const TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"Codec: ${qualityMonitorModel.data.codecFormat ?? ''}",
|
"Codec: ${qualityMonitorModel.data.codecFormat ?? ''}",
|
||||||
style: TextStyle(color: MyTheme.grayBg),
|
style: const TextStyle(color: MyTheme.grayBg),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: SizedBox.shrink())));
|
: const SizedBox.shrink())));
|
||||||
}
|
|
||||||
|
|
||||||
void showOptions(String id) async {
|
|
||||||
final _ffi = ffi(id);
|
|
||||||
String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced';
|
|
||||||
if (quality == '') quality = 'balanced';
|
|
||||||
String viewStyle =
|
|
||||||
await bind.sessionGetOption(id: id, arg: 'view-style') ?? '';
|
|
||||||
String scrollStyle =
|
|
||||||
await bind.sessionGetOption(id: id, arg: 'scroll-style') ?? '';
|
|
||||||
var displays = <Widget>[];
|
|
||||||
final pi = _ffi.ffiModel.pi;
|
|
||||||
final image = _ffi.ffiModel.getConnectionImage();
|
|
||||||
if (image != null)
|
|
||||||
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
|
||||||
if (pi.displays.length > 1) {
|
|
||||||
final cur = pi.currentDisplay;
|
|
||||||
final children = <Widget>[];
|
|
||||||
for (var i = 0; i < pi.displays.length; ++i)
|
|
||||||
children.add(InkWell(
|
|
||||||
onTap: () {
|
|
||||||
if (i == cur) return;
|
|
||||||
bind.sessionSwitchDisplay(id: id, value: i);
|
|
||||||
_ffi.dialogManager.dismissAll();
|
|
||||||
},
|
|
||||||
child: Ink(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.black87),
|
|
||||||
color: i == cur ? Colors.black87 : Colors.white),
|
|
||||||
child: Center(
|
|
||||||
child: Text((i + 1).toString(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: i == cur ? Colors.white : Colors.black87))))));
|
|
||||||
displays.add(Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8),
|
|
||||||
child: Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 8,
|
|
||||||
children: children,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
if (displays.isNotEmpty) {
|
|
||||||
displays.add(Divider(color: MyTheme.border));
|
|
||||||
}
|
|
||||||
final perms = _ffi.ffiModel.permissions;
|
|
||||||
|
|
||||||
_ffi.dialogManager.show((setState, close) {
|
|
||||||
final more = <Widget>[];
|
|
||||||
if (perms['audio'] != false) {
|
|
||||||
more.add(getToggle(id, setState, 'disable-audio', 'Mute'));
|
|
||||||
}
|
|
||||||
if (perms['keyboard'] != false) {
|
|
||||||
if (perms['clipboard'] != false)
|
|
||||||
more.add(
|
|
||||||
getToggle(id, setState, 'disable-clipboard', 'Disable clipboard'));
|
|
||||||
more.add(getToggle(
|
|
||||||
id, setState, 'lock-after-session-end', 'Lock after session end'));
|
|
||||||
if (pi.platform == 'Windows') {
|
|
||||||
more.add(Consumer<FfiModel>(
|
|
||||||
builder: (_context, _ffiModel, _child) => () {
|
|
||||||
return getToggle(
|
|
||||||
id, setState, 'privacy-mode', 'Privacy mode');
|
|
||||||
}()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var setQuality = (String? value) {
|
|
||||||
if (value == null) return;
|
|
||||||
setState(() {
|
|
||||||
quality = value;
|
|
||||||
bind.sessionSetImageQuality(id: id, value: value);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var setViewStyle = (String? value) {
|
|
||||||
if (value == null) return;
|
|
||||||
setState(() {
|
|
||||||
viewStyle = value;
|
|
||||||
bind.sessionPeerOption(id: id, name: "view-style", value: value);
|
|
||||||
_ffi.canvasModel.updateViewStyle();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var setScrollStyle = (String? value) {
|
|
||||||
if (value == null) return;
|
|
||||||
setState(() {
|
|
||||||
scrollStyle = value;
|
|
||||||
bind.sessionPeerOption(id: id, name: "scroll-style", value: value);
|
|
||||||
_ffi.canvasModel.updateScrollStyle();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return CustomAlertDialog(
|
|
||||||
title: SizedBox.shrink(),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: displays +
|
|
||||||
<Widget>[
|
|
||||||
getRadio('Original', 'original', viewStyle, setViewStyle),
|
|
||||||
getRadio('Shrink', 'shrink', viewStyle, setViewStyle),
|
|
||||||
getRadio('Stretch', 'stretch', viewStyle, setViewStyle),
|
|
||||||
Divider(color: MyTheme.border),
|
|
||||||
getRadio(
|
|
||||||
'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle),
|
|
||||||
getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle),
|
|
||||||
Divider(color: MyTheme.border),
|
|
||||||
getRadio('Good image quality', 'best', quality, setQuality),
|
|
||||||
getRadio('Balanced', 'balanced', quality, setQuality),
|
|
||||||
getRadio('Optimize reaction time', 'low', quality, setQuality),
|
|
||||||
Divider(color: MyTheme.border),
|
|
||||||
getToggle(
|
|
||||||
id, setState, 'show-remote-cursor', 'Show remote cursor'),
|
|
||||||
getToggle(id, setState, 'show-quality-monitor',
|
|
||||||
'Show quality monitor',
|
|
||||||
ffi: _ffi),
|
|
||||||
] +
|
|
||||||
more),
|
|
||||||
actions: [],
|
|
||||||
contentPadding: 0,
|
|
||||||
);
|
|
||||||
}, clickMaskDismiss: true, backDismiss: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
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')),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendPrompt(String id, bool isMac, String key) {
|
void sendPrompt(String id, bool isMac, String key) {
|
||||||
|
|||||||
@ -19,10 +19,12 @@ class DesktopServerPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _DesktopServerPageState extends State<DesktopServerPage>
|
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||||
with WindowListener, AutomaticKeepAliveClientMixin {
|
with WindowListener, AutomaticKeepAliveClientMixin {
|
||||||
|
final tabController = gFFI.serverModel.tabController;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
gFFI.ffiModel.updateEventListener("");
|
gFFI.ffiModel.updateEventListener("");
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
|
tabController.onRemove = (_, id) => onRemoveId(id);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +41,13 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
super.onWindowClose();
|
super.onWindowClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onRemoveId(String id) {
|
||||||
|
if (tabController.state.value.tabs.isEmpty) {
|
||||||
|
windowManager.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
@ -49,11 +58,13 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
child: Consumer<ServerModel>(
|
child: Consumer<ServerModel>(
|
||||||
builder: (context, serverModel, child) => Container(
|
builder: (context, serverModel, child) => Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border:
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
Border.all(color: MyTheme.color(context).border!)),
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: MyTheme.color(context).bg,
|
backgroundColor: MyTheme.color(context).bg,
|
||||||
body: Center(
|
body: Overlay(initialEntries: [
|
||||||
|
OverlayEntry(builder: (context) {
|
||||||
|
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
||||||
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -61,9 +72,10 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
SizedBox.fromSize(size: Size(0, 15.0)),
|
SizedBox.fromSize(size: Size(0, 15.0)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
})
|
||||||
)));
|
]),
|
||||||
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -106,12 +118,11 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: DesktopTab(
|
: DesktopTab(
|
||||||
theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(),
|
|
||||||
showTitle: false,
|
showTitle: false,
|
||||||
showMaximize: false,
|
showMaximize: false,
|
||||||
showMinimize: false,
|
showMinimize: true,
|
||||||
|
showClose: true,
|
||||||
controller: serverModel.tabController,
|
controller: serverModel.tabController,
|
||||||
tabType: DesktopTabType.cm,
|
|
||||||
pageViewBuilder: (pageView) => Row(children: [
|
pageViewBuilder: (pageView) => Row(children: [
|
||||||
Expanded(child: pageView),
|
Expanded(child: pageView),
|
||||||
Consumer<ChatModel>(
|
Consumer<ChatModel>(
|
||||||
@ -450,8 +461,10 @@ 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: () =>
|
onTap: () => checkClickTime(client.id, () {
|
||||||
checkClickTime(client.id, () => handleAccept(context)),
|
handleAccept(context);
|
||||||
|
windowManager.minimize();
|
||||||
|
}),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
|
import '../../common/formatter/id_formatter.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/peer_model.dart';
|
import '../../models/peer_model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
@ -15,7 +16,7 @@ class _PopupMenuTheme {
|
|||||||
static const Color commonColor = MyTheme.accent;
|
static const Color commonColor = MyTheme.accent;
|
||||||
// kMinInteractiveDimension
|
// kMinInteractiveDimension
|
||||||
static const double height = 25.0;
|
static const double height = 25.0;
|
||||||
static const double dividerHeight = 12.0;
|
static const double dividerHeight = 3.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
|
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
|
||||||
@ -46,7 +47,8 @@ class _PeerCard extends StatefulWidget {
|
|||||||
/// State for the connection page.
|
/// State for the connection page.
|
||||||
class _PeerCardState extends State<_PeerCard>
|
class _PeerCardState extends State<_PeerCard>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
final double _cardRadis = 20;
|
var _menuPos = RelativeRect.fill;
|
||||||
|
final double _cardRadis = 16;
|
||||||
final double _borderWidth = 2;
|
final double _borderWidth = 2;
|
||||||
final RxBool _iconMoreHover = false.obs;
|
final RxBool _iconMoreHover = false.obs;
|
||||||
|
|
||||||
@ -118,7 +120,7 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
? Colors.green
|
? Colors.green
|
||||||
: Colors.yellow)),
|
: Colors.yellow)),
|
||||||
Text(
|
Text(
|
||||||
'${peer.id}',
|
formatID('${peer.id}'),
|
||||||
style: TextStyle(fontWeight: FontWeight.w400),
|
style: TextStyle(fontWeight: FontWeight.w400),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@ -239,7 +241,7 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
backgroundColor: peer.online
|
backgroundColor: peer.online
|
||||||
? Colors.green
|
? Colors.green
|
||||||
: Colors.yellow)),
|
: Colors.yellow)),
|
||||||
Text(peer.id)
|
Text(formatID(peer.id))
|
||||||
]).paddingSymmetric(vertical: 8),
|
]).paddingSymmetric(vertical: 8),
|
||||||
_actionMore(peer),
|
_actionMore(peer),
|
||||||
],
|
],
|
||||||
@ -253,14 +255,13 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _actionMore(Peer peer) {
|
Widget _actionMore(Peer peer) => Listener(
|
||||||
return FutureBuilder(
|
onPointerDown: (e) {
|
||||||
future: widget.popupMenuEntryBuilder(context),
|
final x = e.position.dx;
|
||||||
initialData: const <mod_menu.PopupMenuEntry<String>>[],
|
final y = e.position.dy;
|
||||||
builder: (BuildContext context,
|
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||||
AsyncSnapshot<List<mod_menu.PopupMenuEntry<String>>> snapshot) {
|
},
|
||||||
if (snapshot.hasData) {
|
onPointerUp: (_) => _showPeerMenu(context, peer.id),
|
||||||
return Listener(
|
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
onEnter: (_) => _iconMoreHover.value = true,
|
onEnter: (_) => _iconMoreHover.value = true,
|
||||||
onExit: (_) => _iconMoreHover.value = false,
|
onExit: (_) => _iconMoreHover.value = false,
|
||||||
@ -269,20 +270,21 @@ class _PeerCardState extends State<_PeerCard>
|
|||||||
backgroundColor: _iconMoreHover.value
|
backgroundColor: _iconMoreHover.value
|
||||||
? MyTheme.color(context).grayBg!
|
? MyTheme.color(context).grayBg!
|
||||||
: MyTheme.color(context).bg!,
|
: MyTheme.color(context).bg!,
|
||||||
child: mod_menu.PopupMenuButton(
|
child: Icon(Icons.more_vert,
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
icon: Icon(Icons.more_vert,
|
|
||||||
size: 18,
|
size: 18,
|
||||||
color: _iconMoreHover.value
|
color: _iconMoreHover.value
|
||||||
? MyTheme.color(context).text
|
? MyTheme.color(context).text
|
||||||
: MyTheme.color(context).lightText),
|
: MyTheme.color(context).lightText))));
|
||||||
position: mod_menu.PopupMenuPosition.under,
|
|
||||||
itemBuilder: (BuildContext context) => snapshot.data!,
|
/// Show the peer menu and handle user's choice.
|
||||||
))));
|
/// User might remove the peer or send a file to the peer.
|
||||||
} else {
|
void _showPeerMenu(BuildContext context, String id) async {
|
||||||
return Container();
|
await mod_menu.showMenu(
|
||||||
}
|
context: context,
|
||||||
});
|
position: _menuPos,
|
||||||
|
items: await super.widget.popupMenuEntryBuilder(context),
|
||||||
|
elevation: 8,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the image for the current [platform].
|
/// Get the image for the current [platform].
|
||||||
@ -411,19 +413,26 @@ abstract class BasePeerCard extends StatelessWidget {
|
|||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
|
MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
|
||||||
return MenuEntryButton<String>(
|
return MenuEntryButton<String>(
|
||||||
childBuilder: (TextStyle? style) => Row(
|
childBuilder: (TextStyle? style) => Container(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
height: _PopupMenuTheme.height,
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
translate('RDP'),
|
translate('RDP'),
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
SizedBox(width: 20),
|
Expanded(
|
||||||
IconButton(
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
icon: Icon(Icons.edit),
|
icon: Icon(Icons.edit),
|
||||||
onPressed: () => _rdpDialog(id),
|
onPressed: () => _rdpDialog(id),
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
)),
|
||||||
proc: () {
|
proc: () {
|
||||||
_connect(context, id, isRDP: true);
|
_connect(context, id, isRDP: true);
|
||||||
},
|
},
|
||||||
@ -554,33 +563,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
return CustomAlertDialog(
|
submit() async {
|
||||||
title: Text(translate('Rename')),
|
|
||||||
content: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
|
||||||
child: Form(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: InputDecoration(border: OutlineInputBorder()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() => Offstage(
|
|
||||||
offstage: isInProgress.isFalse,
|
|
||||||
child: LinearProgressIndicator())),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
isInProgress.value = true;
|
isInProgress.value = true;
|
||||||
name = controller.text;
|
name = controller.text;
|
||||||
await bind.mainSetPeerOption(id: id, key: 'alias', value: name);
|
await bind.mainSetPeerOption(id: id, key: 'alias', value: name);
|
||||||
@ -588,13 +571,39 @@ abstract class BasePeerCard extends StatelessWidget {
|
|||||||
gFFI.abModel.setPeerOption(id, 'alias', name);
|
gFFI.abModel.setPeerOption(id, 'alias', name);
|
||||||
await gFFI.abModel.updateAb();
|
await gFFI.abModel.updateAb();
|
||||||
}
|
}
|
||||||
alias.value =
|
alias.value = await bind.mainGetPeerOption(id: peer.id, key: 'alias');
|
||||||
await bind.mainGetPeerOption(id: peer.id, key: 'alias');
|
|
||||||
close();
|
close();
|
||||||
isInProgress.value = false;
|
isInProgress.value = false;
|
||||||
},
|
}
|
||||||
child: Text(translate("OK"))),
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate('Rename')),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Form(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: FocusNode()..requestFocus(),
|
||||||
|
decoration:
|
||||||
|
const InputDecoration(border: OutlineInputBorder()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Obx(() => Offstage(
|
||||||
|
offstage: isInProgress.isFalse,
|
||||||
|
child: const LinearProgressIndicator())),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -614,6 +623,7 @@ class RecentPeerCard extends BasePeerCard {
|
|||||||
if (peer.platform == 'Windows') {
|
if (peer.platform == 'Windows') {
|
||||||
menuItems.add(_rdpAction(context, peer.id));
|
menuItems.add(_rdpAction(context, peer.id));
|
||||||
}
|
}
|
||||||
|
menuItems.add(MenuEntryDivider());
|
||||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||||
menuItems.add(_renameAction(peer.id, false));
|
menuItems.add(_renameAction(peer.id, false));
|
||||||
menuItems.add(_removeAction(peer.id, () async {
|
menuItems.add(_removeAction(peer.id, () async {
|
||||||
@ -740,13 +750,23 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
|
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
|
submit() async {
|
||||||
|
setState(() {
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||||
|
await gFFI.abModel.updateAb();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("Edit Tag")),
|
title: Text(translate("Edit Tag")),
|
||||||
content: Column(
|
content: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: tags
|
children: tags
|
||||||
.map((e) => _buildTag(e, selectedTag, onTap: () {
|
.map((e) => _buildTag(e, selectedTag, onTap: () {
|
||||||
@ -759,26 +779,16 @@ class AddressBookPeerCard extends BasePeerCard {
|
|||||||
.toList(growable: false),
|
.toList(growable: false),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
Offstage(
|
||||||
|
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
onPressed: () {
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
|
||||||
isInProgress = true;
|
|
||||||
});
|
|
||||||
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
|
||||||
await gFFI.abModel.updateAb();
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("OK"))),
|
|
||||||
],
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -861,25 +871,35 @@ void _rdpDialog(String id) async {
|
|||||||
RxBool secure = true.obs;
|
RxBool secure = true.obs;
|
||||||
|
|
||||||
gFFI.dialogManager.show((setState, close) {
|
gFFI.dialogManager.show((setState, close) {
|
||||||
|
submit() async {
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id, key: 'rdp_port', value: portController.text.trim());
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id, key: 'rdp_username', value: userController.text);
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id, key: 'rdp_password', value: passwordContorller.text);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text('RDP ' + translate('Settings')),
|
title: Text('RDP ${translate('Settings')}'),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: BoxConstraints(minWidth: 500),
|
constraints: const BoxConstraints(minWidth: 500),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 8.0,
|
height: 8.0,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
child: Text(
|
child: Text(
|
||||||
"${translate('Port')}:",
|
"${translate('Port')}:",
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
).marginOnly(bottom: 16.0)),
|
).marginOnly(bottom: 16.0)),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 24.0,
|
width: 24.0,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -888,52 +908,54 @@ void _rdpDialog(String id) async {
|
|||||||
FilteringTextInputFormatter.allow(RegExp(
|
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])$'))
|
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])$'))
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(), hintText: '3389'),
|
border: OutlineInputBorder(), hintText: '3389'),
|
||||||
controller: portController,
|
controller: portController,
|
||||||
|
focusNode: FocusNode()..requestFocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 8.0,
|
height: 8.0,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
child: Text(
|
child: Text(
|
||||||
"${translate('Username')}:",
|
"${translate('Username')}:",
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
).marginOnly(bottom: 16.0)),
|
).marginOnly(bottom: 16.0)),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 24.0,
|
width: 24.0,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
decoration: InputDecoration(border: OutlineInputBorder()),
|
decoration:
|
||||||
|
const InputDecoration(border: OutlineInputBorder()),
|
||||||
controller: userController,
|
controller: userController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
height: 8.0,
|
height: 8.0,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints(minWidth: 100),
|
constraints: const BoxConstraints(minWidth: 100),
|
||||||
child: Text("${translate('Password')}:")
|
child: Text("${translate('Password')}:")
|
||||||
.marginOnly(bottom: 16.0)),
|
.marginOnly(bottom: 16.0)),
|
||||||
SizedBox(
|
const SizedBox(
|
||||||
width: 24.0,
|
width: 24.0,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(() => TextField(
|
child: Obx(() => TextField(
|
||||||
obscureText: secure.value,
|
obscureText: secure.value,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
onPressed: () => secure.value = !secure.value,
|
onPressed: () => secure.value = !secure.value,
|
||||||
icon: Icon(secure.value
|
icon: Icon(secure.value
|
||||||
@ -948,23 +970,11 @@ void _rdpDialog(String id) async {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
onPressed: () {
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await bind.mainSetPeerOption(
|
|
||||||
id: id, key: 'rdp_port', value: portController.text.trim());
|
|
||||||
await bind.mainSetPeerOption(
|
|
||||||
id: id, key: 'rdp_username', value: userController.text);
|
|
||||||
await bind.mainSetPeerOption(
|
|
||||||
id: id, key: 'rdp_password', value: passwordContorller.text);
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate("OK"))),
|
|
||||||
],
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import './material_mod_popup_menu.dart' as mod_menu;
|
import './material_mod_popup_menu.dart' as mod_menu;
|
||||||
@ -174,8 +175,8 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
opt.text,
|
opt.text,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: MyTheme.color(context).text,
|
||||||
fontSize: MenuConfig.fontSize,
|
fontSize: MenuConfig.fontSize,
|
||||||
fontWeight: FontWeight.normal),
|
fontWeight: FontWeight.normal),
|
||||||
),
|
),
|
||||||
@ -256,8 +257,8 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
opt.text,
|
opt.text,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: MyTheme.color(context).text,
|
||||||
fontSize: MenuConfig.fontSize,
|
fontSize: MenuConfig.fontSize,
|
||||||
fontWeight: FontWeight.normal),
|
fontWeight: FontWeight.normal),
|
||||||
),
|
),
|
||||||
@ -300,8 +301,8 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
|||||||
const SizedBox(width: MenuConfig.midPadding),
|
const SizedBox(width: MenuConfig.midPadding),
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: MyTheme.color(context).text,
|
||||||
fontSize: MenuConfig.fontSize,
|
fontSize: MenuConfig.fontSize,
|
||||||
fontWeight: FontWeight.normal),
|
fontWeight: FontWeight.normal),
|
||||||
),
|
),
|
||||||
@ -346,8 +347,8 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
|||||||
// const SizedBox(width: MenuConfig.midPadding),
|
// const SizedBox(width: MenuConfig.midPadding),
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: MyTheme.color(context).text,
|
||||||
fontSize: MenuConfig.fontSize,
|
fontSize: MenuConfig.fontSize,
|
||||||
fontWeight: FontWeight.normal),
|
fontWeight: FontWeight.normal),
|
||||||
),
|
),
|
||||||
@ -450,8 +451,8 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
|||||||
const SizedBox(width: MenuConfig.midPadding),
|
const SizedBox(width: MenuConfig.midPadding),
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: MyTheme.color(context).text,
|
||||||
fontSize: MenuConfig.fontSize,
|
fontSize: MenuConfig.fontSize,
|
||||||
fontWeight: FontWeight.normal),
|
fontWeight: FontWeight.normal),
|
||||||
),
|
),
|
||||||
@ -491,8 +492,8 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
|||||||
alignment: AlignmentDirectional.centerStart,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
constraints: BoxConstraints(minHeight: conf.height),
|
constraints: BoxConstraints(minHeight: conf.height),
|
||||||
child: childBuilder(
|
child: childBuilder(
|
||||||
const TextStyle(
|
TextStyle(
|
||||||
color: Colors.black,
|
color: MyTheme.color(context).text,
|
||||||
fontSize: MenuConfig.fontSize,
|
fontSize: MenuConfig.fontSize,
|
||||||
fontWeight: FontWeight.normal),
|
fontWeight: FontWeight.normal),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@ -496,9 +496,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
});
|
});
|
||||||
final quality =
|
final quality =
|
||||||
await bind.sessionGetCustomImageQuality(id: widget.id);
|
await bind.sessionGetCustomImageQuality(id: widget.id);
|
||||||
final double initValue = quality != null && quality.isNotEmpty
|
double initValue = quality != null && quality.isNotEmpty
|
||||||
? quality[0].toDouble()
|
? quality[0].toDouble()
|
||||||
: 50.0;
|
: 50.0;
|
||||||
|
const minValue = 10.0;
|
||||||
|
const maxValue = 100.0;
|
||||||
|
if (initValue < minValue) {
|
||||||
|
initValue = minValue;
|
||||||
|
}
|
||||||
|
if (initValue > maxValue) {
|
||||||
|
initValue = maxValue;
|
||||||
|
}
|
||||||
final RxDouble sliderValue = RxDouble(initValue);
|
final RxDouble sliderValue = RxDouble(initValue);
|
||||||
final rxReplay = rxdart.ReplaySubject<double>();
|
final rxReplay = rxdart.ReplaySubject<double>();
|
||||||
rxReplay
|
rxReplay
|
||||||
@ -513,30 +521,44 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
final slider = Obx(() {
|
final slider = Obx(() {
|
||||||
return Slider(
|
return Slider(
|
||||||
value: sliderValue.value,
|
value: sliderValue.value,
|
||||||
max: 100,
|
min: minValue,
|
||||||
divisions: 100,
|
max: maxValue,
|
||||||
label: sliderValue.value.round().toString(),
|
divisions: 90,
|
||||||
onChanged: (double value) {
|
onChanged: (double value) {
|
||||||
sliderValue.value = value;
|
sliderValue.value = value;
|
||||||
rxReplay.add(value);
|
rxReplay.add(value);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
final content = Row(
|
||||||
|
children: [
|
||||||
|
slider,
|
||||||
|
SizedBox(
|
||||||
|
width: 90,
|
||||||
|
child: Obx(() => Text(
|
||||||
|
'${sliderValue.value.round()}% Bitrate',
|
||||||
|
style: const TextStyle(fontSize: 15),
|
||||||
|
)))
|
||||||
|
],
|
||||||
|
);
|
||||||
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
|
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
|
||||||
slider, [btnCancel]);
|
content, [btnCancel]);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
MenuEntryDivider<String>(),
|
MenuEntryDivider<String>(),
|
||||||
MenuEntrySwitch<String>(
|
() {
|
||||||
|
final state = ShowRemoteCursorState.find(widget.id);
|
||||||
|
return MenuEntrySwitch2<String>(
|
||||||
text: translate('Show remote cursor'),
|
text: translate('Show remote cursor'),
|
||||||
getter: () async {
|
getter: () {
|
||||||
return bind.sessionGetToggleOptionSync(
|
return state;
|
||||||
id: widget.id, arg: 'show-remote-cursor');
|
|
||||||
},
|
},
|
||||||
setter: (bool v) async {
|
setter: (bool v) async {
|
||||||
|
state.value = v;
|
||||||
await bind.sessionToggleOption(
|
await bind.sessionToggleOption(
|
||||||
id: widget.id, value: 'show-remote-cursor');
|
id: widget.id, value: 'show-remote-cursor');
|
||||||
}),
|
});
|
||||||
|
}(),
|
||||||
MenuEntrySwitch<String>(
|
MenuEntrySwitch<String>(
|
||||||
text: translate('Show quality monitor'),
|
text: translate('Show quality monitor'),
|
||||||
getter: () async {
|
getter: () async {
|
||||||
@ -565,12 +587,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
|||||||
'Lock after session end', 'lock-after-session-end'));
|
'Lock after session end', 'lock-after-session-end'));
|
||||||
if (pi.platform == 'Windows') {
|
if (pi.platform == 'Windows') {
|
||||||
displayMenu.add(MenuEntrySwitch2<String>(
|
displayMenu.add(MenuEntrySwitch2<String>(
|
||||||
|
dismissOnClicked: true,
|
||||||
text: translate('Privacy mode'),
|
text: translate('Privacy mode'),
|
||||||
getter: () {
|
getter: () {
|
||||||
return PrivacyModeState.find(widget.id);
|
return PrivacyModeState.find(widget.id);
|
||||||
},
|
},
|
||||||
setter: (bool v) async {
|
setter: (bool v) async {
|
||||||
Navigator.pop(context);
|
|
||||||
await bind.sessionToggleOption(
|
await bind.sessionToggleOption(
|
||||||
id: widget.id, value: 'privacy-mode');
|
id: widget.id, value: 'privacy-mode');
|
||||||
}));
|
}));
|
||||||
@ -620,6 +642,17 @@ void showSetOSPassword(
|
|||||||
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
||||||
controller.text = password;
|
controller.text = password;
|
||||||
dialogManager.show((setState, close) {
|
dialogManager.show((setState, close) {
|
||||||
|
submit() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate('OS Password')),
|
title: Text(translate('OS Password')),
|
||||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
@ -641,25 +674,17 @@ void showSetOSPassword(
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: close,
|
||||||
close();
|
|
||||||
},
|
|
||||||
child: Text(translate('Cancel')),
|
child: Text(translate('Cancel')),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () {
|
onPressed: submit,
|
||||||
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')),
|
child: Text(translate('OK')),
|
||||||
),
|
),
|
||||||
]);
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ 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';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide TabBarTheme;
|
||||||
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';
|
||||||
@ -59,13 +59,15 @@ class DesktopTabState {
|
|||||||
|
|
||||||
class DesktopTabController {
|
class DesktopTabController {
|
||||||
final state = DesktopTabState().obs;
|
final state = DesktopTabState().obs;
|
||||||
|
final DesktopTabType tabType;
|
||||||
|
|
||||||
/// index, key
|
/// index, key
|
||||||
Function(int, String)? onRemove;
|
Function(int, String)? onRemove;
|
||||||
|
|
||||||
Function(int)? onSelected;
|
Function(int)? onSelected;
|
||||||
|
|
||||||
void add(TabInfo tab) {
|
DesktopTabController({required this.tabType});
|
||||||
|
|
||||||
|
void add(TabInfo tab, {bool authorized = false}) {
|
||||||
if (!isDesktop) return;
|
if (!isDesktop) return;
|
||||||
final index = state.value.tabs.indexWhere((e) => e.key == tab.key);
|
final index = state.value.tabs.indexWhere((e) => e.key == tab.key);
|
||||||
int toIndex;
|
int toIndex;
|
||||||
@ -79,6 +81,16 @@ class DesktopTabController {
|
|||||||
toIndex = state.value.tabs.length - 1;
|
toIndex = state.value.tabs.length - 1;
|
||||||
assert(toIndex >= 0);
|
assert(toIndex >= 0);
|
||||||
}
|
}
|
||||||
|
if (tabType == DesktopTabType.cm) {
|
||||||
|
Future.delayed(Duration.zero, () async {
|
||||||
|
window_on_top(null);
|
||||||
|
});
|
||||||
|
if (authorized) {
|
||||||
|
Future.delayed(const Duration(seconds: 3), () {
|
||||||
|
windowManager.minimize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
jumpTo(toIndex);
|
jumpTo(toIndex);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -106,6 +118,7 @@ class DesktopTabController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void jumpTo(int index) {
|
void jumpTo(int index) {
|
||||||
|
if (!isDesktop || index < 0) return;
|
||||||
state.update((val) {
|
state.update((val) {
|
||||||
val!.selected = index;
|
val!.selected = index;
|
||||||
Future.delayed(Duration.zero, (() {
|
Future.delayed(Duration.zero, (() {
|
||||||
@ -114,13 +127,15 @@ class DesktopTabController {
|
|||||||
}
|
}
|
||||||
if (val.scrollController.hasClients &&
|
if (val.scrollController.hasClients &&
|
||||||
val.scrollController.canScroll &&
|
val.scrollController.canScroll &&
|
||||||
val.scrollController.itemCount >= index) {
|
val.scrollController.itemCount > index) {
|
||||||
val.scrollController.scrollToItem(index, center: true, animate: true);
|
val.scrollController.scrollToItem(index, center: true, animate: true);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
if (state.value.tabs.length > index) {
|
||||||
onSelected?.call(index);
|
onSelected?.call(index);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void closeBy(String? key) {
|
void closeBy(String? key) {
|
||||||
if (!isDesktop) return;
|
if (!isDesktop) return;
|
||||||
@ -143,8 +158,7 @@ class DesktopTabController {
|
|||||||
|
|
||||||
class TabThemeConf {
|
class TabThemeConf {
|
||||||
double iconSize;
|
double iconSize;
|
||||||
TarBarTheme theme;
|
TabThemeConf({required this.iconSize});
|
||||||
TabThemeConf({required this.iconSize, required this.theme});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef TabBuilder = Widget Function(
|
typedef TabBuilder = Widget Function(
|
||||||
@ -153,9 +167,6 @@ 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 DesktopTabType tabType;
|
|
||||||
final bool isMainWindow;
|
|
||||||
final bool showTabBar;
|
final bool showTabBar;
|
||||||
final bool showLogo;
|
final bool showLogo;
|
||||||
final bool showTitle;
|
final bool showTitle;
|
||||||
@ -170,11 +181,12 @@ class DesktopTab extends StatelessWidget {
|
|||||||
|
|
||||||
final DesktopTabController controller;
|
final DesktopTabController controller;
|
||||||
Rx<DesktopTabState> get state => controller.state;
|
Rx<DesktopTabState> get state => controller.state;
|
||||||
|
late final DesktopTabType tabType;
|
||||||
|
late final bool isMainWindow;
|
||||||
|
|
||||||
const DesktopTab({
|
DesktopTab({
|
||||||
|
Key? key,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.tabType,
|
|
||||||
this.theme = const TarBarTheme.light(),
|
|
||||||
this.onTabClose,
|
this.onTabClose,
|
||||||
this.showTabBar = true,
|
this.showTabBar = true,
|
||||||
this.showLogo = true,
|
this.showLogo = true,
|
||||||
@ -187,23 +199,26 @@ class DesktopTab extends StatelessWidget {
|
|||||||
this.onClose,
|
this.onClose,
|
||||||
this.tabBuilder,
|
this.tabBuilder,
|
||||||
this.labelGetter,
|
this.labelGetter,
|
||||||
}) : isMainWindow =
|
}) : super(key: key) {
|
||||||
|
tabType = controller.tabType;
|
||||||
|
isMainWindow =
|
||||||
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
|
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(children: [
|
return Column(children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !showTabBar,
|
offstage: !showTabBar,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: _kTabBarHeight,
|
height: _kTabBarHeight,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
SizedBox(
|
||||||
height: _kTabBarHeight - 1,
|
height: _kTabBarHeight - 1,
|
||||||
child: _buildBar(),
|
child: _buildBar(),
|
||||||
),
|
),
|
||||||
Divider(
|
const Divider(
|
||||||
height: 1,
|
height: 1,
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
),
|
),
|
||||||
@ -282,7 +297,7 @@ class DesktopTab extends StatelessWidget {
|
|||||||
)),
|
)),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !showTitle,
|
offstage: !showTitle,
|
||||||
child: Text(
|
child: const Text(
|
||||||
"RustDesk",
|
"RustDesk",
|
||||||
style: TextStyle(fontSize: 13),
|
style: TextStyle(fontSize: 13),
|
||||||
).marginOnly(left: 2))
|
).marginOnly(left: 2))
|
||||||
@ -303,7 +318,6 @@ class DesktopTab extends StatelessWidget {
|
|||||||
child: _ListView(
|
child: _ListView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onTabClose: onTabClose,
|
onTabClose: onTabClose,
|
||||||
theme: theme,
|
|
||||||
tabBuilder: tabBuilder,
|
tabBuilder: tabBuilder,
|
||||||
labelGetter: labelGetter,
|
labelGetter: labelGetter,
|
||||||
)),
|
)),
|
||||||
@ -314,7 +328,8 @@ class DesktopTab extends StatelessWidget {
|
|||||||
Offstage(offstage: tail == null, child: tail),
|
Offstage(offstage: tail == null, child: tail),
|
||||||
WindowActionPanel(
|
WindowActionPanel(
|
||||||
mainTab: isMainWindow,
|
mainTab: isMainWindow,
|
||||||
theme: theme,
|
tabType: tabType,
|
||||||
|
state: state,
|
||||||
showMinimize: showMinimize,
|
showMinimize: showMinimize,
|
||||||
showMaximize: showMaximize,
|
showMaximize: showMaximize,
|
||||||
showClose: showClose,
|
showClose: showClose,
|
||||||
@ -327,7 +342,8 @@ class DesktopTab extends StatelessWidget {
|
|||||||
|
|
||||||
class WindowActionPanel extends StatelessWidget {
|
class WindowActionPanel extends StatelessWidget {
|
||||||
final bool mainTab;
|
final bool mainTab;
|
||||||
final TarBarTheme theme;
|
final DesktopTabType tabType;
|
||||||
|
final Rx<DesktopTabState> state;
|
||||||
|
|
||||||
final bool showMinimize;
|
final bool showMinimize;
|
||||||
final bool showMaximize;
|
final bool showMaximize;
|
||||||
@ -337,7 +353,8 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
const WindowActionPanel(
|
const WindowActionPanel(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required this.mainTab,
|
required this.mainTab,
|
||||||
required this.theme,
|
required this.tabType,
|
||||||
|
required this.state,
|
||||||
this.showMinimize = true,
|
this.showMinimize = true,
|
||||||
this.showMaximize = true,
|
this.showMaximize = true,
|
||||||
this.showClose = true,
|
this.showClose = true,
|
||||||
@ -353,7 +370,6 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
child: ActionIcon(
|
child: ActionIcon(
|
||||||
message: 'Minimize',
|
message: 'Minimize',
|
||||||
icon: IconFont.min,
|
icon: IconFont.min,
|
||||||
theme: theme,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (mainTab) {
|
if (mainTab) {
|
||||||
windowManager.minimize();
|
windowManager.minimize();
|
||||||
@ -361,31 +377,30 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
WindowController.fromWindowId(windowId!).minimize();
|
WindowController.fromWindowId(windowId!).minimize();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
is_close: false,
|
isClose: false,
|
||||||
)),
|
)),
|
||||||
// TODO: drag makes window restore
|
// TODO: drag makes window restore
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !showMaximize,
|
offstage: !showMaximize,
|
||||||
child: FutureBuilder(builder: (context, snapshot) {
|
child: FutureBuilder(builder: (context, snapshot) {
|
||||||
RxBool is_maximized = false.obs;
|
RxBool isMaximized = false.obs;
|
||||||
if (mainTab) {
|
if (mainTab) {
|
||||||
windowManager.isMaximized().then((maximized) {
|
windowManager.isMaximized().then((maximized) {
|
||||||
is_maximized.value = maximized;
|
isMaximized.value = maximized;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
final wc = WindowController.fromWindowId(windowId!);
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
wc.isMaximized().then((maximized) {
|
wc.isMaximized().then((maximized) {
|
||||||
is_maximized.value = maximized;
|
isMaximized.value = maximized;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Obx(
|
return Obx(
|
||||||
() => ActionIcon(
|
() => ActionIcon(
|
||||||
message: is_maximized.value ? "Restore" : "Maximize",
|
message: isMaximized.value ? "Restore" : "Maximize",
|
||||||
icon: is_maximized.value ? IconFont.restore : IconFont.max,
|
icon: isMaximized.value ? IconFont.restore : IconFont.max,
|
||||||
theme: theme,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (mainTab) {
|
if (mainTab) {
|
||||||
if (is_maximized.value) {
|
if (isMaximized.value) {
|
||||||
windowManager.unmaximize();
|
windowManager.unmaximize();
|
||||||
} else {
|
} else {
|
||||||
windowManager.maximize();
|
windowManager.maximize();
|
||||||
@ -393,15 +408,15 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
} else {
|
} else {
|
||||||
// TODO: subwindow is maximized but first query result is not maximized.
|
// TODO: subwindow is maximized but first query result is not maximized.
|
||||||
final wc = WindowController.fromWindowId(windowId!);
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
if (is_maximized.value) {
|
if (isMaximized.value) {
|
||||||
wc.unmaximize();
|
wc.unmaximize();
|
||||||
} else {
|
} else {
|
||||||
wc.maximize();
|
wc.maximize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is_maximized.value = !is_maximized.value;
|
isMaximized.value = !isMaximized.value;
|
||||||
},
|
},
|
||||||
is_close: false,
|
isClose: false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})),
|
})),
|
||||||
@ -410,8 +425,8 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
child: ActionIcon(
|
child: ActionIcon(
|
||||||
message: 'Close',
|
message: 'Close',
|
||||||
icon: IconFont.close,
|
icon: IconFont.close,
|
||||||
theme: theme,
|
onTap: () async {
|
||||||
onTap: () {
|
action() {
|
||||||
if (mainTab) {
|
if (mainTab) {
|
||||||
windowManager.close();
|
windowManager.close();
|
||||||
} else {
|
} else {
|
||||||
@ -421,29 +436,59 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
onClose?.call();
|
onClose?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabType != DesktopTabType.main &&
|
||||||
|
state.value.tabs.length > 1) {
|
||||||
|
closeConfirmDialog(action);
|
||||||
|
} else {
|
||||||
|
action();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
is_close: true,
|
isClose: true,
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeConfirmDialog(Function() callback) async {
|
||||||
|
final res = await gFFI.dialogManager.show<bool>((setState, close) {
|
||||||
|
submit() => close(true);
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Row(children: [
|
||||||
|
const Icon(Icons.warning_amber_sharp,
|
||||||
|
color: Colors.redAccent, size: 28),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(translate("Warning")),
|
||||||
|
]),
|
||||||
|
content: Text(translate("Disconnect all devices?")),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
|
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (res == true) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class _ListView extends StatelessWidget {
|
class _ListView extends StatelessWidget {
|
||||||
final DesktopTabController controller;
|
final DesktopTabController controller;
|
||||||
final Function(String key)? onTabClose;
|
final Function(String key)? onTabClose;
|
||||||
final TarBarTheme theme;
|
|
||||||
|
|
||||||
final TabBuilder? tabBuilder;
|
final TabBuilder? tabBuilder;
|
||||||
final LabelGetter? labelGetter;
|
final LabelGetter? labelGetter;
|
||||||
|
|
||||||
Rx<DesktopTabState> get state => controller.state;
|
Rx<DesktopTabState> get state => controller.state;
|
||||||
|
|
||||||
_ListView(
|
const _ListView(
|
||||||
{required this.controller,
|
{required this.controller,
|
||||||
required this.onTabClose,
|
required this.onTabClose,
|
||||||
required this.theme,
|
|
||||||
this.tabBuilder,
|
this.tabBuilder,
|
||||||
this.labelGetter});
|
this.labelGetter});
|
||||||
|
|
||||||
@ -453,7 +498,7 @@ class _ListView extends StatelessWidget {
|
|||||||
controller: state.value.scrollController,
|
controller: state.value.scrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
children: state.value.tabs.asMap().entries.map((e) {
|
children: state.value.tabs.asMap().entries.map((e) {
|
||||||
final index = e.key;
|
final index = e.key;
|
||||||
final tab = e.value;
|
final tab = e.value;
|
||||||
@ -468,7 +513,6 @@ class _ListView extends StatelessWidget {
|
|||||||
selected: state.value.selected,
|
selected: state.value.selected,
|
||||||
onClose: () => controller.remove(index),
|
onClose: () => controller.remove(index),
|
||||||
onSelected: () => controller.jumpTo(index),
|
onSelected: () => controller.jumpTo(index),
|
||||||
theme: theme,
|
|
||||||
tabBuilder: tabBuilder == null
|
tabBuilder: tabBuilder == null
|
||||||
? null
|
? null
|
||||||
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
|
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
|
||||||
@ -485,20 +529,19 @@ class _ListView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _Tab extends StatefulWidget {
|
class _Tab extends StatefulWidget {
|
||||||
late final int index;
|
final int index;
|
||||||
late final Rx<String> label;
|
final Rx<String> label;
|
||||||
late final IconData? selectedIcon;
|
final IconData? selectedIcon;
|
||||||
late final IconData? unselectedIcon;
|
final IconData? unselectedIcon;
|
||||||
late final bool closable;
|
final bool closable;
|
||||||
late final int selected;
|
final int selected;
|
||||||
late final Function() onClose;
|
final Function() onClose;
|
||||||
late final Function() onSelected;
|
final Function() onSelected;
|
||||||
late final TarBarTheme theme;
|
|
||||||
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
|
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
|
||||||
tabBuilder;
|
tabBuilder;
|
||||||
|
|
||||||
_Tab(
|
const _Tab({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.index,
|
required this.index,
|
||||||
required this.label,
|
required this.label,
|
||||||
this.selectedIcon,
|
this.selectedIcon,
|
||||||
@ -508,8 +551,7 @@ class _Tab extends StatefulWidget {
|
|||||||
required this.selected,
|
required this.selected,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
required this.theme})
|
}) : super(key: key);
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_Tab> createState() => _TabState();
|
State<_Tab> createState() => _TabState();
|
||||||
@ -529,8 +571,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
|||||||
isSelected ? widget.selectedIcon : widget.unselectedIcon,
|
isSelected ? widget.selectedIcon : widget.unselectedIcon,
|
||||||
size: _kIconSize,
|
size: _kIconSize,
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? widget.theme.selectedtabIconColor
|
? MyTheme.tabbar(context).selectedTabIconColor
|
||||||
: widget.theme.unSelectedtabIconColor,
|
: MyTheme.tabbar(context).unSelectedTabIconColor,
|
||||||
).paddingOnly(right: 5));
|
).paddingOnly(right: 5));
|
||||||
final labelWidget = Obx(() {
|
final labelWidget = Obx(() {
|
||||||
return Text(
|
return Text(
|
||||||
@ -538,8 +580,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? widget.theme.selectedTextColor
|
? MyTheme.tabbar(context).selectedTextColor
|
||||||
: widget.theme.unSelectedTextColor),
|
: MyTheme.tabbar(context).unSelectedTextColor),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -552,8 +594,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return widget.tabBuilder!(icon, labelWidget,
|
return widget.tabBuilder!(
|
||||||
TabThemeConf(iconSize: _kIconSize, theme: widget.theme));
|
icon, labelWidget, TabThemeConf(iconSize: _kIconSize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -582,7 +624,6 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
|||||||
visiable: hover.value && widget.closable,
|
visiable: hover.value && widget.closable,
|
||||||
tabSelected: isSelected,
|
tabSelected: isSelected,
|
||||||
onClose: () => widget.onClose(),
|
onClose: () => widget.onClose(),
|
||||||
theme: widget.theme,
|
|
||||||
)))
|
)))
|
||||||
])).paddingSymmetric(horizontal: 10),
|
])).paddingSymmetric(horizontal: 10),
|
||||||
Offstage(
|
Offstage(
|
||||||
@ -591,7 +632,7 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
|||||||
width: 1,
|
width: 1,
|
||||||
indent: _kDividerIndent,
|
indent: _kDividerIndent,
|
||||||
endIndent: _kDividerIndent,
|
endIndent: _kDividerIndent,
|
||||||
color: widget.theme.dividerColor,
|
color: MyTheme.tabbar(context).dividerColor,
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -614,14 +655,12 @@ class _CloseButton extends StatelessWidget {
|
|||||||
final bool visiable;
|
final bool visiable;
|
||||||
final bool tabSelected;
|
final bool tabSelected;
|
||||||
final Function onClose;
|
final Function onClose;
|
||||||
late final TarBarTheme theme;
|
|
||||||
|
|
||||||
_CloseButton({
|
const _CloseButton({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.visiable,
|
required this.visiable,
|
||||||
required this.tabSelected,
|
required this.tabSelected,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
required this.theme,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -637,8 +676,8 @@ class _CloseButton extends StatelessWidget {
|
|||||||
Icons.close,
|
Icons.close,
|
||||||
size: _kIconSize,
|
size: _kIconSize,
|
||||||
color: tabSelected
|
color: tabSelected
|
||||||
? theme.selectedIconColor
|
? MyTheme.tabbar(context).selectedIconColor
|
||||||
: theme.unSelectedIconColor,
|
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)).paddingOnly(left: 5);
|
)).paddingOnly(left: 5);
|
||||||
@ -648,16 +687,14 @@ class _CloseButton extends StatelessWidget {
|
|||||||
class ActionIcon extends StatelessWidget {
|
class ActionIcon extends StatelessWidget {
|
||||||
final String message;
|
final String message;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final TarBarTheme theme;
|
|
||||||
final Function() onTap;
|
final Function() onTap;
|
||||||
final bool is_close;
|
final bool isClose;
|
||||||
const ActionIcon({
|
const ActionIcon({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.theme,
|
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.is_close,
|
required this.isClose,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -665,34 +702,32 @@ class ActionIcon extends StatelessWidget {
|
|||||||
RxBool hover = false.obs;
|
RxBool hover = false.obs;
|
||||||
return Obx(() => Tooltip(
|
return Obx(() => Tooltip(
|
||||||
message: translate(message),
|
message: translate(message),
|
||||||
waitDuration: Duration(seconds: 1),
|
waitDuration: const Duration(seconds: 1),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
hoverColor:
|
hoverColor: isClose
|
||||||
is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor,
|
? const Color.fromARGB(255, 196, 43, 28)
|
||||||
|
: MyTheme.tabbar(context).hoverColor,
|
||||||
onHover: (value) => hover.value = value,
|
onHover: (value) => hover.value = value,
|
||||||
child: Container(
|
onTap: onTap,
|
||||||
|
child: SizedBox(
|
||||||
height: _kTabBarHeight - 1,
|
height: _kTabBarHeight - 1,
|
||||||
width: _kTabBarHeight - 1,
|
width: _kTabBarHeight - 1,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
color: hover.value && is_close
|
color: hover.value && isClose
|
||||||
? Colors.white
|
? Colors.white
|
||||||
: theme.unSelectedIconColor,
|
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||||
size: _kActionIconSize,
|
size: _kActionIconSize,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddButton extends StatelessWidget {
|
class AddButton extends StatelessWidget {
|
||||||
late final TarBarTheme theme;
|
const AddButton({
|
||||||
|
|
||||||
AddButton({
|
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.theme,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -700,41 +735,101 @@ class AddButton extends StatelessWidget {
|
|||||||
return ActionIcon(
|
return ActionIcon(
|
||||||
message: 'New Connection',
|
message: 'New Connection',
|
||||||
icon: IconFont.add,
|
icon: IconFont.add,
|
||||||
theme: theme,
|
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""),
|
rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""),
|
||||||
is_close: false);
|
isClose: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TarBarTheme {
|
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||||
final Color unSelectedtabIconColor;
|
final Color? selectedTabIconColor;
|
||||||
final Color selectedtabIconColor;
|
final Color? unSelectedTabIconColor;
|
||||||
final Color selectedTextColor;
|
final Color? selectedTextColor;
|
||||||
final Color unSelectedTextColor;
|
final Color? unSelectedTextColor;
|
||||||
final Color selectedIconColor;
|
final Color? selectedIconColor;
|
||||||
final Color unSelectedIconColor;
|
final Color? unSelectedIconColor;
|
||||||
final Color dividerColor;
|
final Color? dividerColor;
|
||||||
final Color hoverColor;
|
final Color? hoverColor;
|
||||||
|
|
||||||
const TarBarTheme.light()
|
const TabbarTheme(
|
||||||
: unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241),
|
{required this.selectedTabIconColor,
|
||||||
selectedtabIconColor = MyTheme.accent,
|
required this.unSelectedTabIconColor,
|
||||||
selectedTextColor = const Color.fromARGB(255, 26, 26, 26),
|
required this.selectedTextColor,
|
||||||
unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96),
|
required this.unSelectedTextColor,
|
||||||
selectedIconColor = const Color.fromARGB(255, 26, 26, 26),
|
required this.selectedIconColor,
|
||||||
unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96),
|
required this.unSelectedIconColor,
|
||||||
dividerColor = const Color.fromARGB(255, 238, 238, 238),
|
required this.dividerColor,
|
||||||
hoverColor = const Color.fromARGB(
|
required this.hoverColor});
|
||||||
51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E
|
|
||||||
|
|
||||||
const TarBarTheme.dark()
|
static const light = TabbarTheme(
|
||||||
: unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98),
|
selectedTabIconColor: MyTheme.accent,
|
||||||
selectedtabIconColor = MyTheme.accent,
|
unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241),
|
||||||
selectedTextColor = const Color.fromARGB(255, 255, 255, 255),
|
selectedTextColor: Color.fromARGB(255, 26, 26, 26),
|
||||||
unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207),
|
unSelectedTextColor: Color.fromARGB(255, 96, 96, 96),
|
||||||
selectedIconColor = const Color.fromARGB(255, 215, 215, 215),
|
selectedIconColor: Color.fromARGB(255, 26, 26, 26),
|
||||||
unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255),
|
unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
|
||||||
dividerColor = const Color.fromARGB(255, 64, 64, 64),
|
dividerColor: Color.fromARGB(255, 238, 238, 238),
|
||||||
hoverColor = Colors.black26;
|
hoverColor: Color.fromARGB(51, 158, 158, 158));
|
||||||
|
|
||||||
|
static const dark = TabbarTheme(
|
||||||
|
selectedTabIconColor: MyTheme.accent,
|
||||||
|
unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98),
|
||||||
|
selectedTextColor: Color.fromARGB(255, 255, 255, 255),
|
||||||
|
unSelectedTextColor: Color.fromARGB(255, 207, 207, 207),
|
||||||
|
selectedIconColor: Color.fromARGB(255, 215, 215, 215),
|
||||||
|
unSelectedIconColor: Color.fromARGB(255, 255, 255, 255),
|
||||||
|
dividerColor: Color.fromARGB(255, 64, 64, 64),
|
||||||
|
hoverColor: Colors.black26);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThemeExtension<TabbarTheme> copyWith({
|
||||||
|
Color? selectedTabIconColor,
|
||||||
|
Color? unSelectedTabIconColor,
|
||||||
|
Color? selectedTextColor,
|
||||||
|
Color? unSelectedTextColor,
|
||||||
|
Color? selectedIconColor,
|
||||||
|
Color? unSelectedIconColor,
|
||||||
|
Color? dividerColor,
|
||||||
|
Color? hoverColor,
|
||||||
|
}) {
|
||||||
|
return TabbarTheme(
|
||||||
|
selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor,
|
||||||
|
unSelectedTabIconColor:
|
||||||
|
unSelectedTabIconColor ?? this.unSelectedTabIconColor,
|
||||||
|
selectedTextColor: selectedTextColor ?? this.selectedTextColor,
|
||||||
|
unSelectedTextColor: unSelectedTextColor ?? this.unSelectedTextColor,
|
||||||
|
selectedIconColor: selectedIconColor ?? this.selectedIconColor,
|
||||||
|
unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor,
|
||||||
|
dividerColor: dividerColor ?? this.dividerColor,
|
||||||
|
hoverColor: hoverColor ?? this.hoverColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThemeExtension<TabbarTheme> lerp(
|
||||||
|
ThemeExtension<TabbarTheme>? other, double t) {
|
||||||
|
if (other is! TabbarTheme) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return TabbarTheme(
|
||||||
|
selectedTabIconColor:
|
||||||
|
Color.lerp(selectedTabIconColor, other.selectedTabIconColor, t),
|
||||||
|
unSelectedTabIconColor:
|
||||||
|
Color.lerp(unSelectedTabIconColor, other.unSelectedTabIconColor, t),
|
||||||
|
selectedTextColor:
|
||||||
|
Color.lerp(selectedTextColor, other.selectedTextColor, t),
|
||||||
|
unSelectedTextColor:
|
||||||
|
Color.lerp(unSelectedTextColor, other.unSelectedTextColor, t),
|
||||||
|
selectedIconColor:
|
||||||
|
Color.lerp(selectedIconColor, other.selectedIconColor, t),
|
||||||
|
unSelectedIconColor:
|
||||||
|
Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t),
|
||||||
|
dividerColor: Color.lerp(dividerColor, other.dividerColor, t),
|
||||||
|
hoverColor: Color.lerp(hoverColor, other.hoverColor, t),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static color(BuildContext context) {
|
||||||
|
return Theme.of(context).extension<ColorThemeExtension>()!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,6 +165,7 @@ void runConnectionManagerScreen() async {
|
|||||||
await windowManager.setAlignment(Alignment.topRight);
|
await windowManager.setAlignment(Alignment.topRight);
|
||||||
await windowManager.show();
|
await windowManager.show();
|
||||||
await windowManager.focus();
|
await windowManager.focus();
|
||||||
|
await windowManager.setAlignment(Alignment.topRight); // ensure
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
runApp(GetMaterialApp(
|
runApp(GetMaterialApp(
|
||||||
|
|||||||
@ -45,7 +45,7 @@ class ChatPage extends StatelessWidget implements PageShape {
|
|||||||
return ChangeNotifierProvider.value(
|
return ChangeNotifierProvider.value(
|
||||||
value: chatModel,
|
value: chatModel,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: MyTheme.grayBg,
|
color: MyTheme.color(context).grayBg,
|
||||||
child: Consumer<ChatModel>(builder: (context, chatModel, child) {
|
child: Consumer<ChatModel>(builder: (context, chatModel, child) {
|
||||||
final currentUser = chatModel.currentUser;
|
final currentUser = chatModel.currentUser;
|
||||||
return Stack(
|
return Stack(
|
||||||
@ -59,6 +59,14 @@ class ChatPage extends StatelessWidget implements PageShape {
|
|||||||
messages: chatModel
|
messages: chatModel
|
||||||
.messages[chatModel.currentID]?.chatMessages ??
|
.messages[chatModel.currentID]?.chatMessages ??
|
||||||
[],
|
[],
|
||||||
|
inputOptions: InputOptions(
|
||||||
|
sendOnEnter: true,
|
||||||
|
inputDecoration: defaultInputDecoration(
|
||||||
|
fillColor: MyTheme.color(context).bg),
|
||||||
|
sendButtonBuilder: defaultSendButton(
|
||||||
|
color: MyTheme.color(context).text!),
|
||||||
|
inputTextStyle:
|
||||||
|
TextStyle(color: MyTheme.color(context).text)),
|
||||||
messageOptions: MessageOptions(
|
messageOptions: MessageOptions(
|
||||||
showOtherUsersAvatar: false,
|
showOtherUsersAvatar: false,
|
||||||
showTime: true,
|
showTime: true,
|
||||||
|
|||||||
@ -209,10 +209,19 @@ class ChatModel with ChangeNotifier {
|
|||||||
id: await bind.mainGetLastRemoteId(),
|
id: await bind.mainGetLastRemoteId(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final client = _ffi.target?.serverModel.clients[id];
|
final client = _ffi.target?.serverModel.clients
|
||||||
|
.firstWhere((client) => client.id == id);
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return debugPrint("Failed to receive msg,user doesn't exist");
|
return debugPrint("Failed to receive msg,user doesn't exist");
|
||||||
}
|
}
|
||||||
|
if (isDesktop) {
|
||||||
|
window_on_top(null);
|
||||||
|
var index = _ffi.target?.serverModel.clients
|
||||||
|
.indexWhere((client) => client.id == id);
|
||||||
|
if (index != null && index >= 0) {
|
||||||
|
gFFI.serverModel.tabController.jumpTo(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
chatUser = ChatUser(id: client.peerId, firstName: client.name);
|
chatUser = ChatUser(id: client.peerId, firstName: client.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -360,9 +360,9 @@ class FileModel extends ChangeNotifier {
|
|||||||
Future refresh({bool? isLocal}) async {
|
Future refresh({bool? isLocal}) async {
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
isLocal = isLocal ?? _isLocal;
|
isLocal = isLocal ?? _isLocal;
|
||||||
await isLocal
|
isLocal
|
||||||
? openDirectory(currentLocalDir.path, isLocal: isLocal)
|
? await openDirectory(currentLocalDir.path, isLocal: isLocal)
|
||||||
: openDirectory(currentRemoteDir.path, isLocal: isLocal);
|
: await openDirectory(currentRemoteDir.path, isLocal: isLocal);
|
||||||
} else {
|
} else {
|
||||||
await openDirectory(currentDir.path);
|
await openDirectory(currentDir.path);
|
||||||
}
|
}
|
||||||
@ -393,7 +393,7 @@ class FileModel extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to openDirectory ${path} :$e");
|
debugPrint("Failed to openDirectory $path: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,11 +559,14 @@ class FileModel extends ChangeNotifier {
|
|||||||
Future<bool?> showRemoveDialog(
|
Future<bool?> showRemoveDialog(
|
||||||
String title, String content, bool showCheckbox) async {
|
String title, String content, bool showCheckbox) async {
|
||||||
return await parent.target?.dialogManager.show<bool>(
|
return await parent.target?.dialogManager.show<bool>(
|
||||||
(setState, Function(bool v) close) => CustomAlertDialog(
|
(setState, Function(bool v) close) {
|
||||||
|
cancel() => close(false);
|
||||||
|
submit() => close(true);
|
||||||
|
return CustomAlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.warning, color: Colors.red),
|
const Icon(Icons.warning, color: Colors.red),
|
||||||
SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
Text(title)
|
Text(title)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -572,9 +575,9 @@ class FileModel extends ChangeNotifier {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(content),
|
Text(content),
|
||||||
SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
Text(translate("This is irreversible!"),
|
Text(translate("This is irreversible!"),
|
||||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
showCheckbox
|
showCheckbox
|
||||||
? CheckboxListTile(
|
? CheckboxListTile(
|
||||||
contentPadding: const EdgeInsets.all(0),
|
contentPadding: const EdgeInsets.all(0),
|
||||||
@ -589,19 +592,22 @@ class FileModel extends ChangeNotifier {
|
|||||||
setState(() => removeCheckboxRemember = v);
|
setState(() => removeCheckboxRemember = v);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: SizedBox.shrink()
|
: const SizedBox.shrink()
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () => close(false),
|
onPressed: cancel,
|
||||||
child: Text(translate("Cancel"))),
|
child: Text(translate("Cancel"))),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () => close(true),
|
onPressed: submit,
|
||||||
child: Text(translate("OK"))),
|
child: Text(translate("OK"))),
|
||||||
]),
|
],
|
||||||
useAnimation: false);
|
onSubmit: submit,
|
||||||
|
onCancel: cancel,
|
||||||
|
);
|
||||||
|
}, useAnimation: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool fileConfirmCheckboxRemember = false;
|
bool fileConfirmCheckboxRemember = false;
|
||||||
@ -610,11 +616,14 @@ class FileModel extends ChangeNotifier {
|
|||||||
String title, String content, bool showCheckbox) async {
|
String title, String content, bool showCheckbox) async {
|
||||||
fileConfirmCheckboxRemember = false;
|
fileConfirmCheckboxRemember = false;
|
||||||
return await parent.target?.dialogManager.show<bool?>(
|
return await parent.target?.dialogManager.show<bool?>(
|
||||||
(setState, Function(bool? v) close) => CustomAlertDialog(
|
(setState, Function(bool? v) close) {
|
||||||
|
cancel() => close(false);
|
||||||
|
submit() => close(true);
|
||||||
|
return CustomAlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.warning, color: Colors.red),
|
const Icon(Icons.warning, color: Colors.red),
|
||||||
SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
Text(title)
|
Text(title)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -622,11 +631,9 @@ class FileModel extends ChangeNotifier {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(translate("This file exists, skip or overwrite this file?"),
|
||||||
translate(
|
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
"This file exists, skip or overwrite this file?"),
|
const SizedBox(height: 5),
|
||||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
SizedBox(height: 5),
|
|
||||||
Text(content),
|
Text(content),
|
||||||
showCheckbox
|
showCheckbox
|
||||||
? CheckboxListTile(
|
? CheckboxListTile(
|
||||||
@ -642,12 +649,12 @@ class FileModel extends ChangeNotifier {
|
|||||||
setState(() => fileConfirmCheckboxRemember = v);
|
setState(() => fileConfirmCheckboxRemember = v);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: SizedBox.shrink()
|
: const SizedBox.shrink()
|
||||||
]),
|
]),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () => close(false),
|
onPressed: cancel,
|
||||||
child: Text(translate("Cancel"))),
|
child: Text(translate("Cancel"))),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
@ -655,10 +662,13 @@ class FileModel extends ChangeNotifier {
|
|||||||
child: Text(translate("Skip"))),
|
child: Text(translate("Skip"))),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: flatButtonStyle,
|
style: flatButtonStyle,
|
||||||
onPressed: () => close(true),
|
onPressed: submit,
|
||||||
child: Text(translate("OK"))),
|
child: Text(translate("OK"))),
|
||||||
]),
|
],
|
||||||
useAnimation: false);
|
onSubmit: submit,
|
||||||
|
onCancel: cancel,
|
||||||
|
);
|
||||||
|
}, useAnimation: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRemoveFile(String path, int fileNum, bool isLocal) {
|
sendRemoveFile(String path, int fileNum, bool isLocal) {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
Display _display = Display();
|
Display _display = Display();
|
||||||
|
|
||||||
var _inputBlocked = false;
|
var _inputBlocked = false;
|
||||||
final _permissions = Map<String, bool>();
|
final _permissions = <String, bool>{};
|
||||||
bool? _secure;
|
bool? _secure;
|
||||||
bool? _direct;
|
bool? _direct;
|
||||||
bool _touchMode = false;
|
bool _touchMode = false;
|
||||||
@ -71,12 +71,13 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updatePermission(Map<String, dynamic> evt) {
|
void updatePermission(Map<String, dynamic> evt, String id) {
|
||||||
evt.forEach((k, v) {
|
evt.forEach((k, v) {
|
||||||
if (k == 'name' || k.isEmpty) return;
|
if (k == 'name' || k.isEmpty) return;
|
||||||
_permissions[k] = v == 'true';
|
_permissions[k] = v == 'true';
|
||||||
});
|
});
|
||||||
print('$_permissions');
|
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
||||||
|
debugPrint('$_permissions');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +147,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'clipboard') {
|
} else if (name == 'clipboard') {
|
||||||
Clipboard.setData(ClipboardData(text: evt['content']));
|
Clipboard.setData(ClipboardData(text: evt['content']));
|
||||||
} else if (name == 'permission') {
|
} else if (name == 'permission') {
|
||||||
parent.target?.ffiModel.updatePermission(evt);
|
parent.target?.ffiModel.updatePermission(evt, peerId);
|
||||||
} else if (name == 'chat_client_mode') {
|
} else if (name == 'chat_client_mode') {
|
||||||
parent.target?.chatModel
|
parent.target?.chatModel
|
||||||
.receive(ChatModel.clientModeID, evt['text'] ?? "");
|
.receive(ChatModel.clientModeID, evt['text'] ?? "");
|
||||||
@ -167,10 +168,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
parent.target?.fileModel.loadLastJob(evt);
|
parent.target?.fileModel.loadLastJob(evt);
|
||||||
} else if (name == 'update_folder_files') {
|
} else if (name == 'update_folder_files') {
|
||||||
parent.target?.fileModel.updateFolderFiles(evt);
|
parent.target?.fileModel.updateFolderFiles(evt);
|
||||||
} else if (name == 'try_start_without_auth') {
|
} else if (name == 'add_connection') {
|
||||||
parent.target?.serverModel.loginRequest(evt);
|
parent.target?.serverModel.addConnection(evt);
|
||||||
} else if (name == 'on_client_authorized') {
|
|
||||||
parent.target?.serverModel.onClientAuthorized(evt);
|
|
||||||
} else if (name == 'on_client_remove') {
|
} else if (name == 'on_client_remove') {
|
||||||
parent.target?.serverModel.onClientRemove(evt);
|
parent.target?.serverModel.onClientRemove(evt);
|
||||||
} else if (name == 'update_quality_status') {
|
} else if (name == 'update_quality_status') {
|
||||||
@ -185,7 +184,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
|
|
||||||
/// Bind the event listener to receive events from the Rust core.
|
/// Bind the event listener to receive events from the Rust core.
|
||||||
void updateEventListener(String peerId) {
|
void updateEventListener(String peerId) {
|
||||||
final void Function(Map<String, dynamic>) cb = (evt) {
|
cb(evt) {
|
||||||
var name = evt['name'];
|
var name = evt['name'];
|
||||||
if (name == 'msgbox') {
|
if (name == 'msgbox') {
|
||||||
handleMsgBox(evt, peerId);
|
handleMsgBox(evt, peerId);
|
||||||
@ -205,7 +204,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'clipboard') {
|
} else if (name == 'clipboard') {
|
||||||
Clipboard.setData(ClipboardData(text: evt['content']));
|
Clipboard.setData(ClipboardData(text: evt['content']));
|
||||||
} else if (name == 'permission') {
|
} else if (name == 'permission') {
|
||||||
parent.target?.ffiModel.updatePermission(evt);
|
parent.target?.ffiModel.updatePermission(evt, peerId);
|
||||||
} else if (name == 'chat_client_mode') {
|
} else if (name == 'chat_client_mode') {
|
||||||
parent.target?.chatModel
|
parent.target?.chatModel
|
||||||
.receive(ChatModel.clientModeID, evt['text'] ?? "");
|
.receive(ChatModel.clientModeID, evt['text'] ?? "");
|
||||||
@ -226,10 +225,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
parent.target?.fileModel.loadLastJob(evt);
|
parent.target?.fileModel.loadLastJob(evt);
|
||||||
} else if (name == 'update_folder_files') {
|
} else if (name == 'update_folder_files') {
|
||||||
parent.target?.fileModel.updateFolderFiles(evt);
|
parent.target?.fileModel.updateFolderFiles(evt);
|
||||||
} else if (name == 'try_start_without_auth') {
|
} else if (name == 'add_connection') {
|
||||||
parent.target?.serverModel.loginRequest(evt);
|
parent.target?.serverModel.addConnection(evt);
|
||||||
} else if (name == 'on_client_authorized') {
|
|
||||||
parent.target?.serverModel.onClientAuthorized(evt);
|
|
||||||
} else if (name == 'on_client_remove') {
|
} else if (name == 'on_client_remove') {
|
||||||
parent.target?.serverModel.onClientRemove(evt);
|
parent.target?.serverModel.onClientRemove(evt);
|
||||||
} else if (name == 'update_quality_status') {
|
} else if (name == 'update_quality_status') {
|
||||||
@ -239,7 +236,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'update_privacy_mode') {
|
} else if (name == 'update_privacy_mode') {
|
||||||
updatePrivacyMode(evt, peerId);
|
updatePrivacyMode(evt, peerId);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
platformFFI.setEventCallback(cb);
|
platformFFI.setEventCallback(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,15 +319,15 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (isPeerAndroid) {
|
if (isPeerAndroid) {
|
||||||
_touchMode = true;
|
_touchMode = true;
|
||||||
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
|
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
|
||||||
Timer(Duration(milliseconds: 100), showMobileActionsOverlay);
|
Timer(const Duration(milliseconds: 100), showMobileActionsOverlay);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_touchMode =
|
_touchMode =
|
||||||
await bind.sessionGetOption(id: peerId, arg: "touch-mode") != '';
|
await bind.sessionGetOption(id: peerId, arg: "touch-mode") != '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evt['is_file_transfer'] == "true") {
|
if (parent.target != null &&
|
||||||
// TODO is file transfer
|
parent.target!.connType == ConnType.fileTransfer) {
|
||||||
parent.target?.fileModel.onReady();
|
parent.target?.fileModel.onReady();
|
||||||
} else {
|
} else {
|
||||||
_pi.displays = [];
|
_pi.displays = [];
|
||||||
@ -464,15 +462,20 @@ enum ScrollStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CanvasModel with ChangeNotifier {
|
class CanvasModel with ChangeNotifier {
|
||||||
|
// image offset of canvas
|
||||||
|
double _x = 0;
|
||||||
|
// image offset of canvas
|
||||||
|
double _y = 0;
|
||||||
|
// image scale
|
||||||
|
double _scale = 1.0;
|
||||||
|
// the tabbar over the image
|
||||||
|
double tabBarHeight = 0.0;
|
||||||
|
// TODO multi canvas model
|
||||||
|
String id = "";
|
||||||
// scroll offset x percent
|
// scroll offset x percent
|
||||||
double _scrollX = 0.0;
|
double _scrollX = 0.0;
|
||||||
// scroll offset y percent
|
// scroll offset y percent
|
||||||
double _scrollY = 0.0;
|
double _scrollY = 0.0;
|
||||||
double _x = 0;
|
|
||||||
double _y = 0;
|
|
||||||
double _scale = 1.0;
|
|
||||||
double _tabBarHeight = 0.0;
|
|
||||||
String id = ""; // TODO multi canvas model
|
|
||||||
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
@ -492,9 +495,6 @@ class CanvasModel with ChangeNotifier {
|
|||||||
double get scrollX => _scrollX;
|
double get scrollX => _scrollX;
|
||||||
double get scrollY => _scrollY;
|
double get scrollY => _scrollY;
|
||||||
|
|
||||||
set tabBarHeight(double h) => _tabBarHeight = h;
|
|
||||||
double get tabBarHeight => _tabBarHeight;
|
|
||||||
|
|
||||||
void updateViewStyle() async {
|
void updateViewStyle() async {
|
||||||
final style = await bind.sessionGetOption(id: id, arg: 'view-style');
|
final style = await bind.sessionGetOption(id: id, arg: 'view-style');
|
||||||
if (style == null) {
|
if (style == null) {
|
||||||
@ -548,12 +548,11 @@ class CanvasModel with ChangeNotifier {
|
|||||||
|
|
||||||
Size get size {
|
Size get size {
|
||||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||||
return Size(size.width, size.height - _tabBarHeight);
|
return Size(size.width, size.height - tabBarHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
void moveDesktopMouse(double x, double y) {
|
void moveDesktopMouse(double x, double y) {
|
||||||
// On mobile platforms, move the canvas with the cursor.
|
// On mobile platforms, move the canvas with the cursor.
|
||||||
//if (!isDesktop) {
|
|
||||||
final dw = getDisplayWidth() * _scale;
|
final dw = getDisplayWidth() * _scale;
|
||||||
final dh = getDisplayHeight() * _scale;
|
final dh = getDisplayHeight() * _scale;
|
||||||
var dxOffset = 0;
|
var dxOffset = 0;
|
||||||
@ -579,8 +578,13 @@ class CanvasModel with ChangeNotifier {
|
|||||||
if (dxOffset != 0 || dyOffset != 0) {
|
if (dxOffset != 0 || dyOffset != 0) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
//}
|
|
||||||
parent.target?.cursorModel.moveLocal(x, y);
|
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
||||||
|
if (parent.target != null) {
|
||||||
|
if (parent.target!.ffiModel.keyboard()) {
|
||||||
|
parent.target!.cursorModel.moveLocal(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set scale(v) {
|
set scale(v) {
|
||||||
@ -597,11 +601,8 @@ class CanvasModel with ChangeNotifier {
|
|||||||
if (isWebDesktop) {
|
if (isWebDesktop) {
|
||||||
updateViewStyle();
|
updateViewStyle();
|
||||||
} else {
|
} else {
|
||||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
_x = (size.width - getDisplayWidth() * _scale) / 2;
|
||||||
final canvasWidth = size.width;
|
_y = (size.height - getDisplayHeight() * _scale) / 2;
|
||||||
final canvasHeight = size.height - _tabBarHeight;
|
|
||||||
_x = (canvasWidth - getDisplayWidth() * _scale) / 2;
|
|
||||||
_y = (canvasHeight - getDisplayHeight() * _scale) / 2;
|
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -613,7 +614,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
|
|
||||||
void updateScale(double v) {
|
void updateScale(double v) {
|
||||||
if (parent.target?.imageModel.image == null) return;
|
if (parent.target?.imageModel.image == null) return;
|
||||||
final offset = parent.target?.cursorModel.offset ?? Offset(0, 0);
|
final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0);
|
||||||
var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
|
var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
|
||||||
final px0 = (offset.dx - r.left) * _scale;
|
final px0 = (offset.dx - r.left) * _scale;
|
||||||
final py0 = (offset.dy - r.top) * _scale;
|
final py0 = (offset.dy - r.top) * _scale;
|
||||||
@ -640,7 +641,7 @@ class CanvasModel with ChangeNotifier {
|
|||||||
|
|
||||||
class CursorModel with ChangeNotifier {
|
class CursorModel with ChangeNotifier {
|
||||||
ui.Image? _image;
|
ui.Image? _image;
|
||||||
final _images = Map<int, Tuple3<ui.Image, double, double>>();
|
final _images = <int, Tuple3<ui.Image, double, double>>{};
|
||||||
double _x = -10000;
|
double _x = -10000;
|
||||||
double _y = -10000;
|
double _y = -10000;
|
||||||
double _hotx = 0;
|
double _hotx = 0;
|
||||||
@ -807,7 +808,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
// my throw exception, because the listener maybe already dispose
|
// my throw exception, because the listener maybe already dispose
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('notify cursor: $e');
|
debugPrint('notify cursor: $e');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -915,6 +916,8 @@ extension ToString on MouseButtons {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ConnType { defaultConn, fileTransfer, portForward, rdp }
|
||||||
|
|
||||||
/// FFI class for communicating with the Rust core.
|
/// FFI class for communicating with the Rust core.
|
||||||
class FFI {
|
class FFI {
|
||||||
var id = "";
|
var id = "";
|
||||||
@ -923,6 +926,7 @@ class FFI {
|
|||||||
var alt = false;
|
var alt = false;
|
||||||
var command = false;
|
var command = false;
|
||||||
var version = "";
|
var version = "";
|
||||||
|
var connType = ConnType.defaultConn;
|
||||||
|
|
||||||
/// dialogManager use late to ensure init after main page binding [globalKey]
|
/// dialogManager use late to ensure init after main page binding [globalKey]
|
||||||
late final dialogManager = OverlayDialogManager();
|
late final dialogManager = OverlayDialogManager();
|
||||||
@ -1076,9 +1080,11 @@ class FFI {
|
|||||||
double tabBarHeight = 0.0}) {
|
double tabBarHeight = 0.0}) {
|
||||||
assert(!(isFileTransfer && isPortForward), "more than one connect type");
|
assert(!(isFileTransfer && isPortForward), "more than one connect type");
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
id = 'ft_${id}';
|
connType = ConnType.fileTransfer;
|
||||||
|
id = 'ft_$id';
|
||||||
} else if (isPortForward) {
|
} else if (isPortForward) {
|
||||||
id = 'pf_${id}';
|
connType = ConnType.portForward;
|
||||||
|
id = 'pf_$id';
|
||||||
} else {
|
} else {
|
||||||
chatModel.resetClientMode();
|
chatModel.resetClientMode();
|
||||||
canvasModel.id = id;
|
canvasModel.id = id;
|
||||||
@ -1107,7 +1113,7 @@ class FFI {
|
|||||||
// every instance will bind a stream
|
// every instance will bind a stream
|
||||||
this.id = id;
|
this.id = id;
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
this.fileModel.initFileFetcher();
|
fileModel.initFileFetcher();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
|
|||||||
import 'package:wakelock/wakelock.dart';
|
import 'package:wakelock/wakelock.dart';
|
||||||
|
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
|
import '../common/formatter/id_formatter.dart';
|
||||||
import '../desktop/pages/server_page.dart' as Desktop;
|
import '../desktop/pages/server_page.dart' as Desktop;
|
||||||
import '../desktop/widgets/tabbar_widget.dart';
|
import '../desktop/widgets/tabbar_widget.dart';
|
||||||
import '../mobile/pages/server_page.dart';
|
import '../mobile/pages/server_page.dart';
|
||||||
@ -29,10 +30,10 @@ class ServerModel with ChangeNotifier {
|
|||||||
String _temporaryPasswordLength = "";
|
String _temporaryPasswordLength = "";
|
||||||
|
|
||||||
late String _emptyIdShow;
|
late String _emptyIdShow;
|
||||||
late final TextEditingController _serverId;
|
late final IDTextEditingController _serverId;
|
||||||
final _serverPasswd = TextEditingController(text: "");
|
final _serverPasswd = TextEditingController(text: "");
|
||||||
|
|
||||||
final tabController = DesktopTabController();
|
final tabController = DesktopTabController(tabType: DesktopTabType.cm);
|
||||||
|
|
||||||
List<Client> _clients = [];
|
List<Client> _clients = [];
|
||||||
|
|
||||||
@ -88,7 +89,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
|
|
||||||
ServerModel(this.parent) {
|
ServerModel(this.parent) {
|
||||||
_emptyIdShow = translate("Generating ...");
|
_emptyIdShow = translate("Generating ...");
|
||||||
_serverId = TextEditingController(text: this._emptyIdShow);
|
_serverId = IDTextEditingController(text: _emptyIdShow);
|
||||||
|
|
||||||
Timer.periodic(Duration(seconds: 1), (timer) async {
|
Timer.periodic(Duration(seconds: 1), (timer) async {
|
||||||
var status = await bind.mainGetOnlineStatue();
|
var status = await bind.mainGetOnlineStatue();
|
||||||
@ -99,7 +100,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
_connectStatus = status;
|
_connectStatus = status;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
final res = await bind.mainCheckClientsLength(length: _clients.length);
|
final res = await bind.cmCheckClientsLength(length: _clients.length);
|
||||||
if (res != null) {
|
if (res != null) {
|
||||||
debugPrint("clients not match!");
|
debugPrint("clients not match!");
|
||||||
updateClientState(res);
|
updateClientState(res);
|
||||||
@ -208,46 +209,48 @@ class ServerModel with ChangeNotifier {
|
|||||||
/// Toggle the screen sharing service.
|
/// Toggle the screen sharing service.
|
||||||
toggleService() async {
|
toggleService() async {
|
||||||
if (_isStart) {
|
if (_isStart) {
|
||||||
final res = await parent.target?.dialogManager
|
final res =
|
||||||
.show<bool>((setState, close) => CustomAlertDialog(
|
await parent.target?.dialogManager.show<bool>((setState, close) {
|
||||||
|
submit() => close(true);
|
||||||
|
return CustomAlertDialog(
|
||||||
title: Row(children: [
|
title: Row(children: [
|
||||||
Icon(Icons.warning_amber_sharp,
|
const Icon(Icons.warning_amber_sharp,
|
||||||
color: Colors.redAccent, size: 28),
|
color: Colors.redAccent, size: 28),
|
||||||
SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(translate("Warning")),
|
Text(translate("Warning")),
|
||||||
]),
|
]),
|
||||||
content: Text(translate("android_stop_service_tip")),
|
content: Text(translate("android_stop_service_tip")),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
onPressed: () => close(),
|
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => close(true),
|
|
||||||
child: Text(translate("OK"))),
|
|
||||||
],
|
],
|
||||||
));
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
if (res == true) {
|
if (res == true) {
|
||||||
stopService();
|
stopService();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final res = await parent.target?.dialogManager
|
final res =
|
||||||
.show<bool>((setState, close) => CustomAlertDialog(
|
await parent.target?.dialogManager.show<bool>((setState, close) {
|
||||||
|
submit() => close(true);
|
||||||
|
return CustomAlertDialog(
|
||||||
title: Row(children: [
|
title: Row(children: [
|
||||||
Icon(Icons.warning_amber_sharp,
|
const Icon(Icons.warning_amber_sharp,
|
||||||
color: Colors.redAccent, size: 28),
|
color: Colors.redAccent, size: 28),
|
||||||
SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(translate("Warning")),
|
Text(translate("Warning")),
|
||||||
]),
|
]),
|
||||||
content: Text(translate("android_service_will_start_tip")),
|
content: Text(translate("android_service_will_start_tip")),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
onPressed: () => close(),
|
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
|
||||||
child: Text(translate("Cancel"))),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => close(true),
|
|
||||||
child: Text(translate("OK"))),
|
|
||||||
],
|
],
|
||||||
));
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
if (res == true) {
|
if (res == true) {
|
||||||
startService();
|
startService();
|
||||||
}
|
}
|
||||||
@ -300,7 +303,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fetchID() async {
|
_fetchID() async {
|
||||||
final old = _serverId.text;
|
final old = _serverId.id;
|
||||||
var count = 0;
|
var count = 0;
|
||||||
const maxCount = 10;
|
const maxCount = 10;
|
||||||
while (count < maxCount) {
|
while (count < maxCount) {
|
||||||
@ -309,12 +312,12 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (id.isEmpty) {
|
if (id.isEmpty) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
_serverId.text = id;
|
_serverId.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint("fetch id again at $count:id:${_serverId.text}");
|
debugPrint("fetch id again at $count:id:${_serverId.id}");
|
||||||
count++;
|
count++;
|
||||||
if (_serverId.text != old) {
|
if (_serverId.id != old) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -344,23 +347,21 @@ class ServerModel with ChangeNotifier {
|
|||||||
|
|
||||||
// force
|
// force
|
||||||
updateClientState([String? json]) async {
|
updateClientState([String? json]) async {
|
||||||
var res = await bind.mainGetClientsState();
|
var res = await bind.cmGetClientsState();
|
||||||
try {
|
try {
|
||||||
final List clientsJson = jsonDecode(res);
|
final List clientsJson = jsonDecode(res);
|
||||||
if (isDesktop && clientsJson.isEmpty && _clients.isNotEmpty) {
|
|
||||||
// exit cm when >1 peers to no peers
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
_clients.clear();
|
_clients.clear();
|
||||||
tabController.state.value.tabs.clear();
|
tabController.state.value.tabs.clear();
|
||||||
for (var clientJson in clientsJson) {
|
for (var clientJson in clientsJson) {
|
||||||
final client = Client.fromJson(clientJson);
|
final client = Client.fromJson(clientJson);
|
||||||
_clients.add(client);
|
_clients.add(client);
|
||||||
tabController.add(TabInfo(
|
tabController.add(
|
||||||
|
TabInfo(
|
||||||
key: client.id.toString(),
|
key: client.id.toString(),
|
||||||
label: client.name,
|
label: client.name,
|
||||||
closable: false,
|
closable: false,
|
||||||
page: Desktop.buildConnectionCard(client)));
|
page: Desktop.buildConnectionCard(client)),
|
||||||
|
authorized: client.authorized);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -368,9 +369,27 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void loginRequest(Map<String, dynamic> evt) {
|
void addConnection(Map<String, dynamic> evt) {
|
||||||
try {
|
try {
|
||||||
final client = Client.fromJson(jsonDecode(evt["client"]));
|
final client = Client.fromJson(jsonDecode(evt["client"]));
|
||||||
|
if (client.authorized) {
|
||||||
|
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id));
|
||||||
|
final index = _clients.indexWhere((c) => c.id == client.id);
|
||||||
|
if (index < 0) {
|
||||||
|
_clients.add(client);
|
||||||
|
} else {
|
||||||
|
_clients[index].authorized = true;
|
||||||
|
}
|
||||||
|
tabController.add(
|
||||||
|
TabInfo(
|
||||||
|
key: client.id.toString(),
|
||||||
|
label: client.name,
|
||||||
|
closable: false,
|
||||||
|
page: Desktop.buildConnectionCard(client)),
|
||||||
|
authorized: true);
|
||||||
|
scrollToBottom();
|
||||||
|
notifyListeners();
|
||||||
|
} else {
|
||||||
if (_clients.any((c) => c.id == client.id)) {
|
if (_clients.any((c) => c.id == client.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -383,25 +402,34 @@ class ServerModel with ChangeNotifier {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
if (isAndroid) showLoginDialog(client);
|
if (isAndroid) showLoginDialog(client);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to call loginRequest,error:$e");
|
debugPrint("Failed to call loginRequest,error:$e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showLoginDialog(Client client) {
|
void showLoginDialog(Client client) {
|
||||||
parent.target?.dialogManager.show(
|
parent.target?.dialogManager.show((setState, close) {
|
||||||
(setState, close) => CustomAlertDialog(
|
cancel() {
|
||||||
title: Row(
|
sendLoginResponse(client, false);
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
close();
|
||||||
children: [
|
}
|
||||||
Text(translate(client.isFileTransfer
|
|
||||||
? "File Connection"
|
submit() {
|
||||||
: "Screen Connection")),
|
sendLoginResponse(client, true);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title:
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
|
Text(translate(
|
||||||
|
client.isFileTransfer ? "File Connection" : "Screen Connection")),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.close))
|
icon: const Icon(Icons.close))
|
||||||
]),
|
]),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -412,26 +440,18 @@ class ServerModel with ChangeNotifier {
|
|||||||
clientInfo(client),
|
clientInfo(client),
|
||||||
Text(
|
Text(
|
||||||
translate("android_new_connection_tip"),
|
translate("android_new_connection_tip"),
|
||||||
style: TextStyle(color: Colors.black54),
|
style: const TextStyle(color: Colors.black54),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(onPressed: cancel, child: Text(translate("Dismiss"))),
|
||||||
child: Text(translate("Dismiss")),
|
ElevatedButton(onPressed: submit, child: Text(translate("Accept"))),
|
||||||
onPressed: () {
|
|
||||||
sendLoginResponse(client, false);
|
|
||||||
close();
|
|
||||||
}),
|
|
||||||
ElevatedButton(
|
|
||||||
child: Text(translate("Accept")),
|
|
||||||
onPressed: () {
|
|
||||||
sendLoginResponse(client, true);
|
|
||||||
close();
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
onSubmit: submit,
|
||||||
tag: getLoginDialogTag(client.id));
|
onCancel: cancel,
|
||||||
|
);
|
||||||
|
}, tag: getLoginDialogTag(client.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
@ -471,14 +491,18 @@ class ServerModel with ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
_clients[index].authorized = true;
|
_clients[index].authorized = true;
|
||||||
}
|
}
|
||||||
tabController.add(TabInfo(
|
tabController.add(
|
||||||
|
TabInfo(
|
||||||
key: client.id.toString(),
|
key: client.id.toString(),
|
||||||
label: client.name,
|
label: client.name,
|
||||||
closable: false,
|
closable: false,
|
||||||
page: Desktop.buildConnectionCard(client)));
|
page: Desktop.buildConnectionCard(client)),
|
||||||
|
authorized: true);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
debugPrint("onClientAuthorized:$e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onClientRemove(Map<String, dynamic> evt) {
|
void onClientRemove(Map<String, dynamic> evt) {
|
||||||
@ -486,8 +510,10 @@ class ServerModel with ChangeNotifier {
|
|||||||
final id = int.parse(evt['id'] as String);
|
final id = int.parse(evt['id'] as String);
|
||||||
if (_clients.any((c) => c.id == id)) {
|
if (_clients.any((c) => c.id == id)) {
|
||||||
final index = _clients.indexWhere((client) => client.id == id);
|
final index = _clients.indexWhere((client) => client.id == id);
|
||||||
|
if (index >= 0) {
|
||||||
_clients.removeAt(index);
|
_clients.removeAt(index);
|
||||||
tabController.remove(index);
|
tabController.remove(index);
|
||||||
|
}
|
||||||
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
|
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
|
||||||
parent.target?.invokeMethod("cancel_notification", id);
|
parent.target?.invokeMethod("cancel_notification", id);
|
||||||
}
|
}
|
||||||
@ -558,24 +584,29 @@ String getLoginDialogTag(int id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showInputWarnAlert(FFI ffi) {
|
showInputWarnAlert(FFI ffi) {
|
||||||
ffi.dialogManager.show((setState, close) => CustomAlertDialog(
|
ffi.dialogManager.show((setState, close) {
|
||||||
|
submit() {
|
||||||
|
ffi.serverModel.initInput();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
title: Text(translate("How to get Android input permission?")),
|
title: Text(translate("How to get Android input permission?")),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(translate("android_input_permission_tip1")),
|
Text(translate("android_input_permission_tip1")),
|
||||||
SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(translate("android_input_permission_tip2")),
|
Text(translate("android_input_permission_tip2")),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(child: Text(translate("Cancel")), onPressed: close),
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
child: Text(translate("Open System Setting")),
|
onPressed: submit, child: Text(translate("Open System Setting"))),
|
||||||
onPressed: () {
|
|
||||||
ffi.serverModel.initInput();
|
|
||||||
close();
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
));
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,8 @@ static void my_application_activate(GApplication* application) {
|
|||||||
MyApplication* self = MY_APPLICATION(application);
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
GtkWindow* window =
|
GtkWindow* window =
|
||||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
|
// we have custom window frame
|
||||||
|
gtk_window_set_decorated(window, FALSE);
|
||||||
|
|
||||||
// Use a header bar when running in GNOME as this is the common style used
|
// Use a header bar when running in GNOME as this is the common style used
|
||||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||||
|
|||||||
@ -62,7 +62,7 @@ dependencies:
|
|||||||
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: e0368a023ba195462acc00d33ab361b499f0e413
|
ref: fee851fa43116e0b91c39acd0ec37063dc6015f8
|
||||||
freezed_annotation: ^2.0.3
|
freezed_annotation: ^2.0.3
|
||||||
tray_manager:
|
tray_manager:
|
||||||
git:
|
git:
|
||||||
|
|||||||
@ -1666,6 +1666,7 @@ pub async fn handle_login_from_ui(
|
|||||||
/// Interface for client to send data and commands.
|
/// Interface for client to send data and commands.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Interface: Send + Clone + 'static + Sized {
|
pub trait Interface: Send + Clone + 'static + Sized {
|
||||||
|
/// Send message data to remote peer.
|
||||||
fn send(&self, data: Data);
|
fn send(&self, data: Data);
|
||||||
fn msgbox(&self, msgtype: &str, title: &str, text: &str);
|
fn msgbox(&self, msgtype: &str, title: &str, text: &str);
|
||||||
fn handle_login_error(&mut self, err: &str) -> bool;
|
fn handle_login_error(&mut self, err: &str) -> bool;
|
||||||
|
|||||||
@ -22,9 +22,9 @@ pub trait FileManager: Interface {
|
|||||||
|
|
||||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||||
fn read_dir(&self, path: &str, include_hidden: bool) -> String {
|
fn read_dir(&self, path: &str, include_hidden: bool) -> String {
|
||||||
use crate::common::make_fd_to_json;
|
use crate::flutter::make_fd_to_json;
|
||||||
match fs::read_dir(&fs::get_path(path), include_hidden) {
|
match fs::read_dir(&fs::get_path(path), include_hidden) {
|
||||||
Ok(fd) => make_fd_to_json(fd),
|
Ok(fd) => make_fd_to_json(fd.id, fd.path, &fd.entries),
|
||||||
Err(_) => "".into(),
|
Err(_) => "".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,17 +2,11 @@ use crate::client::{
|
|||||||
Client, CodecFormat, FileManager, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
|
Client, CodecFormat, FileManager, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
|
||||||
SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED,
|
SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED,
|
||||||
};
|
};
|
||||||
|
use crate::common;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
|
use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
|
||||||
|
|
||||||
#[cfg(windows)]
|
use crate::ui_session_interface::{InvokeUiSession, Session};
|
||||||
use clipboard::{
|
|
||||||
cliprdr::CliprdrClientContext, create_cliprdr_context as create_clipboard_file_context,
|
|
||||||
get_rx_clip_client, server_clip_file,
|
|
||||||
};
|
|
||||||
#[cfg(windows)]
|
|
||||||
use crate::clipboard_file::*;
|
|
||||||
use crate::ui_session_interface::{InvokeUi, Session};
|
|
||||||
use crate::{client::Data, client::Interface};
|
use crate::{client::Data, client::Interface};
|
||||||
|
|
||||||
use hbb_common::config::{PeerConfig, TransferSerde};
|
use hbb_common::config::{PeerConfig, TransferSerde};
|
||||||
@ -28,14 +22,14 @@ use hbb_common::tokio::{
|
|||||||
sync::mpsc,
|
sync::mpsc,
|
||||||
time::{self, Duration, Instant, Interval},
|
time::{self, Duration, Instant, Interval},
|
||||||
};
|
};
|
||||||
use hbb_common::{allow_err, message_proto::*};
|
use hbb_common::{allow_err, message_proto::*, sleep};
|
||||||
use hbb_common::{fs, log, Stream};
|
use hbb_common::{fs, log, Stream};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
pub struct Remote<T: InvokeUi> {
|
pub struct Remote<T: InvokeUiSession> {
|
||||||
handler: Session<T>,
|
handler: Session<T>,
|
||||||
video_sender: MediaSender,
|
video_sender: MediaSender,
|
||||||
audio_sender: MediaSender,
|
audio_sender: MediaSender,
|
||||||
@ -49,13 +43,13 @@ pub struct Remote<T: InvokeUi> {
|
|||||||
last_update_jobs_status: (Instant, HashMap<i32, u64>),
|
last_update_jobs_status: (Instant, HashMap<i32, u64>),
|
||||||
first_frame: bool,
|
first_frame: bool,
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
clipboard_file_context: Option<Box<CliprdrClientContext>>,
|
clipboard_file_context: Option<Box<clipboard::cliprdr::CliprdrClientContext>>,
|
||||||
data_count: Arc<AtomicUsize>,
|
data_count: Arc<AtomicUsize>,
|
||||||
frame_count: Arc<AtomicUsize>,
|
frame_count: Arc<AtomicUsize>,
|
||||||
video_format: CodecFormat,
|
video_format: CodecFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: InvokeUi> Remote<T> {
|
impl<T: InvokeUiSession> Remote<T> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
handler: Session<T>,
|
handler: Session<T>,
|
||||||
video_sender: MediaSender,
|
video_sender: MediaSender,
|
||||||
@ -113,7 +107,7 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::<i32>();
|
let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::<i32>();
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let mut rx_clip_client = get_rx_clip_client().lock().await;
|
let mut rx_clip_client = clipboard::get_rx_clip_client().lock().await;
|
||||||
|
|
||||||
let mut status_timer = time::interval(Duration::new(1, 0));
|
let mut status_timer = time::interval(Duration::new(1, 0));
|
||||||
|
|
||||||
@ -159,7 +153,7 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
match _msg {
|
match _msg {
|
||||||
Some((_, clip)) => {
|
Some((_, clip)) => {
|
||||||
allow_err!(peer.send(&clip_2_msg(clip)).await);
|
allow_err!(peer.send(&crate::clipboard_file::clip_2_msg(clip)).await);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// unreachable!()
|
// unreachable!()
|
||||||
@ -275,51 +269,6 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
Some(tx)
|
Some(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn load_last_jobs(&mut self) {
|
|
||||||
log::info!("start load last jobs");
|
|
||||||
self.handler.clear_all_jobs();
|
|
||||||
let pc = self.handler.load_config();
|
|
||||||
if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() {
|
|
||||||
// no last jobs
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: can add a confirm dialog
|
|
||||||
let mut cnt = 1;
|
|
||||||
for job_str in pc.transfer.read_jobs.iter() {
|
|
||||||
let job: Result<TransferJobMeta, serde_json::Error> = serde_json::from_str(&job_str);
|
|
||||||
if let Ok(job) = job {
|
|
||||||
self.handler.add_job(
|
|
||||||
cnt,
|
|
||||||
job.to.clone(),
|
|
||||||
job.remote.clone(),
|
|
||||||
job.file_num,
|
|
||||||
job.show_hidden,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
cnt += 1;
|
|
||||||
println!("restore read_job: {:?}", job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for job_str in pc.transfer.write_jobs.iter() {
|
|
||||||
let job: Result<TransferJobMeta, serde_json::Error> = serde_json::from_str(&job_str);
|
|
||||||
if let Ok(job) = job {
|
|
||||||
self.handler.add_job(
|
|
||||||
cnt,
|
|
||||||
job.remote.clone(),
|
|
||||||
job.to.clone(),
|
|
||||||
job.file_num,
|
|
||||||
job.show_hidden,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
cnt += 1;
|
|
||||||
println!("restore write_job: {:?}", job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.handler.update_transfer_list();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool {
|
async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool {
|
||||||
match data {
|
match data {
|
||||||
Data::Close => {
|
Data::Close => {
|
||||||
@ -381,8 +330,13 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
to,
|
to,
|
||||||
job.files().len()
|
job.files().len()
|
||||||
);
|
);
|
||||||
// let m = make_fd(job.id(), job.files(), true);
|
self.handler.update_folder_files(
|
||||||
// self.handler.call("updateFolderFiles", &make_args!(m)); // TODO
|
job.id(),
|
||||||
|
job.files(),
|
||||||
|
path,
|
||||||
|
!is_remote,
|
||||||
|
true,
|
||||||
|
);
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let files = job.files().clone();
|
let files = job.files().clone();
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@ -441,8 +395,13 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
to,
|
to,
|
||||||
job.files().len()
|
job.files().len()
|
||||||
);
|
);
|
||||||
// let m = make_fd(job.id(), job.files(), true);
|
self.handler.update_folder_files(
|
||||||
// self.handler.call("updateFolderFiles", &make_args!(m));
|
job.id(),
|
||||||
|
job.files(),
|
||||||
|
path,
|
||||||
|
!is_remote,
|
||||||
|
true,
|
||||||
|
);
|
||||||
job.is_last_job = true;
|
job.is_last_job = true;
|
||||||
self.read_jobs.push(job);
|
self.read_jobs.push(job);
|
||||||
self.timer = time::interval(MILLI1);
|
self.timer = time::interval(MILLI1);
|
||||||
@ -493,7 +452,6 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
file_num,
|
file_num,
|
||||||
job.files[i].name.clone(),
|
job.files[i].name.clone(),
|
||||||
);
|
);
|
||||||
self.handler.confirm_delete_files(id, file_num);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -554,8 +512,13 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
} else {
|
} else {
|
||||||
match fs::get_recursive_files(&path, include_hidden) {
|
match fs::get_recursive_files(&path, include_hidden) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
// let m = make_fd(id, &entries, true);
|
self.handler.update_folder_files(
|
||||||
// self.handler.call("updateFolderFiles", &make_args!(m));
|
id,
|
||||||
|
&entries,
|
||||||
|
path.clone(),
|
||||||
|
!is_remote,
|
||||||
|
false,
|
||||||
|
);
|
||||||
self.remove_jobs
|
self.remove_jobs
|
||||||
.insert(id, RemoveJob::new(entries, path, sep, is_remote));
|
.insert(id, RemoveJob::new(entries, path, sep, is_remote));
|
||||||
}
|
}
|
||||||
@ -757,28 +720,28 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
}
|
}
|
||||||
Some(login_response::Union::PeerInfo(pi)) => {
|
Some(login_response::Union::PeerInfo(pi)) => {
|
||||||
self.handler.handle_peer_info(pi);
|
self.handler.handle_peer_info(pi);
|
||||||
// self.check_clipboard_file_context();
|
self.check_clipboard_file_context();
|
||||||
// if !(self.handler.is_file_transfer()
|
if !(self.handler.is_file_transfer()
|
||||||
// || self.handler.is_port_forward()
|
|| self.handler.is_port_forward()
|
||||||
// || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst)
|
|| !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst)
|
||||||
// || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst)
|
|| !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst)
|
||||||
// || self.handler.lc.read().unwrap().disable_clipboard)
|
|| self.handler.lc.read().unwrap().disable_clipboard)
|
||||||
// {
|
{
|
||||||
// let txt = self.old_clipboard.lock().unwrap().clone();
|
let txt = self.old_clipboard.lock().unwrap().clone();
|
||||||
// if !txt.is_empty() {
|
if !txt.is_empty() {
|
||||||
// let msg_out = crate::create_clipboard_msg(txt);
|
let msg_out = crate::create_clipboard_msg(txt);
|
||||||
// let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
// tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// // due to clipboard service interval time
|
// due to clipboard service interval time
|
||||||
// sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await;
|
sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await;
|
||||||
// sender.send(Data::Message(msg_out)).ok();
|
sender.send(Data::Message(msg_out)).ok();
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if self.handler.is_file_transfer() {
|
if self.handler.is_file_transfer() {
|
||||||
// self.load_last_jobs().await;
|
self.handler.load_last_jobs();
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
@ -812,8 +775,8 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
Some(message::Union::Cliprdr(clip)) => {
|
Some(message::Union::Cliprdr(clip)) => {
|
||||||
if !self.handler.lc.read().unwrap().disable_clipboard {
|
if !self.handler.lc.read().unwrap().disable_clipboard {
|
||||||
if let Some(context) = &mut self.clipboard_file_context {
|
if let Some(context) = &mut self.clipboard_file_context {
|
||||||
if let Some(clip) = msg_2_clip(clip) {
|
if let Some(clip) = crate::clipboard_file::msg_2_clip(clip) {
|
||||||
server_clip_file(context, 0, clip);
|
clipboard::server_clip_file(context, 0, clip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -831,11 +794,13 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
fs::transform_windows_path(&mut entries);
|
fs::transform_windows_path(&mut entries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// let mut m = make_fd(fd.id, &entries, fd.id > 0);
|
self.handler.update_folder_files(
|
||||||
// if fd.id <= 0 {
|
fd.id,
|
||||||
// m.set_item("path", fd.path);
|
&entries,
|
||||||
// }
|
fd.path,
|
||||||
// self.handler.call("updateFolderFiles", &make_args!(m));
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) {
|
if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) {
|
||||||
log::info!("job set_files: {:?}", entries);
|
log::info!("job set_files: {:?}", entries);
|
||||||
job.set_files(entries);
|
job.set_files(entries);
|
||||||
@ -1163,7 +1128,7 @@ impl<T: InvokeUi> Remote<T> {
|
|||||||
&& self.handler.lc.read().unwrap().enable_file_transfer;
|
&& self.handler.lc.read().unwrap().enable_file_transfer;
|
||||||
if enabled == self.clipboard_file_context.is_none() {
|
if enabled == self.clipboard_file_context.is_none() {
|
||||||
self.clipboard_file_context = if enabled {
|
self.clipboard_file_context = if enabled {
|
||||||
match create_clipboard_file_context(true, false) {
|
match clipboard::create_cliprdr_context(true, false) {
|
||||||
Ok(context) => {
|
Ok(context) => {
|
||||||
log::info!("clipboard context for file transfer created.");
|
log::info!("clipboard context for file transfer created.");
|
||||||
Some(context)
|
Some(context)
|
||||||
|
|||||||
@ -667,51 +667,6 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess
|
|||||||
msg_out
|
msg_out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_fd_to_json(fd: FileDirectory) -> String {
|
|
||||||
let mut fd_json = serde_json::Map::new();
|
|
||||||
fd_json.insert("id".into(), json!(fd.id));
|
|
||||||
fd_json.insert("path".into(), json!(fd.path));
|
|
||||||
|
|
||||||
let mut entries = vec![];
|
|
||||||
for entry in fd.entries {
|
|
||||||
let mut entry_map = serde_json::Map::new();
|
|
||||||
entry_map.insert("entry_type".into(), json!(entry.entry_type.value()));
|
|
||||||
entry_map.insert("name".into(), json!(entry.name));
|
|
||||||
entry_map.insert("size".into(), json!(entry.size));
|
|
||||||
entry_map.insert("modified_time".into(), json!(entry.modified_time));
|
|
||||||
entries.push(entry_map);
|
|
||||||
}
|
|
||||||
fd_json.insert("entries".into(), json!(entries));
|
|
||||||
serde_json::to_string(&fd_json).unwrap_or("".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn make_fd_flutter(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> String {
|
|
||||||
let mut m = serde_json::Map::new();
|
|
||||||
m.insert("id".into(), json!(id));
|
|
||||||
let mut a = vec![];
|
|
||||||
let mut n: u64 = 0;
|
|
||||||
for entry in entries {
|
|
||||||
n += entry.size;
|
|
||||||
if only_count {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut e = serde_json::Map::new();
|
|
||||||
e.insert("name".into(), json!(entry.name.to_owned()));
|
|
||||||
let tmp = entry.entry_type.value();
|
|
||||||
e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp }));
|
|
||||||
e.insert("time".into(), json!(entry.modified_time as f64));
|
|
||||||
e.insert("size".into(), json!(entry.size as f64));
|
|
||||||
a.push(e);
|
|
||||||
}
|
|
||||||
if only_count {
|
|
||||||
m.insert("num_entries".into(), json!(entries.len() as i32));
|
|
||||||
} else {
|
|
||||||
m.insert("entries".into(), json!(a));
|
|
||||||
}
|
|
||||||
m.insert("total_size".into(), json!(n as f64));
|
|
||||||
serde_json::to_string(&m).unwrap_or("".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref IS_X11: Mutex<bool> = Mutex::new(false);
|
pub static ref IS_X11: Mutex<bool> = Mutex::new(false);
|
||||||
|
|||||||
769
src/flutter.rs
769
src/flutter.rs
@ -5,16 +5,17 @@ use std::{
|
|||||||
|
|
||||||
use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer};
|
use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer};
|
||||||
|
|
||||||
use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType, rendezvous_proto::ConnType};
|
use hbb_common::{
|
||||||
|
bail, config::LocalConfig, message_proto::*, rendezvous_proto::ConnType, ResultType,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::ui_session_interface::{io_loop, InvokeUi, Session};
|
use crate::ui_session_interface::{io_loop, InvokeUiSession, Session};
|
||||||
|
|
||||||
use crate::{client::*, flutter_ffi::EventToUI};
|
use crate::{client::*, flutter_ffi::EventToUI};
|
||||||
|
|
||||||
pub(super) const APP_TYPE_MAIN: &str = "main";
|
pub(super) const APP_TYPE_MAIN: &str = "main";
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote";
|
pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote";
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer";
|
pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer";
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
@ -46,7 +47,7 @@ impl FlutterHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InvokeUi for FlutterHandler {
|
impl InvokeUiSession for FlutterHandler {
|
||||||
fn set_cursor_data(&self, cd: CursorData) {
|
fn set_cursor_data(&self, cd: CursorData) {
|
||||||
let colors = hbb_common::compress::decompress(&cd.colors);
|
let colors = hbb_common::compress::decompress(&cd.colors);
|
||||||
self.push_event(
|
self.push_event(
|
||||||
@ -87,6 +88,7 @@ impl InvokeUi for FlutterHandler {
|
|||||||
self.push_event("permission", vec![(name, &value.to_string())]);
|
self.push_event("permission", vec![(name, &value.to_string())]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unused in flutter
|
||||||
fn close_success(&self) {}
|
fn close_success(&self) {}
|
||||||
|
|
||||||
fn update_quality_status(&self, status: QualityStatus) {
|
fn update_quality_status(&self, status: QualityStatus) {
|
||||||
@ -119,8 +121,15 @@ impl InvokeUi for FlutterHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn job_error(&self, id: i32, err: String, _file_num: i32) {
|
fn job_error(&self, id: i32, err: String, file_num: i32) {
|
||||||
self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]);
|
self.push_event(
|
||||||
|
"job_error",
|
||||||
|
vec![
|
||||||
|
("id", &id.to_string()),
|
||||||
|
("err", &err),
|
||||||
|
("file_num", &file_num.to_string()),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn job_done(&self, id: i32, file_num: i32) {
|
fn job_done(&self, id: i32, file_num: i32) {
|
||||||
@ -130,31 +139,43 @@ impl InvokeUi for FlutterHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_all_jobs(&self) {
|
// unused in flutter
|
||||||
// todo!()
|
fn clear_all_jobs(&self) {}
|
||||||
|
|
||||||
|
fn load_last_job(&self, _cnt: i32, job_json: &str) {
|
||||||
|
self.push_event("load_last_job", vec![("value", job_json)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
fn update_folder_files(
|
||||||
fn add_job(
|
|
||||||
&self,
|
&self,
|
||||||
id: i32,
|
id: i32,
|
||||||
|
entries: &Vec<FileEntry>,
|
||||||
path: String,
|
path: String,
|
||||||
to: String,
|
is_local: bool,
|
||||||
file_num: i32,
|
only_count: bool,
|
||||||
show_hidden: bool,
|
|
||||||
is_remote: bool,
|
|
||||||
) {
|
) {
|
||||||
// todo!()
|
// TODO opt
|
||||||
|
if only_count {
|
||||||
|
self.push_event(
|
||||||
|
"update_folder_files",
|
||||||
|
vec![("info", &make_fd_flutter(id, entries, only_count))],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.push_event(
|
||||||
|
"file_dir",
|
||||||
|
vec![
|
||||||
|
("value", &make_fd_to_json(id, path, entries)),
|
||||||
|
("is_local", "false"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_transfer_list(&self) {
|
// unused in flutter
|
||||||
// todo!()
|
fn update_transfer_list(&self) {}
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
// unused in flutter // TEST flutter
|
||||||
fn confirm_delete_files(&self, id: i32, i: i32, name: String) {
|
fn confirm_delete_files(&self, _id: i32, _i: i32, _name: String) {}
|
||||||
// todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) {
|
fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) {
|
||||||
self.push_event(
|
self.push_event(
|
||||||
@ -180,6 +201,7 @@ impl InvokeUi for FlutterHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unused in flutter
|
||||||
fn adapt_size(&self) {}
|
fn adapt_size(&self) {}
|
||||||
|
|
||||||
fn on_rgba(&self, data: &[u8]) {
|
fn on_rgba(&self, data: &[u8]) {
|
||||||
@ -287,11 +309,7 @@ pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> R
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.initialize(session_id, conn_type);
|
.initialize(session_id, conn_type);
|
||||||
|
|
||||||
if let Some(same_id_session) = SESSIONS
|
if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) {
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.insert(id.to_owned(), session)
|
|
||||||
{
|
|
||||||
same_id_session.close();
|
same_id_session.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,306 +338,48 @@ pub fn session_start_(id: &str, event_stream: StreamSink<EventToUI>) -> ResultTy
|
|||||||
// Server Side
|
// Server Side
|
||||||
#[cfg(not(any(target_os = "ios")))]
|
#[cfg(not(any(target_os = "ios")))]
|
||||||
pub mod connection_manager {
|
pub mod connection_manager {
|
||||||
use std::{
|
use std::collections::HashMap;
|
||||||
collections::HashMap,
|
|
||||||
iter::FromIterator,
|
|
||||||
sync::{
|
|
||||||
atomic::{AtomicI64, Ordering},
|
|
||||||
RwLock,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde_derive::Serialize;
|
use hbb_common::log;
|
||||||
|
|
||||||
use hbb_common::{
|
|
||||||
allow_err,
|
|
||||||
config::Config,
|
|
||||||
fs::is_write_need_confirmation,
|
|
||||||
fs::{self, get_string, new_send_confirm, DigestCheckResult},
|
|
||||||
log,
|
|
||||||
message_proto::*,
|
|
||||||
protobuf::Message as _,
|
|
||||||
tokio::{
|
|
||||||
self,
|
|
||||||
sync::mpsc::{self, UnboundedSender},
|
|
||||||
task::spawn_blocking,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
#[cfg(any(target_os = "android"))]
|
#[cfg(any(target_os = "android"))]
|
||||||
use scrap::android::call_main_service_set_by_name;
|
use scrap::android::call_main_service_set_by_name;
|
||||||
|
|
||||||
use crate::ipc::Data;
|
use crate::ui_cm_interface::InvokeUiCM;
|
||||||
use crate::ipc::{self, new_listener, Connection};
|
|
||||||
|
|
||||||
use super::GLOBAL_EVENT_STREAM;
|
use super::GLOBAL_EVENT_STREAM;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Clone)]
|
||||||
struct Client {
|
struct FlutterHandler {}
|
||||||
id: i32,
|
|
||||||
pub authorized: bool,
|
|
||||||
is_file_transfer: bool,
|
|
||||||
name: String,
|
|
||||||
peer_id: String,
|
|
||||||
keyboard: bool,
|
|
||||||
clipboard: bool,
|
|
||||||
audio: bool,
|
|
||||||
file: bool,
|
|
||||||
restart: bool,
|
|
||||||
#[serde(skip)]
|
|
||||||
tx: UnboundedSender<Data>,
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
impl InvokeUiCM for FlutterHandler {
|
||||||
static ref CLIENTS: RwLock<HashMap<i32,Client>> = Default::default();
|
//TODO port_forward
|
||||||
}
|
fn add_connection(&self, client: &crate::ui_cm_interface::Client) {
|
||||||
|
|
||||||
static CLICK_TIME: AtomicI64 = AtomicI64::new(0);
|
|
||||||
|
|
||||||
// // TODO clipboard_file
|
|
||||||
// enum ClipboardFileData {
|
|
||||||
// #[cfg(windows)]
|
|
||||||
// Clip((i32, ipc::ClipbaordFile)),
|
|
||||||
// Enable((i32, bool)),
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
||||||
pub fn start_listen_ipc_thread() {
|
|
||||||
std::thread::spawn(move || start_ipc());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
async fn start_ipc() {
|
|
||||||
// TODO clipboard_file
|
|
||||||
// let (tx_file, _rx_file) = mpsc::unbounded_channel::<ClipboardFileData>();
|
|
||||||
// #[cfg(windows)]
|
|
||||||
// let cm_clip = cm.clone();
|
|
||||||
// #[cfg(windows)]
|
|
||||||
// std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file));
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
log::info!("try create privacy mode window");
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
if let Err(e) = crate::platform::windows::check_update_broker_process() {
|
|
||||||
log::warn!(
|
|
||||||
"Failed to check update broker process. Privacy mode may not work properly. {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allow_err!(crate::ui::win_privacy::start());
|
|
||||||
});
|
|
||||||
|
|
||||||
match new_listener("_cm").await {
|
|
||||||
Ok(mut incoming) => {
|
|
||||||
while let Some(result) = incoming.next().await {
|
|
||||||
match result {
|
|
||||||
Ok(stream) => {
|
|
||||||
log::debug!("Got new connection");
|
|
||||||
let mut stream = Connection::new(stream);
|
|
||||||
// let tx_file = tx_file.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
// for tmp use, without real conn id
|
|
||||||
let conn_id_tmp = -1;
|
|
||||||
let mut conn_id: i32 = 0;
|
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel::<Data>();
|
|
||||||
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
res = stream.next() => {
|
|
||||||
match res {
|
|
||||||
Err(err) => {
|
|
||||||
log::info!("cm ipc connection closed: {}", err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(Some(data)) => {
|
|
||||||
match data {
|
|
||||||
Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart} => {
|
|
||||||
log::debug!("conn_id: {}", id);
|
|
||||||
conn_id = id;
|
|
||||||
// tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok();
|
|
||||||
on_login(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone());
|
|
||||||
}
|
|
||||||
Data::Close => {
|
|
||||||
// tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok();
|
|
||||||
log::info!("cm ipc connection closed from connection request");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Data::PrivacyModeState((_, _)) => {
|
|
||||||
conn_id = conn_id_tmp;
|
|
||||||
allow_err!(tx.send(data));
|
|
||||||
}
|
|
||||||
Data::ClickTime(ms) => {
|
|
||||||
CLICK_TIME.store(ms, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
Data::ChatMessage { text } => {
|
|
||||||
handle_chat(conn_id, text);
|
|
||||||
}
|
|
||||||
Data::FS(fs) => {
|
|
||||||
handle_fs(fs, &mut write_jobs, &tx).await;
|
|
||||||
}
|
|
||||||
// TODO ClipbaordFile
|
|
||||||
// #[cfg(windows)]
|
|
||||||
// Data::ClipbaordFile(_clip) => {
|
|
||||||
// tx_file
|
|
||||||
// .send(ClipboardFileData::Clip((id, _clip)))
|
|
||||||
// .ok();
|
|
||||||
// }
|
|
||||||
// #[cfg(windows)]
|
|
||||||
// Data::ClipboardFileEnabled(enabled) => {
|
|
||||||
// tx_file
|
|
||||||
// .send(ClipboardFileData::Enable((id, enabled)))
|
|
||||||
// .ok();
|
|
||||||
// }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(data) = rx.recv() => {
|
|
||||||
if stream.send(&data).await.is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if conn_id != conn_id_tmp {
|
|
||||||
remove_connection(conn_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("Couldn't get cm client: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("Failed to start cm ipc server: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// crate::platform::quit_gui();
|
|
||||||
// TODO flutter quit_gui
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
pub fn start_channel(rx: UnboundedReceiver<Data>, tx: UnboundedSender<Data>) {
|
|
||||||
std::thread::spawn(move || start_listen(rx, tx));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
async fn start_listen(mut rx: UnboundedReceiver<Data>, tx: UnboundedSender<Data>) {
|
|
||||||
let mut current_id = 0;
|
|
||||||
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
|
|
||||||
loop {
|
|
||||||
match rx.recv().await {
|
|
||||||
Some(Data::Login {
|
|
||||||
id,
|
|
||||||
is_file_transfer,
|
|
||||||
port_forward,
|
|
||||||
peer_id,
|
|
||||||
name,
|
|
||||||
authorized,
|
|
||||||
keyboard,
|
|
||||||
clipboard,
|
|
||||||
audio,
|
|
||||||
file,
|
|
||||||
restart,
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
current_id = id;
|
|
||||||
on_login(
|
|
||||||
id,
|
|
||||||
is_file_transfer,
|
|
||||||
port_forward,
|
|
||||||
peer_id,
|
|
||||||
name,
|
|
||||||
authorized,
|
|
||||||
keyboard,
|
|
||||||
clipboard,
|
|
||||||
audio,
|
|
||||||
file,
|
|
||||||
restart,
|
|
||||||
tx.clone(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some(Data::ChatMessage { text }) => {
|
|
||||||
handle_chat(current_id, text);
|
|
||||||
}
|
|
||||||
Some(Data::FS(fs)) => {
|
|
||||||
handle_fs(fs, &mut write_jobs, &tx).await;
|
|
||||||
}
|
|
||||||
Some(Data::Close) => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remove_connection(current_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_login(
|
|
||||||
id: i32,
|
|
||||||
is_file_transfer: bool,
|
|
||||||
_port_forward: String,
|
|
||||||
peer_id: String,
|
|
||||||
name: String,
|
|
||||||
authorized: bool,
|
|
||||||
keyboard: bool,
|
|
||||||
clipboard: bool,
|
|
||||||
audio: bool,
|
|
||||||
file: bool,
|
|
||||||
restart: bool,
|
|
||||||
tx: mpsc::UnboundedSender<Data>,
|
|
||||||
) {
|
|
||||||
let mut client = Client {
|
|
||||||
id,
|
|
||||||
authorized,
|
|
||||||
is_file_transfer,
|
|
||||||
name: name.clone(),
|
|
||||||
peer_id: peer_id.clone(),
|
|
||||||
keyboard,
|
|
||||||
clipboard,
|
|
||||||
audio,
|
|
||||||
file,
|
|
||||||
restart,
|
|
||||||
tx,
|
|
||||||
};
|
|
||||||
if authorized {
|
|
||||||
client.authorized = true;
|
|
||||||
let client_json = serde_json::to_string(&client).unwrap_or("".into());
|
let client_json = serde_json::to_string(&client).unwrap_or("".into());
|
||||||
// send to Android service, active notification no matter UI is shown or not.
|
// send to Android service, active notification no matter UI is shown or not.
|
||||||
#[cfg(any(target_os = "android"))]
|
#[cfg(any(target_os = "android"))]
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
call_main_service_set_by_name("on_client_authorized", Some(&client_json), None)
|
call_main_service_set_by_name("add_connection", Some(&client_json), None)
|
||||||
{
|
{
|
||||||
log::debug!("call_service_set_by_name fail,{}", e);
|
log::debug!("call_service_set_by_name fail,{}", e);
|
||||||
}
|
}
|
||||||
// send to UI, refresh widget
|
// send to UI, refresh widget
|
||||||
push_event("on_client_authorized", vec![("client", &client_json)]);
|
self.push_event("add_connection", vec![("client", &client_json)]); // TODO use add_connection
|
||||||
} else {
|
|
||||||
let client_json = serde_json::to_string(&client).unwrap_or("".into());
|
|
||||||
// send to Android service, active notification no matter UI is shown or not.
|
|
||||||
#[cfg(any(target_os = "android"))]
|
|
||||||
if let Err(e) =
|
|
||||||
call_main_service_set_by_name("try_start_without_auth", Some(&client_json), None)
|
|
||||||
{
|
|
||||||
log::debug!("call_service_set_by_name fail,{}", e);
|
|
||||||
}
|
|
||||||
// send to UI, refresh widget
|
|
||||||
push_event("try_start_without_auth", vec![("client", &client_json)]);
|
|
||||||
}
|
|
||||||
CLIENTS.write().unwrap().insert(id, client);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_event(name: &str, event: Vec<(&str, &str)>) {
|
fn remove_connection(&self, id: i32) {
|
||||||
|
self.push_event("on_client_remove", vec![("id", &id.to_string())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_message(&self, id: i32, text: String) {
|
||||||
|
self.push_event(
|
||||||
|
"chat_server_mode",
|
||||||
|
vec![("id", &id.to_string()), ("text", &text)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlutterHandler {
|
||||||
|
fn push_event(&self, name: &str, event: Vec<(&str, &str)>) {
|
||||||
let mut h: HashMap<&str, &str> = event.iter().cloned().collect();
|
let mut h: HashMap<&str, &str> = event.iter().cloned().collect();
|
||||||
assert!(h.get("name").is_none());
|
assert!(h.get("name").is_none());
|
||||||
h.insert("name", name);
|
h.insert("name", name);
|
||||||
@ -632,312 +392,34 @@ pub mod connection_manager {
|
|||||||
s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned()));
|
s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned()));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_click_time() -> i64 {
|
|
||||||
CLICK_TIME.load(Ordering::SeqCst)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_click_time(id: i32) {
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
pub fn start_listen_ipc_thread() {
|
||||||
allow_err!(client.tx.send(Data::ClickTime(0)));
|
use crate::ui_cm_interface::{start_ipc, ConnectionManager};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
std::thread::spawn(crate::ipc::start_pa);
|
||||||
|
|
||||||
|
let cm = ConnectionManager {
|
||||||
|
ui_handler: FlutterHandler {},
|
||||||
};
|
};
|
||||||
|
std::thread::spawn(move || start_ipc(cm));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn switch_permission(id: i32, name: String, enabled: bool) {
|
#[cfg(target_os = "android")]
|
||||||
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
use hbb_common::tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
allow_err!(client.tx.send(Data::SwitchPermission { name, enabled }));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clients_state() -> String {
|
#[cfg(target_os = "android")]
|
||||||
let clients = CLIENTS.read().unwrap();
|
pub fn start_channel(
|
||||||
let res = Vec::from_iter(clients.values().cloned());
|
rx: UnboundedReceiver<crate::ipc::Data>,
|
||||||
serde_json::to_string(&res).unwrap_or("".into())
|
tx: UnboundedSender<crate::ipc::Data>,
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_clients_length() -> usize {
|
|
||||||
let clients = CLIENTS.read().unwrap();
|
|
||||||
clients.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close_conn(id: i32) {
|
|
||||||
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
|
||||||
allow_err!(client.tx.send(Data::Close));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_login_res(id: i32, res: bool) {
|
|
||||||
if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
|
|
||||||
if res {
|
|
||||||
allow_err!(client.tx.send(Data::Authorize));
|
|
||||||
client.authorized = true;
|
|
||||||
} else {
|
|
||||||
allow_err!(client.tx.send(Data::Close));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_connection(id: i32) {
|
|
||||||
let mut clients = CLIENTS.write().unwrap();
|
|
||||||
clients.remove(&id);
|
|
||||||
|
|
||||||
if clients
|
|
||||||
.iter()
|
|
||||||
.filter(|(_k, v)| !v.is_file_transfer)
|
|
||||||
.next()
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
#[cfg(any(target_os = "android"))]
|
|
||||||
if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) {
|
|
||||||
log::debug!("stop_capture err:{}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
push_event("on_client_remove", vec![("id", &id.to_string())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// server mode handle chat from other peers
|
|
||||||
fn handle_chat(id: i32, text: String) {
|
|
||||||
push_event(
|
|
||||||
"chat_server_mode",
|
|
||||||
vec![("id", &id.to_string()), ("text", &text)],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// server mode send chat to peer
|
|
||||||
pub fn send_chat(id: i32, text: String) {
|
|
||||||
let clients = CLIENTS.read().unwrap();
|
|
||||||
if let Some(client) = clients.get(&id) {
|
|
||||||
allow_err!(client.tx.send(Data::ChatMessage { text }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle FS server
|
|
||||||
async fn handle_fs(
|
|
||||||
fs: ipc::FS,
|
|
||||||
write_jobs: &mut Vec<fs::TransferJob>,
|
|
||||||
tx: &UnboundedSender<Data>,
|
|
||||||
) {
|
) {
|
||||||
match fs {
|
use crate::ui_cm_interface::start_listen;
|
||||||
ipc::FS::ReadDir {
|
let cm = crate::ui_cm_interface::ConnectionManager {
|
||||||
dir,
|
ui_handler: FlutterHandler {},
|
||||||
include_hidden,
|
|
||||||
} => {
|
|
||||||
read_dir(&dir, include_hidden, tx).await;
|
|
||||||
}
|
|
||||||
ipc::FS::RemoveDir {
|
|
||||||
path,
|
|
||||||
id,
|
|
||||||
recursive,
|
|
||||||
} => {
|
|
||||||
remove_dir(path, id, recursive, tx).await;
|
|
||||||
}
|
|
||||||
ipc::FS::RemoveFile { path, id, file_num } => {
|
|
||||||
remove_file(path, id, file_num, tx).await;
|
|
||||||
}
|
|
||||||
ipc::FS::CreateDir { path, id } => {
|
|
||||||
create_dir(path, id, tx).await;
|
|
||||||
}
|
|
||||||
ipc::FS::NewWrite {
|
|
||||||
path,
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
mut files,
|
|
||||||
overwrite_detection,
|
|
||||||
} => {
|
|
||||||
write_jobs.push(fs::TransferJob::new_write(
|
|
||||||
id,
|
|
||||||
"".to_string(),
|
|
||||||
path,
|
|
||||||
file_num,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
files
|
|
||||||
.drain(..)
|
|
||||||
.map(|f| FileEntry {
|
|
||||||
name: f.0,
|
|
||||||
modified_time: f.1,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
overwrite_detection,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
ipc::FS::CancelWrite { id } => {
|
|
||||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
|
||||||
job.remove_download_file();
|
|
||||||
fs::remove_job(id, write_jobs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ipc::FS::WriteDone { id, file_num } => {
|
|
||||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
|
||||||
job.modify_time();
|
|
||||||
send_raw(fs::new_done(id, file_num), tx);
|
|
||||||
fs::remove_job(id, write_jobs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ipc::FS::WriteBlock {
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
data,
|
|
||||||
compressed,
|
|
||||||
} => {
|
|
||||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
|
||||||
if let Err(err) = job
|
|
||||||
.write(
|
|
||||||
FileTransferBlock {
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
data,
|
|
||||||
compressed,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
send_raw(fs::new_error(id, err, file_num), &tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ipc::FS::CheckDigest {
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
file_size,
|
|
||||||
last_modified,
|
|
||||||
is_upload,
|
|
||||||
} => {
|
|
||||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
|
||||||
let mut req = FileTransferSendConfirmRequest {
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)),
|
|
||||||
..Default::default()
|
|
||||||
};
|
};
|
||||||
let digest = FileTransferDigest {
|
std::thread::spawn(move || start_listen(cm, rx, tx));
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
last_modified,
|
|
||||||
file_size,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
if let Some(file) = job.files().get(file_num as usize) {
|
|
||||||
let path = get_string(&job.join(&file.name));
|
|
||||||
match is_write_need_confirmation(&path, &digest) {
|
|
||||||
Ok(digest_result) => {
|
|
||||||
match digest_result {
|
|
||||||
DigestCheckResult::IsSame => {
|
|
||||||
req.set_skip(true);
|
|
||||||
let msg_out = new_send_confirm(req);
|
|
||||||
send_raw(msg_out, &tx);
|
|
||||||
}
|
|
||||||
DigestCheckResult::NeedConfirm(mut digest) => {
|
|
||||||
// upload to server, but server has the same file, request
|
|
||||||
digest.is_upload = is_upload;
|
|
||||||
let mut msg_out = Message::new();
|
|
||||||
let mut fr = FileResponse::new();
|
|
||||||
fr.set_digest(digest);
|
|
||||||
msg_out.set_file_response(fr);
|
|
||||||
send_raw(msg_out, &tx);
|
|
||||||
}
|
|
||||||
DigestCheckResult::NoSuchFile => {
|
|
||||||
let msg_out = new_send_confirm(req);
|
|
||||||
send_raw(msg_out, &tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
send_raw(fs::new_error(id, err, file_num), &tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender<Data>) {
|
|
||||||
let path = {
|
|
||||||
if dir.is_empty() {
|
|
||||||
Config::get_home()
|
|
||||||
} else {
|
|
||||||
fs::get_path(dir)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await {
|
|
||||||
let mut msg_out = Message::new();
|
|
||||||
let mut file_response = FileResponse::new();
|
|
||||||
file_response.set_dir(fd);
|
|
||||||
msg_out.set_file_response(file_response);
|
|
||||||
send_raw(msg_out, tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_result<F: std::fmt::Display, S: std::fmt::Display>(
|
|
||||||
res: std::result::Result<std::result::Result<(), F>, S>,
|
|
||||||
id: i32,
|
|
||||||
file_num: i32,
|
|
||||||
tx: &UnboundedSender<Data>,
|
|
||||||
) {
|
|
||||||
match res {
|
|
||||||
Err(err) => {
|
|
||||||
send_raw(fs::new_error(id, err, file_num), tx);
|
|
||||||
}
|
|
||||||
Ok(Err(err)) => {
|
|
||||||
send_raw(fs::new_error(id, err, file_num), tx);
|
|
||||||
}
|
|
||||||
Ok(Ok(())) => {
|
|
||||||
send_raw(fs::new_done(id, file_num), tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender<Data>) {
|
|
||||||
handle_result(
|
|
||||||
spawn_blocking(move || fs::remove_file(&path)).await,
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
tx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_dir(path: String, id: i32, tx: &UnboundedSender<Data>) {
|
|
||||||
handle_result(
|
|
||||||
spawn_blocking(move || fs::create_dir(&path)).await,
|
|
||||||
id,
|
|
||||||
0,
|
|
||||||
tx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender<Data>) {
|
|
||||||
let path = fs::get_path(&path);
|
|
||||||
handle_result(
|
|
||||||
spawn_blocking(move || {
|
|
||||||
if recursive {
|
|
||||||
fs::remove_all_empty_dir(&path)
|
|
||||||
} else {
|
|
||||||
std::fs::remove_dir(&path).map_err(|err| err.into())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await,
|
|
||||||
id,
|
|
||||||
0,
|
|
||||||
tx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_raw(msg: Message, tx: &UnboundedSender<Data>) {
|
|
||||||
match msg.write_to_bytes() {
|
|
||||||
Ok(bytes) => {
|
|
||||||
allow_err!(tx.send(Data::RawMessage(bytes)));
|
|
||||||
}
|
|
||||||
err => allow_err!(err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -950,30 +432,47 @@ pub fn get_session_id(id: String) -> String {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// async fn start_one_port_forward(
|
pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> String {
|
||||||
// handler: Session,
|
let mut fd_json = serde_json::Map::new();
|
||||||
// port: i32,
|
fd_json.insert("id".into(), json!(id));
|
||||||
// remote_host: String,
|
fd_json.insert("path".into(), json!(path));
|
||||||
// remote_port: i32,
|
|
||||||
// receiver: mpsc::UnboundedReceiver<Data>,
|
let mut entries_out = vec![];
|
||||||
// key: &str,
|
for entry in entries {
|
||||||
// token: &str,
|
let mut entry_map = serde_json::Map::new();
|
||||||
// ) {
|
entry_map.insert("entry_type".into(), json!(entry.entry_type.value()));
|
||||||
// if let Err(err) = crate::port_forward::listen(
|
entry_map.insert("name".into(), json!(entry.name));
|
||||||
// handler.id.clone(),
|
entry_map.insert("size".into(), json!(entry.size));
|
||||||
// String::new(), // TODO
|
entry_map.insert("modified_time".into(), json!(entry.modified_time));
|
||||||
// port,
|
entries_out.push(entry_map);
|
||||||
// handler.clone(),
|
}
|
||||||
// receiver,
|
fd_json.insert("entries".into(), json!(entries_out));
|
||||||
// key,
|
serde_json::to_string(&fd_json).unwrap_or("".into())
|
||||||
// token,
|
}
|
||||||
// handler.lc.clone(),
|
|
||||||
// remote_host,
|
pub fn make_fd_flutter(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> String {
|
||||||
// remote_port,
|
let mut m = serde_json::Map::new();
|
||||||
// )
|
m.insert("id".into(), json!(id));
|
||||||
// .await
|
let mut a = vec![];
|
||||||
// {
|
let mut n: u64 = 0;
|
||||||
// handler.on_error(&format!("Failed to listen on {}: {}", port, err));
|
for entry in entries {
|
||||||
// }
|
n += entry.size;
|
||||||
// log::info!("port forward (:{}) exit", port);
|
if only_count {
|
||||||
// }
|
continue;
|
||||||
|
}
|
||||||
|
let mut e = serde_json::Map::new();
|
||||||
|
e.insert("name".into(), json!(entry.name.to_owned()));
|
||||||
|
let tmp = entry.entry_type.value();
|
||||||
|
e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp }));
|
||||||
|
e.insert("time".into(), json!(entry.modified_time as f64));
|
||||||
|
e.insert("size".into(), json!(entry.size as f64));
|
||||||
|
a.push(e);
|
||||||
|
}
|
||||||
|
if only_count {
|
||||||
|
m.insert("num_entries".into(), json!(entries.len() as i32));
|
||||||
|
} else {
|
||||||
|
m.insert("entries".into(), json!(a));
|
||||||
|
}
|
||||||
|
m.insert("total_size".into(), json!(n as f64));
|
||||||
|
serde_json::to_string(&m).unwrap_or("".into())
|
||||||
|
}
|
||||||
@ -13,8 +13,6 @@ use hbb_common::{
|
|||||||
fs, log,
|
fs, log,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::common::make_fd_to_json;
|
|
||||||
use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state};
|
|
||||||
use crate::flutter::{self, SESSIONS};
|
use crate::flutter::{self, SESSIONS};
|
||||||
use crate::start_server;
|
use crate::start_server;
|
||||||
use crate::ui_interface;
|
use crate::ui_interface;
|
||||||
@ -31,7 +29,7 @@ use crate::ui_interface::{
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
client::file_trait::FileManager,
|
client::file_trait::FileManager,
|
||||||
flutter::{session_add, session_start_},
|
flutter::{make_fd_to_json, session_add, session_start_},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn initialize(app_dir: &str) {
|
fn initialize(app_dir: &str) {
|
||||||
@ -388,10 +386,8 @@ pub fn session_create_dir(id: String, act_id: i32, path: String, is_remote: bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) -> String {
|
pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) -> String {
|
||||||
if let Some(_) = SESSIONS.read().unwrap().get(&id) {
|
|
||||||
if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) {
|
if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) {
|
||||||
return make_fd_to_json(fd);
|
return make_fd_to_json(fd.id, path, &fd.entries);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"".to_string()
|
"".to_string()
|
||||||
}
|
}
|
||||||
@ -404,8 +400,8 @@ 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(_) = 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!(
|
||||||
@ -711,13 +707,13 @@ pub fn main_get_online_statue() -> i64 {
|
|||||||
ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone()
|
ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_get_clients_state() -> String {
|
pub fn cm_get_clients_state() -> String {
|
||||||
get_clients_state()
|
crate::ui_cm_interface::get_clients_state()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_check_clients_length(length: usize) -> Option<String> {
|
pub fn cm_check_clients_length(length: usize) -> Option<String> {
|
||||||
if length != get_clients_length() {
|
if length != crate::ui_cm_interface::get_clients_length() {
|
||||||
Some(get_clients_state())
|
Some(crate::ui_cm_interface::get_clients_state())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -793,7 +789,7 @@ pub fn main_set_home_dir(home: String) {
|
|||||||
pub fn main_stop_service() {
|
pub fn main_stop_service() {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
Config::set_option("stop-service".into(), "Y".into());
|
config::Config::set_option("stop-service".into(), "Y".into());
|
||||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -801,7 +797,7 @@ pub fn main_stop_service() {
|
|||||||
pub fn main_start_service() {
|
pub fn main_start_service() {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
Config::set_option("stop-service".into(), "".into());
|
config::Config::set_option("stop-service".into(), "".into());
|
||||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
@ -829,27 +825,31 @@ pub fn main_get_mouse_time() -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
crate::ui_cm_interface::send_chat(conn_id, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cm_login_res(conn_id: i32, res: bool) {
|
pub fn cm_login_res(conn_id: i32, res: bool) {
|
||||||
connection_manager::on_login_res(conn_id, res);
|
if res {
|
||||||
|
crate::ui_cm_interface::authorize(conn_id);
|
||||||
|
} else {
|
||||||
|
crate::ui_cm_interface::close(conn_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cm_close_connection(conn_id: i32) {
|
pub fn cm_close_connection(conn_id: i32) {
|
||||||
connection_manager::close_conn(conn_id);
|
crate::ui_cm_interface::close(conn_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cm_check_click_time(conn_id: i32) {
|
pub fn cm_check_click_time(conn_id: i32) {
|
||||||
connection_manager::check_click_time(conn_id)
|
crate::ui_cm_interface::check_click_time(conn_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cm_get_click_time() -> f64 {
|
pub fn cm_get_click_time() -> f64 {
|
||||||
connection_manager::get_click_time() as _
|
crate::ui_cm_interface::get_click_time() as _
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) {
|
pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) {
|
||||||
connection_manager::switch_permission(conn_id, name, enabled)
|
crate::ui_cm_interface::switch_permission(conn_id, name, enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main_get_icon() -> String {
|
pub fn main_get_icon() -> String {
|
||||||
|
|||||||
@ -49,6 +49,7 @@ mod tray;
|
|||||||
|
|
||||||
mod ui_interface;
|
mod ui_interface;
|
||||||
mod ui_session_interface;
|
mod ui_session_interface;
|
||||||
|
mod ui_cm_interface;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub mod clipboard_file;
|
pub mod clipboard_file;
|
||||||
|
|||||||
100
src/ui.rs
100
src/ui.rs
@ -14,7 +14,6 @@ use hbb_common::{
|
|||||||
log,
|
log,
|
||||||
protobuf::Message as _,
|
protobuf::Message as _,
|
||||||
rendezvous_proto::*,
|
rendezvous_proto::*,
|
||||||
sleep,
|
|
||||||
tcp::FramedStream,
|
tcp::FramedStream,
|
||||||
tokio::{self, sync::mpsc, time},
|
tokio::{self, sync::mpsc, time},
|
||||||
};
|
};
|
||||||
@ -87,7 +86,7 @@ pub fn start(args: &mut [String]) {
|
|||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
if args.len() > 0 && args[0] == "--tray" {
|
if args.len() > 0 && args[0] == "--tray" {
|
||||||
let options = check_connect_status(false).1;
|
let options = crate::ui_interface::check_connect_status(false).1;
|
||||||
crate::tray::start_tray(options);
|
crate::tray::start_tray(options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -125,7 +124,7 @@ pub fn start(args: &mut [String]) {
|
|||||||
page = "install.html";
|
page = "install.html";
|
||||||
} else if args[0] == "--cm" {
|
} else if args[0] == "--cm" {
|
||||||
frame.register_behavior("connection-manager", move || {
|
frame.register_behavior("connection-manager", move || {
|
||||||
Box::new(cm::ConnectionManager::new())
|
Box::new(cm::SciterConnectionManager::new())
|
||||||
});
|
});
|
||||||
page = "cm.html";
|
page = "cm.html";
|
||||||
} else if (args[0] == "--connect"
|
} else if (args[0] == "--connect"
|
||||||
@ -664,79 +663,6 @@ pub fn check_zombie(childs: Childs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// notice: avoiding create ipc connecton repeatly,
|
|
||||||
// because windows named pipe has serious memory leak issue.
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
async fn check_connect_status_(
|
|
||||||
reconnect: bool,
|
|
||||||
status: Arc<Mutex<Status>>,
|
|
||||||
options: Arc<Mutex<HashMap<String, String>>>,
|
|
||||||
rx: mpsc::UnboundedReceiver<ipc::Data>,
|
|
||||||
password: Arc<Mutex<String>>,
|
|
||||||
) {
|
|
||||||
let mut key_confirmed = false;
|
|
||||||
let mut rx = rx;
|
|
||||||
let mut mouse_time = 0;
|
|
||||||
let mut id = "".to_owned();
|
|
||||||
loop {
|
|
||||||
if let Ok(mut c) = ipc::connect(1000, "").await {
|
|
||||||
let mut timer = time::interval(time::Duration::from_secs(1));
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
res = c.next() => {
|
|
||||||
match res {
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("ipc connection closed: {}", err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(Some(ipc::Data::MouseMoveTime(v))) => {
|
|
||||||
mouse_time = v;
|
|
||||||
status.lock().unwrap().2 = v;
|
|
||||||
}
|
|
||||||
Ok(Some(ipc::Data::Options(Some(v)))) => {
|
|
||||||
*options.lock().unwrap() = v
|
|
||||||
}
|
|
||||||
Ok(Some(ipc::Data::Config((name, Some(value))))) => {
|
|
||||||
if name == "id" {
|
|
||||||
id = value;
|
|
||||||
} else if name == "temporary-password" {
|
|
||||||
*password.lock().unwrap() = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => {
|
|
||||||
if x > 0 {
|
|
||||||
x = 1
|
|
||||||
}
|
|
||||||
key_confirmed = c;
|
|
||||||
*status.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(data) = rx.recv() => {
|
|
||||||
allow_err!(c.send(&data).await);
|
|
||||||
}
|
|
||||||
_ = timer.tick() => {
|
|
||||||
c.send(&ipc::Data::OnlineStatus(None)).await.ok();
|
|
||||||
c.send(&ipc::Data::Options(None)).await.ok();
|
|
||||||
c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok();
|
|
||||||
c.send(&ipc::Data::Config(("temporary-password".to_owned(), None))).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !reconnect {
|
|
||||||
options
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.insert("ipc-closed".to_owned(), "Y".to_owned());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
*status.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone());
|
|
||||||
sleep(1.).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
fn get_sound_inputs() -> Vec<String> {
|
fn get_sound_inputs() -> Vec<String> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
@ -763,28 +689,6 @@ fn get_sound_inputs() -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn check_connect_status(
|
|
||||||
reconnect: bool,
|
|
||||||
) -> (
|
|
||||||
Arc<Mutex<Status>>,
|
|
||||||
Arc<Mutex<HashMap<String, String>>>,
|
|
||||||
mpsc::UnboundedSender<ipc::Data>,
|
|
||||||
Arc<Mutex<String>>,
|
|
||||||
) {
|
|
||||||
let status = Arc::new(Mutex::new((0, false, 0, "".to_owned())));
|
|
||||||
let options = Arc::new(Mutex::new(Config::get_options()));
|
|
||||||
let cloned = status.clone();
|
|
||||||
let cloned_options = options.clone();
|
|
||||||
let (tx, rx) = mpsc::unbounded_channel::<ipc::Data>();
|
|
||||||
let password = Arc::new(Mutex::new(String::default()));
|
|
||||||
let cloned_password = password.clone();
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
check_connect_status_(reconnect, cloned, cloned_options, rx, cloned_password)
|
|
||||||
});
|
|
||||||
(status, options, tx, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
const INVALID_FORMAT: &'static str = "Invalid format";
|
const INVALID_FORMAT: &'static str = "Invalid format";
|
||||||
const UNKNOWN_ERROR: &'static str = "Unknown error";
|
const UNKNOWN_ERROR: &'static str = "Unknown error";
|
||||||
|
|
||||||
|
|||||||
620
src/ui/cm.rs
620
src/ui/cm.rs
@ -1,59 +1,83 @@
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use crate::ipc::start_pa;
|
use crate::ipc::start_pa;
|
||||||
use crate::ipc::{self, new_listener, Connection, Data};
|
use crate::ui_cm_interface::{start_ipc, ConnectionManager, InvokeUiCM};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use clipboard::{
|
use clipboard::{
|
||||||
create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled,
|
create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled,
|
||||||
};
|
};
|
||||||
use hbb_common::fs::{
|
|
||||||
get_string, is_write_need_confirmation, new_send_confirm,
|
|
||||||
DigestCheckResult,
|
|
||||||
};
|
|
||||||
use hbb_common::{
|
|
||||||
allow_err,
|
|
||||||
config::Config,
|
|
||||||
fs, log,
|
|
||||||
message_proto::*,
|
|
||||||
protobuf::Message as _,
|
|
||||||
tokio::{self, sync::mpsc, task::spawn_blocking},
|
|
||||||
};
|
|
||||||
use sciter::{make_args, Element, Value, HELEMENT};
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
ops::Deref,
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ConnectionManagerInner {
|
use hbb_common::{allow_err, log};
|
||||||
root: Option<Element>,
|
use sciter::{make_args, Element, Value, HELEMENT};
|
||||||
senders: HashMap<i32, mpsc::UnboundedSender<Data>>,
|
use std::sync::Mutex;
|
||||||
click_time: i64,
|
use std::{ops::Deref, sync::Arc};
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct SciterHandler {
|
||||||
|
pub element: Arc<Mutex<Option<Element>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
impl InvokeUiCM for SciterHandler {
|
||||||
pub struct ConnectionManager(Arc<RwLock<ConnectionManagerInner>>);
|
fn add_connection(&self, client: &crate::ui_cm_interface::Client) {
|
||||||
|
self.call(
|
||||||
|
"addConnection",
|
||||||
|
&make_args!(
|
||||||
|
client.id,
|
||||||
|
client.is_file_transfer,
|
||||||
|
client.port_forward.clone(),
|
||||||
|
client.peer_id.clone(),
|
||||||
|
client.name.clone(),
|
||||||
|
client.authorized,
|
||||||
|
client.keyboard,
|
||||||
|
client.clipboard,
|
||||||
|
client.audio,
|
||||||
|
client.file,
|
||||||
|
client.restart
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for ConnectionManager {
|
fn remove_connection(&self, id: i32) {
|
||||||
type Target = Arc<RwLock<ConnectionManagerInner>>;
|
self.call("removeConnection", &make_args!(id));
|
||||||
|
if crate::ui_cm_interface::get_clients_length().eq(&0) {
|
||||||
|
crate::platform::quit_gui();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_message(&self, id: i32, text: String) {
|
||||||
|
self.call("newMessage", &make_args!(id, text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SciterHandler {
|
||||||
|
#[inline]
|
||||||
|
fn call(&self, func: &str, args: &[Value]) {
|
||||||
|
if let Some(e) = self.element.lock().unwrap().as_ref() {
|
||||||
|
allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SciterConnectionManager(ConnectionManager<SciterHandler>);
|
||||||
|
|
||||||
|
impl Deref for SciterConnectionManager {
|
||||||
|
type Target = ConnectionManager<SciterHandler>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectionManager {
|
impl SciterConnectionManager {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
std::thread::spawn(start_pa);
|
std::thread::spawn(start_pa);
|
||||||
let inner = ConnectionManagerInner {
|
let cm = ConnectionManager {
|
||||||
root: None,
|
ui_handler: SciterHandler::default(),
|
||||||
senders: HashMap::new(),
|
|
||||||
click_time: Default::default(),
|
|
||||||
};
|
};
|
||||||
let cm = Self(Arc::new(RwLock::new(inner)));
|
|
||||||
let cloned = cm.clone();
|
let cloned = cm.clone();
|
||||||
std::thread::spawn(move || start_ipc(cloned));
|
std::thread::spawn(move || start_ipc(cloned));
|
||||||
cm
|
SciterConnectionManager(cm)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_icon(&mut self) -> String {
|
fn get_icon(&mut self) -> String {
|
||||||
@ -61,359 +85,27 @@ impl ConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_click_time(&mut self, id: i32) {
|
fn check_click_time(&mut self, id: i32) {
|
||||||
let lock = self.read().unwrap();
|
crate::ui_cm_interface::check_click_time(id);
|
||||||
if let Some(s) = lock.senders.get(&id) {
|
|
||||||
allow_err!(s.send(Data::ClickTime(0)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_click_time(&self) -> f64 {
|
fn get_click_time(&self) -> f64 {
|
||||||
self.read().unwrap().click_time as _
|
crate::ui_cm_interface::get_click_time() as _
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn call(&self, func: &str, args: &[Value]) {
|
|
||||||
let r = self.read().unwrap();
|
|
||||||
if let Some(ref e) = r.root {
|
|
||||||
allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_connection(
|
|
||||||
&self,
|
|
||||||
id: i32,
|
|
||||||
is_file_transfer: bool,
|
|
||||||
port_forward: String,
|
|
||||||
peer_id: String,
|
|
||||||
name: String,
|
|
||||||
authorized: bool,
|
|
||||||
keyboard: bool,
|
|
||||||
clipboard: bool,
|
|
||||||
audio: bool,
|
|
||||||
file: bool,
|
|
||||||
restart: bool,
|
|
||||||
tx: mpsc::UnboundedSender<Data>,
|
|
||||||
) {
|
|
||||||
self.call(
|
|
||||||
"addConnection",
|
|
||||||
&make_args!(
|
|
||||||
id,
|
|
||||||
is_file_transfer,
|
|
||||||
port_forward,
|
|
||||||
peer_id,
|
|
||||||
name,
|
|
||||||
authorized,
|
|
||||||
keyboard,
|
|
||||||
clipboard,
|
|
||||||
audio,
|
|
||||||
file,
|
|
||||||
restart
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.write().unwrap().senders.insert(id, tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_connection(&self, id: i32) {
|
|
||||||
self.write().unwrap().senders.remove(&id);
|
|
||||||
if self.read().unwrap().senders.len() == 0 {
|
|
||||||
crate::platform::quit_gui();
|
|
||||||
}
|
|
||||||
self.call("removeConnection", &make_args!(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_data(
|
|
||||||
&self,
|
|
||||||
id: i32,
|
|
||||||
data: Data,
|
|
||||||
_tx_clip_file: &mpsc::UnboundedSender<ClipboardFileData>,
|
|
||||||
write_jobs: &mut Vec<fs::TransferJob>,
|
|
||||||
conn: &mut Connection,
|
|
||||||
) {
|
|
||||||
match data {
|
|
||||||
Data::ChatMessage { text } => {
|
|
||||||
self.call("newMessage", &make_args!(id, text));
|
|
||||||
}
|
|
||||||
Data::ClickTime(ms) => {
|
|
||||||
self.write().unwrap().click_time = ms;
|
|
||||||
}
|
|
||||||
Data::FS(v) => match v {
|
|
||||||
ipc::FS::ReadDir {
|
|
||||||
dir,
|
|
||||||
include_hidden,
|
|
||||||
} => {
|
|
||||||
Self::read_dir(&dir, include_hidden, conn).await;
|
|
||||||
}
|
|
||||||
ipc::FS::RemoveDir {
|
|
||||||
path,
|
|
||||||
id,
|
|
||||||
recursive,
|
|
||||||
} => {
|
|
||||||
Self::remove_dir(path, id, recursive, conn).await;
|
|
||||||
}
|
|
||||||
ipc::FS::RemoveFile { path, id, file_num } => {
|
|
||||||
Self::remove_file(path, id, file_num, conn).await;
|
|
||||||
}
|
|
||||||
ipc::FS::CreateDir { path, id } => {
|
|
||||||
Self::create_dir(path, id, conn).await;
|
|
||||||
}
|
|
||||||
ipc::FS::NewWrite {
|
|
||||||
path,
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
mut files,
|
|
||||||
overwrite_detection,
|
|
||||||
} => {
|
|
||||||
// cm has no show_hidden context
|
|
||||||
// dummy remote, show_hidden, is_remote
|
|
||||||
write_jobs.push(fs::TransferJob::new_write(
|
|
||||||
id,
|
|
||||||
"".to_string(),
|
|
||||||
path,
|
|
||||||
file_num,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
files
|
|
||||||
.drain(..)
|
|
||||||
.map(|f| FileEntry {
|
|
||||||
name: f.0,
|
|
||||||
modified_time: f.1,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
overwrite_detection,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
ipc::FS::CancelWrite { id } => {
|
|
||||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
|
||||||
job.remove_download_file();
|
|
||||||
fs::remove_job(id, write_jobs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ipc::FS::WriteDone { id, file_num } => {
|
|
||||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
|
||||||
job.modify_time();
|
|
||||||
Self::send(fs::new_done(id, file_num), conn).await;
|
|
||||||
fs::remove_job(id, write_jobs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ipc::FS::CheckDigest {
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
file_size,
|
|
||||||
last_modified,
|
|
||||||
is_upload,
|
|
||||||
} => {
|
|
||||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
|
||||||
let mut req = FileTransferSendConfirmRequest {
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let digest = FileTransferDigest {
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
last_modified,
|
|
||||||
file_size,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
if let Some(file) = job.files().get(file_num as usize) {
|
|
||||||
let path = get_string(&job.join(&file.name));
|
|
||||||
match is_write_need_confirmation(&path, &digest) {
|
|
||||||
Ok(digest_result) => {
|
|
||||||
match digest_result {
|
|
||||||
DigestCheckResult::IsSame => {
|
|
||||||
req.set_skip(true);
|
|
||||||
let msg_out = new_send_confirm(req);
|
|
||||||
Self::send(msg_out, conn).await;
|
|
||||||
}
|
|
||||||
DigestCheckResult::NeedConfirm(mut digest) => {
|
|
||||||
// upload to server, but server has the same file, request
|
|
||||||
digest.is_upload = is_upload;
|
|
||||||
let mut msg_out = Message::new();
|
|
||||||
let mut fr = FileResponse::new();
|
|
||||||
fr.set_digest(digest);
|
|
||||||
msg_out.set_file_response(fr);
|
|
||||||
Self::send(msg_out, conn).await;
|
|
||||||
}
|
|
||||||
DigestCheckResult::NoSuchFile => {
|
|
||||||
let msg_out = new_send_confirm(req);
|
|
||||||
Self::send(msg_out, conn).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
Self::send(fs::new_error(id, err, file_num), conn).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ipc::FS::WriteBlock {
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
data,
|
|
||||||
compressed,
|
|
||||||
} => {
|
|
||||||
let raw = if let Ok(bytes) = conn.next_raw().await {
|
|
||||||
Some(bytes)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if let Some(job) = fs::get_job(id, write_jobs) {
|
|
||||||
if let Err(err) = job
|
|
||||||
.write(
|
|
||||||
FileTransferBlock {
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
data,
|
|
||||||
compressed,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
raw.as_ref().map(|x| &x[..]),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Self::send(fs::new_error(id, err, file_num), conn).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ipc::FS::WriteOffset {
|
|
||||||
id: _,
|
|
||||||
file_num: _,
|
|
||||||
offset_blk: _,
|
|
||||||
} => {}
|
|
||||||
},
|
|
||||||
#[cfg(windows)]
|
|
||||||
Data::ClipbaordFile(_clip) => {
|
|
||||||
_tx_clip_file
|
|
||||||
.send(ClipboardFileData::Clip((id, _clip)))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
#[cfg(windows)]
|
|
||||||
Data::ClipboardFileEnabled(enabled) => {
|
|
||||||
_tx_clip_file
|
|
||||||
.send(ClipboardFileData::Enable((id, enabled)))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_dir(dir: &str, include_hidden: bool, conn: &mut Connection) {
|
|
||||||
let path = {
|
|
||||||
if dir.is_empty() {
|
|
||||||
Config::get_home()
|
|
||||||
} else {
|
|
||||||
fs::get_path(dir)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await {
|
|
||||||
let mut msg_out = Message::new();
|
|
||||||
let mut file_response = FileResponse::new();
|
|
||||||
file_response.set_dir(fd);
|
|
||||||
msg_out.set_file_response(file_response);
|
|
||||||
Self::send(msg_out, conn).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_result<F: std::fmt::Display, S: std::fmt::Display>(
|
|
||||||
res: std::result::Result<std::result::Result<(), F>, S>,
|
|
||||||
id: i32,
|
|
||||||
file_num: i32,
|
|
||||||
conn: &mut Connection,
|
|
||||||
) {
|
|
||||||
match res {
|
|
||||||
Err(err) => {
|
|
||||||
Self::send(fs::new_error(id, err, file_num), conn).await;
|
|
||||||
}
|
|
||||||
Ok(Err(err)) => {
|
|
||||||
Self::send(fs::new_error(id, err, file_num), conn).await;
|
|
||||||
}
|
|
||||||
Ok(Ok(())) => {
|
|
||||||
Self::send(fs::new_done(id, file_num), conn).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_file(path: String, id: i32, file_num: i32, conn: &mut Connection) {
|
|
||||||
Self::handle_result(
|
|
||||||
spawn_blocking(move || fs::remove_file(&path)).await,
|
|
||||||
id,
|
|
||||||
file_num,
|
|
||||||
conn,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_dir(path: String, id: i32, conn: &mut Connection) {
|
|
||||||
Self::handle_result(
|
|
||||||
spawn_blocking(move || fs::create_dir(&path)).await,
|
|
||||||
id,
|
|
||||||
0,
|
|
||||||
conn,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_dir(path: String, id: i32, recursive: bool, conn: &mut Connection) {
|
|
||||||
let path = fs::get_path(&path);
|
|
||||||
Self::handle_result(
|
|
||||||
spawn_blocking(move || {
|
|
||||||
if recursive {
|
|
||||||
fs::remove_all_empty_dir(&path)
|
|
||||||
} else {
|
|
||||||
std::fs::remove_dir(&path).map_err(|err| err.into())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await,
|
|
||||||
id,
|
|
||||||
0,
|
|
||||||
conn,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send(msg: Message, conn: &mut Connection) {
|
|
||||||
match msg.write_to_bytes() {
|
|
||||||
Ok(bytes) => allow_err!(conn.send(&Data::RawMessage(bytes)).await),
|
|
||||||
err => allow_err!(err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch_permission(&self, id: i32, name: String, enabled: bool) {
|
fn switch_permission(&self, id: i32, name: String, enabled: bool) {
|
||||||
let lock = self.read().unwrap();
|
crate::ui_cm_interface::switch_permission(id, name, enabled);
|
||||||
if let Some(s) = lock.senders.get(&id) {
|
|
||||||
allow_err!(s.send(Data::SwitchPermission { name, enabled }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(&self, id: i32) {
|
fn close(&self, id: i32) {
|
||||||
let lock = self.read().unwrap();
|
crate::ui_cm_interface::close(id);
|
||||||
if let Some(s) = lock.senders.get(&id) {
|
|
||||||
allow_err!(s.send(Data::Close));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_msg(&self, id: i32, text: String) {
|
|
||||||
let lock = self.read().unwrap();
|
|
||||||
if let Some(s) = lock.senders.get(&id) {
|
|
||||||
allow_err!(s.send(Data::ChatMessage { text }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_data(&self, id: i32, data: Data) {
|
|
||||||
let lock = self.read().unwrap();
|
|
||||||
if let Some(s) = lock.senders.get(&id) {
|
|
||||||
allow_err!(s.send(data));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authorize(&self, id: i32) {
|
fn authorize(&self, id: i32) {
|
||||||
let lock = self.read().unwrap();
|
crate::ui_cm_interface::authorize(id);
|
||||||
if let Some(s) = lock.senders.get(&id) {
|
|
||||||
allow_err!(s.send(Data::Authorize));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_msg(&self, id: i32, text: String) {
|
||||||
|
crate::ui_cm_interface::send_chat(id, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn t(&self, name: String) -> String {
|
fn t(&self, name: String) -> String {
|
||||||
@ -421,9 +113,9 @@ impl ConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl sciter::EventHandler for ConnectionManager {
|
impl sciter::EventHandler for SciterConnectionManager {
|
||||||
fn attached(&mut self, root: HELEMENT) {
|
fn attached(&mut self, root: HELEMENT) {
|
||||||
self.write().unwrap().root = Some(Element::from(root));
|
*self.ui_handler.element.lock().unwrap() = Some(Element::from(root));
|
||||||
}
|
}
|
||||||
|
|
||||||
sciter::dispatch_script_call! {
|
sciter::dispatch_script_call! {
|
||||||
@ -437,179 +129,3 @@ impl sciter::EventHandler for ConnectionManager {
|
|||||||
fn send_msg(i32, String);
|
fn send_msg(i32, String);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ClipboardFileData {
|
|
||||||
#[cfg(windows)]
|
|
||||||
Clip((i32, ipc::ClipbaordFile)),
|
|
||||||
Enable((i32, bool)),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
async fn start_ipc(cm: ConnectionManager) {
|
|
||||||
let (tx_file, _rx_file) = mpsc::unbounded_channel::<ClipboardFileData>();
|
|
||||||
#[cfg(windows)]
|
|
||||||
let cm_clip = cm.clone();
|
|
||||||
#[cfg(windows)]
|
|
||||||
std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file));
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
log::info!("try create privacy mode window");
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
if let Err(e) = crate::platform::windows::check_update_broker_process() {
|
|
||||||
log::warn!(
|
|
||||||
"Failed to check update broker process. Privacy mode may not work properly. {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allow_err!(crate::ui::win_privacy::start());
|
|
||||||
});
|
|
||||||
|
|
||||||
match new_listener("_cm").await {
|
|
||||||
Ok(mut incoming) => {
|
|
||||||
while let Some(result) = incoming.next().await {
|
|
||||||
match result {
|
|
||||||
Ok(stream) => {
|
|
||||||
log::debug!("Got new connection");
|
|
||||||
let mut stream = Connection::new(stream);
|
|
||||||
let cm = cm.clone();
|
|
||||||
let tx_file = tx_file.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
// for tmp use, without real conn id
|
|
||||||
let conn_id_tmp = -1;
|
|
||||||
let mut conn_id: i32 = 0;
|
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel::<Data>();
|
|
||||||
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
res = stream.next() => {
|
|
||||||
match res {
|
|
||||||
Err(err) => {
|
|
||||||
log::info!("cm ipc connection closed: {}", err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(Some(data)) => {
|
|
||||||
match data {
|
|
||||||
Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => {
|
|
||||||
log::debug!("conn_id: {}", id);
|
|
||||||
conn_id = id;
|
|
||||||
tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok();
|
|
||||||
cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone());
|
|
||||||
}
|
|
||||||
Data::Close => {
|
|
||||||
tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok();
|
|
||||||
log::info!("cm ipc connection closed from connection request");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Data::PrivacyModeState((id, _)) => {
|
|
||||||
conn_id = conn_id_tmp;
|
|
||||||
cm.send_data(id, data)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
cm.handle_data(conn_id, data, &tx_file, &mut write_jobs, &mut stream).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(data) = rx.recv() => {
|
|
||||||
if stream.send(&data).await.is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if conn_id != conn_id_tmp {
|
|
||||||
cm.remove_connection(conn_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("Couldn't get cm client: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("Failed to start cm ipc server: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::platform::quit_gui();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
pub async fn start_clipboard_file(
|
|
||||||
cm: ConnectionManager,
|
|
||||||
mut rx: mpsc::UnboundedReceiver<ClipboardFileData>,
|
|
||||||
) {
|
|
||||||
let mut cliprdr_context = None;
|
|
||||||
let mut rx_clip_client = get_rx_clip_client().lock().await;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
clip_file = rx_clip_client.recv() => match clip_file {
|
|
||||||
Some((conn_id, clip)) => {
|
|
||||||
cmd_inner_send(
|
|
||||||
&cm,
|
|
||||||
conn_id,
|
|
||||||
Data::ClipbaordFile(clip)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
},
|
|
||||||
server_msg = rx.recv() => match server_msg {
|
|
||||||
Some(ClipboardFileData::Clip((conn_id, clip))) => {
|
|
||||||
if let Some(ctx) = cliprdr_context.as_mut() {
|
|
||||||
server_clip_file(ctx, conn_id, clip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(ClipboardFileData::Enable((id, enabled))) => {
|
|
||||||
if enabled && cliprdr_context.is_none() {
|
|
||||||
cliprdr_context = Some(match create_cliprdr_context(true, false) {
|
|
||||||
Ok(context) => {
|
|
||||||
log::info!("clipboard context for file transfer created.");
|
|
||||||
context
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(
|
|
||||||
"Create clipboard context for file transfer: {}",
|
|
||||||
err.to_string()
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
set_conn_enabled(id, enabled);
|
|
||||||
if !enabled {
|
|
||||||
if let Some(ctx) = cliprdr_context.as_mut() {
|
|
||||||
empty_clipboard(ctx, id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) {
|
|
||||||
let lock = cm.read().unwrap();
|
|
||||||
if id != 0 {
|
|
||||||
if let Some(s) = lock.senders.get(&id) {
|
|
||||||
allow_err!(s.send(data));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for s in lock.senders.values() {
|
|
||||||
allow_err!(s.send(data.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -695,7 +695,7 @@ handler.clearAllJobs = function() {
|
|||||||
file_transfer.job_table.clearAllJobs();
|
file_transfer.job_table.clearAllJobs();
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) {
|
handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { // load last job
|
||||||
// stdout.println("restore job: " + is_remote);
|
// stdout.println("restore job: " + is_remote);
|
||||||
file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote);
|
file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1055,6 +1055,7 @@ function showSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkConnectStatus() {
|
function checkConnectStatus() {
|
||||||
|
handler.check_mouse_time(); // trigger connection status updater
|
||||||
self.timer(1s, function() {
|
self.timer(1s, function() {
|
||||||
var tmp = !!handler.get_option("stop-service");
|
var tmp = !!handler.get_option("stop-service");
|
||||||
if (tmp != service_stopped) {
|
if (tmp != service_stopped) {
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
sync::{
|
sync::{Arc, Mutex},
|
||||||
Arc, Mutex,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use sciter::{
|
use sciter::{
|
||||||
@ -22,13 +20,15 @@ use clipboard::{
|
|||||||
get_rx_clip_client, server_clip_file,
|
get_rx_clip_client, server_clip_file,
|
||||||
};
|
};
|
||||||
|
|
||||||
use hbb_common::{allow_err, log, message_proto::*, rendezvous_proto::ConnType};
|
use hbb_common::{
|
||||||
|
allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use crate::clipboard_file::*;
|
use crate::clipboard_file::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
client::*,
|
client::*,
|
||||||
ui_session_interface::{InvokeUi, Session},
|
ui_session_interface::{InvokeUiSession, Session},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Video = AssetPtr<video_destination>;
|
type Video = AssetPtr<video_destination>;
|
||||||
@ -37,12 +37,8 @@ lazy_static::lazy_static! {
|
|||||||
static ref VIDEO: Arc<Mutex<Option<Video>>> = Default::default();
|
static ref VIDEO: Arc<Mutex<Option<Video>>> = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
static mut IS_ALT_GR: bool = false;
|
|
||||||
|
|
||||||
/// SciterHandler
|
/// SciterHandler
|
||||||
/// * element
|
/// * element
|
||||||
/// * thread TODO check if flutter need
|
|
||||||
/// * close_state for file path when close
|
/// * close_state for file path when close
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct SciterHandler {
|
pub struct SciterHandler {
|
||||||
@ -66,7 +62,7 @@ impl SciterHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InvokeUi for SciterHandler {
|
impl InvokeUiSession for SciterHandler {
|
||||||
fn set_cursor_data(&self, cd: CursorData) {
|
fn set_cursor_data(&self, cd: CursorData) {
|
||||||
let mut colors = hbb_common::compress::decompress(&cd.colors);
|
let mut colors = hbb_common::compress::decompress(&cd.colors);
|
||||||
if colors.iter().filter(|x| **x != 0).next().is_none() {
|
if colors.iter().filter(|x| **x != 0).next().is_none() {
|
||||||
@ -154,17 +150,36 @@ impl InvokeUi for SciterHandler {
|
|||||||
self.call("clearAllJobs", &make_args!());
|
self.call("clearAllJobs", &make_args!());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
fn load_last_job(&self, cnt: i32, job_json: &str) {
|
||||||
fn add_job(
|
let job: Result<TransferJobMeta, serde_json::Error> = serde_json::from_str(job_json);
|
||||||
|
if let Ok(job) = job {
|
||||||
|
let path;
|
||||||
|
let to;
|
||||||
|
if job.is_remote {
|
||||||
|
path = job.remote.clone();
|
||||||
|
to = job.to.clone();
|
||||||
|
} else {
|
||||||
|
path = job.to.clone();
|
||||||
|
to = job.remote.clone();
|
||||||
|
}
|
||||||
|
self.call(
|
||||||
|
"addJob",
|
||||||
|
&make_args!(cnt, path, to, job.file_num, job.show_hidden, job.is_remote),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_folder_files(
|
||||||
&self,
|
&self,
|
||||||
id: i32,
|
id: i32,
|
||||||
|
entries: &Vec<FileEntry>,
|
||||||
path: String,
|
path: String,
|
||||||
to: String,
|
_is_local: bool,
|
||||||
file_num: i32,
|
only_count: bool,
|
||||||
show_hidden: bool,
|
|
||||||
is_remote: bool,
|
|
||||||
) {
|
) {
|
||||||
todo!()
|
let mut m = make_fd(id, entries, only_count);
|
||||||
|
m.set_item("path", path);
|
||||||
|
self.call("updateFolderFiles", &make_args!(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_transfer_list(&self) {
|
fn update_transfer_list(&self) {
|
||||||
@ -426,6 +441,14 @@ impl SciterSession {
|
|||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn t(&self, name: String) -> String {
|
||||||
|
crate::client::translate(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_icon(&self) -> String {
|
||||||
|
crate::get_icon()
|
||||||
|
}
|
||||||
|
|
||||||
fn supported_hwcodec(&self) -> Value {
|
fn supported_hwcodec(&self) -> Value {
|
||||||
#[cfg(feature = "hwcodec")]
|
#[cfg(feature = "hwcodec")]
|
||||||
{
|
{
|
||||||
@ -693,11 +716,10 @@ pub fn make_fd(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> Value {
|
|||||||
e.set_item("size", entry.size as f64);
|
e.set_item("size", entry.size as f64);
|
||||||
a.push(e);
|
a.push(e);
|
||||||
}
|
}
|
||||||
if only_count {
|
if !only_count {
|
||||||
m.set_item("num_entries", entries.len() as i32);
|
|
||||||
} else {
|
|
||||||
m.set_item("entries", a);
|
m.set_item("entries", a);
|
||||||
}
|
}
|
||||||
|
m.set_item("num_entries", entries.len() as i32);
|
||||||
m.set_item("total_size", n as f64);
|
m.set_item("total_size", n as f64);
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
@ -67,6 +67,7 @@ function adaptDisplay() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
refreshCursor();
|
||||||
handler.style.set {
|
handler.style.set {
|
||||||
width: w / scaleFactor + "px",
|
width: w / scaleFactor + "px",
|
||||||
height: h / scaleFactor + "px",
|
height: h / scaleFactor + "px",
|
||||||
@ -98,6 +99,7 @@ var acc_wheel_delta_y0 = 0;
|
|||||||
var total_wheel_time = 0;
|
var total_wheel_time = 0;
|
||||||
var wheeling = false;
|
var wheeling = false;
|
||||||
var dragging = false;
|
var dragging = false;
|
||||||
|
var is_mouse_event_triggered = false;
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum
|
// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum
|
||||||
function resetWheel() {
|
function resetWheel() {
|
||||||
@ -139,6 +141,7 @@ function accWheel(v, is_x) {
|
|||||||
|
|
||||||
function handler.onMouse(evt)
|
function handler.onMouse(evt)
|
||||||
{
|
{
|
||||||
|
is_mouse_event_triggered = true;
|
||||||
if (is_file_transfer || is_port_forward) return false;
|
if (is_file_transfer || is_port_forward) return false;
|
||||||
if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
|
if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
|
||||||
var dy = evt.y - scroll_body.scroll(#top);
|
var dy = evt.y - scroll_body.scroll(#top);
|
||||||
@ -317,6 +320,7 @@ function handler.onMouse(evt)
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var cur_id = -1;
|
||||||
var cur_hotx = 0;
|
var cur_hotx = 0;
|
||||||
var cur_hoty = 0;
|
var cur_hoty = 0;
|
||||||
var cur_img = null;
|
var cur_img = null;
|
||||||
@ -353,6 +357,12 @@ function updateCursor(system=false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshCursor() {
|
||||||
|
if (cur_id != -1) {
|
||||||
|
handler.setCursorId(cur_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
|
handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
|
||||||
cur_hotx = hotx;
|
cur_hotx = hotx;
|
||||||
cur_hoty = hoty;
|
cur_hoty = hoty;
|
||||||
@ -360,8 +370,9 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
|
|||||||
if (img) {
|
if (img) {
|
||||||
image_binded = true;
|
image_binded = true;
|
||||||
cursors[id] = [img, hotx, hoty, width, height];
|
cursors[id] = [img, hotx, hoty, width, height];
|
||||||
|
cur_id = id;
|
||||||
img = scaleCursorImage(img);
|
img = scaleCursorImage(img);
|
||||||
if (cursor_img.style#display == 'none') {
|
if (!first_mouse_event_triggered || cursor_img.style#display == 'none') {
|
||||||
self.timer(1ms, updateCursor);
|
self.timer(1ms, updateCursor);
|
||||||
}
|
}
|
||||||
cur_img = img;
|
cur_img = img;
|
||||||
@ -371,11 +382,12 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
|
|||||||
handler.setCursorId = function(id) {
|
handler.setCursorId = function(id) {
|
||||||
var img = cursors[id];
|
var img = cursors[id];
|
||||||
if (img) {
|
if (img) {
|
||||||
|
cur_id = id;
|
||||||
image_binded = true;
|
image_binded = true;
|
||||||
cur_hotx = img[1];
|
cur_hotx = img[1];
|
||||||
cur_hoty = img[2];
|
cur_hoty = img[2];
|
||||||
img = scaleCursorImage(img[0]);
|
img = scaleCursorImage(img[0]);
|
||||||
if (cursor_img.style#display == 'none') {
|
if (!first_mouse_event_triggered || cursor_img.style#display == 'none') {
|
||||||
self.timer(1ms, updateCursor);
|
self.timer(1ms, updateCursor);
|
||||||
}
|
}
|
||||||
cur_img = img;
|
cur_img = img;
|
||||||
|
|||||||
671
src/ui_cm_interface.rs
Normal file
671
src/ui_cm_interface.rs
Normal file
@ -0,0 +1,671 @@
|
|||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
iter::FromIterator,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicI64, Ordering},
|
||||||
|
RwLock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde_derive::Serialize;
|
||||||
|
|
||||||
|
use crate::ipc::Data;
|
||||||
|
use crate::ipc::{self, new_listener, Connection};
|
||||||
|
use hbb_common::{
|
||||||
|
allow_err,
|
||||||
|
config::Config,
|
||||||
|
fs::is_write_need_confirmation,
|
||||||
|
fs::{self, get_string, new_send_confirm, DigestCheckResult},
|
||||||
|
log,
|
||||||
|
message_proto::*,
|
||||||
|
protobuf::Message as _,
|
||||||
|
tokio::{
|
||||||
|
self,
|
||||||
|
sync::mpsc::{self, UnboundedSender},
|
||||||
|
task::spawn_blocking,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct Client {
|
||||||
|
pub id: i32,
|
||||||
|
pub authorized: bool,
|
||||||
|
pub is_file_transfer: bool,
|
||||||
|
pub port_forward: String,
|
||||||
|
pub name: String,
|
||||||
|
pub peer_id: String,
|
||||||
|
pub keyboard: bool,
|
||||||
|
pub clipboard: bool,
|
||||||
|
pub audio: bool,
|
||||||
|
pub file: bool,
|
||||||
|
pub restart: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
tx: UnboundedSender<Data>,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref CLIENTS: RwLock<HashMap<i32,Client>> = Default::default();
|
||||||
|
static ref CLICK_TIME: AtomicI64 = AtomicI64::new(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ConnectionManager<T: InvokeUiCM> {
|
||||||
|
pub ui_handler: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait InvokeUiCM: Send + Clone + 'static + Sized {
|
||||||
|
fn add_connection(&self, client: &Client);
|
||||||
|
|
||||||
|
fn remove_connection(&self, id: i32);
|
||||||
|
|
||||||
|
fn new_message(&self, id: i32, text: String);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: InvokeUiCM> Deref for ConnectionManager<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.ui_handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: InvokeUiCM> DerefMut for ConnectionManager<T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.ui_handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: InvokeUiCM> ConnectionManager<T> {
|
||||||
|
fn add_connection(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
is_file_transfer: bool,
|
||||||
|
port_forward: String,
|
||||||
|
peer_id: String,
|
||||||
|
name: String,
|
||||||
|
authorized: bool,
|
||||||
|
keyboard: bool,
|
||||||
|
clipboard: bool,
|
||||||
|
audio: bool,
|
||||||
|
file: bool,
|
||||||
|
restart: bool,
|
||||||
|
tx: mpsc::UnboundedSender<Data>,
|
||||||
|
) {
|
||||||
|
let client = Client {
|
||||||
|
id,
|
||||||
|
authorized,
|
||||||
|
is_file_transfer,
|
||||||
|
port_forward,
|
||||||
|
name: name.clone(),
|
||||||
|
peer_id: peer_id.clone(),
|
||||||
|
keyboard,
|
||||||
|
clipboard,
|
||||||
|
audio,
|
||||||
|
file,
|
||||||
|
restart,
|
||||||
|
tx,
|
||||||
|
};
|
||||||
|
self.ui_handler.add_connection(&client);
|
||||||
|
CLIENTS.write().unwrap().insert(id, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_connection(&self, id: i32) {
|
||||||
|
CLIENTS.write().unwrap().remove(&id);
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "android"))]
|
||||||
|
if CLIENTS
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.filter(|(_k, v)| !v.is_file_transfer)
|
||||||
|
.next()
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
if let Err(e) =
|
||||||
|
scrap::android::call_main_service_set_by_name("stop_capture", None, None)
|
||||||
|
{
|
||||||
|
log::debug!("stop_capture err:{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ui_handler.remove_connection(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn check_click_time(id: i32) {
|
||||||
|
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
||||||
|
allow_err!(client.tx.send(Data::ClickTime(0)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get_click_time() -> i64 {
|
||||||
|
CLICK_TIME.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn authorize(id: i32) {
|
||||||
|
if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
|
||||||
|
client.authorized = true;
|
||||||
|
allow_err!(client.tx.send(Data::Authorize));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn close(id: i32) {
|
||||||
|
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
||||||
|
allow_err!(client.tx.send(Data::Close));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// server mode send chat to peer
|
||||||
|
#[inline]
|
||||||
|
pub fn send_chat(id: i32, text: String) {
|
||||||
|
let clients = CLIENTS.read().unwrap();
|
||||||
|
if let Some(client) = clients.get(&id) {
|
||||||
|
allow_err!(client.tx.send(Data::ChatMessage { text }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn switch_permission(id: i32, name: String, enabled: bool) {
|
||||||
|
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
||||||
|
allow_err!(client.tx.send(Data::SwitchPermission { name, enabled }));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get_clients_state() -> String {
|
||||||
|
let clients = CLIENTS.read().unwrap();
|
||||||
|
let res = Vec::from_iter(clients.values().cloned());
|
||||||
|
serde_json::to_string(&res).unwrap_or("".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get_clients_length() -> usize {
|
||||||
|
let clients = CLIENTS.read().unwrap();
|
||||||
|
clients.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ClipboardFileData {
|
||||||
|
#[cfg(windows)]
|
||||||
|
Clip((i32, ipc::ClipbaordFile)),
|
||||||
|
Enable((i32, bool)),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
pub async fn start_ipc<T: InvokeUiCM>(cm: ConnectionManager<T>) {
|
||||||
|
let (tx_file, _rx_file) = mpsc::unbounded_channel::<ClipboardFileData>();
|
||||||
|
#[cfg(windows)]
|
||||||
|
let cm_clip = cm.clone();
|
||||||
|
#[cfg(windows)]
|
||||||
|
std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file));
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
log::info!("try create privacy mode window");
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
if let Err(e) = crate::platform::windows::check_update_broker_process() {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to check update broker process. Privacy mode may not work properly. {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allow_err!(crate::ui::win_privacy::start());
|
||||||
|
});
|
||||||
|
|
||||||
|
match new_listener("_cm").await {
|
||||||
|
Ok(mut incoming) => {
|
||||||
|
while let Some(result) = incoming.next().await {
|
||||||
|
match result {
|
||||||
|
Ok(stream) => {
|
||||||
|
log::debug!("Got new connection");
|
||||||
|
let mut stream = Connection::new(stream);
|
||||||
|
let cm = cm.clone();
|
||||||
|
let tx_file = tx_file.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// for tmp use, without real conn id
|
||||||
|
let conn_id_tmp = -1;
|
||||||
|
let mut conn_id: i32 = 0;
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel::<Data>();
|
||||||
|
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
res = stream.next() => {
|
||||||
|
match res {
|
||||||
|
Err(err) => {
|
||||||
|
log::info!("cm ipc connection closed: {}", err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(Some(data)) => {
|
||||||
|
match data {
|
||||||
|
Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => {
|
||||||
|
log::debug!("conn_id: {}", id);
|
||||||
|
conn_id = id;
|
||||||
|
tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok();
|
||||||
|
cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone());
|
||||||
|
}
|
||||||
|
Data::Close => {
|
||||||
|
tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok();
|
||||||
|
log::info!("cm ipc connection closed from connection request");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Data::PrivacyModeState((id, _)) => {
|
||||||
|
conn_id = conn_id_tmp;
|
||||||
|
allow_err!(tx.send(data));
|
||||||
|
}
|
||||||
|
Data::ClickTime(ms) => {
|
||||||
|
CLICK_TIME.store(ms, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
Data::ChatMessage { text } => {
|
||||||
|
cm.new_message(conn_id, text);
|
||||||
|
}
|
||||||
|
Data::FS(fs) => {
|
||||||
|
handle_fs(fs, &mut write_jobs, &tx).await;
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
Data::ClipbaordFile(_clip) => {
|
||||||
|
tx_file
|
||||||
|
.send(ClipboardFileData::Clip((conn_id, _clip)))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
Data::ClipboardFileEnabled(enabled) => {
|
||||||
|
tx_file
|
||||||
|
.send(ClipboardFileData::Enable((conn_id, enabled)))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(data) = rx.recv() => {
|
||||||
|
if stream.send(&data).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if conn_id != conn_id_tmp {
|
||||||
|
cm.remove_connection(conn_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Couldn't get cm client: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to start cm ipc server: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::platform::quit_gui();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
pub async fn start_listen<T: InvokeUiCM>(
|
||||||
|
cm: ConnectionManager<T>,
|
||||||
|
mut rx: mpsc::UnboundedReceiver<Data>,
|
||||||
|
tx: mpsc::UnboundedSender<Data>,
|
||||||
|
) {
|
||||||
|
let mut current_id = 0;
|
||||||
|
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
|
||||||
|
loop {
|
||||||
|
match rx.recv().await {
|
||||||
|
Some(Data::Login {
|
||||||
|
id,
|
||||||
|
is_file_transfer,
|
||||||
|
port_forward,
|
||||||
|
peer_id,
|
||||||
|
name,
|
||||||
|
authorized,
|
||||||
|
keyboard,
|
||||||
|
clipboard,
|
||||||
|
audio,
|
||||||
|
file,
|
||||||
|
restart,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
current_id = id;
|
||||||
|
cm.add_connection(
|
||||||
|
id,
|
||||||
|
is_file_transfer,
|
||||||
|
port_forward,
|
||||||
|
peer_id,
|
||||||
|
name,
|
||||||
|
authorized,
|
||||||
|
keyboard,
|
||||||
|
clipboard,
|
||||||
|
audio,
|
||||||
|
file,
|
||||||
|
restart,
|
||||||
|
tx.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(Data::ChatMessage { text }) => {
|
||||||
|
cm.new_message(current_id, text);
|
||||||
|
}
|
||||||
|
Some(Data::FS(fs)) => {
|
||||||
|
handle_fs(fs, &mut write_jobs, &tx).await;
|
||||||
|
}
|
||||||
|
Some(Data::Close) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cm.remove_connection(current_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec<fs::TransferJob>, tx: &UnboundedSender<Data>) {
|
||||||
|
match fs {
|
||||||
|
ipc::FS::ReadDir {
|
||||||
|
dir,
|
||||||
|
include_hidden,
|
||||||
|
} => {
|
||||||
|
read_dir(&dir, include_hidden, tx).await;
|
||||||
|
}
|
||||||
|
ipc::FS::RemoveDir {
|
||||||
|
path,
|
||||||
|
id,
|
||||||
|
recursive,
|
||||||
|
} => {
|
||||||
|
remove_dir(path, id, recursive, tx).await;
|
||||||
|
}
|
||||||
|
ipc::FS::RemoveFile { path, id, file_num } => {
|
||||||
|
remove_file(path, id, file_num, tx).await;
|
||||||
|
}
|
||||||
|
ipc::FS::CreateDir { path, id } => {
|
||||||
|
create_dir(path, id, tx).await;
|
||||||
|
}
|
||||||
|
ipc::FS::NewWrite {
|
||||||
|
path,
|
||||||
|
id,
|
||||||
|
file_num,
|
||||||
|
mut files,
|
||||||
|
overwrite_detection,
|
||||||
|
} => {
|
||||||
|
// cm has no show_hidden context
|
||||||
|
// dummy remote, show_hidden, is_remote
|
||||||
|
write_jobs.push(fs::TransferJob::new_write(
|
||||||
|
id,
|
||||||
|
"".to_string(),
|
||||||
|
path,
|
||||||
|
file_num,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
files
|
||||||
|
.drain(..)
|
||||||
|
.map(|f| FileEntry {
|
||||||
|
name: f.0,
|
||||||
|
modified_time: f.1,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
overwrite_detection,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ipc::FS::CancelWrite { id } => {
|
||||||
|
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||||
|
job.remove_download_file();
|
||||||
|
fs::remove_job(id, write_jobs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipc::FS::WriteDone { id, file_num } => {
|
||||||
|
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||||
|
job.modify_time();
|
||||||
|
send_raw(fs::new_done(id, file_num), tx);
|
||||||
|
fs::remove_job(id, write_jobs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipc::FS::WriteBlock {
|
||||||
|
id,
|
||||||
|
file_num,
|
||||||
|
data,
|
||||||
|
compressed,
|
||||||
|
} => {
|
||||||
|
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||||
|
if let Err(err) = job
|
||||||
|
.write(
|
||||||
|
FileTransferBlock {
|
||||||
|
id,
|
||||||
|
file_num,
|
||||||
|
data,
|
||||||
|
compressed,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
send_raw(fs::new_error(id, err, file_num), &tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipc::FS::CheckDigest {
|
||||||
|
id,
|
||||||
|
file_num,
|
||||||
|
file_size,
|
||||||
|
last_modified,
|
||||||
|
is_upload,
|
||||||
|
} => {
|
||||||
|
if let Some(job) = fs::get_job(id, write_jobs) {
|
||||||
|
let mut req = FileTransferSendConfirmRequest {
|
||||||
|
id,
|
||||||
|
file_num,
|
||||||
|
union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let digest = FileTransferDigest {
|
||||||
|
id,
|
||||||
|
file_num,
|
||||||
|
last_modified,
|
||||||
|
file_size,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if let Some(file) = job.files().get(file_num as usize) {
|
||||||
|
let path = get_string(&job.join(&file.name));
|
||||||
|
match is_write_need_confirmation(&path, &digest) {
|
||||||
|
Ok(digest_result) => {
|
||||||
|
match digest_result {
|
||||||
|
DigestCheckResult::IsSame => {
|
||||||
|
req.set_skip(true);
|
||||||
|
let msg_out = new_send_confirm(req);
|
||||||
|
send_raw(msg_out, &tx);
|
||||||
|
}
|
||||||
|
DigestCheckResult::NeedConfirm(mut digest) => {
|
||||||
|
// upload to server, but server has the same file, request
|
||||||
|
digest.is_upload = is_upload;
|
||||||
|
let mut msg_out = Message::new();
|
||||||
|
let mut fr = FileResponse::new();
|
||||||
|
fr.set_digest(digest);
|
||||||
|
msg_out.set_file_response(fr);
|
||||||
|
send_raw(msg_out, &tx);
|
||||||
|
}
|
||||||
|
DigestCheckResult::NoSuchFile => {
|
||||||
|
let msg_out = new_send_confirm(req);
|
||||||
|
send_raw(msg_out, &tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
send_raw(fs::new_error(id, err, file_num), &tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender<Data>) {
|
||||||
|
let path = {
|
||||||
|
if dir.is_empty() {
|
||||||
|
Config::get_home()
|
||||||
|
} else {
|
||||||
|
fs::get_path(dir)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await {
|
||||||
|
let mut msg_out = Message::new();
|
||||||
|
let mut file_response = FileResponse::new();
|
||||||
|
file_response.set_dir(fd);
|
||||||
|
msg_out.set_file_response(file_response);
|
||||||
|
send_raw(msg_out, tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_result<F: std::fmt::Display, S: std::fmt::Display>(
|
||||||
|
res: std::result::Result<std::result::Result<(), F>, S>,
|
||||||
|
id: i32,
|
||||||
|
file_num: i32,
|
||||||
|
tx: &UnboundedSender<Data>,
|
||||||
|
) {
|
||||||
|
match res {
|
||||||
|
Err(err) => {
|
||||||
|
send_raw(fs::new_error(id, err, file_num), tx);
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
send_raw(fs::new_error(id, err, file_num), tx);
|
||||||
|
}
|
||||||
|
Ok(Ok(())) => {
|
||||||
|
send_raw(fs::new_done(id, file_num), tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender<Data>) {
|
||||||
|
handle_result(
|
||||||
|
spawn_blocking(move || fs::remove_file(&path)).await,
|
||||||
|
id,
|
||||||
|
file_num,
|
||||||
|
tx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_dir(path: String, id: i32, tx: &UnboundedSender<Data>) {
|
||||||
|
handle_result(
|
||||||
|
spawn_blocking(move || fs::create_dir(&path)).await,
|
||||||
|
id,
|
||||||
|
0,
|
||||||
|
tx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender<Data>) {
|
||||||
|
let path = fs::get_path(&path);
|
||||||
|
handle_result(
|
||||||
|
spawn_blocking(move || {
|
||||||
|
if recursive {
|
||||||
|
fs::remove_all_empty_dir(&path)
|
||||||
|
} else {
|
||||||
|
std::fs::remove_dir(&path).map_err(|err| err.into())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await,
|
||||||
|
id,
|
||||||
|
0,
|
||||||
|
tx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_raw(msg: Message, tx: &UnboundedSender<Data>) {
|
||||||
|
match msg.write_to_bytes() {
|
||||||
|
Ok(bytes) => {
|
||||||
|
allow_err!(tx.send(Data::RawMessage(bytes)));
|
||||||
|
}
|
||||||
|
err => allow_err!(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
pub async fn start_clipboard_file<T: InvokeUiCM>(
|
||||||
|
cm: ConnectionManager<T>,
|
||||||
|
mut rx: mpsc::UnboundedReceiver<ClipboardFileData>,
|
||||||
|
) {
|
||||||
|
let mut cliprdr_context = None;
|
||||||
|
let mut rx_clip_client = clipboard::get_rx_clip_client().lock().await;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
clip_file = rx_clip_client.recv() => match clip_file {
|
||||||
|
Some((conn_id, clip)) => {
|
||||||
|
cmd_inner_send(
|
||||||
|
conn_id,
|
||||||
|
Data::ClipbaordFile(clip)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server_msg = rx.recv() => match server_msg {
|
||||||
|
Some(ClipboardFileData::Clip((conn_id, clip))) => {
|
||||||
|
if let Some(ctx) = cliprdr_context.as_mut() {
|
||||||
|
clipboard::server_clip_file(ctx, conn_id, clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(ClipboardFileData::Enable((id, enabled))) => {
|
||||||
|
if enabled && cliprdr_context.is_none() {
|
||||||
|
cliprdr_context = Some(match clipboard::create_cliprdr_context(true, false) {
|
||||||
|
Ok(context) => {
|
||||||
|
log::info!("clipboard context for file transfer created.");
|
||||||
|
context
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!(
|
||||||
|
"Create clipboard context for file transfer: {}",
|
||||||
|
err.to_string()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
clipboard::set_conn_enabled(id, enabled);
|
||||||
|
if !enabled {
|
||||||
|
if let Some(ctx) = cliprdr_context.as_mut() {
|
||||||
|
clipboard::empty_clipboard(ctx, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn cmd_inner_send(id: i32, data: Data) {
|
||||||
|
let lock = CLIENTS.read().unwrap();
|
||||||
|
if id != 0 {
|
||||||
|
if let Some(s) = lock.get(&id) {
|
||||||
|
allow_err!(s.tx.send(data));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for s in lock.values() {
|
||||||
|
allow_err!(s.tx.send(data.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -372,11 +372,12 @@ pub fn get_mouse_time() -> f64 {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
||||||
pub fn check_mouse_time() {
|
pub fn check_mouse_time() {
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]{
|
||||||
let sender = SENDER.lock().unwrap();
|
let sender = SENDER.lock().unwrap();
|
||||||
allow_err!(sender.send(ipc::Data::MouseMoveTime(0)));
|
allow_err!(sender.send(ipc::Data::MouseMoveTime(0)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_connect_status() -> Status {
|
pub fn get_connect_status() -> Status {
|
||||||
let ui_statue = UI_STATUS.lock().unwrap();
|
let ui_statue = UI_STATUS.lock().unwrap();
|
||||||
|
|||||||
@ -34,7 +34,7 @@ lazy_static::lazy_static! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct Session<T: InvokeUi> {
|
pub struct Session<T: InvokeUiSession> {
|
||||||
pub cmd: String,
|
pub cmd: String,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
@ -45,7 +45,7 @@ pub struct Session<T: InvokeUi> {
|
|||||||
pub ui_handler: T,
|
pub ui_handler: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: InvokeUi> Session<T> {
|
impl<T: InvokeUiSession> Session<T> {
|
||||||
pub fn get_view_style(&self) -> String {
|
pub fn get_view_style(&self) -> String {
|
||||||
self.lc.read().unwrap().view_style.clone()
|
self.lc.read().unwrap().view_style.clone()
|
||||||
}
|
}
|
||||||
@ -151,11 +151,6 @@ impl<T: InvokeUi> Session<T> {
|
|||||||
self.send(Data::Message(msg));
|
self.send(Data::Message(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
||||||
pub fn t(&self, name: String) -> String {
|
|
||||||
crate::client::translate(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_audit_server(&self) -> String {
|
pub fn get_audit_server(&self) -> String {
|
||||||
if self.lc.read().unwrap().conn_id <= 0
|
if self.lc.read().unwrap().conn_id <= 0
|
||||||
|| LocalConfig::get_option("access_token").is_empty()
|
|| LocalConfig::get_option("access_token").is_empty()
|
||||||
@ -690,11 +685,6 @@ impl<T: InvokeUi> Session<T> {
|
|||||||
return "".to_owned();
|
return "".to_owned();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
||||||
pub fn get_icon(&self) -> String {
|
|
||||||
crate::get_icon()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_chat(&self, text: String) {
|
pub fn send_chat(&self, text: String) {
|
||||||
let mut misc = Misc::new();
|
let mut misc = Misc::new();
|
||||||
misc.set_chat_message(ChatMessage {
|
misc.set_chat_message(ChatMessage {
|
||||||
@ -961,9 +951,35 @@ impl<T: InvokeUi> Session<T> {
|
|||||||
pub fn close(&self) {
|
pub fn close(&self) {
|
||||||
self.send(Data::Close);
|
self.send(Data::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_last_jobs(&self) {
|
||||||
|
self.clear_all_jobs();
|
||||||
|
let pc = self.load_config();
|
||||||
|
if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() {
|
||||||
|
// no last jobs
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: can add a confirm dialog
|
||||||
|
let mut cnt = 1;
|
||||||
|
for job_str in pc.transfer.read_jobs.iter() {
|
||||||
|
if !job_str.is_empty() {
|
||||||
|
self.load_last_job(cnt, job_str);
|
||||||
|
cnt += 1;
|
||||||
|
log::info!("restore read_job: {:?}", job_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for job_str in pc.transfer.write_jobs.iter() {
|
||||||
|
if !job_str.is_empty() {
|
||||||
|
self.load_last_job(cnt, job_str);
|
||||||
|
cnt += 1;
|
||||||
|
log::info!("restore write_job: {:?}", job_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.update_transfer_list();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default {
|
pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
|
||||||
fn set_cursor_data(&self, cd: CursorData);
|
fn set_cursor_data(&self, cd: CursorData);
|
||||||
fn set_cursor_id(&self, id: String);
|
fn set_cursor_id(&self, id: String);
|
||||||
fn set_cursor_position(&self, cp: CursorPosition);
|
fn set_cursor_position(&self, cp: CursorPosition);
|
||||||
@ -978,18 +994,17 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default {
|
|||||||
fn job_error(&self, id: i32, err: String, file_num: i32);
|
fn job_error(&self, id: i32, err: String, file_num: i32);
|
||||||
fn job_done(&self, id: i32, file_num: i32);
|
fn job_done(&self, id: i32, file_num: i32);
|
||||||
fn clear_all_jobs(&self);
|
fn clear_all_jobs(&self);
|
||||||
fn add_job(
|
|
||||||
&self,
|
|
||||||
id: i32,
|
|
||||||
path: String,
|
|
||||||
to: String,
|
|
||||||
file_num: i32,
|
|
||||||
show_hidden: bool,
|
|
||||||
is_remote: bool,
|
|
||||||
);
|
|
||||||
fn new_message(&self, msg: String);
|
fn new_message(&self, msg: String);
|
||||||
fn update_transfer_list(&self);
|
fn update_transfer_list(&self);
|
||||||
// fn update_folder_files(&self); // TODO flutter with file_dir and update_folder_files
|
fn load_last_job(&self, cnt: i32, job_json: &str);
|
||||||
|
fn update_folder_files(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
entries: &Vec<FileEntry>,
|
||||||
|
path: String,
|
||||||
|
is_local: bool,
|
||||||
|
only_count: bool,
|
||||||
|
);
|
||||||
fn confirm_delete_files(&self, id: i32, i: i32, name: String);
|
fn confirm_delete_files(&self, id: i32, i: i32, name: String);
|
||||||
fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool);
|
fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool);
|
||||||
fn update_block_input_state(&self, on: bool);
|
fn update_block_input_state(&self, on: bool);
|
||||||
@ -1001,7 +1016,7 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default {
|
|||||||
fn clipboard(&self, content: String);
|
fn clipboard(&self, content: String);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: InvokeUi> Deref for Session<T> {
|
impl<T: InvokeUiSession> Deref for Session<T> {
|
||||||
type Target = T;
|
type Target = T;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@ -1009,16 +1024,16 @@ impl<T: InvokeUi> Deref for Session<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: InvokeUi> DerefMut for Session<T> {
|
impl<T: InvokeUiSession> DerefMut for Session<T> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut self.ui_handler
|
&mut self.ui_handler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: InvokeUi> FileManager for Session<T> {}
|
impl<T: InvokeUiSession> FileManager for Session<T> {}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: InvokeUi> Interface for Session<T> {
|
impl<T: InvokeUiSession> Interface for Session<T> {
|
||||||
fn send(&self, data: Data) {
|
fn send(&self, data: Data) {
|
||||||
if let Some(sender) = self.sender.read().unwrap().as_ref() {
|
if let Some(sender) = self.sender.read().unwrap().as_ref() {
|
||||||
sender.send(data).ok();
|
sender.send(data).ok();
|
||||||
@ -1146,7 +1161,7 @@ impl<T: InvokeUi> Interface for Session<T> {
|
|||||||
// TODO use event callbcak
|
// TODO use event callbcak
|
||||||
// sciter only
|
// sciter only
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
impl<T: InvokeUi> Session<T> {
|
impl<T: InvokeUiSession> Session<T> {
|
||||||
fn start_keyboard_hook(&self) {
|
fn start_keyboard_hook(&self) {
|
||||||
if self.is_port_forward() || self.is_file_transfer() {
|
if self.is_port_forward() || self.is_file_transfer() {
|
||||||
return;
|
return;
|
||||||
@ -1211,7 +1226,7 @@ impl<T: InvokeUi> Session<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
pub async fn io_loop<T: InvokeUi>(handler: Session<T>) {
|
pub async fn io_loop<T: InvokeUiSession>(handler: Session<T>) {
|
||||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
||||||
*handler.sender.write().unwrap() = Some(sender.clone());
|
*handler.sender.write().unwrap() = Some(sender.clone());
|
||||||
let mut options = crate::ipc::get_options_async().await;
|
let mut options = crate::ipc::get_options_async().await;
|
||||||
@ -1327,7 +1342,7 @@ pub async fn io_loop<T: InvokeUi>(handler: Session<T>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
async fn start_one_port_forward<T: InvokeUi>(
|
async fn start_one_port_forward<T: InvokeUiSession>(
|
||||||
handler: Session<T>,
|
handler: Session<T>,
|
||||||
port: i32,
|
port: i32,
|
||||||
remote_host: String,
|
remote_host: String,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user