mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge branch 'master' of https://github.com/rustdesk/rustdesk
This commit is contained in:
@@ -105,43 +105,30 @@ class MainService : Service() {
|
||||
@Keep
|
||||
fun rustSetByName(name: String, arg1: String, arg2: String) {
|
||||
when (name) {
|
||||
"try_start_without_auth" -> {
|
||||
try {
|
||||
val jsonObject = JSONObject(arg1)
|
||||
val id = jsonObject["id"] as Int
|
||||
val username = jsonObject["name"] as String
|
||||
val peerId = jsonObject["peer_id"] as String
|
||||
val type = if (jsonObject["is_file_transfer"] as Boolean) {
|
||||
translate("File Connection")
|
||||
} else {
|
||||
translate("Screen Connection")
|
||||
}
|
||||
loginRequestNotification(id, type, username, peerId)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
"on_client_authorized" -> {
|
||||
Log.d(logTag, "from rust:on_client_authorized")
|
||||
"add_connection" -> {
|
||||
try {
|
||||
val jsonObject = JSONObject(arg1)
|
||||
val id = jsonObject["id"] as Int
|
||||
val username = jsonObject["name"] as String
|
||||
val peerId = jsonObject["peer_id"] as String
|
||||
val authorized = jsonObject["authorized"] as Boolean
|
||||
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
|
||||
val type = if (isFileTransfer) {
|
||||
translate("File Connection")
|
||||
} else {
|
||||
translate("Screen Connection")
|
||||
}
|
||||
if (!isFileTransfer && !isStart) {
|
||||
startCapture()
|
||||
if (authorized) {
|
||||
if (!isFileTransfer && !isStart) {
|
||||
startCapture()
|
||||
}
|
||||
onClientAuthorizedNotification(id, type, username, peerId)
|
||||
} else {
|
||||
loginRequestNotification(id, type, username, peerId)
|
||||
}
|
||||
onClientAuthorizedNotification(id, type, username, peerId)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
"stop_capture" -> {
|
||||
Log.d(logTag, "from rust:stop_capture")
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:back_button_interceptor/back_button_interceptor.dart';
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -154,7 +155,7 @@ class MyTheme {
|
||||
brightness: Brightness.light,
|
||||
primarySwatch: Colors.blue,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
tabBarTheme: TabBarTheme(
|
||||
tabBarTheme: const TabBarTheme(
|
||||
labelColor: Colors.black87,
|
||||
),
|
||||
splashColor: Colors.transparent,
|
||||
@@ -162,13 +163,14 @@ class MyTheme {
|
||||
).copyWith(
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ColorThemeExtension.light,
|
||||
TabbarTheme.light,
|
||||
],
|
||||
);
|
||||
static ThemeData darkTheme = ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.blue,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
tabBarTheme: TabBarTheme(
|
||||
tabBarTheme: const TabBarTheme(
|
||||
labelColor: Colors.white70,
|
||||
),
|
||||
splashColor: Colors.transparent,
|
||||
@@ -176,12 +178,17 @@ class MyTheme {
|
||||
).copyWith(
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
ColorThemeExtension.dark,
|
||||
TabbarTheme.dark,
|
||||
],
|
||||
);
|
||||
|
||||
static ColorThemeExtension color(BuildContext context) {
|
||||
return Theme.of(context).extension<ColorThemeExtension>()!;
|
||||
}
|
||||
|
||||
static TabbarTheme tabbar(BuildContext context) {
|
||||
return Theme.of(context).extension<TabbarTheme>()!;
|
||||
}
|
||||
}
|
||||
|
||||
bool isDarkTheme() {
|
||||
@@ -340,34 +347,41 @@ class OverlayDialogManager {
|
||||
{bool clickMaskDismiss = false,
|
||||
bool showCancel = true,
|
||||
VoidCallback? onCancel}) {
|
||||
show((setState, close) => CustomAlertDialog(
|
||||
show((setState, close) {
|
||||
cancel() {
|
||||
dismissAll();
|
||||
if (onCancel != null) {
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Container(
|
||||
constraints: BoxConstraints(maxWidth: 240),
|
||||
constraints: const BoxConstraints(maxWidth: 240),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 30),
|
||||
Center(child: CircularProgressIndicator()),
|
||||
SizedBox(height: 20),
|
||||
const SizedBox(height: 30),
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: Text(translate(text),
|
||||
style: TextStyle(fontSize: 15))),
|
||||
SizedBox(height: 20),
|
||||
style: const TextStyle(fontSize: 15))),
|
||||
const SizedBox(height: 20),
|
||||
Offstage(
|
||||
offstage: !showCancel,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
dismissAll();
|
||||
if (onCancel != null) {
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
onPressed: cancel,
|
||||
child: Text(translate('Cancel'),
|
||||
style: TextStyle(color: MyTheme.accent)))))
|
||||
]))));
|
||||
style:
|
||||
const TextStyle(color: MyTheme.accent)))))
|
||||
])),
|
||||
onCancel: showCancel ? cancel : null,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,18 +391,18 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
||||
final entry = OverlayEntry(builder: (_) {
|
||||
return IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment(0.0, 0.8),
|
||||
alignment: const Alignment(0.0, 0.8),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.all(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 18,
|
||||
@@ -403,23 +417,54 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
|
||||
}
|
||||
|
||||
class CustomAlertDialog extends StatelessWidget {
|
||||
CustomAlertDialog(
|
||||
{this.title, required this.content, this.actions, this.contentPadding});
|
||||
const CustomAlertDialog(
|
||||
{Key? key,
|
||||
this.title,
|
||||
required this.content,
|
||||
this.actions,
|
||||
this.contentPadding,
|
||||
this.onSubmit,
|
||||
this.onCancel})
|
||||
: super(key: key);
|
||||
|
||||
final Widget? title;
|
||||
final Widget content;
|
||||
final List<Widget>? actions;
|
||||
final double? contentPadding;
|
||||
final Function()? onSubmit;
|
||||
final Function()? onCancel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: title,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10),
|
||||
content: content,
|
||||
actions: actions,
|
||||
FocusNode focusNode = FocusNode();
|
||||
// request focus if there is no focused FocusNode in the dialog
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!focusNode.hasFocus) focusNode.requestFocus();
|
||||
});
|
||||
return Focus(
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
onKey: (node, key) {
|
||||
if (key.logicalKey == LogicalKeyboardKey.escape) {
|
||||
if (key is RawKeyDownEvent) {
|
||||
onCancel?.call();
|
||||
}
|
||||
return KeyEventResult.handled; // avoid TextField exception on escape
|
||||
} else if (onSubmit != null &&
|
||||
key.logicalKey == LogicalKeyboardKey.enter) {
|
||||
if (key is RawKeyDownEvent) onSubmit?.call();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: AlertDialog(
|
||||
scrollable: true,
|
||||
title: title,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: contentPadding ?? 25, vertical: 10),
|
||||
content: content,
|
||||
actions: actions,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -429,26 +474,28 @@ void msgBox(
|
||||
{bool? hasCancel}) {
|
||||
dialogManager.dismissAll();
|
||||
List<Widget> buttons = [];
|
||||
bool hasOk = false;
|
||||
submit() {
|
||||
dialogManager.dismissAll();
|
||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||
if (!type.contains("custom")) {
|
||||
closeConnection();
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
dialogManager.dismissAll();
|
||||
}
|
||||
|
||||
if (type != "connecting" && type != "success" && !type.contains("nook")) {
|
||||
buttons.insert(
|
||||
0,
|
||||
msgBoxButton(translate('OK'), () {
|
||||
dialogManager.dismissAll();
|
||||
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
|
||||
if (!type.contains("custom")) {
|
||||
closeConnection();
|
||||
}
|
||||
}));
|
||||
hasOk = true;
|
||||
buttons.insert(0, msgBoxButton(translate('OK'), submit));
|
||||
}
|
||||
hasCancel ??= !type.contains("error") &&
|
||||
!type.contains("nocancel") &&
|
||||
type != "restarting";
|
||||
if (hasCancel) {
|
||||
buttons.insert(
|
||||
0,
|
||||
msgBoxButton(translate('Cancel'), () {
|
||||
dialogManager.dismissAll();
|
||||
}));
|
||||
buttons.insert(0, msgBoxButton(translate('Cancel'), cancel));
|
||||
}
|
||||
// TODO: test this button
|
||||
if (type.contains("hasclose")) {
|
||||
@@ -459,9 +506,12 @@ void msgBox(
|
||||
}));
|
||||
}
|
||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: _msgBoxTitle(title),
|
||||
content: Text(translate(text), style: TextStyle(fontSize: 15)),
|
||||
actions: buttons));
|
||||
title: _msgBoxTitle(title),
|
||||
content: Text(translate(text), style: const TextStyle(fontSize: 15)),
|
||||
actions: buttons,
|
||||
onSubmit: hasOk ? submit : null,
|
||||
onCancel: hasCancel == true ? cancel : null,
|
||||
));
|
||||
}
|
||||
|
||||
Widget msgBoxButton(String text, void Function() onPressed) {
|
||||
@@ -479,15 +529,19 @@ Widget msgBoxButton(String text, void Function() onPressed) {
|
||||
Text(translate(text), style: TextStyle(color: MyTheme.accent))));
|
||||
}
|
||||
|
||||
Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21));
|
||||
Widget _msgBoxTitle(String title) =>
|
||||
Text(translate(title), style: TextStyle(fontSize: 21));
|
||||
|
||||
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
|
||||
Widget content, List<Widget> buttons) {
|
||||
Widget content, List<Widget> buttons,
|
||||
{bool hasCancel = true}) {
|
||||
dialogManager.dismissAll();
|
||||
dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: _msgBoxTitle(title),
|
||||
content: content,
|
||||
actions: buttons));
|
||||
title: _msgBoxTitle(title),
|
||||
content: content,
|
||||
actions: buttons,
|
||||
onCancel: hasCancel ? close : null,
|
||||
));
|
||||
}
|
||||
|
||||
Color str2color(String str, [alpha = 0xFF]) {
|
||||
|
||||
@@ -1,4 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// TODO: Divide every 3 number to display ID
|
||||
class IdFormController extends TextEditingController {}
|
||||
class IDTextEditingController extends TextEditingController {
|
||||
IDTextEditingController({String? text}) : super(text: text);
|
||||
|
||||
String get id => trimID(value.text);
|
||||
|
||||
set id(String newID) => text = formatID(newID);
|
||||
}
|
||||
|
||||
class IDTextInputFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
if (newValue.text.isEmpty) {
|
||||
return newValue.copyWith(text: '');
|
||||
} else if (newValue.text.compareTo(oldValue.text) == 0) {
|
||||
return newValue;
|
||||
} else {
|
||||
int selectionIndexFromTheRight =
|
||||
newValue.text.length - newValue.selection.extentOffset;
|
||||
String newID = formatID(newValue.text);
|
||||
return TextEditingValue(
|
||||
text: newID,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: newID.length - selectionIndexFromTheRight,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String formatID(String id) {
|
||||
String id2 = id.replaceAll(' ', '');
|
||||
String newID = '';
|
||||
if (id2.length <= 3) {
|
||||
newID = id2;
|
||||
} else {
|
||||
var n = id2.length;
|
||||
var a = n % 3 != 0 ? n % 3 : 3;
|
||||
newID = id2.substring(0, a);
|
||||
for (var i = a; i < n; i += 3) {
|
||||
newID += " ${id2.substring(i, i + 3)}";
|
||||
}
|
||||
}
|
||||
return newID;
|
||||
}
|
||||
|
||||
String trimID(String id) {
|
||||
return id.replaceAll(' ', '');
|
||||
}
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../consts.dart';
|
||||
import '../models/platform_model.dart';
|
||||
|
||||
class PrivacyModeState {
|
||||
static String tag(String id) => 'privacy_mode_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: tag(id));
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) => Get.delete(tag: tag(id));
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
}
|
||||
|
||||
@@ -18,11 +28,20 @@ class BlockInputState {
|
||||
static String tag(String id) => 'block_input_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: tag(id));
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) => Get.delete(tag: tag(id));
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
}
|
||||
|
||||
@@ -30,11 +49,20 @@ class CurrentDisplayState {
|
||||
static String tag(String id) => 'current_display_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final RxInt state = RxInt(0);
|
||||
Get.put(state, tag: tag(id));
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxInt state = RxInt(0);
|
||||
Get.put(state, tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) => Get.delete(tag: tag(id));
|
||||
static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
|
||||
}
|
||||
|
||||
@@ -85,3 +113,46 @@ class ConnectionTypeState {
|
||||
static ConnectionType find(String id) =>
|
||||
Get.find<ConnectionType>(tag: tag(id));
|
||||
}
|
||||
|
||||
class ShowRemoteCursorState {
|
||||
static String tag(String id) => 'show_remote_cursor_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
final RxBool state = false.obs;
|
||||
Get.put(state, tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
}
|
||||
|
||||
class KeyboardEnabledState {
|
||||
static String tag(String id) => 'keyboard_enabled_$id';
|
||||
|
||||
static void init(String id) {
|
||||
final key = tag(id);
|
||||
if (!Get.isRegistered(tag: key)) {
|
||||
// Server side, default true
|
||||
final RxBool state = true.obs;
|
||||
Get.put(state, tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static void delete(String id) {
|
||||
final key = tag(id);
|
||||
if (Get.isRegistered(tag: key)) {
|
||||
Get.delete(tag: key);
|
||||
}
|
||||
}
|
||||
|
||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/formatter/id_formatter.dart';
|
||||
import '../../mobile/pages/scan_page.dart';
|
||||
import '../../mobile/pages/settings_page.dart';
|
||||
import '../../models/model.dart';
|
||||
@@ -30,7 +31,7 @@ class ConnectionPage extends StatefulWidget {
|
||||
/// State for the connection page.
|
||||
class _ConnectionPageState extends State<ConnectionPage> {
|
||||
/// Controller for the id input bar.
|
||||
final _idController = TextEditingController();
|
||||
final _idController = IDTextEditingController();
|
||||
|
||||
/// Update url. If it's not null, means an update is available.
|
||||
final _updateUrl = '';
|
||||
@@ -43,9 +44,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
if (_idController.text.isEmpty) {
|
||||
() async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
if (lastRemoteId != _idController.text) {
|
||||
if (lastRemoteId != _idController.id) {
|
||||
setState(() {
|
||||
_idController.text = lastRemoteId;
|
||||
_idController.id = lastRemoteId;
|
||||
});
|
||||
}
|
||||
}();
|
||||
@@ -110,7 +111,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
/// Callback for the connect button.
|
||||
/// Connects to the selected peer.
|
||||
void onConnect({bool isFileTransfer = false}) {
|
||||
final id = _idController.text.trim();
|
||||
final id = _idController.id;
|
||||
connect(id, isFileTransfer: isFileTransfer);
|
||||
}
|
||||
|
||||
@@ -166,7 +167,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
});
|
||||
var w = Container(
|
||||
width: 320 + 20 * 2,
|
||||
padding: EdgeInsets.fromLTRB(20, 24, 20, 22),
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 22),
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).bg,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(13)),
|
||||
@@ -178,42 +179,54 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
children: [
|
||||
Text(
|
||||
translate('Control Remote Desktop'),
|
||||
style: TextStyle(fontSize: 19, height: 1),
|
||||
style: const TextStyle(fontSize: 19, height: 1),
|
||||
),
|
||||
],
|
||||
).marginOnly(bottom: 15),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
style: TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontSize: 22,
|
||||
height: 1,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: translate('Enter Remote ID'),
|
||||
hintStyle: TextStyle(
|
||||
color: MyTheme.color(context).placeholder),
|
||||
border: OutlineInputBorder(
|
||||
child: Obx(
|
||||
() => TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: focusNode,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'WorkSans',
|
||||
fontSize: 22,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
cursorColor: MyTheme.color(context).text!,
|
||||
decoration: InputDecoration(
|
||||
hintText: inputFocused.value
|
||||
? null
|
||||
: translate('Enter Remote ID'),
|
||||
hintStyle: TextStyle(
|
||||
color: MyTheme.color(context).placeholder),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(
|
||||
color: MyTheme.color(context).border!)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(
|
||||
color: MyTheme.color(context).border!)),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(
|
||||
color: MyTheme.color(context).placeholder!)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide:
|
||||
BorderSide(color: MyTheme.button, width: 3),
|
||||
),
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 10, vertical: 12)),
|
||||
controller: _idController,
|
||||
onSubmitted: (s) {
|
||||
onConnect();
|
||||
},
|
||||
borderSide:
|
||||
BorderSide(color: MyTheme.button, width: 3),
|
||||
),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 12)),
|
||||
controller: _idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
onSubmitted: (s) {
|
||||
onConnect();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -259,7 +272,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
).marginSymmetric(horizontal: 12),
|
||||
),
|
||||
)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 17,
|
||||
),
|
||||
Obx(
|
||||
@@ -304,7 +317,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
),
|
||||
);
|
||||
return Center(
|
||||
child: Container(constraints: BoxConstraints(maxWidth: 600), child: w));
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 600), child: w));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -654,71 +668,69 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
var field = "";
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
TextEditingController controller = TextEditingController(text: field);
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
field = controller.text.trim();
|
||||
if (field.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final ids = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = ids.join(',');
|
||||
for (final newId in ids) {
|
||||
if (gFFI.abModel.idContainBy(newId)) {
|
||||
continue;
|
||||
}
|
||||
gFFI.abModel.addId(newId);
|
||||
}
|
||||
await gFFI.abModel.updateAb();
|
||||
this.setState(() {});
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Add ID")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("whitelist_sep")),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
field = s;
|
||||
},
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: TextEditingController(text: field),
|
||||
),
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
focusNode: FocusNode()..requestFocus()),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
field = field.trim();
|
||||
if (field.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final ids = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = ids.join(',');
|
||||
for (final newId in ids) {
|
||||
if (gFFI.abModel.idContainBy(newId)) {
|
||||
continue;
|
||||
}
|
||||
gFFI.abModel.addId(newId);
|
||||
}
|
||||
await gFFI.abModel.updateAb();
|
||||
this.setState(() {});
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -727,67 +739,65 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
var field = "";
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
TextEditingController controller = TextEditingController(text: field);
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
field = controller.text.trim();
|
||||
if (field.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = tags.join(',');
|
||||
for (final tag in tags) {
|
||||
gFFI.abModel.addTag(tag);
|
||||
}
|
||||
await gFFI.abModel.updateAb();
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Add Tag")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("whitelist_sep")),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
field = s;
|
||||
},
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: TextEditingController(text: field),
|
||||
controller: controller,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
field = field.trim();
|
||||
if (field.isEmpty) {
|
||||
// pass
|
||||
} else {
|
||||
final tags = field.trim().split(RegExp(r"[\s,;\n]+"));
|
||||
field = tags.join(',');
|
||||
for (final tag in tags) {
|
||||
gFFI.abModel.addTag(tag);
|
||||
}
|
||||
await gFFI.abModel.updateAb();
|
||||
// final currentPeers
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -799,13 +809,23 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||
await gFFI.abModel.updateAb();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Edit Tag")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Wrap(
|
||||
children: tags
|
||||
.map((e) => buildTag(e, selectedTag, onTap: () {
|
||||
@@ -818,26 +838,16 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
.toList(growable: false),
|
||||
),
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||
await gFFI.abModel.updateAb();
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ class ConnectionTabPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
final tabController = Get.put(DesktopTabController());
|
||||
final tabController =
|
||||
Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen));
|
||||
static const IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||
|
||||
@@ -60,6 +61,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
if (call.method == "new_remote_desktop") {
|
||||
final args = jsonDecode(call.arguments);
|
||||
final id = args['id'];
|
||||
ConnectionTypeState.init(id);
|
||||
window_on_top(windowId());
|
||||
ConnectionTypeState.init(id);
|
||||
tabController.add(TabInfo(
|
||||
@@ -81,7 +83,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
return Obx(() => SubWindowDragToResizeArea(
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
@@ -93,15 +94,11 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Obx(() => DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
tabType: DesktopTabType.remoteScreen,
|
||||
showTabBar: fullscreen.isFalse,
|
||||
onClose: () {
|
||||
tabController.clear();
|
||||
},
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
pageViewBuilder: (pageView) {
|
||||
WindowController.fromWindowId(windowId())
|
||||
.setFullscreen(fullscreen.isTrue);
|
||||
|
||||
@@ -6,6 +6,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
|
||||
as mod_menu;
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/server_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
@@ -52,7 +55,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
return Row(
|
||||
children: [
|
||||
buildServerInfo(context),
|
||||
VerticalDivider(
|
||||
const VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
@@ -90,23 +93,23 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
buildIDBoard(BuildContext context) {
|
||||
final model = gFFI.serverModel;
|
||||
return Container(
|
||||
margin: EdgeInsets.only(left: 20, right: 16),
|
||||
height: 52,
|
||||
margin: const EdgeInsets.only(left: 20, right: 11),
|
||||
height: 57,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Container(
|
||||
width: 2,
|
||||
decoration: BoxDecoration(color: MyTheme.accent),
|
||||
),
|
||||
decoration: const BoxDecoration(color: MyTheme.accent),
|
||||
).marginOnly(top: 5),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
padding: const EdgeInsets.only(left: 7),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
SizedBox(
|
||||
height: 25,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -117,7 +120,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: MyTheme.color(context).lightText),
|
||||
),
|
||||
).marginOnly(top: 5),
|
||||
buildPopupMenu(context)
|
||||
],
|
||||
),
|
||||
@@ -132,11 +135,11 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
child: TextFormField(
|
||||
controller: model.serverId,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(bottom: 18),
|
||||
contentPadding: EdgeInsets.only(bottom: 20),
|
||||
),
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
),
|
||||
),
|
||||
@@ -241,7 +244,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
},
|
||||
child: Obx(
|
||||
() => CircleAvatar(
|
||||
radius: 12,
|
||||
radius: 15,
|
||||
backgroundColor: hover.value
|
||||
? MyTheme.color(context).grayBg!
|
||||
: MyTheme.color(context).bg!,
|
||||
@@ -274,7 +277,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
padding: const EdgeInsets.only(left: 7),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -300,7 +303,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.only(bottom: 8),
|
||||
contentPadding: EdgeInsets.only(bottom: 2),
|
||||
),
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
@@ -314,7 +317,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
? MyTheme.color(context).text
|
||||
: Color(0xFFDDDDDD),
|
||||
size: 22,
|
||||
).marginOnly(right: 10, bottom: 8),
|
||||
).marginOnly(right: 8, bottom: 2),
|
||||
),
|
||||
onTap: () => bind.mainUpdateTemporaryPassword(),
|
||||
onHover: (value) => refreshHover.value = value,
|
||||
@@ -422,13 +425,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
color: editHover.value
|
||||
? MyTheme.color(context).text
|
||||
: Color(0xFFDDDDDD))
|
||||
.marginOnly(bottom: 8)));
|
||||
.marginOnly(bottom: 2)));
|
||||
}
|
||||
|
||||
buildTip(BuildContext context) {
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 14),
|
||||
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -639,76 +642,76 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
var newId = "";
|
||||
var msg = "";
|
||||
var isInProgress = false;
|
||||
TextEditingController controller = TextEditingController();
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
newId = controller.text.trim();
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
bind.mainChangeId(newId: newId);
|
||||
});
|
||||
|
||||
var status = await bind.mainGetAsyncStatus();
|
||||
while (status == " ") {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
status = await bind.mainGetAsyncStatus();
|
||||
}
|
||||
if (status.isEmpty) {
|
||||
// ok
|
||||
close();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
msg = translate(status);
|
||||
});
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Change ID")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("id_change_tip")),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text("ID:").marginOnly(bottom: 16.0),
|
||||
SizedBox(
|
||||
const Text("ID:").marginOnly(bottom: 16.0),
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (s) {
|
||||
newId = s;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg)),
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(16),
|
||||
// FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
|
||||
],
|
||||
maxLength: 16,
|
||||
controller: controller,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
msg = "";
|
||||
isInProgress = true;
|
||||
bind.mainChangeId(newId: newId);
|
||||
});
|
||||
|
||||
var status = await bind.mainGetAsyncStatus();
|
||||
while (status == " ") {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
status = await bind.mainGetAsyncStatus();
|
||||
}
|
||||
if (status.isEmpty) {
|
||||
// ok
|
||||
close();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
msg = translate(status);
|
||||
});
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -717,16 +720,16 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
final appName = await bind.mainGetAppName();
|
||||
final license = await bind.mainGetLicense();
|
||||
final version = await bind.mainGetVersion();
|
||||
final linkStyle = TextStyle(decoration: TextDecoration.underline);
|
||||
const linkStyle = TextStyle(decoration: TextDecoration.underline);
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text("About $appName"),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Text("Version: $version").marginSymmetric(vertical: 4.0),
|
||||
@@ -734,7 +737,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
onTap: () {
|
||||
launchUrlString("https://rustdesk.com/privacy");
|
||||
},
|
||||
child: Text(
|
||||
child: const Text(
|
||||
"Privacy Statement",
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
@@ -742,13 +745,14 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
onTap: () {
|
||||
launchUrlString("https://rustdesk.com");
|
||||
},
|
||||
child: Text(
|
||||
child: const Text(
|
||||
"Website",
|
||||
style: linkStyle,
|
||||
).marginSymmetric(vertical: 4.0)),
|
||||
Container(
|
||||
decoration: BoxDecoration(color: Color(0xFF2c8cff)),
|
||||
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8),
|
||||
decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -757,9 +761,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
children: [
|
||||
Text(
|
||||
"Copyright © 2022 Purslane Ltd.\n$license",
|
||||
style: TextStyle(color: Colors.white),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
const Text(
|
||||
"Made with heart in this chaotic world!",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
@@ -775,12 +779,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: close,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -812,118 +814,124 @@ Future<bool> loginDialog() async {
|
||||
var isInProgress = false;
|
||||
var completer = Completer<bool>();
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
userNameMsg = "";
|
||||
passMsg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
cancel() {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
userName = userContontroller.text;
|
||||
pass = pwdController.text;
|
||||
if (userName.isEmpty) {
|
||||
userNameMsg = translate("Username missed");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (pass.isEmpty) {
|
||||
passMsg = translate("Password missed");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final resp = await gFFI.userModel.login(userName, pass);
|
||||
if (resp.containsKey('error')) {
|
||||
passMsg = resp['error'];
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
// {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w,
|
||||
// token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}}
|
||||
debugPrint("$resp");
|
||||
completer.complete(true);
|
||||
} catch (err) {
|
||||
// ignore: avoid_print
|
||||
print(err.toString());
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
completer.complete(false);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Login")),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.start,
|
||||
).marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
|
||||
controller: userContontroller,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Password')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: passMsg.isNotEmpty ? passMsg : null),
|
||||
controller: pwdController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
completer.complete(false);
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
userNameMsg = "";
|
||||
passMsg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
final cancel = () {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
};
|
||||
userName = userContontroller.text;
|
||||
pass = pwdController.text;
|
||||
if (userName.isEmpty) {
|
||||
userNameMsg = translate("Username missed");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (pass.isEmpty) {
|
||||
passMsg = translate("Password missed");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final resp = await gFFI.userModel.login(userName, pass);
|
||||
if (resp.containsKey('error')) {
|
||||
passMsg = resp['error'];
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
// {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w,
|
||||
// token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}}
|
||||
debugPrint("$resp");
|
||||
completer.complete(true);
|
||||
} catch (err) {
|
||||
print(err.toString());
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: cancel, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
return completer.future;
|
||||
@@ -937,55 +945,78 @@ void setPasswordDialog() async {
|
||||
var errMsg1 = "";
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
setState(() {
|
||||
errMsg0 = "";
|
||||
errMsg1 = "";
|
||||
});
|
||||
final pass = p0.text.trim();
|
||||
if (pass.length < 6) {
|
||||
setState(() {
|
||||
errMsg0 = translate("Too short, at least 6 characters.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (p1.text.trim() != pass) {
|
||||
setState(() {
|
||||
errMsg1 = translate("The confirmation is not identical.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
bind.mainSetPermanentPassword(password: pass);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Set Password")),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text(
|
||||
"${translate('Password')}:",
|
||||
textAlign: TextAlign.start,
|
||||
).marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
|
||||
controller: p0,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Confirmation')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
|
||||
controller: p1,
|
||||
),
|
||||
@@ -996,35 +1027,11 @@ void setPasswordDialog() async {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
errMsg0 = "";
|
||||
errMsg1 = "";
|
||||
});
|
||||
final pass = p0.text.trim();
|
||||
if (pass.length < 6) {
|
||||
setState(() {
|
||||
errMsg0 = translate("Too short, at least 6 characters.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (p1.text.trim() != pass) {
|
||||
setState(() {
|
||||
errMsg1 = translate("The confirmation is not identical.");
|
||||
});
|
||||
return;
|
||||
}
|
||||
bind.mainSetPermanentPassword(password: pass);
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
|
||||
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp),
|
||||
_TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp),
|
||||
_TabInfo('Connection', Icons.link_outlined, Icons.link_sharp),
|
||||
_TabInfo('About RustDesk', Icons.info_outline, Icons.info_sharp)
|
||||
_TabInfo('About', Icons.info_outline, Icons.info_sharp)
|
||||
];
|
||||
|
||||
late PageController controller;
|
||||
@@ -714,7 +714,7 @@ class _AboutState extends State<_About> {
|
||||
],
|
||||
).marginOnly(left: _kContentHMargin)
|
||||
]),
|
||||
]).marginOnly(left: _kCardLeftMargin);
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1038,52 +1038,117 @@ void changeServer() async {
|
||||
var keyController = TextEditingController(text: key);
|
||||
|
||||
var isInProgress = false;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
[idServerMsg, relayServerMsg, apiServerMsg].forEach((element) {
|
||||
element = "";
|
||||
});
|
||||
isInProgress = true;
|
||||
});
|
||||
cancel() {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
idServer = idController.text.trim();
|
||||
relayServer = relayController.text.trim();
|
||||
apiServer = apiController.text.trim().toLowerCase();
|
||||
key = keyController.text.trim();
|
||||
|
||||
if (idServer.isNotEmpty) {
|
||||
idServerMsg =
|
||||
translate(await bind.mainTestIfValidServer(server: idServer));
|
||||
if (idServerMsg.isEmpty) {
|
||||
oldOptions['custom-rendezvous-server'] = idServer;
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['custom-rendezvous-server'] = "";
|
||||
}
|
||||
|
||||
if (relayServer.isNotEmpty) {
|
||||
relayServerMsg =
|
||||
translate(await bind.mainTestIfValidServer(server: relayServer));
|
||||
if (relayServerMsg.isEmpty) {
|
||||
oldOptions['relay-server'] = relayServer;
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['relay-server'] = "";
|
||||
}
|
||||
|
||||
if (apiServer.isNotEmpty) {
|
||||
if (apiServer.startsWith('http://') ||
|
||||
apiServer.startsWith("https://")) {
|
||||
oldOptions['api-server'] = apiServer;
|
||||
return;
|
||||
} else {
|
||||
apiServerMsg = translate("invalid_http");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['api-server'] = "";
|
||||
}
|
||||
// ok
|
||||
oldOptions['key'] = key;
|
||||
await bind.mainSetOptions(json: jsonEncode(oldOptions));
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("ID/Relay Server")),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('ID Server')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
||||
controller: idController,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Relay Server')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText:
|
||||
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
||||
controller: relayController,
|
||||
@@ -1091,22 +1156,22 @@ void changeServer() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('API Server')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText:
|
||||
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
||||
controller: apiController,
|
||||
@@ -1114,21 +1179,21 @@ void changeServer() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child:
|
||||
Text("${translate('Key')}:").marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: keyController,
|
||||
@@ -1136,83 +1201,20 @@ void changeServer() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
[idServerMsg, relayServerMsg, apiServerMsg].forEach((element) {
|
||||
element = "";
|
||||
});
|
||||
isInProgress = true;
|
||||
});
|
||||
final cancel = () {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
};
|
||||
idServer = idController.text.trim();
|
||||
relayServer = relayController.text.trim();
|
||||
apiServer = apiController.text.trim().toLowerCase();
|
||||
key = keyController.text.trim();
|
||||
|
||||
if (idServer.isNotEmpty) {
|
||||
idServerMsg = translate(
|
||||
await bind.mainTestIfValidServer(server: idServer));
|
||||
if (idServerMsg.isEmpty) {
|
||||
oldOptions['custom-rendezvous-server'] = idServer;
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['custom-rendezvous-server'] = "";
|
||||
}
|
||||
|
||||
if (relayServer.isNotEmpty) {
|
||||
relayServerMsg = translate(
|
||||
await bind.mainTestIfValidServer(server: relayServer));
|
||||
if (relayServerMsg.isEmpty) {
|
||||
oldOptions['relay-server'] = relayServer;
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['relay-server'] = "";
|
||||
}
|
||||
|
||||
if (apiServer.isNotEmpty) {
|
||||
if (apiServer.startsWith('http://') ||
|
||||
apiServer.startsWith("https://")) {
|
||||
oldOptions['api-server'] = apiServer;
|
||||
return;
|
||||
} else {
|
||||
apiServerMsg = translate("invalid_http");
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
oldOptions['api-server'] = "";
|
||||
}
|
||||
// ok
|
||||
oldOptions['key'] = key;
|
||||
await bind.mainSetOptions(json: jsonEncode(oldOptions));
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1231,27 +1233,28 @@ void changeWhiteList() async {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("whitelist_sep")),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
),
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: msg.isEmpty ? null : translate(msg),
|
||||
),
|
||||
controller: controller,
|
||||
focusNode: FocusNode()..requestFocus()),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -1277,7 +1280,7 @@ void changeWhiteList() async {
|
||||
final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$");
|
||||
for (final ip in ips) {
|
||||
if (!ipMatch.hasMatch(ip)) {
|
||||
msg = translate("Invalid IP") + " $ip";
|
||||
msg = "${translate("Invalid IP")} $ip";
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
@@ -1292,6 +1295,7 @@ void changeWhiteList() async {
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1314,50 +1318,80 @@ void changeSocks5Proxy() async {
|
||||
|
||||
var isInProgress = false;
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
proxyMsg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
cancel() {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
proxy = proxyController.text.trim();
|
||||
username = userController.text.trim();
|
||||
password = pwdController.text.trim();
|
||||
|
||||
if (proxy.isNotEmpty) {
|
||||
proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy));
|
||||
if (proxyMsg.isEmpty) {
|
||||
// ignore
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
await bind.mainSetSocks(
|
||||
proxy: proxy, username: username, password: password);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Socks5 Proxy")),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Hostname')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
||||
controller: proxyController,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Username')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: userController,
|
||||
@@ -1365,21 +1399,21 @@ void changeSocks5Proxy() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Password')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: pwdController,
|
||||
@@ -1387,50 +1421,20 @@ void changeSocks5Proxy() async {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
proxyMsg = "";
|
||||
isInProgress = true;
|
||||
});
|
||||
final cancel = () {
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
};
|
||||
proxy = proxyController.text.trim();
|
||||
username = userController.text.trim();
|
||||
password = pwdController.text.trim();
|
||||
|
||||
if (proxy.isNotEmpty) {
|
||||
proxyMsg =
|
||||
translate(await bind.mainTestIfValidServer(server: proxy));
|
||||
if (proxyMsg.isEmpty) {
|
||||
// ignore
|
||||
} else {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
await bind.mainSetSocks(
|
||||
proxy: proxy, username: username, password: password);
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class DesktopTabPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
final tabController = DesktopTabController();
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -37,26 +37,27 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
RxBool fullscreen = false.obs;
|
||||
Get.put(fullscreen, tag: 'fullscreen');
|
||||
return Obx(() => DragToResizeArea(
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
tabType: DesktopTabType.main,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
onTap: onAddSetting,
|
||||
is_close: false,
|
||||
),
|
||||
)),
|
||||
),
|
||||
));
|
||||
resizeEdgeSize: fullscreen.value ? 1.0 : 8.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Overlay(initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
tail: ActionIcon(
|
||||
message: 'Settings',
|
||||
icon: IconFont.menu,
|
||||
onTap: onAddSetting,
|
||||
isClose: false,
|
||||
),
|
||||
));
|
||||
})
|
||||
]),
|
||||
)));
|
||||
}
|
||||
|
||||
void onAddSetting() {
|
||||
|
||||
@@ -59,8 +59,11 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
super.initState();
|
||||
_ffi = FFI();
|
||||
_ffi.connect(widget.id, isFileTransfer: true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_ffi.dialogManager
|
||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||
});
|
||||
Get.put(_ffi, tag: 'ft_${widget.id}');
|
||||
// _ffi.ffiModel.updateEventListener(widget.id);
|
||||
if (!Platform.isLinux) {
|
||||
Wakelock.enable();
|
||||
}
|
||||
@@ -117,7 +120,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
|
||||
Widget menu({bool isLocal = false}) {
|
||||
return PopupMenuButton<String>(
|
||||
icon: Icon(Icons.more_vert),
|
||||
icon: const Icon(Icons.more_vert),
|
||||
splashRadius: 20,
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
@@ -413,6 +417,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
/// watch transfer status
|
||||
Widget statusList() {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size(200, double.infinity),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@@ -429,8 +434,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
children: [
|
||||
Transform.rotate(
|
||||
angle: item.isRemote ? pi : 0,
|
||||
child: Icon(Icons.send)),
|
||||
SizedBox(
|
||||
child: const Icon(Icons.send)),
|
||||
const SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
@@ -441,7 +446,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Tooltip(
|
||||
message: item.jobName,
|
||||
child: Text(
|
||||
'${item.jobName}',
|
||||
item.jobName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
@@ -455,7 +460,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
offstage:
|
||||
item.state != JobState.inProgress,
|
||||
child: Text(
|
||||
'${readableFileSize(item.speed) + "/s"} ')),
|
||||
'${"${readableFileSize(item.speed)}/s"} ')),
|
||||
Offstage(
|
||||
offstage: item.totalSize <= 0,
|
||||
child: Text(
|
||||
@@ -475,10 +480,12 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
onPressed: () {
|
||||
model.resumeJob(item.id);
|
||||
},
|
||||
icon: Icon(Icons.restart_alt_rounded)),
|
||||
splashRadius: 20,
|
||||
icon: const Icon(Icons.restart_alt_rounded)),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
icon: const Icon(Icons.delete),
|
||||
splashRadius: 20,
|
||||
onPressed: () {
|
||||
model.jobTable.removeAt(index);
|
||||
model.cancelJob(item.id);
|
||||
@@ -500,8 +507,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
itemCount: model.jobTable.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
preferredSize: Size(200, double.infinity));
|
||||
));
|
||||
}
|
||||
|
||||
goBack({bool? isLocal}) {
|
||||
@@ -551,12 +557,15 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
model.goHome(isLocal: isLocal);
|
||||
},
|
||||
icon: Icon(Icons.home_outlined)),
|
||||
onPressed: () {
|
||||
model.goHome(isLocal: isLocal);
|
||||
},
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
splashRadius: 20,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_upward),
|
||||
icon: const Icon(Icons.arrow_upward),
|
||||
splashRadius: 20,
|
||||
onPressed: () {
|
||||
goBack(isLocal: isLocal);
|
||||
},
|
||||
@@ -622,13 +631,15 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
),
|
||||
))
|
||||
],
|
||||
child: Icon(Icons.search),
|
||||
splashRadius: 20,
|
||||
child: const Icon(Icons.search),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
model.refresh(isLocal: isLocal);
|
||||
},
|
||||
icon: Icon(Icons.refresh)),
|
||||
splashRadius: 20,
|
||||
icon: const Icon(Icons.refresh)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
@@ -642,47 +653,52 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final name = TextEditingController();
|
||||
_ffi.dialogManager
|
||||
.show((setState, close) => CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate(
|
||||
"Please enter the folder name"),
|
||||
),
|
||||
controller: name,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(false),
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
model.createDir(
|
||||
PathUtil.join(
|
||||
model
|
||||
.getCurrentDir(
|
||||
isLocal)
|
||||
.path,
|
||||
name.value.text,
|
||||
model.getCurrentIsWindows(
|
||||
isLocal)),
|
||||
isLocal: isLocal);
|
||||
close();
|
||||
}
|
||||
},
|
||||
child: Text(translate("OK")))
|
||||
]));
|
||||
_ffi.dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
if (name.value.text.isNotEmpty) {
|
||||
model.createDir(
|
||||
PathUtil.join(
|
||||
model.getCurrentDir(isLocal).path,
|
||||
name.value.text,
|
||||
model.getCurrentIsWindows(isLocal)),
|
||||
isLocal: isLocal);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
cancel() => close(false);
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Create Folder")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translate(
|
||||
"Please enter the folder name"),
|
||||
),
|
||||
controller: name,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: cancel,
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: submit,
|
||||
child: Text(translate("OK")))
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
});
|
||||
},
|
||||
icon: Icon(Icons.create_new_folder_outlined)),
|
||||
splashRadius: 20,
|
||||
icon: const Icon(Icons.create_new_folder_outlined)),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final items = isLocal
|
||||
@@ -691,7 +707,8 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
await (model.removeAction(items, isLocal: isLocal));
|
||||
items.clear();
|
||||
},
|
||||
icon: Icon(Icons.delete_forever_outlined)),
|
||||
splashRadius: 20,
|
||||
icon: const Icon(Icons.delete_forever_outlined)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -703,7 +720,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
},
|
||||
icon: Transform.rotate(
|
||||
angle: isLocal ? 0 : pi,
|
||||
child: Icon(
|
||||
child: const Icon(
|
||||
Icons.send,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -25,7 +25,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
static final IconData unselectedIcon = Icons.file_copy_outlined;
|
||||
|
||||
_FileManagerTabPageState(Map<String, dynamic> params) {
|
||||
Get.put(DesktopTabController());
|
||||
Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer));
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@@ -62,8 +62,6 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme =
|
||||
isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light();
|
||||
return SubWindowDragToResizeArea(
|
||||
windowId: windowId(),
|
||||
child: Container(
|
||||
@@ -73,14 +71,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
tabType: DesktopTabType.fileTransfer,
|
||||
onClose: () {
|
||||
tabController.clear();
|
||||
},
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -70,38 +70,45 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).grayBg,
|
||||
body: FutureBuilder(future: () async {
|
||||
if (!isRdp) {
|
||||
refreshTunnelConfig();
|
||||
}
|
||||
}(), builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 20, color: MyTheme.color(context).grayBg!)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
buildPrompt(context),
|
||||
Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).bg,
|
||||
border: Border.all(width: 1, color: MyTheme.border)),
|
||||
child:
|
||||
widget.isRDP ? buildRdp(context) : buildTunnel(context),
|
||||
),
|
||||
return Overlay(initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).grayBg,
|
||||
body: FutureBuilder(future: () async {
|
||||
if (!isRdp) {
|
||||
refreshTunnelConfig();
|
||||
}
|
||||
}(), builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 20, color: MyTheme.color(context).grayBg!)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
buildPrompt(context),
|
||||
Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.color(context).bg,
|
||||
border:
|
||||
Border.all(width: 1, color: MyTheme.border)),
|
||||
child: widget.isRDP
|
||||
? buildRdp(context)
|
||||
: buildTunnel(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Offstage();
|
||||
}),
|
||||
);
|
||||
);
|
||||
}
|
||||
return const Offstage();
|
||||
}),
|
||||
);
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
buildPrompt(BuildContext context) {
|
||||
|
||||
@@ -18,7 +18,7 @@ class PortForwardTabPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
final tabController = Get.put(DesktopTabController());
|
||||
late final DesktopTabController tabController;
|
||||
late final bool isRDP;
|
||||
|
||||
static const IconData selectedIcon = Icons.forward_sharp;
|
||||
@@ -26,6 +26,8 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
|
||||
_PortForwardTabPageState(Map<String, dynamic> params) {
|
||||
isRDP = params['isRDP'];
|
||||
tabController = Get.put(DesktopTabController(
|
||||
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward));
|
||||
tabController.add(TabInfo(
|
||||
key: params['id'],
|
||||
label: params['id'],
|
||||
@@ -67,7 +69,6 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
|
||||
return SubWindowDragToResizeArea(
|
||||
windowId: windowId(),
|
||||
child: Container(
|
||||
@@ -77,14 +78,10 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: DesktopTab(
|
||||
controller: tabController,
|
||||
theme: theme,
|
||||
tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward,
|
||||
onClose: () {
|
||||
tabController.clear();
|
||||
},
|
||||
tail: AddButton(
|
||||
theme: theme,
|
||||
).paddingOnly(left: 10),
|
||||
tail: AddButton().paddingOnly(left: 10),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ import '../../mobile/widgets/dialog.dart';
|
||||
import '../../mobile/widgets/overlay.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/chat_model.dart';
|
||||
import '../../common/shared_state.dart';
|
||||
|
||||
final initText = '\1' * 1024;
|
||||
@@ -39,10 +38,11 @@ class RemotePage extends StatefulWidget {
|
||||
class _RemotePageState extends State<RemotePage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
Timer? _timer;
|
||||
bool _showBar = !isWebDesktop;
|
||||
String _value = '';
|
||||
String keyboardMode = "legacy";
|
||||
final _cursorOverImage = false.obs;
|
||||
late RxBool _showRemoteCursor;
|
||||
late RxBool _keyboardEnabled;
|
||||
|
||||
final FocusNode _mobileFocusNode = FocusNode();
|
||||
final FocusNode _physicalFocusNode = FocusNode();
|
||||
@@ -59,17 +59,24 @@ class _RemotePageState extends State<RemotePage>
|
||||
PrivacyModeState.init(id);
|
||||
BlockInputState.init(id);
|
||||
CurrentDisplayState.init(id);
|
||||
KeyboardEnabledState.init(id);
|
||||
ShowRemoteCursorState.init(id);
|
||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||
}
|
||||
|
||||
void _removeStates(String id) {
|
||||
PrivacyModeState.delete(id);
|
||||
BlockInputState.delete(id);
|
||||
CurrentDisplayState.delete(id);
|
||||
ShowRemoteCursorState.delete(id);
|
||||
KeyboardEnabledState.delete(id);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initStates(widget.id);
|
||||
_ffi = FFI();
|
||||
_updateTabBarHeight();
|
||||
Get.put(_ffi, tag: widget.id);
|
||||
@@ -87,7 +94,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.listenToMouse(true);
|
||||
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
|
||||
// WindowManager.instance.addListener(this);
|
||||
_initStates(widget.id);
|
||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||
id: widget.id, arg: 'show-remote-cursor');
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -132,7 +140,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
common < oldValue.length &&
|
||||
common < newValue.length &&
|
||||
newValue[common] == oldValue[common];
|
||||
++common);
|
||||
++common) {}
|
||||
for (i = 0; i < oldValue.length - common; ++i) {
|
||||
_ffi.inputKey('VK_BACK');
|
||||
}
|
||||
@@ -146,8 +154,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (oldValue.length > 0 &&
|
||||
newValue.length > 0 &&
|
||||
if (oldValue.isNotEmpty &&
|
||||
newValue.isNotEmpty &&
|
||||
oldValue[0] == '\1' &&
|
||||
newValue[0] != '\1') {
|
||||
// clipboard
|
||||
@@ -156,7 +164,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (newValue.length == oldValue.length) {
|
||||
// ?
|
||||
} else if (newValue.length < oldValue.length) {
|
||||
final char = 'VK_BACK';
|
||||
const char = 'VK_BACK';
|
||||
_ffi.inputKey(char);
|
||||
} else {
|
||||
final content = newValue.substring(oldValue.length);
|
||||
@@ -200,25 +208,9 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.inputKey(label, down: down, press: press ?? false);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context, FfiModel ffiModel) {
|
||||
final hasDisplays = ffiModel.pi.displays.length > 0;
|
||||
final keyboard = ffiModel.permissions['keyboard'] != false;
|
||||
Widget buildBody(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
// resizeToAvoidBottomInset: true,
|
||||
// floatingActionButton: _showBar
|
||||
// ? null
|
||||
// : FloatingActionButton(
|
||||
// mini: true,
|
||||
// child: Icon(Icons.expand_less),
|
||||
// backgroundColor: MyTheme.accent,
|
||||
// onPressed: () {
|
||||
// setState(() {
|
||||
// _showBar = !_showBar;
|
||||
// });
|
||||
// }),
|
||||
// bottomNavigationBar:
|
||||
// _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
|
||||
body: Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
@@ -226,8 +218,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: getRawPointerAndKeyBody(
|
||||
getBodyForDesktop(context, keyboard)));
|
||||
child: getRawPointerAndKeyBody(getBodyForDesktop(context)));
|
||||
})
|
||||
],
|
||||
));
|
||||
@@ -242,16 +233,12 @@ class _RemotePageState extends State<RemotePage>
|
||||
clientClose(_ffi.dialogManager);
|
||||
return false;
|
||||
},
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: _ffi.ffiModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.imageModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.cursorModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.canvasModel),
|
||||
],
|
||||
child: Consumer<FfiModel>(
|
||||
builder: (context, ffiModel, _child) =>
|
||||
buildBody(context, ffiModel))));
|
||||
child: MultiProvider(providers: [
|
||||
ChangeNotifierProvider.value(value: _ffi.ffiModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.imageModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.cursorModel),
|
||||
ChangeNotifierProvider.value(value: _ffi.canvasModel),
|
||||
], child: buildBody(context)));
|
||||
}
|
||||
|
||||
KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) {
|
||||
@@ -332,7 +319,8 @@ class _RemotePageState extends State<RemotePage>
|
||||
key == LogicalKeyboardKey.shiftLeft) {
|
||||
_ffi.shift = false;
|
||||
} else if (key == LogicalKeyboardKey.metaLeft ||
|
||||
key == LogicalKeyboardKey.metaRight || key == LogicalKeyboardKey.superKey) {
|
||||
key == LogicalKeyboardKey.metaRight ||
|
||||
key == LogicalKeyboardKey.superKey) {
|
||||
_ffi.command = false;
|
||||
}
|
||||
sendRawKey(e);
|
||||
@@ -340,116 +328,17 @@ class _RemotePageState extends State<RemotePage>
|
||||
}
|
||||
|
||||
Widget getRawPointerAndKeyBody(Widget child) {
|
||||
return Consumer<FfiModel>(
|
||||
builder: (context, FfiModel, _child) => MouseRegion(
|
||||
cursor: FfiModel.permissions['keyboard'] != false
|
||||
? SystemMouseCursors.none
|
||||
: MouseCursor.defer,
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
canRequestFocus: true,
|
||||
focusNode: _physicalFocusNode,
|
||||
onFocusChange: (bool v) {
|
||||
_imageFocused = v;
|
||||
},
|
||||
onKey: handleRawKeyEvent,
|
||||
child: child))));
|
||||
}
|
||||
|
||||
Widget? getBottomAppBar(FfiModel ffiModel) {
|
||||
final RxBool fullscreen = Get.find(tag: 'fullscreen');
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: BottomAppBar(
|
||||
elevation: 10,
|
||||
color: MyTheme.accent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
clientClose(_ffi.dialogManager);
|
||||
},
|
||||
)
|
||||
] +
|
||||
<Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.tv),
|
||||
onPressed: () {
|
||||
_ffi.dialogManager.dismissAll();
|
||||
showOptions(widget.id);
|
||||
},
|
||||
)
|
||||
] +
|
||||
(isWebDesktop
|
||||
? []
|
||||
: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(fullscreen.isTrue
|
||||
? Icons.fullscreen
|
||||
: Icons.close_fullscreen),
|
||||
onPressed: () {
|
||||
fullscreen.value = !fullscreen.value;
|
||||
},
|
||||
)
|
||||
]) +
|
||||
(isWebDesktop
|
||||
? []
|
||||
: _ffi.ffiModel.isPeerAndroid
|
||||
? [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.build),
|
||||
onPressed: () {
|
||||
if (mobileActionsOverlayEntry == null) {
|
||||
showMobileActionsOverlay();
|
||||
} else {
|
||||
hideMobileActionsOverlay();
|
||||
}
|
||||
},
|
||||
)
|
||||
]
|
||||
: []) +
|
||||
(isWeb
|
||||
? []
|
||||
: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.message),
|
||||
onPressed: () {
|
||||
_ffi.chatModel
|
||||
.changeCurrentID(ChatModel.clientModeID);
|
||||
_ffi.chatModel.toggleChatOverlay();
|
||||
},
|
||||
)
|
||||
]) +
|
||||
[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
showActions(widget.id, ffiModel);
|
||||
},
|
||||
),
|
||||
]),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.expand_more),
|
||||
onPressed: () {
|
||||
setState(() => _showBar = !_showBar);
|
||||
}),
|
||||
],
|
||||
),
|
||||
));
|
||||
return FocusScope(
|
||||
autofocus: true,
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
canRequestFocus: true,
|
||||
focusNode: _physicalFocusNode,
|
||||
onFocusChange: (bool v) {
|
||||
_imageFocused = v;
|
||||
},
|
||||
onKey: handleRawKeyEvent,
|
||||
child: child));
|
||||
}
|
||||
|
||||
/// touchMode only:
|
||||
@@ -461,7 +350,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
/// mouseMode only:
|
||||
/// DoubleFiner -> right click
|
||||
/// HoldDrag -> left drag
|
||||
|
||||
void _onPointHoverImage(PointerHoverEvent e) {
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
if (!_isPhysicalMouse) {
|
||||
@@ -509,12 +397,16 @@ class _RemotePageState extends State<RemotePage>
|
||||
if (e is PointerScrollEvent) {
|
||||
var dx = e.scrollDelta.dx.toInt();
|
||||
var dy = e.scrollDelta.dy.toInt();
|
||||
if (dx > 0)
|
||||
if (dx > 0) {
|
||||
dx = -1;
|
||||
else if (dx < 0) dx = 1;
|
||||
if (dy > 0)
|
||||
} else if (dx < 0) {
|
||||
dx = 1;
|
||||
}
|
||||
if (dy > 0) {
|
||||
dy = -1;
|
||||
else if (dy < 0) dy = 1;
|
||||
} else if (dy < 0) {
|
||||
dy = 1;
|
||||
}
|
||||
bind.sessionSendMouse(
|
||||
id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}');
|
||||
}
|
||||
@@ -544,32 +436,30 @@ class _RemotePageState extends State<RemotePage>
|
||||
MouseRegion(onEnter: enterView, onExit: leaveView, child: child));
|
||||
}
|
||||
|
||||
Widget getBodyForDesktop(BuildContext context, bool keyboard) {
|
||||
Widget getBodyForDesktop(BuildContext context) {
|
||||
var paints = <Widget>[
|
||||
MouseRegion(onEnter: (evt) {
|
||||
bind.hostStopSystemKeyPropagate(stopped: false);
|
||||
}, onExit: (evt) {
|
||||
bind.hostStopSystemKeyPropagate(stopped: true);
|
||||
}, child: Container(
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||
});
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
listenerBuilder: _buildImageListener,
|
||||
);
|
||||
}),
|
||||
))
|
||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
||||
});
|
||||
return ImagePaint(
|
||||
id: widget.id,
|
||||
cursorOverImage: _cursorOverImage,
|
||||
keyboardEnabled: _keyboardEnabled,
|
||||
listenerBuilder: _buildImageListener,
|
||||
);
|
||||
}))
|
||||
];
|
||||
final cursor = bind.sessionGetToggleOptionSync(
|
||||
id: widget.id, arg: 'show-remote-cursor');
|
||||
if (keyboard || cursor) {
|
||||
paints.add(CursorPaint(
|
||||
id: widget.id,
|
||||
));
|
||||
}
|
||||
|
||||
paints.add(Obx(() => Visibility(
|
||||
visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue,
|
||||
child: CursorPaint(
|
||||
id: widget.id,
|
||||
))));
|
||||
paints.add(QualityMonitor(_ffi.qualityMonitorModel));
|
||||
paints.add(RemoteMenubar(
|
||||
id: widget.id,
|
||||
@@ -601,106 +491,6 @@ class _RemotePageState extends State<RemotePage>
|
||||
return out;
|
||||
}
|
||||
|
||||
void showActions(String id, FfiModel ffiModel) async {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final x = 120.0;
|
||||
final y = size.height - super.widget.tabBarHeight;
|
||||
final more = <PopupMenuItem<String>>[];
|
||||
final pi = _ffi.ffiModel.pi;
|
||||
final perms = _ffi.ffiModel.permissions;
|
||||
if (pi.version.isNotEmpty) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Refresh')), value: 'refresh'));
|
||||
}
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Row(
|
||||
children: ([
|
||||
Text(translate('OS Password')),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
showSetOSPassword(widget.id, false, _ffi.dialogManager);
|
||||
},
|
||||
child: Icon(Icons.edit, color: MyTheme.accent),
|
||||
)
|
||||
])),
|
||||
value: 'enter_os_password'));
|
||||
if (!isWebDesktop) {
|
||||
if (perms['keyboard'] != false && perms['clipboard'] != false) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Paste')), value: 'paste'));
|
||||
}
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
|
||||
}
|
||||
if (perms['keyboard'] != false) {
|
||||
if (pi.platform == 'Linux' || pi.sasEnabled) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Insert') + ' Ctrl + Alt + Del'),
|
||||
value: 'cad'));
|
||||
}
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Insert Lock')), value: 'lock'));
|
||||
if (pi.platform == 'Windows' &&
|
||||
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
|
||||
true) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate(
|
||||
(ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')),
|
||||
value: 'block-input'));
|
||||
}
|
||||
}
|
||||
if (gFFI.ffiModel.permissions["restart"] != false &&
|
||||
(pi.platform == "Linux" ||
|
||||
pi.platform == "Windows" ||
|
||||
pi.platform == "Mac OS")) {
|
||||
more.add(PopupMenuItem<String>(
|
||||
child: Text(translate('Restart Remote Device')), value: 'restart'));
|
||||
}
|
||||
() async {
|
||||
var value = await showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||
items: more,
|
||||
elevation: 8,
|
||||
);
|
||||
if (value == 'cad') {
|
||||
bind.sessionCtrlAltDel(id: widget.id);
|
||||
} else if (value == 'lock') {
|
||||
bind.sessionLockScreen(id: widget.id);
|
||||
} else if (value == 'block-input') {
|
||||
bind.sessionToggleOption(
|
||||
id: widget.id,
|
||||
value: (_ffi.ffiModel.inputBlocked ? 'un' : '') + 'block-input');
|
||||
_ffi.ffiModel.inputBlocked = !_ffi.ffiModel.inputBlocked;
|
||||
} else if (value == 'refresh') {
|
||||
bind.sessionRefresh(id: widget.id);
|
||||
} else if (value == 'paste') {
|
||||
() async {
|
||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
bind.sessionInputString(id: widget.id, value: data.text ?? "");
|
||||
}
|
||||
}();
|
||||
} else if (value == 'enter_os_password') {
|
||||
// FIXME:
|
||||
// TODO icon diff
|
||||
// null means no session of id
|
||||
// empty string means no password
|
||||
var password = await bind.sessionGetOption(id: id, arg: "os-password");
|
||||
if (password != null) {
|
||||
bind.sessionInputOsPassword(id: widget.id, value: password);
|
||||
} else {
|
||||
showSetOSPassword(widget.id, true, _ffi.dialogManager);
|
||||
}
|
||||
} else if (value == 'reset_canvas') {
|
||||
_ffi.cursorModel.reset();
|
||||
} else if (value == 'restart') {
|
||||
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEvent(String eventName) {
|
||||
print("window event: $eventName");
|
||||
@@ -709,7 +499,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.canvasModel.updateViewStyle();
|
||||
break;
|
||||
case 'maximize':
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_ffi.canvasModel.updateViewStyle();
|
||||
});
|
||||
break;
|
||||
@@ -723,6 +513,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
class ImagePaint extends StatelessWidget {
|
||||
final String id;
|
||||
final Rx<bool> cursorOverImage;
|
||||
final Rx<bool> keyboardEnabled;
|
||||
final Widget Function(Widget)? listenerBuilder;
|
||||
final ScrollController _horizontal = ScrollController();
|
||||
final ScrollController _vertical = ScrollController();
|
||||
@@ -731,7 +522,8 @@ class ImagePaint extends StatelessWidget {
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.cursorOverImage,
|
||||
this.listenerBuilder = null})
|
||||
required this.keyboardEnabled,
|
||||
this.listenerBuilder})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -747,25 +539,26 @@ class ImagePaint extends StatelessWidget {
|
||||
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
||||
));
|
||||
return Center(
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final percentX = _horizontal.position.extentBefore /
|
||||
(_horizontal.position.extentBefore +
|
||||
_horizontal.position.extentInside +
|
||||
_horizontal.position.extentAfter);
|
||||
final percentY = _vertical.position.extentBefore /
|
||||
(_vertical.position.extentBefore +
|
||||
_vertical.position.extentInside +
|
||||
_vertical.position.extentAfter);
|
||||
c.setScrollPercent(percentX, percentY);
|
||||
return false;
|
||||
},
|
||||
child: Obx(() => MouseRegion(
|
||||
cursor: cursorOverImage.value
|
||||
? SystemMouseCursors.none
|
||||
: SystemMouseCursors.basic,
|
||||
child: _buildCrossScrollbar(_buildListener(imageWidget)))),
|
||||
));
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final percentX = _horizontal.position.extentBefore /
|
||||
(_horizontal.position.extentBefore +
|
||||
_horizontal.position.extentInside +
|
||||
_horizontal.position.extentAfter);
|
||||
final percentY = _vertical.position.extentBefore /
|
||||
(_vertical.position.extentBefore +
|
||||
_vertical.position.extentInside +
|
||||
_vertical.position.extentAfter);
|
||||
c.setScrollPercent(percentX, percentY);
|
||||
return false;
|
||||
},
|
||||
child: Obx(() => MouseRegion(
|
||||
cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue)
|
||||
? SystemMouseCursors.none
|
||||
: MouseCursor.defer,
|
||||
child: _buildCrossScrollbar(_buildListener(imageWidget)))),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final imageWidget = SizedBox(
|
||||
width: c.size.width,
|
||||
@@ -824,13 +617,12 @@ class CursorPaint extends StatelessWidget {
|
||||
final m = Provider.of<CursorModel>(context);
|
||||
final c = Provider.of<CanvasModel>(context);
|
||||
// final adjust = m.adjustForKeyboard();
|
||||
var s = c.scale;
|
||||
return CustomPaint(
|
||||
painter: ImagePainter(
|
||||
image: m.image,
|
||||
x: m.x * s - m.hotx + c.x,
|
||||
y: m.y * s - m.hoty + c.y,
|
||||
scale: 1),
|
||||
x: m.x - m.hotx + c.x / c.scale,
|
||||
y: m.y - m.hoty + c.y / c.scale,
|
||||
scale: c.scale),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -882,205 +674,35 @@ class QualityMonitor extends StatelessWidget {
|
||||
right: 10,
|
||||
child: qualityMonitorModel.show
|
||||
? Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: MyTheme.canvasColor.withAlpha(120),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Speed: ${qualityMonitorModel.data.speed ?? ''}",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
Text(
|
||||
"FPS: ${qualityMonitorModel.data.fps ?? ''}",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
Text(
|
||||
"Delay: ${qualityMonitorModel.data.delay ?? ''} ms",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
Text(
|
||||
"Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
Text(
|
||||
"Codec: ${qualityMonitorModel.data.codecFormat ?? ''}",
|
||||
style: TextStyle(color: MyTheme.grayBg),
|
||||
style: const TextStyle(color: MyTheme.grayBg),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SizedBox.shrink())));
|
||||
}
|
||||
|
||||
void showOptions(String id) async {
|
||||
final _ffi = ffi(id);
|
||||
String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced';
|
||||
if (quality == '') quality = 'balanced';
|
||||
String viewStyle =
|
||||
await bind.sessionGetOption(id: id, arg: 'view-style') ?? '';
|
||||
String scrollStyle =
|
||||
await bind.sessionGetOption(id: id, arg: 'scroll-style') ?? '';
|
||||
var displays = <Widget>[];
|
||||
final pi = _ffi.ffiModel.pi;
|
||||
final image = _ffi.ffiModel.getConnectionImage();
|
||||
if (image != null)
|
||||
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
||||
if (pi.displays.length > 1) {
|
||||
final cur = pi.currentDisplay;
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < pi.displays.length; ++i)
|
||||
children.add(InkWell(
|
||||
onTap: () {
|
||||
if (i == cur) return;
|
||||
bind.sessionSwitchDisplay(id: id, value: i);
|
||||
_ffi.dialogManager.dismissAll();
|
||||
},
|
||||
child: Ink(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black87),
|
||||
color: i == cur ? Colors.black87 : Colors.white),
|
||||
child: Center(
|
||||
child: Text((i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: i == cur ? Colors.white : Colors.black87))))));
|
||||
displays.add(Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8,
|
||||
children: children,
|
||||
)));
|
||||
}
|
||||
if (displays.isNotEmpty) {
|
||||
displays.add(Divider(color: MyTheme.border));
|
||||
}
|
||||
final perms = _ffi.ffiModel.permissions;
|
||||
|
||||
_ffi.dialogManager.show((setState, close) {
|
||||
final more = <Widget>[];
|
||||
if (perms['audio'] != false) {
|
||||
more.add(getToggle(id, setState, 'disable-audio', 'Mute'));
|
||||
}
|
||||
if (perms['keyboard'] != false) {
|
||||
if (perms['clipboard'] != false)
|
||||
more.add(
|
||||
getToggle(id, setState, 'disable-clipboard', 'Disable clipboard'));
|
||||
more.add(getToggle(
|
||||
id, setState, 'lock-after-session-end', 'Lock after session end'));
|
||||
if (pi.platform == 'Windows') {
|
||||
more.add(Consumer<FfiModel>(
|
||||
builder: (_context, _ffiModel, _child) => () {
|
||||
return getToggle(
|
||||
id, setState, 'privacy-mode', 'Privacy mode');
|
||||
}()));
|
||||
}
|
||||
}
|
||||
var setQuality = (String? value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
quality = value;
|
||||
bind.sessionSetImageQuality(id: id, value: value);
|
||||
});
|
||||
};
|
||||
var setViewStyle = (String? value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
viewStyle = value;
|
||||
bind.sessionPeerOption(id: id, name: "view-style", value: value);
|
||||
_ffi.canvasModel.updateViewStyle();
|
||||
});
|
||||
};
|
||||
var setScrollStyle = (String? value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
scrollStyle = value;
|
||||
bind.sessionPeerOption(id: id, name: "scroll-style", value: value);
|
||||
_ffi.canvasModel.updateScrollStyle();
|
||||
});
|
||||
};
|
||||
return CustomAlertDialog(
|
||||
title: SizedBox.shrink(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: displays +
|
||||
<Widget>[
|
||||
getRadio('Original', 'original', viewStyle, setViewStyle),
|
||||
getRadio('Shrink', 'shrink', viewStyle, setViewStyle),
|
||||
getRadio('Stretch', 'stretch', viewStyle, setViewStyle),
|
||||
Divider(color: MyTheme.border),
|
||||
getRadio(
|
||||
'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle),
|
||||
getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle),
|
||||
Divider(color: MyTheme.border),
|
||||
getRadio('Good image quality', 'best', quality, setQuality),
|
||||
getRadio('Balanced', 'balanced', quality, setQuality),
|
||||
getRadio('Optimize reaction time', 'low', quality, setQuality),
|
||||
Divider(color: MyTheme.border),
|
||||
getToggle(
|
||||
id, setState, 'show-remote-cursor', 'Show remote cursor'),
|
||||
getToggle(id, setState, 'show-quality-monitor',
|
||||
'Show quality monitor',
|
||||
ffi: _ffi),
|
||||
] +
|
||||
more),
|
||||
actions: [],
|
||||
contentPadding: 0,
|
||||
);
|
||||
}, clickMaskDismiss: true, backDismiss: true);
|
||||
}
|
||||
|
||||
void showSetOSPassword(
|
||||
String id, bool login, OverlayDialogManager dialogManager) async {
|
||||
final controller = TextEditingController();
|
||||
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
|
||||
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
||||
controller.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('OS Password')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: "os-password", value: text);
|
||||
bind.sessionPeerOption(
|
||||
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
|
||||
if (text != "" && login) {
|
||||
bind.sessionInputOsPassword(id: id, value: text);
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
]);
|
||||
});
|
||||
: const SizedBox.shrink())));
|
||||
}
|
||||
|
||||
void sendPrompt(String id, bool isMac, String key) {
|
||||
|
||||
@@ -19,10 +19,12 @@ class DesktopServerPage extends StatefulWidget {
|
||||
|
||||
class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
final tabController = gFFI.serverModel.tabController;
|
||||
@override
|
||||
void initState() {
|
||||
gFFI.ffiModel.updateEventListener("");
|
||||
windowManager.addListener(this);
|
||||
tabController.onRemove = (_, id) => onRemoveId(id);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -39,6 +41,13 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
super.onWindowClose();
|
||||
}
|
||||
|
||||
void onRemoveId(String id) {
|
||||
if (tabController.state.value.tabs.isEmpty) {
|
||||
windowManager.close();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return MultiProvider(
|
||||
@@ -48,22 +57,25 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
],
|
||||
child: Consumer<ServerModel>(
|
||||
builder: (context, serverModel, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: ConnectionManager()),
|
||||
SizedBox.fromSize(size: Size(0, 15.0)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)));
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: Scaffold(
|
||||
backgroundColor: MyTheme.color(context).bg,
|
||||
body: Overlay(initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
gFFI.dialogManager.setOverlayState(Overlay.of(context));
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: ConnectionManager()),
|
||||
SizedBox.fromSize(size: Size(0, 15.0)),
|
||||
],
|
||||
),
|
||||
);
|
||||
})
|
||||
]),
|
||||
))));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -106,12 +118,11 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
||||
],
|
||||
)
|
||||
: DesktopTab(
|
||||
theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(),
|
||||
showTitle: false,
|
||||
showMaximize: false,
|
||||
showMinimize: false,
|
||||
showMinimize: true,
|
||||
showClose: true,
|
||||
controller: serverModel.tabController,
|
||||
tabType: DesktopTabType.cm,
|
||||
pageViewBuilder: (pageView) => Row(children: [
|
||||
Expanded(child: pageView),
|
||||
Consumer<ChatModel>(
|
||||
@@ -450,8 +461,10 @@ class _CmControlPanel extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: MyTheme.accent, borderRadius: BorderRadius.circular(10)),
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
checkClickTime(client.id, () => handleAccept(context)),
|
||||
onTap: () => checkClickTime(client.id, () {
|
||||
handleAccept(context);
|
||||
windowManager.minimize();
|
||||
}),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/formatter/id_formatter.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/peer_model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
@@ -15,7 +16,7 @@ class _PopupMenuTheme {
|
||||
static const Color commonColor = MyTheme.accent;
|
||||
// kMinInteractiveDimension
|
||||
static const double height = 25.0;
|
||||
static const double dividerHeight = 12.0;
|
||||
static const double dividerHeight = 3.0;
|
||||
}
|
||||
|
||||
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
|
||||
@@ -46,7 +47,8 @@ class _PeerCard extends StatefulWidget {
|
||||
/// State for the connection page.
|
||||
class _PeerCardState extends State<_PeerCard>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final double _cardRadis = 20;
|
||||
var _menuPos = RelativeRect.fill;
|
||||
final double _cardRadis = 16;
|
||||
final double _borderWidth = 2;
|
||||
final RxBool _iconMoreHover = false.obs;
|
||||
|
||||
@@ -118,7 +120,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
? Colors.green
|
||||
: Colors.yellow)),
|
||||
Text(
|
||||
'${peer.id}',
|
||||
formatID('${peer.id}'),
|
||||
style: TextStyle(fontWeight: FontWeight.w400),
|
||||
),
|
||||
]),
|
||||
@@ -239,7 +241,7 @@ class _PeerCardState extends State<_PeerCard>
|
||||
backgroundColor: peer.online
|
||||
? Colors.green
|
||||
: Colors.yellow)),
|
||||
Text(peer.id)
|
||||
Text(formatID(peer.id))
|
||||
]).paddingSymmetric(vertical: 8),
|
||||
_actionMore(peer),
|
||||
],
|
||||
@@ -253,36 +255,36 @@ class _PeerCardState extends State<_PeerCard>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionMore(Peer peer) {
|
||||
return FutureBuilder(
|
||||
future: widget.popupMenuEntryBuilder(context),
|
||||
initialData: const <mod_menu.PopupMenuEntry<String>>[],
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<List<mod_menu.PopupMenuEntry<String>>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Listener(
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => _iconMoreHover.value = true,
|
||||
onExit: (_) => _iconMoreHover.value = false,
|
||||
child: CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor: _iconMoreHover.value
|
||||
? MyTheme.color(context).grayBg!
|
||||
: MyTheme.color(context).bg!,
|
||||
child: mod_menu.PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(Icons.more_vert,
|
||||
size: 18,
|
||||
color: _iconMoreHover.value
|
||||
? MyTheme.color(context).text
|
||||
: MyTheme.color(context).lightText),
|
||||
position: mod_menu.PopupMenuPosition.under,
|
||||
itemBuilder: (BuildContext context) => snapshot.data!,
|
||||
))));
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
});
|
||||
Widget _actionMore(Peer peer) => Listener(
|
||||
onPointerDown: (e) {
|
||||
final x = e.position.dx;
|
||||
final y = e.position.dy;
|
||||
_menuPos = RelativeRect.fromLTRB(x, y, x, y);
|
||||
},
|
||||
onPointerUp: (_) => _showPeerMenu(context, peer.id),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => _iconMoreHover.value = true,
|
||||
onExit: (_) => _iconMoreHover.value = false,
|
||||
child: CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor: _iconMoreHover.value
|
||||
? MyTheme.color(context).grayBg!
|
||||
: MyTheme.color(context).bg!,
|
||||
child: Icon(Icons.more_vert,
|
||||
size: 18,
|
||||
color: _iconMoreHover.value
|
||||
? MyTheme.color(context).text
|
||||
: MyTheme.color(context).lightText))));
|
||||
|
||||
/// Show the peer menu and handle user's choice.
|
||||
/// User might remove the peer or send a file to the peer.
|
||||
void _showPeerMenu(BuildContext context, String id) async {
|
||||
await mod_menu.showMenu(
|
||||
context: context,
|
||||
position: _menuPos,
|
||||
items: await super.widget.popupMenuEntryBuilder(context),
|
||||
elevation: 8,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get the image for the current [platform].
|
||||
@@ -411,19 +413,26 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
@protected
|
||||
MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('RDP'),
|
||||
style: style,
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit),
|
||||
onPressed: () => _rdpDialog(id),
|
||||
)
|
||||
],
|
||||
),
|
||||
childBuilder: (TextStyle? style) => Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
height: _PopupMenuTheme.height,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
translate('RDP'),
|
||||
style: style,
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(Icons.edit),
|
||||
onPressed: () => _rdpDialog(id),
|
||||
),
|
||||
))
|
||||
],
|
||||
)),
|
||||
proc: () {
|
||||
_connect(context, id, isRDP: true);
|
||||
},
|
||||
@@ -554,47 +563,47 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
isInProgress.value = true;
|
||||
name = controller.text;
|
||||
await bind.mainSetPeerOption(id: id, key: 'alias', value: name);
|
||||
if (isAddressBook) {
|
||||
gFFI.abModel.setPeerOption(id, 'alias', name);
|
||||
await gFFI.abModel.updateAb();
|
||||
}
|
||||
alias.value = await bind.mainGetPeerOption(id: peer.id, key: 'alias');
|
||||
close();
|
||||
isInProgress.value = false;
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Rename')),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Form(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(border: OutlineInputBorder()),
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
decoration:
|
||||
const InputDecoration(border: OutlineInputBorder()),
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(() => Offstage(
|
||||
offstage: isInProgress.isFalse,
|
||||
child: LinearProgressIndicator())),
|
||||
child: const LinearProgressIndicator())),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
isInProgress.value = true;
|
||||
name = controller.text;
|
||||
await bind.mainSetPeerOption(id: id, key: 'alias', value: name);
|
||||
if (isAddressBook) {
|
||||
gFFI.abModel.setPeerOption(id, 'alias', name);
|
||||
await gFFI.abModel.updateAb();
|
||||
}
|
||||
alias.value =
|
||||
await bind.mainGetPeerOption(id: peer.id, key: 'alias');
|
||||
close();
|
||||
isInProgress.value = false;
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -614,6 +623,7 @@ class RecentPeerCard extends BasePeerCard {
|
||||
if (peer.platform == 'Windows') {
|
||||
menuItems.add(_rdpAction(context, peer.id));
|
||||
}
|
||||
menuItems.add(MenuEntryDivider());
|
||||
menuItems.add(await _forceAlwaysRelayAction(peer.id));
|
||||
menuItems.add(_renameAction(peer.id, false));
|
||||
menuItems.add(_removeAction(peer.id, () async {
|
||||
@@ -740,13 +750,23 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
var selectedTag = gFFI.abModel.getPeerTags(id).obs;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||
await gFFI.abModel.updateAb();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Edit Tag")),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Wrap(
|
||||
children: tags
|
||||
.map((e) => _buildTag(e, selectedTag, onTap: () {
|
||||
@@ -759,26 +779,16 @@ class AddressBookPeerCard extends BasePeerCard {
|
||||
.toList(growable: false),
|
||||
),
|
||||
),
|
||||
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
||||
Offstage(
|
||||
offstage: !isInProgress, child: const LinearProgressIndicator())
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
gFFI.abModel.changeTagForPeer(id, selectedTag);
|
||||
await gFFI.abModel.updateAb();
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -861,25 +871,35 @@ void _rdpDialog(String id) async {
|
||||
RxBool secure = true.obs;
|
||||
|
||||
gFFI.dialogManager.show((setState, close) {
|
||||
submit() async {
|
||||
await bind.mainSetPeerOption(
|
||||
id: id, key: 'rdp_port', value: portController.text.trim());
|
||||
await bind.mainSetPeerOption(
|
||||
id: id, key: 'rdp_username', value: userController.text);
|
||||
await bind.mainSetPeerOption(
|
||||
id: id, key: 'rdp_password', value: passwordContorller.text);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text('RDP ' + translate('Settings')),
|
||||
title: Text('RDP ${translate('Settings')}'),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 500),
|
||||
constraints: const BoxConstraints(minWidth: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text(
|
||||
"${translate('Port')}:",
|
||||
textAlign: TextAlign.start,
|
||||
).marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
@@ -888,52 +908,54 @@ void _rdpDialog(String id) async {
|
||||
FilteringTextInputFormatter.allow(RegExp(
|
||||
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$'))
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(), hintText: '3389'),
|
||||
controller: portController,
|
||||
focusNode: FocusNode()..requestFocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text(
|
||||
"${translate('Username')}:",
|
||||
textAlign: TextAlign.start,
|
||||
).marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(border: OutlineInputBorder()),
|
||||
decoration:
|
||||
const InputDecoration(border: OutlineInputBorder()),
|
||||
controller: userController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
constraints: const BoxConstraints(minWidth: 100),
|
||||
child: Text("${translate('Password')}:")
|
||||
.marginOnly(bottom: 16.0)),
|
||||
SizedBox(
|
||||
const SizedBox(
|
||||
width: 24.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() => TextField(
|
||||
obscureText: secure.value,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => secure.value = !secure.value,
|
||||
icon: Icon(secure.value
|
||||
@@ -948,23 +970,11 @@ void _rdpDialog(String id) async {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await bind.mainSetPeerOption(
|
||||
id: id, key: 'rdp_port', value: portController.text.trim());
|
||||
await bind.mainSetPeerOption(
|
||||
id: id, key: 'rdp_username', value: userController.text);
|
||||
await bind.mainSetPeerOption(
|
||||
id: id, key: 'rdp_password', value: passwordContorller.text);
|
||||
close();
|
||||
},
|
||||
child: Text(translate("OK"))),
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import './material_mod_popup_menu.dart' as mod_menu;
|
||||
@@ -174,8 +175,8 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
||||
children: [
|
||||
Text(
|
||||
opt.text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@@ -256,8 +257,8 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
children: [
|
||||
Text(
|
||||
opt.text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@@ -300,8 +301,8 @@ class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
||||
const SizedBox(width: MenuConfig.midPadding),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@@ -346,8 +347,8 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
||||
// const SizedBox(width: MenuConfig.midPadding),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@@ -450,8 +451,8 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
const SizedBox(width: MenuConfig.midPadding),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
style: TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
),
|
||||
@@ -491,8 +492,8 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(minHeight: conf.height),
|
||||
child: childBuilder(
|
||||
const TextStyle(
|
||||
color: Colors.black,
|
||||
TextStyle(
|
||||
color: MyTheme.color(context).text,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal),
|
||||
)),
|
||||
|
||||
@@ -496,9 +496,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
});
|
||||
final quality =
|
||||
await bind.sessionGetCustomImageQuality(id: widget.id);
|
||||
final double initValue = quality != null && quality.isNotEmpty
|
||||
double initValue = quality != null && quality.isNotEmpty
|
||||
? quality[0].toDouble()
|
||||
: 50.0;
|
||||
const minValue = 10.0;
|
||||
const maxValue = 100.0;
|
||||
if (initValue < minValue) {
|
||||
initValue = minValue;
|
||||
}
|
||||
if (initValue > maxValue) {
|
||||
initValue = maxValue;
|
||||
}
|
||||
final RxDouble sliderValue = RxDouble(initValue);
|
||||
final rxReplay = rxdart.ReplaySubject<double>();
|
||||
rxReplay
|
||||
@@ -513,30 +521,44 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
final slider = Obx(() {
|
||||
return Slider(
|
||||
value: sliderValue.value,
|
||||
max: 100,
|
||||
divisions: 100,
|
||||
label: sliderValue.value.round().toString(),
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
divisions: 90,
|
||||
onChanged: (double value) {
|
||||
sliderValue.value = value;
|
||||
rxReplay.add(value);
|
||||
},
|
||||
);
|
||||
});
|
||||
final content = Row(
|
||||
children: [
|
||||
slider,
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: Obx(() => Text(
|
||||
'${sliderValue.value.round()}% Bitrate',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)))
|
||||
],
|
||||
);
|
||||
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',
|
||||
slider, [btnCancel]);
|
||||
content, [btnCancel]);
|
||||
}
|
||||
}),
|
||||
MenuEntryDivider<String>(),
|
||||
MenuEntrySwitch<String>(
|
||||
text: translate('Show remote cursor'),
|
||||
getter: () async {
|
||||
return bind.sessionGetToggleOptionSync(
|
||||
id: widget.id, arg: 'show-remote-cursor');
|
||||
},
|
||||
setter: (bool v) async {
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'show-remote-cursor');
|
||||
}),
|
||||
() {
|
||||
final state = ShowRemoteCursorState.find(widget.id);
|
||||
return MenuEntrySwitch2<String>(
|
||||
text: translate('Show remote cursor'),
|
||||
getter: () {
|
||||
return state;
|
||||
},
|
||||
setter: (bool v) async {
|
||||
state.value = v;
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'show-remote-cursor');
|
||||
});
|
||||
}(),
|
||||
MenuEntrySwitch<String>(
|
||||
text: translate('Show quality monitor'),
|
||||
getter: () async {
|
||||
@@ -565,12 +587,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
'Lock after session end', 'lock-after-session-end'));
|
||||
if (pi.platform == 'Windows') {
|
||||
displayMenu.add(MenuEntrySwitch2<String>(
|
||||
dismissOnClicked: true,
|
||||
text: translate('Privacy mode'),
|
||||
getter: () {
|
||||
return PrivacyModeState.find(widget.id);
|
||||
},
|
||||
setter: (bool v) async {
|
||||
Navigator.pop(context);
|
||||
await bind.sessionToggleOption(
|
||||
id: widget.id, value: 'privacy-mode');
|
||||
}));
|
||||
@@ -620,46 +642,49 @@ void showSetOSPassword(
|
||||
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
|
||||
controller.text = password;
|
||||
dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: "os-password", value: text);
|
||||
bind.sessionPeerOption(
|
||||
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
|
||||
if (text != "" && login) {
|
||||
bind.sessionInputOsPassword(id: id, value: text);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('OS Password')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
title: Text(translate('OS Password')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
PasswordWidget(controller: controller),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate('Auto Login'),
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () {
|
||||
var text = controller.text.trim();
|
||||
bind.sessionPeerOption(id: id, name: "os-password", value: text);
|
||||
bind.sessionPeerOption(
|
||||
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
|
||||
if (text != "" && login) {
|
||||
bind.sessionInputOsPassword(id: id, value: text);
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
]);
|
||||
value: autoLogin,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => autoLogin = v);
|
||||
},
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: close,
|
||||
child: Text(translate('Cancel')),
|
||||
),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: submit,
|
||||
child: Text(translate('OK')),
|
||||
),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide TabBarTheme;
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
@@ -59,13 +59,15 @@ class DesktopTabState {
|
||||
|
||||
class DesktopTabController {
|
||||
final state = DesktopTabState().obs;
|
||||
final DesktopTabType tabType;
|
||||
|
||||
/// index, key
|
||||
Function(int, String)? onRemove;
|
||||
|
||||
Function(int)? onSelected;
|
||||
|
||||
void add(TabInfo tab) {
|
||||
DesktopTabController({required this.tabType});
|
||||
|
||||
void add(TabInfo tab, {bool authorized = false}) {
|
||||
if (!isDesktop) return;
|
||||
final index = state.value.tabs.indexWhere((e) => e.key == tab.key);
|
||||
int toIndex;
|
||||
@@ -79,6 +81,16 @@ class DesktopTabController {
|
||||
toIndex = state.value.tabs.length - 1;
|
||||
assert(toIndex >= 0);
|
||||
}
|
||||
if (tabType == DesktopTabType.cm) {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
window_on_top(null);
|
||||
});
|
||||
if (authorized) {
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
windowManager.minimize();
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
jumpTo(toIndex);
|
||||
} catch (e) {
|
||||
@@ -106,6 +118,7 @@ class DesktopTabController {
|
||||
}
|
||||
|
||||
void jumpTo(int index) {
|
||||
if (!isDesktop || index < 0) return;
|
||||
state.update((val) {
|
||||
val!.selected = index;
|
||||
Future.delayed(Duration.zero, (() {
|
||||
@@ -114,12 +127,14 @@ class DesktopTabController {
|
||||
}
|
||||
if (val.scrollController.hasClients &&
|
||||
val.scrollController.canScroll &&
|
||||
val.scrollController.itemCount >= index) {
|
||||
val.scrollController.itemCount > index) {
|
||||
val.scrollController.scrollToItem(index, center: true, animate: true);
|
||||
}
|
||||
}));
|
||||
});
|
||||
onSelected?.call(index);
|
||||
if (state.value.tabs.length > index) {
|
||||
onSelected?.call(index);
|
||||
}
|
||||
}
|
||||
|
||||
void closeBy(String? key) {
|
||||
@@ -143,8 +158,7 @@ class DesktopTabController {
|
||||
|
||||
class TabThemeConf {
|
||||
double iconSize;
|
||||
TarBarTheme theme;
|
||||
TabThemeConf({required this.iconSize, required this.theme});
|
||||
TabThemeConf({required this.iconSize});
|
||||
}
|
||||
|
||||
typedef TabBuilder = Widget Function(
|
||||
@@ -153,9 +167,6 @@ typedef LabelGetter = Rx<String> Function(String key);
|
||||
|
||||
class DesktopTab extends StatelessWidget {
|
||||
final Function(String)? onTabClose;
|
||||
final TarBarTheme theme;
|
||||
final DesktopTabType tabType;
|
||||
final bool isMainWindow;
|
||||
final bool showTabBar;
|
||||
final bool showLogo;
|
||||
final bool showTitle;
|
||||
@@ -170,11 +181,12 @@ class DesktopTab extends StatelessWidget {
|
||||
|
||||
final DesktopTabController controller;
|
||||
Rx<DesktopTabState> get state => controller.state;
|
||||
late final DesktopTabType tabType;
|
||||
late final bool isMainWindow;
|
||||
|
||||
const DesktopTab({
|
||||
DesktopTab({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
required this.tabType,
|
||||
this.theme = const TarBarTheme.light(),
|
||||
this.onTabClose,
|
||||
this.showTabBar = true,
|
||||
this.showLogo = true,
|
||||
@@ -187,23 +199,26 @@ class DesktopTab extends StatelessWidget {
|
||||
this.onClose,
|
||||
this.tabBuilder,
|
||||
this.labelGetter,
|
||||
}) : isMainWindow =
|
||||
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
|
||||
}) : super(key: key) {
|
||||
tabType = controller.tabType;
|
||||
isMainWindow =
|
||||
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: [
|
||||
Offstage(
|
||||
offstage: !showTabBar,
|
||||
child: Container(
|
||||
child: SizedBox(
|
||||
height: _kTabBarHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
SizedBox(
|
||||
height: _kTabBarHeight - 1,
|
||||
child: _buildBar(),
|
||||
),
|
||||
Divider(
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
@@ -282,7 +297,7 @@ class DesktopTab extends StatelessWidget {
|
||||
)),
|
||||
Offstage(
|
||||
offstage: !showTitle,
|
||||
child: Text(
|
||||
child: const Text(
|
||||
"RustDesk",
|
||||
style: TextStyle(fontSize: 13),
|
||||
).marginOnly(left: 2))
|
||||
@@ -303,7 +318,6 @@ class DesktopTab extends StatelessWidget {
|
||||
child: _ListView(
|
||||
controller: controller,
|
||||
onTabClose: onTabClose,
|
||||
theme: theme,
|
||||
tabBuilder: tabBuilder,
|
||||
labelGetter: labelGetter,
|
||||
)),
|
||||
@@ -314,7 +328,8 @@ class DesktopTab extends StatelessWidget {
|
||||
Offstage(offstage: tail == null, child: tail),
|
||||
WindowActionPanel(
|
||||
mainTab: isMainWindow,
|
||||
theme: theme,
|
||||
tabType: tabType,
|
||||
state: state,
|
||||
showMinimize: showMinimize,
|
||||
showMaximize: showMaximize,
|
||||
showClose: showClose,
|
||||
@@ -327,7 +342,8 @@ class DesktopTab extends StatelessWidget {
|
||||
|
||||
class WindowActionPanel extends StatelessWidget {
|
||||
final bool mainTab;
|
||||
final TarBarTheme theme;
|
||||
final DesktopTabType tabType;
|
||||
final Rx<DesktopTabState> state;
|
||||
|
||||
final bool showMinimize;
|
||||
final bool showMaximize;
|
||||
@@ -337,7 +353,8 @@ class WindowActionPanel extends StatelessWidget {
|
||||
const WindowActionPanel(
|
||||
{Key? key,
|
||||
required this.mainTab,
|
||||
required this.theme,
|
||||
required this.tabType,
|
||||
required this.state,
|
||||
this.showMinimize = true,
|
||||
this.showMaximize = true,
|
||||
this.showClose = true,
|
||||
@@ -353,7 +370,6 @@ class WindowActionPanel extends StatelessWidget {
|
||||
child: ActionIcon(
|
||||
message: 'Minimize',
|
||||
icon: IconFont.min,
|
||||
theme: theme,
|
||||
onTap: () {
|
||||
if (mainTab) {
|
||||
windowManager.minimize();
|
||||
@@ -361,31 +377,30 @@ class WindowActionPanel extends StatelessWidget {
|
||||
WindowController.fromWindowId(windowId!).minimize();
|
||||
}
|
||||
},
|
||||
is_close: false,
|
||||
isClose: false,
|
||||
)),
|
||||
// TODO: drag makes window restore
|
||||
Offstage(
|
||||
offstage: !showMaximize,
|
||||
child: FutureBuilder(builder: (context, snapshot) {
|
||||
RxBool is_maximized = false.obs;
|
||||
RxBool isMaximized = false.obs;
|
||||
if (mainTab) {
|
||||
windowManager.isMaximized().then((maximized) {
|
||||
is_maximized.value = maximized;
|
||||
isMaximized.value = maximized;
|
||||
});
|
||||
} else {
|
||||
final wc = WindowController.fromWindowId(windowId!);
|
||||
wc.isMaximized().then((maximized) {
|
||||
is_maximized.value = maximized;
|
||||
isMaximized.value = maximized;
|
||||
});
|
||||
}
|
||||
return Obx(
|
||||
() => ActionIcon(
|
||||
message: is_maximized.value ? "Restore" : "Maximize",
|
||||
icon: is_maximized.value ? IconFont.restore : IconFont.max,
|
||||
theme: theme,
|
||||
message: isMaximized.value ? "Restore" : "Maximize",
|
||||
icon: isMaximized.value ? IconFont.restore : IconFont.max,
|
||||
onTap: () {
|
||||
if (mainTab) {
|
||||
if (is_maximized.value) {
|
||||
if (isMaximized.value) {
|
||||
windowManager.unmaximize();
|
||||
} else {
|
||||
windowManager.maximize();
|
||||
@@ -393,15 +408,15 @@ class WindowActionPanel extends StatelessWidget {
|
||||
} else {
|
||||
// TODO: subwindow is maximized but first query result is not maximized.
|
||||
final wc = WindowController.fromWindowId(windowId!);
|
||||
if (is_maximized.value) {
|
||||
if (isMaximized.value) {
|
||||
wc.unmaximize();
|
||||
} else {
|
||||
wc.maximize();
|
||||
}
|
||||
}
|
||||
is_maximized.value = !is_maximized.value;
|
||||
isMaximized.value = !isMaximized.value;
|
||||
},
|
||||
is_close: false,
|
||||
isClose: false,
|
||||
),
|
||||
);
|
||||
})),
|
||||
@@ -410,40 +425,70 @@ class WindowActionPanel extends StatelessWidget {
|
||||
child: ActionIcon(
|
||||
message: 'Close',
|
||||
icon: IconFont.close,
|
||||
theme: theme,
|
||||
onTap: () {
|
||||
if (mainTab) {
|
||||
windowManager.close();
|
||||
} else {
|
||||
// only hide for multi window, not close
|
||||
Future.delayed(Duration.zero, () {
|
||||
WindowController.fromWindowId(windowId!).hide();
|
||||
});
|
||||
onTap: () async {
|
||||
action() {
|
||||
if (mainTab) {
|
||||
windowManager.close();
|
||||
} else {
|
||||
// only hide for multi window, not close
|
||||
Future.delayed(Duration.zero, () {
|
||||
WindowController.fromWindowId(windowId!).hide();
|
||||
});
|
||||
}
|
||||
onClose?.call();
|
||||
}
|
||||
|
||||
if (tabType != DesktopTabType.main &&
|
||||
state.value.tabs.length > 1) {
|
||||
closeConfirmDialog(action);
|
||||
} else {
|
||||
action();
|
||||
}
|
||||
onClose?.call();
|
||||
},
|
||||
is_close: true,
|
||||
isClose: true,
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
closeConfirmDialog(Function() callback) async {
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close) {
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
const Icon(Icons.warning_amber_sharp,
|
||||
color: Colors.redAccent, size: 28),
|
||||
const SizedBox(width: 10),
|
||||
Text(translate("Warning")),
|
||||
]),
|
||||
content: Text(translate("Disconnect all devices?")),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
if (res == true) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class _ListView extends StatelessWidget {
|
||||
final DesktopTabController controller;
|
||||
final Function(String key)? onTabClose;
|
||||
final TarBarTheme theme;
|
||||
|
||||
final TabBuilder? tabBuilder;
|
||||
final LabelGetter? labelGetter;
|
||||
|
||||
Rx<DesktopTabState> get state => controller.state;
|
||||
|
||||
_ListView(
|
||||
const _ListView(
|
||||
{required this.controller,
|
||||
required this.onTabClose,
|
||||
required this.theme,
|
||||
this.tabBuilder,
|
||||
this.labelGetter});
|
||||
|
||||
@@ -453,7 +498,7 @@ class _ListView extends StatelessWidget {
|
||||
controller: state.value.scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
physics: BouncingScrollPhysics(),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: state.value.tabs.asMap().entries.map((e) {
|
||||
final index = e.key;
|
||||
final tab = e.value;
|
||||
@@ -468,7 +513,6 @@ class _ListView extends StatelessWidget {
|
||||
selected: state.value.selected,
|
||||
onClose: () => controller.remove(index),
|
||||
onSelected: () => controller.jumpTo(index),
|
||||
theme: theme,
|
||||
tabBuilder: tabBuilder == null
|
||||
? null
|
||||
: (Widget icon, Widget labelWidget, TabThemeConf themeConf) {
|
||||
@@ -485,31 +529,29 @@ class _ListView extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Tab extends StatefulWidget {
|
||||
late final int index;
|
||||
late final Rx<String> label;
|
||||
late final IconData? selectedIcon;
|
||||
late final IconData? unselectedIcon;
|
||||
late final bool closable;
|
||||
late final int selected;
|
||||
late final Function() onClose;
|
||||
late final Function() onSelected;
|
||||
late final TarBarTheme theme;
|
||||
final int index;
|
||||
final Rx<String> label;
|
||||
final IconData? selectedIcon;
|
||||
final IconData? unselectedIcon;
|
||||
final bool closable;
|
||||
final int selected;
|
||||
final Function() onClose;
|
||||
final Function() onSelected;
|
||||
final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)?
|
||||
tabBuilder;
|
||||
|
||||
_Tab(
|
||||
{Key? key,
|
||||
required this.index,
|
||||
required this.label,
|
||||
this.selectedIcon,
|
||||
this.unselectedIcon,
|
||||
this.tabBuilder,
|
||||
required this.closable,
|
||||
required this.selected,
|
||||
required this.onClose,
|
||||
required this.onSelected,
|
||||
required this.theme})
|
||||
: super(key: key);
|
||||
const _Tab({
|
||||
Key? key,
|
||||
required this.index,
|
||||
required this.label,
|
||||
this.selectedIcon,
|
||||
this.unselectedIcon,
|
||||
this.tabBuilder,
|
||||
required this.closable,
|
||||
required this.selected,
|
||||
required this.onClose,
|
||||
required this.onSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_Tab> createState() => _TabState();
|
||||
@@ -529,8 +571,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
isSelected ? widget.selectedIcon : widget.unselectedIcon,
|
||||
size: _kIconSize,
|
||||
color: isSelected
|
||||
? widget.theme.selectedtabIconColor
|
||||
: widget.theme.unSelectedtabIconColor,
|
||||
? MyTheme.tabbar(context).selectedTabIconColor
|
||||
: MyTheme.tabbar(context).unSelectedTabIconColor,
|
||||
).paddingOnly(right: 5));
|
||||
final labelWidget = Obx(() {
|
||||
return Text(
|
||||
@@ -538,8 +580,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? widget.theme.selectedTextColor
|
||||
: widget.theme.unSelectedTextColor),
|
||||
? MyTheme.tabbar(context).selectedTextColor
|
||||
: MyTheme.tabbar(context).unSelectedTextColor),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -552,8 +594,8 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return widget.tabBuilder!(icon, labelWidget,
|
||||
TabThemeConf(iconSize: _kIconSize, theme: widget.theme));
|
||||
return widget.tabBuilder!(
|
||||
icon, labelWidget, TabThemeConf(iconSize: _kIconSize));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,7 +624,6 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
visiable: hover.value && widget.closable,
|
||||
tabSelected: isSelected,
|
||||
onClose: () => widget.onClose(),
|
||||
theme: widget.theme,
|
||||
)))
|
||||
])).paddingSymmetric(horizontal: 10),
|
||||
Offstage(
|
||||
@@ -591,7 +632,7 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
||||
width: 1,
|
||||
indent: _kDividerIndent,
|
||||
endIndent: _kDividerIndent,
|
||||
color: widget.theme.dividerColor,
|
||||
color: MyTheme.tabbar(context).dividerColor,
|
||||
thickness: 1,
|
||||
),
|
||||
)
|
||||
@@ -614,14 +655,12 @@ class _CloseButton extends StatelessWidget {
|
||||
final bool visiable;
|
||||
final bool tabSelected;
|
||||
final Function onClose;
|
||||
late final TarBarTheme theme;
|
||||
|
||||
_CloseButton({
|
||||
const _CloseButton({
|
||||
Key? key,
|
||||
required this.visiable,
|
||||
required this.tabSelected,
|
||||
required this.onClose,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -637,8 +676,8 @@ class _CloseButton extends StatelessWidget {
|
||||
Icons.close,
|
||||
size: _kIconSize,
|
||||
color: tabSelected
|
||||
? theme.selectedIconColor
|
||||
: theme.unSelectedIconColor,
|
||||
? MyTheme.tabbar(context).selectedIconColor
|
||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||
),
|
||||
),
|
||||
)).paddingOnly(left: 5);
|
||||
@@ -648,16 +687,14 @@ class _CloseButton extends StatelessWidget {
|
||||
class ActionIcon extends StatelessWidget {
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final TarBarTheme theme;
|
||||
final Function() onTap;
|
||||
final bool is_close;
|
||||
final bool isClose;
|
||||
const ActionIcon({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.icon,
|
||||
required this.theme,
|
||||
required this.onTap,
|
||||
required this.is_close,
|
||||
required this.isClose,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -665,34 +702,32 @@ class ActionIcon extends StatelessWidget {
|
||||
RxBool hover = false.obs;
|
||||
return Obx(() => Tooltip(
|
||||
message: translate(message),
|
||||
waitDuration: Duration(seconds: 1),
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
child: InkWell(
|
||||
hoverColor:
|
||||
is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor,
|
||||
hoverColor: isClose
|
||||
? const Color.fromARGB(255, 196, 43, 28)
|
||||
: MyTheme.tabbar(context).hoverColor,
|
||||
onHover: (value) => hover.value = value,
|
||||
child: Container(
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
height: _kTabBarHeight - 1,
|
||||
width: _kTabBarHeight - 1,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: hover.value && is_close
|
||||
color: hover.value && isClose
|
||||
? Colors.white
|
||||
: theme.unSelectedIconColor,
|
||||
: MyTheme.tabbar(context).unSelectedIconColor,
|
||||
size: _kActionIconSize,
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class AddButton extends StatelessWidget {
|
||||
late final TarBarTheme theme;
|
||||
|
||||
AddButton({
|
||||
const AddButton({
|
||||
Key? key,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -700,41 +735,101 @@ class AddButton extends StatelessWidget {
|
||||
return ActionIcon(
|
||||
message: 'New Connection',
|
||||
icon: IconFont.add,
|
||||
theme: theme,
|
||||
onTap: () =>
|
||||
rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""),
|
||||
is_close: false);
|
||||
isClose: false);
|
||||
}
|
||||
}
|
||||
|
||||
class TarBarTheme {
|
||||
final Color unSelectedtabIconColor;
|
||||
final Color selectedtabIconColor;
|
||||
final Color selectedTextColor;
|
||||
final Color unSelectedTextColor;
|
||||
final Color selectedIconColor;
|
||||
final Color unSelectedIconColor;
|
||||
final Color dividerColor;
|
||||
final Color hoverColor;
|
||||
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||
final Color? selectedTabIconColor;
|
||||
final Color? unSelectedTabIconColor;
|
||||
final Color? selectedTextColor;
|
||||
final Color? unSelectedTextColor;
|
||||
final Color? selectedIconColor;
|
||||
final Color? unSelectedIconColor;
|
||||
final Color? dividerColor;
|
||||
final Color? hoverColor;
|
||||
|
||||
const TarBarTheme.light()
|
||||
: unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241),
|
||||
selectedtabIconColor = MyTheme.accent,
|
||||
selectedTextColor = const Color.fromARGB(255, 26, 26, 26),
|
||||
unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96),
|
||||
selectedIconColor = const Color.fromARGB(255, 26, 26, 26),
|
||||
unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96),
|
||||
dividerColor = const Color.fromARGB(255, 238, 238, 238),
|
||||
hoverColor = const Color.fromARGB(
|
||||
51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E
|
||||
const TabbarTheme(
|
||||
{required this.selectedTabIconColor,
|
||||
required this.unSelectedTabIconColor,
|
||||
required this.selectedTextColor,
|
||||
required this.unSelectedTextColor,
|
||||
required this.selectedIconColor,
|
||||
required this.unSelectedIconColor,
|
||||
required this.dividerColor,
|
||||
required this.hoverColor});
|
||||
|
||||
const TarBarTheme.dark()
|
||||
: unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98),
|
||||
selectedtabIconColor = MyTheme.accent,
|
||||
selectedTextColor = const Color.fromARGB(255, 255, 255, 255),
|
||||
unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207),
|
||||
selectedIconColor = const Color.fromARGB(255, 215, 215, 215),
|
||||
unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255),
|
||||
dividerColor = const Color.fromARGB(255, 64, 64, 64),
|
||||
hoverColor = Colors.black26;
|
||||
static const light = TabbarTheme(
|
||||
selectedTabIconColor: MyTheme.accent,
|
||||
unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241),
|
||||
selectedTextColor: Color.fromARGB(255, 26, 26, 26),
|
||||
unSelectedTextColor: Color.fromARGB(255, 96, 96, 96),
|
||||
selectedIconColor: Color.fromARGB(255, 26, 26, 26),
|
||||
unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
|
||||
dividerColor: Color.fromARGB(255, 238, 238, 238),
|
||||
hoverColor: Color.fromARGB(51, 158, 158, 158));
|
||||
|
||||
static const dark = TabbarTheme(
|
||||
selectedTabIconColor: MyTheme.accent,
|
||||
unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98),
|
||||
selectedTextColor: Color.fromARGB(255, 255, 255, 255),
|
||||
unSelectedTextColor: Color.fromARGB(255, 207, 207, 207),
|
||||
selectedIconColor: Color.fromARGB(255, 215, 215, 215),
|
||||
unSelectedIconColor: Color.fromARGB(255, 255, 255, 255),
|
||||
dividerColor: Color.fromARGB(255, 64, 64, 64),
|
||||
hoverColor: Colors.black26);
|
||||
|
||||
@override
|
||||
ThemeExtension<TabbarTheme> copyWith({
|
||||
Color? selectedTabIconColor,
|
||||
Color? unSelectedTabIconColor,
|
||||
Color? selectedTextColor,
|
||||
Color? unSelectedTextColor,
|
||||
Color? selectedIconColor,
|
||||
Color? unSelectedIconColor,
|
||||
Color? dividerColor,
|
||||
Color? hoverColor,
|
||||
}) {
|
||||
return TabbarTheme(
|
||||
selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor,
|
||||
unSelectedTabIconColor:
|
||||
unSelectedTabIconColor ?? this.unSelectedTabIconColor,
|
||||
selectedTextColor: selectedTextColor ?? this.selectedTextColor,
|
||||
unSelectedTextColor: unSelectedTextColor ?? this.unSelectedTextColor,
|
||||
selectedIconColor: selectedIconColor ?? this.selectedIconColor,
|
||||
unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor,
|
||||
dividerColor: dividerColor ?? this.dividerColor,
|
||||
hoverColor: hoverColor ?? this.hoverColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ThemeExtension<TabbarTheme> lerp(
|
||||
ThemeExtension<TabbarTheme>? other, double t) {
|
||||
if (other is! TabbarTheme) {
|
||||
return this;
|
||||
}
|
||||
return TabbarTheme(
|
||||
selectedTabIconColor:
|
||||
Color.lerp(selectedTabIconColor, other.selectedTabIconColor, t),
|
||||
unSelectedTabIconColor:
|
||||
Color.lerp(unSelectedTabIconColor, other.unSelectedTabIconColor, t),
|
||||
selectedTextColor:
|
||||
Color.lerp(selectedTextColor, other.selectedTextColor, t),
|
||||
unSelectedTextColor:
|
||||
Color.lerp(unSelectedTextColor, other.unSelectedTextColor, t),
|
||||
selectedIconColor:
|
||||
Color.lerp(selectedIconColor, other.selectedIconColor, t),
|
||||
unSelectedIconColor:
|
||||
Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t),
|
||||
dividerColor: Color.lerp(dividerColor, other.dividerColor, t),
|
||||
hoverColor: Color.lerp(hoverColor, other.hoverColor, t),
|
||||
);
|
||||
}
|
||||
|
||||
static color(BuildContext context) {
|
||||
return Theme.of(context).extension<ColorThemeExtension>()!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ void runConnectionManagerScreen() async {
|
||||
await windowManager.setAlignment(Alignment.topRight);
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setAlignment(Alignment.topRight); // ensure
|
||||
})
|
||||
]);
|
||||
runApp(GetMaterialApp(
|
||||
|
||||
@@ -45,7 +45,7 @@ class ChatPage extends StatelessWidget implements PageShape {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: chatModel,
|
||||
child: Container(
|
||||
color: MyTheme.grayBg,
|
||||
color: MyTheme.color(context).grayBg,
|
||||
child: Consumer<ChatModel>(builder: (context, chatModel, child) {
|
||||
final currentUser = chatModel.currentUser;
|
||||
return Stack(
|
||||
@@ -59,6 +59,14 @@ class ChatPage extends StatelessWidget implements PageShape {
|
||||
messages: chatModel
|
||||
.messages[chatModel.currentID]?.chatMessages ??
|
||||
[],
|
||||
inputOptions: InputOptions(
|
||||
sendOnEnter: true,
|
||||
inputDecoration: defaultInputDecoration(
|
||||
fillColor: MyTheme.color(context).bg),
|
||||
sendButtonBuilder: defaultSendButton(
|
||||
color: MyTheme.color(context).text!),
|
||||
inputTextStyle:
|
||||
TextStyle(color: MyTheme.color(context).text)),
|
||||
messageOptions: MessageOptions(
|
||||
showOtherUsersAvatar: false,
|
||||
showTime: true,
|
||||
|
||||
@@ -209,10 +209,19 @@ class ChatModel with ChangeNotifier {
|
||||
id: await bind.mainGetLastRemoteId(),
|
||||
);
|
||||
} else {
|
||||
final client = _ffi.target?.serverModel.clients[id];
|
||||
final client = _ffi.target?.serverModel.clients
|
||||
.firstWhere((client) => client.id == id);
|
||||
if (client == null) {
|
||||
return debugPrint("Failed to receive msg,user doesn't exist");
|
||||
}
|
||||
if (isDesktop) {
|
||||
window_on_top(null);
|
||||
var index = _ffi.target?.serverModel.clients
|
||||
.indexWhere((client) => client.id == id);
|
||||
if (index != null && index >= 0) {
|
||||
gFFI.serverModel.tabController.jumpTo(index);
|
||||
}
|
||||
}
|
||||
chatUser = ChatUser(id: client.peerId, firstName: client.name);
|
||||
}
|
||||
|
||||
|
||||
@@ -360,9 +360,9 @@ class FileModel extends ChangeNotifier {
|
||||
Future refresh({bool? isLocal}) async {
|
||||
if (isDesktop) {
|
||||
isLocal = isLocal ?? _isLocal;
|
||||
await isLocal
|
||||
? openDirectory(currentLocalDir.path, isLocal: isLocal)
|
||||
: openDirectory(currentRemoteDir.path, isLocal: isLocal);
|
||||
isLocal
|
||||
? await openDirectory(currentLocalDir.path, isLocal: isLocal)
|
||||
: await openDirectory(currentRemoteDir.path, isLocal: isLocal);
|
||||
} else {
|
||||
await openDirectory(currentDir.path);
|
||||
}
|
||||
@@ -393,7 +393,7 @@ class FileModel extends ChangeNotifier {
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint("Failed to openDirectory ${path} :$e");
|
||||
debugPrint("Failed to openDirectory $path: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,49 +559,55 @@ class FileModel extends ChangeNotifier {
|
||||
Future<bool?> showRemoveDialog(
|
||||
String title, String content, bool showCheckbox) async {
|
||||
return await parent.target?.dialogManager.show<bool>(
|
||||
(setState, Function(bool v) close) => CustomAlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.red),
|
||||
SizedBox(width: 20),
|
||||
Text(title)
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(content),
|
||||
SizedBox(height: 5),
|
||||
Text(translate("This is irreversible!"),
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
showCheckbox
|
||||
? CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate("Do this for all conflicts"),
|
||||
),
|
||||
value: removeCheckboxRemember,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => removeCheckboxRemember = v);
|
||||
},
|
||||
)
|
||||
: SizedBox.shrink()
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(false),
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(true),
|
||||
child: Text(translate("OK"))),
|
||||
]),
|
||||
useAnimation: false);
|
||||
(setState, Function(bool v) close) {
|
||||
cancel() => close(false);
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning, color: Colors.red),
|
||||
const SizedBox(width: 20),
|
||||
Text(title)
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(content),
|
||||
const SizedBox(height: 5),
|
||||
Text(translate("This is irreversible!"),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
showCheckbox
|
||||
? CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate("Do this for all conflicts"),
|
||||
),
|
||||
value: removeCheckboxRemember,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => removeCheckboxRemember = v);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink()
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: cancel,
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: submit,
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
}, useAnimation: false);
|
||||
}
|
||||
|
||||
bool fileConfirmCheckboxRemember = false;
|
||||
@@ -610,55 +616,59 @@ class FileModel extends ChangeNotifier {
|
||||
String title, String content, bool showCheckbox) async {
|
||||
fileConfirmCheckboxRemember = false;
|
||||
return await parent.target?.dialogManager.show<bool?>(
|
||||
(setState, Function(bool? v) close) => CustomAlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.red),
|
||||
SizedBox(width: 20),
|
||||
Text(title)
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
translate(
|
||||
"This file exists, skip or overwrite this file?"),
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 5),
|
||||
Text(content),
|
||||
showCheckbox
|
||||
? CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate("Do this for all conflicts"),
|
||||
),
|
||||
value: fileConfirmCheckboxRemember,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => fileConfirmCheckboxRemember = v);
|
||||
},
|
||||
)
|
||||
: SizedBox.shrink()
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(false),
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(null),
|
||||
child: Text(translate("Skip"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(true),
|
||||
child: Text(translate("OK"))),
|
||||
]),
|
||||
useAnimation: false);
|
||||
(setState, Function(bool? v) close) {
|
||||
cancel() => close(false);
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning, color: Colors.red),
|
||||
const SizedBox(width: 20),
|
||||
Text(title)
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(translate("This file exists, skip or overwrite this file?"),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 5),
|
||||
Text(content),
|
||||
showCheckbox
|
||||
? CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(
|
||||
translate("Do this for all conflicts"),
|
||||
),
|
||||
value: fileConfirmCheckboxRemember,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => fileConfirmCheckboxRemember = v);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink()
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: cancel,
|
||||
child: Text(translate("Cancel"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: () => close(null),
|
||||
child: Text(translate("Skip"))),
|
||||
TextButton(
|
||||
style: flatButtonStyle,
|
||||
onPressed: submit,
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
}, useAnimation: false);
|
||||
}
|
||||
|
||||
sendRemoveFile(String path, int fileNum, bool isLocal) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class FfiModel with ChangeNotifier {
|
||||
Display _display = Display();
|
||||
|
||||
var _inputBlocked = false;
|
||||
final _permissions = Map<String, bool>();
|
||||
final _permissions = <String, bool>{};
|
||||
bool? _secure;
|
||||
bool? _direct;
|
||||
bool _touchMode = false;
|
||||
@@ -71,12 +71,13 @@ class FfiModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void updatePermission(Map<String, dynamic> evt) {
|
||||
void updatePermission(Map<String, dynamic> evt, String id) {
|
||||
evt.forEach((k, v) {
|
||||
if (k == 'name' || k.isEmpty) return;
|
||||
_permissions[k] = v == 'true';
|
||||
});
|
||||
print('$_permissions');
|
||||
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
||||
debugPrint('$_permissions');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -146,7 +147,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'clipboard') {
|
||||
Clipboard.setData(ClipboardData(text: evt['content']));
|
||||
} else if (name == 'permission') {
|
||||
parent.target?.ffiModel.updatePermission(evt);
|
||||
parent.target?.ffiModel.updatePermission(evt, peerId);
|
||||
} else if (name == 'chat_client_mode') {
|
||||
parent.target?.chatModel
|
||||
.receive(ChatModel.clientModeID, evt['text'] ?? "");
|
||||
@@ -167,10 +168,8 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.loadLastJob(evt);
|
||||
} else if (name == 'update_folder_files') {
|
||||
parent.target?.fileModel.updateFolderFiles(evt);
|
||||
} else if (name == 'try_start_without_auth') {
|
||||
parent.target?.serverModel.loginRequest(evt);
|
||||
} else if (name == 'on_client_authorized') {
|
||||
parent.target?.serverModel.onClientAuthorized(evt);
|
||||
} else if (name == 'add_connection') {
|
||||
parent.target?.serverModel.addConnection(evt);
|
||||
} else if (name == 'on_client_remove') {
|
||||
parent.target?.serverModel.onClientRemove(evt);
|
||||
} else if (name == 'update_quality_status') {
|
||||
@@ -185,7 +184,7 @@ class FfiModel with ChangeNotifier {
|
||||
|
||||
/// Bind the event listener to receive events from the Rust core.
|
||||
void updateEventListener(String peerId) {
|
||||
final void Function(Map<String, dynamic>) cb = (evt) {
|
||||
cb(evt) {
|
||||
var name = evt['name'];
|
||||
if (name == 'msgbox') {
|
||||
handleMsgBox(evt, peerId);
|
||||
@@ -205,7 +204,7 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'clipboard') {
|
||||
Clipboard.setData(ClipboardData(text: evt['content']));
|
||||
} else if (name == 'permission') {
|
||||
parent.target?.ffiModel.updatePermission(evt);
|
||||
parent.target?.ffiModel.updatePermission(evt, peerId);
|
||||
} else if (name == 'chat_client_mode') {
|
||||
parent.target?.chatModel
|
||||
.receive(ChatModel.clientModeID, evt['text'] ?? "");
|
||||
@@ -226,10 +225,8 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.loadLastJob(evt);
|
||||
} else if (name == 'update_folder_files') {
|
||||
parent.target?.fileModel.updateFolderFiles(evt);
|
||||
} else if (name == 'try_start_without_auth') {
|
||||
parent.target?.serverModel.loginRequest(evt);
|
||||
} else if (name == 'on_client_authorized') {
|
||||
parent.target?.serverModel.onClientAuthorized(evt);
|
||||
} else if (name == 'add_connection') {
|
||||
parent.target?.serverModel.addConnection(evt);
|
||||
} else if (name == 'on_client_remove') {
|
||||
parent.target?.serverModel.onClientRemove(evt);
|
||||
} else if (name == 'update_quality_status') {
|
||||
@@ -239,7 +236,8 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == 'update_privacy_mode') {
|
||||
updatePrivacyMode(evt, peerId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
platformFFI.setEventCallback(cb);
|
||||
}
|
||||
|
||||
@@ -321,15 +319,15 @@ class FfiModel with ChangeNotifier {
|
||||
if (isPeerAndroid) {
|
||||
_touchMode = true;
|
||||
if (parent.target?.ffiModel.permissions['keyboard'] != false) {
|
||||
Timer(Duration(milliseconds: 100), showMobileActionsOverlay);
|
||||
Timer(const Duration(milliseconds: 100), showMobileActionsOverlay);
|
||||
}
|
||||
} else {
|
||||
_touchMode =
|
||||
await bind.sessionGetOption(id: peerId, arg: "touch-mode") != '';
|
||||
}
|
||||
|
||||
if (evt['is_file_transfer'] == "true") {
|
||||
// TODO is file transfer
|
||||
if (parent.target != null &&
|
||||
parent.target!.connType == ConnType.fileTransfer) {
|
||||
parent.target?.fileModel.onReady();
|
||||
} else {
|
||||
_pi.displays = [];
|
||||
@@ -464,15 +462,20 @@ enum ScrollStyle {
|
||||
}
|
||||
|
||||
class CanvasModel with ChangeNotifier {
|
||||
// image offset of canvas
|
||||
double _x = 0;
|
||||
// image offset of canvas
|
||||
double _y = 0;
|
||||
// image scale
|
||||
double _scale = 1.0;
|
||||
// the tabbar over the image
|
||||
double tabBarHeight = 0.0;
|
||||
// TODO multi canvas model
|
||||
String id = "";
|
||||
// scroll offset x percent
|
||||
double _scrollX = 0.0;
|
||||
// scroll offset y percent
|
||||
double _scrollY = 0.0;
|
||||
double _x = 0;
|
||||
double _y = 0;
|
||||
double _scale = 1.0;
|
||||
double _tabBarHeight = 0.0;
|
||||
String id = ""; // TODO multi canvas model
|
||||
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
@@ -492,9 +495,6 @@ class CanvasModel with ChangeNotifier {
|
||||
double get scrollX => _scrollX;
|
||||
double get scrollY => _scrollY;
|
||||
|
||||
set tabBarHeight(double h) => _tabBarHeight = h;
|
||||
double get tabBarHeight => _tabBarHeight;
|
||||
|
||||
void updateViewStyle() async {
|
||||
final style = await bind.sessionGetOption(id: id, arg: 'view-style');
|
||||
if (style == null) {
|
||||
@@ -548,12 +548,11 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
Size get size {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
return Size(size.width, size.height - _tabBarHeight);
|
||||
return Size(size.width, size.height - tabBarHeight);
|
||||
}
|
||||
|
||||
void moveDesktopMouse(double x, double y) {
|
||||
// On mobile platforms, move the canvas with the cursor.
|
||||
//if (!isDesktop) {
|
||||
final dw = getDisplayWidth() * _scale;
|
||||
final dh = getDisplayHeight() * _scale;
|
||||
var dxOffset = 0;
|
||||
@@ -579,8 +578,13 @@ class CanvasModel with ChangeNotifier {
|
||||
if (dxOffset != 0 || dyOffset != 0) {
|
||||
notifyListeners();
|
||||
}
|
||||
//}
|
||||
parent.target?.cursorModel.moveLocal(x, y);
|
||||
|
||||
// If keyboard is not permitted, do not move cursor when mouse is moving.
|
||||
if (parent.target != null) {
|
||||
if (parent.target!.ffiModel.keyboard()) {
|
||||
parent.target!.cursorModel.moveLocal(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set scale(v) {
|
||||
@@ -597,11 +601,8 @@ class CanvasModel with ChangeNotifier {
|
||||
if (isWebDesktop) {
|
||||
updateViewStyle();
|
||||
} else {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
final canvasWidth = size.width;
|
||||
final canvasHeight = size.height - _tabBarHeight;
|
||||
_x = (canvasWidth - getDisplayWidth() * _scale) / 2;
|
||||
_y = (canvasHeight - getDisplayHeight() * _scale) / 2;
|
||||
_x = (size.width - getDisplayWidth() * _scale) / 2;
|
||||
_y = (size.height - getDisplayHeight() * _scale) / 2;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -613,7 +614,7 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
void updateScale(double v) {
|
||||
if (parent.target?.imageModel.image == null) return;
|
||||
final offset = parent.target?.cursorModel.offset ?? Offset(0, 0);
|
||||
final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0);
|
||||
var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero;
|
||||
final px0 = (offset.dx - r.left) * _scale;
|
||||
final py0 = (offset.dy - r.top) * _scale;
|
||||
@@ -640,7 +641,7 @@ class CanvasModel with ChangeNotifier {
|
||||
|
||||
class CursorModel with ChangeNotifier {
|
||||
ui.Image? _image;
|
||||
final _images = Map<int, Tuple3<ui.Image, double, double>>();
|
||||
final _images = <int, Tuple3<ui.Image, double, double>>{};
|
||||
double _x = -10000;
|
||||
double _y = -10000;
|
||||
double _hotx = 0;
|
||||
@@ -807,7 +808,7 @@ class CursorModel with ChangeNotifier {
|
||||
// my throw exception, because the listener maybe already dispose
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
print('notify cursor: $e');
|
||||
debugPrint('notify cursor: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -915,6 +916,8 @@ extension ToString on MouseButtons {
|
||||
}
|
||||
}
|
||||
|
||||
enum ConnType { defaultConn, fileTransfer, portForward, rdp }
|
||||
|
||||
/// FFI class for communicating with the Rust core.
|
||||
class FFI {
|
||||
var id = "";
|
||||
@@ -923,6 +926,7 @@ class FFI {
|
||||
var alt = false;
|
||||
var command = false;
|
||||
var version = "";
|
||||
var connType = ConnType.defaultConn;
|
||||
|
||||
/// dialogManager use late to ensure init after main page binding [globalKey]
|
||||
late final dialogManager = OverlayDialogManager();
|
||||
@@ -1076,9 +1080,11 @@ class FFI {
|
||||
double tabBarHeight = 0.0}) {
|
||||
assert(!(isFileTransfer && isPortForward), "more than one connect type");
|
||||
if (isFileTransfer) {
|
||||
id = 'ft_${id}';
|
||||
connType = ConnType.fileTransfer;
|
||||
id = 'ft_$id';
|
||||
} else if (isPortForward) {
|
||||
id = 'pf_${id}';
|
||||
connType = ConnType.portForward;
|
||||
id = 'pf_$id';
|
||||
} else {
|
||||
chatModel.resetClientMode();
|
||||
canvasModel.id = id;
|
||||
@@ -1107,7 +1113,7 @@ class FFI {
|
||||
// every instance will bind a stream
|
||||
this.id = id;
|
||||
if (isFileTransfer) {
|
||||
this.fileModel.initFileFetcher();
|
||||
fileModel.initFileFetcher();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
import '../common.dart';
|
||||
import '../common/formatter/id_formatter.dart';
|
||||
import '../desktop/pages/server_page.dart' as Desktop;
|
||||
import '../desktop/widgets/tabbar_widget.dart';
|
||||
import '../mobile/pages/server_page.dart';
|
||||
@@ -29,10 +30,10 @@ class ServerModel with ChangeNotifier {
|
||||
String _temporaryPasswordLength = "";
|
||||
|
||||
late String _emptyIdShow;
|
||||
late final TextEditingController _serverId;
|
||||
late final IDTextEditingController _serverId;
|
||||
final _serverPasswd = TextEditingController(text: "");
|
||||
|
||||
final tabController = DesktopTabController();
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.cm);
|
||||
|
||||
List<Client> _clients = [];
|
||||
|
||||
@@ -88,7 +89,7 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
ServerModel(this.parent) {
|
||||
_emptyIdShow = translate("Generating ...");
|
||||
_serverId = TextEditingController(text: this._emptyIdShow);
|
||||
_serverId = IDTextEditingController(text: _emptyIdShow);
|
||||
|
||||
Timer.periodic(Duration(seconds: 1), (timer) async {
|
||||
var status = await bind.mainGetOnlineStatue();
|
||||
@@ -99,7 +100,7 @@ class ServerModel with ChangeNotifier {
|
||||
_connectStatus = status;
|
||||
notifyListeners();
|
||||
}
|
||||
final res = await bind.mainCheckClientsLength(length: _clients.length);
|
||||
final res = await bind.cmCheckClientsLength(length: _clients.length);
|
||||
if (res != null) {
|
||||
debugPrint("clients not match!");
|
||||
updateClientState(res);
|
||||
@@ -208,46 +209,48 @@ class ServerModel with ChangeNotifier {
|
||||
/// Toggle the screen sharing service.
|
||||
toggleService() async {
|
||||
if (_isStart) {
|
||||
final res = await parent.target?.dialogManager
|
||||
.show<bool>((setState, close) => CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_amber_sharp,
|
||||
color: Colors.redAccent, size: 28),
|
||||
SizedBox(width: 10),
|
||||
Text(translate("Warning")),
|
||||
]),
|
||||
content: Text(translate("android_stop_service_tip")),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(),
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
onPressed: () => close(true),
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
));
|
||||
final res =
|
||||
await parent.target?.dialogManager.show<bool>((setState, close) {
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
const Icon(Icons.warning_amber_sharp,
|
||||
color: Colors.redAccent, size: 28),
|
||||
const SizedBox(width: 10),
|
||||
Text(translate("Warning")),
|
||||
]),
|
||||
content: Text(translate("android_stop_service_tip")),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
if (res == true) {
|
||||
stopService();
|
||||
}
|
||||
} else {
|
||||
final res = await parent.target?.dialogManager
|
||||
.show<bool>((setState, close) => CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
Icon(Icons.warning_amber_sharp,
|
||||
color: Colors.redAccent, size: 28),
|
||||
SizedBox(width: 10),
|
||||
Text(translate("Warning")),
|
||||
]),
|
||||
content: Text(translate("android_service_will_start_tip")),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => close(),
|
||||
child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
onPressed: () => close(true),
|
||||
child: Text(translate("OK"))),
|
||||
],
|
||||
));
|
||||
final res =
|
||||
await parent.target?.dialogManager.show<bool>((setState, close) {
|
||||
submit() => close(true);
|
||||
return CustomAlertDialog(
|
||||
title: Row(children: [
|
||||
const Icon(Icons.warning_amber_sharp,
|
||||
color: Colors.redAccent, size: 28),
|
||||
const SizedBox(width: 10),
|
||||
Text(translate("Warning")),
|
||||
]),
|
||||
content: Text(translate("android_service_will_start_tip")),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
if (res == true) {
|
||||
startService();
|
||||
}
|
||||
@@ -300,7 +303,7 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
_fetchID() async {
|
||||
final old = _serverId.text;
|
||||
final old = _serverId.id;
|
||||
var count = 0;
|
||||
const maxCount = 10;
|
||||
while (count < maxCount) {
|
||||
@@ -309,12 +312,12 @@ class ServerModel with ChangeNotifier {
|
||||
if (id.isEmpty) {
|
||||
continue;
|
||||
} else {
|
||||
_serverId.text = id;
|
||||
_serverId.id = id;
|
||||
}
|
||||
|
||||
debugPrint("fetch id again at $count:id:${_serverId.text}");
|
||||
debugPrint("fetch id again at $count:id:${_serverId.id}");
|
||||
count++;
|
||||
if (_serverId.text != old) {
|
||||
if (_serverId.id != old) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -344,23 +347,21 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
// force
|
||||
updateClientState([String? json]) async {
|
||||
var res = await bind.mainGetClientsState();
|
||||
var res = await bind.cmGetClientsState();
|
||||
try {
|
||||
final List clientsJson = jsonDecode(res);
|
||||
if (isDesktop && clientsJson.isEmpty && _clients.isNotEmpty) {
|
||||
// exit cm when >1 peers to no peers
|
||||
exit(0);
|
||||
}
|
||||
_clients.clear();
|
||||
tabController.state.value.tabs.clear();
|
||||
for (var clientJson in clientsJson) {
|
||||
final client = Client.fromJson(clientJson);
|
||||
_clients.add(client);
|
||||
tabController.add(TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)));
|
||||
tabController.add(
|
||||
TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)),
|
||||
authorized: client.authorized);
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
@@ -368,70 +369,89 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void loginRequest(Map<String, dynamic> evt) {
|
||||
void addConnection(Map<String, dynamic> evt) {
|
||||
try {
|
||||
final client = Client.fromJson(jsonDecode(evt["client"]));
|
||||
if (_clients.any((c) => c.id == client.id)) {
|
||||
return;
|
||||
if (client.authorized) {
|
||||
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id));
|
||||
final index = _clients.indexWhere((c) => c.id == client.id);
|
||||
if (index < 0) {
|
||||
_clients.add(client);
|
||||
} else {
|
||||
_clients[index].authorized = true;
|
||||
}
|
||||
tabController.add(
|
||||
TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)),
|
||||
authorized: true);
|
||||
scrollToBottom();
|
||||
notifyListeners();
|
||||
} else {
|
||||
if (_clients.any((c) => c.id == client.id)) {
|
||||
return;
|
||||
}
|
||||
_clients.add(client);
|
||||
tabController.add(TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)));
|
||||
scrollToBottom();
|
||||
notifyListeners();
|
||||
if (isAndroid) showLoginDialog(client);
|
||||
}
|
||||
_clients.add(client);
|
||||
tabController.add(TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)));
|
||||
scrollToBottom();
|
||||
notifyListeners();
|
||||
if (isAndroid) showLoginDialog(client);
|
||||
} catch (e) {
|
||||
debugPrint("Failed to call loginRequest,error:$e");
|
||||
}
|
||||
}
|
||||
|
||||
void showLoginDialog(Client client) {
|
||||
parent.target?.dialogManager.show(
|
||||
(setState, close) => CustomAlertDialog(
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(translate(client.isFileTransfer
|
||||
? "File Connection"
|
||||
: "Screen Connection")),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
icon: Icon(Icons.close))
|
||||
]),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("Do you accept?")),
|
||||
clientInfo(client),
|
||||
Text(
|
||||
translate("android_new_connection_tip"),
|
||||
style: TextStyle(color: Colors.black54),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(translate("Dismiss")),
|
||||
onPressed: () {
|
||||
sendLoginResponse(client, false);
|
||||
close();
|
||||
}),
|
||||
ElevatedButton(
|
||||
child: Text(translate("Accept")),
|
||||
onPressed: () {
|
||||
sendLoginResponse(client, true);
|
||||
close();
|
||||
}),
|
||||
],
|
||||
parent.target?.dialogManager.show((setState, close) {
|
||||
cancel() {
|
||||
sendLoginResponse(client, false);
|
||||
close();
|
||||
}
|
||||
|
||||
submit() {
|
||||
sendLoginResponse(client, true);
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title:
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text(translate(
|
||||
client.isFileTransfer ? "File Connection" : "Screen Connection")),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
close();
|
||||
},
|
||||
icon: const Icon(Icons.close))
|
||||
]),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translate("Do you accept?")),
|
||||
clientInfo(client),
|
||||
Text(
|
||||
translate("android_new_connection_tip"),
|
||||
style: const TextStyle(color: Colors.black54),
|
||||
),
|
||||
tag: getLoginDialogTag(client.id));
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: cancel, child: Text(translate("Dismiss"))),
|
||||
ElevatedButton(onPressed: submit, child: Text(translate("Accept"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: cancel,
|
||||
);
|
||||
}, tag: getLoginDialogTag(client.id));
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
@@ -471,14 +491,18 @@ class ServerModel with ChangeNotifier {
|
||||
} else {
|
||||
_clients[index].authorized = true;
|
||||
}
|
||||
tabController.add(TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)));
|
||||
tabController.add(
|
||||
TabInfo(
|
||||
key: client.id.toString(),
|
||||
label: client.name,
|
||||
closable: false,
|
||||
page: Desktop.buildConnectionCard(client)),
|
||||
authorized: true);
|
||||
scrollToBottom();
|
||||
notifyListeners();
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
debugPrint("onClientAuthorized:$e");
|
||||
}
|
||||
}
|
||||
|
||||
void onClientRemove(Map<String, dynamic> evt) {
|
||||
@@ -486,8 +510,10 @@ class ServerModel with ChangeNotifier {
|
||||
final id = int.parse(evt['id'] as String);
|
||||
if (_clients.any((c) => c.id == id)) {
|
||||
final index = _clients.indexWhere((client) => client.id == id);
|
||||
_clients.removeAt(index);
|
||||
tabController.remove(index);
|
||||
if (index >= 0) {
|
||||
_clients.removeAt(index);
|
||||
tabController.remove(index);
|
||||
}
|
||||
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
|
||||
parent.target?.invokeMethod("cancel_notification", id);
|
||||
}
|
||||
@@ -558,24 +584,29 @@ String getLoginDialogTag(int id) {
|
||||
}
|
||||
|
||||
showInputWarnAlert(FFI ffi) {
|
||||
ffi.dialogManager.show((setState, close) => CustomAlertDialog(
|
||||
title: Text(translate("How to get Android input permission?")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(translate("android_input_permission_tip1")),
|
||||
SizedBox(height: 10),
|
||||
Text(translate("android_input_permission_tip2")),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(child: Text(translate("Cancel")), onPressed: close),
|
||||
ElevatedButton(
|
||||
child: Text(translate("Open System Setting")),
|
||||
onPressed: () {
|
||||
ffi.serverModel.initInput();
|
||||
close();
|
||||
}),
|
||||
ffi.dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
ffi.serverModel.initInput();
|
||||
close();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("How to get Android input permission?")),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(translate("android_input_permission_tip1")),
|
||||
const SizedBox(height: 10),
|
||||
Text(translate("android_input_permission_tip2")),
|
||||
],
|
||||
));
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
||||
ElevatedButton(
|
||||
onPressed: submit, child: Text(translate("Open System Setting"))),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
// we have custom window frame
|
||||
gtk_window_set_decorated(window, FALSE);
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
|
||||
@@ -62,7 +62,7 @@ dependencies:
|
||||
desktop_multi_window:
|
||||
git:
|
||||
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
||||
ref: e0368a023ba195462acc00d33ab361b499f0e413
|
||||
ref: fee851fa43116e0b91c39acd0ec37063dc6015f8
|
||||
freezed_annotation: ^2.0.3
|
||||
tray_manager:
|
||||
git:
|
||||
|
||||
Reference in New Issue
Block a user