Merge branch 'master' of https://github.com/rustdesk/rustdesk
This commit is contained in:
commit
8da4fbabf5
@ -105,43 +105,30 @@ class MainService : Service() {
|
||||
@Keep
|
||||
fun rustSetByName(name: String, arg1: String, arg2: String) {
|
||||
when (name) {
|
||||
"try_start_without_auth" -> {
|
||||
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")
|
||||
"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 authorized = jsonObject["authorized"] as Boolean
|
||||
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
|
||||
val type = if (isFileTransfer) {
|
||||
translate("File Connection")
|
||||
} else {
|
||||
translate("Screen Connection")
|
||||
}
|
||||
if (!isFileTransfer && !isStart) {
|
||||
startCapture()
|
||||
if (authorized) {
|
||||
if (!isFileTransfer && !isStart) {
|
||||
startCapture()
|
||||
}
|
||||
onClientAuthorizedNotification(id, type, username, peerId)
|
||||
} else {
|
||||
loginRequestNotification(id, type, username, peerId)
|
||||
}
|
||||
onClientAuthorizedNotification(id, type, username, peerId)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
"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:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -154,7 +155,7 @@ class MyTheme {
|
||||
brightness: Brightness.light,
|
||||
primarySwatch: Colors.blue,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
tabBarTheme: TabBarTheme(
|
||||
tabBarTheme: const TabBarTheme(
|
||||
labelColor: Colors.black87,
|
||||
),
|
||||
splashColor: Colors.transparent,
|
||||
@ -162,13 +163,14 @@ class MyTheme {
|
||||
).copyWith(
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ColorThemeExtension.light,
|
||||
TabbarTheme.light,
|
||||
],
|
||||
);
|
||||
static ThemeData darkTheme = ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.blue,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
tabBarTheme: TabBarTheme(
|
||||
tabBarTheme: const TabBarTheme(
|
||||
labelColor: Colors.white70,
|
||||
),
|
||||
splashColor: Colors.transparent,
|
||||
@ -176,12 +178,17 @@ class MyTheme {
|
||||
).copyWith(
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ColorThemeExtension.dark,
|
||||
TabbarTheme.dark,
|
||||
],
|
||||
);
|
||||
|
||||
static ColorThemeExtension color(BuildContext context) {
|
||||
return Theme.of(context).extension<ColorThemeExtension>()!;
|
||||
}
|
||||
|
||||
static TabbarTheme tabbar(BuildContext context) {
|
||||
return Theme.of(context).extension<TabbarTheme>()!;
|
||||
}
|
||||
}
|
||||
|
||||
bool isDarkTheme() {
|
||||
@ -340,34 +347,41 @@ class OverlayDialogManager {
|
||||
{bool clickMaskDismiss = false,
|
||||
bool showCancel = true,
|
||||
VoidCallback? onCancel}) {
|
||||
show((setState, close) => CustomAlertDialog(
|
||||
show((setState, close) {
|
||||
cancel() {
|
||||
dismissAll();
|
||||
if (onCancel != null) {
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Container(
|
||||
constraints: BoxConstraints(maxWidth: 240),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 30),
|
||||
Center(child: CircularProgressIndicator()),
|
||||
SizedBox(height: 20),
|
||||
const SizedBox(height: 30),
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: Text(translate(text),
|
||||
style: TextStyle(fontSize: 15))),
|
||||
SizedBox(height: 20),
|
||||
style: const TextStyle(fontSize: 15))),
|
||||
const SizedBox(height: 20),
|
||||
Offstage(
|
||||
offstage: !showCancel,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
dismissAll();
|
||||
if (onCancel != null) {
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
onPressed: 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: (_) {
|
||||
return IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment(0.0, 0.8),
|
||||
alignment: const Alignment(0.0, 0.8),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.all(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 18,
|
||||
@ -403,23 +417,54 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
||||
}
|
||||
|
||||
class CustomAlertDialog extends StatelessWidget {
|
||||
CustomAlertDialog(
|
||||
{this.title, required this.content, this.actions, this.contentPadding});
|
||||
const CustomAlertDialog(
|
||||
{Key? key,
|
||||
this.title,
|
||||
required this.content,
|
||||
this.actions,
|
||||
this.contentPadding,
|
||||
this.onSubmit,
|
||||
this.onCancel})
|
||||
: super(key: key);
|
||||
|
||||
final Widget? title;
|
||||
final Widget content;
|
||||
final List<Widget>? actions;
|
||||
final double? contentPadding;
|
||||
final Function()? onSubmit;
|
||||
final Function()? onCancel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: title,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10),
|
||||
content: content,
|
||||
actions: actions,
|
||||
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,
|
||||
title: title,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: contentPadding ?? 25, vertical: 10),
|
||||
content: content,
|
||||
actions: actions,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -429,26 +474,28 @@ void msgBox(
|
||||
{bool? hasCancel}) {
|
||||
dialogManager.dismissAll();
|
||||
List<Widget> buttons = [];
|
||||
bool hasOk = false;
|
||||
submit() {
|
||||
dialogManager.dismissAll();
|
||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||
if (!type.contains("custom")) {
|
||||
closeConnection();
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
dialogManager.dismissAll();
|
||||
}
|
||||
|
||||
if (type != "connecting" && type != "success" && !type.contains("nook")) {
|
||||
buttons.insert(
|
||||
0,
|
||||
msgBoxButton(translate('OK'), () {
|
||||
dialogManager.dismissAll();
|
||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||
if (!type.contains("custom")) {
|
||||
closeConnection();
|
||||
}
|
||||
}));
|
||||
hasOk = true;
|
||||
buttons.insert(0, msgBoxButton(translate('OK'), submit));
|
||||
}
|
||||
hasCancel ??= !type.contains("error") &&
|
||||
!type.contains("nocancel") &&
|
||||
type != "restarting";
|
||||
if (hasCancel) {
|
||||
buttons.insert(
|
||||
0,
|
||||
msgBoxButton(translate('Cancel'), () {
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
buttons.insert(0, msgBoxButton(translate('Cancel'), cancel));
|
||||
}
|
||||
// TODO: test this button
|
||||
if (type.contains("hasclose")) {
|
||||
@ -459,9 +506,12 @@ void msgBox(
|
||||
}));
|
||||
}
|
||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: _msgBoxTitle(title),
|
||||
content: Text(translate(text), style: TextStyle(fontSize: 15)),
|
||||
actions: buttons));
|
||||
title: _msgBoxTitle(title),
|
||||
content: Text(translate(text), style: const TextStyle(fontSize: 15)),
|
||||
actions: buttons,
|
||||
onSubmit: hasOk ? submit : null,
|
||||
onCancel: hasCancel == true ? cancel : null,
|
||||
));
|
||||
}
|
||||
|
||||
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))));
|
||||
}
|
||||
|
||||
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,
|
||||
Widget content, List<Widget> buttons) {
|
||||
Widget content, List<Widget> buttons,
|
||||
{bool hasCancel = true}) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: _msgBoxTitle(title),
|
||||
content: content,
|
||||
actions: buttons));
|
||||
title: _msgBoxTitle(title),
|
||||
content: content,
|
||||
actions: buttons,
|
||||
onCancel: hasCancel ? close : null,
|
||||
));
|
||||
}
|
||||
|
||||
Color str2color(String str, [alpha = 0xFF]) {
|
||||
|
@ -1,4 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// TODO: Divide every 3 number to display ID
|
||||
class IdFormController extends TextEditingController {}
|
||||
class IDTextEditingController 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 '../consts.dart';
|
||||
import '../models/platform_model.dart';
|
||||
|
||||
class PrivacyModeState {
|
||||
static String tag(String id) => 'privacy_mode_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: tag(id));
|
||||
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 void delete(String id) => Get.delete(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 void init(String id) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: tag(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 void delete(String id) => Get.delete(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 void init(String id) {
|
||||
final RxInt state = RxInt(0);
|
||||
Get.put(state, tag: tag(id));
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxInt state = RxInt(0);
|
||||
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));
|
||||
}
|
||||
|
||||
@ -85,3 +113,46 @@ class ConnectionTypeState {
|
||||
static ConnectionType find(String 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 '../../common.dart';
|
||||
import '../../common/formatter/id_formatter.dart';
|
||||
import '../../mobile/pages/scan_page.dart';
|
||||
import '../../mobile/pages/settings_page.dart';
|
||||
import '../../models/model.dart';
|
||||
@ -30,7 +31,7 @@ class ConnectionPage extends StatefulWidget {
|
||||
/// State for the connection page.
|
||||
class _ConnectionPageState extends State<ConnectionPage> {
|
||||
/// Controller for the id input bar.
|
||||
final _idController = TextEditingController();
|
||||
final _idController = IDTextEditingController();
|
||||
|
||||
/// Update url. If it's not null, means an update is available.
|
||||
final _updateUrl = '';
|
||||
@ -43,9 +44,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
if (lastRemoteId != _idController.text) {
|
||||
if (lastRemoteId != _idController.id) {
|
||||
setState(() {
|
||||
_idController.text = lastRemoteId;
|
||||
_idController.id = lastRemoteId;
|
||||
});
|
||||
}
|
||||
}();
|
||||
@ -110,7 +111,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
/// Callback for the connect button.
|
||||
/// Connects to the selected peer.
|
||||
void onConnect({bool isFileTransfer = false}) {
|
||||
final id = _idController.text.trim();
|
||||
final id = _idController.id;
|
||||
connect(id, isFileTransfer: isFileTransfer);
|
||||
}
|
||||
|
||||
@ -166,7 +167,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
});
|
||||
var w = Container(
|
||||
width: 320 + 20 * 2,
|
||||
padding: EdgeInsets.fromLTRB(20, 24, 20, 22),
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 22),
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).bg,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(13)),
|
||||
@ -178,42 +179,54 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
children: [
|
||||
Text(
|
||||
translate('Control Remote Desktop'),
|
||||
style: TextStyle(fontSize: 19, height: 1),
|
||||
style: const TextStyle(fontSize: 19, height: 1),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 15),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
style: TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontSize: 22,
|
||||
height: 1,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: translate('Enter Remote ID'),
|
||||
hintStyle: TextStyle(
|
||||
color: MyTheme.color(context).placeholder),
|
||||
border: OutlineInputBorder(
|
||||
child: Obx(
|
||||
() => TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: focusNode,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontSize: 22,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
cursorColor: MyTheme.color(context).text!,
|
||||
decoration: InputDecoration(
|
||||
hintText: inputFocused.value
|
||||
? null
|
||||
: translate('Enter Remote ID'),
|
||||
hintStyle: TextStyle(
|
||||
color: MyTheme.color(context).placeholder),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(
|
||||
color: MyTheme.color(context).border!)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(
|
||||
color: MyTheme.color(context).border!)),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(
|
||||
color: MyTheme.color(context).placeholder!)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide:
|
||||
BorderSide(color: MyTheme.button, width: 3),
|
||||
),
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 10, vertical: 12)),
|
||||
controller: _idController,
|
||||
onSubmitted: (s) {
|
||||
onConnect();
|
||||
},
|
||||
borderSide:
|
||||
BorderSide(color: MyTheme.button, width: 3),
|
||||
),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 12)),
|
||||
controller: _idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
onSubmitted: (s) {
|
||||
onConnect();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -259,7 +272,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
).marginSymmetric(horizontal: 12),
|
||||
),
|
||||
)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 17,
|
||||
),
|
||||
Obx(
|
||||
@ -304,7 +317,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
),
|
||||
);
|
||||
return Center(
|
||||
child: Container(constraints: BoxConstraints(maxWidth: 600), child: w));
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600), child: w));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -654,71 +668,69 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
var field = "";
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
TextEditingController controller = TextEditingController(text: field);
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
field = controller.text.trim();
|
||||
if (field.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final ids = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = ids.join(',');
|
||||
for (final newId in ids) {
|
||||
if (gFFI.abModel.idContainBy(newId)) {
|
||||
continue;
|
||||
}
|
||||
gFFI.abModel.addId(newId);
|
||||
}
|
||||
await gFFI.abModel.updateAb();
|
||||
this.setState(() {});
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Add ID")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("whitelist_sep")),
|
||||
SizedBox(
|
||||
const 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),
|
||||
),
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
focusNode: FocusNode()..requestFocus()),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
field = field.trim();
|
||||
if (field.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final ids = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = ids.join(',');
|
||||
for (final newId in ids) {
|
||||
if (gFFI.abModel.idContainBy(newId)) {
|
||||
continue;
|
||||
}
|
||||
gFFI.abModel.addId(newId);
|
||||
}
|
||||
await gFFI.abModel.updateAb();
|
||||
this.setState(() {});
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -727,67 +739,65 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
var field = "";
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
TextEditingController controller = TextEditingController(text: field);
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
field = controller.text.trim();
|
||||
if (field.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = tags.join(',');
|
||||
for (final tag in tags) {
|
||||
gFFI.abModel.addTag(tag);
|
||||
}
|
||||
await gFFI.abModel.updateAb();
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Add Tag")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("whitelist_sep")),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
field = s;
|
||||
},
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: TextEditingController(text: field),
|
||||
controller: controller,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
field = field.trim();
|
||||
if (field.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = tags.join(',');
|
||||
for (final tag in tags) {
|
||||
gFFI.abModel.addTag(tag);
|
||||
}
|
||||
await gFFI.abModel.updateAb();
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
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;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||
await gFFI.abModel.updateAb();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Edit Tag")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Wrap(
|
||||
children: tags
|
||||
.map((e) => buildTag(e, selectedTag, onTap: () {
|
||||
@ -818,26 +838,16 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
.toList(growable: false),
|
||||
),
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
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"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ class ConnectionTabPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
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 unselectedIcon = Icons.desktop_windows_outlined;
|
||||
|
||||
@ -60,6 +61,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
if (call.method == "new_remote_desktop") {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
ConnectionTypeState.init(id);
|
||||
window_on_top(windowId());
|
||||
ConnectionTypeState.init(id);
|
||||
tabController.add(TabInfo(
|
||||
@ -81,7 +83,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
return Obx(() => SubWindowDragToResizeArea(
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
@ -93,15 +94,11 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Obx(() => DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
tabType: DesktopTabType.remoteScreen,
|
||||
showTabBar: fullscreen.isFalse,
|
||||
onClose: () {
|
||||
tabController.clear();
|
||||
},
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
pageViewBuilder: (pageView) {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setFullscreen(fullscreen.isTrue);
|
||||
|
@ -6,6 +6,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.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/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/server_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
@ -52,7 +55,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
return Row(
|
||||
children: [
|
||||
buildServerInfo(context),
|
||||
VerticalDivider(
|
||||
const VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
@ -90,23 +93,23 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
buildIDBoard(BuildContext context) {
|
||||
final model = gFFI.serverModel;
|
||||
return Container(
|
||||
margin: EdgeInsets.only(left: 20, right: 16),
|
||||
height: 52,
|
||||
margin: const EdgeInsets.only(left: 20, right: 11),
|
||||
height: 57,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Container(
|
||||
width: 2,
|
||||
decoration: BoxDecoration(color: MyTheme.accent),
|
||||
),
|
||||
decoration: const BoxDecoration(color: MyTheme.accent),
|
||||
).marginOnly(top: 5),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
padding: const EdgeInsets.only(left: 7),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
SizedBox(
|
||||
height: 25,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -117,7 +120,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: MyTheme.color(context).lightText),
|
||||
),
|
||||
).marginOnly(top: 5),
|
||||
buildPopupMenu(context)
|
||||
],
|
||||
),
|
||||
@ -132,11 +135,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: TextFormField(
|
||||
controller: model.serverId,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(bottom: 18),
|
||||
contentPadding: EdgeInsets.only(bottom: 20),
|
||||
),
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
@ -241,7 +244,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
},
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
radius: 12,
|
||||
radius: 15,
|
||||
backgroundColor: hover.value
|
||||
? MyTheme.color(context).grayBg!
|
||||
: MyTheme.color(context).bg!,
|
||||
@ -274,7 +277,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
padding: const EdgeInsets.only(left: 7),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -300,7 +303,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(bottom: 8),
|
||||
contentPadding: EdgeInsets.only(bottom: 2),
|
||||
),
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
@ -314,7 +317,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
? MyTheme.color(context).text
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
).marginOnly(right: 10, bottom: 8),
|
||||
).marginOnly(right: 8, bottom: 2),
|
||||
),
|
||||
onTap: () => bind.mainUpdateTemporaryPassword(),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
@ -422,13 +425,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
color: editHover.value
|
||||
? MyTheme.color(context).text
|
||||
: Color(0xFFDDDDDD))
|
||||
.marginOnly(bottom: 8)));
|
||||
.marginOnly(bottom: 2)));
|
||||
}
|
||||
|
||||
buildTip(BuildContext context) {
|
||||
return 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(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -639,76 +642,76 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
var newId = "";
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
TextEditingController controller = TextEditingController();
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
newId = controller.text.trim();
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
bind.mainChangeId(newId: newId);
|
||||
});
|
||||
|
||||
var status = await bind.mainGetAsyncStatus();
|
||||
while (status == " ") {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
status = await bind.mainGetAsyncStatus();
|
||||
}
|
||||
if (status.isEmpty) {
|
||||
// ok
|
||||
close();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
msg = translate(status);
|
||||
});
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Change ID")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("id_change_tip")),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text("ID:").marginOnly(bottom: 16.0),
|
||||
SizedBox(
|
||||
const Text("ID:").marginOnly(bottom: 16.0),
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
newId = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
bind.mainChangeId(newId: newId);
|
||||
});
|
||||
|
||||
var status = await bind.mainGetAsyncStatus();
|
||||
while (status == " ") {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
status = await bind.mainGetAsyncStatus();
|
||||
}
|
||||
if (status.isEmpty) {
|
||||
// ok
|
||||
close();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
msg = translate(status);
|
||||
});
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
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 license = await bind.mainGetLicense();
|
||||
final version = await bind.mainGetVersion();
|
||||
final linkStyle = TextStyle(decoration: TextDecoration.underline);
|
||||
const linkStyle = TextStyle(decoration: TextDecoration.underline);
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text("About $appName"),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Text("Version: $version").marginSymmetric(vertical: 4.0),
|
||||
@ -734,7 +737,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
onTap: () {
|
||||
launchUrlString("https://rustdesk.com/privacy");
|
||||
},
|
||||
child: Text(
|
||||
child: const Text(
|
||||
"Privacy Statement",
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
@ -742,13 +745,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
onTap: () {
|
||||
launchUrlString("https://rustdesk.com");
|
||||
},
|
||||
child: Text(
|
||||
child: const Text(
|
||||
"Website",
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
Container(
|
||||
decoration: BoxDecoration(color: Color(0xFF2c8cff)),
|
||||
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8),
|
||||
decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -757,9 +761,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
children: [
|
||||
Text(
|
||||
"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!",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
@ -775,12 +779,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: close,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -812,118 +814,124 @@ Future<bool> loginDialog() async {
|
||||
var isInProgress = false;
|
||||
var completer = Completer<bool>();
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
userNameMsg = "";
|
||||
passMsg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
cancel() {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
userName = userContontroller.text;
|
||||
pass = pwdController.text;
|
||||
if (userName.isEmpty) {
|
||||
userNameMsg = translate("Username missed");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (pass.isEmpty) {
|
||||
passMsg = translate("Password missed");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final resp = await gFFI.userModel.login(userName, pass);
|
||||
if (resp.containsKey('error')) {
|
||||
passMsg = resp['error'];
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
// {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w,
|
||||
// token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}}
|
||||
debugPrint("$resp");
|
||||
completer.complete(true);
|
||||
} catch (err) {
|
||||
// ignore: avoid_print
|
||||
print(err.toString());
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
completer.complete(false);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Login")),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.start,
|
||||
).marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
||||
controller: userContontroller,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Password')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: passMsg.isNotEmpty ? passMsg : null),
|
||||
controller: pwdController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
completer.complete(false);
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
userNameMsg = "";
|
||||
passMsg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
final cancel = () {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
};
|
||||
userName = userContontroller.text;
|
||||
pass = pwdController.text;
|
||||
if (userName.isEmpty) {
|
||||
userNameMsg = translate("Username missed");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (pass.isEmpty) {
|
||||
passMsg = translate("Password missed");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final resp = await gFFI.userModel.login(userName, pass);
|
||||
if (resp.containsKey('error')) {
|
||||
passMsg = resp['error'];
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
// {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w,
|
||||
// token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}}
|
||||
debugPrint("$resp");
|
||||
completer.complete(true);
|
||||
} catch (err) {
|
||||
print(err.toString());
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: cancel, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
return completer.future;
|
||||
@ -937,55 +945,78 @@ void setPasswordDialog() async {
|
||||
var errMsg1 = "";
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
setState(() {
|
||||
errMsg0 = "";
|
||||
errMsg1 = "";
|
||||
});
|
||||
final pass = p0.text.trim();
|
||||
if (pass.length < 6) {
|
||||
setState(() {
|
||||
errMsg0 = translate("Too short, at least 6 characters.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (p1.text.trim() != pass) {
|
||||
setState(() {
|
||||
errMsg1 = translate("The confirmation is not identical.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
bind.mainSetPermanentPassword(password: pass);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set Password")),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.start,
|
||||
).marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
|
||||
controller: p0,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Confirmation')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
|
||||
controller: p1,
|
||||
),
|
||||
@ -996,35 +1027,11 @@ void setPasswordDialog() async {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
errMsg0 = "";
|
||||
errMsg1 = "";
|
||||
});
|
||||
final pass = p0.text.trim();
|
||||
if (pass.length < 6) {
|
||||
setState(() {
|
||||
errMsg0 = translate("Too short, at least 6 characters.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (p1.text.trim() != pass) {
|
||||
setState(() {
|
||||
errMsg1 = translate("The confirmation is not identical.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
bind.mainSetPermanentPassword(password: pass);
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
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),
|
||||
_TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_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;
|
||||
@ -714,7 +714,7 @@ class _AboutState extends State<_About> {
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin)
|
||||
]),
|
||||
]).marginOnly(left: _kCardLeftMargin);
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1038,52 +1038,117 @@ void changeServer() async {
|
||||
var keyController = TextEditingController(text: key);
|
||||
|
||||
var isInProgress = false;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
[idServerMsg, relayServerMsg, apiServerMsg].forEach((element) {
|
||||
element = "";
|
||||
});
|
||||
isInProgress = true;
|
||||
});
|
||||
cancel() {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
idServer = idController.text.trim();
|
||||
relayServer = relayController.text.trim();
|
||||
apiServer = apiController.text.trim().toLowerCase();
|
||||
key = keyController.text.trim();
|
||||
|
||||
if (idServer.isNotEmpty) {
|
||||
idServerMsg =
|
||||
translate(await bind.mainTestIfValidServer(server: idServer));
|
||||
if (idServerMsg.isEmpty) {
|
||||
oldOptions['custom-rendezvous-server'] = idServer;
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['custom-rendezvous-server'] = "";
|
||||
}
|
||||
|
||||
if (relayServer.isNotEmpty) {
|
||||
relayServerMsg =
|
||||
translate(await bind.mainTestIfValidServer(server: relayServer));
|
||||
if (relayServerMsg.isEmpty) {
|
||||
oldOptions['relay-server'] = relayServer;
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['relay-server'] = "";
|
||||
}
|
||||
|
||||
if (apiServer.isNotEmpty) {
|
||||
if (apiServer.startsWith('http://') ||
|
||||
apiServer.startsWith("https://")) {
|
||||
oldOptions['api-server'] = apiServer;
|
||||
return;
|
||||
} else {
|
||||
apiServerMsg = translate("invalid_http");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['api-server'] = "";
|
||||
}
|
||||
// ok
|
||||
oldOptions['key'] = key;
|
||||
await bind.mainSetOptions(json: jsonEncode(oldOptions));
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("ID/Relay Server")),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('ID Server')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
||||
controller: idController,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Relay Server')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText:
|
||||
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
||||
controller: relayController,
|
||||
@ -1091,22 +1156,22 @@ void changeServer() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('API Server')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText:
|
||||
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
||||
controller: apiController,
|
||||
@ -1114,21 +1179,21 @@ void changeServer() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child:
|
||||
Text("${translate('Key')}:").marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: keyController,
|
||||
@ -1136,83 +1201,20 @@ void changeServer() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
[idServerMsg, relayServerMsg, apiServerMsg].forEach((element) {
|
||||
element = "";
|
||||
});
|
||||
isInProgress = true;
|
||||
});
|
||||
final cancel = () {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
};
|
||||
idServer = idController.text.trim();
|
||||
relayServer = relayController.text.trim();
|
||||
apiServer = apiController.text.trim().toLowerCase();
|
||||
key = keyController.text.trim();
|
||||
|
||||
if (idServer.isNotEmpty) {
|
||||
idServerMsg = translate(
|
||||
await bind.mainTestIfValidServer(server: idServer));
|
||||
if (idServerMsg.isEmpty) {
|
||||
oldOptions['custom-rendezvous-server'] = idServer;
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['custom-rendezvous-server'] = "";
|
||||
}
|
||||
|
||||
if (relayServer.isNotEmpty) {
|
||||
relayServerMsg = translate(
|
||||
await bind.mainTestIfValidServer(server: relayServer));
|
||||
if (relayServerMsg.isEmpty) {
|
||||
oldOptions['relay-server'] = relayServer;
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['relay-server'] = "";
|
||||
}
|
||||
|
||||
if (apiServer.isNotEmpty) {
|
||||
if (apiServer.startsWith('http://') ||
|
||||
apiServer.startsWith("https://")) {
|
||||
oldOptions['api-server'] = apiServer;
|
||||
return;
|
||||
} else {
|
||||
apiServerMsg = translate("invalid_http");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['api-server'] = "";
|
||||
}
|
||||
// ok
|
||||
oldOptions['key'] = key;
|
||||
await bind.mainSetOptions(json: jsonEncode(oldOptions));
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -1231,27 +1233,28 @@ void changeWhiteList() async {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("whitelist_sep")),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
),
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
focusNode: FocusNode()..requestFocus()),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@ -1277,7 +1280,7 @@ void changeWhiteList() async {
|
||||
final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$");
|
||||
for (final ip in ips) {
|
||||
if (!ipMatch.hasMatch(ip)) {
|
||||
msg = translate("Invalid IP") + " $ip";
|
||||
msg = "${translate("Invalid IP")} $ip";
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
@ -1292,6 +1295,7 @@ void changeWhiteList() async {
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -1314,50 +1318,80 @@ void changeSocks5Proxy() async {
|
||||
|
||||
var isInProgress = false;
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
proxyMsg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
cancel() {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
proxy = proxyController.text.trim();
|
||||
username = userController.text.trim();
|
||||
password = pwdController.text.trim();
|
||||
|
||||
if (proxy.isNotEmpty) {
|
||||
proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy));
|
||||
if (proxyMsg.isEmpty) {
|
||||
// ignore
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
await bind.mainSetSocks(
|
||||
proxy: proxy, username: username, password: password);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Socks5 Proxy")),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Hostname')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
||||
controller: proxyController,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Username')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: userController,
|
||||
@ -1365,21 +1399,21 @@ void changeSocks5Proxy() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Password')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: pwdController,
|
||||
@ -1387,50 +1421,20 @@ void changeSocks5Proxy() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
proxyMsg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
final cancel = () {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
};
|
||||
proxy = proxyController.text.trim();
|
||||
username = userController.text.trim();
|
||||
password = pwdController.text.trim();
|
||||
|
||||
if (proxy.isNotEmpty) {
|
||||
proxyMsg =
|
||||
translate(await bind.mainTestIfValidServer(server: proxy));
|
||||
if (proxyMsg.isEmpty) {
|
||||
// ignore
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
await bind.mainSetSocks(
|
||||
proxy: proxy, username: username, password: password);
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
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> {
|
||||
final tabController = DesktopTabController();
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -37,26 +37,27 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
RxBool fullscreen = false.obs;
|
||||
Get.put(fullscreen, tag: 'fullscreen');
|
||||
return Obx(() => DragToResizeArea(
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
tabType: DesktopTabType.main,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
onTap: onAddSetting,
|
||||
is_close: false,
|
||||
),
|
||||
)),
|
||||
),
|
||||
));
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Overlay(initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
onTap: onAddSetting,
|
||||
isClose: false,
|
||||
),
|
||||
));
|
||||
})
|
||||
]),
|
||||
)));
|
||||
}
|
||||
|
||||
void onAddSetting() {
|
||||
|
@ -59,8 +59,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
super.initState();
|
||||
_ffi = FFI();
|
||||
_ffi.connect(widget.id, isFileTransfer: true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
||||
// _ffi.ffiModel.updateEventListener(widget.id);
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
}
|
||||
@ -117,7 +120,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
|
||||
Widget menu({bool isLocal = false}) {
|
||||
return PopupMenuButton<String>(
|
||||
icon: Icon(Icons.more_vert),
|
||||
icon: const Icon(Icons.more_vert),
|
||||
splashRadius: 20,
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
@ -413,6 +417,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
/// watch transfer status
|
||||
Widget statusList() {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size(200, double.infinity),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@ -429,8 +434,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.isRemote ? pi : 0,
|
||||
child: Icon(Icons.send)),
|
||||
SizedBox(
|
||||
child: const Icon(Icons.send)),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
@ -441,7 +446,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Tooltip(
|
||||
message: item.jobName,
|
||||
child: Text(
|
||||
'${item.jobName}',
|
||||
item.jobName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
@ -455,7 +460,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
offstage:
|
||||
item.state != JobState.inProgress,
|
||||
child: Text(
|
||||
'${readableFileSize(item.speed) + "/s"} ')),
|
||||
'${"${readableFileSize(item.speed)}/s"} ')),
|
||||
Offstage(
|
||||
offstage: item.totalSize <= 0,
|
||||
child: Text(
|
||||
@ -475,10 +480,12 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
onPressed: () {
|
||||
model.resumeJob(item.id);
|
||||
},
|
||||
icon: Icon(Icons.restart_alt_rounded)),
|
||||
splashRadius: 20,
|
||||
icon: const Icon(Icons.restart_alt_rounded)),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
icon: const Icon(Icons.delete),
|
||||
splashRadius: 20,
|
||||
onPressed: () {
|
||||
model.jobTable.removeAt(index);
|
||||
model.cancelJob(item.id);
|
||||
@ -500,8 +507,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
itemCount: model.jobTable.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
preferredSize: Size(200, double.infinity));
|
||||
));
|
||||
}
|
||||
|
||||
goBack({bool? isLocal}) {
|
||||
@ -551,12 +557,15 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
model.goHome(isLocal: isLocal);
|
||||
},
|
||||
icon: Icon(Icons.home_outlined)),
|
||||
onPressed: () {
|
||||
model.goHome(isLocal: isLocal);
|
||||
},
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
splashRadius: 20,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_upward),
|
||||
icon: const Icon(Icons.arrow_upward),
|
||||
splashRadius: 20,
|
||||
onPressed: () {
|
||||
goBack(isLocal: isLocal);
|
||||
},
|
||||
@ -622,13 +631,15 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
),
|
||||
))
|
||||
],
|
||||
child: Icon(Icons.search),
|
||||
splashRadius: 20,
|
||||
child: const Icon(Icons.search),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
model.refresh(isLocal: isLocal);
|
||||
},
|
||||
icon: Icon(Icons.refresh)),
|
||||
splashRadius: 20,
|
||||
icon: const Icon(Icons.refresh)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
@ -642,47 +653,52 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final name = TextEditingController();
|
||||
_ffi.dialogManager
|
||||
.show((setState, close) => CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate(
|
||||
"Please enter the folder name"),
|
||||
),
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(false),
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
model.createDir(
|
||||
PathUtil.join(
|
||||
model
|
||||
.getCurrentDir(
|
||||
isLocal)
|
||||
.path,
|
||||
name.value.text,
|
||||
model.getCurrentIsWindows(
|
||||
isLocal)),
|
||||
isLocal: isLocal);
|
||||
close();
|
||||
}
|
||||
},
|
||||
child: Text(translate("OK")))
|
||||
]));
|
||||
_ffi.dialogManager.show((setState, close) {
|
||||
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")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate(
|
||||
"Please enter the folder name"),
|
||||
),
|
||||
controller: name,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: cancel,
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: submit,
|
||||
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(
|
||||
onPressed: () async {
|
||||
final items = isLocal
|
||||
@ -691,7 +707,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
await (model.removeAction(items, isLocal: isLocal));
|
||||
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(
|
||||
angle: isLocal ? 0 : pi,
|
||||
child: Icon(
|
||||
child: const Icon(
|
||||
Icons.send,
|
||||
),
|
||||
),
|
||||
|
@ -25,7 +25,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
static final IconData unselectedIcon = Icons.file_copy_outlined;
|
||||
|
||||
_FileManagerTabPageState(Map<String, dynamic> params) {
|
||||
Get.put(DesktopTabController());
|
||||
Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer));
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@ -62,8 +62,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme =
|
||||
isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light();
|
||||
return SubWindowDragToResizeArea(
|
||||
windowId: windowId(),
|
||||
child: Container(
|
||||
@ -73,14 +71,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
tabType: DesktopTabType.fileTransfer,
|
||||
onClose: () {
|
||||
tabController.clear();
|
||||
},
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
@ -70,38 +70,45 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).grayBg,
|
||||
body: FutureBuilder(future: () async {
|
||||
if (!isRdp) {
|
||||
refreshTunnelConfig();
|
||||
}
|
||||
}(), builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 20, color: MyTheme.color(context).grayBg!)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
buildPrompt(context),
|
||||
Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).bg,
|
||||
border: Border.all(width: 1, color: MyTheme.border)),
|
||||
child:
|
||||
widget.isRDP ? buildRdp(context) : buildTunnel(context),
|
||||
),
|
||||
return Overlay(initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).grayBg,
|
||||
body: FutureBuilder(future: () async {
|
||||
if (!isRdp) {
|
||||
refreshTunnelConfig();
|
||||
}
|
||||
}(), builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 20, color: MyTheme.color(context).grayBg!)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
buildPrompt(context),
|
||||
Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).bg,
|
||||
border:
|
||||
Border.all(width: 1, color: MyTheme.border)),
|
||||
child: widget.isRDP
|
||||
? buildRdp(context)
|
||||
: buildTunnel(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Offstage();
|
||||
}),
|
||||
);
|
||||
);
|
||||
}
|
||||
return const Offstage();
|
||||
}),
|
||||
);
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
buildPrompt(BuildContext context) {
|
||||
|
@ -18,7 +18,7 @@ class PortForwardTabPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
final tabController = Get.put(DesktopTabController());
|
||||
late final DesktopTabController tabController;
|
||||
late final bool isRDP;
|
||||
|
||||
static const IconData selectedIcon = Icons.forward_sharp;
|
||||
@ -26,6 +26,8 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
|
||||
_PortForwardTabPageState(Map<String, dynamic> params) {
|
||||
isRDP = params['isRDP'];
|
||||
tabController = Get.put(DesktopTabController(
|
||||
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward));
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@ -67,7 +69,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||
return SubWindowDragToResizeArea(
|
||||
windowId: windowId(),
|
||||
child: Container(
|
||||
@ -77,14 +78,10 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward,
|
||||
onClose: () {
|
||||
tabController.clear();
|
||||
},
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
@ -17,7 +17,6 @@ import '../../mobile/widgets/dialog.dart';
|
||||
import '../../mobile/widgets/overlay.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/chat_model.dart';
|
||||
import '../../common/shared_state.dart';
|
||||
|
||||
final initText = '\1' * 1024;
|
||||
@ -39,10 +38,11 @@ class RemotePage extends StatefulWidget {
|
||||
class _RemotePageState extends State<RemotePage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
Timer? _timer;
|
||||
bool _showBar = !isWebDesktop;
|
||||
String _value = '';
|
||||
String keyboardMode = "legacy";
|
||||
final _cursorOverImage = false.obs;
|
||||
late RxBool _showRemoteCursor;
|
||||
late RxBool _keyboardEnabled;
|
||||
|
||||
final FocusNode _mobileFocusNode = FocusNode();
|
||||
final FocusNode _physicalFocusNode = FocusNode();
|
||||
@ -59,17 +59,24 @@ class _RemotePageState extends State<RemotePage>
|
||||
PrivacyModeState.init(id);
|
||||
BlockInputState.init(id);
|
||||
CurrentDisplayState.init(id);
|
||||
KeyboardEnabledState.init(id);
|
||||
ShowRemoteCursorState.init(id);
|
||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||
}
|
||||
|
||||
void _removeStates(String id) {
|
||||
PrivacyModeState.delete(id);
|
||||
BlockInputState.delete(id);
|
||||
CurrentDisplayState.delete(id);
|
||||
ShowRemoteCursorState.delete(id);
|
||||
KeyboardEnabledState.delete(id);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initStates(widget.id);
|
||||
_ffi = FFI();
|
||||
_updateTabBarHeight();
|
||||
Get.put(_ffi, tag: widget.id);
|
||||
@ -87,7 +94,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.listenToMouse(true);
|
||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||
// WindowManager.instance.addListener(this);
|
||||
_initStates(widget.id);
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
id: widget.id, arg: 'show-remote-cursor');
|
||||
}
|
||||
|
||||
@override
|
||||
@ -132,7 +140,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
common < oldValue.length &&
|
||||
common < newValue.length &&
|
||||
newValue[common] == oldValue[common];
|
||||
++common);
|
||||
++common) {}
|
||||
for (i = 0; i < oldValue.length - common; ++i) {
|
||||
_ffi.inputKey('VK_BACK');
|
||||
}
|
||||
@ -146,8 +154,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (oldValue.length > 0 &&
|
||||
newValue.length > 0 &&
|
||||
if (oldValue.isNotEmpty &&
|
||||
newValue.isNotEmpty &&
|
||||
oldValue[0] == '\1' &&
|
||||
newValue[0] != '\1') {
|
||||
// clipboard
|
||||
@ -156,7 +164,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (newValue.length == oldValue.length) {
|
||||
// ?
|
||||
} else if (newValue.length < oldValue.length) {
|
||||
final char = 'VK_BACK';
|
||||
const char = 'VK_BACK';
|
||||
_ffi.inputKey(char);
|
||||
} else {
|
||||
final content = newValue.substring(oldValue.length);
|
||||
@ -200,25 +208,9 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.inputKey(label, down: down, press: press ?? false);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context, FfiModel ffiModel) {
|
||||
final hasDisplays = ffiModel.pi.displays.length > 0;
|
||||
final keyboard = ffiModel.permissions['keyboard'] != false;
|
||||
Widget buildBody(BuildContext context) {
|
||||
return Scaffold(
|
||||
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(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
@ -226,8 +218,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: getRawPointerAndKeyBody(
|
||||
getBodyForDesktop(context, keyboard)));
|
||||
child: getRawPointerAndKeyBody(getBodyForDesktop(context)));
|
||||
})
|
||||
],
|
||||
));
|
||||
@ -242,16 +233,12 @@ class _RemotePageState extends State<RemotePage>
|
||||
clientClose(_ffi.dialogManager);
|
||||
return false;
|
||||
},
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: _ffi.ffiModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.imageModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.cursorModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.canvasModel),
|
||||
],
|
||||
child: Consumer<FfiModel>(
|
||||
builder: (context, ffiModel, _child) =>
|
||||
buildBody(context, ffiModel))));
|
||||
child: MultiProvider(providers: [
|
||||
ChangeNotifierProvider.value(value: _ffi.ffiModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.imageModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.cursorModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.canvasModel),
|
||||
], child: buildBody(context)));
|
||||
}
|
||||
|
||||
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
|
||||
@ -332,7 +319,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
key == LogicalKeyboardKey.shiftLeft) {
|
||||
_ffi.shift = false;
|
||||
} else if (key == LogicalKeyboardKey.metaLeft ||
|
||||
key == LogicalKeyboardKey.metaRight || key == LogicalKeyboardKey.superKey) {
|
||||
key == LogicalKeyboardKey.metaRight ||
|
||||
key == LogicalKeyboardKey.superKey) {
|
||||
_ffi.command = false;
|
||||
}
|
||||
sendRawKey(e);
|
||||
@ -340,116 +328,17 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
Widget getRawPointerAndKeyBody(Widget child) {
|
||||
return Consumer<FfiModel>(
|
||||
builder: (context, FfiModel, _child) => MouseRegion(
|
||||
cursor: FfiModel.permissions['keyboard'] != false
|
||||
? SystemMouseCursors.none
|
||||
: MouseCursor.defer,
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
canRequestFocus: true,
|
||||
focusNode: _physicalFocusNode,
|
||||
onFocusChange: (bool v) {
|
||||
_imageFocused = v;
|
||||
},
|
||||
onKey: handleRawKeyEvent,
|
||||
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);
|
||||
}),
|
||||
],
|
||||
),
|
||||
));
|
||||
return FocusScope(
|
||||
autofocus: true,
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
canRequestFocus: true,
|
||||
focusNode: _physicalFocusNode,
|
||||
onFocusChange: (bool v) {
|
||||
_imageFocused = v;
|
||||
},
|
||||
onKey: handleRawKeyEvent,
|
||||
child: child));
|
||||
}
|
||||
|
||||
/// touchMode only:
|
||||
@ -461,7 +350,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
/// mouseMode only:
|
||||
/// DoubleFiner -> right click
|
||||
/// HoldDrag -> left drag
|
||||
|
||||
void _onPointHoverImage(PointerHoverEvent e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (!_isPhysicalMouse) {
|
||||
@ -509,12 +397,16 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (e is PointerScrollEvent) {
|
||||
var dx = e.scrollDelta.dx.toInt();
|
||||
var dy = e.scrollDelta.dy.toInt();
|
||||
if (dx > 0)
|
||||
if (dx > 0) {
|
||||
dx = -1;
|
||||
else if (dx < 0) dx = 1;
|
||||
if (dy > 0)
|
||||
} else if (dx < 0) {
|
||||
dx = 1;
|
||||
}
|
||||
if (dy > 0) {
|
||||
dy = -1;
|
||||
else if (dy < 0) dy = 1;
|
||||
} else if (dy < 0) {
|
||||
dy = 1;
|
||||
}
|
||||
bind.sessionSendMouse(
|
||||
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));
|
||||
}
|
||||
|
||||
Widget getBodyForDesktop(BuildContext context, bool keyboard) {
|
||||
Widget getBodyForDesktop(BuildContext context) {
|
||||
var paints = <Widget>[
|
||||
MouseRegion(onEnter: (evt) {
|
||||
bind.hostStopSystemKeyPropagate(stopped: false);
|
||||
}, onExit: (evt) {
|
||||
bind.hostStopSystemKeyPropagate(stopped: true);
|
||||
}, child: Container(
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||
});
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
listenerBuilder: _buildImageListener,
|
||||
);
|
||||
}),
|
||||
))
|
||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||
});
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
listenerBuilder: _buildImageListener,
|
||||
);
|
||||
}))
|
||||
];
|
||||
final cursor = bind.sessionGetToggleOptionSync(
|
||||
id: widget.id, arg: 'show-remote-cursor');
|
||||
if (keyboard || cursor) {
|
||||
paints.add(CursorPaint(
|
||||
id: widget.id,
|
||||
));
|
||||
}
|
||||
|
||||
paints.add(Obx(() => Visibility(
|
||||
visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue,
|
||||
child: CursorPaint(
|
||||
id: widget.id,
|
||||
))));
|
||||
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
|
||||
paints.add(RemoteMenubar(
|
||||
id: widget.id,
|
||||
@ -601,106 +491,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
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
|
||||
void onWindowEvent(String eventName) {
|
||||
print("window event: $eventName");
|
||||
@ -709,7 +499,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.canvasModel.updateViewStyle();
|
||||
break;
|
||||
case 'maximize':
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_ffi.canvasModel.updateViewStyle();
|
||||
});
|
||||
break;
|
||||
@ -723,6 +513,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
class ImagePaint extends StatelessWidget {
|
||||
final String id;
|
||||
final Rx<bool> cursorOverImage;
|
||||
final Rx<bool> keyboardEnabled;
|
||||
final Widget Function(Widget)? listenerBuilder;
|
||||
final ScrollController _horizontal = ScrollController();
|
||||
final ScrollController _vertical = ScrollController();
|
||||
@ -731,7 +522,8 @@ class ImagePaint extends StatelessWidget {
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.cursorOverImage,
|
||||
this.listenerBuilder = null})
|
||||
required this.keyboardEnabled,
|
||||
this.listenerBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@ -747,25 +539,26 @@ class ImagePaint extends StatelessWidget {
|
||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
));
|
||||
return Center(
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final percentX = _horizontal.position.extentBefore /
|
||||
(_horizontal.position.extentBefore +
|
||||
_horizontal.position.extentInside +
|
||||
_horizontal.position.extentAfter);
|
||||
final percentY = _vertical.position.extentBefore /
|
||||
(_vertical.position.extentBefore +
|
||||
_vertical.position.extentInside +
|
||||
_vertical.position.extentAfter);
|
||||
c.setScrollPercent(percentX, percentY);
|
||||
return false;
|
||||
},
|
||||
child: Obx(() => MouseRegion(
|
||||
cursor: cursorOverImage.value
|
||||
? SystemMouseCursors.none
|
||||
: SystemMouseCursors.basic,
|
||||
child: _buildCrossScrollbar(_buildListener(imageWidget)))),
|
||||
));
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final percentX = _horizontal.position.extentBefore /
|
||||
(_horizontal.position.extentBefore +
|
||||
_horizontal.position.extentInside +
|
||||
_horizontal.position.extentAfter);
|
||||
final percentY = _vertical.position.extentBefore /
|
||||
(_vertical.position.extentBefore +
|
||||
_vertical.position.extentInside +
|
||||
_vertical.position.extentAfter);
|
||||
c.setScrollPercent(percentX, percentY);
|
||||
return false;
|
||||
},
|
||||
child: Obx(() => MouseRegion(
|
||||
cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue)
|
||||
? SystemMouseCursors.none
|
||||
: MouseCursor.defer,
|
||||
child: _buildCrossScrollbar(_buildListener(imageWidget)))),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final imageWidget = SizedBox(
|
||||
width: c.size.width,
|
||||
@ -824,13 +617,12 @@ class CursorPaint extends StatelessWidget {
|
||||
final m = Provider.of<CursorModel>(context);
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
// final adjust = m.adjustForKeyboard();
|
||||
var s = c.scale;
|
||||
return CustomPaint(
|
||||
painter: ImagePainter(
|
||||
image: m.image,
|
||||
x: m.x * s - m.hotx + c.x,
|
||||
y: m.y * s - m.hoty + c.y,
|
||||
scale: 1),
|
||||
x: m.x - m.hotx + c.x / c.scale,
|
||||
y: m.y - m.hoty + c.y / c.scale,
|
||||
scale: c.scale),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -882,205 +674,35 @@ class QualityMonitor extends StatelessWidget {
|
||||
right: 10,
|
||||
child: qualityMonitorModel.show
|
||||
? Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: MyTheme.canvasColor.withAlpha(120),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Speed: ${qualityMonitorModel.data.speed ?? ''}",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
Text(
|
||||
"FPS: ${qualityMonitorModel.data.fps ?? ''}",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
Text(
|
||||
"Delay: ${qualityMonitorModel.data.delay ?? ''} ms",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
Text(
|
||||
"Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
Text(
|
||||
"Codec: ${qualityMonitorModel.data.codecFormat ?? ''}",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: 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')),
|
||||
),
|
||||
]);
|
||||
});
|
||||
: const SizedBox.shrink())));
|
||||
}
|
||||
|
||||
void sendPrompt(String id, bool isMac, String key) {
|
||||
|
@ -19,10 +19,12 @@ class DesktopServerPage extends StatefulWidget {
|
||||
|
||||
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
final tabController = gFFI.serverModel.tabController;
|
||||
@override
|
||||
void initState() {
|
||||
gFFI.ffiModel.updateEventListener("");
|
||||
windowManager.addListener(this);
|
||||
tabController.onRemove = (_, id) => onRemoveId(id);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -39,6 +41,13 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
super.onWindowClose();
|
||||
}
|
||||
|
||||
void onRemoveId(String id) {
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
windowManager.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return MultiProvider(
|
||||
@ -48,22 +57,25 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
],
|
||||
child: Consumer<ServerModel>(
|
||||
builder: (context, serverModel, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: ConnectionManager()),
|
||||
SizedBox.fromSize(size: Size(0, 15.0)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)));
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Overlay(initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: ConnectionManager()),
|
||||
SizedBox.fromSize(size: Size(0, 15.0)),
|
||||
],
|
||||
),
|
||||
);
|
||||
})
|
||||
]),
|
||||
))));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -106,12 +118,11 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
],
|
||||
)
|
||||
: DesktopTab(
|
||||
theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
showTitle: false,
|
||||
showMaximize: false,
|
||||
showMinimize: false,
|
||||
showMinimize: true,
|
||||
showClose: true,
|
||||
controller: serverModel.tabController,
|
||||
tabType: DesktopTabType.cm,
|
||||
pageViewBuilder: (pageView) => Row(children: [
|
||||
Expanded(child: pageView),
|
||||
Consumer<ChatModel>(
|
||||
@ -450,8 +461,10 @@ class _CmControlPanel extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.accent, borderRadius: BorderRadius.circular(10)),
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
checkClickTime(client.id, () => handleAccept(context)),
|
||||
onTap: () => checkClickTime(client.id, () {
|
||||
handleAccept(context);
|
||||
windowManager.minimize();
|
||||
}),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
@ -5,6 +5,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/formatter/id_formatter.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/peer_model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
@ -15,7 +16,7 @@ class _PopupMenuTheme {
|
||||
static const Color commonColor = MyTheme.accent;
|
||||
// kMinInteractiveDimension
|
||||
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>>>
|
||||
@ -46,7 +47,8 @@ class _PeerCard extends StatefulWidget {
|
||||
/// State for the connection page.
|
||||
class _PeerCardState extends State<_PeerCard>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final double _cardRadis = 20;
|
||||
var _menuPos = RelativeRect.fill;
|
||||
final double _cardRadis = 16;
|
||||
final double _borderWidth = 2;
|
||||
final RxBool _iconMoreHover = false.obs;
|
||||
|
||||
@ -118,7 +120,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
? Colors.green
|
||||
: Colors.yellow)),
|
||||
Text(
|
||||
'${peer.id}',
|
||||
formatID('${peer.id}'),
|
||||
style: TextStyle(fontWeight: FontWeight.w400),
|
||||
),
|
||||
]),
|
||||
@ -239,7 +241,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
backgroundColor: peer.online
|
||||
? Colors.green
|
||||
: Colors.yellow)),
|
||||
Text(peer.id)
|
||||
Text(formatID(peer.id))
|
||||
]).paddingSymmetric(vertical: 8),
|
||||
_actionMore(peer),
|
||||
],
|
||||
@ -253,36 +255,36 @@ class _PeerCardState extends State<_PeerCard>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionMore(Peer peer) {
|
||||
return FutureBuilder(
|
||||
future: widget.popupMenuEntryBuilder(context),
|
||||
initialData: const <mod_menu.PopupMenuEntry<String>>[],
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<List<mod_menu.PopupMenuEntry<String>>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Listener(
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => _iconMoreHover.value = true,
|
||||
onExit: (_) => _iconMoreHover.value = false,
|
||||
child: CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor: _iconMoreHover.value
|
||||
? MyTheme.color(context).grayBg!
|
||||
: MyTheme.color(context).bg!,
|
||||
child: mod_menu.PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(Icons.more_vert,
|
||||
size: 18,
|
||||
color: _iconMoreHover.value
|
||||
? MyTheme.color(context).text
|
||||
: MyTheme.color(context).lightText),
|
||||
position: mod_menu.PopupMenuPosition.under,
|
||||
itemBuilder: (BuildContext context) => snapshot.data!,
|
||||
))));
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
});
|
||||
Widget _actionMore(Peer peer) => Listener(
|
||||
onPointerDown: (e) {
|
||||
final x = e.position.dx;
|
||||
final y = e.position.dy;
|
||||
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
onPointerUp: (_) => _showPeerMenu(context, peer.id),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => _iconMoreHover.value = true,
|
||||
onExit: (_) => _iconMoreHover.value = false,
|
||||
child: CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor: _iconMoreHover.value
|
||||
? MyTheme.color(context).grayBg!
|
||||
: MyTheme.color(context).bg!,
|
||||
child: Icon(Icons.more_vert,
|
||||
size: 18,
|
||||
color: _iconMoreHover.value
|
||||
? MyTheme.color(context).text
|
||||
: MyTheme.color(context).lightText))));
|
||||
|
||||
/// Show the peer menu and handle user's choice.
|
||||
/// User might remove the peer or send a file to the peer.
|
||||
void _showPeerMenu(BuildContext context, String id) async {
|
||||
await mod_menu.showMenu(
|
||||
context: context,
|
||||
position: _menuPos,
|
||||
items: await super.widget.popupMenuEntryBuilder(context),
|
||||
elevation: 8,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get the image for the current [platform].
|
||||
@ -411,19 +413,26 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
@protected
|
||||
MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('RDP'),
|
||||
style: style,
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit),
|
||||
onPressed: () => _rdpDialog(id),
|
||||
)
|
||||
],
|
||||
),
|
||||
childBuilder: (TextStyle? style) => Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
height: _PopupMenuTheme.height,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('RDP'),
|
||||
style: style,
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(Icons.edit),
|
||||
onPressed: () => _rdpDialog(id),
|
||||
),
|
||||
))
|
||||
],
|
||||
)),
|
||||
proc: () {
|
||||
_connect(context, id, isRDP: true);
|
||||
},
|
||||
@ -554,47 +563,47 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
isInProgress.value = true;
|
||||
name = controller.text;
|
||||
await bind.mainSetPeerOption(id: id, key: 'alias', value: name);
|
||||
if (isAddressBook) {
|
||||
gFFI.abModel.setPeerOption(id, 'alias', name);
|
||||
await gFFI.abModel.updateAb();
|
||||
}
|
||||
alias.value = await bind.mainGetPeerOption(id: peer.id, key: 'alias');
|
||||
close();
|
||||
isInProgress.value = false;
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Rename')),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Form(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(border: OutlineInputBorder()),
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
decoration:
|
||||
const InputDecoration(border: OutlineInputBorder()),
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(() => Offstage(
|
||||
offstage: isInProgress.isFalse,
|
||||
child: LinearProgressIndicator())),
|
||||
child: const LinearProgressIndicator())),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
isInProgress.value = true;
|
||||
name = controller.text;
|
||||
await bind.mainSetPeerOption(id: id, key: 'alias', value: name);
|
||||
if (isAddressBook) {
|
||||
gFFI.abModel.setPeerOption(id, 'alias', name);
|
||||
await gFFI.abModel.updateAb();
|
||||
}
|
||||
alias.value =
|
||||
await bind.mainGetPeerOption(id: peer.id, key: 'alias');
|
||||
close();
|
||||
isInProgress.value = false;
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
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') {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
menuItems.add(_renameAction(peer.id, false));
|
||||
menuItems.add(_removeAction(peer.id, () async {
|
||||
@ -740,13 +750,23 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||
await gFFI.abModel.updateAb();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Edit Tag")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Wrap(
|
||||
children: tags
|
||||
.map((e) => _buildTag(e, selectedTag, onTap: () {
|
||||
@ -759,26 +779,16 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
.toList(growable: false),
|
||||
),
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
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"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -861,25 +871,35 @@ void _rdpDialog(String id) async {
|
||||
RxBool secure = true.obs;
|
||||
|
||||
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(
|
||||
title: Text('RDP ' + translate('Settings')),
|
||||
title: Text('RDP ${translate('Settings')}'),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text(
|
||||
"${translate('Port')}:",
|
||||
textAlign: TextAlign.start,
|
||||
).marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
@ -888,52 +908,54 @@ void _rdpDialog(String id) async {
|
||||
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])$'))
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(), hintText: '3389'),
|
||||
controller: portController,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.start,
|
||||
).marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(border: OutlineInputBorder()),
|
||||
decoration:
|
||||
const InputDecoration(border: OutlineInputBorder()),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Password')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
@ -948,23 +970,11 @@ void _rdpDialog(String id) async {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
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"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import './material_mod_popup_menu.dart' as mod_menu;
|
||||
@ -174,8 +175,8 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
||||
children: [
|
||||
Text(
|
||||
opt.text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@ -256,8 +257,8 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
children: [
|
||||
Text(
|
||||
opt.text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@ -300,8 +301,8 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
const SizedBox(width: MenuConfig.midPadding),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@ -346,8 +347,8 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
// const SizedBox(width: MenuConfig.midPadding),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@ -450,8 +451,8 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
const SizedBox(width: MenuConfig.midPadding),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@ -491,8 +492,8 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: conf.height),
|
||||
child: childBuilder(
|
||||
const TextStyle(
|
||||
color: Colors.black,
|
||||
TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
)),
|
||||
|
@ -496,9 +496,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
});
|
||||
final quality =
|
||||
await bind.sessionGetCustomImageQuality(id: widget.id);
|
||||
final double initValue = quality != null && quality.isNotEmpty
|
||||
double initValue = quality != null && quality.isNotEmpty
|
||||
? quality[0].toDouble()
|
||||
: 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 rxReplay = rxdart.ReplaySubject<double>();
|
||||
rxReplay
|
||||
@ -513,30 +521,44 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
final slider = Obx(() {
|
||||
return Slider(
|
||||
value: sliderValue.value,
|
||||
max: 100,
|
||||
divisions: 100,
|
||||
label: sliderValue.value.round().toString(),
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
divisions: 90,
|
||||
onChanged: (double value) {
|
||||
sliderValue.value = 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',
|
||||
slider, [btnCancel]);
|
||||
content, [btnCancel]);
|
||||
}
|
||||
}),
|
||||
MenuEntryDivider<String>(),
|
||||
MenuEntrySwitch<String>(
|
||||
text: translate('Show remote cursor'),
|
||||
getter: () async {
|
||||
return bind.sessionGetToggleOptionSync(
|
||||
id: widget.id, arg: 'show-remote-cursor');
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'show-remote-cursor');
|
||||
}),
|
||||
() {
|
||||
final state = ShowRemoteCursorState.find(widget.id);
|
||||
return MenuEntrySwitch2<String>(
|
||||
text: translate('Show remote cursor'),
|
||||
getter: () {
|
||||
return state;
|
||||
},
|
||||
setter: (bool v) async {
|
||||
state.value = v;
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'show-remote-cursor');
|
||||
});
|
||||
}(),
|
||||
MenuEntrySwitch<String>(
|
||||
text: translate('Show quality monitor'),
|
||||
getter: () async {
|
||||
@ -565,12 +587,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
'Lock after session end', 'lock-after-session-end'));
|
||||
if (pi.platform == 'Windows') {
|
||||
displayMenu.add(MenuEntrySwitch2<String>(
|
||||
dismissOnClicked: true,
|
||||
text: translate('Privacy mode'),
|
||||
getter: () {
|
||||
return PrivacyModeState.find(widget.id);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
Navigator.pop(context);
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'privacy-mode');
|
||||
}));
|
||||
@ -620,46 +642,49 @@ void showSetOSPassword(
|
||||
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
||||
controller.text = password;
|
||||
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(
|
||||
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);
|
||||
},
|
||||
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'),
|
||||
),
|
||||
]),
|
||||
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')),
|
||||
),
|
||||
]);
|
||||
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: submit,
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
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/consts.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
@ -59,13 +59,15 @@ class DesktopTabState {
|
||||
|
||||
class DesktopTabController {
|
||||
final state = DesktopTabState().obs;
|
||||
final DesktopTabType tabType;
|
||||
|
||||
/// index, key
|
||||
Function(int, String)? onRemove;
|
||||
|
||||
Function(int)? onSelected;
|
||||
|
||||
void add(TabInfo tab) {
|
||||
DesktopTabController({required this.tabType});
|
||||
|
||||
void add(TabInfo tab, {bool authorized = false}) {
|
||||
if (!isDesktop) return;
|
||||
final index = state.value.tabs.indexWhere((e) => e.key == tab.key);
|
||||
int toIndex;
|
||||
@ -79,6 +81,16 @@ class DesktopTabController {
|
||||
toIndex = state.value.tabs.length - 1;
|
||||
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 {
|
||||
jumpTo(toIndex);
|
||||
} catch (e) {
|
||||
@ -106,6 +118,7 @@ class DesktopTabController {
|
||||
}
|
||||
|
||||
void jumpTo(int index) {
|
||||
if (!isDesktop || index < 0) return;
|
||||
state.update((val) {
|
||||
val!.selected = index;
|
||||
Future.delayed(Duration.zero, (() {
|
||||
@ -114,12 +127,14 @@ class DesktopTabController {
|
||||
}
|
||||
if (val.scrollController.hasClients &&
|
||||
val.scrollController.canScroll &&
|
||||
val.scrollController.itemCount >= index) {
|
||||
val.scrollController.itemCount > index) {
|
||||
val.scrollController.scrollToItem(index, center: true, animate: true);
|
||||
}
|
||||
}));
|
||||
});
|
||||
onSelected?.call(index);
|
||||
if (state.value.tabs.length > index) {
|
||||
onSelected?.call(index);
|
||||
}
|
||||
}
|
||||
|
||||
void closeBy(String? key) {
|
||||
@ -143,8 +158,7 @@ class DesktopTabController {
|
||||
|
||||
class TabThemeConf {
|
||||
double iconSize;
|
||||
TarBarTheme theme;
|
||||
TabThemeConf({required this.iconSize, required this.theme});
|
||||
TabThemeConf({required this.iconSize});
|
||||
}
|
||||
|
||||
typedef TabBuilder = Widget Function(
|
||||
@ -153,9 +167,6 @@ typedef LabelGetter = Rx<String> Function(String key);
|
||||
|
||||
class DesktopTab extends StatelessWidget {
|
||||
final Function(String)? onTabClose;
|
||||
final TarBarTheme theme;
|
||||
final DesktopTabType tabType;
|
||||
final bool isMainWindow;
|
||||
final bool showTabBar;
|
||||
final bool showLogo;
|
||||
final bool showTitle;
|
||||
@ -170,11 +181,12 @@ class DesktopTab extends StatelessWidget {
|
||||
|
||||
final DesktopTabController controller;
|
||||
Rx<DesktopTabState> get state => controller.state;
|
||||
late final DesktopTabType tabType;
|
||||
late final bool isMainWindow;
|
||||
|
||||
const DesktopTab({
|
||||
DesktopTab({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
required this.tabType,
|
||||
this.theme = const TarBarTheme.light(),
|
||||
this.onTabClose,
|
||||
this.showTabBar = true,
|
||||
this.showLogo = true,
|
||||
@ -187,23 +199,26 @@ class DesktopTab extends StatelessWidget {
|
||||
this.onClose,
|
||||
this.tabBuilder,
|
||||
this.labelGetter,
|
||||
}) : isMainWindow =
|
||||
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
|
||||
}) : super(key: key) {
|
||||
tabType = controller.tabType;
|
||||
isMainWindow =
|
||||
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
Offstage(
|
||||
offstage: !showTabBar,
|
||||
child: Container(
|
||||
child: SizedBox(
|
||||
height: _kTabBarHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
SizedBox(
|
||||
height: _kTabBarHeight - 1,
|
||||
child: _buildBar(),
|
||||
),
|
||||
Divider(
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
@ -282,7 +297,7 @@ class DesktopTab extends StatelessWidget {
|
||||
)),
|
||||
Offstage(
|
||||
offstage: !showTitle,
|
||||
child: Text(
|
||||
child: const Text(
|
||||
"RustDesk",
|
||||
style: TextStyle(fontSize: 13),
|
||||
).marginOnly(left: 2))
|
||||
@ -303,7 +318,6 @@ class DesktopTab extends StatelessWidget {
|
||||
child: _ListView(
|
||||
controller: controller,
|
||||
onTabClose: onTabClose,
|
||||
theme: theme,
|
||||
tabBuilder: tabBuilder,
|
||||
labelGetter: labelGetter,
|
||||
)),
|
||||
@ -314,7 +328,8 @@ class DesktopTab extends StatelessWidget {
|
||||
Offstage(offstage: tail == null, child: tail),
|
||||
WindowActionPanel(
|
||||
mainTab: isMainWindow,
|
||||
theme: theme,
|
||||
tabType: tabType,
|
||||
state: state,
|
||||
showMinimize: showMinimize,
|
||||
showMaximize: showMaximize,
|
||||
showClose: showClose,
|
||||
@ -327,7 +342,8 @@ class DesktopTab extends StatelessWidget {
|
||||
|
||||
class WindowActionPanel extends StatelessWidget {
|
||||
final bool mainTab;
|
||||
final TarBarTheme theme;
|
||||
final DesktopTabType tabType;
|
||||
final Rx<DesktopTabState> state;
|
||||
|
||||
final bool showMinimize;
|
||||
final bool showMaximize;
|
||||
@ -337,7 +353,8 @@ class WindowActionPanel extends StatelessWidget {
|
||||
const WindowActionPanel(
|
||||
{Key? key,
|
||||
required this.mainTab,
|
||||
required this.theme,
|
||||
required this.tabType,
|
||||
required this.state,
|
||||
this.showMinimize = true,
|
||||
this.showMaximize = true,
|
||||
this.showClose = true,
|
||||
@ -353,7 +370,6 @@ class WindowActionPanel extends StatelessWidget {
|
||||
child: ActionIcon(
|
||||
message: 'Minimize',
|
||||
icon: IconFont.min,
|
||||
theme: theme,
|
||||
onTap: () {
|
||||
if (mainTab) {
|
||||
windowManager.minimize();
|
||||
@ -361,31 +377,30 @@ class WindowActionPanel extends StatelessWidget {
|
||||
WindowController.fromWindowId(windowId!).minimize();
|
||||
}
|
||||
},
|
||||
is_close: false,
|
||||
isClose: false,
|
||||
)),
|
||||
// TODO: drag makes window restore
|
||||
Offstage(
|
||||
offstage: !showMaximize,
|
||||
child: FutureBuilder(builder: (context, snapshot) {
|
||||
RxBool is_maximized = false.obs;
|
||||
RxBool isMaximized = false.obs;
|
||||
if (mainTab) {
|
||||
windowManager.isMaximized().then((maximized) {
|
||||
is_maximized.value = maximized;
|
||||
isMaximized.value = maximized;
|
||||
});
|
||||
} else {
|
||||
final wc = WindowController.fromWindowId(windowId!);
|
||||
wc.isMaximized().then((maximized) {
|
||||
is_maximized.value = maximized;
|
||||
isMaximized.value = maximized;
|
||||
});
|
||||
}
|
||||
return Obx(
|
||||
() => ActionIcon(
|
||||
message: is_maximized.value ? "Restore" : "Maximize",
|
||||
icon: is_maximized.value ? IconFont.restore : IconFont.max,
|
||||
theme: theme,
|
||||
message: isMaximized.value ? "Restore" : "Maximize",
|
||||
icon: isMaximized.value ? IconFont.restore : IconFont.max,
|
||||
onTap: () {
|
||||
if (mainTab) {
|
||||
if (is_maximized.value) {
|
||||
if (isMaximized.value) {
|
||||
windowManager.unmaximize();
|
||||
} else {
|
||||
windowManager.maximize();
|
||||
@ -393,15 +408,15 @@ class WindowActionPanel extends StatelessWidget {
|
||||
} else {
|
||||
// TODO: subwindow is maximized but first query result is not maximized.
|
||||
final wc = WindowController.fromWindowId(windowId!);
|
||||
if (is_maximized.value) {
|
||||
if (isMaximized.value) {
|
||||
wc.unmaximize();
|
||||
} else {
|
||||
wc.maximize();
|
||||
}
|
||||
}
|
||||
is_maximized.value = !is_maximized.value;
|
||||
isMaximized.value = !isMaximized.value;
|
||||
},
|
||||
is_close: false,
|
||||
isClose: false,
|
||||
),
|
||||
);
|
||||
})),
|
||||
@ -410,40 +425,70 @@ class WindowActionPanel extends StatelessWidget {
|
||||
child: ActionIcon(
|
||||
message: 'Close',
|
||||
icon: IconFont.close,
|
||||
theme: theme,
|
||||
onTap: () {
|
||||
if (mainTab) {
|
||||
windowManager.close();
|
||||
} else {
|
||||
// only hide for multi window, not close
|
||||
Future.delayed(Duration.zero, () {
|
||||
WindowController.fromWindowId(windowId!).hide();
|
||||
});
|
||||
onTap: () async {
|
||||
action() {
|
||||
if (mainTab) {
|
||||
windowManager.close();
|
||||
} else {
|
||||
// only hide for multi window, not close
|
||||
Future.delayed(Duration.zero, () {
|
||||
WindowController.fromWindowId(windowId!).hide();
|
||||
});
|
||||
}
|
||||
onClose?.call();
|
||||
}
|
||||
|
||||
if (tabType != DesktopTabType.main &&
|
||||
state.value.tabs.length > 1) {
|
||||
closeConfirmDialog(action);
|
||||
} else {
|
||||
action();
|
||||
}
|
||||
onClose?.call();
|
||||
},
|
||||
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
|
||||
class _ListView extends StatelessWidget {
|
||||
final DesktopTabController controller;
|
||||
final Function(String key)? onTabClose;
|
||||
final TarBarTheme theme;
|
||||
|
||||
final TabBuilder? tabBuilder;
|
||||
final LabelGetter? labelGetter;
|
||||
|
||||
Rx<DesktopTabState> get state => controller.state;
|
||||
|
||||
_ListView(
|
||||
const _ListView(
|
||||
{required this.controller,
|
||||
required this.onTabClose,
|
||||
required this.theme,
|
||||
this.tabBuilder,
|
||||
this.labelGetter});
|
||||
|
||||
@ -453,7 +498,7 @@ class _ListView extends StatelessWidget {
|
||||
controller: state.value.scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
physics: BouncingScrollPhysics(),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: state.value.tabs.asMap().entries.map((e) {
|
||||
final index = e.key;
|
||||
final tab = e.value;
|
||||
@ -468,7 +513,6 @@ class _ListView extends StatelessWidget {
|
||||
selected: state.value.selected,
|
||||
onClose: () => controller.remove(index),
|
||||
onSelected: () => controller.jumpTo(index),
|
||||
theme: theme,
|
||||
tabBuilder: tabBuilder == null
|
||||
? null
|
||||
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
|
||||
@ -485,31 +529,29 @@ class _ListView extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Tab extends StatefulWidget {
|
||||
late final int index;
|
||||
late final Rx<String> label;
|
||||
late final IconData? selectedIcon;
|
||||
late final IconData? unselectedIcon;
|
||||
late final bool closable;
|
||||
late final int selected;
|
||||
late final Function() onClose;
|
||||
late final Function() onSelected;
|
||||
late final TarBarTheme theme;
|
||||
final int index;
|
||||
final Rx<String> label;
|
||||
final IconData? selectedIcon;
|
||||
final IconData? unselectedIcon;
|
||||
final bool closable;
|
||||
final int selected;
|
||||
final Function() onClose;
|
||||
final Function() onSelected;
|
||||
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
|
||||
tabBuilder;
|
||||
|
||||
_Tab(
|
||||
{Key? key,
|
||||
required this.index,
|
||||
required this.label,
|
||||
this.selectedIcon,
|
||||
this.unselectedIcon,
|
||||
this.tabBuilder,
|
||||
required this.closable,
|
||||
required this.selected,
|
||||
required this.onClose,
|
||||
required this.onSelected,
|
||||
required this.theme})
|
||||
: super(key: key);
|
||||
const _Tab({
|
||||
Key? key,
|
||||
required this.index,
|
||||
required this.label,
|
||||
this.selectedIcon,
|
||||
this.unselectedIcon,
|
||||
this.tabBuilder,
|
||||
required this.closable,
|
||||
required this.selected,
|
||||
required this.onClose,
|
||||
required this.onSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_Tab> createState() => _TabState();
|
||||
@ -529,8 +571,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
isSelected ? widget.selectedIcon : widget.unselectedIcon,
|
||||
size: _kIconSize,
|
||||
color: isSelected
|
||||
? widget.theme.selectedtabIconColor
|
||||
: widget.theme.unSelectedtabIconColor,
|
||||
? MyTheme.tabbar(context).selectedTabIconColor
|
||||
: MyTheme.tabbar(context).unSelectedTabIconColor,
|
||||
).paddingOnly(right: 5));
|
||||
final labelWidget = Obx(() {
|
||||
return Text(
|
||||
@ -538,8 +580,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? widget.theme.selectedTextColor
|
||||
: widget.theme.unSelectedTextColor),
|
||||
? MyTheme.tabbar(context).selectedTextColor
|
||||
: MyTheme.tabbar(context).unSelectedTextColor),
|
||||
);
|
||||
});
|
||||
|
||||
@ -552,8 +594,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return widget.tabBuilder!(icon, labelWidget,
|
||||
TabThemeConf(iconSize: _kIconSize, theme: widget.theme));
|
||||
return widget.tabBuilder!(
|
||||
icon, labelWidget, TabThemeConf(iconSize: _kIconSize));
|
||||
}
|
||||
}
|
||||
|
||||
@ -582,7 +624,6 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
visiable: hover.value && widget.closable,
|
||||
tabSelected: isSelected,
|
||||
onClose: () => widget.onClose(),
|
||||
theme: widget.theme,
|
||||
)))
|
||||
])).paddingSymmetric(horizontal: 10),
|
||||
Offstage(
|
||||
@ -591,7 +632,7 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
width: 1,
|
||||
indent: _kDividerIndent,
|
||||
endIndent: _kDividerIndent,
|
||||
color: widget.theme.dividerColor,
|
||||
color: MyTheme.tabbar(context).dividerColor,
|
||||
thickness: 1,
|
||||
),
|
||||
)
|
||||
@ -614,14 +655,12 @@ class _CloseButton extends StatelessWidget {
|
||||
final bool visiable;
|
||||
final bool tabSelected;
|
||||
final Function onClose;
|
||||
late final TarBarTheme theme;
|
||||
|
||||
_CloseButton({
|
||||
const _CloseButton({
|
||||
Key? key,
|
||||
required this.visiable,
|
||||
required this.tabSelected,
|
||||
required this.onClose,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -637,8 +676,8 @@ class _CloseButton extends StatelessWidget {
|
||||
Icons.close,
|
||||
size: _kIconSize,
|
||||
color: tabSelected
|
||||
? theme.selectedIconColor
|
||||
: theme.unSelectedIconColor,
|
||||
? MyTheme.tabbar(context).selectedIconColor
|
||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||
),
|
||||
),
|
||||
)).paddingOnly(left: 5);
|
||||
@ -648,16 +687,14 @@ class _CloseButton extends StatelessWidget {
|
||||
class ActionIcon extends StatelessWidget {
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final TarBarTheme theme;
|
||||
final Function() onTap;
|
||||
final bool is_close;
|
||||
final bool isClose;
|
||||
const ActionIcon({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.icon,
|
||||
required this.theme,
|
||||
required this.onTap,
|
||||
required this.is_close,
|
||||
required this.isClose,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -665,34 +702,32 @@ class ActionIcon extends StatelessWidget {
|
||||
RxBool hover = false.obs;
|
||||
return Obx(() => Tooltip(
|
||||
message: translate(message),
|
||||
waitDuration: Duration(seconds: 1),
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: InkWell(
|
||||
hoverColor:
|
||||
is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor,
|
||||
hoverColor: isClose
|
||||
? const Color.fromARGB(255, 196, 43, 28)
|
||||
: MyTheme.tabbar(context).hoverColor,
|
||||
onHover: (value) => hover.value = value,
|
||||
child: Container(
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
height: _kTabBarHeight - 1,
|
||||
width: _kTabBarHeight - 1,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: hover.value && is_close
|
||||
color: hover.value && isClose
|
||||
? Colors.white
|
||||
: theme.unSelectedIconColor,
|
||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||
size: _kActionIconSize,
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class AddButton extends StatelessWidget {
|
||||
late final TarBarTheme theme;
|
||||
|
||||
AddButton({
|
||||
const AddButton({
|
||||
Key? key,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -700,41 +735,101 @@ class AddButton extends StatelessWidget {
|
||||
return ActionIcon(
|
||||
message: 'New Connection',
|
||||
icon: IconFont.add,
|
||||
theme: theme,
|
||||
onTap: () =>
|
||||
rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""),
|
||||
is_close: false);
|
||||
isClose: false);
|
||||
}
|
||||
}
|
||||
|
||||
class TarBarTheme {
|
||||
final Color unSelectedtabIconColor;
|
||||
final Color selectedtabIconColor;
|
||||
final Color selectedTextColor;
|
||||
final Color unSelectedTextColor;
|
||||
final Color selectedIconColor;
|
||||
final Color unSelectedIconColor;
|
||||
final Color dividerColor;
|
||||
final Color hoverColor;
|
||||
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||
final Color? selectedTabIconColor;
|
||||
final Color? unSelectedTabIconColor;
|
||||
final Color? selectedTextColor;
|
||||
final Color? unSelectedTextColor;
|
||||
final Color? selectedIconColor;
|
||||
final Color? unSelectedIconColor;
|
||||
final Color? dividerColor;
|
||||
final Color? hoverColor;
|
||||
|
||||
const TarBarTheme.light()
|
||||
: unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241),
|
||||
selectedtabIconColor = MyTheme.accent,
|
||||
selectedTextColor = const Color.fromARGB(255, 26, 26, 26),
|
||||
unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96),
|
||||
selectedIconColor = const Color.fromARGB(255, 26, 26, 26),
|
||||
unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96),
|
||||
dividerColor = const Color.fromARGB(255, 238, 238, 238),
|
||||
hoverColor = const Color.fromARGB(
|
||||
51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E
|
||||
const TabbarTheme(
|
||||
{required this.selectedTabIconColor,
|
||||
required this.unSelectedTabIconColor,
|
||||
required this.selectedTextColor,
|
||||
required this.unSelectedTextColor,
|
||||
required this.selectedIconColor,
|
||||
required this.unSelectedIconColor,
|
||||
required this.dividerColor,
|
||||
required this.hoverColor});
|
||||
|
||||
const TarBarTheme.dark()
|
||||
: unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98),
|
||||
selectedtabIconColor = MyTheme.accent,
|
||||
selectedTextColor = const Color.fromARGB(255, 255, 255, 255),
|
||||
unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207),
|
||||
selectedIconColor = const Color.fromARGB(255, 215, 215, 215),
|
||||
unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255),
|
||||
dividerColor = const Color.fromARGB(255, 64, 64, 64),
|
||||
hoverColor = Colors.black26;
|
||||
static const light = TabbarTheme(
|
||||
selectedTabIconColor: MyTheme.accent,
|
||||
unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241),
|
||||
selectedTextColor: Color.fromARGB(255, 26, 26, 26),
|
||||
unSelectedTextColor: Color.fromARGB(255, 96, 96, 96),
|
||||
selectedIconColor: Color.fromARGB(255, 26, 26, 26),
|
||||
unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
|
||||
dividerColor: Color.fromARGB(255, 238, 238, 238),
|
||||
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.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setAlignment(Alignment.topRight); // ensure
|
||||
})
|
||||
]);
|
||||
runApp(GetMaterialApp(
|
||||
|
@ -45,7 +45,7 @@ class ChatPage extends StatelessWidget implements PageShape {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: chatModel,
|
||||
child: Container(
|
||||
color: MyTheme.grayBg,
|
||||
color: MyTheme.color(context).grayBg,
|
||||
child: Consumer<ChatModel>(builder: (context, chatModel, child) {
|
||||
final currentUser = chatModel.currentUser;
|
||||
return Stack(
|
||||
@ -59,6 +59,14 @@ class ChatPage extends StatelessWidget implements PageShape {
|
||||
messages: chatModel
|
||||
.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(
|
||||
showOtherUsersAvatar: false,
|
||||
showTime: true,
|
||||
|
@ -209,10 +209,19 @@ class ChatModel with ChangeNotifier {
|
||||
id: await bind.mainGetLastRemoteId(),
|
||||
);
|
||||
} else {
|
||||
final client = _ffi.target?.serverModel.clients[id];
|
||||
final client = _ffi.target?.serverModel.clients
|
||||
.firstWhere((client) => client.id == id);
|
||||
if (client == null) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -360,9 +360,9 @@ class FileModel extends ChangeNotifier {
|
||||
Future refresh({bool? isLocal}) async {
|
||||
if (isDesktop) {
|
||||
isLocal = isLocal ?? _isLocal;
|
||||
await isLocal
|
||||
? openDirectory(currentLocalDir.path, isLocal: isLocal)
|
||||
: openDirectory(currentRemoteDir.path, isLocal: isLocal);
|
||||
isLocal
|
||||
? await openDirectory(currentLocalDir.path, isLocal: isLocal)
|
||||
: await openDirectory(currentRemoteDir.path, isLocal: isLocal);
|
||||
} else {
|
||||
await openDirectory(currentDir.path);
|
||||
}
|
||||
@ -393,7 +393,7 @@ class FileModel extends ChangeNotifier {
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint("Failed to openDirectory ${path} :$e");
|
||||
debugPrint("Failed to openDirectory $path: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@ -559,49 +559,55 @@ class FileModel extends ChangeNotifier {
|
||||
Future<bool?> showRemoveDialog(
|
||||
String title, String content, bool showCheckbox) async {
|
||||
return await parent.target?.dialogManager.show<bool>(
|
||||
(setState, Function(bool v) close) => CustomAlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.red),
|
||||
SizedBox(width: 20),
|
||||
Text(title)
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(content),
|
||||
SizedBox(height: 5),
|
||||
Text(translate("This is irreversible!"),
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
showCheckbox
|
||||
? CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate("Do this for all conflicts"),
|
||||
),
|
||||
value: removeCheckboxRemember,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => removeCheckboxRemember = v);
|
||||
},
|
||||
)
|
||||
: SizedBox.shrink()
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(false),
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(true),
|
||||
child: Text(translate("OK"))),
|
||||
]),
|
||||
useAnimation: false);
|
||||
(setState, Function(bool v) close) {
|
||||
cancel() => close(false);
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning, color: Colors.red),
|
||||
const SizedBox(width: 20),
|
||||
Text(title)
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(content),
|
||||
const SizedBox(height: 5),
|
||||
Text(translate("This is irreversible!"),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
showCheckbox
|
||||
? CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate("Do this for all conflicts"),
|
||||
),
|
||||
value: removeCheckboxRemember,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => removeCheckboxRemember = v);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink()
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: cancel,
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: submit,
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
}, useAnimation: false);
|
||||
}
|
||||
|
||||
bool fileConfirmCheckboxRemember = false;
|
||||
@ -610,55 +616,59 @@ class FileModel extends ChangeNotifier {
|
||||
String title, String content, bool showCheckbox) async {
|
||||
fileConfirmCheckboxRemember = false;
|
||||
return await parent.target?.dialogManager.show<bool?>(
|
||||
(setState, Function(bool? v) close) => CustomAlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.red),
|
||||
SizedBox(width: 20),
|
||||
Text(title)
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
translate(
|
||||
"This file exists, skip or overwrite this file?"),
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 5),
|
||||
Text(content),
|
||||
showCheckbox
|
||||
? CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate("Do this for all conflicts"),
|
||||
),
|
||||
value: fileConfirmCheckboxRemember,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => fileConfirmCheckboxRemember = v);
|
||||
},
|
||||
)
|
||||
: SizedBox.shrink()
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(false),
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(null),
|
||||
child: Text(translate("Skip"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(true),
|
||||
child: Text(translate("OK"))),
|
||||
]),
|
||||
useAnimation: false);
|
||||
(setState, Function(bool? v) close) {
|
||||
cancel() => close(false);
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning, color: Colors.red),
|
||||
const SizedBox(width: 20),
|
||||
Text(title)
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(translate("This file exists, skip or overwrite this file?"),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 5),
|
||||
Text(content),
|
||||
showCheckbox
|
||||
? CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate("Do this for all conflicts"),
|
||||
),
|
||||
value: fileConfirmCheckboxRemember,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => fileConfirmCheckboxRemember = v);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink()
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: cancel,
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(null),
|
||||
child: Text(translate("Skip"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: submit,
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
}, useAnimation: false);
|
||||
}
|
||||
|
||||
sendRemoveFile(String path, int fileNum, bool isLocal) {
|
||||
|
@ -32,7 +32,7 @@ class FfiModel with ChangeNotifier {
|
||||
Display _display = Display();
|
||||
|
||||
var _inputBlocked = false;
|
||||
final _permissions = Map<String, bool>();
|
||||
final _permissions = <String, bool>{};
|
||||
bool? _secure;
|
||||
bool? _direct;
|
||||
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) {
|
||||
if (k == 'name' || k.isEmpty) return;
|
||||
_permissions[k] = v == 'true';
|
||||
});
|
||||
print('$_permissions');
|
||||
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
||||
debugPrint('$_permissions');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -146,7 +147,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'clipboard') {
|
||||
Clipboard.setData(ClipboardData(text: evt['content']));
|
||||
} else if (name == 'permission') {
|
||||
parent.target?.ffiModel.updatePermission(evt);
|
||||
parent.target?.ffiModel.updatePermission(evt, peerId);
|
||||
} else if (name == 'chat_client_mode') {
|
||||
parent.target?.chatModel
|
||||
.receive(ChatModel.clientModeID, evt['text'] ?? "");
|
||||
@ -167,10 +168,8 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.loadLastJob(evt);
|
||||
} else if (name == 'update_folder_files') {
|
||||
parent.target?.fileModel.updateFolderFiles(evt);
|
||||
} else if (name == 'try_start_without_auth') {
|
||||
parent.target?.serverModel.loginRequest(evt);
|
||||
} else if (name == 'on_client_authorized') {
|
||||
parent.target?.serverModel.onClientAuthorized(evt);
|
||||
} else if (name == 'add_connection') {
|
||||
parent.target?.serverModel.addConnection(evt);
|
||||
} else if (name == 'on_client_remove') {
|
||||
parent.target?.serverModel.onClientRemove(evt);
|
||||
} 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.
|
||||
void updateEventListener(String peerId) {
|
||||
final void Function(Map<String, dynamic>) cb = (evt) {
|
||||
cb(evt) {
|
||||
var name = evt['name'];
|
||||
if (name == 'msgbox') {
|
||||
handleMsgBox(evt, peerId);
|
||||
@ -205,7 +204,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'clipboard') {
|
||||
Clipboard.setData(ClipboardData(text: evt['content']));
|
||||
} else if (name == 'permission') {
|
||||
parent.target?.ffiModel.updatePermission(evt);
|
||||
parent.target?.ffiModel.updatePermission(evt, peerId);
|
||||
} else if (name == 'chat_client_mode') {
|
||||
parent.target?.chatModel
|
||||
.receive(ChatModel.clientModeID, evt['text'] ?? "");
|
||||
@ -226,10 +225,8 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.loadLastJob(evt);
|
||||
} else if (name == 'update_folder_files') {
|
||||
parent.target?.fileModel.updateFolderFiles(evt);
|
||||
} else if (name == 'try_start_without_auth') {
|
||||
parent.target?.serverModel.loginRequest(evt);
|
||||
} else if (name == 'on_client_authorized') {
|
||||
parent.target?.serverModel.onClientAuthorized(evt);
|
||||
} else if (name == 'add_connection') {
|
||||
parent.target?.serverModel.addConnection(evt);
|
||||
} else if (name == 'on_client_remove') {
|
||||
parent.target?.serverModel.onClientRemove(evt);
|
||||
} else if (name == 'update_quality_status') {
|
||||
@ -239,7 +236,8 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'update_privacy_mode') {
|
||||
updatePrivacyMode(evt, peerId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
platformFFI.setEventCallback(cb);
|
||||
}
|
||||
|
||||
@ -321,15 +319,15 @@ class FfiModel with ChangeNotifier {
|
||||
if (isPeerAndroid) {
|
||||
_touchMode = true;
|
||||
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
|
||||
Timer(Duration(milliseconds: 100), showMobileActionsOverlay);
|
||||
Timer(const Duration(milliseconds: 100), showMobileActionsOverlay);
|
||||
}
|
||||
} else {
|
||||
_touchMode =
|
||||
await bind.sessionGetOption(id: peerId, arg: "touch-mode") != '';
|
||||
}
|
||||
|
||||
if (evt['is_file_transfer'] == "true") {
|
||||
// TODO is file transfer
|
||||
if (parent.target != null &&
|
||||
parent.target!.connType == ConnType.fileTransfer) {
|
||||
parent.target?.fileModel.onReady();
|
||||
} else {
|
||||
_pi.displays = [];
|
||||
@ -464,15 +462,20 @@ enum ScrollStyle {
|
||||
}
|
||||
|
||||
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
|
||||
double _scrollX = 0.0;
|
||||
// scroll offset y percent
|
||||
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;
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
@ -492,9 +495,6 @@ class CanvasModel with ChangeNotifier {
|
||||
double get scrollX => _scrollX;
|
||||
double get scrollY => _scrollY;
|
||||
|
||||
set tabBarHeight(double h) => _tabBarHeight = h;
|
||||
double get tabBarHeight => _tabBarHeight;
|
||||
|
||||
void updateViewStyle() async {
|
||||
final style = await bind.sessionGetOption(id: id, arg: 'view-style');
|
||||
if (style == null) {
|
||||
@ -548,12 +548,11 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
Size get 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) {
|
||||
// On mobile platforms, move the canvas with the cursor.
|
||||
//if (!isDesktop) {
|
||||
final dw = getDisplayWidth() * _scale;
|
||||
final dh = getDisplayHeight() * _scale;
|
||||
var dxOffset = 0;
|
||||
@ -579,8 +578,13 @@ class CanvasModel with ChangeNotifier {
|
||||
if (dxOffset != 0 || dyOffset != 0) {
|
||||
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) {
|
||||
@ -597,11 +601,8 @@ class CanvasModel with ChangeNotifier {
|
||||
if (isWebDesktop) {
|
||||
updateViewStyle();
|
||||
} else {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final canvasWidth = size.width;
|
||||
final canvasHeight = size.height - _tabBarHeight;
|
||||
_x = (canvasWidth - getDisplayWidth() * _scale) / 2;
|
||||
_y = (canvasHeight - getDisplayHeight() * _scale) / 2;
|
||||
_x = (size.width - getDisplayWidth() * _scale) / 2;
|
||||
_y = (size.height - getDisplayHeight() * _scale) / 2;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
@ -613,7 +614,7 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
void updateScale(double v) {
|
||||
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;
|
||||
final px0 = (offset.dx - r.left) * _scale;
|
||||
final py0 = (offset.dy - r.top) * _scale;
|
||||
@ -640,7 +641,7 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
class CursorModel with ChangeNotifier {
|
||||
ui.Image? _image;
|
||||
final _images = Map<int, Tuple3<ui.Image, double, double>>();
|
||||
final _images = <int, Tuple3<ui.Image, double, double>>{};
|
||||
double _x = -10000;
|
||||
double _y = -10000;
|
||||
double _hotx = 0;
|
||||
@ -807,7 +808,7 @@ class CursorModel with ChangeNotifier {
|
||||
// my throw exception, because the listener maybe already dispose
|
||||
notifyListeners();
|
||||
} 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.
|
||||
class FFI {
|
||||
var id = "";
|
||||
@ -923,6 +926,7 @@ class FFI {
|
||||
var alt = false;
|
||||
var command = false;
|
||||
var version = "";
|
||||
var connType = ConnType.defaultConn;
|
||||
|
||||
/// dialogManager use late to ensure init after main page binding [globalKey]
|
||||
late final dialogManager = OverlayDialogManager();
|
||||
@ -1076,9 +1080,11 @@ class FFI {
|
||||
double tabBarHeight = 0.0}) {
|
||||
assert(!(isFileTransfer && isPortForward), "more than one connect type");
|
||||
if (isFileTransfer) {
|
||||
id = 'ft_${id}';
|
||||
connType = ConnType.fileTransfer;
|
||||
id = 'ft_$id';
|
||||
} else if (isPortForward) {
|
||||
id = 'pf_${id}';
|
||||
connType = ConnType.portForward;
|
||||
id = 'pf_$id';
|
||||
} else {
|
||||
chatModel.resetClientMode();
|
||||
canvasModel.id = id;
|
||||
@ -1107,7 +1113,7 @@ class FFI {
|
||||
// every instance will bind a stream
|
||||
this.id = id;
|
||||
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 '../common.dart';
|
||||
import '../common/formatter/id_formatter.dart';
|
||||
import '../desktop/pages/server_page.dart' as Desktop;
|
||||
import '../desktop/widgets/tabbar_widget.dart';
|
||||
import '../mobile/pages/server_page.dart';
|
||||
@ -29,10 +30,10 @@ class ServerModel with ChangeNotifier {
|
||||
String _temporaryPasswordLength = "";
|
||||
|
||||
late String _emptyIdShow;
|
||||
late final TextEditingController _serverId;
|
||||
late final IDTextEditingController _serverId;
|
||||
final _serverPasswd = TextEditingController(text: "");
|
||||
|
||||
final tabController = DesktopTabController();
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.cm);
|
||||
|
||||
List<Client> _clients = [];
|
||||
|
||||
@ -88,7 +89,7 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
ServerModel(this.parent) {
|
||||
_emptyIdShow = translate("Generating ...");
|
||||
_serverId = TextEditingController(text: this._emptyIdShow);
|
||||
_serverId = IDTextEditingController(text: _emptyIdShow);
|
||||
|
||||
Timer.periodic(Duration(seconds: 1), (timer) async {
|
||||
var status = await bind.mainGetOnlineStatue();
|
||||
@ -99,7 +100,7 @@ class ServerModel with ChangeNotifier {
|
||||
_connectStatus = status;
|
||||
notifyListeners();
|
||||
}
|
||||
final res = await bind.mainCheckClientsLength(length: _clients.length);
|
||||
final res = await bind.cmCheckClientsLength(length: _clients.length);
|
||||
if (res != null) {
|
||||
debugPrint("clients not match!");
|
||||
updateClientState(res);
|
||||
@ -208,46 +209,48 @@ class ServerModel with ChangeNotifier {
|
||||
/// Toggle the screen sharing service.
|
||||
toggleService() async {
|
||||
if (_isStart) {
|
||||
final res = await parent.target?.dialogManager
|
||||
.show<bool>((setState, close) => CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_amber_sharp,
|
||||
color: Colors.redAccent, size: 28),
|
||||
SizedBox(width: 10),
|
||||
Text(translate("Warning")),
|
||||
]),
|
||||
content: Text(translate("android_stop_service_tip")),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(),
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
onPressed: () => close(true),
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
));
|
||||
final res =
|
||||
await parent.target?.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("android_stop_service_tip")),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
if (res == true) {
|
||||
stopService();
|
||||
}
|
||||
} else {
|
||||
final res = await parent.target?.dialogManager
|
||||
.show<bool>((setState, close) => CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_amber_sharp,
|
||||
color: Colors.redAccent, size: 28),
|
||||
SizedBox(width: 10),
|
||||
Text(translate("Warning")),
|
||||
]),
|
||||
content: Text(translate("android_service_will_start_tip")),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(),
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
onPressed: () => close(true),
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
));
|
||||
final res =
|
||||
await parent.target?.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("android_service_will_start_tip")),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
if (res == true) {
|
||||
startService();
|
||||
}
|
||||
@ -300,7 +303,7 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
_fetchID() async {
|
||||
final old = _serverId.text;
|
||||
final old = _serverId.id;
|
||||
var count = 0;
|
||||
const maxCount = 10;
|
||||
while (count < maxCount) {
|
||||
@ -309,12 +312,12 @@ class ServerModel with ChangeNotifier {
|
||||
if (id.isEmpty) {
|
||||
continue;
|
||||
} 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++;
|
||||
if (_serverId.text != old) {
|
||||
if (_serverId.id != old) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -344,23 +347,21 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
// force
|
||||
updateClientState([String? json]) async {
|
||||
var res = await bind.mainGetClientsState();
|
||||
var res = await bind.cmGetClientsState();
|
||||
try {
|
||||
final List clientsJson = jsonDecode(res);
|
||||
if (isDesktop && clientsJson.isEmpty && _clients.isNotEmpty) {
|
||||
// exit cm when >1 peers to no peers
|
||||
exit(0);
|
||||
}
|
||||
_clients.clear();
|
||||
tabController.state.value.tabs.clear();
|
||||
for (var clientJson in clientsJson) {
|
||||
final client = Client.fromJson(clientJson);
|
||||
_clients.add(client);
|
||||
tabController.add(TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)));
|
||||
tabController.add(
|
||||
TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)),
|
||||
authorized: client.authorized);
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
@ -368,70 +369,89 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void loginRequest(Map<String, dynamic> evt) {
|
||||
void addConnection(Map<String, dynamic> evt) {
|
||||
try {
|
||||
final client = Client.fromJson(jsonDecode(evt["client"]));
|
||||
if (_clients.any((c) => c.id == client.id)) {
|
||||
return;
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
_clients.add(client);
|
||||
tabController.add(TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)));
|
||||
scrollToBottom();
|
||||
notifyListeners();
|
||||
if (isAndroid) showLoginDialog(client);
|
||||
}
|
||||
_clients.add(client);
|
||||
tabController.add(TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)));
|
||||
scrollToBottom();
|
||||
notifyListeners();
|
||||
if (isAndroid) showLoginDialog(client);
|
||||
} catch (e) {
|
||||
debugPrint("Failed to call loginRequest,error:$e");
|
||||
}
|
||||
}
|
||||
|
||||
void showLoginDialog(Client client) {
|
||||
parent.target?.dialogManager.show(
|
||||
(setState, close) => CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(translate(client.isFileTransfer
|
||||
? "File Connection"
|
||||
: "Screen Connection")),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
icon: Icon(Icons.close))
|
||||
]),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("Do you accept?")),
|
||||
clientInfo(client),
|
||||
Text(
|
||||
translate("android_new_connection_tip"),
|
||||
style: TextStyle(color: Colors.black54),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(translate("Dismiss")),
|
||||
onPressed: () {
|
||||
sendLoginResponse(client, false);
|
||||
close();
|
||||
}),
|
||||
ElevatedButton(
|
||||
child: Text(translate("Accept")),
|
||||
onPressed: () {
|
||||
sendLoginResponse(client, true);
|
||||
close();
|
||||
}),
|
||||
],
|
||||
parent.target?.dialogManager.show((setState, close) {
|
||||
cancel() {
|
||||
sendLoginResponse(client, false);
|
||||
close();
|
||||
}
|
||||
|
||||
submit() {
|
||||
sendLoginResponse(client, true);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title:
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text(translate(
|
||||
client.isFileTransfer ? "File Connection" : "Screen Connection")),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
icon: const Icon(Icons.close))
|
||||
]),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("Do you accept?")),
|
||||
clientInfo(client),
|
||||
Text(
|
||||
translate("android_new_connection_tip"),
|
||||
style: const TextStyle(color: Colors.black54),
|
||||
),
|
||||
tag: getLoginDialogTag(client.id));
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: cancel, child: Text(translate("Dismiss"))),
|
||||
ElevatedButton(onPressed: submit, child: Text(translate("Accept"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
}, tag: getLoginDialogTag(client.id));
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
@ -471,14 +491,18 @@ class ServerModel with ChangeNotifier {
|
||||
} else {
|
||||
_clients[index].authorized = true;
|
||||
}
|
||||
tabController.add(TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)));
|
||||
tabController.add(
|
||||
TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)),
|
||||
authorized: true);
|
||||
scrollToBottom();
|
||||
notifyListeners();
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
debugPrint("onClientAuthorized:$e");
|
||||
}
|
||||
}
|
||||
|
||||
void onClientRemove(Map<String, dynamic> evt) {
|
||||
@ -486,8 +510,10 @@ class ServerModel with ChangeNotifier {
|
||||
final id = int.parse(evt['id'] as String);
|
||||
if (_clients.any((c) => c.id == id)) {
|
||||
final index = _clients.indexWhere((client) => client.id == id);
|
||||
_clients.removeAt(index);
|
||||
tabController.remove(index);
|
||||
if (index >= 0) {
|
||||
_clients.removeAt(index);
|
||||
tabController.remove(index);
|
||||
}
|
||||
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
|
||||
parent.target?.invokeMethod("cancel_notification", id);
|
||||
}
|
||||
@ -558,24 +584,29 @@ String getLoginDialogTag(int id) {
|
||||
}
|
||||
|
||||
showInputWarnAlert(FFI ffi) {
|
||||
ffi.dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: Text(translate("How to get Android input permission?")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(translate("android_input_permission_tip1")),
|
||||
SizedBox(height: 10),
|
||||
Text(translate("android_input_permission_tip2")),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(child: Text(translate("Cancel")), onPressed: close),
|
||||
ElevatedButton(
|
||||
child: Text(translate("Open System Setting")),
|
||||
onPressed: () {
|
||||
ffi.serverModel.initInput();
|
||||
close();
|
||||
}),
|
||||
ffi.dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
ffi.serverModel.initInput();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("How to get Android input permission?")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(translate("android_input_permission_tip1")),
|
||||
const SizedBox(height: 10),
|
||||
Text(translate("android_input_permission_tip2")),
|
||||
],
|
||||
));
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
onPressed: submit, child: Text(translate("Open System Setting"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
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
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
|
@ -62,7 +62,7 @@ dependencies:
|
||||
desktop_multi_window:
|
||||
git:
|
||||
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
||||
ref: e0368a023ba195462acc00d33ab361b499f0e413
|
||||
ref: fee851fa43116e0b91c39acd0ec37063dc6015f8
|
||||
freezed_annotation: ^2.0.3
|
||||
tray_manager:
|
||||
git:
|
||||
|
@ -1666,6 +1666,7 @@ pub async fn handle_login_from_ui(
|
||||
/// Interface for client to send data and commands.
|
||||
#[async_trait]
|
||||
pub trait Interface: Send + Clone + 'static + Sized {
|
||||
/// Send message data to remote peer.
|
||||
fn send(&self, data: Data);
|
||||
fn msgbox(&self, msgtype: &str, title: &str, text: &str);
|
||||
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"))]
|
||||
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) {
|
||||
Ok(fd) => make_fd_to_json(fd),
|
||||
Ok(fd) => make_fd_to_json(fd.id, fd.path, &fd.entries),
|
||||
Err(_) => "".into(),
|
||||
}
|
||||
}
|
||||
|
@ -2,17 +2,11 @@ use crate::client::{
|
||||
Client, CodecFormat, FileManager, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
|
||||
SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED,
|
||||
};
|
||||
use crate::common;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
|
||||
|
||||
#[cfg(windows)]
|
||||
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::ui_session_interface::{InvokeUiSession, Session};
|
||||
use crate::{client::Data, client::Interface};
|
||||
|
||||
use hbb_common::config::{PeerConfig, TransferSerde};
|
||||
@ -28,14 +22,14 @@ use hbb_common::tokio::{
|
||||
sync::mpsc,
|
||||
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 std::collections::HashMap;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct Remote<T: InvokeUi> {
|
||||
pub struct Remote<T: InvokeUiSession> {
|
||||
handler: Session<T>,
|
||||
video_sender: MediaSender,
|
||||
audio_sender: MediaSender,
|
||||
@ -49,13 +43,13 @@ pub struct Remote<T: InvokeUi> {
|
||||
last_update_jobs_status: (Instant, HashMap<i32, u64>),
|
||||
first_frame: bool,
|
||||
#[cfg(windows)]
|
||||
clipboard_file_context: Option<Box<CliprdrClientContext>>,
|
||||
clipboard_file_context: Option<Box<clipboard::cliprdr::CliprdrClientContext>>,
|
||||
data_count: Arc<AtomicUsize>,
|
||||
frame_count: Arc<AtomicUsize>,
|
||||
video_format: CodecFormat,
|
||||
}
|
||||
|
||||
impl<T: InvokeUi> Remote<T> {
|
||||
impl<T: InvokeUiSession> Remote<T> {
|
||||
pub fn new(
|
||||
handler: Session<T>,
|
||||
video_sender: MediaSender,
|
||||
@ -113,7 +107,7 @@ impl<T: InvokeUi> Remote<T> {
|
||||
#[cfg(not(windows))]
|
||||
let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::<i32>();
|
||||
#[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));
|
||||
|
||||
@ -159,7 +153,7 @@ impl<T: InvokeUi> Remote<T> {
|
||||
#[cfg(windows)]
|
||||
match _msg {
|
||||
Some((_, clip)) => {
|
||||
allow_err!(peer.send(&clip_2_msg(clip)).await);
|
||||
allow_err!(peer.send(&crate::clipboard_file::clip_2_msg(clip)).await);
|
||||
}
|
||||
None => {
|
||||
// unreachable!()
|
||||
@ -275,51 +269,6 @@ impl<T: InvokeUi> Remote<T> {
|
||||
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 {
|
||||
match data {
|
||||
Data::Close => {
|
||||
@ -381,8 +330,13 @@ impl<T: InvokeUi> Remote<T> {
|
||||
to,
|
||||
job.files().len()
|
||||
);
|
||||
// let m = make_fd(job.id(), job.files(), true);
|
||||
// self.handler.call("updateFolderFiles", &make_args!(m)); // TODO
|
||||
self.handler.update_folder_files(
|
||||
job.id(),
|
||||
job.files(),
|
||||
path,
|
||||
!is_remote,
|
||||
true,
|
||||
);
|
||||
#[cfg(not(windows))]
|
||||
let files = job.files().clone();
|
||||
#[cfg(windows)]
|
||||
@ -441,8 +395,13 @@ impl<T: InvokeUi> Remote<T> {
|
||||
to,
|
||||
job.files().len()
|
||||
);
|
||||
// let m = make_fd(job.id(), job.files(), true);
|
||||
// self.handler.call("updateFolderFiles", &make_args!(m));
|
||||
self.handler.update_folder_files(
|
||||
job.id(),
|
||||
job.files(),
|
||||
path,
|
||||
!is_remote,
|
||||
true,
|
||||
);
|
||||
job.is_last_job = true;
|
||||
self.read_jobs.push(job);
|
||||
self.timer = time::interval(MILLI1);
|
||||
@ -493,7 +452,6 @@ impl<T: InvokeUi> Remote<T> {
|
||||
file_num,
|
||||
job.files[i].name.clone(),
|
||||
);
|
||||
self.handler.confirm_delete_files(id, file_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -554,8 +512,13 @@ impl<T: InvokeUi> Remote<T> {
|
||||
} else {
|
||||
match fs::get_recursive_files(&path, include_hidden) {
|
||||
Ok(entries) => {
|
||||
// let m = make_fd(id, &entries, true);
|
||||
// self.handler.call("updateFolderFiles", &make_args!(m));
|
||||
self.handler.update_folder_files(
|
||||
id,
|
||||
&entries,
|
||||
path.clone(),
|
||||
!is_remote,
|
||||
false,
|
||||
);
|
||||
self.remove_jobs
|
||||
.insert(id, RemoveJob::new(entries, path, sep, is_remote));
|
||||
}
|
||||
@ -757,28 +720,28 @@ impl<T: InvokeUi> Remote<T> {
|
||||
}
|
||||
Some(login_response::Union::PeerInfo(pi)) => {
|
||||
self.handler.handle_peer_info(pi);
|
||||
// self.check_clipboard_file_context();
|
||||
// if !(self.handler.is_file_transfer()
|
||||
// || self.handler.is_port_forward()
|
||||
// || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst)
|
||||
// || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst)
|
||||
// || self.handler.lc.read().unwrap().disable_clipboard)
|
||||
// {
|
||||
// let txt = self.old_clipboard.lock().unwrap().clone();
|
||||
// if !txt.is_empty() {
|
||||
// let msg_out = crate::create_clipboard_msg(txt);
|
||||
// let sender = self.sender.clone();
|
||||
// tokio::spawn(async move {
|
||||
// // due to clipboard service interval time
|
||||
// sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await;
|
||||
// sender.send(Data::Message(msg_out)).ok();
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
self.check_clipboard_file_context();
|
||||
if !(self.handler.is_file_transfer()
|
||||
|| self.handler.is_port_forward()
|
||||
|| !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst)
|
||||
|| !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst)
|
||||
|| self.handler.lc.read().unwrap().disable_clipboard)
|
||||
{
|
||||
let txt = self.old_clipboard.lock().unwrap().clone();
|
||||
if !txt.is_empty() {
|
||||
let msg_out = crate::create_clipboard_msg(txt);
|
||||
let sender = self.sender.clone();
|
||||
tokio::spawn(async move {
|
||||
// due to clipboard service interval time
|
||||
sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await;
|
||||
sender.send(Data::Message(msg_out)).ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if self.handler.is_file_transfer() {
|
||||
// self.load_last_jobs().await;
|
||||
// }
|
||||
if self.handler.is_file_transfer() {
|
||||
self.handler.load_last_jobs();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@ -812,8 +775,8 @@ impl<T: InvokeUi> Remote<T> {
|
||||
Some(message::Union::Cliprdr(clip)) => {
|
||||
if !self.handler.lc.read().unwrap().disable_clipboard {
|
||||
if let Some(context) = &mut self.clipboard_file_context {
|
||||
if let Some(clip) = msg_2_clip(clip) {
|
||||
server_clip_file(context, 0, clip);
|
||||
if let Some(clip) = crate::clipboard_file::msg_2_clip(clip) {
|
||||
clipboard::server_clip_file(context, 0, clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -831,11 +794,13 @@ impl<T: InvokeUi> Remote<T> {
|
||||
fs::transform_windows_path(&mut entries);
|
||||
}
|
||||
}
|
||||
// let mut m = make_fd(fd.id, &entries, fd.id > 0);
|
||||
// if fd.id <= 0 {
|
||||
// m.set_item("path", fd.path);
|
||||
// }
|
||||
// self.handler.call("updateFolderFiles", &make_args!(m));
|
||||
self.handler.update_folder_files(
|
||||
fd.id,
|
||||
&entries,
|
||||
fd.path,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) {
|
||||
log::info!("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;
|
||||
if enabled == self.clipboard_file_context.is_none() {
|
||||
self.clipboard_file_context = if enabled {
|
||||
match create_clipboard_file_context(true, false) {
|
||||
match clipboard::create_cliprdr_context(true, false) {
|
||||
Ok(context) => {
|
||||
log::info!("clipboard context for file transfer created.");
|
||||
Some(context)
|
||||
|
@ -667,51 +667,6 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess
|
||||
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"))]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref IS_X11: Mutex<bool> = Mutex::new(false);
|
||||
|
801
src/flutter.rs
801
src/flutter.rs
@ -5,16 +5,17 @@ use std::{
|
||||
|
||||
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};
|
||||
|
||||
pub(super) const APP_TYPE_MAIN: &str = "main";
|
||||
#[allow(dead_code)]
|
||||
pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote";
|
||||
#[allow(dead_code)]
|
||||
pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer";
|
||||
|
||||
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) {
|
||||
let colors = hbb_common::compress::decompress(&cd.colors);
|
||||
self.push_event(
|
||||
@ -87,6 +88,7 @@ impl InvokeUi for FlutterHandler {
|
||||
self.push_event("permission", vec![(name, &value.to_string())]);
|
||||
}
|
||||
|
||||
// unused in flutter
|
||||
fn close_success(&self) {}
|
||||
|
||||
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) {
|
||||
self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]);
|
||||
fn job_error(&self, id: i32, err: String, file_num: i32) {
|
||||
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) {
|
||||
@ -130,31 +139,43 @@ impl InvokeUi for FlutterHandler {
|
||||
);
|
||||
}
|
||||
|
||||
fn clear_all_jobs(&self) {
|
||||
// todo!()
|
||||
// unused in flutter
|
||||
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 add_job(
|
||||
fn update_folder_files(
|
||||
&self,
|
||||
id: i32,
|
||||
entries: &Vec<FileEntry>,
|
||||
path: String,
|
||||
to: String,
|
||||
file_num: i32,
|
||||
show_hidden: bool,
|
||||
is_remote: bool,
|
||||
is_local: bool,
|
||||
only_count: 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) {
|
||||
// todo!()
|
||||
}
|
||||
// unused in flutter
|
||||
fn update_transfer_list(&self) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn confirm_delete_files(&self, id: i32, i: i32, name: String) {
|
||||
// todo!()
|
||||
}
|
||||
// unused in flutter // TEST flutter
|
||||
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) {
|
||||
self.push_event(
|
||||
@ -180,6 +201,7 @@ impl InvokeUi for FlutterHandler {
|
||||
);
|
||||
}
|
||||
|
||||
// unused in flutter
|
||||
fn adapt_size(&self) {}
|
||||
|
||||
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()
|
||||
.initialize(session_id, conn_type);
|
||||
|
||||
if let Some(same_id_session) = SESSIONS
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(id.to_owned(), session)
|
||||
{
|
||||
if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) {
|
||||
same_id_session.close();
|
||||
}
|
||||
|
||||
@ -320,624 +338,88 @@ pub fn session_start_(id: &str, event_stream: StreamSink<EventToUI>) -> ResultTy
|
||||
// Server Side
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
pub mod connection_manager {
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
iter::FromIterator,
|
||||
sync::{
|
||||
atomic::{AtomicI64, Ordering},
|
||||
RwLock,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_derive::Serialize;
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
use hbb_common::log;
|
||||
#[cfg(any(target_os = "android"))]
|
||||
use scrap::android::call_main_service_set_by_name;
|
||||
|
||||
use crate::ipc::Data;
|
||||
use crate::ipc::{self, new_listener, Connection};
|
||||
use crate::ui_cm_interface::InvokeUiCM;
|
||||
|
||||
use super::GLOBAL_EVENT_STREAM;
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
struct Client {
|
||||
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>,
|
||||
#[derive(Clone)]
|
||||
struct FlutterHandler {}
|
||||
|
||||
impl InvokeUiCM for FlutterHandler {
|
||||
//TODO port_forward
|
||||
fn add_connection(&self, client: &crate::ui_cm_interface::Client) {
|
||||
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("add_connection", Some(&client_json), None)
|
||||
{
|
||||
log::debug!("call_service_set_by_name fail,{}", e);
|
||||
}
|
||||
// send to UI, refresh widget
|
||||
self.push_event("add_connection", vec![("client", &client_json)]); // TODO use add_connection
|
||||
}
|
||||
|
||||
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)],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CLIENTS: RwLock<HashMap<i32,Client>> = Default::default();
|
||||
impl FlutterHandler {
|
||||
fn push_event(&self, name: &str, event: Vec<(&str, &str)>) {
|
||||
let mut h: HashMap<&str, &str> = event.iter().cloned().collect();
|
||||
assert!(h.get("name").is_none());
|
||||
h.insert("name", name);
|
||||
|
||||
if let Some(s) = GLOBAL_EVENT_STREAM
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(super::APP_TYPE_MAIN)
|
||||
{
|
||||
s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned()));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
use crate::ui_cm_interface::{start_ipc, ConnectionManager};
|
||||
|
||||
#[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(target_os = "linux")]
|
||||
std::thread::spawn(crate::ipc::start_pa);
|
||||
|
||||
#[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
|
||||
let cm = ConnectionManager {
|
||||
ui_handler: FlutterHandler {},
|
||||
};
|
||||
std::thread::spawn(move || start_ipc(cm));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn start_channel(rx: UnboundedReceiver<Data>, tx: UnboundedSender<Data>) {
|
||||
std::thread::spawn(move || start_listen(rx, tx));
|
||||
}
|
||||
use hbb_common::tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
|
||||
#[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>,
|
||||
pub fn start_channel(
|
||||
rx: UnboundedReceiver<crate::ipc::Data>,
|
||||
tx: UnboundedSender<crate::ipc::Data>,
|
||||
) {
|
||||
let mut client = Client {
|
||||
id,
|
||||
authorized,
|
||||
is_file_transfer,
|
||||
name: name.clone(),
|
||||
peer_id: peer_id.clone(),
|
||||
keyboard,
|
||||
clipboard,
|
||||
audio,
|
||||
file,
|
||||
restart,
|
||||
tx,
|
||||
use crate::ui_cm_interface::start_listen;
|
||||
let cm = crate::ui_cm_interface::ConnectionManager {
|
||||
ui_handler: FlutterHandler {},
|
||||
};
|
||||
if authorized {
|
||||
client.authorized = true;
|
||||
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("on_client_authorized", Some(&client_json), None)
|
||||
{
|
||||
log::debug!("call_service_set_by_name fail,{}", e);
|
||||
}
|
||||
// send to UI, refresh widget
|
||||
push_event("on_client_authorized", vec![("client", &client_json)]);
|
||||
} 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)>) {
|
||||
let mut h: HashMap<&str, &str> = event.iter().cloned().collect();
|
||||
assert!(h.get("name").is_none());
|
||||
h.insert("name", name);
|
||||
|
||||
if let Some(s) = GLOBAL_EVENT_STREAM
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(super::APP_TYPE_MAIN)
|
||||
{
|
||||
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) {
|
||||
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
||||
allow_err!(client.tx.send(Data::ClickTime(0)));
|
||||
};
|
||||
}
|
||||
|
||||
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 }));
|
||||
};
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
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 {
|
||||
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,
|
||||
} => {
|
||||
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),
|
||||
}
|
||||
std::thread::spawn(move || start_listen(cm, rx, tx));
|
||||
}
|
||||
}
|
||||
|
||||
@ -950,30 +432,47 @@ pub fn get_session_id(id: String) -> String {
|
||||
};
|
||||
}
|
||||
|
||||
// async fn start_one_port_forward(
|
||||
// handler: Session,
|
||||
// port: i32,
|
||||
// remote_host: String,
|
||||
// remote_port: i32,
|
||||
// receiver: mpsc::UnboundedReceiver<Data>,
|
||||
// key: &str,
|
||||
// token: &str,
|
||||
// ) {
|
||||
// if let Err(err) = crate::port_forward::listen(
|
||||
// handler.id.clone(),
|
||||
// String::new(), // TODO
|
||||
// port,
|
||||
// handler.clone(),
|
||||
// receiver,
|
||||
// key,
|
||||
// token,
|
||||
// handler.lc.clone(),
|
||||
// remote_host,
|
||||
// remote_port,
|
||||
// )
|
||||
// .await
|
||||
// {
|
||||
// handler.on_error(&format!("Failed to listen on {}: {}", port, err));
|
||||
// }
|
||||
// log::info!("port forward (:{}) exit", port);
|
||||
// }
|
||||
pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> String {
|
||||
let mut fd_json = serde_json::Map::new();
|
||||
fd_json.insert("id".into(), json!(id));
|
||||
fd_json.insert("path".into(), json!(path));
|
||||
|
||||
let mut entries_out = vec![];
|
||||
for entry in 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_out.push(entry_map);
|
||||
}
|
||||
fd_json.insert("entries".into(), json!(entries_out));
|
||||
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())
|
||||
}
|
@ -13,8 +13,6 @@ use hbb_common::{
|
||||
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::start_server;
|
||||
use crate::ui_interface;
|
||||
@ -31,7 +29,7 @@ use crate::ui_interface::{
|
||||
};
|
||||
use crate::{
|
||||
client::file_trait::FileManager,
|
||||
flutter::{session_add, session_start_},
|
||||
flutter::{make_fd_to_json, session_add, session_start_},
|
||||
};
|
||||
|
||||
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 {
|
||||
if let Some(_) = SESSIONS.read().unwrap().get(&id) {
|
||||
if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) {
|
||||
return make_fd_to_json(fd);
|
||||
}
|
||||
if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) {
|
||||
return make_fd_to_json(fd.id, path, &fd.entries);
|
||||
}
|
||||
"".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) {
|
||||
if let Some(_) = SESSIONS.read().unwrap().get(&id) {
|
||||
// return session.load_last_jobs();
|
||||
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
|
||||
return session.load_last_jobs();
|
||||
} else {
|
||||
// a tip for flutter dev
|
||||
eprintln!(
|
||||
@ -711,13 +707,13 @@ pub fn main_get_online_statue() -> i64 {
|
||||
ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone()
|
||||
}
|
||||
|
||||
pub fn main_get_clients_state() -> String {
|
||||
get_clients_state()
|
||||
pub fn cm_get_clients_state() -> String {
|
||||
crate::ui_cm_interface::get_clients_state()
|
||||
}
|
||||
|
||||
pub fn main_check_clients_length(length: usize) -> Option<String> {
|
||||
if length != get_clients_length() {
|
||||
Some(get_clients_state())
|
||||
pub fn cm_check_clients_length(length: usize) -> Option<String> {
|
||||
if length != crate::ui_cm_interface::get_clients_length() {
|
||||
Some(crate::ui_cm_interface::get_clients_state())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -793,7 +789,7 @@ pub fn main_set_home_dir(home: String) {
|
||||
pub fn main_stop_service() {
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
@ -801,7 +797,7 @@ pub fn main_stop_service() {
|
||||
pub fn main_start_service() {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
Config::set_option("stop-service".into(), "".into());
|
||||
config::Config::set_option("stop-service".into(), "".into());
|
||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||
}
|
||||
#[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) {
|
||||
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) {
|
||||
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) {
|
||||
connection_manager::close_conn(conn_id);
|
||||
crate::ui_cm_interface::close(conn_id);
|
||||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
connection_manager::switch_permission(conn_id, name, enabled)
|
||||
crate::ui_cm_interface::switch_permission(conn_id, name, enabled)
|
||||
}
|
||||
|
||||
pub fn main_get_icon() -> String {
|
||||
|
@ -49,6 +49,7 @@ mod tray;
|
||||
|
||||
mod ui_interface;
|
||||
mod ui_session_interface;
|
||||
mod ui_cm_interface;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod clipboard_file;
|
||||
|
100
src/ui.rs
100
src/ui.rs
@ -14,7 +14,6 @@ use hbb_common::{
|
||||
log,
|
||||
protobuf::Message as _,
|
||||
rendezvous_proto::*,
|
||||
sleep,
|
||||
tcp::FramedStream,
|
||||
tokio::{self, sync::mpsc, time},
|
||||
};
|
||||
@ -87,7 +86,7 @@ pub fn start(args: &mut [String]) {
|
||||
}
|
||||
#[cfg(windows)]
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@ -125,7 +124,7 @@ pub fn start(args: &mut [String]) {
|
||||
page = "install.html";
|
||||
} else if args[0] == "--cm" {
|
||||
frame.register_behavior("connection-manager", move || {
|
||||
Box::new(cm::ConnectionManager::new())
|
||||
Box::new(cm::SciterConnectionManager::new())
|
||||
});
|
||||
page = "cm.html";
|
||||
} 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"))]
|
||||
fn get_sound_inputs() -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
@ -763,28 +689,6 @@ fn get_sound_inputs() -> Vec<String> {
|
||||
.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 UNKNOWN_ERROR: &'static str = "Unknown error";
|
||||
|
||||
|
622
src/ui/cm.rs
622
src/ui/cm.rs
@ -1,59 +1,83 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::ipc::start_pa;
|
||||
use crate::ipc::{self, new_listener, Connection, Data};
|
||||
use crate::ui_cm_interface::{start_ipc, ConnectionManager, InvokeUiCM};
|
||||
|
||||
#[cfg(windows)]
|
||||
use clipboard::{
|
||||
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 {
|
||||
root: Option<Element>,
|
||||
senders: HashMap<i32, mpsc::UnboundedSender<Data>>,
|
||||
click_time: i64,
|
||||
use hbb_common::{allow_err, log};
|
||||
use sciter::{make_args, Element, Value, HELEMENT};
|
||||
use std::sync::Mutex;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SciterHandler {
|
||||
pub element: Arc<Mutex<Option<Element>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConnectionManager(Arc<RwLock<ConnectionManagerInner>>);
|
||||
impl InvokeUiCM for SciterHandler {
|
||||
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 {
|
||||
type Target = Arc<RwLock<ConnectionManagerInner>>;
|
||||
fn remove_connection(&self, id: i32) {
|
||||
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 {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionManager {
|
||||
impl SciterConnectionManager {
|
||||
pub fn new() -> Self {
|
||||
#[cfg(target_os = "linux")]
|
||||
std::thread::spawn(start_pa);
|
||||
let inner = ConnectionManagerInner {
|
||||
root: None,
|
||||
senders: HashMap::new(),
|
||||
click_time: Default::default(),
|
||||
let cm = ConnectionManager {
|
||||
ui_handler: SciterHandler::default(),
|
||||
};
|
||||
let cm = Self(Arc::new(RwLock::new(inner)));
|
||||
let cloned = cm.clone();
|
||||
std::thread::spawn(move || start_ipc(cloned));
|
||||
cm
|
||||
SciterConnectionManager(cm)
|
||||
}
|
||||
|
||||
fn get_icon(&mut self) -> String {
|
||||
@ -61,359 +85,27 @@ impl ConnectionManager {
|
||||
}
|
||||
|
||||
fn check_click_time(&mut self, id: i32) {
|
||||
let lock = self.read().unwrap();
|
||||
if let Some(s) = lock.senders.get(&id) {
|
||||
allow_err!(s.send(Data::ClickTime(0)));
|
||||
}
|
||||
crate::ui_cm_interface::check_click_time(id);
|
||||
}
|
||||
|
||||
fn get_click_time(&self) -> f64 {
|
||||
self.read().unwrap().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),
|
||||
}
|
||||
crate::ui_cm_interface::get_click_time() as _
|
||||
}
|
||||
|
||||
fn switch_permission(&self, id: i32, name: String, enabled: bool) {
|
||||
let lock = self.read().unwrap();
|
||||
if let Some(s) = lock.senders.get(&id) {
|
||||
allow_err!(s.send(Data::SwitchPermission { name, enabled }));
|
||||
}
|
||||
crate::ui_cm_interface::switch_permission(id, name, enabled);
|
||||
}
|
||||
|
||||
fn close(&self, id: i32) {
|
||||
let lock = self.read().unwrap();
|
||||
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));
|
||||
}
|
||||
crate::ui_cm_interface::close(id);
|
||||
}
|
||||
|
||||
fn authorize(&self, id: i32) {
|
||||
let lock = self.read().unwrap();
|
||||
if let Some(s) = lock.senders.get(&id) {
|
||||
allow_err!(s.send(Data::Authorize));
|
||||
}
|
||||
crate::ui_cm_interface::authorize(id);
|
||||
}
|
||||
|
||||
fn send_msg(&self, id: i32, text: String) {
|
||||
crate::ui_cm_interface::send_chat(id, text);
|
||||
}
|
||||
|
||||
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) {
|
||||
self.write().unwrap().root = Some(Element::from(root));
|
||||
*self.ui_handler.element.lock().unwrap() = Some(Element::from(root));
|
||||
}
|
||||
|
||||
sciter::dispatch_script_call! {
|
||||
@ -437,179 +129,3 @@ impl sciter::EventHandler for ConnectionManager {
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote);
|
||||
}
|
||||
|
@ -1055,6 +1055,7 @@ function showSettings() {
|
||||
}
|
||||
|
||||
function checkConnectStatus() {
|
||||
handler.check_mouse_time(); // trigger connection status updater
|
||||
self.timer(1s, function() {
|
||||
var tmp = !!handler.get_option("stop-service");
|
||||
if (tmp != service_stopped) {
|
||||
|
@ -1,9 +1,7 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use sciter::{
|
||||
@ -22,13 +20,15 @@ use clipboard::{
|
||||
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)]
|
||||
use crate::clipboard_file::*;
|
||||
use crate::{
|
||||
client::*,
|
||||
ui_session_interface::{InvokeUi, Session},
|
||||
ui_session_interface::{InvokeUiSession, Session},
|
||||
};
|
||||
|
||||
type Video = AssetPtr<video_destination>;
|
||||
@ -37,12 +37,8 @@ lazy_static::lazy_static! {
|
||||
static ref VIDEO: Arc<Mutex<Option<Video>>> = Default::default();
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
static mut IS_ALT_GR: bool = false;
|
||||
|
||||
/// SciterHandler
|
||||
/// * element
|
||||
/// * thread TODO check if flutter need
|
||||
/// * close_state for file path when close
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SciterHandler {
|
||||
@ -66,7 +62,7 @@ impl SciterHandler {
|
||||
}
|
||||
}
|
||||
|
||||
impl InvokeUi for SciterHandler {
|
||||
impl InvokeUiSession for SciterHandler {
|
||||
fn set_cursor_data(&self, cd: CursorData) {
|
||||
let mut colors = hbb_common::compress::decompress(&cd.colors);
|
||||
if colors.iter().filter(|x| **x != 0).next().is_none() {
|
||||
@ -154,17 +150,36 @@ impl InvokeUi for SciterHandler {
|
||||
self.call("clearAllJobs", &make_args!());
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn add_job(
|
||||
fn load_last_job(&self, cnt: i32, job_json: &str) {
|
||||
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,
|
||||
id: i32,
|
||||
entries: &Vec<FileEntry>,
|
||||
path: String,
|
||||
to: String,
|
||||
file_num: i32,
|
||||
show_hidden: bool,
|
||||
is_remote: bool,
|
||||
_is_local: bool,
|
||||
only_count: 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) {
|
||||
@ -426,6 +441,14 @@ impl SciterSession {
|
||||
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 {
|
||||
#[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);
|
||||
a.push(e);
|
||||
}
|
||||
if only_count {
|
||||
m.set_item("num_entries", entries.len() as i32);
|
||||
} else {
|
||||
if !only_count {
|
||||
m.set_item("entries", a);
|
||||
}
|
||||
m.set_item("num_entries", entries.len() as i32);
|
||||
m.set_item("total_size", n as f64);
|
||||
m
|
||||
}
|
@ -67,6 +67,7 @@ function adaptDisplay() {
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshCursor();
|
||||
handler.style.set {
|
||||
width: w / scaleFactor + "px",
|
||||
height: h / scaleFactor + "px",
|
||||
@ -98,6 +99,7 @@ var acc_wheel_delta_y0 = 0;
|
||||
var total_wheel_time = 0;
|
||||
var wheeling = false;
|
||||
var dragging = false;
|
||||
var is_mouse_event_triggered = false;
|
||||
|
||||
// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum
|
||||
function resetWheel() {
|
||||
@ -139,6 +141,7 @@ function accWheel(v, is_x) {
|
||||
|
||||
function handler.onMouse(evt)
|
||||
{
|
||||
is_mouse_event_triggered = true;
|
||||
if (is_file_transfer || is_port_forward) return false;
|
||||
if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
|
||||
var dy = evt.y - scroll_body.scroll(#top);
|
||||
@ -317,6 +320,7 @@ function handler.onMouse(evt)
|
||||
return true;
|
||||
};
|
||||
|
||||
var cur_id = -1;
|
||||
var cur_hotx = 0;
|
||||
var cur_hoty = 0;
|
||||
var cur_img = null;
|
||||
@ -345,7 +349,7 @@ function scaleCursorImage(img) {
|
||||
var useSystemCursor = true;
|
||||
function updateCursor(system=false) {
|
||||
stdout.println("Update cursor, system: " + system);
|
||||
useSystemCursor= system;
|
||||
useSystemCursor = system;
|
||||
if (system) {
|
||||
handler.style#cursor = undefined;
|
||||
} else if (cur_img) {
|
||||
@ -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) {
|
||||
cur_hotx = hotx;
|
||||
cur_hoty = hoty;
|
||||
@ -360,8 +370,9 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
|
||||
if (img) {
|
||||
image_binded = true;
|
||||
cursors[id] = [img, hotx, hoty, width, height];
|
||||
cur_id = id;
|
||||
img = scaleCursorImage(img);
|
||||
if (cursor_img.style#display == 'none') {
|
||||
if (!first_mouse_event_triggered || cursor_img.style#display == 'none') {
|
||||
self.timer(1ms, updateCursor);
|
||||
}
|
||||
cur_img = img;
|
||||
@ -371,11 +382,12 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
|
||||
handler.setCursorId = function(id) {
|
||||
var img = cursors[id];
|
||||
if (img) {
|
||||
cur_id = id;
|
||||
image_binded = true;
|
||||
cur_hotx = img[1];
|
||||
cur_hoty = img[2];
|
||||
img = scaleCursorImage(img[0]);
|
||||
if (cursor_img.style#display == 'none') {
|
||||
if (!first_mouse_event_triggered || cursor_img.style#display == 'none') {
|
||||
self.timer(1ms, updateCursor);
|
||||
}
|
||||
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,10 +372,11 @@ pub fn get_mouse_time() -> f64 {
|
||||
return res;
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn check_mouse_time() {
|
||||
let sender = SENDER.lock().unwrap();
|
||||
allow_err!(sender.send(ipc::Data::MouseMoveTime(0)));
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]{
|
||||
let sender = SENDER.lock().unwrap();
|
||||
allow_err!(sender.send(ipc::Data::MouseMoveTime(0)));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_connect_status() -> Status {
|
||||
|
@ -34,7 +34,7 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Session<T: InvokeUi> {
|
||||
pub struct Session<T: InvokeUiSession> {
|
||||
pub cmd: String,
|
||||
pub id: String,
|
||||
pub password: String,
|
||||
@ -45,7 +45,7 @@ pub struct Session<T: InvokeUi> {
|
||||
pub ui_handler: T,
|
||||
}
|
||||
|
||||
impl<T: InvokeUi> Session<T> {
|
||||
impl<T: InvokeUiSession> Session<T> {
|
||||
pub fn get_view_style(&self) -> String {
|
||||
self.lc.read().unwrap().view_style.clone()
|
||||
}
|
||||
@ -151,11 +151,6 @@ impl<T: InvokeUi> Session<T> {
|
||||
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 {
|
||||
if self.lc.read().unwrap().conn_id <= 0
|
||||
|| LocalConfig::get_option("access_token").is_empty()
|
||||
@ -690,11 +685,6 @@ impl<T: InvokeUi> Session<T> {
|
||||
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) {
|
||||
let mut misc = Misc::new();
|
||||
misc.set_chat_message(ChatMessage {
|
||||
@ -961,9 +951,35 @@ impl<T: InvokeUi> Session<T> {
|
||||
pub fn close(&self) {
|
||||
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_id(&self, id: String);
|
||||
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_done(&self, id: i32, file_num: i32);
|
||||
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 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 override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: 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);
|
||||
}
|
||||
|
||||
impl<T: InvokeUi> Deref for Session<T> {
|
||||
impl<T: InvokeUiSession> Deref for Session<T> {
|
||||
type Target = T;
|
||||
|
||||
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 {
|
||||
&mut self.ui_handler
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: InvokeUi> FileManager for Session<T> {}
|
||||
impl<T: InvokeUiSession> FileManager for Session<T> {}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: InvokeUi> Interface for Session<T> {
|
||||
impl<T: InvokeUiSession> Interface for Session<T> {
|
||||
fn send(&self, data: Data) {
|
||||
if let Some(sender) = self.sender.read().unwrap().as_ref() {
|
||||
sender.send(data).ok();
|
||||
@ -1146,7 +1161,7 @@ impl<T: InvokeUi> Interface for Session<T> {
|
||||
// TODO use event callbcak
|
||||
// sciter only
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
impl<T: InvokeUi> Session<T> {
|
||||
impl<T: InvokeUiSession> Session<T> {
|
||||
fn start_keyboard_hook(&self) {
|
||||
if self.is_port_forward() || self.is_file_transfer() {
|
||||
return;
|
||||
@ -1211,7 +1226,7 @@ impl<T: InvokeUi> Session<T> {
|
||||
}
|
||||
|
||||
#[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>();
|
||||
*handler.sender.write().unwrap() = Some(sender.clone());
|
||||
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")))]
|
||||
async fn start_one_port_forward<T: InvokeUi>(
|
||||
async fn start_one_port_forward<T: InvokeUiSession>(
|
||||
handler: Session<T>,
|
||||
port: i32,
|
||||
remote_host: String,
|
||||
|
Loading…
Reference in New Issue
Block a user