mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-07 21:11:05 +03:00
TCP punching was the one direct transport without a switch, while UDP, IPv6 and WebRTC each had one. Add "Enable TCP hole punching" above the UDP toggle on both desktop and mobile, default on — including on self-hosted servers, since unlike the other three (whose default-off there guards against an hbbs that cannot forward their fields) TCP punching has always been supported by every server. Turning all four off would leave no way to punch at all, so TCP runs regardless in that case. That backstop keys off the switches alone: a transport that is enabled but fails to materialize — no public v6 address, no NAT port, a failed offerer — is already covered by the relay fallback for a round that ends up with no usable direct transport. With the TCP punch off, the fallback request is skipped too: it exists only to carry that punch, and would otherwise reach connect() with nothing to try and merely open a second relay. Known cost, unchanged behavior for the peer: the request carries no field for this choice, so a peer that receives one with no udp_port and no offer still punches a TCP hole and listens for a connection the controller will not make. Representing the transport choice on the wire needs a proto field and the server forwarding it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HV43uh1ztv6Wm5qi3Y1ne
1443 lines
51 KiB
Dart
1443 lines
51 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
|
|
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:settings_ui/settings_ui.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
|
|
|
import '../../common.dart';
|
|
import '../../common/widgets/dialog.dart';
|
|
import '../../common/widgets/login.dart';
|
|
import '../../consts.dart';
|
|
import '../../models/model.dart';
|
|
import '../../models/platform_model.dart';
|
|
import '../widgets/deploy_dialog.dart';
|
|
import '../widgets/dialog.dart';
|
|
import 'home_page.dart';
|
|
import 'scan_page.dart';
|
|
|
|
class SettingsPage extends StatefulWidget implements PageShape {
|
|
@override
|
|
final title = translate("Settings");
|
|
|
|
@override
|
|
final icon = Icon(Icons.settings);
|
|
|
|
@override
|
|
final appBarActions = bind.isDisableSettings() ? [] : [ScanButton()];
|
|
|
|
@override
|
|
State<SettingsPage> createState() => _SettingsState();
|
|
}
|
|
|
|
const url = 'https://rustdesk.com/';
|
|
|
|
enum KeepScreenOn {
|
|
never,
|
|
duringControlled,
|
|
serviceOn,
|
|
}
|
|
|
|
String _keepScreenOnToOption(KeepScreenOn value) {
|
|
switch (value) {
|
|
case KeepScreenOn.never:
|
|
return 'never';
|
|
case KeepScreenOn.duringControlled:
|
|
return 'during-controlled';
|
|
case KeepScreenOn.serviceOn:
|
|
return 'service-on';
|
|
}
|
|
}
|
|
|
|
KeepScreenOn optionToKeepScreenOn(String value) {
|
|
switch (value) {
|
|
case 'never':
|
|
return KeepScreenOn.never;
|
|
case 'service-on':
|
|
return KeepScreenOn.serviceOn;
|
|
default:
|
|
return KeepScreenOn.duringControlled;
|
|
}
|
|
}
|
|
|
|
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|
final _hasIgnoreBattery =
|
|
false; //androidVersion >= 26; // remove because not work on every device
|
|
var _ignoreBatteryOpt = false;
|
|
var _enableStartOnBoot = false;
|
|
var _checkUpdateOnStartup = false;
|
|
var _showTerminalExtraKeys = false;
|
|
var _floatingWindowDisabled = false;
|
|
var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
|
|
var _enableAbr = false;
|
|
var _denyLANDiscovery = false;
|
|
var _onlyWhiteList = false;
|
|
var _onlyIdWhiteList = false;
|
|
var _enableDirectIPAccess = false;
|
|
var _enableRecordSession = false;
|
|
var _enableHardwareCodec = false;
|
|
var _allowWebSocket = false;
|
|
var _autoRecordIncomingSession = false;
|
|
var _autoRecordOutgoingSession = false;
|
|
var _allowAutoDisconnect = false;
|
|
var _localIP = "";
|
|
var _directAccessPort = "";
|
|
var _fingerprint = "";
|
|
var _buildDate = "";
|
|
var _myId = "";
|
|
var _autoDisconnectTimeout = "";
|
|
var _hideServer = false;
|
|
var _hideProxy = false;
|
|
var _hideNetwork = false;
|
|
var _hideWebSocket = false;
|
|
var _enableTrustedDevices = false;
|
|
var _enableTcpPunch = false;
|
|
var _enableUdpPunch = false;
|
|
var _allowInsecureTlsFallback = false;
|
|
var _disableUdp = false;
|
|
var _enableIpv6Punch = false;
|
|
var _enableWebrtc = false;
|
|
var _isUsingPublicServer = false;
|
|
var _allowAskForNoteAtEndOfConnection = false;
|
|
var _preventSleepWhileConnected = true;
|
|
|
|
_SettingsState() {
|
|
_enableAbr = option2bool(
|
|
kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
|
|
_denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
|
|
bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
|
|
_onlyWhiteList = whitelistNotEmpty();
|
|
_onlyIdWhiteList = idWhitelistNotEmpty();
|
|
_enableDirectIPAccess = option2bool(
|
|
kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
|
|
_enableRecordSession = option2bool(kOptionEnableRecordSession,
|
|
bind.mainGetOptionSync(key: kOptionEnableRecordSession));
|
|
_enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
|
bind.mainGetOptionSync(key: kOptionEnableHwcodec));
|
|
_allowWebSocket = mainGetBoolOptionSync(kOptionAllowWebSocket);
|
|
_allowInsecureTlsFallback =
|
|
mainGetBoolOptionSync(kOptionAllowInsecureTLSFallback);
|
|
_disableUdp = bind.mainGetOptionSync(key: kOptionDisableUdp) == 'Y';
|
|
_autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
|
|
bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
|
|
_autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
|
|
bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing));
|
|
_localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
|
|
_directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
|
|
_allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
|
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
|
|
_autoDisconnectTimeout =
|
|
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
|
|
_hideServer =
|
|
bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
|
|
_hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
|
|
_hideNetwork =
|
|
bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
|
|
_hideWebSocket =
|
|
bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y' ||
|
|
isWeb;
|
|
_enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
|
|
_enableTcpPunch = mainGetLocalBoolOptionSync(kOptionEnableTcpPunch);
|
|
_enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
|
_enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
|
_enableWebrtc = mainGetLocalBoolOptionSync(kOptionEnableWebrtc);
|
|
_allowAskForNoteAtEndOfConnection =
|
|
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
|
|
_preventSleepWhileConnected =
|
|
mainGetLocalBoolOptionSync(kOptionKeepAwakeDuringOutgoingSessions);
|
|
_showTerminalExtraKeys =
|
|
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
var update = false;
|
|
|
|
if (_hasIgnoreBattery) {
|
|
if (await checkAndUpdateIgnoreBatteryStatus()) {
|
|
update = true;
|
|
}
|
|
}
|
|
|
|
if (await checkAndUpdateStartOnBoot()) {
|
|
update = true;
|
|
}
|
|
|
|
// start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
|
|
var enableStartOnBoot =
|
|
await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt);
|
|
if (enableStartOnBoot) {
|
|
if (!await canStartOnBoot()) {
|
|
enableStartOnBoot = false;
|
|
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
|
|
}
|
|
}
|
|
|
|
if (enableStartOnBoot != _enableStartOnBoot) {
|
|
update = true;
|
|
_enableStartOnBoot = enableStartOnBoot;
|
|
}
|
|
|
|
var checkUpdateOnStartup =
|
|
mainGetLocalBoolOptionSync(kOptionEnableCheckUpdate);
|
|
if (checkUpdateOnStartup != _checkUpdateOnStartup) {
|
|
update = true;
|
|
_checkUpdateOnStartup = checkUpdateOnStartup;
|
|
}
|
|
|
|
var floatingWindowDisabled =
|
|
bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
|
|
!await AndroidPermissionManager.check(kSystemAlertWindow);
|
|
if (floatingWindowDisabled != _floatingWindowDisabled) {
|
|
update = true;
|
|
_floatingWindowDisabled = floatingWindowDisabled;
|
|
}
|
|
|
|
final keepScreenOn = _floatingWindowDisabled
|
|
? KeepScreenOn.never
|
|
: optionToKeepScreenOn(
|
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn));
|
|
if (keepScreenOn != _keepScreenOn) {
|
|
update = true;
|
|
_keepScreenOn = keepScreenOn;
|
|
}
|
|
|
|
final fingerprint = await bind.mainGetFingerprint();
|
|
if (_fingerprint != fingerprint) {
|
|
update = true;
|
|
_fingerprint = fingerprint;
|
|
}
|
|
|
|
final buildDate = await bind.mainGetBuildDate();
|
|
if (_buildDate != buildDate) {
|
|
update = true;
|
|
_buildDate = buildDate;
|
|
}
|
|
|
|
final myId = await bind.mainGetMyId();
|
|
if (_myId != myId) {
|
|
update = true;
|
|
_myId = myId;
|
|
}
|
|
|
|
final isUsingPublicServer = await bind.mainIsUsingPublicServer();
|
|
if (_isUsingPublicServer != isUsingPublicServer) {
|
|
update = true;
|
|
_isUsingPublicServer = isUsingPublicServer;
|
|
}
|
|
|
|
if (update) {
|
|
setState(() {});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed) {
|
|
() async {
|
|
final ibs = await checkAndUpdateIgnoreBatteryStatus();
|
|
final sob = await checkAndUpdateStartOnBoot();
|
|
if (ibs || sob) {
|
|
setState(() {});
|
|
}
|
|
}();
|
|
}
|
|
}
|
|
|
|
Future<bool> checkAndUpdateIgnoreBatteryStatus() async {
|
|
final res = await AndroidPermissionManager.check(
|
|
kRequestIgnoreBatteryOptimizations);
|
|
if (_ignoreBatteryOpt != res) {
|
|
_ignoreBatteryOpt = res;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<bool> checkAndUpdateStartOnBoot() async {
|
|
if (!await canStartOnBoot() && _enableStartOnBoot) {
|
|
_enableStartOnBoot = false;
|
|
debugPrint(
|
|
"checkAndUpdateStartOnBoot and set _enableStartOnBoot -> false");
|
|
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Provider.of<FfiModel>(context);
|
|
final outgoingOnly = bind.isOutgoingOnly();
|
|
final incomingOnly = bind.isIncomingOnly();
|
|
final customClientSection = CustomSettingsSection(
|
|
child: Column(
|
|
children: [
|
|
if (bind.isCustomClient())
|
|
Align(
|
|
alignment: Alignment.center,
|
|
child: loadPowered(context),
|
|
),
|
|
Align(
|
|
alignment: Alignment.center,
|
|
child: loadLogo(),
|
|
)
|
|
],
|
|
));
|
|
final List<AbstractSettingsTile> enhancementsTiles = [];
|
|
final enable2fa = bind.mainHasValid2FaSync();
|
|
final List<AbstractSettingsTile> tfaTiles = [
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('enable-2fa-title')),
|
|
initialValue: enable2fa,
|
|
onToggle: (v) async {
|
|
update() async {
|
|
setState(() {});
|
|
}
|
|
|
|
if (v == false) {
|
|
CommonConfirmDialog(
|
|
gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
|
|
change2fa(callback: update);
|
|
});
|
|
} else {
|
|
change2fa(callback: update);
|
|
}
|
|
},
|
|
),
|
|
if (enable2fa)
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Telegram bot')),
|
|
initialValue: bind.mainHasValidBotSync(),
|
|
onToggle: (v) async {
|
|
update() async {
|
|
setState(() {});
|
|
}
|
|
|
|
if (v == false) {
|
|
CommonConfirmDialog(
|
|
gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
|
|
changeBot(callback: update);
|
|
});
|
|
} else {
|
|
changeBot(callback: update);
|
|
}
|
|
},
|
|
),
|
|
if (enable2fa)
|
|
SettingsTile.switchTile(
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(translate('Enable trusted devices')),
|
|
Text('* ${translate('enable-trusted-devices-tip')}',
|
|
style: Theme.of(context).textTheme.bodySmall),
|
|
],
|
|
),
|
|
initialValue: _enableTrustedDevices,
|
|
onToggle: isOptionFixed(kOptionEnableTrustedDevices)
|
|
? null
|
|
: (v) async {
|
|
mainSetBoolOption(kOptionEnableTrustedDevices, v);
|
|
setState(() {
|
|
_enableTrustedDevices = v;
|
|
});
|
|
},
|
|
),
|
|
if (enable2fa && _enableTrustedDevices)
|
|
SettingsTile(
|
|
title: Text(translate('Manage trusted devices')),
|
|
trailing: Icon(Icons.arrow_forward_ios),
|
|
onPressed: (context) {
|
|
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
|
return _ManageTrustedDevices();
|
|
}));
|
|
})
|
|
];
|
|
final List<AbstractSettingsTile> shareScreenTiles = [
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Deny LAN discovery')),
|
|
initialValue: _denyLANDiscovery,
|
|
onToggle: isOptionFixed(kOptionEnableLanDiscovery)
|
|
? null
|
|
: (v) async {
|
|
await bind.mainSetOption(
|
|
key: kOptionEnableLanDiscovery,
|
|
value: bool2option(kOptionEnableLanDiscovery, !v));
|
|
final newValue = !option2bool(kOptionEnableLanDiscovery,
|
|
await bind.mainGetOption(key: kOptionEnableLanDiscovery));
|
|
setState(() {
|
|
_denyLANDiscovery = newValue;
|
|
});
|
|
},
|
|
),
|
|
SettingsTile.switchTile(
|
|
title: Row(children: [
|
|
Expanded(child: Text(translate('Use IP Whitelisting'))),
|
|
Offstage(
|
|
offstage: !_onlyWhiteList,
|
|
child: const Icon(Icons.warning_amber_rounded,
|
|
color: Color.fromARGB(255, 255, 204, 0)))
|
|
.marginOnly(left: 5)
|
|
]),
|
|
initialValue: _onlyWhiteList,
|
|
onToggle: (_) async {
|
|
update() async {
|
|
final onlyWhiteList = whitelistNotEmpty();
|
|
if (onlyWhiteList != _onlyWhiteList) {
|
|
setState(() {
|
|
_onlyWhiteList = onlyWhiteList;
|
|
});
|
|
}
|
|
}
|
|
|
|
changeWhiteList(callback: update);
|
|
},
|
|
),
|
|
SettingsTile.switchTile(
|
|
title: Row(children: [
|
|
Expanded(child: Text(translate('Use ID whitelisting'))),
|
|
Offstage(
|
|
offstage: !_onlyIdWhiteList,
|
|
child: const Icon(Icons.warning_amber_rounded,
|
|
color: Color.fromARGB(255, 255, 204, 0)))
|
|
.marginOnly(left: 5)
|
|
]),
|
|
initialValue: _onlyIdWhiteList,
|
|
onToggle: (_) async {
|
|
update() async {
|
|
final onlyIdWhiteList = idWhitelistNotEmpty();
|
|
if (onlyIdWhiteList != _onlyIdWhiteList) {
|
|
setState(() {
|
|
_onlyIdWhiteList = onlyIdWhiteList;
|
|
});
|
|
}
|
|
}
|
|
|
|
changeIdWhiteList(callback: update);
|
|
},
|
|
),
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Adaptive bitrate')),
|
|
initialValue: _enableAbr,
|
|
onToggle: isOptionFixed(kOptionEnableAbr)
|
|
? null
|
|
: (v) async {
|
|
await mainSetBoolOption(kOptionEnableAbr, v);
|
|
final newValue = await mainGetBoolOption(kOptionEnableAbr);
|
|
setState(() {
|
|
_enableAbr = newValue;
|
|
});
|
|
},
|
|
),
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Enable recording session')),
|
|
initialValue: _enableRecordSession,
|
|
onToggle: isOptionFixed(kOptionEnableRecordSession)
|
|
? null
|
|
: (v) async {
|
|
await mainSetBoolOption(kOptionEnableRecordSession, v);
|
|
final newValue =
|
|
await mainGetBoolOption(kOptionEnableRecordSession);
|
|
setState(() {
|
|
_enableRecordSession = newValue;
|
|
});
|
|
},
|
|
),
|
|
SettingsTile.switchTile(
|
|
title: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(translate("Direct IP Access")),
|
|
Offstage(
|
|
offstage: !_enableDirectIPAccess,
|
|
child: Text(
|
|
'${translate("Local Address")}: $_localIP${_directAccessPort.isEmpty ? "" : ":$_directAccessPort"}',
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
)),
|
|
])),
|
|
Offstage(
|
|
offstage: !_enableDirectIPAccess,
|
|
child: IconButton(
|
|
padding: EdgeInsets.zero,
|
|
icon: Icon(
|
|
Icons.edit,
|
|
size: 20,
|
|
),
|
|
onPressed: isOptionFixed(kOptionDirectAccessPort)
|
|
? null
|
|
: () async {
|
|
final port = await changeDirectAccessPort(
|
|
_localIP, _directAccessPort);
|
|
setState(() {
|
|
_directAccessPort = port;
|
|
});
|
|
}))
|
|
]),
|
|
initialValue: _enableDirectIPAccess,
|
|
onToggle: isOptionFixed(kOptionDirectServer)
|
|
? null
|
|
: (_) async {
|
|
_enableDirectIPAccess = !_enableDirectIPAccess;
|
|
String value =
|
|
bool2option(kOptionDirectServer, _enableDirectIPAccess);
|
|
await bind.mainSetOption(
|
|
key: kOptionDirectServer, value: value);
|
|
setState(() {});
|
|
},
|
|
),
|
|
SettingsTile.switchTile(
|
|
title: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(translate("auto_disconnect_option_tip")),
|
|
Offstage(
|
|
offstage: !_allowAutoDisconnect,
|
|
child: Text(
|
|
'${_autoDisconnectTimeout.isEmpty ? '10' : _autoDisconnectTimeout} min',
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
)),
|
|
])),
|
|
Offstage(
|
|
offstage: !_allowAutoDisconnect,
|
|
child: IconButton(
|
|
padding: EdgeInsets.zero,
|
|
icon: Icon(
|
|
Icons.edit,
|
|
size: 20,
|
|
),
|
|
onPressed: isOptionFixed(kOptionAutoDisconnectTimeout)
|
|
? null
|
|
: () async {
|
|
final timeout = await changeAutoDisconnectTimeout(
|
|
_autoDisconnectTimeout);
|
|
setState(() {
|
|
_autoDisconnectTimeout = timeout;
|
|
});
|
|
}))
|
|
]),
|
|
initialValue: _allowAutoDisconnect,
|
|
onToggle: isOptionFixed(kOptionAllowAutoDisconnect)
|
|
? null
|
|
: (_) async {
|
|
_allowAutoDisconnect = !_allowAutoDisconnect;
|
|
String value = bool2option(
|
|
kOptionAllowAutoDisconnect, _allowAutoDisconnect);
|
|
await bind.mainSetOption(
|
|
key: kOptionAllowAutoDisconnect, value: value);
|
|
setState(() {});
|
|
},
|
|
)
|
|
];
|
|
if (_hasIgnoreBattery) {
|
|
enhancementsTiles.insert(
|
|
0,
|
|
SettingsTile.switchTile(
|
|
initialValue: _ignoreBatteryOpt,
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(translate('Keep RustDesk background service')),
|
|
Text('* ${translate('Ignore Battery Optimizations')}',
|
|
style: Theme.of(context).textTheme.bodySmall),
|
|
]),
|
|
onToggle: (v) async {
|
|
if (v) {
|
|
await AndroidPermissionManager.request(
|
|
kRequestIgnoreBatteryOptimizations);
|
|
} else {
|
|
final res = await gFFI.dialogManager.show<bool>(
|
|
(setState, close, context) => CustomAlertDialog(
|
|
title: Text(translate("Open System Setting")),
|
|
content: Text(translate(
|
|
"android_open_battery_optimizations_tip")),
|
|
actions: [
|
|
dialogButton("Cancel",
|
|
onPressed: () => close(), isOutline: true),
|
|
dialogButton(
|
|
"Open System Setting",
|
|
onPressed: () => close(true),
|
|
),
|
|
],
|
|
));
|
|
if (res == true) {
|
|
AndroidPermissionManager.startAction(
|
|
kActionApplicationDetailsSettings);
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
enhancementsTiles.add(SettingsTile.switchTile(
|
|
initialValue: _enableStartOnBoot,
|
|
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(translate('Start on boot')),
|
|
Text(
|
|
'* ${translate('Start the screen sharing service on boot, requires special permissions')}',
|
|
style: Theme.of(context).textTheme.bodySmall),
|
|
]),
|
|
onToggle: (toValue) async {
|
|
if (toValue) {
|
|
// 1. request kIgnoreBatteryOptimizations
|
|
if (!await AndroidPermissionManager.check(
|
|
kRequestIgnoreBatteryOptimizations)) {
|
|
if (!await AndroidPermissionManager.request(
|
|
kRequestIgnoreBatteryOptimizations)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 2. request kSystemAlertWindow
|
|
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
|
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// (Optional) 3. request input permission
|
|
}
|
|
setState(() => _enableStartOnBoot = toValue);
|
|
|
|
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
|
}));
|
|
|
|
if (!bind.isCustomClient()) {
|
|
enhancementsTiles.add(
|
|
SettingsTile.switchTile(
|
|
initialValue: _checkUpdateOnStartup,
|
|
title:
|
|
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(translate('Check for software update on startup')),
|
|
]),
|
|
onToggle: (bool toValue) async {
|
|
await mainSetLocalBoolOption(kOptionEnableCheckUpdate, toValue);
|
|
setState(() => _checkUpdateOnStartup = toValue);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
enhancementsTiles.add(
|
|
SettingsTile.switchTile(
|
|
initialValue: _showTerminalExtraKeys,
|
|
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(translate('Show terminal extra keys')),
|
|
]),
|
|
onToggle: (bool v) async {
|
|
await mainSetLocalBoolOption(kOptionEnableShowTerminalExtraKeys, v);
|
|
final newValue =
|
|
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
|
setState(() {
|
|
_showTerminalExtraKeys = newValue;
|
|
});
|
|
},
|
|
),
|
|
);
|
|
|
|
onFloatingWindowChanged(bool toValue) async {
|
|
if (toValue) {
|
|
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
|
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
final disable = !toValue;
|
|
bind.mainSetLocalOption(
|
|
key: kOptionDisableFloatingWindow,
|
|
value: disable ? 'Y' : defaultOptionNo);
|
|
setState(() => _floatingWindowDisabled = disable);
|
|
gFFI.serverModel.androidUpdatekeepScreenOn();
|
|
}
|
|
|
|
enhancementsTiles.add(SettingsTile.switchTile(
|
|
initialValue: !_floatingWindowDisabled,
|
|
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text(translate('Floating window')),
|
|
Text('* ${translate('floating_window_tip')}',
|
|
style: Theme.of(context).textTheme.bodySmall),
|
|
]),
|
|
onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
|
|
? null
|
|
: onFloatingWindowChanged));
|
|
|
|
enhancementsTiles.add(_getPopupDialogRadioEntry(
|
|
title: 'Keep screen on',
|
|
list: [
|
|
_RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
|
|
_RadioEntry('During controlled',
|
|
_keepScreenOnToOption(KeepScreenOn.duringControlled)),
|
|
_RadioEntry('During service is on',
|
|
_keepScreenOnToOption(KeepScreenOn.serviceOn)),
|
|
],
|
|
getter: () => _keepScreenOnToOption(_floatingWindowDisabled
|
|
? KeepScreenOn.never
|
|
: optionToKeepScreenOn(
|
|
bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
|
|
asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
|
|
? null
|
|
: (value) async {
|
|
await bind.mainSetLocalOption(
|
|
key: kOptionKeepScreenOn, value: value);
|
|
setState(() => _keepScreenOn = optionToKeepScreenOn(value));
|
|
gFFI.serverModel.androidUpdatekeepScreenOn();
|
|
},
|
|
));
|
|
|
|
final disabledSettings = bind.isDisableSettings();
|
|
final hideSecuritySettings =
|
|
bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) == 'Y';
|
|
final settings = SettingsList(
|
|
sections: [
|
|
customClientSection,
|
|
if (!bind.isDisableAccount())
|
|
SettingsSection(
|
|
title: Text(translate('Account')),
|
|
tiles: [
|
|
SettingsTile(
|
|
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
|
? translate('Login')
|
|
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')),
|
|
leading: Obx(() {
|
|
final avatar = bind.mainResolveAvatarUrl(
|
|
avatar: gFFI.userModel.avatar.value);
|
|
return buildAvatarWidget(
|
|
avatar: avatar,
|
|
size: 28,
|
|
borderRadius: null,
|
|
fallback: Icon(Icons.person),
|
|
) ??
|
|
Icon(Icons.person);
|
|
}),
|
|
onPressed: (context) {
|
|
if (gFFI.userModel.userName.value.isEmpty) {
|
|
loginDialog();
|
|
} else {
|
|
logOutConfirmDialog();
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
SettingsSection(title: Text(translate("Settings")), tiles: [
|
|
if (!disabledSettings && !_hideNetwork && !_hideServer)
|
|
SettingsTile(
|
|
title: Text(translate('ID/Relay Server')),
|
|
leading: Icon(Icons.cloud),
|
|
onPressed: (context) {
|
|
showServerSettings(gFFI.dialogManager, (callback) async {
|
|
_isUsingPublicServer = await bind.mainIsUsingPublicServer();
|
|
setState(callback);
|
|
});
|
|
}),
|
|
if (!_hideNetwork && !_hideProxy)
|
|
SettingsTile(
|
|
title: Text(translate('Socks5/Http(s) Proxy')),
|
|
leading: Icon(Icons.network_ping),
|
|
onPressed: (context) {
|
|
changeSocks5Proxy();
|
|
}),
|
|
if (isAndroid && !bind.isOutgoingOnly())
|
|
SettingsTile(
|
|
title: Text(translate('Deploy')),
|
|
leading: Icon(Icons.cloud_upload),
|
|
onPressed: (context) {
|
|
showDeployDialog();
|
|
}),
|
|
if (!disabledSettings && !_hideNetwork && !_hideWebSocket)
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Use WebSocket')),
|
|
initialValue: _allowWebSocket,
|
|
onToggle: isOptionFixed(kOptionAllowWebSocket)
|
|
? null
|
|
: (v) async {
|
|
await mainSetBoolOption(kOptionAllowWebSocket, v);
|
|
final newValue =
|
|
await mainGetBoolOption(kOptionAllowWebSocket);
|
|
setState(() {
|
|
_allowWebSocket = newValue;
|
|
});
|
|
},
|
|
),
|
|
if (!_isUsingPublicServer)
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Allow insecure TLS fallback')),
|
|
initialValue: _allowInsecureTlsFallback,
|
|
onToggle: isOptionFixed(kOptionAllowInsecureTLSFallback)
|
|
? null
|
|
: (v) async {
|
|
await mainSetBoolOption(
|
|
kOptionAllowInsecureTLSFallback, v);
|
|
final newValue = mainGetBoolOptionSync(
|
|
kOptionAllowInsecureTLSFallback);
|
|
setState(() {
|
|
_allowInsecureTlsFallback = newValue;
|
|
});
|
|
},
|
|
),
|
|
if (isAndroid && !outgoingOnly && !_isUsingPublicServer)
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Disable UDP')),
|
|
initialValue: _disableUdp,
|
|
onToggle: isOptionFixed(kOptionDisableUdp)
|
|
? null
|
|
: (v) async {
|
|
await bind.mainSetOption(
|
|
key: kOptionDisableUdp, value: v ? 'Y' : 'N');
|
|
final newValue =
|
|
bind.mainGetOptionSync(key: kOptionDisableUdp) == 'Y';
|
|
setState(() {
|
|
_disableUdp = newValue;
|
|
});
|
|
},
|
|
),
|
|
if (!incomingOnly)
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Enable TCP hole punching')),
|
|
initialValue: _enableTcpPunch,
|
|
onToggle: isOptionFixed(kOptionEnableTcpPunch)
|
|
? null
|
|
: (v) async {
|
|
await mainSetLocalBoolOption(kOptionEnableTcpPunch, v);
|
|
final newValue =
|
|
mainGetLocalBoolOptionSync(kOptionEnableTcpPunch);
|
|
setState(() {
|
|
_enableTcpPunch = newValue;
|
|
});
|
|
},
|
|
),
|
|
if (!incomingOnly)
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Enable UDP hole punching')),
|
|
initialValue: _enableUdpPunch,
|
|
onToggle: isOptionFixed(kOptionEnableUdpPunch)
|
|
? null
|
|
: (v) async {
|
|
await mainSetLocalBoolOption(kOptionEnableUdpPunch, v);
|
|
final newValue =
|
|
mainGetLocalBoolOptionSync(kOptionEnableUdpPunch);
|
|
setState(() {
|
|
_enableUdpPunch = newValue;
|
|
});
|
|
},
|
|
),
|
|
if (!incomingOnly)
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Enable IPv6 P2P connection')),
|
|
initialValue: _enableIpv6Punch,
|
|
onToggle: isOptionFixed(kOptionEnableIpv6Punch)
|
|
? null
|
|
: (v) async {
|
|
await mainSetLocalBoolOption(kOptionEnableIpv6Punch, v);
|
|
final newValue =
|
|
mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch);
|
|
setState(() {
|
|
_enableIpv6Punch = newValue;
|
|
});
|
|
},
|
|
),
|
|
if (!incomingOnly)
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Enable WebRTC P2P connection')),
|
|
initialValue: _enableWebrtc,
|
|
onToggle: isOptionFixed(kOptionEnableWebrtc)
|
|
? null
|
|
: (v) async {
|
|
await mainSetLocalBoolOption(kOptionEnableWebrtc, v);
|
|
final newValue =
|
|
mainGetLocalBoolOptionSync(kOptionEnableWebrtc);
|
|
setState(() {
|
|
_enableWebrtc = newValue;
|
|
});
|
|
},
|
|
),
|
|
SettingsTile(
|
|
title: Text(translate('Language')),
|
|
leading: Icon(Icons.translate),
|
|
onPressed: (context) {
|
|
showLanguageSettings(gFFI.dialogManager);
|
|
}),
|
|
SettingsTile(
|
|
title: Text(translate(
|
|
Theme.of(context).brightness == Brightness.light
|
|
? 'Light Theme'
|
|
: 'Dark Theme')),
|
|
leading: Icon(Theme.of(context).brightness == Brightness.light
|
|
? Icons.dark_mode
|
|
: Icons.light_mode),
|
|
onPressed: (context) {
|
|
showThemeSettings(gFFI.dialogManager);
|
|
},
|
|
),
|
|
if (!bind.isDisableAccount())
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('note-at-conn-end-tip')),
|
|
initialValue: _allowAskForNoteAtEndOfConnection,
|
|
onToggle: (v) async {
|
|
if (v && !gFFI.userModel.isLogin) {
|
|
final res = await loginDialog();
|
|
if (res != true) return;
|
|
}
|
|
await mainSetLocalBoolOption(
|
|
kOptionAllowAskForNoteAtEndOfConnection, v);
|
|
final newValue = mainGetLocalBoolOptionSync(
|
|
kOptionAllowAskForNoteAtEndOfConnection);
|
|
setState(() {
|
|
_allowAskForNoteAtEndOfConnection = newValue;
|
|
});
|
|
},
|
|
),
|
|
if (!incomingOnly)
|
|
SettingsTile.switchTile(
|
|
title:
|
|
Text(translate('keep-awake-during-outgoing-sessions-label')),
|
|
initialValue: _preventSleepWhileConnected,
|
|
onToggle: (v) async {
|
|
await mainSetLocalBoolOption(
|
|
kOptionKeepAwakeDuringOutgoingSessions, v);
|
|
setState(() {
|
|
_preventSleepWhileConnected = v;
|
|
});
|
|
},
|
|
),
|
|
]),
|
|
if (isAndroid)
|
|
SettingsSection(title: Text(translate('Hardware Codec')), tiles: [
|
|
SettingsTile.switchTile(
|
|
title: Text(translate('Enable hardware codec')),
|
|
initialValue: _enableHardwareCodec,
|
|
onToggle: isOptionFixed(kOptionEnableHwcodec)
|
|
? null
|
|
: (v) async {
|
|
await mainSetBoolOption(kOptionEnableHwcodec, v);
|
|
final newValue =
|
|
await mainGetBoolOption(kOptionEnableHwcodec);
|
|
setState(() {
|
|
_enableHardwareCodec = newValue;
|
|
});
|
|
},
|
|
),
|
|
]),
|
|
if (isAndroid)
|
|
SettingsSection(
|
|
title: Text(translate("Recording")),
|
|
tiles: [
|
|
if (!outgoingOnly)
|
|
SettingsTile.switchTile(
|
|
title:
|
|
Text(translate('Automatically record incoming sessions')),
|
|
initialValue: _autoRecordIncomingSession,
|
|
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
|
? null
|
|
: (v) async {
|
|
await bind.mainSetOption(
|
|
key: kOptionAllowAutoRecordIncoming,
|
|
value: bool2option(
|
|
kOptionAllowAutoRecordIncoming, v));
|
|
final newValue = option2bool(
|
|
kOptionAllowAutoRecordIncoming,
|
|
await bind.mainGetOption(
|
|
key: kOptionAllowAutoRecordIncoming));
|
|
setState(() {
|
|
_autoRecordIncomingSession = newValue;
|
|
});
|
|
},
|
|
),
|
|
if (!incomingOnly)
|
|
SettingsTile.switchTile(
|
|
title:
|
|
Text(translate('Automatically record outgoing sessions')),
|
|
initialValue: _autoRecordOutgoingSession,
|
|
onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing)
|
|
? null
|
|
: (v) async {
|
|
await bind.mainSetLocalOption(
|
|
key: kOptionAllowAutoRecordOutgoing,
|
|
value: bool2option(
|
|
kOptionAllowAutoRecordOutgoing, v));
|
|
final newValue = option2bool(
|
|
kOptionAllowAutoRecordOutgoing,
|
|
bind.mainGetLocalOption(
|
|
key: kOptionAllowAutoRecordOutgoing));
|
|
setState(() {
|
|
_autoRecordOutgoingSession = newValue;
|
|
});
|
|
},
|
|
),
|
|
SettingsTile(
|
|
title: Text(translate("Directory")),
|
|
description: Text(bind.mainVideoSaveDirectory(root: false)),
|
|
),
|
|
],
|
|
),
|
|
if (isAndroid &&
|
|
!disabledSettings &&
|
|
!outgoingOnly &&
|
|
!hideSecuritySettings)
|
|
SettingsSection(title: Text('2FA'), tiles: tfaTiles),
|
|
if (isAndroid &&
|
|
!disabledSettings &&
|
|
!outgoingOnly &&
|
|
!hideSecuritySettings)
|
|
SettingsSection(
|
|
title: Text(translate("Share screen")),
|
|
tiles: shareScreenTiles,
|
|
),
|
|
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
|
if (isAndroid &&
|
|
!disabledSettings &&
|
|
!outgoingOnly &&
|
|
!hideSecuritySettings)
|
|
SettingsSection(
|
|
title: Text(translate("Enhancements")),
|
|
tiles: enhancementsTiles,
|
|
),
|
|
SettingsSection(
|
|
title: Text(translate("About")),
|
|
tiles: [
|
|
SettingsTile(
|
|
onPressed: (context) async {
|
|
await launchUrl(Uri.parse(url));
|
|
},
|
|
title: Text(translate("Version: ") + version),
|
|
value: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8),
|
|
child: Text('rustdesk.com',
|
|
style: TextStyle(
|
|
decoration: TextDecoration.underline,
|
|
)),
|
|
),
|
|
leading: Icon(Icons.info)),
|
|
SettingsTile(
|
|
title: Text(translate("Build Date")),
|
|
value: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8),
|
|
child: Text(_buildDate),
|
|
),
|
|
leading: Icon(Icons.query_builder)),
|
|
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)),
|
|
SettingsTile(
|
|
onPressed: (context) => onCopyId(_myId),
|
|
title: Text(translate("ID")),
|
|
value: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8),
|
|
child: Text(_myId),
|
|
),
|
|
leading: Icon(Icons.perm_identity)),
|
|
SettingsTile(
|
|
title: Text(translate("Privacy Statement")),
|
|
onPressed: (context) =>
|
|
launchUrlString('https://rustdesk.com/privacy.html'),
|
|
leading: Icon(Icons.privacy_tip),
|
|
)
|
|
],
|
|
),
|
|
],
|
|
);
|
|
return settings;
|
|
}
|
|
|
|
Future<bool> canStartOnBoot() async {
|
|
// start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
|
|
if (_hasIgnoreBattery && !_ignoreBatteryOpt) {
|
|
return false;
|
|
}
|
|
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
|
return false;
|
|
}
|
|
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 showLanguageSettings(OverlayDialogManager dialogManager) async {
|
|
try {
|
|
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
|
var lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
|
|
dialogManager.show((setState, close, context) {
|
|
setLang(v) async {
|
|
if (lang != v) {
|
|
setState(() {
|
|
lang = v;
|
|
});
|
|
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: v);
|
|
HomePage.homeKey.currentState?.refreshPages();
|
|
Future.delayed(Duration(milliseconds: 200), close);
|
|
}
|
|
}
|
|
|
|
final isOptFixed = isOptionFixed(kCommConfKeyLang);
|
|
return CustomAlertDialog(
|
|
content: Column(
|
|
children: [
|
|
getRadio(Text(translate('Default')), defaultOptionLang, lang,
|
|
isOptFixed ? null : setLang),
|
|
Divider(color: MyTheme.border),
|
|
] +
|
|
langs.map((e) {
|
|
final key = e[0] as String;
|
|
final name = e[1] as String;
|
|
return getRadio(Text(translate(name)), key, lang,
|
|
isOptFixed ? null : setLang);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}, backDismiss: true, clickMaskDismiss: true);
|
|
} catch (e) {
|
|
//
|
|
}
|
|
}
|
|
|
|
void showThemeSettings(OverlayDialogManager dialogManager) async {
|
|
var themeMode = MyTheme.getThemeModePreference();
|
|
|
|
dialogManager.show((setState, close, context) {
|
|
setTheme(v) {
|
|
if (themeMode != v) {
|
|
setState(() {
|
|
themeMode = v;
|
|
});
|
|
MyTheme.changeDarkMode(themeMode);
|
|
Future.delayed(Duration(milliseconds: 200), close);
|
|
}
|
|
}
|
|
|
|
final isOptFixed = isOptionFixed(kCommConfKeyTheme);
|
|
return CustomAlertDialog(
|
|
content: Column(children: [
|
|
getRadio(Text(translate('Light')), ThemeMode.light, themeMode,
|
|
isOptFixed ? null : setTheme),
|
|
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode,
|
|
isOptFixed ? null : setTheme),
|
|
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
|
isOptFixed ? null : setTheme)
|
|
]),
|
|
);
|
|
}, backDismiss: true, clickMaskDismiss: true);
|
|
}
|
|
|
|
void showAbout(OverlayDialogManager dialogManager) {
|
|
dialogManager.show((setState, close, context) {
|
|
return CustomAlertDialog(
|
|
title: Text(translate('About RustDesk')),
|
|
content: Wrap(direction: Axis.vertical, spacing: 12, children: [
|
|
Text('Version: $version'),
|
|
InkWell(
|
|
onTap: () async {
|
|
const url = 'https://rustdesk.com/';
|
|
await launchUrl(Uri.parse(url));
|
|
},
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8),
|
|
child: Text('rustdesk.com',
|
|
style: TextStyle(
|
|
decoration: TextDecoration.underline,
|
|
)),
|
|
)),
|
|
]),
|
|
actions: [],
|
|
);
|
|
}, clickMaskDismiss: true, backDismiss: true);
|
|
}
|
|
|
|
class ScanButton extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return IconButton(
|
|
icon: Icon(Icons.qr_code_scanner),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (BuildContext context) => ScanPage(),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DisplayPage extends StatefulWidget {
|
|
const _DisplayPage();
|
|
|
|
@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: kOptionViewStyle),
|
|
asyncSetter: isOptionFixed(kOptionViewStyle)
|
|
? null
|
|
: (value) async {
|
|
await bind.mainSetUserDefaultOption(
|
|
key: kOptionViewStyle, 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: kOptionImageQuality);
|
|
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
|
|
return v;
|
|
},
|
|
asyncSetter: isOptionFixed(kOptionImageQuality)
|
|
? null
|
|
: (value) async {
|
|
await bind.mainSetUserDefaultOption(
|
|
key: kOptionImageQuality, value: value);
|
|
showCustomImageQuality.value =
|
|
value == kRemoteImageQualityCustom;
|
|
},
|
|
tail: customImageQualitySetting(),
|
|
showTail: showCustomImageQuality,
|
|
notCloseValue: kRemoteImageQualityCustom,
|
|
),
|
|
_getPopupDialogRadioEntry(
|
|
title: 'Default Codec',
|
|
list: codecList,
|
|
getter: () =>
|
|
bind.mainGetUserDefaultOption(key: kOptionCodecPreference),
|
|
asyncSetter: isOptionFixed(kOptionCodecPreference)
|
|
? null
|
|
: (value) async {
|
|
await bind.mainSetUserDefaultOption(
|
|
key: kOptionCodecPreference, value: value);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
SettingsSection(
|
|
title: Text(translate('Other Default Options')),
|
|
tiles:
|
|
otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList(),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
|
|
SettingsTile otherRow(String label, String key) {
|
|
final value = getOtherDefaultSettingOption(key) == 'Y';
|
|
final isOptFixed = isOtherDefaultSettingReadOnly(key);
|
|
return SettingsTile.switchTile(
|
|
initialValue: value,
|
|
title: Text(translate(label)),
|
|
onToggle: isOptFixed
|
|
? null
|
|
: (b) async {
|
|
await setOtherDefaultSettingOption(
|
|
key,
|
|
b ? 'Y' : defaultOptionNo,
|
|
);
|
|
setState(() {});
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ManageTrustedDevices extends StatefulWidget {
|
|
const _ManageTrustedDevices();
|
|
|
|
@override
|
|
State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState();
|
|
}
|
|
|
|
class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> {
|
|
RxList<TrustedDevice> trustedDevices = RxList.empty(growable: true);
|
|
RxList<Uint8List> selectedDevices = RxList.empty();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(translate('Manage trusted devices')),
|
|
centerTitle: true,
|
|
actions: [
|
|
Obx(() => IconButton(
|
|
icon: Icon(Icons.delete, color: Colors.white),
|
|
onPressed: selectedDevices.isEmpty
|
|
? null
|
|
: () {
|
|
confrimDeleteTrustedDevicesDialog(
|
|
trustedDevices, selectedDevices);
|
|
}))
|
|
],
|
|
),
|
|
body: FutureBuilder(
|
|
future: TrustedDevice.get(),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return Center(child: CircularProgressIndicator());
|
|
}
|
|
if (snapshot.hasError) {
|
|
return Center(child: Text('Error: ${snapshot.error}'));
|
|
}
|
|
final devices = snapshot.data as List<TrustedDevice>;
|
|
trustedDevices = devices.obs;
|
|
return trustedDevicesTable(trustedDevices, selectedDevices);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RadioEntry {
|
|
final String label;
|
|
final String value;
|
|
_RadioEntry(this.label, this.value);
|
|
}
|
|
|
|
typedef _RadioEntryGetter = String Function();
|
|
typedef _RadioEntrySetter = Future<void> Function(String);
|
|
|
|
SettingsTile _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) {
|
|
final onChanged = asyncSetter == null
|
|
? null
|
|
: (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, onChanged))
|
|
.toList(),
|
|
Offstage(
|
|
offstage:
|
|
!(tail != null && showTail != null && showTail.value == true),
|
|
child: tail,
|
|
),
|
|
]),
|
|
));
|
|
}, backDismiss: true, clickMaskDismiss: true);
|
|
}
|
|
|
|
return SettingsTile(
|
|
title: Text(translate(title)),
|
|
onPressed: asyncSetter == null ? null : (context) => showDialog(),
|
|
value: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8),
|
|
child: Obx(() => Text(translate(valueText.value))),
|
|
),
|
|
);
|
|
}
|