This commit is contained in:
asur4s
2022-09-07 03:52:31 -04:00
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: