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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,16 +5,17 @@ use std::{
use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer};
use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType, rendezvous_proto::ConnType};
use hbb_common::{
bail, config::LocalConfig, message_proto::*, rendezvous_proto::ConnType, ResultType,
};
use serde_json::json;
use crate::ui_session_interface::{io_loop, InvokeUi, Session};
use crate::ui_session_interface::{io_loop, InvokeUiSession, Session};
use crate::{client::*, flutter_ffi::EventToUI};
pub(super) const APP_TYPE_MAIN: &str = "main";
#[allow(dead_code)]
pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote";
#[allow(dead_code)]
pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer";
lazy_static::lazy_static! {
@ -46,7 +47,7 @@ impl FlutterHandler {
}
}
impl InvokeUi for FlutterHandler {
impl InvokeUiSession for FlutterHandler {
fn set_cursor_data(&self, cd: CursorData) {
let colors = hbb_common::compress::decompress(&cd.colors);
self.push_event(
@ -87,6 +88,7 @@ impl InvokeUi for FlutterHandler {
self.push_event("permission", vec![(name, &value.to_string())]);
}
// unused in flutter
fn close_success(&self) {}
fn update_quality_status(&self, status: QualityStatus) {
@ -119,8 +121,15 @@ impl InvokeUi for FlutterHandler {
);
}
fn job_error(&self, id: i32, err: String, _file_num: i32) {
self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]);
fn job_error(&self, id: i32, err: String, file_num: i32) {
self.push_event(
"job_error",
vec![
("id", &id.to_string()),
("err", &err),
("file_num", &file_num.to_string()),
],
);
}
fn job_done(&self, id: i32, file_num: i32) {
@ -130,31 +139,43 @@ impl InvokeUi for FlutterHandler {
);
}
fn clear_all_jobs(&self) {
// todo!()
// unused in flutter
fn clear_all_jobs(&self) {}
fn load_last_job(&self, _cnt: i32, job_json: &str) {
self.push_event("load_last_job", vec![("value", job_json)]);
}
#[allow(unused_variables)]
fn add_job(
fn update_folder_files(
&self,
id: i32,
entries: &Vec<FileEntry>,
path: String,
to: String,
file_num: i32,
show_hidden: bool,
is_remote: bool,
is_local: bool,
only_count: bool,
) {
// todo!()
// TODO opt
if only_count {
self.push_event(
"update_folder_files",
vec![("info", &make_fd_flutter(id, entries, only_count))],
);
} else {
self.push_event(
"file_dir",
vec![
("value", &make_fd_to_json(id, path, entries)),
("is_local", "false"),
],
);
}
}
fn update_transfer_list(&self) {
// todo!()
}
// unused in flutter
fn update_transfer_list(&self) {}
#[allow(unused_variables)]
fn confirm_delete_files(&self, id: i32, i: i32, name: String) {
// todo!()
}
// unused in flutter // TEST flutter
fn confirm_delete_files(&self, _id: i32, _i: i32, _name: String) {}
fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) {
self.push_event(
@ -180,6 +201,7 @@ impl InvokeUi for FlutterHandler {
);
}
// unused in flutter
fn adapt_size(&self) {}
fn on_rgba(&self, data: &[u8]) {
@ -287,11 +309,7 @@ pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> R
.unwrap()
.initialize(session_id, conn_type);
if let Some(same_id_session) = SESSIONS
.write()
.unwrap()
.insert(id.to_owned(), session)
{
if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) {
same_id_session.close();
}
@ -320,624 +338,88 @@ pub fn session_start_(id: &str, event_stream: StreamSink<EventToUI>) -> ResultTy
// Server Side
#[cfg(not(any(target_os = "ios")))]
pub mod connection_manager {
use std::{
collections::HashMap,
iter::FromIterator,
sync::{
atomic::{AtomicI64, Ordering},
RwLock,
},
};
use std::collections::HashMap;
use serde_derive::Serialize;
use hbb_common::{
allow_err,
config::Config,
fs::is_write_need_confirmation,
fs::{self, get_string, new_send_confirm, DigestCheckResult},
log,
message_proto::*,
protobuf::Message as _,
tokio::{
self,
sync::mpsc::{self, UnboundedSender},
task::spawn_blocking,
},
};
use hbb_common::log;
#[cfg(any(target_os = "android"))]
use scrap::android::call_main_service_set_by_name;
use crate::ipc::Data;
use crate::ipc::{self, new_listener, Connection};
use crate::ui_cm_interface::InvokeUiCM;
use super::GLOBAL_EVENT_STREAM;
#[derive(Debug, Serialize, Clone)]
struct Client {
id: i32,
pub authorized: bool,
is_file_transfer: bool,
name: String,
peer_id: String,
keyboard: bool,
clipboard: bool,
audio: bool,
file: bool,
restart: bool,
#[serde(skip)]
tx: UnboundedSender<Data>,
#[derive(Clone)]
struct FlutterHandler {}
impl InvokeUiCM for FlutterHandler {
//TODO port_forward
fn add_connection(&self, client: &crate::ui_cm_interface::Client) {
let client_json = serde_json::to_string(&client).unwrap_or("".into());
// send to Android service, active notification no matter UI is shown or not.
#[cfg(any(target_os = "android"))]
if let Err(e) =
call_main_service_set_by_name("add_connection", Some(&client_json), None)
{
log::debug!("call_service_set_by_name fail,{}", e);
}
// send to UI, refresh widget
self.push_event("add_connection", vec![("client", &client_json)]); // TODO use add_connection
}
fn remove_connection(&self, id: i32) {
self.push_event("on_client_remove", vec![("id", &id.to_string())]);
}
fn new_message(&self, id: i32, text: String) {
self.push_event(
"chat_server_mode",
vec![("id", &id.to_string()), ("text", &text)],
);
}
}
lazy_static::lazy_static! {
static ref CLIENTS: RwLock<HashMap<i32,Client>> = Default::default();
impl FlutterHandler {
fn push_event(&self, name: &str, event: Vec<(&str, &str)>) {
let mut h: HashMap<&str, &str> = event.iter().cloned().collect();
assert!(h.get("name").is_none());
h.insert("name", name);
if let Some(s) = GLOBAL_EVENT_STREAM
.read()
.unwrap()
.get(super::APP_TYPE_MAIN)
{
s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned()));
};
}
}
static CLICK_TIME: AtomicI64 = AtomicI64::new(0);
// // TODO clipboard_file
// enum ClipboardFileData {
// #[cfg(windows)]
// Clip((i32, ipc::ClipbaordFile)),
// Enable((i32, bool)),
// }
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn start_listen_ipc_thread() {
std::thread::spawn(move || start_ipc());
}
use crate::ui_cm_interface::{start_ipc, ConnectionManager};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tokio::main(flavor = "current_thread")]
async fn start_ipc() {
// TODO clipboard_file
// let (tx_file, _rx_file) = mpsc::unbounded_channel::<ClipboardFileData>();
// #[cfg(windows)]
// let cm_clip = cm.clone();
// #[cfg(windows)]
// std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file));
#[cfg(target_os = "linux")]
std::thread::spawn(crate::ipc::start_pa);
#[cfg(windows)]
std::thread::spawn(move || {
log::info!("try create privacy mode window");
#[cfg(windows)]
{
if let Err(e) = crate::platform::windows::check_update_broker_process() {
log::warn!(
"Failed to check update broker process. Privacy mode may not work properly. {}",
e
);
}
}
allow_err!(crate::ui::win_privacy::start());
});
match new_listener("_cm").await {
Ok(mut incoming) => {
while let Some(result) = incoming.next().await {
match result {
Ok(stream) => {
log::debug!("Got new connection");
let mut stream = Connection::new(stream);
// let tx_file = tx_file.clone();
tokio::spawn(async move {
// for tmp use, without real conn id
let conn_id_tmp = -1;
let mut conn_id: i32 = 0;
let (tx, mut rx) = mpsc::unbounded_channel::<Data>();
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
loop {
tokio::select! {
res = stream.next() => {
match res {
Err(err) => {
log::info!("cm ipc connection closed: {}", err);
break;
}
Ok(Some(data)) => {
match data {
Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart} => {
log::debug!("conn_id: {}", id);
conn_id = id;
// tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok();
on_login(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone());
}
Data::Close => {
// tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok();
log::info!("cm ipc connection closed from connection request");
break;
}
Data::PrivacyModeState((_, _)) => {
conn_id = conn_id_tmp;
allow_err!(tx.send(data));
}
Data::ClickTime(ms) => {
CLICK_TIME.store(ms, Ordering::SeqCst);
}
Data::ChatMessage { text } => {
handle_chat(conn_id, text);
}
Data::FS(fs) => {
handle_fs(fs, &mut write_jobs, &tx).await;
}
// TODO ClipbaordFile
// #[cfg(windows)]
// Data::ClipbaordFile(_clip) => {
// tx_file
// .send(ClipboardFileData::Clip((id, _clip)))
// .ok();
// }
// #[cfg(windows)]
// Data::ClipboardFileEnabled(enabled) => {
// tx_file
// .send(ClipboardFileData::Enable((id, enabled)))
// .ok();
// }
_ => {}
}
}
_ => {}
}
}
Some(data) = rx.recv() => {
if stream.send(&data).await.is_err() {
break;
}
}
}
}
if conn_id != conn_id_tmp {
remove_connection(conn_id);
}
});
}
Err(err) => {
log::error!("Couldn't get cm client: {:?}", err);
}
}
}
}
Err(err) => {
log::error!("Failed to start cm ipc server: {}", err);
}
}
// crate::platform::quit_gui();
// TODO flutter quit_gui
let cm = ConnectionManager {
ui_handler: FlutterHandler {},
};
std::thread::spawn(move || start_ipc(cm));
}
#[cfg(target_os = "android")]
pub fn start_channel(rx: UnboundedReceiver<Data>, tx: UnboundedSender<Data>) {
std::thread::spawn(move || start_listen(rx, tx));
}
use hbb_common::tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
#[cfg(target_os = "android")]
#[tokio::main(flavor = "current_thread")]
async fn start_listen(mut rx: UnboundedReceiver<Data>, tx: UnboundedSender<Data>) {
let mut current_id = 0;
let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
loop {
match rx.recv().await {
Some(Data::Login {
id,
is_file_transfer,
port_forward,
peer_id,
name,
authorized,
keyboard,
clipboard,
audio,
file,
restart,
..
}) => {
current_id = id;
on_login(
id,
is_file_transfer,
port_forward,
peer_id,
name,
authorized,
keyboard,
clipboard,
audio,
file,
restart,
tx.clone(),
);
}
Some(Data::ChatMessage { text }) => {
handle_chat(current_id, text);
}
Some(Data::FS(fs)) => {
handle_fs(fs, &mut write_jobs, &tx).await;
}
Some(Data::Close) => {
break;
}
None => {
break;
}
_ => {}
}
}
remove_connection(current_id);
}
fn on_login(
id: i32,
is_file_transfer: bool,
_port_forward: String,
peer_id: String,
name: String,
authorized: bool,
keyboard: bool,
clipboard: bool,
audio: bool,
file: bool,
restart: bool,
tx: mpsc::UnboundedSender<Data>,
pub fn start_channel(
rx: UnboundedReceiver<crate::ipc::Data>,
tx: UnboundedSender<crate::ipc::Data>,
) {
let mut client = Client {
id,
authorized,
is_file_transfer,
name: name.clone(),
peer_id: peer_id.clone(),
keyboard,
clipboard,
audio,
file,
restart,
tx,
use crate::ui_cm_interface::start_listen;
let cm = crate::ui_cm_interface::ConnectionManager {
ui_handler: FlutterHandler {},
};
if authorized {
client.authorized = true;
let client_json = serde_json::to_string(&client).unwrap_or("".into());
// send to Android service, active notification no matter UI is shown or not.
#[cfg(any(target_os = "android"))]
if let Err(e) =
call_main_service_set_by_name("on_client_authorized", Some(&client_json), None)
{
log::debug!("call_service_set_by_name fail,{}", e);
}
// send to UI, refresh widget
push_event("on_client_authorized", vec![("client", &client_json)]);
} else {
let client_json = serde_json::to_string(&client).unwrap_or("".into());
// send to Android service, active notification no matter UI is shown or not.
#[cfg(any(target_os = "android"))]
if let Err(e) =
call_main_service_set_by_name("try_start_without_auth", Some(&client_json), None)
{
log::debug!("call_service_set_by_name fail,{}", e);
}
// send to UI, refresh widget
push_event("try_start_without_auth", vec![("client", &client_json)]);
}
CLIENTS.write().unwrap().insert(id, client);
}
fn push_event(name: &str, event: Vec<(&str, &str)>) {
let mut h: HashMap<&str, &str> = event.iter().cloned().collect();
assert!(h.get("name").is_none());
h.insert("name", name);
if let Some(s) = GLOBAL_EVENT_STREAM
.read()
.unwrap()
.get(super::APP_TYPE_MAIN)
{
s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned()));
};
}
pub fn get_click_time() -> i64 {
CLICK_TIME.load(Ordering::SeqCst)
}
pub fn check_click_time(id: i32) {
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
allow_err!(client.tx.send(Data::ClickTime(0)));
};
}
pub fn switch_permission(id: i32, name: String, enabled: bool) {
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
allow_err!(client.tx.send(Data::SwitchPermission { name, enabled }));
};
}
pub fn get_clients_state() -> String {
let clients = CLIENTS.read().unwrap();
let res = Vec::from_iter(clients.values().cloned());
serde_json::to_string(&res).unwrap_or("".into())
}
pub fn get_clients_length() -> usize {
let clients = CLIENTS.read().unwrap();
clients.len()
}
pub fn close_conn(id: i32) {
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
allow_err!(client.tx.send(Data::Close));
};
}
pub fn on_login_res(id: i32, res: bool) {
if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
if res {
allow_err!(client.tx.send(Data::Authorize));
client.authorized = true;
} else {
allow_err!(client.tx.send(Data::Close));
}
};
}
fn remove_connection(id: i32) {
let mut clients = CLIENTS.write().unwrap();
clients.remove(&id);
if clients
.iter()
.filter(|(_k, v)| !v.is_file_transfer)
.next()
.is_none()
{
#[cfg(any(target_os = "android"))]
if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) {
log::debug!("stop_capture err:{}", e);
}
}
push_event("on_client_remove", vec![("id", &id.to_string())]);
}
// server mode handle chat from other peers
fn handle_chat(id: i32, text: String) {
push_event(
"chat_server_mode",
vec![("id", &id.to_string()), ("text", &text)],
);
}
// server mode send chat to peer
pub fn send_chat(id: i32, text: String) {
let clients = CLIENTS.read().unwrap();
if let Some(client) = clients.get(&id) {
allow_err!(client.tx.send(Data::ChatMessage { text }));
}
}
// handle FS server
async fn handle_fs(
fs: ipc::FS,
write_jobs: &mut Vec<fs::TransferJob>,
tx: &UnboundedSender<Data>,
) {
match fs {
ipc::FS::ReadDir {
dir,
include_hidden,
} => {
read_dir(&dir, include_hidden, tx).await;
}
ipc::FS::RemoveDir {
path,
id,
recursive,
} => {
remove_dir(path, id, recursive, tx).await;
}
ipc::FS::RemoveFile { path, id, file_num } => {
remove_file(path, id, file_num, tx).await;
}
ipc::FS::CreateDir { path, id } => {
create_dir(path, id, tx).await;
}
ipc::FS::NewWrite {
path,
id,
file_num,
mut files,
overwrite_detection,
} => {
write_jobs.push(fs::TransferJob::new_write(
id,
"".to_string(),
path,
file_num,
false,
false,
files
.drain(..)
.map(|f| FileEntry {
name: f.0,
modified_time: f.1,
..Default::default()
})
.collect(),
overwrite_detection,
));
}
ipc::FS::CancelWrite { id } => {
if let Some(job) = fs::get_job(id, write_jobs) {
job.remove_download_file();
fs::remove_job(id, write_jobs);
}
}
ipc::FS::WriteDone { id, file_num } => {
if let Some(job) = fs::get_job(id, write_jobs) {
job.modify_time();
send_raw(fs::new_done(id, file_num), tx);
fs::remove_job(id, write_jobs);
}
}
ipc::FS::WriteBlock {
id,
file_num,
data,
compressed,
} => {
if let Some(job) = fs::get_job(id, write_jobs) {
if let Err(err) = job
.write(
FileTransferBlock {
id,
file_num,
data,
compressed,
..Default::default()
},
None,
)
.await
{
send_raw(fs::new_error(id, err, file_num), &tx);
}
}
}
ipc::FS::CheckDigest {
id,
file_num,
file_size,
last_modified,
is_upload,
} => {
if let Some(job) = fs::get_job(id, write_jobs) {
let mut req = FileTransferSendConfirmRequest {
id,
file_num,
union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)),
..Default::default()
};
let digest = FileTransferDigest {
id,
file_num,
last_modified,
file_size,
..Default::default()
};
if let Some(file) = job.files().get(file_num as usize) {
let path = get_string(&job.join(&file.name));
match is_write_need_confirmation(&path, &digest) {
Ok(digest_result) => {
match digest_result {
DigestCheckResult::IsSame => {
req.set_skip(true);
let msg_out = new_send_confirm(req);
send_raw(msg_out, &tx);
}
DigestCheckResult::NeedConfirm(mut digest) => {
// upload to server, but server has the same file, request
digest.is_upload = is_upload;
let mut msg_out = Message::new();
let mut fr = FileResponse::new();
fr.set_digest(digest);
msg_out.set_file_response(fr);
send_raw(msg_out, &tx);
}
DigestCheckResult::NoSuchFile => {
let msg_out = new_send_confirm(req);
send_raw(msg_out, &tx);
}
}
}
Err(err) => {
send_raw(fs::new_error(id, err, file_num), &tx);
}
}
}
}
}
_ => {}
}
}
async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender<Data>) {
let path = {
if dir.is_empty() {
Config::get_home()
} else {
fs::get_path(dir)
}
};
if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await {
let mut msg_out = Message::new();
let mut file_response = FileResponse::new();
file_response.set_dir(fd);
msg_out.set_file_response(file_response);
send_raw(msg_out, tx);
}
}
async fn handle_result<F: std::fmt::Display, S: std::fmt::Display>(
res: std::result::Result<std::result::Result<(), F>, S>,
id: i32,
file_num: i32,
tx: &UnboundedSender<Data>,
) {
match res {
Err(err) => {
send_raw(fs::new_error(id, err, file_num), tx);
}
Ok(Err(err)) => {
send_raw(fs::new_error(id, err, file_num), tx);
}
Ok(Ok(())) => {
send_raw(fs::new_done(id, file_num), tx);
}
}
}
async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender<Data>) {
handle_result(
spawn_blocking(move || fs::remove_file(&path)).await,
id,
file_num,
tx,
)
.await;
}
async fn create_dir(path: String, id: i32, tx: &UnboundedSender<Data>) {
handle_result(
spawn_blocking(move || fs::create_dir(&path)).await,
id,
0,
tx,
)
.await;
}
async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender<Data>) {
let path = fs::get_path(&path);
handle_result(
spawn_blocking(move || {
if recursive {
fs::remove_all_empty_dir(&path)
} else {
std::fs::remove_dir(&path).map_err(|err| err.into())
}
})
.await,
id,
0,
tx,
)
.await;
}
fn send_raw(msg: Message, tx: &UnboundedSender<Data>) {
match msg.write_to_bytes() {
Ok(bytes) => {
allow_err!(tx.send(Data::RawMessage(bytes)));
}
err => allow_err!(err),
}
std::thread::spawn(move || start_listen(cm, rx, tx));
}
}
@ -950,30 +432,47 @@ pub fn get_session_id(id: String) -> String {
};
}
// async fn start_one_port_forward(
// handler: Session,
// port: i32,
// remote_host: String,
// remote_port: i32,
// receiver: mpsc::UnboundedReceiver<Data>,
// key: &str,
// token: &str,
// ) {
// if let Err(err) = crate::port_forward::listen(
// handler.id.clone(),
// String::new(), // TODO
// port,
// handler.clone(),
// receiver,
// key,
// token,
// handler.lc.clone(),
// remote_host,
// remote_port,
// )
// .await
// {
// handler.on_error(&format!("Failed to listen on {}: {}", port, err));
// }
// log::info!("port forward (:{}) exit", port);
// }
pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> String {
let mut fd_json = serde_json::Map::new();
fd_json.insert("id".into(), json!(id));
fd_json.insert("path".into(), json!(path));
let mut entries_out = vec![];
for entry in entries {
let mut entry_map = serde_json::Map::new();
entry_map.insert("entry_type".into(), json!(entry.entry_type.value()));
entry_map.insert("name".into(), json!(entry.name));
entry_map.insert("size".into(), json!(entry.size));
entry_map.insert("modified_time".into(), json!(entry.modified_time));
entries_out.push(entry_map);
}
fd_json.insert("entries".into(), json!(entries_out));
serde_json::to_string(&fd_json).unwrap_or("".into())
}
pub fn make_fd_flutter(id: i32, entries: &Vec<FileEntry>, only_count: bool) -> String {
let mut m = serde_json::Map::new();
m.insert("id".into(), json!(id));
let mut a = vec![];
let mut n: u64 = 0;
for entry in entries {
n += entry.size;
if only_count {
continue;
}
let mut e = serde_json::Map::new();
e.insert("name".into(), json!(entry.name.to_owned()));
let tmp = entry.entry_type.value();
e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp }));
e.insert("time".into(), json!(entry.modified_time as f64));
e.insert("size".into(), json!(entry.size as f64));
a.push(e);
}
if only_count {
m.insert("num_entries".into(), json!(entries.len() as i32));
} else {
m.insert("entries".into(), json!(a));
}
m.insert("total_size".into(), json!(n as f64));
serde_json::to_string(&m).unwrap_or("".into())
}

View File

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

View File

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

100
src/ui.rs
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

671
src/ui_cm_interface.rs Normal file
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;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn check_mouse_time() {
let sender = SENDER.lock().unwrap();
allow_err!(sender.send(ipc::Data::MouseMoveTime(0)));
#[cfg(not(any(target_os = "android", target_os = "ios")))]{
let sender = SENDER.lock().unwrap();
allow_err!(sender.send(ipc::Data::MouseMoveTime(0)));
}
}
pub fn get_connect_status() -> Status {

View File

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