This commit is contained in:
asur4s 2022-09-07 03:52:31 -04:00
commit 8da4fbabf5
43 changed files with 2863 additions and 3273 deletions

View File

@ -105,43 +105,30 @@ class MainService : Service() {
@Keep @Keep
fun rustSetByName(name: String, arg1: String, arg2: String) { fun rustSetByName(name: String, arg1: String, arg2: String) {
when (name) { when (name) {
"try_start_without_auth" -> { "add_connection" -> {
try {
val jsonObject = JSONObject(arg1)
val id = jsonObject["id"] as Int
val username = jsonObject["name"] as String
val peerId = jsonObject["peer_id"] as String
val type = if (jsonObject["is_file_transfer"] as Boolean) {
translate("File Connection")
} else {
translate("Screen Connection")
}
loginRequestNotification(id, type, username, peerId)
} catch (e: JSONException) {
e.printStackTrace()
}
}
"on_client_authorized" -> {
Log.d(logTag, "from rust:on_client_authorized")
try { try {
val jsonObject = JSONObject(arg1) val jsonObject = JSONObject(arg1)
val id = jsonObject["id"] as Int val id = jsonObject["id"] as Int
val username = jsonObject["name"] as String val username = jsonObject["name"] as String
val peerId = jsonObject["peer_id"] as String val peerId = jsonObject["peer_id"] as String
val authorized = jsonObject["authorized"] as Boolean
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
val type = if (isFileTransfer) { val type = if (isFileTransfer) {
translate("File Connection") translate("File Connection")
} else { } else {
translate("Screen Connection") translate("Screen Connection")
} }
if (!isFileTransfer && !isStart) { if (authorized) {
startCapture() if (!isFileTransfer && !isStart) {
startCapture()
}
onClientAuthorizedNotification(id, type, username, peerId)
} else {
loginRequestNotification(id, type, username, peerId)
} }
onClientAuthorizedNotification(id, type, username, peerId)
} catch (e: JSONException) { } catch (e: JSONException) {
e.printStackTrace() e.printStackTrace()
} }
} }
"stop_capture" -> { "stop_capture" -> {
Log.d(logTag, "from rust:stop_capture") Log.d(logTag, "from rust:stop_capture")

View File

@ -7,6 +7,7 @@ import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -154,7 +155,7 @@ class MyTheme {
brightness: Brightness.light, brightness: Brightness.light,
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: TabBarTheme( tabBarTheme: const TabBarTheme(
labelColor: Colors.black87, labelColor: Colors.black87,
), ),
splashColor: Colors.transparent, splashColor: Colors.transparent,
@ -162,13 +163,14 @@ class MyTheme {
).copyWith( ).copyWith(
extensions: <ThemeExtension<dynamic>>[ extensions: <ThemeExtension<dynamic>>[
ColorThemeExtension.light, ColorThemeExtension.light,
TabbarTheme.light,
], ],
); );
static ThemeData darkTheme = ThemeData( static ThemeData darkTheme = ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: TabBarTheme( tabBarTheme: const TabBarTheme(
labelColor: Colors.white70, labelColor: Colors.white70,
), ),
splashColor: Colors.transparent, splashColor: Colors.transparent,
@ -176,12 +178,17 @@ class MyTheme {
).copyWith( ).copyWith(
extensions: <ThemeExtension<dynamic>>[ extensions: <ThemeExtension<dynamic>>[
ColorThemeExtension.dark, ColorThemeExtension.dark,
TabbarTheme.dark,
], ],
); );
static ColorThemeExtension color(BuildContext context) { static ColorThemeExtension color(BuildContext context) {
return Theme.of(context).extension<ColorThemeExtension>()!; return Theme.of(context).extension<ColorThemeExtension>()!;
} }
static TabbarTheme tabbar(BuildContext context) {
return Theme.of(context).extension<TabbarTheme>()!;
}
} }
bool isDarkTheme() { bool isDarkTheme() {
@ -340,34 +347,41 @@ class OverlayDialogManager {
{bool clickMaskDismiss = false, {bool clickMaskDismiss = false,
bool showCancel = true, bool showCancel = true,
VoidCallback? onCancel}) { VoidCallback? onCancel}) {
show((setState, close) => CustomAlertDialog( show((setState, close) {
cancel() {
dismissAll();
if (onCancel != null) {
onCancel();
}
}
return CustomAlertDialog(
content: Container( content: Container(
constraints: BoxConstraints(maxWidth: 240), constraints: const BoxConstraints(maxWidth: 240),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 30), const SizedBox(height: 30),
Center(child: CircularProgressIndicator()), const Center(child: CircularProgressIndicator()),
SizedBox(height: 20), const SizedBox(height: 20),
Center( Center(
child: Text(translate(text), child: Text(translate(text),
style: TextStyle(fontSize: 15))), style: const TextStyle(fontSize: 15))),
SizedBox(height: 20), const SizedBox(height: 20),
Offstage( Offstage(
offstage: !showCancel, offstage: !showCancel,
child: Center( child: Center(
child: TextButton( child: TextButton(
style: flatButtonStyle, style: flatButtonStyle,
onPressed: () { onPressed: cancel,
dismissAll();
if (onCancel != null) {
onCancel();
}
},
child: Text(translate('Cancel'), child: Text(translate('Cancel'),
style: TextStyle(color: MyTheme.accent))))) style:
])))); const TextStyle(color: MyTheme.accent)))))
])),
onCancel: showCancel ? cancel : null,
);
});
} }
} }
@ -377,18 +391,18 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
final entry = OverlayEntry(builder: (_) { final entry = OverlayEntry(builder: (_) {
return IgnorePointer( return IgnorePointer(
child: Align( child: Align(
alignment: Alignment(0.0, 0.8), alignment: const Alignment(0.0, 0.8),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6), color: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(20), Radius.circular(20),
), ),
), ),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
child: Text( child: Text(
text, text,
style: TextStyle( style: const TextStyle(
decoration: TextDecoration.none, decoration: TextDecoration.none,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
fontSize: 18, fontSize: 18,
@ -403,23 +417,54 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
} }
class CustomAlertDialog extends StatelessWidget { class CustomAlertDialog extends StatelessWidget {
CustomAlertDialog( const CustomAlertDialog(
{this.title, required this.content, this.actions, this.contentPadding}); {Key? key,
this.title,
required this.content,
this.actions,
this.contentPadding,
this.onSubmit,
this.onCancel})
: super(key: key);
final Widget? title; final Widget? title;
final Widget content; final Widget content;
final List<Widget>? actions; final List<Widget>? actions;
final double? contentPadding; final double? contentPadding;
final Function()? onSubmit;
final Function()? onCancel;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( FocusNode focusNode = FocusNode();
scrollable: true, // request focus if there is no focused FocusNode in the dialog
title: title, Future.delayed(Duration.zero, () {
contentPadding: if (!focusNode.hasFocus) focusNode.requestFocus();
EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), });
content: content, return Focus(
actions: actions, 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}) { {bool? hasCancel}) {
dialogManager.dismissAll(); dialogManager.dismissAll();
List<Widget> buttons = []; 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")) { if (type != "connecting" && type != "success" && !type.contains("nook")) {
buttons.insert( hasOk = true;
0, buttons.insert(0, msgBoxButton(translate('OK'), submit));
msgBoxButton(translate('OK'), () {
dialogManager.dismissAll();
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (!type.contains("custom")) {
closeConnection();
}
}));
} }
hasCancel ??= !type.contains("error") && hasCancel ??= !type.contains("error") &&
!type.contains("nocancel") && !type.contains("nocancel") &&
type != "restarting"; type != "restarting";
if (hasCancel) { if (hasCancel) {
buttons.insert( buttons.insert(0, msgBoxButton(translate('Cancel'), cancel));
0,
msgBoxButton(translate('Cancel'), () {
dialogManager.dismissAll();
}));
} }
// TODO: test this button // TODO: test this button
if (type.contains("hasclose")) { if (type.contains("hasclose")) {
@ -459,9 +506,12 @@ void msgBox(
})); }));
} }
dialogManager.show((setState, close) => CustomAlertDialog( dialogManager.show((setState, close) => CustomAlertDialog(
title: _msgBoxTitle(title), title: _msgBoxTitle(title),
content: Text(translate(text), style: TextStyle(fontSize: 15)), content: Text(translate(text), style: const TextStyle(fontSize: 15)),
actions: buttons)); actions: buttons,
onSubmit: hasOk ? submit : null,
onCancel: hasCancel == true ? cancel : null,
));
} }
Widget msgBoxButton(String text, void Function() onPressed) { Widget msgBoxButton(String text, void Function() onPressed) {
@ -479,15 +529,19 @@ Widget msgBoxButton(String text, void Function() onPressed) {
Text(translate(text), style: TextStyle(color: MyTheme.accent)))); Text(translate(text), style: TextStyle(color: MyTheme.accent))));
} }
Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21)); Widget _msgBoxTitle(String title) =>
Text(translate(title), style: TextStyle(fontSize: 21));
void msgBoxCommon(OverlayDialogManager dialogManager, String title, void msgBoxCommon(OverlayDialogManager dialogManager, String title,
Widget content, List<Widget> buttons) { Widget content, List<Widget> buttons,
{bool hasCancel = true}) {
dialogManager.dismissAll(); dialogManager.dismissAll();
dialogManager.show((setState, close) => CustomAlertDialog( dialogManager.show((setState, close) => CustomAlertDialog(
title: _msgBoxTitle(title), title: _msgBoxTitle(title),
content: content, content: content,
actions: buttons)); actions: buttons,
onCancel: hasCancel ? close : null,
));
} }
Color str2color(String str, [alpha = 0xFF]) { Color str2color(String str, [alpha = 0xFF]) {

View File

@ -1,4 +1,52 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// TODO: Divide every 3 number to display ID class IDTextEditingController extends TextEditingController {
class IdFormController extends TextEditingController {} IDTextEditingController({String? text}) : super(text: text);
String get id => trimID(value.text);
set id(String newID) => text = formatID(newID);
}
class IDTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
if (newValue.text.isEmpty) {
return newValue.copyWith(text: '');
} else if (newValue.text.compareTo(oldValue.text) == 0) {
return newValue;
} else {
int selectionIndexFromTheRight =
newValue.text.length - newValue.selection.extentOffset;
String newID = formatID(newValue.text);
return TextEditingValue(
text: newID,
selection: TextSelection.collapsed(
offset: newID.length - selectionIndexFromTheRight,
),
);
}
}
}
String formatID(String id) {
String id2 = id.replaceAll(' ', '');
String newID = '';
if (id2.length <= 3) {
newID = id2;
} else {
var n = id2.length;
var a = n % 3 != 0 ? n % 3 : 3;
newID = id2.substring(0, a);
for (var i = a; i < n; i += 3) {
newID += " ${id2.substring(i, i + 3)}";
}
}
return newID;
}
String trimID(String id) {
return id.replaceAll(' ', '');
}

View File

@ -1,16 +1,26 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../consts.dart'; import '../consts.dart';
import '../models/platform_model.dart';
class PrivacyModeState { class PrivacyModeState {
static String tag(String id) => 'privacy_mode_$id'; static String tag(String id) => 'privacy_mode_$id';
static void init(String id) { static void init(String id) {
final RxBool state = false.obs; final key = tag(id);
Get.put(state, tag: 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)); static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
} }
@ -18,11 +28,20 @@ class BlockInputState {
static String tag(String id) => 'block_input_$id'; static String tag(String id) => 'block_input_$id';
static void init(String id) { static void init(String id) {
final RxBool state = false.obs; final key = tag(id);
Get.put(state, tag: 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)); static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
} }
@ -30,11 +49,20 @@ class CurrentDisplayState {
static String tag(String id) => 'current_display_$id'; static String tag(String id) => 'current_display_$id';
static void init(String id) { static void init(String id) {
final RxInt state = RxInt(0); final key = tag(id);
Get.put(state, tag: 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)); static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
} }
@ -85,3 +113,46 @@ class ConnectionTypeState {
static ConnectionType find(String id) => static ConnectionType find(String id) =>
Get.find<ConnectionType>(tag: tag(id)); Get.find<ConnectionType>(tag: tag(id));
} }
class ShowRemoteCursorState {
static String tag(String id) => 'show_remote_cursor_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
final RxBool state = false.obs;
Get.put(state, tag: key);
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class KeyboardEnabledState {
static String tag(String id) => 'keyboard_enabled_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
// Server side, default true
final RxBool state = true.obs;
Get.put(state, tag: key);
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}

View File

@ -12,6 +12,7 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/formatter/id_formatter.dart';
import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/scan_page.dart';
import '../../mobile/pages/settings_page.dart'; import '../../mobile/pages/settings_page.dart';
import '../../models/model.dart'; import '../../models/model.dart';
@ -30,7 +31,7 @@ class ConnectionPage extends StatefulWidget {
/// State for the connection page. /// State for the connection page.
class _ConnectionPageState extends State<ConnectionPage> { class _ConnectionPageState extends State<ConnectionPage> {
/// Controller for the id input bar. /// Controller for the id input bar.
final _idController = TextEditingController(); final _idController = IDTextEditingController();
/// Update url. If it's not null, means an update is available. /// Update url. If it's not null, means an update is available.
final _updateUrl = ''; final _updateUrl = '';
@ -43,9 +44,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
if (_idController.text.isEmpty) { if (_idController.text.isEmpty) {
() async { () async {
final lastRemoteId = await bind.mainGetLastRemoteId(); final lastRemoteId = await bind.mainGetLastRemoteId();
if (lastRemoteId != _idController.text) { if (lastRemoteId != _idController.id) {
setState(() { setState(() {
_idController.text = lastRemoteId; _idController.id = lastRemoteId;
}); });
} }
}(); }();
@ -110,7 +111,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
/// Callback for the connect button. /// Callback for the connect button.
/// Connects to the selected peer. /// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) { void onConnect({bool isFileTransfer = false}) {
final id = _idController.text.trim(); final id = _idController.id;
connect(id, isFileTransfer: isFileTransfer); connect(id, isFileTransfer: isFileTransfer);
} }
@ -166,7 +167,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
}); });
var w = Container( var w = Container(
width: 320 + 20 * 2, width: 320 + 20 * 2,
padding: EdgeInsets.fromLTRB(20, 24, 20, 22), padding: const EdgeInsets.fromLTRB(20, 24, 20, 22),
decoration: BoxDecoration( decoration: BoxDecoration(
color: MyTheme.color(context).bg, color: MyTheme.color(context).bg,
borderRadius: const BorderRadius.all(Radius.circular(13)), borderRadius: const BorderRadius.all(Radius.circular(13)),
@ -178,42 +179,54 @@ class _ConnectionPageState extends State<ConnectionPage> {
children: [ children: [
Text( Text(
translate('Control Remote Desktop'), translate('Control Remote Desktop'),
style: TextStyle(fontSize: 19, height: 1), style: const TextStyle(fontSize: 19, height: 1),
), ),
], ],
).marginOnly(bottom: 15), ).marginOnly(bottom: 15),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: Obx(
autocorrect: false, () => TextField(
enableSuggestions: false, autocorrect: false,
keyboardType: TextInputType.visiblePassword, enableSuggestions: false,
style: TextStyle( keyboardType: TextInputType.visiblePassword,
fontFamily: 'WorkSans', focusNode: focusNode,
fontSize: 22, style: const TextStyle(
height: 1, fontFamily: 'WorkSans',
), fontSize: 22,
decoration: InputDecoration( height: 1,
hintText: translate('Enter Remote ID'), ),
hintStyle: TextStyle( maxLines: 1,
color: MyTheme.color(context).placeholder), cursorColor: MyTheme.color(context).text!,
border: OutlineInputBorder( 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, borderRadius: BorderRadius.zero,
borderSide: BorderSide( borderSide:
color: MyTheme.color(context).placeholder!)), BorderSide(color: MyTheme.button, width: 3),
focusedBorder: OutlineInputBorder( ),
borderRadius: BorderRadius.zero, isDense: true,
borderSide: contentPadding: const EdgeInsets.symmetric(
BorderSide(color: MyTheme.button, width: 3), horizontal: 10, vertical: 12)),
), controller: _idController,
isDense: true, inputFormatters: [IDTextInputFormatter()],
contentPadding: onSubmitted: (s) {
EdgeInsets.symmetric(horizontal: 10, vertical: 12)), onConnect();
controller: _idController, },
onSubmitted: (s) { ),
onConnect();
},
), ),
), ),
], ],
@ -259,7 +272,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
).marginSymmetric(horizontal: 12), ).marginSymmetric(horizontal: 12),
), ),
)), )),
SizedBox( const SizedBox(
width: 17, width: 17,
), ),
Obx( Obx(
@ -304,7 +317,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
), ),
); );
return Center( return Center(
child: Container(constraints: BoxConstraints(maxWidth: 600), child: w)); child: Container(
constraints: const BoxConstraints(maxWidth: 600), child: w));
} }
@override @override
@ -654,71 +668,69 @@ class _ConnectionPageState extends State<ConnectionPage> {
var field = ""; var field = "";
var msg = ""; var msg = "";
var isInProgress = false; var isInProgress = false;
TextEditingController controller = TextEditingController(text: field);
gFFI.dialogManager.show((setState, close) { gFFI.dialogManager.show((setState, close) {
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( return CustomAlertDialog(
title: Text(translate("Add ID")), title: Text(translate("Add ID")),
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(translate("whitelist_sep")), Text(translate("whitelist_sep")),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
onChanged: (s) { maxLines: null,
field = s; decoration: InputDecoration(
}, border: const OutlineInputBorder(),
maxLines: null, errorText: msg.isEmpty ? null : translate(msg),
decoration: InputDecoration( ),
border: OutlineInputBorder(), controller: controller,
errorText: msg.isEmpty ? null : translate(msg), focusNode: FocusNode()..requestFocus()),
),
controller: TextEditingController(text: field),
),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 4.0, height: 4.0,
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
], ],
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
setState(() {
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"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }
@ -727,67 +739,65 @@ class _ConnectionPageState extends State<ConnectionPage> {
var field = ""; var field = "";
var msg = ""; var msg = "";
var isInProgress = false; var isInProgress = false;
TextEditingController controller = TextEditingController(text: field);
gFFI.dialogManager.show((setState, close) { gFFI.dialogManager.show((setState, close) {
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( return CustomAlertDialog(
title: Text(translate("Add Tag")), title: Text(translate("Add Tag")),
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(translate("whitelist_sep")), Text(translate("whitelist_sep")),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
onChanged: (s) {
field = s;
},
maxLines: null, maxLines: null,
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: msg.isEmpty ? null : translate(msg), errorText: msg.isEmpty ? null : translate(msg),
), ),
controller: TextEditingController(text: field), controller: controller,
focusNode: FocusNode()..requestFocus(),
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 4.0, height: 4.0,
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
], ],
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
setState(() {
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"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }
@ -799,13 +809,23 @@ class _ConnectionPageState extends State<ConnectionPage> {
var selectedTag = gFFI.abModel.getPeerTags(id).obs; var selectedTag = gFFI.abModel.getPeerTags(id).obs;
gFFI.dialogManager.show((setState, close) { gFFI.dialogManager.show((setState, close) {
submit() async {
setState(() {
isInProgress = true;
});
gFFI.abModel.changeTagForPeer(id, selectedTag);
await gFFI.abModel.updateAb();
close();
}
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate("Edit Tag")), title: Text(translate("Edit Tag")),
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Wrap( child: Wrap(
children: tags children: tags
.map((e) => buildTag(e, selectedTag, onTap: () { .map((e) => buildTag(e, selectedTag, onTap: () {
@ -818,26 +838,16 @@ class _ConnectionPageState extends State<ConnectionPage> {
.toList(growable: false), .toList(growable: false),
), ),
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
], ],
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
setState(() {
isInProgress = true;
});
gFFI.abModel.changeTagForPeer(id, selectedTag);
await gFFI.abModel.updateAb();
close();
},
child: Text(translate("OK"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }

View File

@ -20,7 +20,8 @@ class ConnectionTabPage extends StatefulWidget {
} }
class _ConnectionTabPageState extends State<ConnectionTabPage> { class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabController = Get.put(DesktopTabController()); final tabController =
Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen));
static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined; static const IconData unselectedIcon = Icons.desktop_windows_outlined;
@ -60,6 +61,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
if (call.method == "new_remote_desktop") { if (call.method == "new_remote_desktop") {
final args = jsonDecode(call.arguments); final args = jsonDecode(call.arguments);
final id = args['id']; final id = args['id'];
ConnectionTypeState.init(id);
window_on_top(windowId()); window_on_top(windowId());
ConnectionTypeState.init(id); ConnectionTypeState.init(id);
tabController.add(TabInfo( tabController.add(TabInfo(
@ -81,7 +83,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
final RxBool fullscreen = Get.find(tag: 'fullscreen'); final RxBool fullscreen = Get.find(tag: 'fullscreen');
return Obx(() => SubWindowDragToResizeArea( return Obx(() => SubWindowDragToResizeArea(
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
@ -93,15 +94,11 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
backgroundColor: MyTheme.color(context).bg, backgroundColor: MyTheme.color(context).bg,
body: Obx(() => DesktopTab( body: Obx(() => DesktopTab(
controller: tabController, controller: tabController,
theme: theme,
tabType: DesktopTabType.remoteScreen,
showTabBar: fullscreen.isFalse, showTabBar: fullscreen.isFalse,
onClose: () { onClose: () {
tabController.clear(); tabController.clear();
}, },
tail: AddButton( tail: AddButton().paddingOnly(left: 10),
theme: theme,
).paddingOnly(left: 10),
pageViewBuilder: (pageView) { pageViewBuilder: (pageView) {
WindowController.fromWindowId(windowId()) WindowController.fromWindowId(windowId())
.setFullscreen(fullscreen.isTrue); .setFullscreen(fullscreen.isTrue);

View File

@ -6,6 +6,9 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
@ -52,7 +55,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
return Row( return Row(
children: [ children: [
buildServerInfo(context), buildServerInfo(context),
VerticalDivider( const VerticalDivider(
width: 1, width: 1,
thickness: 1, thickness: 1,
), ),
@ -90,23 +93,23 @@ class _DesktopHomePageState extends State<DesktopHomePage>
buildIDBoard(BuildContext context) { buildIDBoard(BuildContext context) {
final model = gFFI.serverModel; final model = gFFI.serverModel;
return Container( return Container(
margin: EdgeInsets.only(left: 20, right: 16), margin: const EdgeInsets.only(left: 20, right: 11),
height: 52, height: 57,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.baseline, crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic, textBaseline: TextBaseline.alphabetic,
children: [ children: [
Container( Container(
width: 2, width: 2,
decoration: BoxDecoration(color: MyTheme.accent), decoration: const BoxDecoration(color: MyTheme.accent),
), ).marginOnly(top: 5),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 7),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( SizedBox(
height: 25, height: 25,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -117,7 +120,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: MyTheme.color(context).lightText), color: MyTheme.color(context).lightText),
), ).marginOnly(top: 5),
buildPopupMenu(context) buildPopupMenu(context)
], ],
), ),
@ -132,11 +135,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
child: TextFormField( child: TextFormField(
controller: model.serverId, controller: model.serverId,
readOnly: true, readOnly: true,
decoration: InputDecoration( decoration: const InputDecoration(
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.only(bottom: 18), contentPadding: EdgeInsets.only(bottom: 20),
), ),
style: TextStyle( style: const TextStyle(
fontSize: 22, fontSize: 22,
), ),
), ),
@ -241,7 +244,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}, },
child: Obx( child: Obx(
() => CircleAvatar( () => CircleAvatar(
radius: 12, radius: 15,
backgroundColor: hover.value backgroundColor: hover.value
? MyTheme.color(context).grayBg! ? MyTheme.color(context).grayBg!
: MyTheme.color(context).bg!, : MyTheme.color(context).bg!,
@ -274,7 +277,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 7),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -300,7 +303,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
readOnly: true, readOnly: true,
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.only(bottom: 8), contentPadding: EdgeInsets.only(bottom: 2),
), ),
style: TextStyle(fontSize: 15), style: TextStyle(fontSize: 15),
), ),
@ -314,7 +317,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
? MyTheme.color(context).text ? MyTheme.color(context).text
: Color(0xFFDDDDDD), : Color(0xFFDDDDDD),
size: 22, size: 22,
).marginOnly(right: 10, bottom: 8), ).marginOnly(right: 8, bottom: 2),
), ),
onTap: () => bind.mainUpdateTemporaryPassword(), onTap: () => bind.mainUpdateTemporaryPassword(),
onHover: (value) => refreshHover.value = value, onHover: (value) => refreshHover.value = value,
@ -422,13 +425,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
color: editHover.value color: editHover.value
? MyTheme.color(context).text ? MyTheme.color(context).text
: Color(0xFFDDDDDD)) : Color(0xFFDDDDDD))
.marginOnly(bottom: 8))); .marginOnly(bottom: 2)));
} }
buildTip(BuildContext context) { buildTip(BuildContext context) {
return Padding( return Padding(
padding: padding:
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 14), const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -639,76 +642,76 @@ class _DesktopHomePageState extends State<DesktopHomePage>
var newId = ""; var newId = "";
var msg = ""; var msg = "";
var isInProgress = false; var isInProgress = false;
TextEditingController controller = TextEditingController();
gFFI.dialogManager.show((setState, close) { gFFI.dialogManager.show((setState, close) {
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( return CustomAlertDialog(
title: Text(translate("Change ID")), title: Text(translate("Change ID")),
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(translate("id_change_tip")), Text(translate("id_change_tip")),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
Text("ID:").marginOnly(bottom: 16.0), const Text("ID:").marginOnly(bottom: 16.0),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
onChanged: (s) {
newId = s;
},
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: msg.isEmpty ? null : translate(msg)), errorText: msg.isEmpty ? null : translate(msg)),
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(16), LengthLimitingTextInputFormatter(16),
// FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
], ],
maxLength: 16, maxLength: 16,
controller: controller,
focusNode: FocusNode()..requestFocus(),
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 4.0, height: 4.0,
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
], ],
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
setState(() {
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"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }
@ -717,16 +720,16 @@ class _DesktopHomePageState extends State<DesktopHomePage>
final appName = await bind.mainGetAppName(); final appName = await bind.mainGetAppName();
final license = await bind.mainGetLicense(); final license = await bind.mainGetLicense();
final version = await bind.mainGetVersion(); final version = await bind.mainGetVersion();
final linkStyle = TextStyle(decoration: TextDecoration.underline); const linkStyle = TextStyle(decoration: TextDecoration.underline);
gFFI.dialogManager.show((setState, close) { gFFI.dialogManager.show((setState, close) {
return CustomAlertDialog( return CustomAlertDialog(
title: Text("About $appName"), title: Text("About $appName"),
content: ConstrainedBox( content: ConstrainedBox(
constraints: BoxConstraints(minWidth: 500), constraints: const BoxConstraints(minWidth: 500),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Text("Version: $version").marginSymmetric(vertical: 4.0), Text("Version: $version").marginSymmetric(vertical: 4.0),
@ -734,7 +737,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
onTap: () { onTap: () {
launchUrlString("https://rustdesk.com/privacy"); launchUrlString("https://rustdesk.com/privacy");
}, },
child: Text( child: const Text(
"Privacy Statement", "Privacy Statement",
style: linkStyle, style: linkStyle,
).marginSymmetric(vertical: 4.0)), ).marginSymmetric(vertical: 4.0)),
@ -742,13 +745,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
onTap: () { onTap: () {
launchUrlString("https://rustdesk.com"); launchUrlString("https://rustdesk.com");
}, },
child: Text( child: const Text(
"Website", "Website",
style: linkStyle, style: linkStyle,
).marginSymmetric(vertical: 4.0)), ).marginSymmetric(vertical: 4.0)),
Container( Container(
decoration: BoxDecoration(color: Color(0xFF2c8cff)), decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), padding:
const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
@ -757,9 +761,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
children: [ children: [
Text( Text(
"Copyright &copy; 2022 Purslane Ltd.\n$license", "Copyright &copy; 2022 Purslane Ltd.\n$license",
style: TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
Text( const Text(
"Made with heart in this chaotic world!", "Made with heart in this chaotic world!",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
@ -775,12 +779,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
), ),
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("OK"))),
onPressed: () async {
close();
},
child: Text(translate("OK"))),
], ],
onSubmit: close,
onCancel: close,
); );
}); });
} }
@ -812,118 +814,124 @@ Future<bool> loginDialog() async {
var isInProgress = false; var isInProgress = false;
var completer = Completer<bool>(); var completer = Completer<bool>();
gFFI.dialogManager.show((setState, close) { gFFI.dialogManager.show((setState, close) {
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( return CustomAlertDialog(
title: Text(translate("Login")), title: Text(translate("Login")),
content: ConstrainedBox( content: ConstrainedBox(
constraints: BoxConstraints(minWidth: 500), constraints: const BoxConstraints(minWidth: 500),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text( child: Text(
"${translate('Username')}:", "${translate('Username')}:",
textAlign: TextAlign.start, textAlign: TextAlign.start,
).marginOnly(bottom: 16.0)), ).marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: userNameMsg.isNotEmpty ? userNameMsg : null), errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
controller: userContontroller, controller: userContontroller,
focusNode: FocusNode()..requestFocus(),
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('Password')}:") child: Text("${translate('Password')}:")
.marginOnly(bottom: 16.0)), .marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
obscureText: true, obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: passMsg.isNotEmpty ? passMsg : null), errorText: passMsg.isNotEmpty ? passMsg : null),
controller: pwdController, controller: pwdController,
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 4.0, height: 4.0,
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
], ],
), ),
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: cancel, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
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"))),
], ],
onSubmit: submit,
onCancel: cancel,
); );
}); });
return completer.future; return completer.future;
@ -937,55 +945,78 @@ void setPasswordDialog() async {
var errMsg1 = ""; var errMsg1 = "";
gFFI.dialogManager.show((setState, close) { 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( return CustomAlertDialog(
title: Text(translate("Set Password")), title: Text(translate("Set Password")),
content: ConstrainedBox( content: ConstrainedBox(
constraints: BoxConstraints(minWidth: 500), constraints: const BoxConstraints(minWidth: 500),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text( child: Text(
"${translate('Password')}:", "${translate('Password')}:",
textAlign: TextAlign.start, textAlign: TextAlign.start,
).marginOnly(bottom: 16.0)), ).marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
obscureText: true, obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: errMsg0.isNotEmpty ? errMsg0 : null), errorText: errMsg0.isNotEmpty ? errMsg0 : null),
controller: p0, controller: p0,
focusNode: FocusNode()..requestFocus(),
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('Confirmation')}:") child: Text("${translate('Confirmation')}:")
.marginOnly(bottom: 16.0)), .marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
obscureText: true, obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: errMsg1.isNotEmpty ? errMsg1 : null), errorText: errMsg1.isNotEmpty ? errMsg1 : null),
controller: p1, controller: p1,
), ),
@ -996,35 +1027,11 @@ void setPasswordDialog() async {
), ),
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () {
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"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }

View File

@ -49,7 +49,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp), 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp),
_TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp), _TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp),
_TabInfo('Connection', Icons.link_outlined, Icons.link_sharp), _TabInfo('Connection', Icons.link_outlined, Icons.link_sharp),
_TabInfo('About RustDesk', Icons.info_outline, Icons.info_sharp) _TabInfo('About', Icons.info_outline, Icons.info_sharp)
]; ];
late PageController controller; late PageController controller;
@ -714,7 +714,7 @@ class _AboutState extends State<_About> {
], ],
).marginOnly(left: _kContentHMargin) ).marginOnly(left: _kContentHMargin)
]), ]),
]).marginOnly(left: _kCardLeftMargin); ]);
}); });
} }
} }
@ -1038,52 +1038,117 @@ void changeServer() async {
var keyController = TextEditingController(text: key); var keyController = TextEditingController(text: key);
var isInProgress = false; var isInProgress = false;
gFFI.dialogManager.show((setState, close) { gFFI.dialogManager.show((setState, close) {
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( return CustomAlertDialog(
title: Text(translate("ID/Relay Server")), title: Text(translate("ID/Relay Server")),
content: ConstrainedBox( content: ConstrainedBox(
constraints: BoxConstraints(minWidth: 500), constraints: const BoxConstraints(minWidth: 500),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('ID Server')}:") child: Text("${translate('ID Server')}:")
.marginOnly(bottom: 16.0)), .marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: idServerMsg.isNotEmpty ? idServerMsg : null), errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
controller: idController, controller: idController,
focusNode: FocusNode()..requestFocus(),
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('Relay Server')}:") child: Text("${translate('Relay Server')}:")
.marginOnly(bottom: 16.0)), .marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: errorText:
relayServerMsg.isNotEmpty ? relayServerMsg : null), relayServerMsg.isNotEmpty ? relayServerMsg : null),
controller: relayController, controller: relayController,
@ -1091,22 +1156,22 @@ void changeServer() async {
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('API Server')}:") child: Text("${translate('API Server')}:")
.marginOnly(bottom: 16.0)), .marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: errorText:
apiServerMsg.isNotEmpty ? apiServerMsg : null), apiServerMsg.isNotEmpty ? apiServerMsg : null),
controller: apiController, controller: apiController,
@ -1114,21 +1179,21 @@ void changeServer() async {
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: child:
Text("${translate('Key')}:").marginOnly(bottom: 16.0)), Text("${translate('Key')}:").marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
controller: keyController, controller: keyController,
@ -1136,83 +1201,20 @@ void changeServer() async {
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 4.0, height: 4.0,
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
], ],
), ),
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
setState(() {
[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"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }
@ -1231,27 +1233,28 @@ void changeWhiteList() async {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(translate("whitelist_sep")), Text(translate("whitelist_sep")),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
maxLines: null, maxLines: null,
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: msg.isEmpty ? null : translate(msg), errorText: msg.isEmpty ? null : translate(msg),
), ),
controller: controller, controller: controller,
), focusNode: FocusNode()..requestFocus()),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 4.0, height: 4.0,
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
], ],
), ),
actions: [ actions: [
@ -1277,7 +1280,7 @@ void changeWhiteList() async {
final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$");
for (final ip in ips) { for (final ip in ips) {
if (!ipMatch.hasMatch(ip)) { if (!ipMatch.hasMatch(ip)) {
msg = translate("Invalid IP") + " $ip"; msg = "${translate("Invalid IP")} $ip";
setState(() { setState(() {
isInProgress = false; isInProgress = false;
}); });
@ -1292,6 +1295,7 @@ void changeWhiteList() async {
}, },
child: Text(translate("OK"))), child: Text(translate("OK"))),
], ],
onCancel: close,
); );
}); });
} }
@ -1314,50 +1318,80 @@ void changeSocks5Proxy() async {
var isInProgress = false; var isInProgress = false;
gFFI.dialogManager.show((setState, close) { 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( return CustomAlertDialog(
title: Text(translate("Socks5 Proxy")), title: Text(translate("Socks5 Proxy")),
content: ConstrainedBox( content: ConstrainedBox(
constraints: BoxConstraints(minWidth: 500), constraints: const BoxConstraints(minWidth: 500),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('Hostname')}:") child: Text("${translate('Hostname')}:")
.marginOnly(bottom: 16.0)), .marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
errorText: proxyMsg.isNotEmpty ? proxyMsg : null), errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
controller: proxyController, controller: proxyController,
focusNode: FocusNode()..requestFocus(),
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('Username')}:") child: Text("${translate('Username')}:")
.marginOnly(bottom: 16.0)), .marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
controller: userController, controller: userController,
@ -1365,21 +1399,21 @@ void changeSocks5Proxy() async {
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('Password')}:") child: Text("${translate('Password')}:")
.marginOnly(bottom: 16.0)), .marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
controller: pwdController, controller: pwdController,
@ -1387,50 +1421,20 @@ void changeSocks5Proxy() async {
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
], ],
), ),
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
setState(() {
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"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }

View File

@ -15,7 +15,7 @@ class DesktopTabPage extends StatefulWidget {
} }
class _DesktopTabPageState extends State<DesktopTabPage> { class _DesktopTabPageState extends State<DesktopTabPage> {
final tabController = DesktopTabController(); final tabController = DesktopTabController(tabType: DesktopTabType.main);
@override @override
void initState() { void initState() {
@ -37,26 +37,27 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
RxBool fullscreen = false.obs; RxBool fullscreen = false.obs;
Get.put(fullscreen, tag: 'fullscreen'); Get.put(fullscreen, tag: 'fullscreen');
return Obx(() => DragToResizeArea( return Obx(() => DragToResizeArea(
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)), border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold( child: Overlay(initialEntries: [
backgroundColor: MyTheme.color(context).bg, OverlayEntry(builder: (context) {
body: DesktopTab( gFFI.dialogManager.setOverlayState(Overlay.of(context));
controller: tabController, return Scaffold(
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), backgroundColor: MyTheme.color(context).bg,
tabType: DesktopTabType.main, body: DesktopTab(
tail: ActionIcon( controller: tabController,
message: 'Settings', tail: ActionIcon(
icon: IconFont.menu, message: 'Settings',
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), icon: IconFont.menu,
onTap: onAddSetting, onTap: onAddSetting,
is_close: false, isClose: false,
), ),
)), ));
), })
)); ]),
)));
} }
void onAddSetting() { void onAddSetting() {

View File

@ -59,8 +59,11 @@ class _FileManagerPageState extends State<FileManagerPage>
super.initState(); super.initState();
_ffi = FFI(); _ffi = FFI();
_ffi.connect(widget.id, isFileTransfer: true); _ffi.connect(widget.id, isFileTransfer: true);
WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
Get.put(_ffi, tag: 'ft_${widget.id}'); Get.put(_ffi, tag: 'ft_${widget.id}');
// _ffi.ffiModel.updateEventListener(widget.id);
if (!Platform.isLinux) { if (!Platform.isLinux) {
Wakelock.enable(); Wakelock.enable();
} }
@ -117,7 +120,8 @@ class _FileManagerPageState extends State<FileManagerPage>
Widget menu({bool isLocal = false}) { Widget menu({bool isLocal = false}) {
return PopupMenuButton<String>( return PopupMenuButton<String>(
icon: Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
splashRadius: 20,
itemBuilder: (context) { itemBuilder: (context) {
return [ return [
PopupMenuItem( PopupMenuItem(
@ -413,6 +417,7 @@ class _FileManagerPageState extends State<FileManagerPage>
/// watch transfer status /// watch transfer status
Widget statusList() { Widget statusList() {
return PreferredSize( return PreferredSize(
preferredSize: const Size(200, double.infinity),
child: Container( child: Container(
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@ -429,8 +434,8 @@ class _FileManagerPageState extends State<FileManagerPage>
children: [ children: [
Transform.rotate( Transform.rotate(
angle: item.isRemote ? pi : 0, angle: item.isRemote ? pi : 0,
child: Icon(Icons.send)), child: const Icon(Icons.send)),
SizedBox( const SizedBox(
width: 16.0, width: 16.0,
), ),
Expanded( Expanded(
@ -441,7 +446,7 @@ class _FileManagerPageState extends State<FileManagerPage>
Tooltip( Tooltip(
message: item.jobName, message: item.jobName,
child: Text( child: Text(
'${item.jobName}', item.jobName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
)), )),
@ -455,7 +460,7 @@ class _FileManagerPageState extends State<FileManagerPage>
offstage: offstage:
item.state != JobState.inProgress, item.state != JobState.inProgress,
child: Text( child: Text(
'${readableFileSize(item.speed) + "/s"} ')), '${"${readableFileSize(item.speed)}/s"} ')),
Offstage( Offstage(
offstage: item.totalSize <= 0, offstage: item.totalSize <= 0,
child: Text( child: Text(
@ -475,10 +480,12 @@ class _FileManagerPageState extends State<FileManagerPage>
onPressed: () { onPressed: () {
model.resumeJob(item.id); model.resumeJob(item.id);
}, },
icon: Icon(Icons.restart_alt_rounded)), splashRadius: 20,
icon: const Icon(Icons.restart_alt_rounded)),
), ),
IconButton( IconButton(
icon: Icon(Icons.delete), icon: const Icon(Icons.delete),
splashRadius: 20,
onPressed: () { onPressed: () {
model.jobTable.removeAt(index); model.jobTable.removeAt(index);
model.cancelJob(item.id); model.cancelJob(item.id);
@ -500,8 +507,7 @@ class _FileManagerPageState extends State<FileManagerPage>
itemCount: model.jobTable.length, itemCount: model.jobTable.length,
), ),
), ),
), ));
preferredSize: Size(200, double.infinity));
} }
goBack({bool? isLocal}) { goBack({bool? isLocal}) {
@ -551,12 +557,15 @@ class _FileManagerPageState extends State<FileManagerPage>
Row( Row(
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {
model.goHome(isLocal: isLocal); model.goHome(isLocal: isLocal);
}, },
icon: Icon(Icons.home_outlined)), icon: const Icon(Icons.home_outlined),
splashRadius: 20,
),
IconButton( IconButton(
icon: Icon(Icons.arrow_upward), icon: const Icon(Icons.arrow_upward),
splashRadius: 20,
onPressed: () { onPressed: () {
goBack(isLocal: isLocal); goBack(isLocal: isLocal);
}, },
@ -622,13 +631,15 @@ class _FileManagerPageState extends State<FileManagerPage>
), ),
)) ))
], ],
child: Icon(Icons.search), splashRadius: 20,
child: const Icon(Icons.search),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
model.refresh(isLocal: isLocal); model.refresh(isLocal: isLocal);
}, },
icon: Icon(Icons.refresh)), splashRadius: 20,
icon: const Icon(Icons.refresh)),
], ],
), ),
Row( Row(
@ -642,47 +653,52 @@ class _FileManagerPageState extends State<FileManagerPage>
IconButton( IconButton(
onPressed: () { onPressed: () {
final name = TextEditingController(); final name = TextEditingController();
_ffi.dialogManager _ffi.dialogManager.show((setState, close) {
.show((setState, close) => CustomAlertDialog( submit() {
title: Text(translate("Create Folder")), if (name.value.text.isNotEmpty) {
content: Column( model.createDir(
mainAxisSize: MainAxisSize.min, PathUtil.join(
children: [ model.getCurrentDir(isLocal).path,
TextFormField( name.value.text,
decoration: InputDecoration( model.getCurrentIsWindows(isLocal)),
labelText: translate( isLocal: isLocal);
"Please enter the folder name"), close();
), }
controller: name, }
),
], cancel() => close(false);
), return CustomAlertDialog(
actions: [ title: Text(translate("Create Folder")),
TextButton( content: Column(
style: flatButtonStyle, mainAxisSize: MainAxisSize.min,
onPressed: () => close(false), children: [
child: Text(translate("Cancel"))), TextFormField(
ElevatedButton( decoration: InputDecoration(
style: flatButtonStyle, labelText: translate(
onPressed: () { "Please enter the folder name"),
if (name.value.text.isNotEmpty) { ),
model.createDir( controller: name,
PathUtil.join( focusNode: FocusNode()..requestFocus(),
model ),
.getCurrentDir( ],
isLocal) ),
.path, actions: [
name.value.text, TextButton(
model.getCurrentIsWindows( style: flatButtonStyle,
isLocal)), onPressed: cancel,
isLocal: isLocal); child: Text(translate("Cancel"))),
close(); ElevatedButton(
} style: flatButtonStyle,
}, onPressed: submit,
child: Text(translate("OK"))) child: Text(translate("OK")))
])); ],
onSubmit: submit,
onCancel: cancel,
);
});
}, },
icon: Icon(Icons.create_new_folder_outlined)), splashRadius: 20,
icon: const Icon(Icons.create_new_folder_outlined)),
IconButton( IconButton(
onPressed: () async { onPressed: () async {
final items = isLocal final items = isLocal
@ -691,7 +707,8 @@ class _FileManagerPageState extends State<FileManagerPage>
await (model.removeAction(items, isLocal: isLocal)); await (model.removeAction(items, isLocal: isLocal));
items.clear(); items.clear();
}, },
icon: Icon(Icons.delete_forever_outlined)), splashRadius: 20,
icon: const Icon(Icons.delete_forever_outlined)),
], ],
), ),
), ),
@ -703,7 +720,7 @@ class _FileManagerPageState extends State<FileManagerPage>
}, },
icon: Transform.rotate( icon: Transform.rotate(
angle: isLocal ? 0 : pi, angle: isLocal ? 0 : pi,
child: Icon( child: const Icon(
Icons.send, Icons.send,
), ),
), ),

View File

@ -25,7 +25,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
static final IconData unselectedIcon = Icons.file_copy_outlined; static final IconData unselectedIcon = Icons.file_copy_outlined;
_FileManagerTabPageState(Map<String, dynamic> params) { _FileManagerTabPageState(Map<String, dynamic> params) {
Get.put(DesktopTabController()); Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer));
tabController.add(TabInfo( tabController.add(TabInfo(
key: params['id'], key: params['id'],
label: params['id'], label: params['id'],
@ -62,8 +62,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme =
isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light();
return SubWindowDragToResizeArea( return SubWindowDragToResizeArea(
windowId: windowId(), windowId: windowId(),
child: Container( child: Container(
@ -73,14 +71,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
backgroundColor: MyTheme.color(context).bg, backgroundColor: MyTheme.color(context).bg,
body: DesktopTab( body: DesktopTab(
controller: tabController, controller: tabController,
theme: theme,
tabType: DesktopTabType.fileTransfer,
onClose: () { onClose: () {
tabController.clear(); tabController.clear();
}, },
tail: AddButton( tail: AddButton().paddingOnly(left: 10),
theme: theme,
).paddingOnly(left: 10),
)), )),
), ),
); );

View File

@ -70,38 +70,45 @@ class _PortForwardPageState extends State<PortForwardPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return Scaffold( return Overlay(initialEntries: [
backgroundColor: MyTheme.color(context).grayBg, OverlayEntry(builder: (context) {
body: FutureBuilder(future: () async { _ffi.dialogManager.setOverlayState(Overlay.of(context));
if (!isRdp) { return Scaffold(
refreshTunnelConfig(); backgroundColor: MyTheme.color(context).grayBg,
} body: FutureBuilder(future: () async {
}(), builder: (context, snapshot) { if (!isRdp) {
if (snapshot.connectionState == ConnectionState.done) { refreshTunnelConfig();
return Container( }
decoration: BoxDecoration( }(), builder: (context, snapshot) {
border: Border.all( if (snapshot.connectionState == ConnectionState.done) {
width: 20, color: MyTheme.color(context).grayBg!)), return Container(
child: Column( decoration: BoxDecoration(
crossAxisAlignment: CrossAxisAlignment.stretch, border: Border.all(
children: [ width: 20, color: MyTheme.color(context).grayBg!)),
buildPrompt(context), child: Column(
Flexible( crossAxisAlignment: CrossAxisAlignment.stretch,
child: Container( children: [
decoration: BoxDecoration( buildPrompt(context),
color: MyTheme.color(context).bg, Flexible(
border: Border.all(width: 1, color: MyTheme.border)), child: Container(
child: decoration: BoxDecoration(
widget.isRDP ? buildRdp(context) : buildTunnel(context), 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) { buildPrompt(BuildContext context) {

View File

@ -18,7 +18,7 @@ class PortForwardTabPage extends StatefulWidget {
} }
class _PortForwardTabPageState extends State<PortForwardTabPage> { class _PortForwardTabPageState extends State<PortForwardTabPage> {
final tabController = Get.put(DesktopTabController()); late final DesktopTabController tabController;
late final bool isRDP; late final bool isRDP;
static const IconData selectedIcon = Icons.forward_sharp; static const IconData selectedIcon = Icons.forward_sharp;
@ -26,6 +26,8 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
_PortForwardTabPageState(Map<String, dynamic> params) { _PortForwardTabPageState(Map<String, dynamic> params) {
isRDP = params['isRDP']; isRDP = params['isRDP'];
tabController = Get.put(DesktopTabController(
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward));
tabController.add(TabInfo( tabController.add(TabInfo(
key: params['id'], key: params['id'],
label: params['id'], label: params['id'],
@ -67,7 +69,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
return SubWindowDragToResizeArea( return SubWindowDragToResizeArea(
windowId: windowId(), windowId: windowId(),
child: Container( child: Container(
@ -77,14 +78,10 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
backgroundColor: MyTheme.color(context).bg, backgroundColor: MyTheme.color(context).bg,
body: DesktopTab( body: DesktopTab(
controller: tabController, controller: tabController,
theme: theme,
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward,
onClose: () { onClose: () {
tabController.clear(); tabController.clear();
}, },
tail: AddButton( tail: AddButton().paddingOnly(left: 10),
theme: theme,
).paddingOnly(left: 10),
)), )),
), ),
); );

View File

@ -17,7 +17,6 @@ import '../../mobile/widgets/dialog.dart';
import '../../mobile/widgets/overlay.dart'; import '../../mobile/widgets/overlay.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../../models/chat_model.dart';
import '../../common/shared_state.dart'; import '../../common/shared_state.dart';
final initText = '\1' * 1024; final initText = '\1' * 1024;
@ -39,10 +38,11 @@ class RemotePage extends StatefulWidget {
class _RemotePageState extends State<RemotePage> class _RemotePageState extends State<RemotePage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
Timer? _timer; Timer? _timer;
bool _showBar = !isWebDesktop;
String _value = ''; String _value = '';
String keyboardMode = "legacy"; String keyboardMode = "legacy";
final _cursorOverImage = false.obs; final _cursorOverImage = false.obs;
late RxBool _showRemoteCursor;
late RxBool _keyboardEnabled;
final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _mobileFocusNode = FocusNode();
final FocusNode _physicalFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode();
@ -59,17 +59,24 @@ class _RemotePageState extends State<RemotePage>
PrivacyModeState.init(id); PrivacyModeState.init(id);
BlockInputState.init(id); BlockInputState.init(id);
CurrentDisplayState.init(id); CurrentDisplayState.init(id);
KeyboardEnabledState.init(id);
ShowRemoteCursorState.init(id);
_showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id);
} }
void _removeStates(String id) { void _removeStates(String id) {
PrivacyModeState.delete(id); PrivacyModeState.delete(id);
BlockInputState.delete(id); BlockInputState.delete(id);
CurrentDisplayState.delete(id); CurrentDisplayState.delete(id);
ShowRemoteCursorState.delete(id);
KeyboardEnabledState.delete(id);
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initStates(widget.id);
_ffi = FFI(); _ffi = FFI();
_updateTabBarHeight(); _updateTabBarHeight();
Get.put(_ffi, tag: widget.id); Get.put(_ffi, tag: widget.id);
@ -87,7 +94,8 @@ class _RemotePageState extends State<RemotePage>
_ffi.listenToMouse(true); _ffi.listenToMouse(true);
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
// WindowManager.instance.addListener(this); // WindowManager.instance.addListener(this);
_initStates(widget.id); _showRemoteCursor.value = bind.sessionGetToggleOptionSync(
id: widget.id, arg: 'show-remote-cursor');
} }
@override @override
@ -132,7 +140,7 @@ class _RemotePageState extends State<RemotePage>
common < oldValue.length && common < oldValue.length &&
common < newValue.length && common < newValue.length &&
newValue[common] == oldValue[common]; newValue[common] == oldValue[common];
++common); ++common) {}
for (i = 0; i < oldValue.length - common; ++i) { for (i = 0; i < oldValue.length - common; ++i) {
_ffi.inputKey('VK_BACK'); _ffi.inputKey('VK_BACK');
} }
@ -146,8 +154,8 @@ class _RemotePageState extends State<RemotePage>
} }
return; return;
} }
if (oldValue.length > 0 && if (oldValue.isNotEmpty &&
newValue.length > 0 && newValue.isNotEmpty &&
oldValue[0] == '\1' && oldValue[0] == '\1' &&
newValue[0] != '\1') { newValue[0] != '\1') {
// clipboard // clipboard
@ -156,7 +164,7 @@ class _RemotePageState extends State<RemotePage>
if (newValue.length == oldValue.length) { if (newValue.length == oldValue.length) {
// ? // ?
} else if (newValue.length < oldValue.length) { } else if (newValue.length < oldValue.length) {
final char = 'VK_BACK'; const char = 'VK_BACK';
_ffi.inputKey(char); _ffi.inputKey(char);
} else { } else {
final content = newValue.substring(oldValue.length); final content = newValue.substring(oldValue.length);
@ -200,25 +208,9 @@ class _RemotePageState extends State<RemotePage>
_ffi.inputKey(label, down: down, press: press ?? false); _ffi.inputKey(label, down: down, press: press ?? false);
} }
Widget buildBody(BuildContext context, FfiModel ffiModel) { Widget buildBody(BuildContext context) {
final hasDisplays = ffiModel.pi.displays.length > 0;
final keyboard = ffiModel.permissions['keyboard'] != false;
return Scaffold( return Scaffold(
backgroundColor: MyTheme.color(context).bg, backgroundColor: MyTheme.color(context).bg,
// resizeToAvoidBottomInset: true,
// floatingActionButton: _showBar
// ? null
// : FloatingActionButton(
// mini: true,
// child: Icon(Icons.expand_less),
// backgroundColor: MyTheme.accent,
// onPressed: () {
// setState(() {
// _showBar = !_showBar;
// });
// }),
// bottomNavigationBar:
// _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
body: Overlay( body: Overlay(
initialEntries: [ initialEntries: [
OverlayEntry(builder: (context) { OverlayEntry(builder: (context) {
@ -226,8 +218,7 @@ class _RemotePageState extends State<RemotePage>
_ffi.dialogManager.setOverlayState(Overlay.of(context)); _ffi.dialogManager.setOverlayState(Overlay.of(context));
return Container( return Container(
color: Colors.black, color: Colors.black,
child: getRawPointerAndKeyBody( child: getRawPointerAndKeyBody(getBodyForDesktop(context)));
getBodyForDesktop(context, keyboard)));
}) })
], ],
)); ));
@ -242,16 +233,12 @@ class _RemotePageState extends State<RemotePage>
clientClose(_ffi.dialogManager); clientClose(_ffi.dialogManager);
return false; return false;
}, },
child: MultiProvider( child: MultiProvider(providers: [
providers: [ ChangeNotifierProvider.value(value: _ffi.ffiModel),
ChangeNotifierProvider.value(value: _ffi.ffiModel), ChangeNotifierProvider.value(value: _ffi.imageModel),
ChangeNotifierProvider.value(value: _ffi.imageModel), ChangeNotifierProvider.value(value: _ffi.cursorModel),
ChangeNotifierProvider.value(value: _ffi.cursorModel), ChangeNotifierProvider.value(value: _ffi.canvasModel),
ChangeNotifierProvider.value(value: _ffi.canvasModel), ], child: buildBody(context)));
],
child: Consumer<FfiModel>(
builder: (context, ffiModel, _child) =>
buildBody(context, ffiModel))));
} }
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
@ -332,7 +319,8 @@ class _RemotePageState extends State<RemotePage>
key == LogicalKeyboardKey.shiftLeft) { key == LogicalKeyboardKey.shiftLeft) {
_ffi.shift = false; _ffi.shift = false;
} else if (key == LogicalKeyboardKey.metaLeft || } else if (key == LogicalKeyboardKey.metaLeft ||
key == LogicalKeyboardKey.metaRight || key == LogicalKeyboardKey.superKey) { key == LogicalKeyboardKey.metaRight ||
key == LogicalKeyboardKey.superKey) {
_ffi.command = false; _ffi.command = false;
} }
sendRawKey(e); sendRawKey(e);
@ -340,116 +328,17 @@ class _RemotePageState extends State<RemotePage>
} }
Widget getRawPointerAndKeyBody(Widget child) { Widget getRawPointerAndKeyBody(Widget child) {
return Consumer<FfiModel>( return FocusScope(
builder: (context, FfiModel, _child) => MouseRegion( autofocus: true,
cursor: FfiModel.permissions['keyboard'] != false child: Focus(
? SystemMouseCursors.none autofocus: true,
: MouseCursor.defer, canRequestFocus: true,
child: FocusScope( focusNode: _physicalFocusNode,
autofocus: true, onFocusChange: (bool v) {
child: Focus( _imageFocused = v;
autofocus: true, },
canRequestFocus: true, onKey: handleRawKeyEvent,
focusNode: _physicalFocusNode, child: child));
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);
}),
],
),
));
} }
/// touchMode only: /// touchMode only:
@ -461,7 +350,6 @@ class _RemotePageState extends State<RemotePage>
/// mouseMode only: /// mouseMode only:
/// DoubleFiner -> right click /// DoubleFiner -> right click
/// HoldDrag -> left drag /// HoldDrag -> left drag
void _onPointHoverImage(PointerHoverEvent e) { void _onPointHoverImage(PointerHoverEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return; if (e.kind != ui.PointerDeviceKind.mouse) return;
if (!_isPhysicalMouse) { if (!_isPhysicalMouse) {
@ -509,12 +397,16 @@ class _RemotePageState extends State<RemotePage>
if (e is PointerScrollEvent) { if (e is PointerScrollEvent) {
var dx = e.scrollDelta.dx.toInt(); var dx = e.scrollDelta.dx.toInt();
var dy = e.scrollDelta.dy.toInt(); var dy = e.scrollDelta.dy.toInt();
if (dx > 0) if (dx > 0) {
dx = -1; dx = -1;
else if (dx < 0) dx = 1; } else if (dx < 0) {
if (dy > 0) dx = 1;
}
if (dy > 0) {
dy = -1; dy = -1;
else if (dy < 0) dy = 1; } else if (dy < 0) {
dy = 1;
}
bind.sessionSendMouse( bind.sessionSendMouse(
id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}');
} }
@ -544,32 +436,30 @@ class _RemotePageState extends State<RemotePage>
MouseRegion(onEnter: enterView, onExit: leaveView, child: child)); MouseRegion(onEnter: enterView, onExit: leaveView, child: child));
} }
Widget getBodyForDesktop(BuildContext context, bool keyboard) { Widget getBodyForDesktop(BuildContext context) {
var paints = <Widget>[ var paints = <Widget>[
MouseRegion(onEnter: (evt) { MouseRegion(onEnter: (evt) {
bind.hostStopSystemKeyPropagate(stopped: false); bind.hostStopSystemKeyPropagate(stopped: false);
}, onExit: (evt) { }, onExit: (evt) {
bind.hostStopSystemKeyPropagate(stopped: true); bind.hostStopSystemKeyPropagate(stopped: true);
}, child: Container( }, child: LayoutBuilder(builder: (context, constraints) {
child: LayoutBuilder(builder: (context, constraints) { Future.delayed(Duration.zero, () {
Future.delayed(Duration.zero, () { Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
Provider.of<CanvasModel>(context, listen: false).updateViewStyle(); });
}); return ImagePaint(
return ImagePaint( id: widget.id,
id: widget.id, cursorOverImage: _cursorOverImage,
cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled,
listenerBuilder: _buildImageListener, listenerBuilder: _buildImageListener,
); );
}), }))
))
]; ];
final cursor = bind.sessionGetToggleOptionSync(
id: widget.id, arg: 'show-remote-cursor'); paints.add(Obx(() => Visibility(
if (keyboard || cursor) { visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue,
paints.add(CursorPaint( child: CursorPaint(
id: widget.id, id: widget.id,
)); ))));
}
paints.add(QualityMonitor(_ffi.qualityMonitorModel)); paints.add(QualityMonitor(_ffi.qualityMonitorModel));
paints.add(RemoteMenubar( paints.add(RemoteMenubar(
id: widget.id, id: widget.id,
@ -601,106 +491,6 @@ class _RemotePageState extends State<RemotePage>
return out; return out;
} }
void showActions(String id, FfiModel ffiModel) async {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height - super.widget.tabBarHeight;
final more = <PopupMenuItem<String>>[];
final pi = _ffi.ffiModel.pi;
final perms = _ffi.ffiModel.permissions;
if (pi.version.isNotEmpty) {
more.add(PopupMenuItem<String>(
child: Text(translate('Refresh')), value: 'refresh'));
}
more.add(PopupMenuItem<String>(
child: Row(
children: ([
Text(translate('OS Password')),
TextButton(
style: flatButtonStyle,
onPressed: () {
showSetOSPassword(widget.id, false, _ffi.dialogManager);
},
child: Icon(Icons.edit, color: MyTheme.accent),
)
])),
value: 'enter_os_password'));
if (!isWebDesktop) {
if (perms['keyboard'] != false && perms['clipboard'] != false) {
more.add(PopupMenuItem<String>(
child: Text(translate('Paste')), value: 'paste'));
}
more.add(PopupMenuItem<String>(
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
}
if (perms['keyboard'] != false) {
if (pi.platform == 'Linux' || pi.sasEnabled) {
more.add(PopupMenuItem<String>(
child: Text(translate('Insert') + ' Ctrl + Alt + Del'),
value: 'cad'));
}
more.add(PopupMenuItem<String>(
child: Text(translate('Insert Lock')), value: 'lock'));
if (pi.platform == 'Windows' &&
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
true) {
more.add(PopupMenuItem<String>(
child: Text(translate(
(ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')),
value: 'block-input'));
}
}
if (gFFI.ffiModel.permissions["restart"] != false &&
(pi.platform == "Linux" ||
pi.platform == "Windows" ||
pi.platform == "Mac OS")) {
more.add(PopupMenuItem<String>(
child: Text(translate('Restart Remote Device')), value: 'restart'));
}
() async {
var value = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: more,
elevation: 8,
);
if (value == 'cad') {
bind.sessionCtrlAltDel(id: widget.id);
} else if (value == 'lock') {
bind.sessionLockScreen(id: widget.id);
} else if (value == 'block-input') {
bind.sessionToggleOption(
id: widget.id,
value: (_ffi.ffiModel.inputBlocked ? 'un' : '') + 'block-input');
_ffi.ffiModel.inputBlocked = !_ffi.ffiModel.inputBlocked;
} else if (value == 'refresh') {
bind.sessionRefresh(id: widget.id);
} else if (value == 'paste') {
() async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
bind.sessionInputString(id: widget.id, value: data.text ?? "");
}
}();
} else if (value == 'enter_os_password') {
// FIXME:
// TODO icon diff
// null means no session of id
// empty string means no password
var password = await bind.sessionGetOption(id: id, arg: "os-password");
if (password != null) {
bind.sessionInputOsPassword(id: widget.id, value: password);
} else {
showSetOSPassword(widget.id, true, _ffi.dialogManager);
}
} else if (value == 'reset_canvas') {
_ffi.cursorModel.reset();
} else if (value == 'restart') {
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
}
}();
}
@override @override
void onWindowEvent(String eventName) { void onWindowEvent(String eventName) {
print("window event: $eventName"); print("window event: $eventName");
@ -709,7 +499,7 @@ class _RemotePageState extends State<RemotePage>
_ffi.canvasModel.updateViewStyle(); _ffi.canvasModel.updateViewStyle();
break; break;
case 'maximize': case 'maximize':
Future.delayed(Duration(milliseconds: 100), () { Future.delayed(const Duration(milliseconds: 100), () {
_ffi.canvasModel.updateViewStyle(); _ffi.canvasModel.updateViewStyle();
}); });
break; break;
@ -723,6 +513,7 @@ class _RemotePageState extends State<RemotePage>
class ImagePaint extends StatelessWidget { class ImagePaint extends StatelessWidget {
final String id; final String id;
final Rx<bool> cursorOverImage; final Rx<bool> cursorOverImage;
final Rx<bool> keyboardEnabled;
final Widget Function(Widget)? listenerBuilder; final Widget Function(Widget)? listenerBuilder;
final ScrollController _horizontal = ScrollController(); final ScrollController _horizontal = ScrollController();
final ScrollController _vertical = ScrollController(); final ScrollController _vertical = ScrollController();
@ -731,7 +522,8 @@ class ImagePaint extends StatelessWidget {
{Key? key, {Key? key,
required this.id, required this.id,
required this.cursorOverImage, required this.cursorOverImage,
this.listenerBuilder = null}) required this.keyboardEnabled,
this.listenerBuilder})
: super(key: key); : super(key: key);
@override @override
@ -747,25 +539,26 @@ class ImagePaint extends StatelessWidget {
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
)); ));
return Center( return Center(
child: NotificationListener<ScrollNotification>( child: NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
final percentX = _horizontal.position.extentBefore / final percentX = _horizontal.position.extentBefore /
(_horizontal.position.extentBefore + (_horizontal.position.extentBefore +
_horizontal.position.extentInside + _horizontal.position.extentInside +
_horizontal.position.extentAfter); _horizontal.position.extentAfter);
final percentY = _vertical.position.extentBefore / final percentY = _vertical.position.extentBefore /
(_vertical.position.extentBefore + (_vertical.position.extentBefore +
_vertical.position.extentInside + _vertical.position.extentInside +
_vertical.position.extentAfter); _vertical.position.extentAfter);
c.setScrollPercent(percentX, percentY); c.setScrollPercent(percentX, percentY);
return false; return false;
}, },
child: Obx(() => MouseRegion( child: Obx(() => MouseRegion(
cursor: cursorOverImage.value cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue)
? SystemMouseCursors.none ? SystemMouseCursors.none
: SystemMouseCursors.basic, : MouseCursor.defer,
child: _buildCrossScrollbar(_buildListener(imageWidget)))), child: _buildCrossScrollbar(_buildListener(imageWidget)))),
)); ),
);
} else { } else {
final imageWidget = SizedBox( final imageWidget = SizedBox(
width: c.size.width, width: c.size.width,
@ -824,13 +617,12 @@ class CursorPaint extends StatelessWidget {
final m = Provider.of<CursorModel>(context); final m = Provider.of<CursorModel>(context);
final c = Provider.of<CanvasModel>(context); final c = Provider.of<CanvasModel>(context);
// final adjust = m.adjustForKeyboard(); // final adjust = m.adjustForKeyboard();
var s = c.scale;
return CustomPaint( return CustomPaint(
painter: ImagePainter( painter: ImagePainter(
image: m.image, image: m.image,
x: m.x * s - m.hotx + c.x, x: m.x - m.hotx + c.x / c.scale,
y: m.y * s - m.hoty + c.y, y: m.y - m.hoty + c.y / c.scale,
scale: 1), scale: c.scale),
); );
} }
} }
@ -882,205 +674,35 @@ class QualityMonitor extends StatelessWidget {
right: 10, right: 10,
child: qualityMonitorModel.show child: qualityMonitorModel.show
? Container( ? Container(
padding: EdgeInsets.all(8), padding: const EdgeInsets.all(8),
color: MyTheme.canvasColor.withAlpha(120), color: MyTheme.canvasColor.withAlpha(120),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"Speed: ${qualityMonitorModel.data.speed ?? ''}", "Speed: ${qualityMonitorModel.data.speed ?? ''}",
style: TextStyle(color: MyTheme.grayBg), style: const TextStyle(color: MyTheme.grayBg),
), ),
Text( Text(
"FPS: ${qualityMonitorModel.data.fps ?? ''}", "FPS: ${qualityMonitorModel.data.fps ?? ''}",
style: TextStyle(color: MyTheme.grayBg), style: const TextStyle(color: MyTheme.grayBg),
), ),
Text( Text(
"Delay: ${qualityMonitorModel.data.delay ?? ''} ms", "Delay: ${qualityMonitorModel.data.delay ?? ''} ms",
style: TextStyle(color: MyTheme.grayBg), style: const TextStyle(color: MyTheme.grayBg),
), ),
Text( Text(
"Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb",
style: TextStyle(color: MyTheme.grayBg), style: const TextStyle(color: MyTheme.grayBg),
), ),
Text( Text(
"Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}",
style: TextStyle(color: MyTheme.grayBg), style: const TextStyle(color: MyTheme.grayBg),
), ),
], ],
), ),
) )
: SizedBox.shrink()))); : const SizedBox.shrink())));
}
void showOptions(String id) async {
final _ffi = ffi(id);
String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced';
if (quality == '') quality = 'balanced';
String viewStyle =
await bind.sessionGetOption(id: id, arg: 'view-style') ?? '';
String scrollStyle =
await bind.sessionGetOption(id: id, arg: 'scroll-style') ?? '';
var displays = <Widget>[];
final pi = _ffi.ffiModel.pi;
final image = _ffi.ffiModel.getConnectionImage();
if (image != null)
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
if (pi.displays.length > 1) {
final cur = pi.currentDisplay;
final children = <Widget>[];
for (var i = 0; i < pi.displays.length; ++i)
children.add(InkWell(
onTap: () {
if (i == cur) return;
bind.sessionSwitchDisplay(id: id, value: i);
_ffi.dialogManager.dismissAll();
},
child: Ink(
width: 40,
height: 40,
decoration: BoxDecoration(
border: Border.all(color: Colors.black87),
color: i == cur ? Colors.black87 : Colors.white),
child: Center(
child: Text((i + 1).toString(),
style: TextStyle(
color: i == cur ? Colors.white : Colors.black87))))));
displays.add(Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 8,
children: children,
)));
}
if (displays.isNotEmpty) {
displays.add(Divider(color: MyTheme.border));
}
final perms = _ffi.ffiModel.permissions;
_ffi.dialogManager.show((setState, close) {
final more = <Widget>[];
if (perms['audio'] != false) {
more.add(getToggle(id, setState, 'disable-audio', 'Mute'));
}
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false)
more.add(
getToggle(id, setState, 'disable-clipboard', 'Disable clipboard'));
more.add(getToggle(
id, setState, 'lock-after-session-end', 'Lock after session end'));
if (pi.platform == 'Windows') {
more.add(Consumer<FfiModel>(
builder: (_context, _ffiModel, _child) => () {
return getToggle(
id, setState, 'privacy-mode', 'Privacy mode');
}()));
}
}
var setQuality = (String? value) {
if (value == null) return;
setState(() {
quality = value;
bind.sessionSetImageQuality(id: id, value: value);
});
};
var setViewStyle = (String? value) {
if (value == null) return;
setState(() {
viewStyle = value;
bind.sessionPeerOption(id: id, name: "view-style", value: value);
_ffi.canvasModel.updateViewStyle();
});
};
var setScrollStyle = (String? value) {
if (value == null) return;
setState(() {
scrollStyle = value;
bind.sessionPeerOption(id: id, name: "scroll-style", value: value);
_ffi.canvasModel.updateScrollStyle();
});
};
return CustomAlertDialog(
title: SizedBox.shrink(),
content: Column(
mainAxisSize: MainAxisSize.min,
children: displays +
<Widget>[
getRadio('Original', 'original', viewStyle, setViewStyle),
getRadio('Shrink', 'shrink', viewStyle, setViewStyle),
getRadio('Stretch', 'stretch', viewStyle, setViewStyle),
Divider(color: MyTheme.border),
getRadio(
'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle),
getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle),
Divider(color: MyTheme.border),
getRadio('Good image quality', 'best', quality, setQuality),
getRadio('Balanced', 'balanced', quality, setQuality),
getRadio('Optimize reaction time', 'low', quality, setQuality),
Divider(color: MyTheme.border),
getToggle(
id, setState, 'show-remote-cursor', 'Show remote cursor'),
getToggle(id, setState, 'show-quality-monitor',
'Show quality monitor',
ffi: _ffi),
] +
more),
actions: [],
contentPadding: 0,
);
}, clickMaskDismiss: true, backDismiss: true);
}
void showSetOSPassword(
String id, bool login, OverlayDialogManager dialogManager) async {
final controller = TextEditingController();
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
controller.text = password;
dialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('OS Password')),
content: Column(mainAxisSize: MainAxisSize.min, children: [
PasswordWidget(controller: controller),
CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate('Auto Login'),
),
value: autoLogin,
onChanged: (v) {
if (v == null) return;
setState(() => autoLogin = v);
},
),
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () {
close();
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: () {
var text = controller.text.trim();
bind.sessionPeerOption(id: id, name: "os-password", value: text);
bind.sessionPeerOption(
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
if (text != "" && login) {
bind.sessionInputOsPassword(id: id, value: text);
}
close();
},
child: Text(translate('OK')),
),
]);
});
} }
void sendPrompt(String id, bool isMac, String key) { void sendPrompt(String id, bool isMac, String key) {

View File

@ -19,10 +19,12 @@ class DesktopServerPage extends StatefulWidget {
class _DesktopServerPageState extends State<DesktopServerPage> class _DesktopServerPageState extends State<DesktopServerPage>
with WindowListener, AutomaticKeepAliveClientMixin { with WindowListener, AutomaticKeepAliveClientMixin {
final tabController = gFFI.serverModel.tabController;
@override @override
void initState() { void initState() {
gFFI.ffiModel.updateEventListener(""); gFFI.ffiModel.updateEventListener("");
windowManager.addListener(this); windowManager.addListener(this);
tabController.onRemove = (_, id) => onRemoveId(id);
super.initState(); super.initState();
} }
@ -39,6 +41,13 @@ class _DesktopServerPageState extends State<DesktopServerPage>
super.onWindowClose(); super.onWindowClose();
} }
void onRemoveId(String id) {
if (tabController.state.value.tabs.isEmpty) {
windowManager.close();
}
}
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return MultiProvider( return MultiProvider(
@ -48,22 +57,25 @@ class _DesktopServerPageState extends State<DesktopServerPage>
], ],
child: Consumer<ServerModel>( child: Consumer<ServerModel>(
builder: (context, serverModel, child) => Container( builder: (context, serverModel, child) => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: border: Border.all(color: MyTheme.color(context).border!)),
Border.all(color: MyTheme.color(context).border!)), child: Scaffold(
child: Scaffold( backgroundColor: MyTheme.color(context).bg,
backgroundColor: MyTheme.color(context).bg, body: Overlay(initialEntries: [
body: Center( OverlayEntry(builder: (context) {
child: Column( gFFI.dialogManager.setOverlayState(Overlay.of(context));
mainAxisAlignment: MainAxisAlignment.start, return Center(
children: [ child: Column(
Expanded(child: ConnectionManager()), mainAxisAlignment: MainAxisAlignment.start,
SizedBox.fromSize(size: Size(0, 15.0)), children: [
], Expanded(child: ConnectionManager()),
), SizedBox.fromSize(size: Size(0, 15.0)),
), ],
), ),
))); );
})
]),
))));
} }
@override @override
@ -106,12 +118,11 @@ class ConnectionManagerState extends State<ConnectionManager> {
], ],
) )
: DesktopTab( : DesktopTab(
theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(),
showTitle: false, showTitle: false,
showMaximize: false, showMaximize: false,
showMinimize: false, showMinimize: true,
showClose: true,
controller: serverModel.tabController, controller: serverModel.tabController,
tabType: DesktopTabType.cm,
pageViewBuilder: (pageView) => Row(children: [ pageViewBuilder: (pageView) => Row(children: [
Expanded(child: pageView), Expanded(child: pageView),
Consumer<ChatModel>( Consumer<ChatModel>(
@ -450,8 +461,10 @@ class _CmControlPanel extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), color: MyTheme.accent, borderRadius: BorderRadius.circular(10)),
child: InkWell( child: InkWell(
onTap: () => onTap: () => checkClickTime(client.id, () {
checkClickTime(client.id, () => handleAccept(context)), handleAccept(context);
windowManager.minimize();
}),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [

View File

@ -5,6 +5,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/formatter/id_formatter.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/peer_model.dart'; import '../../models/peer_model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
@ -15,7 +16,7 @@ class _PopupMenuTheme {
static const Color commonColor = MyTheme.accent; static const Color commonColor = MyTheme.accent;
// kMinInteractiveDimension // kMinInteractiveDimension
static const double height = 25.0; static const double height = 25.0;
static const double dividerHeight = 12.0; static const double dividerHeight = 3.0;
} }
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>> typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
@ -46,7 +47,8 @@ class _PeerCard extends StatefulWidget {
/// State for the connection page. /// State for the connection page.
class _PeerCardState extends State<_PeerCard> class _PeerCardState extends State<_PeerCard>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
final double _cardRadis = 20; var _menuPos = RelativeRect.fill;
final double _cardRadis = 16;
final double _borderWidth = 2; final double _borderWidth = 2;
final RxBool _iconMoreHover = false.obs; final RxBool _iconMoreHover = false.obs;
@ -118,7 +120,7 @@ class _PeerCardState extends State<_PeerCard>
? Colors.green ? Colors.green
: Colors.yellow)), : Colors.yellow)),
Text( Text(
'${peer.id}', formatID('${peer.id}'),
style: TextStyle(fontWeight: FontWeight.w400), style: TextStyle(fontWeight: FontWeight.w400),
), ),
]), ]),
@ -239,7 +241,7 @@ class _PeerCardState extends State<_PeerCard>
backgroundColor: peer.online backgroundColor: peer.online
? Colors.green ? Colors.green
: Colors.yellow)), : Colors.yellow)),
Text(peer.id) Text(formatID(peer.id))
]).paddingSymmetric(vertical: 8), ]).paddingSymmetric(vertical: 8),
_actionMore(peer), _actionMore(peer),
], ],
@ -253,36 +255,36 @@ class _PeerCardState extends State<_PeerCard>
); );
} }
Widget _actionMore(Peer peer) { Widget _actionMore(Peer peer) => Listener(
return FutureBuilder( onPointerDown: (e) {
future: widget.popupMenuEntryBuilder(context), final x = e.position.dx;
initialData: const <mod_menu.PopupMenuEntry<String>>[], final y = e.position.dy;
builder: (BuildContext context, _menuPos = RelativeRect.fromLTRB(x, y, x, y);
AsyncSnapshot<List<mod_menu.PopupMenuEntry<String>>> snapshot) { },
if (snapshot.hasData) { onPointerUp: (_) => _showPeerMenu(context, peer.id),
return Listener( child: MouseRegion(
child: MouseRegion( onEnter: (_) => _iconMoreHover.value = true,
onEnter: (_) => _iconMoreHover.value = true, onExit: (_) => _iconMoreHover.value = false,
onExit: (_) => _iconMoreHover.value = false, child: CircleAvatar(
child: CircleAvatar( radius: 14,
radius: 14, backgroundColor: _iconMoreHover.value
backgroundColor: _iconMoreHover.value ? MyTheme.color(context).grayBg!
? MyTheme.color(context).grayBg! : MyTheme.color(context).bg!,
: MyTheme.color(context).bg!, child: Icon(Icons.more_vert,
child: mod_menu.PopupMenuButton( size: 18,
padding: EdgeInsets.zero, color: _iconMoreHover.value
icon: Icon(Icons.more_vert, ? MyTheme.color(context).text
size: 18, : MyTheme.color(context).lightText))));
color: _iconMoreHover.value
? MyTheme.color(context).text /// Show the peer menu and handle user's choice.
: MyTheme.color(context).lightText), /// User might remove the peer or send a file to the peer.
position: mod_menu.PopupMenuPosition.under, void _showPeerMenu(BuildContext context, String id) async {
itemBuilder: (BuildContext context) => snapshot.data!, await mod_menu.showMenu(
)))); context: context,
} else { position: _menuPos,
return Container(); items: await super.widget.popupMenuEntryBuilder(context),
} elevation: 8,
}); );
} }
/// Get the image for the current [platform]. /// Get the image for the current [platform].
@ -411,19 +413,26 @@ abstract class BasePeerCard extends StatelessWidget {
@protected @protected
MenuEntryBase<String> _rdpAction(BuildContext context, String id) { MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
return MenuEntryButton<String>( return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Row( childBuilder: (TextStyle? style) => Container(
children: [ alignment: AlignmentDirectional.center,
Text( height: _PopupMenuTheme.height,
translate('RDP'), child: Row(
style: style, children: [
), Text(
SizedBox(width: 20), translate('RDP'),
IconButton( style: style,
icon: Icon(Icons.edit), ),
onPressed: () => _rdpDialog(id), Expanded(
) child: Align(
], alignment: Alignment.centerRight,
), child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(Icons.edit),
onPressed: () => _rdpDialog(id),
),
))
],
)),
proc: () { proc: () {
_connect(context, id, isRDP: true); _connect(context, id, isRDP: true);
}, },
@ -554,47 +563,47 @@ abstract class BasePeerCard extends StatelessWidget {
} }
} }
gFFI.dialogManager.show((setState, close) { 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( return CustomAlertDialog(
title: Text(translate('Rename')), title: Text(translate('Rename')),
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Form( child: Form(
child: TextFormField( child: TextFormField(
controller: controller, controller: controller,
decoration: InputDecoration(border: OutlineInputBorder()), focusNode: FocusNode()..requestFocus(),
decoration:
const InputDecoration(border: OutlineInputBorder()),
), ),
), ),
), ),
Obx(() => Offstage( Obx(() => Offstage(
offstage: isInProgress.isFalse, offstage: isInProgress.isFalse,
child: LinearProgressIndicator())), child: const LinearProgressIndicator())),
], ],
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
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"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }
@ -614,6 +623,7 @@ class RecentPeerCard extends BasePeerCard {
if (peer.platform == 'Windows') { if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
menuItems.add(MenuEntryDivider());
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
menuItems.add(_renameAction(peer.id, false)); menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async { menuItems.add(_removeAction(peer.id, () async {
@ -740,13 +750,23 @@ class AddressBookPeerCard extends BasePeerCard {
var selectedTag = gFFI.abModel.getPeerTags(id).obs; var selectedTag = gFFI.abModel.getPeerTags(id).obs;
gFFI.dialogManager.show((setState, close) { gFFI.dialogManager.show((setState, close) {
submit() async {
setState(() {
isInProgress = true;
});
gFFI.abModel.changeTagForPeer(id, selectedTag);
await gFFI.abModel.updateAb();
close();
}
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate("Edit Tag")), title: Text(translate("Edit Tag")),
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Wrap( child: Wrap(
children: tags children: tags
.map((e) => _buildTag(e, selectedTag, onTap: () { .map((e) => _buildTag(e, selectedTag, onTap: () {
@ -759,26 +779,16 @@ class AddressBookPeerCard extends BasePeerCard {
.toList(growable: false), .toList(growable: false),
), ),
), ),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
], ],
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
setState(() {
isInProgress = true;
});
gFFI.abModel.changeTagForPeer(id, selectedTag);
await gFFI.abModel.updateAb();
close();
},
child: Text(translate("OK"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }
@ -861,25 +871,35 @@ void _rdpDialog(String id) async {
RxBool secure = true.obs; RxBool secure = true.obs;
gFFI.dialogManager.show((setState, close) { gFFI.dialogManager.show((setState, close) {
submit() async {
await bind.mainSetPeerOption(
id: id, key: 'rdp_port', value: portController.text.trim());
await bind.mainSetPeerOption(
id: id, key: 'rdp_username', value: userController.text);
await bind.mainSetPeerOption(
id: id, key: 'rdp_password', value: passwordContorller.text);
close();
}
return CustomAlertDialog( return CustomAlertDialog(
title: Text('RDP ' + translate('Settings')), title: Text('RDP ${translate('Settings')}'),
content: ConstrainedBox( content: ConstrainedBox(
constraints: BoxConstraints(minWidth: 500), constraints: const BoxConstraints(minWidth: 500),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text( child: Text(
"${translate('Port')}:", "${translate('Port')}:",
textAlign: TextAlign.start, textAlign: TextAlign.start,
).marginOnly(bottom: 16.0)), ).marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
@ -888,52 +908,54 @@ void _rdpDialog(String id) async {
FilteringTextInputFormatter.allow(RegExp( FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')) r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$'))
], ],
decoration: InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), hintText: '3389'), border: OutlineInputBorder(), hintText: '3389'),
controller: portController, controller: portController,
focusNode: FocusNode()..requestFocus(),
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text( child: Text(
"${translate('Username')}:", "${translate('Username')}:",
textAlign: TextAlign.start, textAlign: TextAlign.start,
).marginOnly(bottom: 16.0)), ).marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: InputDecoration(border: OutlineInputBorder()), decoration:
const InputDecoration(border: OutlineInputBorder()),
controller: userController, controller: userController,
), ),
), ),
], ],
), ),
SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Row( Row(
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(minWidth: 100), constraints: const BoxConstraints(minWidth: 100),
child: Text("${translate('Password')}:") child: Text("${translate('Password')}:")
.marginOnly(bottom: 16.0)), .marginOnly(bottom: 16.0)),
SizedBox( const SizedBox(
width: 24.0, width: 24.0,
), ),
Expanded( Expanded(
child: Obx(() => TextField( child: Obx(() => TextField(
obscureText: secure.value, obscureText: secure.value,
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: () => secure.value = !secure.value, onPressed: () => secure.value = !secure.value,
icon: Icon(secure.value icon: Icon(secure.value
@ -948,23 +970,11 @@ void _rdpDialog(String id) async {
), ),
), ),
actions: [ actions: [
TextButton( TextButton(onPressed: close, child: Text(translate("Cancel"))),
onPressed: () { TextButton(onPressed: submit, child: Text(translate("OK"))),
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
await bind.mainSetPeerOption(
id: id, key: 'rdp_port', value: portController.text.trim());
await bind.mainSetPeerOption(
id: id, key: 'rdp_username', value: userController.text);
await bind.mainSetPeerOption(
id: id, key: 'rdp_password', value: passwordContorller.text);
close();
},
child: Text(translate("OK"))),
], ],
onSubmit: submit,
onCancel: close,
); );
}); });
} }

View File

@ -1,6 +1,7 @@
import 'dart:core'; import 'dart:core';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import './material_mod_popup_menu.dart' as mod_menu; import './material_mod_popup_menu.dart' as mod_menu;
@ -174,8 +175,8 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
children: [ children: [
Text( Text(
opt.text, opt.text,
style: const TextStyle( style: TextStyle(
color: Colors.black, color: MyTheme.color(context).text,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
), ),
@ -256,8 +257,8 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
children: [ children: [
Text( Text(
opt.text, opt.text,
style: const TextStyle( style: TextStyle(
color: Colors.black, color: MyTheme.color(context).text,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
), ),
@ -300,8 +301,8 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
const SizedBox(width: MenuConfig.midPadding), const SizedBox(width: MenuConfig.midPadding),
Text( Text(
text, text,
style: const TextStyle( style: TextStyle(
color: Colors.black, color: MyTheme.color(context).text,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
), ),
@ -346,8 +347,8 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
// const SizedBox(width: MenuConfig.midPadding), // const SizedBox(width: MenuConfig.midPadding),
Text( Text(
text, text,
style: const TextStyle( style: TextStyle(
color: Colors.black, color: MyTheme.color(context).text,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
), ),
@ -450,8 +451,8 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
const SizedBox(width: MenuConfig.midPadding), const SizedBox(width: MenuConfig.midPadding),
Text( Text(
text, text,
style: const TextStyle( style: TextStyle(
color: Colors.black, color: MyTheme.color(context).text,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
), ),
@ -491,8 +492,8 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height), constraints: BoxConstraints(minHeight: conf.height),
child: childBuilder( child: childBuilder(
const TextStyle( TextStyle(
color: Colors.black, color: MyTheme.color(context).text,
fontSize: MenuConfig.fontSize, fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal), fontWeight: FontWeight.normal),
)), )),

View File

@ -496,9 +496,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}); });
final quality = final quality =
await bind.sessionGetCustomImageQuality(id: widget.id); await bind.sessionGetCustomImageQuality(id: widget.id);
final double initValue = quality != null && quality.isNotEmpty double initValue = quality != null && quality.isNotEmpty
? quality[0].toDouble() ? quality[0].toDouble()
: 50.0; : 50.0;
const minValue = 10.0;
const maxValue = 100.0;
if (initValue < minValue) {
initValue = minValue;
}
if (initValue > maxValue) {
initValue = maxValue;
}
final RxDouble sliderValue = RxDouble(initValue); final RxDouble sliderValue = RxDouble(initValue);
final rxReplay = rxdart.ReplaySubject<double>(); final rxReplay = rxdart.ReplaySubject<double>();
rxReplay rxReplay
@ -513,30 +521,44 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
final slider = Obx(() { final slider = Obx(() {
return Slider( return Slider(
value: sliderValue.value, value: sliderValue.value,
max: 100, min: minValue,
divisions: 100, max: maxValue,
label: sliderValue.value.round().toString(), divisions: 90,
onChanged: (double value) { onChanged: (double value) {
sliderValue.value = value; sliderValue.value = value;
rxReplay.add(value); rxReplay.add(value);
}, },
); );
}); });
final content = Row(
children: [
slider,
SizedBox(
width: 90,
child: Obx(() => Text(
'${sliderValue.value.round()}% Bitrate',
style: const TextStyle(fontSize: 15),
)))
],
);
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
slider, [btnCancel]); content, [btnCancel]);
} }
}), }),
MenuEntryDivider<String>(), MenuEntryDivider<String>(),
MenuEntrySwitch<String>( () {
text: translate('Show remote cursor'), final state = ShowRemoteCursorState.find(widget.id);
getter: () async { return MenuEntrySwitch2<String>(
return bind.sessionGetToggleOptionSync( text: translate('Show remote cursor'),
id: widget.id, arg: 'show-remote-cursor'); getter: () {
}, return state;
setter: (bool v) async { },
await bind.sessionToggleOption( setter: (bool v) async {
id: widget.id, value: 'show-remote-cursor'); state.value = v;
}), await bind.sessionToggleOption(
id: widget.id, value: 'show-remote-cursor');
});
}(),
MenuEntrySwitch<String>( MenuEntrySwitch<String>(
text: translate('Show quality monitor'), text: translate('Show quality monitor'),
getter: () async { getter: () async {
@ -565,12 +587,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
'Lock after session end', 'lock-after-session-end')); 'Lock after session end', 'lock-after-session-end'));
if (pi.platform == 'Windows') { if (pi.platform == 'Windows') {
displayMenu.add(MenuEntrySwitch2<String>( displayMenu.add(MenuEntrySwitch2<String>(
dismissOnClicked: true,
text: translate('Privacy mode'), text: translate('Privacy mode'),
getter: () { getter: () {
return PrivacyModeState.find(widget.id); return PrivacyModeState.find(widget.id);
}, },
setter: (bool v) async { setter: (bool v) async {
Navigator.pop(context);
await bind.sessionToggleOption( await bind.sessionToggleOption(
id: widget.id, value: 'privacy-mode'); id: widget.id, value: 'privacy-mode');
})); }));
@ -620,46 +642,49 @@ void showSetOSPassword(
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
controller.text = password; controller.text = password;
dialogManager.show((setState, close) { dialogManager.show((setState, close) {
submit() {
var text = controller.text.trim();
bind.sessionPeerOption(id: id, name: "os-password", value: text);
bind.sessionPeerOption(
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
if (text != "" && login) {
bind.sessionInputOsPassword(id: id, value: text);
}
close();
}
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate('OS Password')), title: Text(translate('OS Password')),
content: Column(mainAxisSize: MainAxisSize.min, children: [ content: Column(mainAxisSize: MainAxisSize.min, children: [
PasswordWidget(controller: controller), PasswordWidget(controller: controller),
CheckboxListTile( CheckboxListTile(
contentPadding: const EdgeInsets.all(0), contentPadding: const EdgeInsets.all(0),
dense: true, dense: true,
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
title: Text( title: Text(
translate('Auto Login'), translate('Auto Login'),
),
value: autoLogin,
onChanged: (v) {
if (v == null) return;
setState(() => autoLogin = v);
},
), ),
]), value: autoLogin,
actions: [ onChanged: (v) {
TextButton( if (v == null) return;
style: flatButtonStyle, setState(() => autoLogin = v);
onPressed: () { },
close(); ),
}, ]),
child: Text(translate('Cancel')), actions: [
), TextButton(
TextButton( style: flatButtonStyle,
style: flatButtonStyle, onPressed: close,
onPressed: () { child: Text(translate('Cancel')),
var text = controller.text.trim(); ),
bind.sessionPeerOption(id: id, name: "os-password", value: text); TextButton(
bind.sessionPeerOption( style: flatButtonStyle,
id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); onPressed: submit,
if (text != "" && login) { child: Text(translate('OK')),
bind.sessionInputOsPassword(id: id, value: text); ),
} ],
close(); onSubmit: submit,
}, onCancel: close,
child: Text(translate('OK')), );
),
]);
}); });
} }

View File

@ -3,7 +3,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
@ -59,13 +59,15 @@ class DesktopTabState {
class DesktopTabController { class DesktopTabController {
final state = DesktopTabState().obs; final state = DesktopTabState().obs;
final DesktopTabType tabType;
/// index, key /// index, key
Function(int, String)? onRemove; Function(int, String)? onRemove;
Function(int)? onSelected; Function(int)? onSelected;
void add(TabInfo tab) { DesktopTabController({required this.tabType});
void add(TabInfo tab, {bool authorized = false}) {
if (!isDesktop) return; if (!isDesktop) return;
final index = state.value.tabs.indexWhere((e) => e.key == tab.key); final index = state.value.tabs.indexWhere((e) => e.key == tab.key);
int toIndex; int toIndex;
@ -79,6 +81,16 @@ class DesktopTabController {
toIndex = state.value.tabs.length - 1; toIndex = state.value.tabs.length - 1;
assert(toIndex >= 0); assert(toIndex >= 0);
} }
if (tabType == DesktopTabType.cm) {
Future.delayed(Duration.zero, () async {
window_on_top(null);
});
if (authorized) {
Future.delayed(const Duration(seconds: 3), () {
windowManager.minimize();
});
}
}
try { try {
jumpTo(toIndex); jumpTo(toIndex);
} catch (e) { } catch (e) {
@ -106,6 +118,7 @@ class DesktopTabController {
} }
void jumpTo(int index) { void jumpTo(int index) {
if (!isDesktop || index < 0) return;
state.update((val) { state.update((val) {
val!.selected = index; val!.selected = index;
Future.delayed(Duration.zero, (() { Future.delayed(Duration.zero, (() {
@ -114,12 +127,14 @@ class DesktopTabController {
} }
if (val.scrollController.hasClients && if (val.scrollController.hasClients &&
val.scrollController.canScroll && val.scrollController.canScroll &&
val.scrollController.itemCount >= index) { val.scrollController.itemCount > index) {
val.scrollController.scrollToItem(index, center: true, animate: true); val.scrollController.scrollToItem(index, center: true, animate: true);
} }
})); }));
}); });
onSelected?.call(index); if (state.value.tabs.length > index) {
onSelected?.call(index);
}
} }
void closeBy(String? key) { void closeBy(String? key) {
@ -143,8 +158,7 @@ class DesktopTabController {
class TabThemeConf { class TabThemeConf {
double iconSize; double iconSize;
TarBarTheme theme; TabThemeConf({required this.iconSize});
TabThemeConf({required this.iconSize, required this.theme});
} }
typedef TabBuilder = Widget Function( typedef TabBuilder = Widget Function(
@ -153,9 +167,6 @@ typedef LabelGetter = Rx<String> Function(String key);
class DesktopTab extends StatelessWidget { class DesktopTab extends StatelessWidget {
final Function(String)? onTabClose; final Function(String)? onTabClose;
final TarBarTheme theme;
final DesktopTabType tabType;
final bool isMainWindow;
final bool showTabBar; final bool showTabBar;
final bool showLogo; final bool showLogo;
final bool showTitle; final bool showTitle;
@ -170,11 +181,12 @@ class DesktopTab extends StatelessWidget {
final DesktopTabController controller; final DesktopTabController controller;
Rx<DesktopTabState> get state => controller.state; Rx<DesktopTabState> get state => controller.state;
late final DesktopTabType tabType;
late final bool isMainWindow;
const DesktopTab({ DesktopTab({
Key? key,
required this.controller, required this.controller,
required this.tabType,
this.theme = const TarBarTheme.light(),
this.onTabClose, this.onTabClose,
this.showTabBar = true, this.showTabBar = true,
this.showLogo = true, this.showLogo = true,
@ -187,23 +199,26 @@ class DesktopTab extends StatelessWidget {
this.onClose, this.onClose,
this.tabBuilder, this.tabBuilder,
this.labelGetter, this.labelGetter,
}) : isMainWindow = }) : super(key: key) {
tabType == DesktopTabType.main || tabType == DesktopTabType.cm; tabType = controller.tabType;
isMainWindow =
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column(children: [ return Column(children: [
Offstage( Offstage(
offstage: !showTabBar, offstage: !showTabBar,
child: Container( child: SizedBox(
height: _kTabBarHeight, height: _kTabBarHeight,
child: Column( child: Column(
children: [ children: [
Container( SizedBox(
height: _kTabBarHeight - 1, height: _kTabBarHeight - 1,
child: _buildBar(), child: _buildBar(),
), ),
Divider( const Divider(
height: 1, height: 1,
thickness: 1, thickness: 1,
), ),
@ -282,7 +297,7 @@ class DesktopTab extends StatelessWidget {
)), )),
Offstage( Offstage(
offstage: !showTitle, offstage: !showTitle,
child: Text( child: const Text(
"RustDesk", "RustDesk",
style: TextStyle(fontSize: 13), style: TextStyle(fontSize: 13),
).marginOnly(left: 2)) ).marginOnly(left: 2))
@ -303,7 +318,6 @@ class DesktopTab extends StatelessWidget {
child: _ListView( child: _ListView(
controller: controller, controller: controller,
onTabClose: onTabClose, onTabClose: onTabClose,
theme: theme,
tabBuilder: tabBuilder, tabBuilder: tabBuilder,
labelGetter: labelGetter, labelGetter: labelGetter,
)), )),
@ -314,7 +328,8 @@ class DesktopTab extends StatelessWidget {
Offstage(offstage: tail == null, child: tail), Offstage(offstage: tail == null, child: tail),
WindowActionPanel( WindowActionPanel(
mainTab: isMainWindow, mainTab: isMainWindow,
theme: theme, tabType: tabType,
state: state,
showMinimize: showMinimize, showMinimize: showMinimize,
showMaximize: showMaximize, showMaximize: showMaximize,
showClose: showClose, showClose: showClose,
@ -327,7 +342,8 @@ class DesktopTab extends StatelessWidget {
class WindowActionPanel extends StatelessWidget { class WindowActionPanel extends StatelessWidget {
final bool mainTab; final bool mainTab;
final TarBarTheme theme; final DesktopTabType tabType;
final Rx<DesktopTabState> state;
final bool showMinimize; final bool showMinimize;
final bool showMaximize; final bool showMaximize;
@ -337,7 +353,8 @@ class WindowActionPanel extends StatelessWidget {
const WindowActionPanel( const WindowActionPanel(
{Key? key, {Key? key,
required this.mainTab, required this.mainTab,
required this.theme, required this.tabType,
required this.state,
this.showMinimize = true, this.showMinimize = true,
this.showMaximize = true, this.showMaximize = true,
this.showClose = true, this.showClose = true,
@ -353,7 +370,6 @@ class WindowActionPanel extends StatelessWidget {
child: ActionIcon( child: ActionIcon(
message: 'Minimize', message: 'Minimize',
icon: IconFont.min, icon: IconFont.min,
theme: theme,
onTap: () { onTap: () {
if (mainTab) { if (mainTab) {
windowManager.minimize(); windowManager.minimize();
@ -361,31 +377,30 @@ class WindowActionPanel extends StatelessWidget {
WindowController.fromWindowId(windowId!).minimize(); WindowController.fromWindowId(windowId!).minimize();
} }
}, },
is_close: false, isClose: false,
)), )),
// TODO: drag makes window restore // TODO: drag makes window restore
Offstage( Offstage(
offstage: !showMaximize, offstage: !showMaximize,
child: FutureBuilder(builder: (context, snapshot) { child: FutureBuilder(builder: (context, snapshot) {
RxBool is_maximized = false.obs; RxBool isMaximized = false.obs;
if (mainTab) { if (mainTab) {
windowManager.isMaximized().then((maximized) { windowManager.isMaximized().then((maximized) {
is_maximized.value = maximized; isMaximized.value = maximized;
}); });
} else { } else {
final wc = WindowController.fromWindowId(windowId!); final wc = WindowController.fromWindowId(windowId!);
wc.isMaximized().then((maximized) { wc.isMaximized().then((maximized) {
is_maximized.value = maximized; isMaximized.value = maximized;
}); });
} }
return Obx( return Obx(
() => ActionIcon( () => ActionIcon(
message: is_maximized.value ? "Restore" : "Maximize", message: isMaximized.value ? "Restore" : "Maximize",
icon: is_maximized.value ? IconFont.restore : IconFont.max, icon: isMaximized.value ? IconFont.restore : IconFont.max,
theme: theme,
onTap: () { onTap: () {
if (mainTab) { if (mainTab) {
if (is_maximized.value) { if (isMaximized.value) {
windowManager.unmaximize(); windowManager.unmaximize();
} else { } else {
windowManager.maximize(); windowManager.maximize();
@ -393,15 +408,15 @@ class WindowActionPanel extends StatelessWidget {
} else { } else {
// TODO: subwindow is maximized but first query result is not maximized. // TODO: subwindow is maximized but first query result is not maximized.
final wc = WindowController.fromWindowId(windowId!); final wc = WindowController.fromWindowId(windowId!);
if (is_maximized.value) { if (isMaximized.value) {
wc.unmaximize(); wc.unmaximize();
} else { } else {
wc.maximize(); wc.maximize();
} }
} }
is_maximized.value = !is_maximized.value; isMaximized.value = !isMaximized.value;
}, },
is_close: false, isClose: false,
), ),
); );
})), })),
@ -410,40 +425,70 @@ class WindowActionPanel extends StatelessWidget {
child: ActionIcon( child: ActionIcon(
message: 'Close', message: 'Close',
icon: IconFont.close, icon: IconFont.close,
theme: theme, onTap: () async {
onTap: () { action() {
if (mainTab) { if (mainTab) {
windowManager.close(); windowManager.close();
} else { } else {
// only hide for multi window, not close // only hide for multi window, not close
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
WindowController.fromWindowId(windowId!).hide(); 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 // ignore: must_be_immutable
class _ListView extends StatelessWidget { class _ListView extends StatelessWidget {
final DesktopTabController controller; final DesktopTabController controller;
final Function(String key)? onTabClose; final Function(String key)? onTabClose;
final TarBarTheme theme;
final TabBuilder? tabBuilder; final TabBuilder? tabBuilder;
final LabelGetter? labelGetter; final LabelGetter? labelGetter;
Rx<DesktopTabState> get state => controller.state; Rx<DesktopTabState> get state => controller.state;
_ListView( const _ListView(
{required this.controller, {required this.controller,
required this.onTabClose, required this.onTabClose,
required this.theme,
this.tabBuilder, this.tabBuilder,
this.labelGetter}); this.labelGetter});
@ -453,7 +498,7 @@ class _ListView extends StatelessWidget {
controller: state.value.scrollController, controller: state.value.scrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
shrinkWrap: true, shrinkWrap: true,
physics: BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
children: state.value.tabs.asMap().entries.map((e) { children: state.value.tabs.asMap().entries.map((e) {
final index = e.key; final index = e.key;
final tab = e.value; final tab = e.value;
@ -468,7 +513,6 @@ class _ListView extends StatelessWidget {
selected: state.value.selected, selected: state.value.selected,
onClose: () => controller.remove(index), onClose: () => controller.remove(index),
onSelected: () => controller.jumpTo(index), onSelected: () => controller.jumpTo(index),
theme: theme,
tabBuilder: tabBuilder == null tabBuilder: tabBuilder == null
? null ? null
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) { : (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
@ -485,31 +529,29 @@ class _ListView extends StatelessWidget {
} }
class _Tab extends StatefulWidget { class _Tab extends StatefulWidget {
late final int index; final int index;
late final Rx<String> label; final Rx<String> label;
late final IconData? selectedIcon; final IconData? selectedIcon;
late final IconData? unselectedIcon; final IconData? unselectedIcon;
late final bool closable; final bool closable;
late final int selected; final int selected;
late final Function() onClose; final Function() onClose;
late final Function() onSelected; final Function() onSelected;
late final TarBarTheme theme;
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)? final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
tabBuilder; tabBuilder;
_Tab( const _Tab({
{Key? key, Key? key,
required this.index, required this.index,
required this.label, required this.label,
this.selectedIcon, this.selectedIcon,
this.unselectedIcon, this.unselectedIcon,
this.tabBuilder, this.tabBuilder,
required this.closable, required this.closable,
required this.selected, required this.selected,
required this.onClose, required this.onClose,
required this.onSelected, required this.onSelected,
required this.theme}) }) : super(key: key);
: super(key: key);
@override @override
State<_Tab> createState() => _TabState(); State<_Tab> createState() => _TabState();
@ -529,8 +571,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
isSelected ? widget.selectedIcon : widget.unselectedIcon, isSelected ? widget.selectedIcon : widget.unselectedIcon,
size: _kIconSize, size: _kIconSize,
color: isSelected color: isSelected
? widget.theme.selectedtabIconColor ? MyTheme.tabbar(context).selectedTabIconColor
: widget.theme.unSelectedtabIconColor, : MyTheme.tabbar(context).unSelectedTabIconColor,
).paddingOnly(right: 5)); ).paddingOnly(right: 5));
final labelWidget = Obx(() { final labelWidget = Obx(() {
return Text( return Text(
@ -538,8 +580,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: isSelected color: isSelected
? widget.theme.selectedTextColor ? MyTheme.tabbar(context).selectedTextColor
: widget.theme.unSelectedTextColor), : MyTheme.tabbar(context).unSelectedTextColor),
); );
}); });
@ -552,8 +594,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
], ],
); );
} else { } else {
return widget.tabBuilder!(icon, labelWidget, return widget.tabBuilder!(
TabThemeConf(iconSize: _kIconSize, theme: widget.theme)); icon, labelWidget, TabThemeConf(iconSize: _kIconSize));
} }
} }
@ -582,7 +624,6 @@ class _TabState extends State<_Tab> with RestorationMixin {
visiable: hover.value && widget.closable, visiable: hover.value && widget.closable,
tabSelected: isSelected, tabSelected: isSelected,
onClose: () => widget.onClose(), onClose: () => widget.onClose(),
theme: widget.theme,
))) )))
])).paddingSymmetric(horizontal: 10), ])).paddingSymmetric(horizontal: 10),
Offstage( Offstage(
@ -591,7 +632,7 @@ class _TabState extends State<_Tab> with RestorationMixin {
width: 1, width: 1,
indent: _kDividerIndent, indent: _kDividerIndent,
endIndent: _kDividerIndent, endIndent: _kDividerIndent,
color: widget.theme.dividerColor, color: MyTheme.tabbar(context).dividerColor,
thickness: 1, thickness: 1,
), ),
) )
@ -614,14 +655,12 @@ class _CloseButton extends StatelessWidget {
final bool visiable; final bool visiable;
final bool tabSelected; final bool tabSelected;
final Function onClose; final Function onClose;
late final TarBarTheme theme;
_CloseButton({ const _CloseButton({
Key? key, Key? key,
required this.visiable, required this.visiable,
required this.tabSelected, required this.tabSelected,
required this.onClose, required this.onClose,
required this.theme,
}) : super(key: key); }) : super(key: key);
@override @override
@ -637,8 +676,8 @@ class _CloseButton extends StatelessWidget {
Icons.close, Icons.close,
size: _kIconSize, size: _kIconSize,
color: tabSelected color: tabSelected
? theme.selectedIconColor ? MyTheme.tabbar(context).selectedIconColor
: theme.unSelectedIconColor, : MyTheme.tabbar(context).unSelectedIconColor,
), ),
), ),
)).paddingOnly(left: 5); )).paddingOnly(left: 5);
@ -648,16 +687,14 @@ class _CloseButton extends StatelessWidget {
class ActionIcon extends StatelessWidget { class ActionIcon extends StatelessWidget {
final String message; final String message;
final IconData icon; final IconData icon;
final TarBarTheme theme;
final Function() onTap; final Function() onTap;
final bool is_close; final bool isClose;
const ActionIcon({ const ActionIcon({
Key? key, Key? key,
required this.message, required this.message,
required this.icon, required this.icon,
required this.theme,
required this.onTap, required this.onTap,
required this.is_close, required this.isClose,
}) : super(key: key); }) : super(key: key);
@override @override
@ -665,34 +702,32 @@ class ActionIcon extends StatelessWidget {
RxBool hover = false.obs; RxBool hover = false.obs;
return Obx(() => Tooltip( return Obx(() => Tooltip(
message: translate(message), message: translate(message),
waitDuration: Duration(seconds: 1), waitDuration: const Duration(seconds: 1),
child: InkWell( child: InkWell(
hoverColor: hoverColor: isClose
is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor, ? const Color.fromARGB(255, 196, 43, 28)
: MyTheme.tabbar(context).hoverColor,
onHover: (value) => hover.value = value, onHover: (value) => hover.value = value,
child: Container( onTap: onTap,
child: SizedBox(
height: _kTabBarHeight - 1, height: _kTabBarHeight - 1,
width: _kTabBarHeight - 1, width: _kTabBarHeight - 1,
child: Icon( child: Icon(
icon, icon,
color: hover.value && is_close color: hover.value && isClose
? Colors.white ? Colors.white
: theme.unSelectedIconColor, : MyTheme.tabbar(context).unSelectedIconColor,
size: _kActionIconSize, size: _kActionIconSize,
), ),
), ),
onTap: onTap,
), ),
)); ));
} }
} }
class AddButton extends StatelessWidget { class AddButton extends StatelessWidget {
late final TarBarTheme theme; const AddButton({
AddButton({
Key? key, Key? key,
required this.theme,
}) : super(key: key); }) : super(key: key);
@override @override
@ -700,41 +735,101 @@ class AddButton extends StatelessWidget {
return ActionIcon( return ActionIcon(
message: 'New Connection', message: 'New Connection',
icon: IconFont.add, icon: IconFont.add,
theme: theme,
onTap: () => onTap: () =>
rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""),
is_close: false); isClose: false);
} }
} }
class TarBarTheme { class TabbarTheme extends ThemeExtension<TabbarTheme> {
final Color unSelectedtabIconColor; final Color? selectedTabIconColor;
final Color selectedtabIconColor; final Color? unSelectedTabIconColor;
final Color selectedTextColor; final Color? selectedTextColor;
final Color unSelectedTextColor; final Color? unSelectedTextColor;
final Color selectedIconColor; final Color? selectedIconColor;
final Color unSelectedIconColor; final Color? unSelectedIconColor;
final Color dividerColor; final Color? dividerColor;
final Color hoverColor; final Color? hoverColor;
const TarBarTheme.light() const TabbarTheme(
: unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241), {required this.selectedTabIconColor,
selectedtabIconColor = MyTheme.accent, required this.unSelectedTabIconColor,
selectedTextColor = const Color.fromARGB(255, 26, 26, 26), required this.selectedTextColor,
unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96), required this.unSelectedTextColor,
selectedIconColor = const Color.fromARGB(255, 26, 26, 26), required this.selectedIconColor,
unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96), required this.unSelectedIconColor,
dividerColor = const Color.fromARGB(255, 238, 238, 238), required this.dividerColor,
hoverColor = const Color.fromARGB( required this.hoverColor});
51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E
const TarBarTheme.dark() static const light = TabbarTheme(
: unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98), selectedTabIconColor: MyTheme.accent,
selectedtabIconColor = MyTheme.accent, unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241),
selectedTextColor = const Color.fromARGB(255, 255, 255, 255), selectedTextColor: Color.fromARGB(255, 26, 26, 26),
unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207), unSelectedTextColor: Color.fromARGB(255, 96, 96, 96),
selectedIconColor = const Color.fromARGB(255, 215, 215, 215), selectedIconColor: Color.fromARGB(255, 26, 26, 26),
unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255), unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
dividerColor = const Color.fromARGB(255, 64, 64, 64), dividerColor: Color.fromARGB(255, 238, 238, 238),
hoverColor = Colors.black26; hoverColor: Color.fromARGB(51, 158, 158, 158));
static const dark = TabbarTheme(
selectedTabIconColor: MyTheme.accent,
unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98),
selectedTextColor: Color.fromARGB(255, 255, 255, 255),
unSelectedTextColor: Color.fromARGB(255, 207, 207, 207),
selectedIconColor: Color.fromARGB(255, 215, 215, 215),
unSelectedIconColor: Color.fromARGB(255, 255, 255, 255),
dividerColor: Color.fromARGB(255, 64, 64, 64),
hoverColor: Colors.black26);
@override
ThemeExtension<TabbarTheme> copyWith({
Color? selectedTabIconColor,
Color? unSelectedTabIconColor,
Color? selectedTextColor,
Color? unSelectedTextColor,
Color? selectedIconColor,
Color? unSelectedIconColor,
Color? dividerColor,
Color? hoverColor,
}) {
return TabbarTheme(
selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor,
unSelectedTabIconColor:
unSelectedTabIconColor ?? this.unSelectedTabIconColor,
selectedTextColor: selectedTextColor ?? this.selectedTextColor,
unSelectedTextColor: unSelectedTextColor ?? this.unSelectedTextColor,
selectedIconColor: selectedIconColor ?? this.selectedIconColor,
unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor,
dividerColor: dividerColor ?? this.dividerColor,
hoverColor: hoverColor ?? this.hoverColor,
);
}
@override
ThemeExtension<TabbarTheme> lerp(
ThemeExtension<TabbarTheme>? other, double t) {
if (other is! TabbarTheme) {
return this;
}
return TabbarTheme(
selectedTabIconColor:
Color.lerp(selectedTabIconColor, other.selectedTabIconColor, t),
unSelectedTabIconColor:
Color.lerp(unSelectedTabIconColor, other.unSelectedTabIconColor, t),
selectedTextColor:
Color.lerp(selectedTextColor, other.selectedTextColor, t),
unSelectedTextColor:
Color.lerp(unSelectedTextColor, other.unSelectedTextColor, t),
selectedIconColor:
Color.lerp(selectedIconColor, other.selectedIconColor, t),
unSelectedIconColor:
Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t),
dividerColor: Color.lerp(dividerColor, other.dividerColor, t),
hoverColor: Color.lerp(hoverColor, other.hoverColor, t),
);
}
static color(BuildContext context) {
return Theme.of(context).extension<ColorThemeExtension>()!;
}
} }

View File

@ -165,6 +165,7 @@ void runConnectionManagerScreen() async {
await windowManager.setAlignment(Alignment.topRight); await windowManager.setAlignment(Alignment.topRight);
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
await windowManager.setAlignment(Alignment.topRight); // ensure
}) })
]); ]);
runApp(GetMaterialApp( runApp(GetMaterialApp(

View File

@ -45,7 +45,7 @@ class ChatPage extends StatelessWidget implements PageShape {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: chatModel, value: chatModel,
child: Container( child: Container(
color: MyTheme.grayBg, color: MyTheme.color(context).grayBg,
child: Consumer<ChatModel>(builder: (context, chatModel, child) { child: Consumer<ChatModel>(builder: (context, chatModel, child) {
final currentUser = chatModel.currentUser; final currentUser = chatModel.currentUser;
return Stack( return Stack(
@ -59,6 +59,14 @@ class ChatPage extends StatelessWidget implements PageShape {
messages: chatModel messages: chatModel
.messages[chatModel.currentID]?.chatMessages ?? .messages[chatModel.currentID]?.chatMessages ??
[], [],
inputOptions: InputOptions(
sendOnEnter: true,
inputDecoration: defaultInputDecoration(
fillColor: MyTheme.color(context).bg),
sendButtonBuilder: defaultSendButton(
color: MyTheme.color(context).text!),
inputTextStyle:
TextStyle(color: MyTheme.color(context).text)),
messageOptions: MessageOptions( messageOptions: MessageOptions(
showOtherUsersAvatar: false, showOtherUsersAvatar: false,
showTime: true, showTime: true,

View File

@ -209,10 +209,19 @@ class ChatModel with ChangeNotifier {
id: await bind.mainGetLastRemoteId(), id: await bind.mainGetLastRemoteId(),
); );
} else { } else {
final client = _ffi.target?.serverModel.clients[id]; final client = _ffi.target?.serverModel.clients
.firstWhere((client) => client.id == id);
if (client == null) { if (client == null) {
return debugPrint("Failed to receive msg,user doesn't exist"); return debugPrint("Failed to receive msg,user doesn't exist");
} }
if (isDesktop) {
window_on_top(null);
var index = _ffi.target?.serverModel.clients
.indexWhere((client) => client.id == id);
if (index != null && index >= 0) {
gFFI.serverModel.tabController.jumpTo(index);
}
}
chatUser = ChatUser(id: client.peerId, firstName: client.name); chatUser = ChatUser(id: client.peerId, firstName: client.name);
} }

View File

@ -360,9 +360,9 @@ class FileModel extends ChangeNotifier {
Future refresh({bool? isLocal}) async { Future refresh({bool? isLocal}) async {
if (isDesktop) { if (isDesktop) {
isLocal = isLocal ?? _isLocal; isLocal = isLocal ?? _isLocal;
await isLocal isLocal
? openDirectory(currentLocalDir.path, isLocal: isLocal) ? await openDirectory(currentLocalDir.path, isLocal: isLocal)
: openDirectory(currentRemoteDir.path, isLocal: isLocal); : await openDirectory(currentRemoteDir.path, isLocal: isLocal);
} else { } else {
await openDirectory(currentDir.path); await openDirectory(currentDir.path);
} }
@ -393,7 +393,7 @@ class FileModel extends ChangeNotifier {
} }
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
debugPrint("Failed to openDirectory ${path} :$e"); debugPrint("Failed to openDirectory $path: $e");
} }
} }
@ -559,49 +559,55 @@ class FileModel extends ChangeNotifier {
Future<bool?> showRemoveDialog( Future<bool?> showRemoveDialog(
String title, String content, bool showCheckbox) async { String title, String content, bool showCheckbox) async {
return await parent.target?.dialogManager.show<bool>( return await parent.target?.dialogManager.show<bool>(
(setState, Function(bool v) close) => CustomAlertDialog( (setState, Function(bool v) close) {
title: Row( cancel() => close(false);
children: [ submit() => close(true);
Icon(Icons.warning, color: Colors.red), return CustomAlertDialog(
SizedBox(width: 20), title: Row(
Text(title) children: [
], const Icon(Icons.warning, color: Colors.red),
), const SizedBox(width: 20),
content: Column( Text(title)
crossAxisAlignment: CrossAxisAlignment.start, ],
mainAxisSize: MainAxisSize.min, ),
children: [ content: Column(
Text(content), crossAxisAlignment: CrossAxisAlignment.start,
SizedBox(height: 5), mainAxisSize: MainAxisSize.min,
Text(translate("This is irreversible!"), children: [
style: TextStyle(fontWeight: FontWeight.bold)), Text(content),
showCheckbox const SizedBox(height: 5),
? CheckboxListTile( Text(translate("This is irreversible!"),
contentPadding: const EdgeInsets.all(0), style: const TextStyle(fontWeight: FontWeight.bold)),
dense: true, showCheckbox
controlAffinity: ListTileControlAffinity.leading, ? CheckboxListTile(
title: Text( contentPadding: const EdgeInsets.all(0),
translate("Do this for all conflicts"), dense: true,
), controlAffinity: ListTileControlAffinity.leading,
value: removeCheckboxRemember, title: Text(
onChanged: (v) { translate("Do this for all conflicts"),
if (v == null) return; ),
setState(() => removeCheckboxRemember = v); value: removeCheckboxRemember,
}, onChanged: (v) {
) if (v == null) return;
: SizedBox.shrink() setState(() => removeCheckboxRemember = v);
]), },
actions: [ )
TextButton( : const SizedBox.shrink()
style: flatButtonStyle, ]),
onPressed: () => close(false), actions: [
child: Text(translate("Cancel"))), TextButton(
TextButton( style: flatButtonStyle,
style: flatButtonStyle, onPressed: cancel,
onPressed: () => close(true), child: Text(translate("Cancel"))),
child: Text(translate("OK"))), TextButton(
]), style: flatButtonStyle,
useAnimation: false); onPressed: submit,
child: Text(translate("OK"))),
],
onSubmit: submit,
onCancel: cancel,
);
}, useAnimation: false);
} }
bool fileConfirmCheckboxRemember = false; bool fileConfirmCheckboxRemember = false;
@ -610,55 +616,59 @@ class FileModel extends ChangeNotifier {
String title, String content, bool showCheckbox) async { String title, String content, bool showCheckbox) async {
fileConfirmCheckboxRemember = false; fileConfirmCheckboxRemember = false;
return await parent.target?.dialogManager.show<bool?>( return await parent.target?.dialogManager.show<bool?>(
(setState, Function(bool? v) close) => CustomAlertDialog( (setState, Function(bool? v) close) {
title: Row( cancel() => close(false);
children: [ submit() => close(true);
Icon(Icons.warning, color: Colors.red), return CustomAlertDialog(
SizedBox(width: 20), title: Row(
Text(title) children: [
], const Icon(Icons.warning, color: Colors.red),
), const SizedBox(width: 20),
content: Column( Text(title)
crossAxisAlignment: CrossAxisAlignment.start, ],
mainAxisSize: MainAxisSize.min, ),
children: [ content: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
translate( mainAxisSize: MainAxisSize.min,
"This file exists, skip or overwrite this file?"), children: [
style: TextStyle(fontWeight: FontWeight.bold)), Text(translate("This file exists, skip or overwrite this file?"),
SizedBox(height: 5), style: const TextStyle(fontWeight: FontWeight.bold)),
Text(content), const SizedBox(height: 5),
showCheckbox Text(content),
? CheckboxListTile( showCheckbox
contentPadding: const EdgeInsets.all(0), ? CheckboxListTile(
dense: true, contentPadding: const EdgeInsets.all(0),
controlAffinity: ListTileControlAffinity.leading, dense: true,
title: Text( controlAffinity: ListTileControlAffinity.leading,
translate("Do this for all conflicts"), title: Text(
), translate("Do this for all conflicts"),
value: fileConfirmCheckboxRemember, ),
onChanged: (v) { value: fileConfirmCheckboxRemember,
if (v == null) return; onChanged: (v) {
setState(() => fileConfirmCheckboxRemember = v); if (v == null) return;
}, setState(() => fileConfirmCheckboxRemember = v);
) },
: SizedBox.shrink() )
]), : const SizedBox.shrink()
actions: [ ]),
TextButton( actions: [
style: flatButtonStyle, TextButton(
onPressed: () => close(false), style: flatButtonStyle,
child: Text(translate("Cancel"))), onPressed: cancel,
TextButton( child: Text(translate("Cancel"))),
style: flatButtonStyle, TextButton(
onPressed: () => close(null), style: flatButtonStyle,
child: Text(translate("Skip"))), onPressed: () => close(null),
TextButton( child: Text(translate("Skip"))),
style: flatButtonStyle, TextButton(
onPressed: () => close(true), style: flatButtonStyle,
child: Text(translate("OK"))), onPressed: submit,
]), child: Text(translate("OK"))),
useAnimation: false); ],
onSubmit: submit,
onCancel: cancel,
);
}, useAnimation: false);
} }
sendRemoveFile(String path, int fileNum, bool isLocal) { sendRemoveFile(String path, int fileNum, bool isLocal) {

View File

@ -32,7 +32,7 @@ class FfiModel with ChangeNotifier {
Display _display = Display(); Display _display = Display();
var _inputBlocked = false; var _inputBlocked = false;
final _permissions = Map<String, bool>(); final _permissions = <String, bool>{};
bool? _secure; bool? _secure;
bool? _direct; bool? _direct;
bool _touchMode = false; bool _touchMode = false;
@ -71,12 +71,13 @@ class FfiModel with ChangeNotifier {
} }
} }
void updatePermission(Map<String, dynamic> evt) { void updatePermission(Map<String, dynamic> evt, String id) {
evt.forEach((k, v) { evt.forEach((k, v) {
if (k == 'name' || k.isEmpty) return; if (k == 'name' || k.isEmpty) return;
_permissions[k] = v == 'true'; _permissions[k] = v == 'true';
}); });
print('$_permissions'); KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
debugPrint('$_permissions');
notifyListeners(); notifyListeners();
} }
@ -146,7 +147,7 @@ class FfiModel with ChangeNotifier {
} else if (name == 'clipboard') { } else if (name == 'clipboard') {
Clipboard.setData(ClipboardData(text: evt['content'])); Clipboard.setData(ClipboardData(text: evt['content']));
} else if (name == 'permission') { } else if (name == 'permission') {
parent.target?.ffiModel.updatePermission(evt); parent.target?.ffiModel.updatePermission(evt, peerId);
} else if (name == 'chat_client_mode') { } else if (name == 'chat_client_mode') {
parent.target?.chatModel parent.target?.chatModel
.receive(ChatModel.clientModeID, evt['text'] ?? ""); .receive(ChatModel.clientModeID, evt['text'] ?? "");
@ -167,10 +168,8 @@ class FfiModel with ChangeNotifier {
parent.target?.fileModel.loadLastJob(evt); parent.target?.fileModel.loadLastJob(evt);
} else if (name == 'update_folder_files') { } else if (name == 'update_folder_files') {
parent.target?.fileModel.updateFolderFiles(evt); parent.target?.fileModel.updateFolderFiles(evt);
} else if (name == 'try_start_without_auth') { } else if (name == 'add_connection') {
parent.target?.serverModel.loginRequest(evt); parent.target?.serverModel.addConnection(evt);
} else if (name == 'on_client_authorized') {
parent.target?.serverModel.onClientAuthorized(evt);
} else if (name == 'on_client_remove') { } else if (name == 'on_client_remove') {
parent.target?.serverModel.onClientRemove(evt); parent.target?.serverModel.onClientRemove(evt);
} else if (name == 'update_quality_status') { } else if (name == 'update_quality_status') {
@ -185,7 +184,7 @@ class FfiModel with ChangeNotifier {
/// Bind the event listener to receive events from the Rust core. /// Bind the event listener to receive events from the Rust core.
void updateEventListener(String peerId) { void updateEventListener(String peerId) {
final void Function(Map<String, dynamic>) cb = (evt) { cb(evt) {
var name = evt['name']; var name = evt['name'];
if (name == 'msgbox') { if (name == 'msgbox') {
handleMsgBox(evt, peerId); handleMsgBox(evt, peerId);
@ -205,7 +204,7 @@ class FfiModel with ChangeNotifier {
} else if (name == 'clipboard') { } else if (name == 'clipboard') {
Clipboard.setData(ClipboardData(text: evt['content'])); Clipboard.setData(ClipboardData(text: evt['content']));
} else if (name == 'permission') { } else if (name == 'permission') {
parent.target?.ffiModel.updatePermission(evt); parent.target?.ffiModel.updatePermission(evt, peerId);
} else if (name == 'chat_client_mode') { } else if (name == 'chat_client_mode') {
parent.target?.chatModel parent.target?.chatModel
.receive(ChatModel.clientModeID, evt['text'] ?? ""); .receive(ChatModel.clientModeID, evt['text'] ?? "");
@ -226,10 +225,8 @@ class FfiModel with ChangeNotifier {
parent.target?.fileModel.loadLastJob(evt); parent.target?.fileModel.loadLastJob(evt);
} else if (name == 'update_folder_files') { } else if (name == 'update_folder_files') {
parent.target?.fileModel.updateFolderFiles(evt); parent.target?.fileModel.updateFolderFiles(evt);
} else if (name == 'try_start_without_auth') { } else if (name == 'add_connection') {
parent.target?.serverModel.loginRequest(evt); parent.target?.serverModel.addConnection(evt);
} else if (name == 'on_client_authorized') {
parent.target?.serverModel.onClientAuthorized(evt);
} else if (name == 'on_client_remove') { } else if (name == 'on_client_remove') {
parent.target?.serverModel.onClientRemove(evt); parent.target?.serverModel.onClientRemove(evt);
} else if (name == 'update_quality_status') { } else if (name == 'update_quality_status') {
@ -239,7 +236,8 @@ class FfiModel with ChangeNotifier {
} else if (name == 'update_privacy_mode') { } else if (name == 'update_privacy_mode') {
updatePrivacyMode(evt, peerId); updatePrivacyMode(evt, peerId);
} }
}; }
platformFFI.setEventCallback(cb); platformFFI.setEventCallback(cb);
} }
@ -321,15 +319,15 @@ class FfiModel with ChangeNotifier {
if (isPeerAndroid) { if (isPeerAndroid) {
_touchMode = true; _touchMode = true;
if (parent.target?.ffiModel.permissions['keyboard'] != false) { if (parent.target?.ffiModel.permissions['keyboard'] != false) {
Timer(Duration(milliseconds: 100), showMobileActionsOverlay); Timer(const Duration(milliseconds: 100), showMobileActionsOverlay);
} }
} else { } else {
_touchMode = _touchMode =
await bind.sessionGetOption(id: peerId, arg: "touch-mode") != ''; await bind.sessionGetOption(id: peerId, arg: "touch-mode") != '';
} }
if (evt['is_file_transfer'] == "true") { if (parent.target != null &&
// TODO is file transfer parent.target!.connType == ConnType.fileTransfer) {
parent.target?.fileModel.onReady(); parent.target?.fileModel.onReady();
} else { } else {
_pi.displays = []; _pi.displays = [];
@ -464,15 +462,20 @@ enum ScrollStyle {
} }
class CanvasModel with ChangeNotifier { class CanvasModel with ChangeNotifier {
// image offset of canvas
double _x = 0;
// image offset of canvas
double _y = 0;
// image scale
double _scale = 1.0;
// the tabbar over the image
double tabBarHeight = 0.0;
// TODO multi canvas model
String id = "";
// scroll offset x percent // scroll offset x percent
double _scrollX = 0.0; double _scrollX = 0.0;
// scroll offset y percent // scroll offset y percent
double _scrollY = 0.0; double _scrollY = 0.0;
double _x = 0;
double _y = 0;
double _scale = 1.0;
double _tabBarHeight = 0.0;
String id = ""; // TODO multi canvas model
ScrollStyle _scrollStyle = ScrollStyle.scrollauto; ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
WeakReference<FFI> parent; WeakReference<FFI> parent;
@ -492,9 +495,6 @@ class CanvasModel with ChangeNotifier {
double get scrollX => _scrollX; double get scrollX => _scrollX;
double get scrollY => _scrollY; double get scrollY => _scrollY;
set tabBarHeight(double h) => _tabBarHeight = h;
double get tabBarHeight => _tabBarHeight;
void updateViewStyle() async { void updateViewStyle() async {
final style = await bind.sessionGetOption(id: id, arg: 'view-style'); final style = await bind.sessionGetOption(id: id, arg: 'view-style');
if (style == null) { if (style == null) {
@ -548,12 +548,11 @@ class CanvasModel with ChangeNotifier {
Size get size { Size get size {
final size = MediaQueryData.fromWindow(ui.window).size; final size = MediaQueryData.fromWindow(ui.window).size;
return Size(size.width, size.height - _tabBarHeight); return Size(size.width, size.height - tabBarHeight);
} }
void moveDesktopMouse(double x, double y) { void moveDesktopMouse(double x, double y) {
// On mobile platforms, move the canvas with the cursor. // On mobile platforms, move the canvas with the cursor.
//if (!isDesktop) {
final dw = getDisplayWidth() * _scale; final dw = getDisplayWidth() * _scale;
final dh = getDisplayHeight() * _scale; final dh = getDisplayHeight() * _scale;
var dxOffset = 0; var dxOffset = 0;
@ -579,8 +578,13 @@ class CanvasModel with ChangeNotifier {
if (dxOffset != 0 || dyOffset != 0) { if (dxOffset != 0 || dyOffset != 0) {
notifyListeners(); notifyListeners();
} }
//}
parent.target?.cursorModel.moveLocal(x, y); // If keyboard is not permitted, do not move cursor when mouse is moving.
if (parent.target != null) {
if (parent.target!.ffiModel.keyboard()) {
parent.target!.cursorModel.moveLocal(x, y);
}
}
} }
set scale(v) { set scale(v) {
@ -597,11 +601,8 @@ class CanvasModel with ChangeNotifier {
if (isWebDesktop) { if (isWebDesktop) {
updateViewStyle(); updateViewStyle();
} else { } else {
final size = MediaQueryData.fromWindow(ui.window).size; _x = (size.width - getDisplayWidth() * _scale) / 2;
final canvasWidth = size.width; _y = (size.height - getDisplayHeight() * _scale) / 2;
final canvasHeight = size.height - _tabBarHeight;
_x = (canvasWidth - getDisplayWidth() * _scale) / 2;
_y = (canvasHeight - getDisplayHeight() * _scale) / 2;
} }
notifyListeners(); notifyListeners();
} }
@ -613,7 +614,7 @@ class CanvasModel with ChangeNotifier {
void updateScale(double v) { void updateScale(double v) {
if (parent.target?.imageModel.image == null) return; if (parent.target?.imageModel.image == null) return;
final offset = parent.target?.cursorModel.offset ?? Offset(0, 0); final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0);
var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero; var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
final px0 = (offset.dx - r.left) * _scale; final px0 = (offset.dx - r.left) * _scale;
final py0 = (offset.dy - r.top) * _scale; final py0 = (offset.dy - r.top) * _scale;
@ -640,7 +641,7 @@ class CanvasModel with ChangeNotifier {
class CursorModel with ChangeNotifier { class CursorModel with ChangeNotifier {
ui.Image? _image; ui.Image? _image;
final _images = Map<int, Tuple3<ui.Image, double, double>>(); final _images = <int, Tuple3<ui.Image, double, double>>{};
double _x = -10000; double _x = -10000;
double _y = -10000; double _y = -10000;
double _hotx = 0; double _hotx = 0;
@ -807,7 +808,7 @@ class CursorModel with ChangeNotifier {
// my throw exception, because the listener maybe already dispose // my throw exception, because the listener maybe already dispose
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
print('notify cursor: $e'); debugPrint('notify cursor: $e');
} }
}); });
} }
@ -915,6 +916,8 @@ extension ToString on MouseButtons {
} }
} }
enum ConnType { defaultConn, fileTransfer, portForward, rdp }
/// FFI class for communicating with the Rust core. /// FFI class for communicating with the Rust core.
class FFI { class FFI {
var id = ""; var id = "";
@ -923,6 +926,7 @@ class FFI {
var alt = false; var alt = false;
var command = false; var command = false;
var version = ""; var version = "";
var connType = ConnType.defaultConn;
/// dialogManager use late to ensure init after main page binding [globalKey] /// dialogManager use late to ensure init after main page binding [globalKey]
late final dialogManager = OverlayDialogManager(); late final dialogManager = OverlayDialogManager();
@ -1076,9 +1080,11 @@ class FFI {
double tabBarHeight = 0.0}) { double tabBarHeight = 0.0}) {
assert(!(isFileTransfer && isPortForward), "more than one connect type"); assert(!(isFileTransfer && isPortForward), "more than one connect type");
if (isFileTransfer) { if (isFileTransfer) {
id = 'ft_${id}'; connType = ConnType.fileTransfer;
id = 'ft_$id';
} else if (isPortForward) { } else if (isPortForward) {
id = 'pf_${id}'; connType = ConnType.portForward;
id = 'pf_$id';
} else { } else {
chatModel.resetClientMode(); chatModel.resetClientMode();
canvasModel.id = id; canvasModel.id = id;
@ -1107,7 +1113,7 @@ class FFI {
// every instance will bind a stream // every instance will bind a stream
this.id = id; this.id = id;
if (isFileTransfer) { if (isFileTransfer) {
this.fileModel.initFileFetcher(); fileModel.initFileFetcher();
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
import 'package:wakelock/wakelock.dart'; import 'package:wakelock/wakelock.dart';
import '../common.dart'; import '../common.dart';
import '../common/formatter/id_formatter.dart';
import '../desktop/pages/server_page.dart' as Desktop; import '../desktop/pages/server_page.dart' as Desktop;
import '../desktop/widgets/tabbar_widget.dart'; import '../desktop/widgets/tabbar_widget.dart';
import '../mobile/pages/server_page.dart'; import '../mobile/pages/server_page.dart';
@ -29,10 +30,10 @@ class ServerModel with ChangeNotifier {
String _temporaryPasswordLength = ""; String _temporaryPasswordLength = "";
late String _emptyIdShow; late String _emptyIdShow;
late final TextEditingController _serverId; late final IDTextEditingController _serverId;
final _serverPasswd = TextEditingController(text: ""); final _serverPasswd = TextEditingController(text: "");
final tabController = DesktopTabController(); final tabController = DesktopTabController(tabType: DesktopTabType.cm);
List<Client> _clients = []; List<Client> _clients = [];
@ -88,7 +89,7 @@ class ServerModel with ChangeNotifier {
ServerModel(this.parent) { ServerModel(this.parent) {
_emptyIdShow = translate("Generating ..."); _emptyIdShow = translate("Generating ...");
_serverId = TextEditingController(text: this._emptyIdShow); _serverId = IDTextEditingController(text: _emptyIdShow);
Timer.periodic(Duration(seconds: 1), (timer) async { Timer.periodic(Duration(seconds: 1), (timer) async {
var status = await bind.mainGetOnlineStatue(); var status = await bind.mainGetOnlineStatue();
@ -99,7 +100,7 @@ class ServerModel with ChangeNotifier {
_connectStatus = status; _connectStatus = status;
notifyListeners(); notifyListeners();
} }
final res = await bind.mainCheckClientsLength(length: _clients.length); final res = await bind.cmCheckClientsLength(length: _clients.length);
if (res != null) { if (res != null) {
debugPrint("clients not match!"); debugPrint("clients not match!");
updateClientState(res); updateClientState(res);
@ -208,46 +209,48 @@ class ServerModel with ChangeNotifier {
/// Toggle the screen sharing service. /// Toggle the screen sharing service.
toggleService() async { toggleService() async {
if (_isStart) { if (_isStart) {
final res = await parent.target?.dialogManager final res =
.show<bool>((setState, close) => CustomAlertDialog( await parent.target?.dialogManager.show<bool>((setState, close) {
title: Row(children: [ submit() => close(true);
Icon(Icons.warning_amber_sharp, return CustomAlertDialog(
color: Colors.redAccent, size: 28), title: Row(children: [
SizedBox(width: 10), const Icon(Icons.warning_amber_sharp,
Text(translate("Warning")), color: Colors.redAccent, size: 28),
]), const SizedBox(width: 10),
content: Text(translate("android_stop_service_tip")), Text(translate("Warning")),
actions: [ ]),
TextButton( content: Text(translate("android_stop_service_tip")),
onPressed: () => close(), actions: [
child: Text(translate("Cancel"))), TextButton(onPressed: close, child: Text(translate("Cancel"))),
ElevatedButton( ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
onPressed: () => close(true), ],
child: Text(translate("OK"))), onSubmit: submit,
], onCancel: close,
)); );
});
if (res == true) { if (res == true) {
stopService(); stopService();
} }
} else { } else {
final res = await parent.target?.dialogManager final res =
.show<bool>((setState, close) => CustomAlertDialog( await parent.target?.dialogManager.show<bool>((setState, close) {
title: Row(children: [ submit() => close(true);
Icon(Icons.warning_amber_sharp, return CustomAlertDialog(
color: Colors.redAccent, size: 28), title: Row(children: [
SizedBox(width: 10), const Icon(Icons.warning_amber_sharp,
Text(translate("Warning")), color: Colors.redAccent, size: 28),
]), const SizedBox(width: 10),
content: Text(translate("android_service_will_start_tip")), Text(translate("Warning")),
actions: [ ]),
TextButton( content: Text(translate("android_service_will_start_tip")),
onPressed: () => close(), actions: [
child: Text(translate("Cancel"))), TextButton(onPressed: close, child: Text(translate("Cancel"))),
ElevatedButton( ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
onPressed: () => close(true), ],
child: Text(translate("OK"))), onSubmit: submit,
], onCancel: close,
)); );
});
if (res == true) { if (res == true) {
startService(); startService();
} }
@ -300,7 +303,7 @@ class ServerModel with ChangeNotifier {
} }
_fetchID() async { _fetchID() async {
final old = _serverId.text; final old = _serverId.id;
var count = 0; var count = 0;
const maxCount = 10; const maxCount = 10;
while (count < maxCount) { while (count < maxCount) {
@ -309,12 +312,12 @@ class ServerModel with ChangeNotifier {
if (id.isEmpty) { if (id.isEmpty) {
continue; continue;
} else { } else {
_serverId.text = id; _serverId.id = id;
} }
debugPrint("fetch id again at $count:id:${_serverId.text}"); debugPrint("fetch id again at $count:id:${_serverId.id}");
count++; count++;
if (_serverId.text != old) { if (_serverId.id != old) {
break; break;
} }
} }
@ -344,23 +347,21 @@ class ServerModel with ChangeNotifier {
// force // force
updateClientState([String? json]) async { updateClientState([String? json]) async {
var res = await bind.mainGetClientsState(); var res = await bind.cmGetClientsState();
try { try {
final List clientsJson = jsonDecode(res); final List clientsJson = jsonDecode(res);
if (isDesktop && clientsJson.isEmpty && _clients.isNotEmpty) {
// exit cm when >1 peers to no peers
exit(0);
}
_clients.clear(); _clients.clear();
tabController.state.value.tabs.clear(); tabController.state.value.tabs.clear();
for (var clientJson in clientsJson) { for (var clientJson in clientsJson) {
final client = Client.fromJson(clientJson); final client = Client.fromJson(clientJson);
_clients.add(client); _clients.add(client);
tabController.add(TabInfo( tabController.add(
key: client.id.toString(), TabInfo(
label: client.name, key: client.id.toString(),
closable: false, label: client.name,
page: Desktop.buildConnectionCard(client))); closable: false,
page: Desktop.buildConnectionCard(client)),
authorized: client.authorized);
} }
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
@ -368,70 +369,89 @@ class ServerModel with ChangeNotifier {
} }
} }
void loginRequest(Map<String, dynamic> evt) { void addConnection(Map<String, dynamic> evt) {
try { try {
final client = Client.fromJson(jsonDecode(evt["client"])); final client = Client.fromJson(jsonDecode(evt["client"]));
if (_clients.any((c) => c.id == client.id)) { if (client.authorized) {
return; 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) { } catch (e) {
debugPrint("Failed to call loginRequest,error:$e"); debugPrint("Failed to call loginRequest,error:$e");
} }
} }
void showLoginDialog(Client client) { void showLoginDialog(Client client) {
parent.target?.dialogManager.show( parent.target?.dialogManager.show((setState, close) {
(setState, close) => CustomAlertDialog( cancel() {
title: Row( sendLoginResponse(client, false);
mainAxisAlignment: MainAxisAlignment.spaceBetween, close();
children: [ }
Text(translate(client.isFileTransfer
? "File Connection" submit() {
: "Screen Connection")), sendLoginResponse(client, true);
IconButton( close();
onPressed: () { }
close();
}, return CustomAlertDialog(
icon: Icon(Icons.close)) title:
]), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
content: Column( Text(translate(
mainAxisSize: MainAxisSize.min, client.isFileTransfer ? "File Connection" : "Screen Connection")),
mainAxisAlignment: MainAxisAlignment.center, IconButton(
crossAxisAlignment: CrossAxisAlignment.start, onPressed: () {
children: [ close();
Text(translate("Do you accept?")), },
clientInfo(client), icon: const Icon(Icons.close))
Text( ]),
translate("android_new_connection_tip"), content: Column(
style: TextStyle(color: Colors.black54), mainAxisSize: MainAxisSize.min,
), mainAxisAlignment: MainAxisAlignment.center,
], crossAxisAlignment: CrossAxisAlignment.start,
), children: [
actions: [ Text(translate("Do you accept?")),
TextButton( clientInfo(client),
child: Text(translate("Dismiss")), Text(
onPressed: () { translate("android_new_connection_tip"),
sendLoginResponse(client, false); style: const TextStyle(color: Colors.black54),
close();
}),
ElevatedButton(
child: Text(translate("Accept")),
onPressed: () {
sendLoginResponse(client, true);
close();
}),
],
), ),
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() { scrollToBottom() {
@ -471,14 +491,18 @@ class ServerModel with ChangeNotifier {
} else { } else {
_clients[index].authorized = true; _clients[index].authorized = true;
} }
tabController.add(TabInfo( tabController.add(
key: client.id.toString(), TabInfo(
label: client.name, key: client.id.toString(),
closable: false, label: client.name,
page: Desktop.buildConnectionCard(client))); closable: false,
page: Desktop.buildConnectionCard(client)),
authorized: true);
scrollToBottom(); scrollToBottom();
notifyListeners(); notifyListeners();
} catch (e) {} } catch (e) {
debugPrint("onClientAuthorized:$e");
}
} }
void onClientRemove(Map<String, dynamic> evt) { void onClientRemove(Map<String, dynamic> evt) {
@ -486,8 +510,10 @@ class ServerModel with ChangeNotifier {
final id = int.parse(evt['id'] as String); final id = int.parse(evt['id'] as String);
if (_clients.any((c) => c.id == id)) { if (_clients.any((c) => c.id == id)) {
final index = _clients.indexWhere((client) => client.id == id); final index = _clients.indexWhere((client) => client.id == id);
_clients.removeAt(index); if (index >= 0) {
tabController.remove(index); _clients.removeAt(index);
tabController.remove(index);
}
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
parent.target?.invokeMethod("cancel_notification", id); parent.target?.invokeMethod("cancel_notification", id);
} }
@ -558,24 +584,29 @@ String getLoginDialogTag(int id) {
} }
showInputWarnAlert(FFI ffi) { showInputWarnAlert(FFI ffi) {
ffi.dialogManager.show((setState, close) => CustomAlertDialog( ffi.dialogManager.show((setState, close) {
title: Text(translate("How to get Android input permission?")), submit() {
content: Column( ffi.serverModel.initInput();
mainAxisSize: MainAxisSize.min, close();
children: [ }
Text(translate("android_input_permission_tip1")),
SizedBox(height: 10), return CustomAlertDialog(
Text(translate("android_input_permission_tip2")), title: Text(translate("How to get Android input permission?")),
], content: Column(
), mainAxisSize: MainAxisSize.min,
actions: [ children: [
TextButton(child: Text(translate("Cancel")), onPressed: close), Text(translate("android_input_permission_tip1")),
ElevatedButton( const SizedBox(height: 10),
child: Text(translate("Open System Setting")), Text(translate("android_input_permission_tip2")),
onPressed: () {
ffi.serverModel.initInput();
close();
}),
], ],
)); ),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
ElevatedButton(
onPressed: submit, child: Text(translate("Open System Setting"))),
],
onSubmit: submit,
onCancel: close,
);
});
} }

View File

@ -19,6 +19,8 @@ static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application); MyApplication* self = MY_APPLICATION(application);
GtkWindow* window = GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// we have custom window frame
gtk_window_set_decorated(window, FALSE);
// Use a header bar when running in GNOME as this is the common style used // Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu // by applications and is the setup most users will be using (e.g. Ubuntu

View File

@ -62,7 +62,7 @@ dependencies:
desktop_multi_window: desktop_multi_window:
git: git:
url: https://github.com/Kingtous/rustdesk_desktop_multi_window url: https://github.com/Kingtous/rustdesk_desktop_multi_window
ref: e0368a023ba195462acc00d33ab361b499f0e413 ref: fee851fa43116e0b91c39acd0ec37063dc6015f8
freezed_annotation: ^2.0.3 freezed_annotation: ^2.0.3
tray_manager: tray_manager:
git: git:

View File

@ -1666,6 +1666,7 @@ pub async fn handle_login_from_ui(
/// Interface for client to send data and commands. /// Interface for client to send data and commands.
#[async_trait] #[async_trait]
pub trait Interface: Send + Clone + 'static + Sized { pub trait Interface: Send + Clone + 'static + Sized {
/// Send message data to remote peer.
fn send(&self, data: Data); fn send(&self, data: Data);
fn msgbox(&self, msgtype: &str, title: &str, text: &str); fn msgbox(&self, msgtype: &str, title: &str, text: &str);
fn handle_login_error(&mut self, err: &str) -> bool; fn handle_login_error(&mut self, err: &str) -> bool;

View File

@ -22,9 +22,9 @@ pub trait FileManager: Interface {
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
fn read_dir(&self, path: &str, include_hidden: bool) -> String { fn read_dir(&self, path: &str, include_hidden: bool) -> String {
use crate::common::make_fd_to_json; use crate::flutter::make_fd_to_json;
match fs::read_dir(&fs::get_path(path), include_hidden) { match fs::read_dir(&fs::get_path(path), include_hidden) {
Ok(fd) => make_fd_to_json(fd), Ok(fd) => make_fd_to_json(fd.id, fd.path, &fd.entries),
Err(_) => "".into(), Err(_) => "".into(),
} }
} }

View File

@ -2,17 +2,11 @@ use crate::client::{
Client, CodecFormat, FileManager, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, Client, CodecFormat, FileManager, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED,
}; };
use crate::common;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
#[cfg(windows)] use crate::ui_session_interface::{InvokeUiSession, Session};
use clipboard::{
cliprdr::CliprdrClientContext, create_cliprdr_context as create_clipboard_file_context,
get_rx_clip_client, server_clip_file,
};
#[cfg(windows)]
use crate::clipboard_file::*;
use crate::ui_session_interface::{InvokeUi, Session};
use crate::{client::Data, client::Interface}; use crate::{client::Data, client::Interface};
use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::config::{PeerConfig, TransferSerde};
@ -28,14 +22,14 @@ use hbb_common::tokio::{
sync::mpsc, sync::mpsc,
time::{self, Duration, Instant, Interval}, time::{self, Duration, Instant, Interval},
}; };
use hbb_common::{allow_err, message_proto::*}; use hbb_common::{allow_err, message_proto::*, sleep};
use hbb_common::{fs, log, Stream}; use hbb_common::{fs, log, Stream};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
pub struct Remote<T: InvokeUi> { pub struct Remote<T: InvokeUiSession> {
handler: Session<T>, handler: Session<T>,
video_sender: MediaSender, video_sender: MediaSender,
audio_sender: MediaSender, audio_sender: MediaSender,
@ -49,13 +43,13 @@ pub struct Remote<T: InvokeUi> {
last_update_jobs_status: (Instant, HashMap<i32, u64>), last_update_jobs_status: (Instant, HashMap<i32, u64>),
first_frame: bool, first_frame: bool,
#[cfg(windows)] #[cfg(windows)]
clipboard_file_context: Option<Box<CliprdrClientContext>>, clipboard_file_context: Option<Box<clipboard::cliprdr::CliprdrClientContext>>,
data_count: Arc<AtomicUsize>, data_count: Arc<AtomicUsize>,
frame_count: Arc<AtomicUsize>, frame_count: Arc<AtomicUsize>,
video_format: CodecFormat, video_format: CodecFormat,
} }
impl<T: InvokeUi> Remote<T> { impl<T: InvokeUiSession> Remote<T> {
pub fn new( pub fn new(
handler: Session<T>, handler: Session<T>,
video_sender: MediaSender, video_sender: MediaSender,
@ -113,7 +107,7 @@ impl<T: InvokeUi> Remote<T> {
#[cfg(not(windows))] #[cfg(not(windows))]
let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::<i32>(); let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::<i32>();
#[cfg(windows)] #[cfg(windows)]
let mut rx_clip_client = get_rx_clip_client().lock().await; let mut rx_clip_client = clipboard::get_rx_clip_client().lock().await;
let mut status_timer = time::interval(Duration::new(1, 0)); let mut status_timer = time::interval(Duration::new(1, 0));
@ -159,7 +153,7 @@ impl<T: InvokeUi> Remote<T> {
#[cfg(windows)] #[cfg(windows)]
match _msg { match _msg {
Some((_, clip)) => { Some((_, clip)) => {
allow_err!(peer.send(&clip_2_msg(clip)).await); allow_err!(peer.send(&crate::clipboard_file::clip_2_msg(clip)).await);
} }
None => { None => {
// unreachable!() // unreachable!()
@ -275,51 +269,6 @@ impl<T: InvokeUi> Remote<T> {
Some(tx) Some(tx)
} }
// TODO
#[allow(dead_code)]
fn load_last_jobs(&mut self) {
log::info!("start load last jobs");
self.handler.clear_all_jobs();
let pc = self.handler.load_config();
if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() {
// no last jobs
return;
}
// TODO: can add a confirm dialog
let mut cnt = 1;
for job_str in pc.transfer.read_jobs.iter() {
let job: Result<TransferJobMeta, serde_json::Error> = serde_json::from_str(&job_str);
if let Ok(job) = job {
self.handler.add_job(
cnt,
job.to.clone(),
job.remote.clone(),
job.file_num,
job.show_hidden,
false,
);
cnt += 1;
println!("restore read_job: {:?}", job);
}
}
for job_str in pc.transfer.write_jobs.iter() {
let job: Result<TransferJobMeta, serde_json::Error> = serde_json::from_str(&job_str);
if let Ok(job) = job {
self.handler.add_job(
cnt,
job.remote.clone(),
job.to.clone(),
job.file_num,
job.show_hidden,
true,
);
cnt += 1;
println!("restore write_job: {:?}", job);
}
}
self.handler.update_transfer_list();
}
async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool {
match data { match data {
Data::Close => { Data::Close => {
@ -381,8 +330,13 @@ impl<T: InvokeUi> Remote<T> {
to, to,
job.files().len() job.files().len()
); );
// let m = make_fd(job.id(), job.files(), true); self.handler.update_folder_files(
// self.handler.call("updateFolderFiles", &make_args!(m)); // TODO job.id(),
job.files(),
path,
!is_remote,
true,
);
#[cfg(not(windows))] #[cfg(not(windows))]
let files = job.files().clone(); let files = job.files().clone();
#[cfg(windows)] #[cfg(windows)]
@ -441,8 +395,13 @@ impl<T: InvokeUi> Remote<T> {
to, to,
job.files().len() job.files().len()
); );
// let m = make_fd(job.id(), job.files(), true); self.handler.update_folder_files(
// self.handler.call("updateFolderFiles", &make_args!(m)); job.id(),
job.files(),
path,
!is_remote,
true,
);
job.is_last_job = true; job.is_last_job = true;
self.read_jobs.push(job); self.read_jobs.push(job);
self.timer = time::interval(MILLI1); self.timer = time::interval(MILLI1);
@ -493,7 +452,6 @@ impl<T: InvokeUi> Remote<T> {
file_num, file_num,
job.files[i].name.clone(), job.files[i].name.clone(),
); );
self.handler.confirm_delete_files(id, file_num);
} }
} }
} }
@ -554,8 +512,13 @@ impl<T: InvokeUi> Remote<T> {
} else { } else {
match fs::get_recursive_files(&path, include_hidden) { match fs::get_recursive_files(&path, include_hidden) {
Ok(entries) => { Ok(entries) => {
// let m = make_fd(id, &entries, true); self.handler.update_folder_files(
// self.handler.call("updateFolderFiles", &make_args!(m)); id,
&entries,
path.clone(),
!is_remote,
false,
);
self.remove_jobs self.remove_jobs
.insert(id, RemoveJob::new(entries, path, sep, is_remote)); .insert(id, RemoveJob::new(entries, path, sep, is_remote));
} }
@ -757,28 +720,28 @@ impl<T: InvokeUi> Remote<T> {
} }
Some(login_response::Union::PeerInfo(pi)) => { Some(login_response::Union::PeerInfo(pi)) => {
self.handler.handle_peer_info(pi); self.handler.handle_peer_info(pi);
// self.check_clipboard_file_context(); self.check_clipboard_file_context();
// if !(self.handler.is_file_transfer() if !(self.handler.is_file_transfer()
// || self.handler.is_port_forward() || self.handler.is_port_forward()
// || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst)
// || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst)
// || self.handler.lc.read().unwrap().disable_clipboard) || self.handler.lc.read().unwrap().disable_clipboard)
// { {
// let txt = self.old_clipboard.lock().unwrap().clone(); let txt = self.old_clipboard.lock().unwrap().clone();
// if !txt.is_empty() { if !txt.is_empty() {
// let msg_out = crate::create_clipboard_msg(txt); let msg_out = crate::create_clipboard_msg(txt);
// let sender = self.sender.clone(); let sender = self.sender.clone();
// tokio::spawn(async move { tokio::spawn(async move {
// // due to clipboard service interval time // due to clipboard service interval time
// sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await;
// sender.send(Data::Message(msg_out)).ok(); sender.send(Data::Message(msg_out)).ok();
// }); });
// } }
// } }
// if self.handler.is_file_transfer() { if self.handler.is_file_transfer() {
// self.load_last_jobs().await; self.handler.load_last_jobs();
// } }
} }
_ => {} _ => {}
}, },
@ -812,8 +775,8 @@ impl<T: InvokeUi> Remote<T> {
Some(message::Union::Cliprdr(clip)) => { Some(message::Union::Cliprdr(clip)) => {
if !self.handler.lc.read().unwrap().disable_clipboard { if !self.handler.lc.read().unwrap().disable_clipboard {
if let Some(context) = &mut self.clipboard_file_context { if let Some(context) = &mut self.clipboard_file_context {
if let Some(clip) = msg_2_clip(clip) { if let Some(clip) = crate::clipboard_file::msg_2_clip(clip) {
server_clip_file(context, 0, clip); clipboard::server_clip_file(context, 0, clip);
} }
} }
} }
@ -831,11 +794,13 @@ impl<T: InvokeUi> Remote<T> {
fs::transform_windows_path(&mut entries); fs::transform_windows_path(&mut entries);
} }
} }
// let mut m = make_fd(fd.id, &entries, fd.id > 0); self.handler.update_folder_files(
// if fd.id <= 0 { fd.id,
// m.set_item("path", fd.path); &entries,
// } fd.path,
// self.handler.call("updateFolderFiles", &make_args!(m)); false,
false,
);
if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) {
log::info!("job set_files: {:?}", entries); log::info!("job set_files: {:?}", entries);
job.set_files(entries); job.set_files(entries);
@ -1163,7 +1128,7 @@ impl<T: InvokeUi> Remote<T> {
&& self.handler.lc.read().unwrap().enable_file_transfer; && self.handler.lc.read().unwrap().enable_file_transfer;
if enabled == self.clipboard_file_context.is_none() { if enabled == self.clipboard_file_context.is_none() {
self.clipboard_file_context = if enabled { self.clipboard_file_context = if enabled {
match create_clipboard_file_context(true, false) { match clipboard::create_cliprdr_context(true, false) {
Ok(context) => { Ok(context) => {
log::info!("clipboard context for file transfer created."); log::info!("clipboard context for file transfer created.");
Some(context) Some(context)

View File

@ -667,51 +667,6 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess
msg_out msg_out
} }
pub fn make_fd_to_json(fd: FileDirectory) -> String {
let mut fd_json = serde_json::Map::new();
fd_json.insert("id".into(), json!(fd.id));
fd_json.insert("path".into(), json!(fd.path));
let mut entries = vec![];
for entry in fd.entries {
let mut entry_map = serde_json::Map::new();
entry_map.insert("entry_type".into(), json!(entry.entry_type.value()));
entry_map.insert("name".into(), json!(entry.name));
entry_map.insert("size".into(), json!(entry.size));
entry_map.insert("modified_time".into(), json!(entry.modified_time));
entries.push(entry_map);
}
fd_json.insert("entries".into(), json!(entries));
serde_json::to_string(&fd_json).unwrap_or("".into())
}
pub fn make_fd_flutter(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> String {
let mut m = serde_json::Map::new();
m.insert("id".into(), json!(id));
let mut a = vec![];
let mut n: u64 = 0;
for entry in entries {
n += entry.size;
if only_count {
continue;
}
let mut e = serde_json::Map::new();
e.insert("name".into(), json!(entry.name.to_owned()));
let tmp = entry.entry_type.value();
e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp }));
e.insert("time".into(), json!(entry.modified_time as f64));
e.insert("size".into(), json!(entry.size as f64));
a.push(e);
}
if only_count {
m.insert("num_entries".into(), json!(entries.len() as i32));
} else {
m.insert("entries".into(), json!(a));
}
m.insert("total_size".into(), json!(n as f64));
serde_json::to_string(&m).unwrap_or("".into())
}
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
lazy_static::lazy_static! { lazy_static::lazy_static! {
pub static ref IS_X11: Mutex<bool> = Mutex::new(false); pub static ref IS_X11: Mutex<bool> = Mutex::new(false);

View File

@ -5,16 +5,17 @@ use std::{
use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer};
use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType, rendezvous_proto::ConnType}; use hbb_common::{
bail, config::LocalConfig, message_proto::*, rendezvous_proto::ConnType, ResultType,
};
use serde_json::json;
use crate::ui_session_interface::{io_loop, InvokeUi, Session}; use crate::ui_session_interface::{io_loop, InvokeUiSession, Session};
use crate::{client::*, flutter_ffi::EventToUI}; use crate::{client::*, flutter_ffi::EventToUI};
pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_MAIN: &str = "main";
#[allow(dead_code)]
pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote";
#[allow(dead_code)]
pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer";
lazy_static::lazy_static! { lazy_static::lazy_static! {
@ -46,7 +47,7 @@ impl FlutterHandler {
} }
} }
impl InvokeUi for FlutterHandler { impl InvokeUiSession for FlutterHandler {
fn set_cursor_data(&self, cd: CursorData) { fn set_cursor_data(&self, cd: CursorData) {
let colors = hbb_common::compress::decompress(&cd.colors); let colors = hbb_common::compress::decompress(&cd.colors);
self.push_event( self.push_event(
@ -87,6 +88,7 @@ impl InvokeUi for FlutterHandler {
self.push_event("permission", vec![(name, &value.to_string())]); self.push_event("permission", vec![(name, &value.to_string())]);
} }
// unused in flutter
fn close_success(&self) {} fn close_success(&self) {}
fn update_quality_status(&self, status: QualityStatus) { fn update_quality_status(&self, status: QualityStatus) {
@ -119,8 +121,15 @@ impl InvokeUi for FlutterHandler {
); );
} }
fn job_error(&self, id: i32, err: String, _file_num: i32) { fn job_error(&self, id: i32, err: String, file_num: i32) {
self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); self.push_event(
"job_error",
vec![
("id", &id.to_string()),
("err", &err),
("file_num", &file_num.to_string()),
],
);
} }
fn job_done(&self, id: i32, file_num: i32) { fn job_done(&self, id: i32, file_num: i32) {
@ -130,31 +139,43 @@ impl InvokeUi for FlutterHandler {
); );
} }
fn clear_all_jobs(&self) { // unused in flutter
// todo!() fn clear_all_jobs(&self) {}
fn load_last_job(&self, _cnt: i32, job_json: &str) {
self.push_event("load_last_job", vec![("value", job_json)]);
} }
#[allow(unused_variables)] fn update_folder_files(
fn add_job(
&self, &self,
id: i32, id: i32,
entries: &Vec<FileEntry>,
path: String, path: String,
to: String, is_local: bool,
file_num: i32, only_count: bool,
show_hidden: bool,
is_remote: bool,
) { ) {
// todo!() // TODO opt
if only_count {
self.push_event(
"update_folder_files",
vec![("info", &make_fd_flutter(id, entries, only_count))],
);
} else {
self.push_event(
"file_dir",
vec![
("value", &make_fd_to_json(id, path, entries)),
("is_local", "false"),
],
);
}
} }
fn update_transfer_list(&self) { // unused in flutter
// todo!() fn update_transfer_list(&self) {}
}
#[allow(unused_variables)] // unused in flutter // TEST flutter
fn confirm_delete_files(&self, id: i32, i: i32, name: String) { fn confirm_delete_files(&self, _id: i32, _i: i32, _name: String) {}
// todo!()
}
fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) {
self.push_event( self.push_event(
@ -180,6 +201,7 @@ impl InvokeUi for FlutterHandler {
); );
} }
// unused in flutter
fn adapt_size(&self) {} fn adapt_size(&self) {}
fn on_rgba(&self, data: &[u8]) { fn on_rgba(&self, data: &[u8]) {
@ -287,11 +309,7 @@ pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> R
.unwrap() .unwrap()
.initialize(session_id, conn_type); .initialize(session_id, conn_type);
if let Some(same_id_session) = SESSIONS if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) {
.write()
.unwrap()
.insert(id.to_owned(), session)
{
same_id_session.close(); same_id_session.close();
} }
@ -320,624 +338,88 @@ pub fn session_start_(id: &str, event_stream: StreamSink<EventToUI>) -> ResultTy
// Server Side // Server Side
#[cfg(not(any(target_os = "ios")))] #[cfg(not(any(target_os = "ios")))]
pub mod connection_manager { pub mod connection_manager {
use std::{ use std::collections::HashMap;
collections::HashMap,
iter::FromIterator,
sync::{
atomic::{AtomicI64, Ordering},
RwLock,
},
};
use serde_derive::Serialize; use hbb_common::log;
use hbb_common::{
allow_err,
config::Config,
fs::is_write_need_confirmation,
fs::{self, get_string, new_send_confirm, DigestCheckResult},
log,
message_proto::*,
protobuf::Message as _,
tokio::{
self,
sync::mpsc::{self, UnboundedSender},
task::spawn_blocking,
},
};
#[cfg(any(target_os = "android"))] #[cfg(any(target_os = "android"))]
use scrap::android::call_main_service_set_by_name; use scrap::android::call_main_service_set_by_name;
use crate::ipc::Data; use crate::ui_cm_interface::InvokeUiCM;
use crate::ipc::{self, new_listener, Connection};
use super::GLOBAL_EVENT_STREAM; use super::GLOBAL_EVENT_STREAM;
#[derive(Debug, Serialize, Clone)] #[derive(Clone)]
struct Client { struct FlutterHandler {}
id: i32,
pub authorized: bool, impl InvokeUiCM for FlutterHandler {
is_file_transfer: bool, //TODO port_forward
name: String, fn add_connection(&self, client: &crate::ui_cm_interface::Client) {
peer_id: String, let client_json = serde_json::to_string(&client).unwrap_or("".into());
keyboard: bool, // send to Android service, active notification no matter UI is shown or not.
clipboard: bool, #[cfg(any(target_os = "android"))]
audio: bool, if let Err(e) =
file: bool, call_main_service_set_by_name("add_connection", Some(&client_json), None)
restart: bool, {
#[serde(skip)] log::debug!("call_service_set_by_name fail,{}", e);
tx: UnboundedSender<Data>, }
// 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! { impl FlutterHandler {
static ref CLIENTS: RwLock<HashMap<i32,Client>> = Default::default(); 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")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn start_listen_ipc_thread() { 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")))] #[cfg(target_os = "linux")]
#[tokio::main(flavor = "current_thread")] std::thread::spawn(crate::ipc::start_pa);
async fn start_ipc() {
// TODO clipboard_file
// let (tx_file, _rx_file) = mpsc::unbounded_channel::<ClipboardFileData>();
// #[cfg(windows)]
// let cm_clip = cm.clone();
// #[cfg(windows)]
// std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file));
#[cfg(windows)] let cm = ConnectionManager {
std::thread::spawn(move || { ui_handler: FlutterHandler {},
log::info!("try create privacy mode window"); };
#[cfg(windows)] std::thread::spawn(move || start_ipc(cm));
{
if let Err(e) = crate::platform::windows::check_update_broker_process() {
log::warn!(
"Failed to check update broker process. Privacy mode may not work properly. {}",
e
);
}
}
allow_err!(crate::ui::win_privacy::start());
});
match new_listener("_cm").await {
Ok(mut incoming) => {
while let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
log::debug!("Got new connection");
let mut stream = Connection::new(stream);
// let tx_file = tx_file.clone();
tokio::spawn(async move {
// for tmp use, without real conn id
let conn_id_tmp = -1;
let mut conn_id: i32 = 0;
let (tx, mut rx) = mpsc::unbounded_channel::<Data>();
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
loop {
tokio::select! {
res = stream.next() => {
match res {
Err(err) => {
log::info!("cm ipc connection closed: {}", err);
break;
}
Ok(Some(data)) => {
match data {
Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart} => {
log::debug!("conn_id: {}", id);
conn_id = id;
// tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok();
on_login(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone());
}
Data::Close => {
// tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok();
log::info!("cm ipc connection closed from connection request");
break;
}
Data::PrivacyModeState((_, _)) => {
conn_id = conn_id_tmp;
allow_err!(tx.send(data));
}
Data::ClickTime(ms) => {
CLICK_TIME.store(ms, Ordering::SeqCst);
}
Data::ChatMessage { text } => {
handle_chat(conn_id, text);
}
Data::FS(fs) => {
handle_fs(fs, &mut write_jobs, &tx).await;
}
// TODO ClipbaordFile
// #[cfg(windows)]
// Data::ClipbaordFile(_clip) => {
// tx_file
// .send(ClipboardFileData::Clip((id, _clip)))
// .ok();
// }
// #[cfg(windows)]
// Data::ClipboardFileEnabled(enabled) => {
// tx_file
// .send(ClipboardFileData::Enable((id, enabled)))
// .ok();
// }
_ => {}
}
}
_ => {}
}
}
Some(data) = rx.recv() => {
if stream.send(&data).await.is_err() {
break;
}
}
}
}
if conn_id != conn_id_tmp {
remove_connection(conn_id);
}
});
}
Err(err) => {
log::error!("Couldn't get cm client: {:?}", err);
}
}
}
}
Err(err) => {
log::error!("Failed to start cm ipc server: {}", err);
}
}
// crate::platform::quit_gui();
// TODO flutter quit_gui
} }
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub fn start_channel(rx: UnboundedReceiver<Data>, tx: UnboundedSender<Data>) { use hbb_common::tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
std::thread::spawn(move || start_listen(rx, tx));
}
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
#[tokio::main(flavor = "current_thread")] pub fn start_channel(
async fn start_listen(mut rx: UnboundedReceiver<Data>, tx: UnboundedSender<Data>) { rx: UnboundedReceiver<crate::ipc::Data>,
let mut current_id = 0; tx: UnboundedSender<crate::ipc::Data>,
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
loop {
match rx.recv().await {
Some(Data::Login {
id,
is_file_transfer,
port_forward,
peer_id,
name,
authorized,
keyboard,
clipboard,
audio,
file,
restart,
..
}) => {
current_id = id;
on_login(
id,
is_file_transfer,
port_forward,
peer_id,
name,
authorized,
keyboard,
clipboard,
audio,
file,
restart,
tx.clone(),
);
}
Some(Data::ChatMessage { text }) => {
handle_chat(current_id, text);
}
Some(Data::FS(fs)) => {
handle_fs(fs, &mut write_jobs, &tx).await;
}
Some(Data::Close) => {
break;
}
None => {
break;
}
_ => {}
}
}
remove_connection(current_id);
}
fn on_login(
id: i32,
is_file_transfer: bool,
_port_forward: String,
peer_id: String,
name: String,
authorized: bool,
keyboard: bool,
clipboard: bool,
audio: bool,
file: bool,
restart: bool,
tx: mpsc::UnboundedSender<Data>,
) { ) {
let mut client = Client { use crate::ui_cm_interface::start_listen;
id, let cm = crate::ui_cm_interface::ConnectionManager {
authorized, ui_handler: FlutterHandler {},
is_file_transfer,
name: name.clone(),
peer_id: peer_id.clone(),
keyboard,
clipboard,
audio,
file,
restart,
tx,
}; };
if authorized { std::thread::spawn(move || start_listen(cm, rx, tx));
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),
}
} }
} }
@ -950,30 +432,47 @@ pub fn get_session_id(id: String) -> String {
}; };
} }
// async fn start_one_port_forward( pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> String {
// handler: Session, let mut fd_json = serde_json::Map::new();
// port: i32, fd_json.insert("id".into(), json!(id));
// remote_host: String, fd_json.insert("path".into(), json!(path));
// remote_port: i32,
// receiver: mpsc::UnboundedReceiver<Data>, let mut entries_out = vec![];
// key: &str, for entry in entries {
// token: &str, let mut entry_map = serde_json::Map::new();
// ) { entry_map.insert("entry_type".into(), json!(entry.entry_type.value()));
// if let Err(err) = crate::port_forward::listen( entry_map.insert("name".into(), json!(entry.name));
// handler.id.clone(), entry_map.insert("size".into(), json!(entry.size));
// String::new(), // TODO entry_map.insert("modified_time".into(), json!(entry.modified_time));
// port, entries_out.push(entry_map);
// handler.clone(), }
// receiver, fd_json.insert("entries".into(), json!(entries_out));
// key, serde_json::to_string(&fd_json).unwrap_or("".into())
// token, }
// handler.lc.clone(),
// remote_host, pub fn make_fd_flutter(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> String {
// remote_port, let mut m = serde_json::Map::new();
// ) m.insert("id".into(), json!(id));
// .await let mut a = vec![];
// { let mut n: u64 = 0;
// handler.on_error(&format!("Failed to listen on {}: {}", port, err)); for entry in entries {
// } n += entry.size;
// log::info!("port forward (:{}) exit", port); if only_count {
// } continue;
}
let mut e = serde_json::Map::new();
e.insert("name".into(), json!(entry.name.to_owned()));
let tmp = entry.entry_type.value();
e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp }));
e.insert("time".into(), json!(entry.modified_time as f64));
e.insert("size".into(), json!(entry.size as f64));
a.push(e);
}
if only_count {
m.insert("num_entries".into(), json!(entries.len() as i32));
} else {
m.insert("entries".into(), json!(a));
}
m.insert("total_size".into(), json!(n as f64));
serde_json::to_string(&m).unwrap_or("".into())
}

View File

@ -13,8 +13,6 @@ use hbb_common::{
fs, log, fs, log,
}; };
use crate::common::make_fd_to_json;
use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state};
use crate::flutter::{self, SESSIONS}; use crate::flutter::{self, SESSIONS};
use crate::start_server; use crate::start_server;
use crate::ui_interface; use crate::ui_interface;
@ -31,7 +29,7 @@ use crate::ui_interface::{
}; };
use crate::{ use crate::{
client::file_trait::FileManager, client::file_trait::FileManager,
flutter::{session_add, session_start_}, flutter::{make_fd_to_json, session_add, session_start_},
}; };
fn initialize(app_dir: &str) { fn initialize(app_dir: &str) {
@ -388,10 +386,8 @@ pub fn session_create_dir(id: String, act_id: i32, path: String, is_remote: bool
} }
pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) -> String { pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) -> String {
if let Some(_) = SESSIONS.read().unwrap().get(&id) { if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) {
if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) { return make_fd_to_json(fd.id, path, &fd.entries);
return make_fd_to_json(fd);
}
} }
"".to_string() "".to_string()
} }
@ -404,8 +400,8 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String {
} }
pub fn session_load_last_transfer_jobs(id: String) { pub fn session_load_last_transfer_jobs(id: String) {
if let Some(_) = SESSIONS.read().unwrap().get(&id) { if let Some(session) = SESSIONS.read().unwrap().get(&id) {
// return session.load_last_jobs(); return session.load_last_jobs();
} else { } else {
// a tip for flutter dev // a tip for flutter dev
eprintln!( eprintln!(
@ -711,13 +707,13 @@ pub fn main_get_online_statue() -> i64 {
ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone() ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone()
} }
pub fn main_get_clients_state() -> String { pub fn cm_get_clients_state() -> String {
get_clients_state() crate::ui_cm_interface::get_clients_state()
} }
pub fn main_check_clients_length(length: usize) -> Option<String> { pub fn cm_check_clients_length(length: usize) -> Option<String> {
if length != get_clients_length() { if length != crate::ui_cm_interface::get_clients_length() {
Some(get_clients_state()) Some(crate::ui_cm_interface::get_clients_state())
} else { } else {
None None
} }
@ -793,7 +789,7 @@ pub fn main_set_home_dir(home: String) {
pub fn main_stop_service() { pub fn main_stop_service() {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
Config::set_option("stop-service".into(), "Y".into()); config::Config::set_option("stop-service".into(), "Y".into());
crate::rendezvous_mediator::RendezvousMediator::restart(); crate::rendezvous_mediator::RendezvousMediator::restart();
} }
} }
@ -801,7 +797,7 @@ pub fn main_stop_service() {
pub fn main_start_service() { pub fn main_start_service() {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
Config::set_option("stop-service".into(), "".into()); config::Config::set_option("stop-service".into(), "".into());
crate::rendezvous_mediator::RendezvousMediator::restart(); crate::rendezvous_mediator::RendezvousMediator::restart();
} }
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
@ -829,27 +825,31 @@ pub fn main_get_mouse_time() -> f64 {
} }
pub fn cm_send_chat(conn_id: i32, msg: String) { pub fn cm_send_chat(conn_id: i32, msg: String) {
connection_manager::send_chat(conn_id, msg); crate::ui_cm_interface::send_chat(conn_id, msg);
} }
pub fn cm_login_res(conn_id: i32, res: bool) { pub fn cm_login_res(conn_id: i32, res: bool) {
connection_manager::on_login_res(conn_id, res); if res {
crate::ui_cm_interface::authorize(conn_id);
} else {
crate::ui_cm_interface::close(conn_id);
}
} }
pub fn cm_close_connection(conn_id: i32) { pub fn cm_close_connection(conn_id: i32) {
connection_manager::close_conn(conn_id); crate::ui_cm_interface::close(conn_id);
} }
pub fn cm_check_click_time(conn_id: i32) { pub fn cm_check_click_time(conn_id: i32) {
connection_manager::check_click_time(conn_id) crate::ui_cm_interface::check_click_time(conn_id)
} }
pub fn cm_get_click_time() -> f64 { pub fn cm_get_click_time() -> f64 {
connection_manager::get_click_time() as _ crate::ui_cm_interface::get_click_time() as _
} }
pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) { pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) {
connection_manager::switch_permission(conn_id, name, enabled) crate::ui_cm_interface::switch_permission(conn_id, name, enabled)
} }
pub fn main_get_icon() -> String { pub fn main_get_icon() -> String {

View File

@ -49,6 +49,7 @@ mod tray;
mod ui_interface; mod ui_interface;
mod ui_session_interface; mod ui_session_interface;
mod ui_cm_interface;
#[cfg(windows)] #[cfg(windows)]
pub mod clipboard_file; pub mod clipboard_file;

100
src/ui.rs
View File

@ -14,7 +14,6 @@ use hbb_common::{
log, log,
protobuf::Message as _, protobuf::Message as _,
rendezvous_proto::*, rendezvous_proto::*,
sleep,
tcp::FramedStream, tcp::FramedStream,
tokio::{self, sync::mpsc, time}, tokio::{self, sync::mpsc, time},
}; };
@ -87,7 +86,7 @@ pub fn start(args: &mut [String]) {
} }
#[cfg(windows)] #[cfg(windows)]
if args.len() > 0 && args[0] == "--tray" { if args.len() > 0 && args[0] == "--tray" {
let options = check_connect_status(false).1; let options = crate::ui_interface::check_connect_status(false).1;
crate::tray::start_tray(options); crate::tray::start_tray(options);
return; return;
} }
@ -125,7 +124,7 @@ pub fn start(args: &mut [String]) {
page = "install.html"; page = "install.html";
} else if args[0] == "--cm" { } else if args[0] == "--cm" {
frame.register_behavior("connection-manager", move || { frame.register_behavior("connection-manager", move || {
Box::new(cm::ConnectionManager::new()) Box::new(cm::SciterConnectionManager::new())
}); });
page = "cm.html"; page = "cm.html";
} else if (args[0] == "--connect" } else if (args[0] == "--connect"
@ -664,79 +663,6 @@ pub fn check_zombie(childs: Childs) {
} }
} }
// notice: avoiding create ipc connecton repeatly,
// because windows named pipe has serious memory leak issue.
#[tokio::main(flavor = "current_thread")]
async fn check_connect_status_(
reconnect: bool,
status: Arc<Mutex<Status>>,
options: Arc<Mutex<HashMap<String, String>>>,
rx: mpsc::UnboundedReceiver<ipc::Data>,
password: Arc<Mutex<String>>,
) {
let mut key_confirmed = false;
let mut rx = rx;
let mut mouse_time = 0;
let mut id = "".to_owned();
loop {
if let Ok(mut c) = ipc::connect(1000, "").await {
let mut timer = time::interval(time::Duration::from_secs(1));
loop {
tokio::select! {
res = c.next() => {
match res {
Err(err) => {
log::error!("ipc connection closed: {}", err);
break;
}
Ok(Some(ipc::Data::MouseMoveTime(v))) => {
mouse_time = v;
status.lock().unwrap().2 = v;
}
Ok(Some(ipc::Data::Options(Some(v)))) => {
*options.lock().unwrap() = v
}
Ok(Some(ipc::Data::Config((name, Some(value))))) => {
if name == "id" {
id = value;
} else if name == "temporary-password" {
*password.lock().unwrap() = value;
}
}
Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => {
if x > 0 {
x = 1
}
key_confirmed = c;
*status.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone());
}
_ => {}
}
}
Some(data) = rx.recv() => {
allow_err!(c.send(&data).await);
}
_ = timer.tick() => {
c.send(&ipc::Data::OnlineStatus(None)).await.ok();
c.send(&ipc::Data::Options(None)).await.ok();
c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok();
c.send(&ipc::Data::Config(("temporary-password".to_owned(), None))).await.ok();
}
}
}
}
if !reconnect {
options
.lock()
.unwrap()
.insert("ipc-closed".to_owned(), "Y".to_owned());
break;
}
*status.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone());
sleep(1.).await;
}
}
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
fn get_sound_inputs() -> Vec<String> { fn get_sound_inputs() -> Vec<String> {
let mut out = Vec::new(); let mut out = Vec::new();
@ -763,28 +689,6 @@ fn get_sound_inputs() -> Vec<String> {
.collect() .collect()
} }
#[allow(dead_code)]
fn check_connect_status(
reconnect: bool,
) -> (
Arc<Mutex<Status>>,
Arc<Mutex<HashMap<String, String>>>,
mpsc::UnboundedSender<ipc::Data>,
Arc<Mutex<String>>,
) {
let status = Arc::new(Mutex::new((0, false, 0, "".to_owned())));
let options = Arc::new(Mutex::new(Config::get_options()));
let cloned = status.clone();
let cloned_options = options.clone();
let (tx, rx) = mpsc::unbounded_channel::<ipc::Data>();
let password = Arc::new(Mutex::new(String::default()));
let cloned_password = password.clone();
std::thread::spawn(move || {
check_connect_status_(reconnect, cloned, cloned_options, rx, cloned_password)
});
(status, options, tx, password)
}
const INVALID_FORMAT: &'static str = "Invalid format"; const INVALID_FORMAT: &'static str = "Invalid format";
const UNKNOWN_ERROR: &'static str = "Unknown error"; const UNKNOWN_ERROR: &'static str = "Unknown error";

View File

@ -1,59 +1,83 @@
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use crate::ipc::start_pa; use crate::ipc::start_pa;
use crate::ipc::{self, new_listener, Connection, Data}; use crate::ui_cm_interface::{start_ipc, ConnectionManager, InvokeUiCM};
#[cfg(windows)] #[cfg(windows)]
use clipboard::{ use clipboard::{
create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled, create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled,
}; };
use hbb_common::fs::{
get_string, is_write_need_confirmation, new_send_confirm,
DigestCheckResult,
};
use hbb_common::{
allow_err,
config::Config,
fs, log,
message_proto::*,
protobuf::Message as _,
tokio::{self, sync::mpsc, task::spawn_blocking},
};
use sciter::{make_args, Element, Value, HELEMENT};
use std::{
collections::HashMap,
ops::Deref,
sync::{Arc, RwLock},
};
pub struct ConnectionManagerInner { use hbb_common::{allow_err, log};
root: Option<Element>, use sciter::{make_args, Element, Value, HELEMENT};
senders: HashMap<i32, mpsc::UnboundedSender<Data>>, use std::sync::Mutex;
click_time: i64, use std::{ops::Deref, sync::Arc};
#[derive(Clone, Default)]
pub struct SciterHandler {
pub element: Arc<Mutex<Option<Element>>>,
} }
#[derive(Clone)] impl InvokeUiCM for SciterHandler {
pub struct ConnectionManager(Arc<RwLock<ConnectionManagerInner>>); fn add_connection(&self, client: &crate::ui_cm_interface::Client) {
self.call(
"addConnection",
&make_args!(
client.id,
client.is_file_transfer,
client.port_forward.clone(),
client.peer_id.clone(),
client.name.clone(),
client.authorized,
client.keyboard,
client.clipboard,
client.audio,
client.file,
client.restart
),
);
}
impl Deref for ConnectionManager { fn remove_connection(&self, id: i32) {
type Target = Arc<RwLock<ConnectionManagerInner>>; self.call("removeConnection", &make_args!(id));
if crate::ui_cm_interface::get_clients_length().eq(&0) {
crate::platform::quit_gui();
}
}
fn new_message(&self, id: i32, text: String) {
self.call("newMessage", &make_args!(id, text));
}
}
impl SciterHandler {
#[inline]
fn call(&self, func: &str, args: &[Value]) {
if let Some(e) = self.element.lock().unwrap().as_ref() {
allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..]));
}
}
}
pub struct SciterConnectionManager(ConnectionManager<SciterHandler>);
impl Deref for SciterConnectionManager {
type Target = ConnectionManager<SciterHandler>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
} }
} }
impl ConnectionManager { impl SciterConnectionManager {
pub fn new() -> Self { pub fn new() -> Self {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
std::thread::spawn(start_pa); std::thread::spawn(start_pa);
let inner = ConnectionManagerInner { let cm = ConnectionManager {
root: None, ui_handler: SciterHandler::default(),
senders: HashMap::new(),
click_time: Default::default(),
}; };
let cm = Self(Arc::new(RwLock::new(inner)));
let cloned = cm.clone(); let cloned = cm.clone();
std::thread::spawn(move || start_ipc(cloned)); std::thread::spawn(move || start_ipc(cloned));
cm SciterConnectionManager(cm)
} }
fn get_icon(&mut self) -> String { fn get_icon(&mut self) -> String {
@ -61,359 +85,27 @@ impl ConnectionManager {
} }
fn check_click_time(&mut self, id: i32) { fn check_click_time(&mut self, id: i32) {
let lock = self.read().unwrap(); crate::ui_cm_interface::check_click_time(id);
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::ClickTime(0)));
}
} }
fn get_click_time(&self) -> f64 { fn get_click_time(&self) -> f64 {
self.read().unwrap().click_time as _ crate::ui_cm_interface::get_click_time() as _
}
#[inline]
fn call(&self, func: &str, args: &[Value]) {
let r = self.read().unwrap();
if let Some(ref e) = r.root {
allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..]));
}
}
fn add_connection(
&self,
id: i32,
is_file_transfer: bool,
port_forward: String,
peer_id: String,
name: String,
authorized: bool,
keyboard: bool,
clipboard: bool,
audio: bool,
file: bool,
restart: bool,
tx: mpsc::UnboundedSender<Data>,
) {
self.call(
"addConnection",
&make_args!(
id,
is_file_transfer,
port_forward,
peer_id,
name,
authorized,
keyboard,
clipboard,
audio,
file,
restart
),
);
self.write().unwrap().senders.insert(id, tx);
}
fn remove_connection(&self, id: i32) {
self.write().unwrap().senders.remove(&id);
if self.read().unwrap().senders.len() == 0 {
crate::platform::quit_gui();
}
self.call("removeConnection", &make_args!(id));
}
async fn handle_data(
&self,
id: i32,
data: Data,
_tx_clip_file: &mpsc::UnboundedSender<ClipboardFileData>,
write_jobs: &mut Vec<fs::TransferJob>,
conn: &mut Connection,
) {
match data {
Data::ChatMessage { text } => {
self.call("newMessage", &make_args!(id, text));
}
Data::ClickTime(ms) => {
self.write().unwrap().click_time = ms;
}
Data::FS(v) => match v {
ipc::FS::ReadDir {
dir,
include_hidden,
} => {
Self::read_dir(&dir, include_hidden, conn).await;
}
ipc::FS::RemoveDir {
path,
id,
recursive,
} => {
Self::remove_dir(path, id, recursive, conn).await;
}
ipc::FS::RemoveFile { path, id, file_num } => {
Self::remove_file(path, id, file_num, conn).await;
}
ipc::FS::CreateDir { path, id } => {
Self::create_dir(path, id, conn).await;
}
ipc::FS::NewWrite {
path,
id,
file_num,
mut files,
overwrite_detection,
} => {
// cm has no show_hidden context
// dummy remote, show_hidden, is_remote
write_jobs.push(fs::TransferJob::new_write(
id,
"".to_string(),
path,
file_num,
false,
false,
files
.drain(..)
.map(|f| FileEntry {
name: f.0,
modified_time: f.1,
..Default::default()
})
.collect(),
overwrite_detection,
));
}
ipc::FS::CancelWrite { id } => {
if let Some(job) = fs::get_job(id, write_jobs) {
job.remove_download_file();
fs::remove_job(id, write_jobs);
}
}
ipc::FS::WriteDone { id, file_num } => {
if let Some(job) = fs::get_job(id, write_jobs) {
job.modify_time();
Self::send(fs::new_done(id, file_num), conn).await;
fs::remove_job(id, write_jobs);
}
}
ipc::FS::CheckDigest {
id,
file_num,
file_size,
last_modified,
is_upload,
} => {
if let Some(job) = fs::get_job(id, write_jobs) {
let mut req = FileTransferSendConfirmRequest {
id,
file_num,
union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)),
..Default::default()
};
let digest = FileTransferDigest {
id,
file_num,
last_modified,
file_size,
..Default::default()
};
if let Some(file) = job.files().get(file_num as usize) {
let path = get_string(&job.join(&file.name));
match is_write_need_confirmation(&path, &digest) {
Ok(digest_result) => {
match digest_result {
DigestCheckResult::IsSame => {
req.set_skip(true);
let msg_out = new_send_confirm(req);
Self::send(msg_out, conn).await;
}
DigestCheckResult::NeedConfirm(mut digest) => {
// upload to server, but server has the same file, request
digest.is_upload = is_upload;
let mut msg_out = Message::new();
let mut fr = FileResponse::new();
fr.set_digest(digest);
msg_out.set_file_response(fr);
Self::send(msg_out, conn).await;
}
DigestCheckResult::NoSuchFile => {
let msg_out = new_send_confirm(req);
Self::send(msg_out, conn).await;
}
}
}
Err(err) => {
Self::send(fs::new_error(id, err, file_num), conn).await;
}
}
}
}
}
ipc::FS::WriteBlock {
id,
file_num,
data,
compressed,
} => {
let raw = if let Ok(bytes) = conn.next_raw().await {
Some(bytes)
} else {
None
};
if let Some(job) = fs::get_job(id, write_jobs) {
if let Err(err) = job
.write(
FileTransferBlock {
id,
file_num,
data,
compressed,
..Default::default()
},
raw.as_ref().map(|x| &x[..]),
)
.await
{
Self::send(fs::new_error(id, err, file_num), conn).await;
}
}
}
ipc::FS::WriteOffset {
id: _,
file_num: _,
offset_blk: _,
} => {}
},
#[cfg(windows)]
Data::ClipbaordFile(_clip) => {
_tx_clip_file
.send(ClipboardFileData::Clip((id, _clip)))
.ok();
}
#[cfg(windows)]
Data::ClipboardFileEnabled(enabled) => {
_tx_clip_file
.send(ClipboardFileData::Enable((id, enabled)))
.ok();
}
_ => {}
}
}
async fn read_dir(dir: &str, include_hidden: bool, conn: &mut Connection) {
let path = {
if dir.is_empty() {
Config::get_home()
} else {
fs::get_path(dir)
}
};
if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await {
let mut msg_out = Message::new();
let mut file_response = FileResponse::new();
file_response.set_dir(fd);
msg_out.set_file_response(file_response);
Self::send(msg_out, conn).await;
}
}
async fn handle_result<F: std::fmt::Display, S: std::fmt::Display>(
res: std::result::Result<std::result::Result<(), F>, S>,
id: i32,
file_num: i32,
conn: &mut Connection,
) {
match res {
Err(err) => {
Self::send(fs::new_error(id, err, file_num), conn).await;
}
Ok(Err(err)) => {
Self::send(fs::new_error(id, err, file_num), conn).await;
}
Ok(Ok(())) => {
Self::send(fs::new_done(id, file_num), conn).await;
}
}
}
async fn remove_file(path: String, id: i32, file_num: i32, conn: &mut Connection) {
Self::handle_result(
spawn_blocking(move || fs::remove_file(&path)).await,
id,
file_num,
conn,
)
.await;
}
async fn create_dir(path: String, id: i32, conn: &mut Connection) {
Self::handle_result(
spawn_blocking(move || fs::create_dir(&path)).await,
id,
0,
conn,
)
.await;
}
async fn remove_dir(path: String, id: i32, recursive: bool, conn: &mut Connection) {
let path = fs::get_path(&path);
Self::handle_result(
spawn_blocking(move || {
if recursive {
fs::remove_all_empty_dir(&path)
} else {
std::fs::remove_dir(&path).map_err(|err| err.into())
}
})
.await,
id,
0,
conn,
)
.await;
}
async fn send(msg: Message, conn: &mut Connection) {
match msg.write_to_bytes() {
Ok(bytes) => allow_err!(conn.send(&Data::RawMessage(bytes)).await),
err => allow_err!(err),
}
} }
fn switch_permission(&self, id: i32, name: String, enabled: bool) { fn switch_permission(&self, id: i32, name: String, enabled: bool) {
let lock = self.read().unwrap(); crate::ui_cm_interface::switch_permission(id, name, enabled);
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::SwitchPermission { name, enabled }));
}
} }
fn close(&self, id: i32) { fn close(&self, id: i32) {
let lock = self.read().unwrap(); crate::ui_cm_interface::close(id);
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::Close));
}
}
fn send_msg(&self, id: i32, text: String) {
let lock = self.read().unwrap();
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(Data::ChatMessage { text }));
}
}
fn send_data(&self, id: i32, data: Data) {
let lock = self.read().unwrap();
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(data));
}
} }
fn authorize(&self, id: i32) { fn authorize(&self, id: i32) {
let lock = self.read().unwrap(); crate::ui_cm_interface::authorize(id);
if let Some(s) = lock.senders.get(&id) { }
allow_err!(s.send(Data::Authorize));
} fn send_msg(&self, id: i32, text: String) {
crate::ui_cm_interface::send_chat(id, text);
} }
fn t(&self, name: String) -> String { fn t(&self, name: String) -> String {
@ -421,9 +113,9 @@ impl ConnectionManager {
} }
} }
impl sciter::EventHandler for ConnectionManager { impl sciter::EventHandler for SciterConnectionManager {
fn attached(&mut self, root: HELEMENT) { fn attached(&mut self, root: HELEMENT) {
self.write().unwrap().root = Some(Element::from(root)); *self.ui_handler.element.lock().unwrap() = Some(Element::from(root));
} }
sciter::dispatch_script_call! { sciter::dispatch_script_call! {
@ -437,179 +129,3 @@ impl sciter::EventHandler for ConnectionManager {
fn send_msg(i32, String); fn send_msg(i32, String);
} }
} }
pub enum ClipboardFileData {
#[cfg(windows)]
Clip((i32, ipc::ClipbaordFile)),
Enable((i32, bool)),
}
#[tokio::main(flavor = "current_thread")]
async fn start_ipc(cm: ConnectionManager) {
let (tx_file, _rx_file) = mpsc::unbounded_channel::<ClipboardFileData>();
#[cfg(windows)]
let cm_clip = cm.clone();
#[cfg(windows)]
std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file));
#[cfg(windows)]
std::thread::spawn(move || {
log::info!("try create privacy mode window");
#[cfg(windows)]
{
if let Err(e) = crate::platform::windows::check_update_broker_process() {
log::warn!(
"Failed to check update broker process. Privacy mode may not work properly. {}",
e
);
}
}
allow_err!(crate::ui::win_privacy::start());
});
match new_listener("_cm").await {
Ok(mut incoming) => {
while let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
log::debug!("Got new connection");
let mut stream = Connection::new(stream);
let cm = cm.clone();
let tx_file = tx_file.clone();
tokio::spawn(async move {
// for tmp use, without real conn id
let conn_id_tmp = -1;
let mut conn_id: i32 = 0;
let (tx, mut rx) = mpsc::unbounded_channel::<Data>();
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
loop {
tokio::select! {
res = stream.next() => {
match res {
Err(err) => {
log::info!("cm ipc connection closed: {}", err);
break;
}
Ok(Some(data)) => {
match data {
Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => {
log::debug!("conn_id: {}", id);
conn_id = id;
tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok();
cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone());
}
Data::Close => {
tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok();
log::info!("cm ipc connection closed from connection request");
break;
}
Data::PrivacyModeState((id, _)) => {
conn_id = conn_id_tmp;
cm.send_data(id, data)
}
_ => {
cm.handle_data(conn_id, data, &tx_file, &mut write_jobs, &mut stream).await;
}
}
}
_ => {}
}
}
Some(data) = rx.recv() => {
if stream.send(&data).await.is_err() {
break;
}
}
}
}
if conn_id != conn_id_tmp {
cm.remove_connection(conn_id);
}
});
}
Err(err) => {
log::error!("Couldn't get cm client: {:?}", err);
}
}
}
}
Err(err) => {
log::error!("Failed to start cm ipc server: {}", err);
}
}
crate::platform::quit_gui();
}
#[cfg(windows)]
#[tokio::main(flavor = "current_thread")]
pub async fn start_clipboard_file(
cm: ConnectionManager,
mut rx: mpsc::UnboundedReceiver<ClipboardFileData>,
) {
let mut cliprdr_context = None;
let mut rx_clip_client = get_rx_clip_client().lock().await;
loop {
tokio::select! {
clip_file = rx_clip_client.recv() => match clip_file {
Some((conn_id, clip)) => {
cmd_inner_send(
&cm,
conn_id,
Data::ClipbaordFile(clip)
);
}
None => {
//
}
},
server_msg = rx.recv() => match server_msg {
Some(ClipboardFileData::Clip((conn_id, clip))) => {
if let Some(ctx) = cliprdr_context.as_mut() {
server_clip_file(ctx, conn_id, clip);
}
}
Some(ClipboardFileData::Enable((id, enabled))) => {
if enabled && cliprdr_context.is_none() {
cliprdr_context = Some(match create_cliprdr_context(true, false) {
Ok(context) => {
log::info!("clipboard context for file transfer created.");
context
}
Err(err) => {
log::error!(
"Create clipboard context for file transfer: {}",
err.to_string()
);
return;
}
});
}
set_conn_enabled(id, enabled);
if !enabled {
if let Some(ctx) = cliprdr_context.as_mut() {
empty_clipboard(ctx, id);
}
}
}
None => {
break
}
}
}
}
}
#[cfg(windows)]
fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) {
let lock = cm.read().unwrap();
if id != 0 {
if let Some(s) = lock.senders.get(&id) {
allow_err!(s.send(data));
}
} else {
for s in lock.senders.values() {
allow_err!(s.send(data.clone()));
}
}
}

View File

@ -695,7 +695,7 @@ handler.clearAllJobs = function() {
file_transfer.job_table.clearAllJobs(); file_transfer.job_table.clearAllJobs();
} }
handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { // load last job
// stdout.println("restore job: " + is_remote); // stdout.println("restore job: " + is_remote);
file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote); file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote);
} }

View File

@ -1055,6 +1055,7 @@ function showSettings() {
} }
function checkConnectStatus() { function checkConnectStatus() {
handler.check_mouse_time(); // trigger connection status updater
self.timer(1s, function() { self.timer(1s, function() {
var tmp = !!handler.get_option("stop-service"); var tmp = !!handler.get_option("stop-service");
if (tmp != service_stopped) { if (tmp != service_stopped) {

View File

@ -1,9 +1,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::{ sync::{Arc, Mutex},
Arc, Mutex,
},
}; };
use sciter::{ use sciter::{
@ -22,13 +20,15 @@ use clipboard::{
get_rx_clip_client, server_clip_file, get_rx_clip_client, server_clip_file,
}; };
use hbb_common::{allow_err, log, message_proto::*, rendezvous_proto::ConnType}; use hbb_common::{
allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType,
};
#[cfg(windows)] #[cfg(windows)]
use crate::clipboard_file::*; use crate::clipboard_file::*;
use crate::{ use crate::{
client::*, client::*,
ui_session_interface::{InvokeUi, Session}, ui_session_interface::{InvokeUiSession, Session},
}; };
type Video = AssetPtr<video_destination>; type Video = AssetPtr<video_destination>;
@ -37,12 +37,8 @@ lazy_static::lazy_static! {
static ref VIDEO: Arc<Mutex<Option<Video>>> = Default::default(); static ref VIDEO: Arc<Mutex<Option<Video>>> = Default::default();
} }
#[cfg(windows)]
static mut IS_ALT_GR: bool = false;
/// SciterHandler /// SciterHandler
/// * element /// * element
/// * thread TODO check if flutter need
/// * close_state for file path when close /// * close_state for file path when close
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct SciterHandler { pub struct SciterHandler {
@ -66,7 +62,7 @@ impl SciterHandler {
} }
} }
impl InvokeUi for SciterHandler { impl InvokeUiSession for SciterHandler {
fn set_cursor_data(&self, cd: CursorData) { fn set_cursor_data(&self, cd: CursorData) {
let mut colors = hbb_common::compress::decompress(&cd.colors); let mut colors = hbb_common::compress::decompress(&cd.colors);
if colors.iter().filter(|x| **x != 0).next().is_none() { if colors.iter().filter(|x| **x != 0).next().is_none() {
@ -154,17 +150,36 @@ impl InvokeUi for SciterHandler {
self.call("clearAllJobs", &make_args!()); self.call("clearAllJobs", &make_args!());
} }
#[allow(unused_variables)] fn load_last_job(&self, cnt: i32, job_json: &str) {
fn add_job( let job: Result<TransferJobMeta, serde_json::Error> = serde_json::from_str(job_json);
if let Ok(job) = job {
let path;
let to;
if job.is_remote {
path = job.remote.clone();
to = job.to.clone();
} else {
path = job.to.clone();
to = job.remote.clone();
}
self.call(
"addJob",
&make_args!(cnt, path, to, job.file_num, job.show_hidden, job.is_remote),
);
}
}
fn update_folder_files(
&self, &self,
id: i32, id: i32,
entries: &Vec<FileEntry>,
path: String, path: String,
to: String, _is_local: bool,
file_num: i32, only_count: bool,
show_hidden: bool,
is_remote: bool,
) { ) {
todo!() let mut m = make_fd(id, entries, only_count);
m.set_item("path", path);
self.call("updateFolderFiles", &make_args!(m));
} }
fn update_transfer_list(&self) { fn update_transfer_list(&self) {
@ -426,6 +441,14 @@ impl SciterSession {
v v
} }
pub fn t(&self, name: String) -> String {
crate::client::translate(name)
}
pub fn get_icon(&self) -> String {
crate::get_icon()
}
fn supported_hwcodec(&self) -> Value { fn supported_hwcodec(&self) -> Value {
#[cfg(feature = "hwcodec")] #[cfg(feature = "hwcodec")]
{ {
@ -693,11 +716,10 @@ pub fn make_fd(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> Value {
e.set_item("size", entry.size as f64); e.set_item("size", entry.size as f64);
a.push(e); a.push(e);
} }
if only_count { if !only_count {
m.set_item("num_entries", entries.len() as i32);
} else {
m.set_item("entries", a); m.set_item("entries", a);
} }
m.set_item("num_entries", entries.len() as i32);
m.set_item("total_size", n as f64); m.set_item("total_size", n as f64);
m m
} }

View File

@ -67,6 +67,7 @@ function adaptDisplay() {
} }
} }
} }
refreshCursor();
handler.style.set { handler.style.set {
width: w / scaleFactor + "px", width: w / scaleFactor + "px",
height: h / scaleFactor + "px", height: h / scaleFactor + "px",
@ -98,6 +99,7 @@ var acc_wheel_delta_y0 = 0;
var total_wheel_time = 0; var total_wheel_time = 0;
var wheeling = false; var wheeling = false;
var dragging = false; var dragging = false;
var is_mouse_event_triggered = false;
// https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum // https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum
function resetWheel() { function resetWheel() {
@ -139,6 +141,7 @@ function accWheel(v, is_x) {
function handler.onMouse(evt) function handler.onMouse(evt)
{ {
is_mouse_event_triggered = true;
if (is_file_transfer || is_port_forward) return false; if (is_file_transfer || is_port_forward) return false;
if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) { if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) {
var dy = evt.y - scroll_body.scroll(#top); var dy = evt.y - scroll_body.scroll(#top);
@ -317,6 +320,7 @@ function handler.onMouse(evt)
return true; return true;
}; };
var cur_id = -1;
var cur_hotx = 0; var cur_hotx = 0;
var cur_hoty = 0; var cur_hoty = 0;
var cur_img = null; var cur_img = null;
@ -345,7 +349,7 @@ function scaleCursorImage(img) {
var useSystemCursor = true; var useSystemCursor = true;
function updateCursor(system=false) { function updateCursor(system=false) {
stdout.println("Update cursor, system: " + system); stdout.println("Update cursor, system: " + system);
useSystemCursor= system; useSystemCursor = system;
if (system) { if (system) {
handler.style#cursor = undefined; handler.style#cursor = undefined;
} else if (cur_img) { } 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) { handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
cur_hotx = hotx; cur_hotx = hotx;
cur_hoty = hoty; cur_hoty = hoty;
@ -360,8 +370,9 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
if (img) { if (img) {
image_binded = true; image_binded = true;
cursors[id] = [img, hotx, hoty, width, height]; cursors[id] = [img, hotx, hoty, width, height];
cur_id = id;
img = scaleCursorImage(img); img = scaleCursorImage(img);
if (cursor_img.style#display == 'none') { if (!first_mouse_event_triggered || cursor_img.style#display == 'none') {
self.timer(1ms, updateCursor); self.timer(1ms, updateCursor);
} }
cur_img = img; cur_img = img;
@ -371,11 +382,12 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) {
handler.setCursorId = function(id) { handler.setCursorId = function(id) {
var img = cursors[id]; var img = cursors[id];
if (img) { if (img) {
cur_id = id;
image_binded = true; image_binded = true;
cur_hotx = img[1]; cur_hotx = img[1];
cur_hoty = img[2]; cur_hoty = img[2];
img = scaleCursorImage(img[0]); img = scaleCursorImage(img[0]);
if (cursor_img.style#display == 'none') { if (!first_mouse_event_triggered || cursor_img.style#display == 'none') {
self.timer(1ms, updateCursor); self.timer(1ms, updateCursor);
} }
cur_img = img; cur_img = img;

671
src/ui_cm_interface.rs Normal file
View 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()));
}
}
}

View File

@ -372,10 +372,11 @@ pub fn get_mouse_time() -> f64 {
return res; return res;
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn check_mouse_time() { pub fn check_mouse_time() {
let sender = SENDER.lock().unwrap(); #[cfg(not(any(target_os = "android", target_os = "ios")))]{
allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); let sender = SENDER.lock().unwrap();
allow_err!(sender.send(ipc::Data::MouseMoveTime(0)));
}
} }
pub fn get_connect_status() -> Status { pub fn get_connect_status() -> Status {

View File

@ -34,7 +34,7 @@ lazy_static::lazy_static! {
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct Session<T: InvokeUi> { pub struct Session<T: InvokeUiSession> {
pub cmd: String, pub cmd: String,
pub id: String, pub id: String,
pub password: String, pub password: String,
@ -45,7 +45,7 @@ pub struct Session<T: InvokeUi> {
pub ui_handler: T, pub ui_handler: T,
} }
impl<T: InvokeUi> Session<T> { impl<T: InvokeUiSession> Session<T> {
pub fn get_view_style(&self) -> String { pub fn get_view_style(&self) -> String {
self.lc.read().unwrap().view_style.clone() self.lc.read().unwrap().view_style.clone()
} }
@ -151,11 +151,6 @@ impl<T: InvokeUi> Session<T> {
self.send(Data::Message(msg)); self.send(Data::Message(msg));
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn t(&self, name: String) -> String {
crate::client::translate(name)
}
pub fn get_audit_server(&self) -> String { pub fn get_audit_server(&self) -> String {
if self.lc.read().unwrap().conn_id <= 0 if self.lc.read().unwrap().conn_id <= 0
|| LocalConfig::get_option("access_token").is_empty() || LocalConfig::get_option("access_token").is_empty()
@ -690,11 +685,6 @@ impl<T: InvokeUi> Session<T> {
return "".to_owned(); return "".to_owned();
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn get_icon(&self) -> String {
crate::get_icon()
}
pub fn send_chat(&self, text: String) { pub fn send_chat(&self, text: String) {
let mut misc = Misc::new(); let mut misc = Misc::new();
misc.set_chat_message(ChatMessage { misc.set_chat_message(ChatMessage {
@ -961,9 +951,35 @@ impl<T: InvokeUi> Session<T> {
pub fn close(&self) { pub fn close(&self) {
self.send(Data::Close); self.send(Data::Close);
} }
pub fn load_last_jobs(&self) {
self.clear_all_jobs();
let pc = self.load_config();
if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() {
// no last jobs
return;
}
// TODO: can add a confirm dialog
let mut cnt = 1;
for job_str in pc.transfer.read_jobs.iter() {
if !job_str.is_empty() {
self.load_last_job(cnt, job_str);
cnt += 1;
log::info!("restore read_job: {:?}", job_str);
}
}
for job_str in pc.transfer.write_jobs.iter() {
if !job_str.is_empty() {
self.load_last_job(cnt, job_str);
cnt += 1;
log::info!("restore write_job: {:?}", job_str);
}
}
self.update_transfer_list();
}
} }
pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
fn set_cursor_data(&self, cd: CursorData); fn set_cursor_data(&self, cd: CursorData);
fn set_cursor_id(&self, id: String); fn set_cursor_id(&self, id: String);
fn set_cursor_position(&self, cp: CursorPosition); fn set_cursor_position(&self, cp: CursorPosition);
@ -978,18 +994,17 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default {
fn job_error(&self, id: i32, err: String, file_num: i32); fn job_error(&self, id: i32, err: String, file_num: i32);
fn job_done(&self, id: i32, file_num: i32); fn job_done(&self, id: i32, file_num: i32);
fn clear_all_jobs(&self); fn clear_all_jobs(&self);
fn add_job(
&self,
id: i32,
path: String,
to: String,
file_num: i32,
show_hidden: bool,
is_remote: bool,
);
fn new_message(&self, msg: String); fn new_message(&self, msg: String);
fn update_transfer_list(&self); fn update_transfer_list(&self);
// fn update_folder_files(&self); // TODO flutter with file_dir and update_folder_files fn load_last_job(&self, cnt: i32, job_json: &str);
fn update_folder_files(
&self,
id: i32,
entries: &Vec<FileEntry>,
path: String,
is_local: bool,
only_count: bool,
);
fn confirm_delete_files(&self, id: i32, i: i32, name: String); fn confirm_delete_files(&self, id: i32, i: i32, name: String);
fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool); fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool);
fn update_block_input_state(&self, on: bool); fn update_block_input_state(&self, on: bool);
@ -1001,7 +1016,7 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default {
fn clipboard(&self, content: String); fn clipboard(&self, content: String);
} }
impl<T: InvokeUi> Deref for Session<T> { impl<T: InvokeUiSession> Deref for Session<T> {
type Target = T; type Target = T;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@ -1009,16 +1024,16 @@ impl<T: InvokeUi> Deref for Session<T> {
} }
} }
impl<T: InvokeUi> DerefMut for Session<T> { impl<T: InvokeUiSession> DerefMut for Session<T> {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.ui_handler &mut self.ui_handler
} }
} }
impl<T: InvokeUi> FileManager for Session<T> {} impl<T: InvokeUiSession> FileManager for Session<T> {}
#[async_trait] #[async_trait]
impl<T: InvokeUi> Interface for Session<T> { impl<T: InvokeUiSession> Interface for Session<T> {
fn send(&self, data: Data) { fn send(&self, data: Data) {
if let Some(sender) = self.sender.read().unwrap().as_ref() { if let Some(sender) = self.sender.read().unwrap().as_ref() {
sender.send(data).ok(); sender.send(data).ok();
@ -1146,7 +1161,7 @@ impl<T: InvokeUi> Interface for Session<T> {
// TODO use event callbcak // TODO use event callbcak
// sciter only // sciter only
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
impl<T: InvokeUi> Session<T> { impl<T: InvokeUiSession> Session<T> {
fn start_keyboard_hook(&self) { fn start_keyboard_hook(&self) {
if self.is_port_forward() || self.is_file_transfer() { if self.is_port_forward() || self.is_file_transfer() {
return; return;
@ -1211,7 +1226,7 @@ impl<T: InvokeUi> Session<T> {
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
pub async fn io_loop<T: InvokeUi>(handler: Session<T>) { pub async fn io_loop<T: InvokeUiSession>(handler: Session<T>) {
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>(); let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
*handler.sender.write().unwrap() = Some(sender.clone()); *handler.sender.write().unwrap() = Some(sender.clone());
let mut options = crate::ipc::get_options_async().await; let mut options = crate::ipc::get_options_async().await;
@ -1327,7 +1342,7 @@ pub async fn io_loop<T: InvokeUi>(handler: Session<T>) {
} }
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
async fn start_one_port_forward<T: InvokeUi>( async fn start_one_port_forward<T: InvokeUiSession>(
handler: Session<T>, handler: Session<T>,
port: i32, port: i32,
remote_host: String, remote_host: String,