mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge branch 'ios_1.2.2'
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/shared_state.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
@@ -1225,76 +1225,9 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId);
|
||||
qualityInitValue =
|
||||
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
|
||||
const qualityMinValue = 10.0;
|
||||
const qualityMoreThresholdValue = 100.0;
|
||||
const qualityMaxValue = 2000.0;
|
||||
if (qualityInitValue < qualityMinValue) {
|
||||
qualityInitValue = qualityMinValue;
|
||||
if (qualityInitValue < 10 || qualityInitValue > 2000) {
|
||||
qualityInitValue = 50;
|
||||
}
|
||||
if (qualityInitValue > qualityMaxValue) {
|
||||
qualityInitValue = qualityMaxValue;
|
||||
}
|
||||
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
|
||||
final moreQualityInitValue = qualityInitValue > qualityMoreThresholdValue;
|
||||
final RxBool moreQualityChecked = RxBool(moreQualityInitValue);
|
||||
final debouncerQuality = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setCustomValues(quality: v);
|
||||
},
|
||||
initialValue: qualityInitValue,
|
||||
);
|
||||
final qualitySlider = Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Slider(
|
||||
value: qualitySliderValue.value,
|
||||
min: qualityMinValue,
|
||||
max: moreQualityChecked.value
|
||||
? qualityMaxValue
|
||||
: qualityMoreThresholdValue,
|
||||
divisions: 18,
|
||||
onChanged: (double value) {
|
||||
qualitySliderValue.value = value;
|
||||
debouncerQuality.value = value;
|
||||
},
|
||||
)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${qualitySliderValue.value.round()}%',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
translate('Bitrate'),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: moreQualityChecked.value,
|
||||
onChanged: (bool? value) {
|
||||
moreQualityChecked.value = value!;
|
||||
if (!value &&
|
||||
qualitySliderValue.value >
|
||||
qualityMoreThresholdValue) {
|
||||
qualitySliderValue.value = qualityMoreThresholdValue;
|
||||
debouncerQuality.value = qualityMoreThresholdValue;
|
||||
}
|
||||
},
|
||||
).marginOnly(right: 5),
|
||||
Expanded(
|
||||
child: Text(translate('More')),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
));
|
||||
// fps
|
||||
final fpsOption =
|
||||
await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps');
|
||||
@@ -1302,55 +1235,20 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
|
||||
if (fpsInitValue < 5 || fpsInitValue > 120) {
|
||||
fpsInitValue = 30;
|
||||
}
|
||||
final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
|
||||
final debouncerFps = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setCustomValues(fps: v);
|
||||
},
|
||||
initialValue: qualityInitValue,
|
||||
);
|
||||
bool? direct;
|
||||
try {
|
||||
direct =
|
||||
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
|
||||
} catch (_) {}
|
||||
final fpsSlider = Offstage(
|
||||
offstage: (await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Obx((() => Slider(
|
||||
value: fpsSliderValue.value,
|
||||
min: 5,
|
||||
max: 120,
|
||||
divisions: 23,
|
||||
onChanged: (double value) {
|
||||
fpsSliderValue.value = value;
|
||||
debouncerFps.value = value;
|
||||
},
|
||||
)))),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Obx(() => Text(
|
||||
'${fpsSliderValue.value.round()}',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
))),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
translate('FPS'),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
|
||||
version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
|
||||
|
||||
final content = Column(
|
||||
children: [qualitySlider, fpsSlider],
|
||||
);
|
||||
final content = customImageQualityWidget(
|
||||
initQuality: qualityInitValue,
|
||||
initFps: fpsInitValue,
|
||||
setQuality: (v) => setCustomValues(quality: v),
|
||||
setFps: (v) => setCustomValues(fps: v),
|
||||
showFps: !notShowFps);
|
||||
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,15 +26,31 @@ class DraggableChatWindow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Draggable(
|
||||
return isIOS
|
||||
? IOSDraggable (
|
||||
position: position,
|
||||
chatModel: chatModel,
|
||||
width: width,
|
||||
height: height,
|
||||
builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildMobileAppBar(context),
|
||||
Expanded(
|
||||
child: ChatPage(chatModel: chatModel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
: Draggable(
|
||||
checkKeyboard: true,
|
||||
position: position,
|
||||
width: width,
|
||||
height: height,
|
||||
builder: (context, onPanUpdate) {
|
||||
final child = isIOS
|
||||
? ChatPage(chatModel: chatModel)
|
||||
: Scaffold(
|
||||
final child =
|
||||
Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: CustomAppBar(
|
||||
onPanUpdate: onPanUpdate,
|
||||
@@ -331,6 +347,69 @@ class _DraggableState extends State<Draggable> {
|
||||
}
|
||||
}
|
||||
|
||||
class IOSDraggable extends StatefulWidget {
|
||||
const IOSDraggable({
|
||||
Key? key,
|
||||
this.position = Offset.zero,
|
||||
required this.chatModel,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.builder})
|
||||
: super(key: key);
|
||||
|
||||
final Offset position;
|
||||
final ChatModel chatModel;
|
||||
final double width;
|
||||
final double height;
|
||||
final Widget Function(BuildContext) builder;
|
||||
|
||||
@override
|
||||
_IOSDraggableState createState() => _IOSDraggableState();
|
||||
}
|
||||
|
||||
class _IOSDraggableState extends State<IOSDraggable> {
|
||||
late Offset _position;
|
||||
late ChatModel _chatModel;
|
||||
late double _width;
|
||||
late double _height;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_position = widget.position;
|
||||
_chatModel = widget.chatModel;
|
||||
_width = widget.width;
|
||||
_height = widget.height;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: _position.dx,
|
||||
top: _position.dy,
|
||||
child: GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
setState(() {
|
||||
_position += details.delta;
|
||||
});
|
||||
},
|
||||
child: Material(
|
||||
child:
|
||||
Container(
|
||||
width: _width,
|
||||
height: _height,
|
||||
child: widget.builder(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QualityMonitor extends StatelessWidget {
|
||||
final QualityMonitorModel qualityMonitorModel;
|
||||
QualityMonitor(this.qualityMonitorModel);
|
||||
|
||||
277
flutter/lib/common/widgets/setting_widgets.dart
Normal file
277
flutter/lib/common/widgets/setting_widgets.dart
Normal file
@@ -0,0 +1,277 @@
|
||||
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
customImageQualityWidget(
|
||||
{required double initQuality,
|
||||
required double initFps,
|
||||
required Function(double) setQuality,
|
||||
required Function(double) setFps,
|
||||
required bool showFps}) {
|
||||
final qualityValue = initQuality.obs;
|
||||
final fpsValue = initFps.obs;
|
||||
|
||||
final RxBool moreQualityChecked = RxBool(qualityValue.value > 100);
|
||||
final debouncerQuality = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setQuality(v);
|
||||
},
|
||||
initialValue: qualityValue.value,
|
||||
);
|
||||
final debouncerFps = Debouncer<double>(
|
||||
Duration(milliseconds: 1000),
|
||||
onChanged: (double v) {
|
||||
setFps(v);
|
||||
},
|
||||
initialValue: fpsValue.value,
|
||||
);
|
||||
|
||||
onMoreChanged(bool? value) {
|
||||
if (value == null) return;
|
||||
moreQualityChecked.value = value;
|
||||
if (!value && qualityValue.value > 100) {
|
||||
qualityValue.value = 100;
|
||||
}
|
||||
debouncerQuality.value = qualityValue.value;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Slider(
|
||||
value: qualityValue.value,
|
||||
min: 10.0,
|
||||
max: moreQualityChecked.value ? 2000 : 100,
|
||||
divisions: moreQualityChecked.value ? 199 : 18,
|
||||
onChanged: (double value) async {
|
||||
qualityValue.value = value;
|
||||
debouncerQuality.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${qualityValue.value.round()}%',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
Expanded(
|
||||
flex: isMobile ? 2 : 1,
|
||||
child: Text(
|
||||
translate('Bitrate'),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
// mobile doesn't have enough space
|
||||
if (!isMobile)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: moreQualityChecked.value,
|
||||
onChanged: onMoreChanged,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(translate('More')),
|
||||
)
|
||||
],
|
||||
))
|
||||
],
|
||||
)),
|
||||
if (isMobile)
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Checkbox(
|
||||
value: moreQualityChecked.value,
|
||||
onChanged: onMoreChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(translate('More')),
|
||||
)
|
||||
],
|
||||
)),
|
||||
if (showFps)
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Slider(
|
||||
value: fpsValue.value,
|
||||
min: 5.0,
|
||||
max: 120.0,
|
||||
divisions: 23,
|
||||
onChanged: (double value) async {
|
||||
fpsValue.value = value;
|
||||
debouncerFps.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${fpsValue.value.round()}',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
translate('FPS'),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
))
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
customImageQualitySetting() {
|
||||
final qualityKey = 'custom_image_quality';
|
||||
final fpsKey = 'custom-fps';
|
||||
|
||||
var initQuality =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? 50.0);
|
||||
if (initQuality < 10 || initQuality > 2000) {
|
||||
initQuality = 50;
|
||||
}
|
||||
var initFps =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0);
|
||||
if (initFps < 5 || initFps > 120) {
|
||||
initFps = 30;
|
||||
}
|
||||
|
||||
return customImageQualityWidget(
|
||||
initQuality: initQuality,
|
||||
initFps: initFps,
|
||||
setQuality: (v) {
|
||||
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
|
||||
},
|
||||
setFps: (v) {
|
||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||
},
|
||||
showFps: true);
|
||||
}
|
||||
|
||||
Future<bool> setServerConfig(
|
||||
List<TextEditingController> controllers,
|
||||
List<RxString> errMsgs,
|
||||
ServerConfig config,
|
||||
) async {
|
||||
config.idServer = config.idServer.trim();
|
||||
config.relayServer = config.relayServer.trim();
|
||||
config.apiServer = config.apiServer.trim();
|
||||
config.key = config.key.trim();
|
||||
// id
|
||||
if (config.idServer.isNotEmpty) {
|
||||
errMsgs[0].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.idServer));
|
||||
if (errMsgs[0].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// relay
|
||||
if (config.relayServer.isNotEmpty) {
|
||||
errMsgs[1].value =
|
||||
translate(await bind.mainTestIfValidServer(server: config.relayServer));
|
||||
if (errMsgs[1].isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// api
|
||||
if (config.apiServer.isNotEmpty) {
|
||||
if (!config.apiServer.startsWith('http://') &&
|
||||
!config.apiServer.startsWith('https://')) {
|
||||
errMsgs[2].value =
|
||||
'${translate("API Server")}: ${translate("invalid_http")}';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final oldApiServer = await bind.mainGetApiServer();
|
||||
|
||||
// should set one by one
|
||||
await bind.mainSetOption(
|
||||
key: 'custom-rendezvous-server', value: config.idServer);
|
||||
await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
|
||||
await bind.mainSetOption(key: 'api-server', value: config.apiServer);
|
||||
await bind.mainSetOption(key: 'key', value: config.key);
|
||||
|
||||
final newApiServer = await bind.mainGetApiServer();
|
||||
if (oldApiServer.isNotEmpty &&
|
||||
oldApiServer != newApiServer &&
|
||||
gFFI.userModel.isLogin) {
|
||||
gFFI.userModel.logOut(apiServer: oldApiServer);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
List<Widget> ServerConfigImportExportWidgets(
|
||||
List<TextEditingController> controllers,
|
||||
List<RxString> errMsgs,
|
||||
) {
|
||||
import() {
|
||||
Clipboard.getData(Clipboard.kTextPlain).then((value) {
|
||||
final text = value?.text;
|
||||
if (text != null && text.isNotEmpty) {
|
||||
try {
|
||||
final sc = ServerConfig.decode(text);
|
||||
if (sc.idServer.isNotEmpty) {
|
||||
controllers[0].text = sc.idServer;
|
||||
controllers[1].text = sc.relayServer;
|
||||
controllers[2].text = sc.apiServer;
|
||||
controllers[3].text = sc.key;
|
||||
Future<bool> success = setServerConfig(controllers, errMsgs, sc);
|
||||
success.then((value) {
|
||||
if (value) {
|
||||
showToast(
|
||||
translate('Import server configuration successfully'));
|
||||
} else {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
} else {
|
||||
showToast(translate('Clipboard is empty'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export() {
|
||||
final text = ServerConfig(
|
||||
idServer: controllers[0].text.trim(),
|
||||
relayServer: controllers[1].text.trim(),
|
||||
apiServer: controllers[2].text.trim(),
|
||||
key: controllers[3].text.trim())
|
||||
.encode();
|
||||
debugPrint("ServerConfig export: $text");
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
showToast(translate('Export server configuration successfully'));
|
||||
}
|
||||
|
||||
return [
|
||||
Tooltip(
|
||||
message: translate('Import Server Config'),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.paste, color: Colors.grey), onPressed: import),
|
||||
),
|
||||
Tooltip(
|
||||
message: translate('Export Server Config'),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.copy, color: Colors.grey), onPressed: export))
|
||||
];
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
|
||||
@@ -965,54 +966,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
var relayController = TextEditingController(text: old('relay-server'));
|
||||
var apiController = TextEditingController(text: old('api-server'));
|
||||
var keyController = TextEditingController(text: old('key'));
|
||||
|
||||
set(String idServer, String relayServer, String apiServer,
|
||||
String key) async {
|
||||
idServer = idServer.trim();
|
||||
relayServer = relayServer.trim();
|
||||
apiServer = apiServer.trim();
|
||||
key = key.trim();
|
||||
if (idServer.isNotEmpty) {
|
||||
idErrMsg.value =
|
||||
translate(await bind.mainTestIfValidServer(server: idServer));
|
||||
if (idErrMsg.isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (relayServer.isNotEmpty) {
|
||||
relayErrMsg.value =
|
||||
translate(await bind.mainTestIfValidServer(server: relayServer));
|
||||
if (relayErrMsg.isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (apiServer.isNotEmpty) {
|
||||
if (!apiServer.startsWith('http://') &&
|
||||
!apiServer.startsWith('https://')) {
|
||||
apiErrMsg.value =
|
||||
'${translate("API Server")}: ${translate("invalid_http")}';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final oldApiServer = await bind.mainGetApiServer();
|
||||
|
||||
// should set one by one
|
||||
await bind.mainSetOption(
|
||||
key: 'custom-rendezvous-server', value: idServer);
|
||||
await bind.mainSetOption(key: 'relay-server', value: relayServer);
|
||||
await bind.mainSetOption(key: 'api-server', value: apiServer);
|
||||
await bind.mainSetOption(key: 'key', value: key);
|
||||
|
||||
final newApiServer = await bind.mainGetApiServer();
|
||||
if (oldApiServer.isNotEmpty && oldApiServer != newApiServer) {
|
||||
await gFFI.userModel.logOut(apiServer: oldApiServer);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
final controllers = [
|
||||
idController,
|
||||
relayController,
|
||||
apiController,
|
||||
keyController,
|
||||
];
|
||||
final errMsgs = [
|
||||
idErrMsg,
|
||||
relayErrMsg,
|
||||
apiErrMsg,
|
||||
];
|
||||
|
||||
submit() async {
|
||||
bool result = await set(idController.text, relayController.text,
|
||||
apiController.text, keyController.text);
|
||||
bool result = await setServerConfig(
|
||||
controllers,
|
||||
errMsgs,
|
||||
ServerConfig(
|
||||
idServer: idController.text,
|
||||
relayServer: relayController.text,
|
||||
apiServer: apiController.text,
|
||||
key: keyController.text));
|
||||
if (result) {
|
||||
setState(() {});
|
||||
showToast(translate('Successful'));
|
||||
@@ -1021,83 +995,28 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
}
|
||||
|
||||
import() {
|
||||
Clipboard.getData(Clipboard.kTextPlain).then((value) {
|
||||
final text = value?.text;
|
||||
if (text != null && text.isNotEmpty) {
|
||||
try {
|
||||
final sc = ServerConfig.decode(text);
|
||||
if (sc.idServer.isNotEmpty) {
|
||||
idController.text = sc.idServer;
|
||||
relayController.text = sc.relayServer;
|
||||
apiController.text = sc.apiServer;
|
||||
keyController.text = sc.key;
|
||||
Future<bool> success =
|
||||
set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
|
||||
success.then((value) {
|
||||
if (value) {
|
||||
showToast(
|
||||
translate('Import server configuration successfully'));
|
||||
} else {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(translate('Invalid server configuration'));
|
||||
}
|
||||
} else {
|
||||
showToast(translate('Clipboard is empty'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export() {
|
||||
final text = ServerConfig(
|
||||
idServer: idController.text,
|
||||
relayServer: relayController.text,
|
||||
apiServer: apiController.text,
|
||||
key: keyController.text)
|
||||
.encode();
|
||||
debugPrint("ServerConfig export: $text");
|
||||
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
showToast(translate('Export server configuration successfully'));
|
||||
}
|
||||
|
||||
bool secure = !enabled;
|
||||
return _Card(title: 'ID/Relay Server', title_suffix: [
|
||||
Tooltip(
|
||||
message: translate('Import Server Config'),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.paste, color: Colors.grey),
|
||||
onPressed: enabled ? import : null),
|
||||
),
|
||||
Tooltip(
|
||||
message: translate('Export Server Config'),
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.copy, color: Colors.grey),
|
||||
onPressed: enabled ? export : null)),
|
||||
], children: [
|
||||
Column(
|
||||
return _Card(
|
||||
title: 'ID/Relay Server',
|
||||
title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
|
||||
children: [
|
||||
Obx(() => _LabeledTextField(context, 'ID Server', idController,
|
||||
idErrMsg.value, enabled, secure)),
|
||||
Obx(() => _LabeledTextField(context, 'Relay Server',
|
||||
relayController, relayErrMsg.value, enabled, secure)),
|
||||
Obx(() => _LabeledTextField(context, 'API Server', apiController,
|
||||
apiErrMsg.value, enabled, secure)),
|
||||
_LabeledTextField(
|
||||
context, 'Key', keyController, '', enabled, secure),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [_Button('Apply', submit, enabled: enabled)],
|
||||
).marginOnly(top: 10),
|
||||
],
|
||||
)
|
||||
]);
|
||||
Column(
|
||||
children: [
|
||||
Obx(() => _LabeledTextField(context, 'ID Server', idController,
|
||||
idErrMsg.value, enabled, secure)),
|
||||
Obx(() => _LabeledTextField(context, 'Relay Server',
|
||||
relayController, relayErrMsg.value, enabled, secure)),
|
||||
Obx(() => _LabeledTextField(context, 'API Server',
|
||||
apiController, apiErrMsg.value, enabled, secure)),
|
||||
_LabeledTextField(
|
||||
context, 'Key', keyController, '', enabled, secure),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [_Button('Apply', submit, enabled: enabled)],
|
||||
).marginOnly(top: 10),
|
||||
],
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
return tmpWrapper();
|
||||
@@ -1181,15 +1100,6 @@ class _DisplayState extends State<_Display> {
|
||||
}
|
||||
|
||||
final groupValue = bind.mainGetUserDefaultOption(key: key);
|
||||
final qualityKey = 'custom_image_quality';
|
||||
final qualityValue =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
||||
50.0)
|
||||
.obs;
|
||||
final fpsKey = 'custom-fps';
|
||||
final fpsValue =
|
||||
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0)
|
||||
.obs;
|
||||
return _Card(title: 'Default Image Quality', children: [
|
||||
_Radio(context,
|
||||
value: kRemoteImageQualityBest,
|
||||
@@ -1213,64 +1123,7 @@ class _DisplayState extends State<_Display> {
|
||||
onChanged: onChanged),
|
||||
Offstage(
|
||||
offstage: groupValue != kRemoteImageQualityCustom,
|
||||
child: Column(
|
||||
children: [
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Slider(
|
||||
value: qualityValue.value,
|
||||
min: 10.0,
|
||||
max: 100.0,
|
||||
divisions: 18,
|
||||
onChanged: (double value) async {
|
||||
qualityValue.value = value;
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: qualityKey, value: value.toString());
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 40,
|
||||
child: Text(
|
||||
'${qualityValue.value.round()}%',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Text(
|
||||
translate('Bitrate'),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
))
|
||||
],
|
||||
)),
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Slider(
|
||||
value: fpsValue.value,
|
||||
min: 5.0,
|
||||
max: 120.0,
|
||||
divisions: 23,
|
||||
onChanged: (double value) async {
|
||||
fpsValue.value = value;
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: fpsKey, value: value.toString());
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 40,
|
||||
child: Text(
|
||||
'${fpsValue.value.round()}',
|
||||
style: const TextStyle(fontSize: 15),
|
||||
)),
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Text(
|
||||
translate('FPS'),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
))
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
child: customImageQualitySetting(),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ class _AppState extends State<App> {
|
||||
themeMode: MyTheme.currentThemeMode(),
|
||||
home: isDesktop
|
||||
? const DesktopTabPage()
|
||||
: !isAndroid
|
||||
: isWeb
|
||||
? WebHomePage()
|
||||
: HomePage(),
|
||||
localizationsDelegates: const [
|
||||
|
||||
@@ -28,7 +28,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
|
||||
final title = translate("Connection");
|
||||
|
||||
@override
|
||||
final appBarActions = !isAndroid ? <Widget>[const WebMenu()] : <Widget>[];
|
||||
final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[];
|
||||
|
||||
@override
|
||||
State<ConnectionPage> createState() => _ConnectionPageState();
|
||||
@@ -211,25 +211,6 @@ class WebMenu extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _WebMenuState extends State<WebMenu> {
|
||||
String url = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
() async {
|
||||
final urlRes = await bind.mainGetApiServer();
|
||||
var update = false;
|
||||
if (urlRes != url) {
|
||||
url = urlRes;
|
||||
update = true;
|
||||
}
|
||||
|
||||
if (update) {
|
||||
setState(() {});
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
@@ -251,16 +232,14 @@ class _WebMenuState extends State<WebMenu> {
|
||||
child: Text(translate('ID/Relay Server')),
|
||||
)
|
||||
] +
|
||||
(url.contains('admin.rustdesk.com')
|
||||
? <PopupMenuItem<String>>[]
|
||||
: [
|
||||
PopupMenuItem(
|
||||
value: "login",
|
||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate("Login")
|
||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
||||
)
|
||||
]) +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "login",
|
||||
child: Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate("Login")
|
||||
: '${translate("Logout")} (${gFFI.userModel.userName.value})'),
|
||||
)
|
||||
] +
|
||||
[
|
||||
PopupMenuItem(
|
||||
value: "about",
|
||||
|
||||
@@ -494,7 +494,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
gFFI.ffiModel.toggleTouchMode();
|
||||
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
||||
bind.sessionPeerOption(
|
||||
sessionId: sessionId, name: "touch", value: v);
|
||||
sessionId: sessionId, name: "touch-mode", value: v);
|
||||
})));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
@@ -383,7 +383,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
SettingsSection(
|
||||
title: Text(translate('Account')),
|
||||
tiles: [
|
||||
SettingsTile.navigation(
|
||||
SettingsTile(
|
||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate('Login')
|
||||
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
|
||||
@@ -399,19 +399,19 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
],
|
||||
),
|
||||
SettingsSection(title: Text(translate("Settings")), tiles: [
|
||||
SettingsTile.navigation(
|
||||
SettingsTile(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
leading: Icon(Icons.cloud),
|
||||
onPressed: (context) {
|
||||
showServerSettings(gFFI.dialogManager);
|
||||
}),
|
||||
SettingsTile.navigation(
|
||||
SettingsTile(
|
||||
title: Text(translate('Language')),
|
||||
leading: Icon(Icons.translate),
|
||||
onPressed: (context) {
|
||||
showLanguageSettings(gFFI.dialogManager);
|
||||
}),
|
||||
SettingsTile.navigation(
|
||||
SettingsTile(
|
||||
title: Text(translate(
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? 'Dark Theme'
|
||||
@@ -424,45 +424,50 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
},
|
||||
)
|
||||
]),
|
||||
SettingsSection(
|
||||
title: Text(translate("Recording")),
|
||||
tiles: [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Automatically record incoming sessions')),
|
||||
leading: Icon(Icons.videocam),
|
||||
description: FutureBuilder(
|
||||
builder: (ctx, data) => Offstage(
|
||||
offstage: !data.hasData,
|
||||
child: Text("${translate("Directory")}: ${data.data}")),
|
||||
future: bind.mainDefaultVideoSaveDirectory()),
|
||||
initialValue: _autoRecordIncomingSession,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: "allow-auto-record-incoming",
|
||||
value: bool2option("allow-auto-record-incoming", v));
|
||||
final newValue = option2bool(
|
||||
'allow-auto-record-incoming',
|
||||
await bind.mainGetOption(
|
||||
key: 'allow-auto-record-incoming'));
|
||||
setState(() {
|
||||
_autoRecordIncomingSession = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsSection(
|
||||
title: Text(translate("Share Screen")),
|
||||
tiles: shareScreenTiles,
|
||||
),
|
||||
SettingsSection(
|
||||
title: Text(translate("Enhancements")),
|
||||
tiles: enhancementsTiles,
|
||||
),
|
||||
if (isAndroid)
|
||||
SettingsSection(
|
||||
title: Text(translate("Recording")),
|
||||
tiles: [
|
||||
SettingsTile.switchTile(
|
||||
title:
|
||||
Text(translate('Automatically record incoming sessions')),
|
||||
leading: Icon(Icons.videocam),
|
||||
description: FutureBuilder(
|
||||
builder: (ctx, data) => Offstage(
|
||||
offstage: !data.hasData,
|
||||
child: Text("${translate("Directory")}: ${data.data}")),
|
||||
future: bind.mainDefaultVideoSaveDirectory()),
|
||||
initialValue: _autoRecordIncomingSession,
|
||||
onToggle: (v) async {
|
||||
await bind.mainSetOption(
|
||||
key: "allow-auto-record-incoming",
|
||||
value: bool2option("allow-auto-record-incoming", v));
|
||||
final newValue = option2bool(
|
||||
'allow-auto-record-incoming',
|
||||
await bind.mainGetOption(
|
||||
key: 'allow-auto-record-incoming'));
|
||||
setState(() {
|
||||
_autoRecordIncomingSession = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isAndroid)
|
||||
SettingsSection(
|
||||
title: Text(translate("Share Screen")),
|
||||
tiles: shareScreenTiles,
|
||||
),
|
||||
defaultDisplaySection(),
|
||||
if (isAndroid)
|
||||
SettingsSection(
|
||||
title: Text(translate("Enhancements")),
|
||||
tiles: enhancementsTiles,
|
||||
),
|
||||
SettingsSection(
|
||||
title: Text(translate("About")),
|
||||
tiles: [
|
||||
SettingsTile.navigation(
|
||||
SettingsTile(
|
||||
onPressed: (context) async {
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
@@ -477,21 +482,22 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
)),
|
||||
),
|
||||
leading: Icon(Icons.info)),
|
||||
SettingsTile.navigation(
|
||||
SettingsTile(
|
||||
title: Text(translate("Build Date")),
|
||||
value: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(_buildDate),
|
||||
),
|
||||
leading: Icon(Icons.query_builder)),
|
||||
SettingsTile.navigation(
|
||||
onPressed: (context) => onCopyFingerprint(_fingerprint),
|
||||
title: Text(translate("Fingerprint")),
|
||||
value: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(_fingerprint),
|
||||
),
|
||||
leading: Icon(Icons.fingerprint)),
|
||||
if (isAndroid)
|
||||
SettingsTile(
|
||||
onPressed: (context) => onCopyFingerprint(_fingerprint),
|
||||
title: Text(translate("Fingerprint")),
|
||||
value: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(_fingerprint),
|
||||
),
|
||||
leading: Icon(Icons.fingerprint)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -508,6 +514,23 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultDisplaySection() {
|
||||
return SettingsSection(
|
||||
title: Text(translate("Display Settings")),
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
title: Text(translate('Display Settings')),
|
||||
leading: Icon(Icons.desktop_windows_outlined),
|
||||
trailing: Icon(Icons.arrow_forward_ios),
|
||||
onPressed: (context) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return _DisplayPage();
|
||||
}));
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||
@@ -618,3 +641,181 @@ class ScanButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DisplayPage extends StatefulWidget {
|
||||
const _DisplayPage({super.key});
|
||||
|
||||
@override
|
||||
State<_DisplayPage> createState() => __DisplayPageState();
|
||||
}
|
||||
|
||||
class __DisplayPageState extends State<_DisplayPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
|
||||
final h264 = codecsJson['h264'] ?? false;
|
||||
final h265 = codecsJson['h265'] ?? false;
|
||||
var codecList = [
|
||||
_RadioEntry('Auto', 'auto'),
|
||||
_RadioEntry('VP8', 'vp8'),
|
||||
_RadioEntry('VP9', 'vp9'),
|
||||
_RadioEntry('AV1', 'av1'),
|
||||
if (h264) _RadioEntry('H264', 'h264'),
|
||||
if (h265) _RadioEntry('H265', 'h265')
|
||||
];
|
||||
RxBool showCustomImageQuality = false.obs;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: Icon(Icons.arrow_back_ios)),
|
||||
title: Text(translate('Display Settings')),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SettingsList(sections: [
|
||||
SettingsSection(
|
||||
tiles: [
|
||||
_getPopupDialogRadioEntry(
|
||||
title: 'Default View Style',
|
||||
list: [
|
||||
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
|
||||
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
|
||||
],
|
||||
getter: () => bind.mainGetUserDefaultOption(key: 'view_style'),
|
||||
asyncSetter: (value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: 'view_style', value: value);
|
||||
},
|
||||
),
|
||||
_getPopupDialogRadioEntry(
|
||||
title: 'Default Image Quality',
|
||||
list: [
|
||||
_RadioEntry('Good image quality', kRemoteImageQualityBest),
|
||||
_RadioEntry('Balanced', kRemoteImageQualityBalanced),
|
||||
_RadioEntry('Optimize reaction time', kRemoteImageQualityLow),
|
||||
_RadioEntry('Custom', kRemoteImageQualityCustom),
|
||||
],
|
||||
getter: () {
|
||||
final v = bind.mainGetUserDefaultOption(key: 'image_quality');
|
||||
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
|
||||
return v;
|
||||
},
|
||||
asyncSetter: (value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: 'image_quality', value: value);
|
||||
showCustomImageQuality.value =
|
||||
value == kRemoteImageQualityCustom;
|
||||
},
|
||||
tail: customImageQualitySetting(),
|
||||
showTail: showCustomImageQuality,
|
||||
notCloseValue: kRemoteImageQualityCustom,
|
||||
),
|
||||
_getPopupDialogRadioEntry(
|
||||
title: 'Default Codec',
|
||||
list: codecList,
|
||||
getter: () =>
|
||||
bind.mainGetUserDefaultOption(key: 'codec-preference'),
|
||||
asyncSetter: (value) async {
|
||||
await bind.mainSetUserDefaultOption(
|
||||
key: 'codec-preference', value: value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsSection(
|
||||
title: Text(translate('Other Default Options')),
|
||||
tiles: [
|
||||
otherRow('Show remote cursor', 'show_remote_cursor'),
|
||||
otherRow('Show quality monitor', 'show_quality_monitor'),
|
||||
otherRow('Mute', 'disable_audio'),
|
||||
otherRow('Disable clipboard', 'disable_clipboard'),
|
||||
otherRow('Lock after session end', 'lock_after_session_end'),
|
||||
otherRow('Privacy mode', 'privacy_mode'),
|
||||
otherRow('Touch mode', 'touch-mode'),
|
||||
],
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
otherRow(String label, String key) {
|
||||
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
||||
return SettingsTile.switchTile(
|
||||
initialValue: value,
|
||||
title: Text(translate(label)),
|
||||
onToggle: (b) async {
|
||||
await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RadioEntry {
|
||||
final String label;
|
||||
final String value;
|
||||
_RadioEntry(this.label, this.value);
|
||||
}
|
||||
|
||||
typedef _RadioEntryGetter = String Function();
|
||||
typedef _RadioEntrySetter = Future<void> Function(String);
|
||||
|
||||
_getPopupDialogRadioEntry({
|
||||
required String title,
|
||||
required List<_RadioEntry> list,
|
||||
required _RadioEntryGetter getter,
|
||||
required _RadioEntrySetter asyncSetter,
|
||||
Widget? tail,
|
||||
RxBool? showTail,
|
||||
String? notCloseValue,
|
||||
}) {
|
||||
RxString groupValue = ''.obs;
|
||||
RxString valueText = ''.obs;
|
||||
|
||||
init() {
|
||||
groupValue.value = getter();
|
||||
final e = list.firstWhereOrNull((e) => e.value == groupValue.value);
|
||||
if (e != null) {
|
||||
valueText.value = e.label;
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
void showDialog() async {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
onChanged(String? value) async {
|
||||
if (value == null) return;
|
||||
await asyncSetter(value);
|
||||
init();
|
||||
if (value != notCloseValue) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Obx(
|
||||
() => Column(children: [
|
||||
...list
|
||||
.map((e) => getRadio(Text(translate(e.label)), e.value,
|
||||
groupValue.value, (String? value) => onChanged(value)))
|
||||
.toList(),
|
||||
Offstage(
|
||||
offstage:
|
||||
!(tail != null && showTail != null && showTail.value == true),
|
||||
child: tail,
|
||||
),
|
||||
]),
|
||||
));
|
||||
}, backDismiss: true, clickMaskDismiss: true);
|
||||
}
|
||||
|
||||
return SettingsTile(
|
||||
title: Text(translate(title)),
|
||||
onPressed: (context) => showDialog(),
|
||||
value: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Obx(() => Text(translate(valueText.value))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
@@ -147,59 +147,72 @@ void setTemporaryPasswordLengthDialog(
|
||||
|
||||
void showServerSettingsWithValue(
|
||||
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
|
||||
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
||||
final oldCfg = ServerConfig.fromOptions(oldOptions);
|
||||
|
||||
var isInProgress = false;
|
||||
final idCtrl = TextEditingController(text: serverConfig.idServer);
|
||||
final relayCtrl = TextEditingController(text: serverConfig.relayServer);
|
||||
final apiCtrl = TextEditingController(text: serverConfig.apiServer);
|
||||
final keyCtrl = TextEditingController(text: serverConfig.key);
|
||||
|
||||
String? idServerMsg;
|
||||
String? relayServerMsg;
|
||||
String? apiServerMsg;
|
||||
RxString idServerMsg = ''.obs;
|
||||
RxString relayServerMsg = ''.obs;
|
||||
RxString apiServerMsg = ''.obs;
|
||||
|
||||
final controllers = [idCtrl, relayCtrl, apiCtrl, keyCtrl];
|
||||
final errMsgs = [
|
||||
idServerMsg,
|
||||
relayServerMsg,
|
||||
apiServerMsg,
|
||||
];
|
||||
|
||||
dialogManager.show((setState, close, context) {
|
||||
Future<bool> validate() async {
|
||||
if (idCtrl.text != oldCfg.idServer) {
|
||||
final res = await validateAsync(idCtrl.text);
|
||||
setState(() => idServerMsg = res);
|
||||
if (idServerMsg != null) return false;
|
||||
}
|
||||
if (relayCtrl.text != oldCfg.relayServer) {
|
||||
relayServerMsg = await validateAsync(relayCtrl.text);
|
||||
if (relayServerMsg != null) return false;
|
||||
}
|
||||
if (apiCtrl.text != oldCfg.apiServer) {
|
||||
if (apiServerMsg != null) return false;
|
||||
}
|
||||
return true;
|
||||
Future<bool> submit() async {
|
||||
setState(() {
|
||||
isInProgress = true;
|
||||
});
|
||||
bool ret = await setServerConfig(
|
||||
controllers,
|
||||
errMsgs,
|
||||
ServerConfig(
|
||||
idServer: idCtrl.text.trim(),
|
||||
relayServer: relayCtrl.text.trim(),
|
||||
apiServer: apiCtrl.text.trim(),
|
||||
key: keyCtrl.text.trim()));
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('ID/Relay Server')),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(translate('ID/Relay Server'))),
|
||||
...ServerConfigImportExportWidgets(controllers, errMsgs),
|
||||
],
|
||||
),
|
||||
content: Form(
|
||||
child: Column(
|
||||
child: Obx(() => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextFormField(
|
||||
controller: idCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('ID Server'),
|
||||
errorText: idServerMsg),
|
||||
errorText: idServerMsg.value.isEmpty
|
||||
? null
|
||||
: idServerMsg.value),
|
||||
)
|
||||
] +
|
||||
[
|
||||
TextFormField(
|
||||
controller: relayCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Relay Server'),
|
||||
errorText: relayServerMsg.value.isEmpty
|
||||
? null
|
||||
: relayServerMsg.value),
|
||||
)
|
||||
] +
|
||||
(isAndroid
|
||||
? [
|
||||
TextFormField(
|
||||
controller: relayCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('Relay Server'),
|
||||
errorText: relayServerMsg),
|
||||
)
|
||||
]
|
||||
: []) +
|
||||
[
|
||||
TextFormField(
|
||||
controller: apiCtrl,
|
||||
@@ -214,7 +227,7 @@ void showServerSettingsWithValue(
|
||||
return translate("invalid_http");
|
||||
}
|
||||
}
|
||||
return apiServerMsg;
|
||||
return null;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
@@ -225,7 +238,7 @@ void showServerSettingsWithValue(
|
||||
),
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
if (isInProgress) const LinearProgressIndicator(),
|
||||
])),
|
||||
]))),
|
||||
actions: [
|
||||
dialogButton('Cancel', onPressed: () {
|
||||
close();
|
||||
@@ -233,35 +246,12 @@ void showServerSettingsWithValue(
|
||||
dialogButton(
|
||||
'OK',
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
idServerMsg = null;
|
||||
relayServerMsg = null;
|
||||
apiServerMsg = null;
|
||||
isInProgress = true;
|
||||
});
|
||||
if (await validate()) {
|
||||
if (idCtrl.text != oldCfg.idServer) {
|
||||
if (oldCfg.idServer.isNotEmpty) {
|
||||
await gFFI.userModel.logOut();
|
||||
}
|
||||
bind.mainSetOption(
|
||||
key: "custom-rendezvous-server", value: idCtrl.text);
|
||||
}
|
||||
if (relayCtrl.text != oldCfg.relayServer) {
|
||||
bind.mainSetOption(key: "relay-server", value: relayCtrl.text);
|
||||
}
|
||||
if (keyCtrl.text != oldCfg.key) {
|
||||
bind.mainSetOption(key: "key", value: keyCtrl.text);
|
||||
}
|
||||
if (apiCtrl.text != oldCfg.apiServer) {
|
||||
bind.mainSetOption(key: "api-server", value: apiCtrl.text);
|
||||
}
|
||||
if (await submit()) {
|
||||
close();
|
||||
showToast(translate('Successful'));
|
||||
} else {
|
||||
showToast(translate('Failed'));
|
||||
}
|
||||
setState(() {
|
||||
isInProgress = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -63,7 +63,7 @@ class ChatModel with ChangeNotifier {
|
||||
bool isConnManager = false;
|
||||
|
||||
RxBool isWindowFocus = true.obs;
|
||||
BlockableOverlayState? _blockableOverlayState;
|
||||
BlockableOverlayState _blockableOverlayState = BlockableOverlayState();
|
||||
final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
|
||||
|
||||
Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
|
||||
@@ -154,7 +154,7 @@ class ChatModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
final overlayState = _blockableOverlayState?.state;
|
||||
final overlayState = _blockableOverlayState.state;
|
||||
if (overlayState == null) return;
|
||||
|
||||
final overlay = OverlayEntry(builder: (context) {
|
||||
|
||||
@@ -1815,7 +1815,7 @@ class FFI {
|
||||
} else {
|
||||
// Fetch the image buffer from rust codes.
|
||||
final sz = platformFFI.getRgbaSize(sessionId);
|
||||
if (sz == null || sz == 0) {
|
||||
if (sz == 0) {
|
||||
return;
|
||||
}
|
||||
final rgba = platformFFI.getRgba(sessionId, sz);
|
||||
|
||||
@@ -21,16 +21,8 @@ class RgbaFrame extends Struct {
|
||||
external Pointer<Uint8> data;
|
||||
}
|
||||
|
||||
typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
|
||||
typedef F3 = Pointer<Uint8> Function(Pointer<Utf8>);
|
||||
typedef F4 = Uint64 Function(Pointer<Utf8>);
|
||||
typedef F4Dart = int Function(Pointer<Utf8>);
|
||||
typedef F5 = Void Function(Pointer<Utf8>);
|
||||
typedef F5Dart = void Function(Pointer<Utf8>);
|
||||
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
|
||||
// pub fn session_register_texture(id: *const char, ptr: usize)
|
||||
typedef F6 = Void Function(Pointer<Utf8>, Uint64);
|
||||
typedef F6Dart = void Function(Pointer<Utf8>, int);
|
||||
|
||||
/// FFI wrapper around the native Rust core.
|
||||
/// Hides the platform differences.
|
||||
@@ -38,7 +30,6 @@ class PlatformFFI {
|
||||
String _dir = '';
|
||||
// _homeDir is only needed for Android and IOS.
|
||||
String _homeDir = '';
|
||||
F2? _translate;
|
||||
final _eventHandlers = <String, Map<String, HandleEvent>>{};
|
||||
late RustdeskImpl _ffiBind;
|
||||
late String _appType;
|
||||
@@ -51,9 +42,6 @@ class PlatformFFI {
|
||||
|
||||
RustdeskImpl get ffiBind => _ffiBind;
|
||||
F3? _session_get_rgba;
|
||||
F4Dart? _session_get_rgba_size;
|
||||
F5Dart? _session_next_rgba;
|
||||
F6Dart? _session_register_texture;
|
||||
|
||||
static get localeName => Platform.localeName;
|
||||
|
||||
@@ -89,18 +77,8 @@ class PlatformFFI {
|
||||
}
|
||||
}
|
||||
|
||||
String translate(String name, String locale) {
|
||||
if (_translate == null) return name;
|
||||
var a = name.toNativeUtf8();
|
||||
var b = locale.toNativeUtf8();
|
||||
var p = _translate!(a, b);
|
||||
assert(p != nullptr);
|
||||
final res = p.toDartString();
|
||||
calloc.free(p);
|
||||
calloc.free(a);
|
||||
calloc.free(b);
|
||||
return res;
|
||||
}
|
||||
String translate(String name, String locale) =>
|
||||
_ffiBind.translate(name: name, locale: locale);
|
||||
|
||||
Uint8List? getRgba(SessionID sessionId, int bufSize) {
|
||||
if (_session_get_rgba == null) return null;
|
||||
@@ -118,30 +96,11 @@ class PlatformFFI {
|
||||
}
|
||||
}
|
||||
|
||||
int? getRgbaSize(SessionID sessionId) {
|
||||
if (_session_get_rgba_size == null) return null;
|
||||
final sessionIdStr = sessionId.toString();
|
||||
var a = sessionIdStr.toNativeUtf8();
|
||||
final bufferSize = _session_get_rgba_size!(a);
|
||||
malloc.free(a);
|
||||
return bufferSize;
|
||||
}
|
||||
|
||||
void nextRgba(SessionID sessionId) {
|
||||
if (_session_next_rgba == null) return;
|
||||
final sessionIdStr = sessionId.toString();
|
||||
final a = sessionIdStr.toNativeUtf8();
|
||||
_session_next_rgba!(a);
|
||||
malloc.free(a);
|
||||
}
|
||||
|
||||
void registerTexture(SessionID sessionId, int ptr) {
|
||||
if (_session_register_texture == null) return;
|
||||
final sessionIdStr = sessionId.toString();
|
||||
final a = sessionIdStr.toNativeUtf8();
|
||||
_session_register_texture!(a, ptr);
|
||||
malloc.free(a);
|
||||
}
|
||||
int getRgbaSize(SessionID sessionId) =>
|
||||
_ffiBind.sessionGetRgbaSize(sessionId: sessionId);
|
||||
void nextRgba(SessionID sessionId) => _ffiBind.sessionNextRgba(sessionId: sessionId);
|
||||
void registerTexture(SessionID sessionId, int ptr) =>
|
||||
_ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr);
|
||||
|
||||
/// Init the FFI class, loads the native Rust core library.
|
||||
Future<void> init(String appType) async {
|
||||
@@ -157,14 +116,7 @@ class PlatformFFI {
|
||||
: DynamicLibrary.process();
|
||||
debugPrint('initializing FFI $_appType');
|
||||
try {
|
||||
_translate = dylib.lookupFunction<F2, F2>('translate');
|
||||
_session_get_rgba = dylib.lookupFunction<F3, F3>("session_get_rgba");
|
||||
_session_get_rgba_size =
|
||||
dylib.lookupFunction<F4, F4Dart>("session_get_rgba_size");
|
||||
_session_next_rgba =
|
||||
dylib.lookupFunction<F5, F5Dart>("session_next_rgba");
|
||||
_session_register_texture =
|
||||
dylib.lookupFunction<F6, F6Dart>("session_register_texture");
|
||||
try {
|
||||
// SYSTEM user failed
|
||||
_dir = (await getApplicationDocumentsDirectory()).path;
|
||||
|
||||
@@ -40,8 +40,6 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
late String _emptyIdShow;
|
||||
late final IDTextEditingController _serverId;
|
||||
final _serverPasswd =
|
||||
TextEditingController(text: translate("Generating ..."));
|
||||
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.cm);
|
||||
|
||||
@@ -63,6 +61,9 @@ class ServerModel with ChangeNotifier {
|
||||
|
||||
int get connectStatus => _connectStatus;
|
||||
|
||||
TextEditingController get _serverPasswd =>
|
||||
TextEditingController(text: translate("Generating ..."));
|
||||
|
||||
String get verificationMethod {
|
||||
final index = [
|
||||
kUseTemporaryPassword,
|
||||
|
||||
Reference in New Issue
Block a user