rustdesk/flutter/lib/desktop/pages/desktop_setting_page.dart

2308 lines
73 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:convert';
import 'dart:io';
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/audio_input.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';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/plugin/manager.dart';
import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
2022-09-21 18:49:28 +08:00
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
2022-09-16 21:52:08 +08:00
import '../../common/widgets/dialog.dart';
2023-01-08 22:30:34 +08:00
import '../../common/widgets/login.dart';
2022-09-16 21:52:08 +08:00
const double _kTabWidth = 200;
const double _kTabHeight = 42;
const double _kCardFixedWidth = 540;
const double _kCardLeftMargin = 15;
const double _kContentHMargin = 15;
const double _kContentHSubMargin = _kContentHMargin + 33;
const double _kCheckBoxLeftMargin = 10;
const double _kRadioLeftMargin = 10;
const double _kListViewBottomMargin = 15;
const double _kTitleFontSize = 20;
const double _kContentFontSize = 15;
const Color _accentColor = MyTheme.accent;
const String _kSettingPageControllerTag = 'settingPageController';
const String _kSettingPageTabKeyTag = 'settingPageTabKey';
class _TabInfo {
late final SettingsTabKey key;
late final String label;
late final IconData unselected;
late final IconData selected;
_TabInfo(this.key, this.label, this.unselected, this.selected);
}
enum SettingsTabKey {
general,
safety,
network,
display,
plugin,
account,
about,
}
class DesktopSettingPage extends StatefulWidget {
final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general,
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
SettingsTabKey.safety,
if (!bind.isDisableSettings()) SettingsTabKey.network,
if (!bind.isIncomingOnly()) SettingsTabKey.display,
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin,
if (!bind.isDisableAccount()) SettingsTabKey.account,
SettingsTabKey.about,
];
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
@override
State<DesktopSettingPage> createState() => _DesktopSettingPageState();
static void switch2page(SettingsTabKey page) {
try {
int index = tabKeys.indexOf(page);
if (index == -1) {
return;
}
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
DesktopTabPage.onAddSetting(initialPage: page);
PageController controller = Get.find(tag: _kSettingPageControllerTag);
Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag);
selected.value = page;
controller.jumpToPage(index);
} else {
DesktopTabPage.onAddSetting(initialPage: page);
}
} catch (e) {
debugPrintStack(label: '$e');
}
}
}
class _DesktopSettingPageState extends State<DesktopSettingPage>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
late PageController controller;
late Rx<SettingsTabKey> selectedTab;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
if (initialIndex == -1) {
initialIndex = 0;
}
selectedTab = DesktopSettingPage.tabKeys[initialIndex].obs;
Get.put<Rx<SettingsTabKey>>(selectedTab, tag: _kSettingPageTabKeyTag);
controller = PageController(initialPage: initialIndex);
Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
controller.addListener(() {
if (controller.page != null) {
int page = controller.page!.toInt();
if (page < DesktopSettingPage.tabKeys.length) {
selectedTab.value = DesktopSettingPage.tabKeys[page];
}
}
});
}
@override
void dispose() {
super.dispose();
Get.delete<PageController>(tag: _kSettingPageControllerTag);
Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
}
List<_TabInfo> _settingTabs() {
final List<_TabInfo> settingTabs = <_TabInfo>[];
for (final tab in DesktopSettingPage.tabKeys) {
switch (tab) {
case SettingsTabKey.general:
settingTabs.add(_TabInfo(
tab, 'General', Icons.settings_outlined, Icons.settings));
break;
case SettingsTabKey.safety:
settingTabs.add(_TabInfo(tab, 'Security',
Icons.enhanced_encryption_outlined, Icons.enhanced_encryption));
break;
case SettingsTabKey.network:
settingTabs
.add(_TabInfo(tab, 'Network', Icons.link_outlined, Icons.link));
break;
case SettingsTabKey.display:
settingTabs.add(_TabInfo(tab, 'Display',
Icons.desktop_windows_outlined, Icons.desktop_windows));
break;
case SettingsTabKey.plugin:
settingTabs.add(_TabInfo(
tab, 'Plugin', Icons.extension_outlined, Icons.extension));
break;
case SettingsTabKey.account:
settingTabs.add(
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
break;
case SettingsTabKey.about:
settingTabs
.add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info));
break;
}
}
return settingTabs;
}
List<Widget> _children() {
final children = [
_General(),
2024-03-15 00:26:53 +08:00
if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(),
if (!bind.isDisableSettings()) _Network(),
if (!bind.isIncomingOnly()) _Display(),
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
_Plugin(),
if (!bind.isDisableAccount()) _Account(),
_About(),
];
return children;
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
2023-02-23 23:49:31 +08:00
backgroundColor: Theme.of(context).colorScheme.background,
body: Row(
children: <Widget>[
SizedBox(
width: _kTabWidth,
child: Column(
children: [
_header(),
Flexible(child: _listView(tabs: _settingTabs())),
],
),
),
2023-03-04 14:28:43 +08:00
const VerticalDivider(width: 1),
Expanded(
child: Container(
2022-09-23 16:31:50 +08:00
color: Theme.of(context).scaffoldBackgroundColor,
2022-09-21 18:49:28 +08:00
child: DesktopScrollWrapper(
scrollController: controller,
child: PageView(
controller: controller,
physics: NeverScrollableScrollPhysics(),
children: _children(),
2022-09-21 18:49:28 +08:00
)),
),
)
],
),
);
}
Widget _header() {
return Row(
children: [
SizedBox(
height: 62,
child: Text(
translate('Settings'),
textAlign: TextAlign.left,
style: const TextStyle(
color: _accentColor,
fontSize: _kTitleFontSize,
fontWeight: FontWeight.w400,
),
),
).marginOnly(left: 20, top: 10),
const Spacer(),
],
);
}
Widget _listView({required List<_TabInfo> tabs}) {
2022-09-21 18:49:28 +08:00
final scrollController = ScrollController();
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
2023-02-13 16:18:46 +08:00
physics: DraggableNeverScrollableScrollPhysics(),
2022-09-21 18:49:28 +08:00
controller: scrollController,
children: tabs.map((tab) => _listItem(tab: tab)).toList(),
2022-09-21 18:49:28 +08:00
));
}
Widget _listItem({required _TabInfo tab}) {
return Obx(() {
bool selected = tab.key == selectedTab.value;
return SizedBox(
width: _kTabWidth,
height: _kTabHeight,
child: InkWell(
onTap: () {
if (selectedTab.value != tab.key) {
int index = DesktopSettingPage.tabKeys.indexOf(tab.key);
if (index == -1) {
return;
}
controller.jumpToPage(index);
}
selectedTab.value = tab.key;
},
child: Row(children: [
Container(
width: 4,
height: _kTabHeight * 0.7,
color: selected ? _accentColor : null,
),
Icon(
selected ? tab.selected : tab.unselected,
color: selected ? _accentColor : null,
size: 20,
).marginOnly(left: 13, right: 10),
Text(
translate(tab.label),
style: TextStyle(
color: selected ? _accentColor : null,
fontWeight: FontWeight.w400,
fontSize: _kContentFontSize),
),
]),
),
);
});
}
}
//#region pages
class _General extends StatefulWidget {
const _General({Key? key}) : super(key: key);
@override
State<_General> createState() => _GeneralState();
}
class _GeneralState extends State<_General> {
2023-06-06 21:51:40 +08:00
final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service');
RxBool serviceBtnEnabled = true.obs;
2023-06-06 21:51:40 +08:00
@override
Widget build(BuildContext context) {
2022-09-21 18:49:28 +08:00
final scrollController = ScrollController();
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
2023-02-13 16:18:46 +08:00
physics: DraggableNeverScrollableScrollPhysics(),
2022-09-21 18:49:28 +08:00
controller: scrollController,
children: [
2023-06-06 21:51:40 +08:00
service(),
2022-09-21 18:49:28 +08:00
theme(),
hwcodec(),
audio(context),
record(context),
WaylandCard(),
2022-09-21 18:49:28 +08:00
_Card(title: 'Language', children: [language()]),
other()
2022-09-21 18:49:28 +08:00
],
).marginOnly(bottom: _kListViewBottomMargin));
}
Widget theme() {
final current = MyTheme.getThemeModePreference().toShortString();
onChanged(String value) {
MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
setState(() {});
}
final isOptFixed = isOptionFixed(kCommConfKeyTheme);
return _Card(title: 'Theme', children: [
_Radio<String>(context,
value: 'light',
groupValue: current,
label: 'Light',
onChanged: isOptFixed ? null : onChanged),
_Radio<String>(context,
value: 'dark',
groupValue: current,
label: 'Dark',
onChanged: isOptFixed ? null : onChanged),
_Radio<String>(context,
value: 'system',
groupValue: current,
label: 'Follow System',
onChanged: isOptFixed ? null : onChanged),
]);
}
2023-06-06 21:51:40 +08:00
Widget service() {
if (bind.isOutgoingOnly()) {
return const Offstage();
}
2023-06-06 21:51:40 +08:00
return _Card(title: 'Service', children: [
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
() async {
serviceBtnEnabled.value = false;
2023-06-06 21:51:40 +08:00
await start_service(serviceStop.value);
// enable the button after 1 second
Future.delayed(const Duration(seconds: 1), () {
serviceBtnEnabled.value = true;
2023-06-06 21:51:40 +08:00
});
}();
}, enabled: serviceBtnEnabled.value))
2023-06-06 21:51:40 +08:00
]);
}
Widget other() {
final children = <Widget>[
if (!bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
'enable-confirm-closing-tabs',
isServer: false),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
wallpaper(),
if (!bind.isIncomingOnly()) ...[
_OptionCheckBox(
context,
'Open connection in new tab',
kOptionOpenNewConnInTabs,
isServer: false,
),
// though this is related to GUI, but opengl problem affects all users, so put in config rather than local
Tooltip(
message: translate('software_render_tip'),
child: _OptionCheckBox(context, "Always use software rendering",
'allow-always-software-render'),
),
_OptionCheckBox(
context,
'Check for software update on startup',
'enable-check-update',
isServer: false,
)
],
];
if (bind.mainShowOption(key: 'allow-linux-headless')) {
children.add(_OptionCheckBox(
context, 'Allow linux headless', 'allow-linux-headless'));
}
return _Card(title: 'Other', children: children);
}
Widget wallpaper() {
if (bind.isOutgoingOnly()) {
return const Offstage();
}
return futureBuilder(future: () async {
final support = await bind.mainSupportRemoveWallpaper();
return support;
}(), hasData: (data) {
if (data is bool && data == true) {
final option = 'allow-remove-wallpaper';
bool value = mainGetBoolOptionSync(option);
return Row(
children: [
Flexible(
child: _OptionCheckBox(
context,
'Remove wallpaper during incoming sessions',
option,
update: () {
setState(() {});
},
),
),
if (value)
_CountDownButton(
text: 'Test',
second: 5,
onPressed: () {
bind.mainTestWallpaper(second: 5);
},
)
],
);
}
return Offstage();
});
}
Widget hwcodec() {
final hwcodec = bind.mainHasHwcodec();
final vram = bind.mainHasVram();
2022-09-16 19:43:28 +08:00
return Offstage(
offstage: !(hwcodec || vram),
2022-09-16 19:43:28 +08:00
child: _Card(title: 'Hardware Codec', children: [
_OptionCheckBox(
context,
'Enable hardware codec',
kOptionEnableHwcodec,
update: () {
if (mainGetBoolOptionSync(kOptionEnableHwcodec)) {
bind.mainCheckHwcodec();
}
},
)
2022-09-16 19:43:28 +08:00
]),
);
}
Widget audio(BuildContext context) {
if (bind.isOutgoingOnly()) {
return const Offstage();
}
return AudioInput(builder: (devices, currentDevice, setDevice) {
return _Card(title: 'Audio Input Device', children: [
...devices.map((device) => _Radio<String>(context,
value: device,
groupValue: currentDevice,
autoNewLine: false,
label: device, onChanged: (value) {
setDevice(value);
setState(() {});
}))
]);
});
}
Widget record(BuildContext context) {
final showRootDir = isWindows && bind.mainIsInstalled();
return futureBuilder(future: () async {
String user_dir = await bind.mainVideoSaveDirectory(root: false);
String root_dir =
showRootDir ? await bind.mainVideoSaveDirectory(root: true) : '';
bool user_dir_exists = await Directory(user_dir).exists();
bool root_dir_exists =
showRootDir ? await Directory(root_dir).exists() : false;
// canLaunchUrl blocked on windows portable, user SYSTEM
return {
'user_dir': user_dir,
'root_dir': root_dir,
'user_dir_exists': user_dir_exists,
'root_dir_exists': root_dir_exists,
};
}(), hasData: (data) {
Map<String, dynamic> map = data as Map<String, dynamic>;
String user_dir = map['user_dir']!;
String root_dir = map['root_dir']!;
bool root_dir_exists = map['root_dir_exists']!;
bool user_dir_exists = map['user_dir_exists']!;
return _Card(title: 'Recording', children: [
_OptionCheckBox(context, 'Automatically record incoming sessions',
kOptionAllowAutoRecordIncoming),
if (showRootDir)
Row(
children: [
Text('${translate("Incoming")}:'),
Expanded(
child: GestureDetector(
onTap: root_dir_exists
? () => launchUrl(Uri.file(root_dir))
: null,
child: Text(
root_dir,
softWrap: true,
style: root_dir_exists
? const TextStyle(
decoration: TextDecoration.underline)
: null,
)).marginOnly(left: 10),
),
],
).marginOnly(left: _kContentHMargin),
Row(
children: [
Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'),
Expanded(
child: GestureDetector(
onTap: user_dir_exists
? () => launchUrl(Uri.file(user_dir))
: null,
child: Text(
user_dir,
softWrap: true,
style: user_dir_exists
? const TextStyle(decoration: TextDecoration.underline)
: null,
)).marginOnly(left: 10),
),
ElevatedButton(
onPressed: isOptionFixed(kOptionVideoSaveDirectory)
? null
: () async {
String? initialDirectory;
if (await Directory.fromUri(Uri.directory(user_dir))
.exists()) {
initialDirectory = user_dir;
}
String? selectedDirectory =
await FilePicker.platform.getDirectoryPath(
initialDirectory: initialDirectory);
if (selectedDirectory != null) {
await bind.mainSetOption(
key: kOptionVideoSaveDirectory,
value: selectedDirectory);
setState(() {});
}
},
child: Text(translate('Change')))
.marginOnly(left: 5),
],
).marginOnly(left: _kContentHMargin),
]);
});
}
Widget language() {
return futureBuilder(future: () async {
String langs = await bind.mainGetLangs();
return {'langs': langs};
}(), hasData: (res) {
Map<String, String> data = res as Map<String, String>;
List<dynamic> langsList = jsonDecode(data['langs']!);
Map<String, String> langsMap = {for (var v in langsList) v[0]: v[1]};
List<String> keys = langsMap.keys.toList();
List<String> values = langsMap.values.toList();
keys.insert(0, defaultOptionLang);
2023-04-13 16:06:46 +08:00
values.insert(0, translate('Default'));
String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang);
if (!keys.contains(currentKey)) {
currentKey = defaultOptionLang;
}
final isOptFixed = isOptionFixed(kCommConfKeyLang);
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
return ComboBox(
keys: keys,
values: values,
initialKey: currentKey,
onChanged: (key) async {
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
reloadAllWindows();
bind.mainChangeLanguage(lang: key);
},
enabled: !isOptFixed,
).marginOnly(left: _kContentHMargin);
});
}
}
enum _AccessMode {
custom,
full,
view,
}
class _Safety extends StatefulWidget {
const _Safety({Key? key}) : super(key: key);
@override
State<_Safety> createState() => _SafetyState();
}
class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
bool locked = bind.mainIsInstalled();
2022-09-21 18:49:28 +08:00
final scrollController = ScrollController();
@override
Widget build(BuildContext context) {
super.build(context);
2022-09-21 18:49:28 +08:00
return DesktopScrollWrapper(
scrollController: scrollController,
child: SingleChildScrollView(
2023-02-13 16:18:46 +08:00
physics: DraggableNeverScrollableScrollPhysics(),
2022-09-21 18:49:28 +08:00
controller: scrollController,
child: Column(
children: [
_lock(locked, 'Unlock Security Settings', () {
locked = false;
setState(() => {});
}),
AbsorbPointer(
absorbing: locked,
child: Column(children: [
permissions(context),
password(context),
2024-01-19 15:35:58 +08:00
_Card(title: '2FA', children: [tfa()]),
2022-09-21 18:49:28 +08:00
_Card(title: 'ID', children: [changeId()]),
2022-09-21 19:18:40 +08:00
more(context),
2022-09-21 18:49:28 +08:00
]),
),
],
)).marginOnly(bottom: _kListViewBottomMargin));
}
2024-01-19 15:35:58 +08:00
Widget tfa() {
bool enabled = !locked;
// Simple temp wrapper for PR check
tmpWrapper() {
RxBool has2fa = bind.mainHasValid2FaSync().obs;
update() async {
has2fa.value = bind.mainHasValid2FaSync();
}
onChanged(bool? checked) async {
change2fa(callback: update);
}
return GestureDetector(
child: InkWell(
child: Obx(() => Row(
children: [
Checkbox(
value: has2fa.value,
onChanged: enabled ? onChanged : null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('enable-2fa-title'),
style:
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
TextStyle(color: disabledTextColor(context, enabled)),
2024-01-19 15:35:58 +08:00
))
],
)),
),
onTap: () {
onChanged(!has2fa.value);
},
).marginOnly(left: _kCheckBoxLeftMargin);
}
return tmpWrapper();
}
2022-09-21 18:49:28 +08:00
Widget changeId() {
2023-06-26 23:27:18 +08:00
return ChangeNotifierProvider.value(
value: gFFI.serverModel,
child: Consumer<ServerModel>(builder: ((context, model, child) {
return _Button('Change ID', changeIdDialog,
enabled: !locked && model.connectStatus > 0);
})));
}
Widget permissions(context) {
bool enabled = !locked;
// Simple temp wrapper for PR check
tmpWrapper() {
String accessMode = bind.mainGetOptionSync(key: kOptionAccessMode);
_AccessMode mode;
if (accessMode == 'full') {
mode = _AccessMode.full;
} else if (accessMode == 'view') {
mode = _AccessMode.view;
} else {
mode = _AccessMode.custom;
}
String initialKey;
bool? fakeValue;
switch (mode) {
case _AccessMode.custom:
initialKey = '';
fakeValue = null;
break;
case _AccessMode.full:
initialKey = 'full';
fakeValue = true;
break;
case _AccessMode.view:
initialKey = 'view';
fakeValue = false;
break;
}
return _Card(title: 'Permissions', children: [
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
ComboBox(
keys: [
defaultOptionAccessMode,
'full',
'view',
],
values: [
translate('Custom'),
translate('Full Access'),
translate('Screen Share'),
],
enabled: enabled && !isOptionFixed(kOptionAccessMode),
initialKey: initialKey,
onChanged: (mode) async {
await bind.mainSetOption(key: kOptionAccessMode, value: mode);
setState(() {});
}).marginOnly(left: _kContentHMargin),
Column(
children: [
_OptionCheckBox(
context, 'Enable keyboard/mouse', kOptionEnableKeyboard,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable file transfer', kOptionEnableFileTransfer,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable audio', kOptionEnableAudio,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable TCP tunneling', kOptionEnableTunnel,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable remote restart', kOptionEnableRemoteRestart,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(
context, 'Enable recording session', kOptionEnableRecordSession,
enabled: enabled, fakeValue: fakeValue),
if (isWindows)
_OptionCheckBox(context, 'Enable blocking user input',
kOptionEnableBlockInput,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable remote configuration modification',
kOptionAllowRemoteConfigModification,
enabled: enabled, fakeValue: fakeValue),
],
),
]);
}
return tmpWrapper();
}
Widget password(BuildContext context) {
return ChangeNotifierProvider.value(
value: gFFI.serverModel,
child: Consumer<ServerModel>(builder: ((context, model, child) {
List<String> passwordKeys = [
kUseTemporaryPassword,
kUsePermanentPassword,
kUseBothPasswords,
];
List<String> passwordValues = [
translate('Use one-time password'),
translate('Use permanent password'),
translate('Use both passwords'),
];
bool tmpEnabled = model.verificationMethod != kUsePermanentPassword;
bool permEnabled = model.verificationMethod != kUseTemporaryPassword;
String currentValue =
passwordValues[passwordKeys.indexOf(model.verificationMethod)];
List<Widget> radios = passwordValues
.map((value) => _Radio<String>(
context,
value: value,
groupValue: currentValue,
label: value,
onChanged: locked
? null
: ((value) {
() async {
await model.setVerificationMethod(
passwordKeys[passwordValues.indexOf(value)]);
await model.updatePasswordModel();
}();
}),
))
.toList();
var onChanged = tmpEnabled && !locked
? (value) {
if (value != null) {
() async {
await model.setTemporaryPasswordLength(value.toString());
await model.updatePasswordModel();
}();
}
}
: null;
List<Widget> lengthRadios = ['6', '8', '10']
.map((value) => GestureDetector(
child: Row(
children: [
Radio(
value: value,
groupValue: model.temporaryPasswordLength,
onChanged: onChanged),
Text(
value,
style: TextStyle(
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
color: disabledTextColor(
context, onChanged != null)),
),
],
2023-02-12 19:59:51 +08:00
).paddingOnly(right: 10),
onTap: () => onChanged?.call(value),
))
.toList();
final modeKeys = <String>[
'password',
'click',
defaultOptionApproveMode
];
final modeValues = [
translate('Accept sessions via password'),
translate('Accept sessions via click'),
translate('Accept sessions via both'),
];
var modeInitialKey = model.approveMode;
if (!modeKeys.contains(modeInitialKey)) modeInitialKey = '';
final usePassword = model.approveMode != 'click';
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
return _Card(title: 'Password', children: [
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
ComboBox(
enabled: !locked && !isApproveModeFixed,
keys: modeKeys,
values: modeValues,
initialKey: modeInitialKey,
onChanged: (key) => model.setApproveMode(key),
).marginOnly(left: _kContentHMargin),
if (usePassword) radios[0],
if (usePassword)
_SubLabeledWidget(
2023-02-12 19:59:51 +08:00
context,
'One-time password length',
Row(
children: [
...lengthRadios,
],
),
enabled: tmpEnabled && !locked),
if (usePassword) radios[1],
if (usePassword)
_SubButton('Set permanent password', setPasswordDialog,
permEnabled && !locked),
// if (usePassword)
// hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
if (usePassword) radios[2],
]);
})));
}
2022-09-21 19:18:40 +08:00
Widget more(BuildContext context) {
bool enabled = !locked;
2022-09-21 18:49:28 +08:00
return _Card(title: 'Security', children: [
shareRdp(context, enabled),
2023-11-06 20:12:01 +08:00
_OptionCheckBox(context, 'Deny LAN discovery', 'enable-lan-discovery',
reverse: true, enabled: enabled),
...directIp(context),
whitelist(),
...autoDisconnect(context),
if (bind.mainIsInstalled())
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
'allow-only-conn-window-open',
reverse: false, enabled: enabled),
]);
}
shareRdp(BuildContext context, bool enabled) {
onChanged(bool b) async {
await bind.mainSetShareRdp(enable: b);
setState(() {});
}
bool value = bind.mainIsShareRdp();
return Offstage(
offstage: !(isWindows && bind.mainIsInstalled()),
child: GestureDetector(
child: Row(
children: [
Checkbox(
value: value,
onChanged: enabled ? (_) => onChanged(!value) : null)
.marginOnly(right: 5),
Expanded(
child: Text(translate('Enable RDP session sharing'),
style:
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
TextStyle(color: disabledTextColor(context, enabled))),
)
],
).marginOnly(left: _kCheckBoxLeftMargin),
onTap: enabled ? () => onChanged(!value) : null),
);
}
List<Widget> directIp(BuildContext context) {
TextEditingController controller = TextEditingController();
update() => setState(() {});
RxBool applyEnabled = false.obs;
return [
_OptionCheckBox(context, 'Enable direct IP access', kOptionDirectServer,
update: update, enabled: !locked),
() {
// Simple temp wrapper for PR check
tmpWrapper() {
bool enabled = option2bool(kOptionDirectServer,
bind.mainGetOptionSync(key: kOptionDirectServer));
if (!enabled) applyEnabled.value = false;
controller.text =
bind.mainGetOptionSync(key: kOptionDirectAccessPort);
final isOptFixed = isOptionFixed(kOptionDirectAccessPort);
return Offstage(
offstage: !enabled,
child: _SubLabeledWidget(
context,
'Port',
Row(children: [
SizedBox(
width: 95,
child: TextField(
controller: controller,
enabled: enabled && !locked && !isOptFixed,
onChanged: (_) => applyEnabled.value = true,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
decoration: const InputDecoration(
hintText: '21118',
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
).marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed: applyEnabled.value &&
enabled &&
!locked &&
!isOptFixed
? () async {
applyEnabled.value = false;
await bind.mainSetOption(
key: kOptionDirectAccessPort,
value: controller.text);
}
: null,
child: Text(
translate('Apply'),
),
))
]),
enabled: enabled && !locked && !isOptFixed,
),
);
}
return tmpWrapper();
}(),
];
}
Widget whitelist() {
bool enabled = !locked;
// Simple temp wrapper for PR check
tmpWrapper() {
RxBool hasWhitelist = (bind.mainGetOptionSync(key: kOptionWhitelist) !=
defaultOptionWhitelist)
.obs;
update() async {
hasWhitelist.value = bind.mainGetOptionSync(key: kOptionWhitelist) !=
defaultOptionWhitelist;
}
onChanged(bool? checked) async {
changeWhiteList(callback: update);
}
final isOptFixed = isOptionFixed(kOptionWhitelist);
return GestureDetector(
child: Tooltip(
message: translate('whitelist_tip'),
child: Obx(() => Row(
children: [
Checkbox(
value: hasWhitelist.value,
onChanged: enabled && !isOptFixed ? onChanged : null)
.marginOnly(right: 5),
Offstage(
offstage: !hasWhitelist.value,
child: MouseRegion(
child: const Icon(Icons.warning_amber_rounded,
color: Color.fromARGB(255, 255, 204, 0))
.marginOnly(right: 5),
cursor: SystemMouseCursors.click,
),
),
Expanded(
child: Text(
translate('Use IP Whitelisting'),
style:
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
TextStyle(color: disabledTextColor(context, enabled)),
))
],
)),
),
onTap: enabled
? () {
onChanged(!hasWhitelist.value);
}
: null,
).marginOnly(left: _kCheckBoxLeftMargin);
}
return tmpWrapper();
}
Widget hide_cm(bool enabled) {
return ChangeNotifierProvider.value(
value: gFFI.serverModel,
child: Consumer<ServerModel>(builder: (context, model, child) {
final enableHideCm = model.approveMode == 'password' &&
model.verificationMethod == kUsePermanentPassword;
onHideCmChanged(bool? b) {
if (b != null) {
bind.mainSetOption(
key: 'allow-hide-cm', value: bool2option('allow-hide-cm', b));
}
}
return Tooltip(
message: enableHideCm ? "" : translate('hide_cm_tip'),
child: GestureDetector(
onTap:
enableHideCm ? () => onHideCmChanged(!model.hideCm) : null,
child: Row(
children: [
Checkbox(
value: model.hideCm,
onChanged: enabled && enableHideCm
? onHideCmChanged
: null)
.marginOnly(right: 5),
Expanded(
child: Text(
translate('Hide connection management window'),
style: TextStyle(
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
color: disabledTextColor(
context, enabled && enableHideCm)),
),
),
],
),
));
}));
}
List<Widget> autoDisconnect(BuildContext context) {
TextEditingController controller = TextEditingController();
update() => setState(() {});
RxBool applyEnabled = false.obs;
return [
_OptionCheckBox(
context, 'auto_disconnect_option_tip', kOptionAllowAutoDisconnect,
update: update, enabled: !locked),
() {
bool enabled = option2bool(kOptionAllowAutoDisconnect,
bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
if (!enabled) applyEnabled.value = false;
controller.text =
bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
final isOptFixed = isOptionFixed(kOptionAutoDisconnectTimeout);
return Offstage(
offstage: !enabled,
child: _SubLabeledWidget(
context,
'Timeout in minutes',
Row(children: [
SizedBox(
width: 95,
child: TextField(
controller: controller,
enabled: enabled && !locked && isOptFixed,
onChanged: (_) => applyEnabled.value = true,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
decoration: const InputDecoration(
hintText: '10',
contentPadding:
EdgeInsets.symmetric(vertical: 12, horizontal: 12),
),
).marginOnly(right: 15),
),
Obx(() => ElevatedButton(
onPressed:
applyEnabled.value && enabled && !locked && !isOptFixed
? () async {
applyEnabled.value = false;
await bind.mainSetOption(
key: kOptionAutoDisconnectTimeout,
value: controller.text);
}
: null,
child: Text(
translate('Apply'),
),
))
]),
enabled: enabled && !locked && !isOptFixed,
),
);
}(),
];
}
}
class _Network extends StatefulWidget {
const _Network({Key? key}) : super(key: key);
@override
State<_Network> createState() => _NetworkState();
}
class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
bool locked = bind.mainIsInstalled();
@override
Widget build(BuildContext context) {
super.build(context);
bool enabled = !locked;
2022-09-21 18:49:28 +08:00
final scrollController = ScrollController();
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
controller: scrollController,
2023-02-13 16:18:46 +08:00
physics: DraggableNeverScrollableScrollPhysics(),
2022-09-21 18:49:28 +08:00
children: [
_lock(locked, 'Unlock Network Settings', () {
locked = false;
setState(() => {});
}),
AbsorbPointer(
absorbing: locked,
child: Column(children: [
server(enabled),
2022-09-21 18:49:28 +08:00
_Card(title: 'Proxy', children: [
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
2022-09-21 18:49:28 +08:00
enabled: enabled),
]),
]),
),
]).marginOnly(bottom: _kListViewBottomMargin));
}
server(bool enabled) {
// Simple temp wrapper for PR check
tmpWrapper() {
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
Map<String, dynamic> oldOptions = jsonDecode(bind.mainGetOptionsSync());
old(String key) {
return (oldOptions[key] ?? '').trim();
}
RxString idErrMsg = ''.obs;
RxString relayErrMsg = ''.obs;
RxString apiErrMsg = ''.obs;
var idController =
TextEditingController(text: old('custom-rendezvous-server'));
var relayController = TextEditingController(text: old('relay-server'));
var apiController = TextEditingController(text: old('api-server'));
var keyController = TextEditingController(text: old('key'));
final controllers = [
idController,
relayController,
apiController,
keyController,
];
final errMsgs = [
idErrMsg,
relayErrMsg,
apiErrMsg,
];
submit() async {
bool result = await setServerConfig(
null,
errMsgs,
ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text));
if (result) {
setState(() {});
showToast(translate('Successful'));
} else {
showToast(translate('Failed'));
}
}
bool secure = !enabled;
return _Card(
title: 'ID/Relay Server',
title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
children: [
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();
}
}
class _Display extends StatefulWidget {
const _Display({Key? key}) : super(key: key);
@override
State<_Display> createState() => _DisplayState();
}
class _DisplayState extends State<_Display> {
@override
Widget build(BuildContext context) {
final scrollController = ScrollController();
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
controller: scrollController,
2023-02-13 16:18:46 +08:00
physics: DraggableNeverScrollableScrollPhysics(),
children: [
viewStyle(context),
scrollStyle(context),
imageQuality(context),
codec(context),
privacyModeImpl(context),
other(context),
]).marginOnly(bottom: _kListViewBottomMargin));
}
Widget viewStyle(BuildContext context) {
final isOptFixed = isOptionFixed(kOptionViewStyle);
onChanged(String value) async {
await bind.mainSetUserDefaultOption(key: kOptionViewStyle, value: value);
setState(() {});
}
final groupValue = bind.mainGetUserDefaultOption(key: kOptionViewStyle);
return _Card(title: 'Default View Style', children: [
_Radio(context,
value: kRemoteViewStyleOriginal,
groupValue: groupValue,
label: 'Scale original',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: kRemoteViewStyleAdaptive,
groupValue: groupValue,
label: 'Scale adaptive',
onChanged: isOptFixed ? null : onChanged),
]);
}
Widget scrollStyle(BuildContext context) {
final isOptFixed = isOptionFixed(kOptionScrollStyle);
onChanged(String value) async {
await bind.mainSetUserDefaultOption(
key: kOptionScrollStyle, value: value);
setState(() {});
}
final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle);
return _Card(title: 'Default Scroll Style', children: [
_Radio(context,
value: kRemoteScrollStyleAuto,
groupValue: groupValue,
label: 'ScrollAuto',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: kRemoteScrollStyleBar,
groupValue: groupValue,
label: 'Scrollbar',
onChanged: isOptFixed ? null : onChanged),
]);
}
Widget imageQuality(BuildContext context) {
onChanged(String value) async {
await bind.mainSetUserDefaultOption(
key: kOptionImageQuality, value: value);
setState(() {});
}
final isOptFixed = isOptionFixed(kOptionImageQuality);
final groupValue = bind.mainGetUserDefaultOption(key: kOptionImageQuality);
return _Card(title: 'Default Image Quality', children: [
_Radio(context,
value: kRemoteImageQualityBest,
groupValue: groupValue,
label: 'Good image quality',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: kRemoteImageQualityBalanced,
groupValue: groupValue,
label: 'Balanced',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: kRemoteImageQualityLow,
groupValue: groupValue,
label: 'Optimize reaction time',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: kRemoteImageQualityCustom,
groupValue: groupValue,
label: 'Custom',
onChanged: isOptFixed ? null : onChanged),
Offstage(
offstage: groupValue != kRemoteImageQualityCustom,
child: customImageQualitySetting(),
)
]);
}
Widget codec(BuildContext context) {
onChanged(String value) async {
await bind.mainSetUserDefaultOption(
key: kOptionCodecPreference, value: value);
setState(() {});
}
final groupValue =
bind.mainGetUserDefaultOption(key: kOptionCodecPreference);
var hwRadios = [];
final isOptFixed = isOptionFixed(kOptionCodecPreference);
try {
final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
final h264 = codecsJson['h264'] ?? false;
final h265 = codecsJson['h265'] ?? false;
if (h264) {
hwRadios.add(_Radio(context,
value: 'h264',
groupValue: groupValue,
label: 'H264',
onChanged: isOptFixed ? null : onChanged));
}
if (h265) {
hwRadios.add(_Radio(context,
value: 'h265',
groupValue: groupValue,
label: 'H265',
onChanged: isOptFixed ? null : onChanged));
}
} catch (e) {
debugPrint("failed to parse supported hwdecodings, err=$e");
}
return _Card(title: 'Default Codec', children: [
_Radio(context,
value: 'auto',
groupValue: groupValue,
label: 'Auto',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: 'vp8',
groupValue: groupValue,
label: 'VP8',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: 'vp9',
2023-04-01 10:13:39 +08:00
groupValue: groupValue,
label: 'VP9',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: 'av1',
groupValue: groupValue,
label: 'AV1',
onChanged: isOptFixed ? null : onChanged),
...hwRadios,
]);
}
Widget privacyModeImpl(BuildContext context) {
final supportedPrivacyModeImpls = bind.mainSupportedPrivacyModeImpls();
late final List<dynamic> privacyModeImpls;
try {
privacyModeImpls = jsonDecode(supportedPrivacyModeImpls);
} catch (e) {
debugPrint('failed to parse supported privacy mode impls, err=$e');
return Offstage();
}
if (privacyModeImpls.length < 2) {
return Offstage();
}
final key = 'privacy-mode-impl-key';
onChanged(String value) async {
await bind.mainSetOption(key: key, value: value);
setState(() {});
}
String groupValue = bind.mainGetOptionSync(key: key);
if (groupValue.isEmpty) {
groupValue = bind.mainDefaultPrivacyModeImpl();
}
return _Card(
title: 'Privacy mode',
children: privacyModeImpls.map((impl) {
final d = impl as List<dynamic>;
return _Radio(context,
value: d[0] as String,
groupValue: groupValue,
label: d[1] as String,
onChanged: onChanged);
}).toList(),
);
}
Widget otherRow(String label, String key) {
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
final isOptFixed = isOptionFixed(key);
onChanged(bool b) async {
await bind.mainSetUserDefaultOption(
key: key,
value: b
? 'Y'
: (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo));
setState(() {});
}
return GestureDetector(
child: Row(
children: [
Checkbox(
value: value,
onChanged: isOptFixed ? null : (_) => onChanged(!value))
.marginOnly(right: 5),
Expanded(
child: Text(translate(label)),
)
],
).marginOnly(left: _kCheckBoxLeftMargin),
onTap: isOptFixed ? null : () => onChanged(!value));
}
Widget other(BuildContext context) {
final children =
otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList();
return _Card(title: 'Other Default Options', children: children);
}
}
class _Account extends StatefulWidget {
const _Account({Key? key}) : super(key: key);
@override
State<_Account> createState() => _AccountState();
}
class _AccountState extends State<_Account> {
@override
Widget build(BuildContext context) {
2022-09-21 18:49:28 +08:00
final scrollController = ScrollController();
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
2023-02-13 16:18:46 +08:00
physics: DraggableNeverScrollableScrollPhysics(),
2022-09-21 18:49:28 +08:00
controller: scrollController,
children: [
_Card(title: 'Account', children: [accountAction(), useInfo()]),
2022-09-21 18:49:28 +08:00
],
).marginOnly(bottom: _kListViewBottomMargin));
}
Widget accountAction() {
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
: logOutConfirmDialog()
}));
}
Widget useInfo() {
text(String key, String value) {
return Align(
alignment: Alignment.centerLeft,
child: SelectionArea(child: Text('${translate(key)}: $value'))
.marginSymmetric(vertical: 4),
);
}
return Obx(() => Offstage(
offstage: gFFI.userModel.userName.value.isEmpty,
child: Column(
children: [
text('Username', gFFI.userModel.userName.value),
// text('Group', gFFI.groupModel.groupName.value),
],
),
)).marginOnly(left: 18, top: 16);
}
}
class _Checkbox extends StatefulWidget {
final String label;
final bool Function() getValue;
final Future<void> Function(bool) setValue;
const _Checkbox(
{Key? key,
required this.label,
required this.getValue,
required this.setValue})
: super(key: key);
@override
State<_Checkbox> createState() => _CheckboxState();
}
class _CheckboxState extends State<_Checkbox> {
var value = false;
@override
initState() {
super.initState();
value = widget.getValue();
}
@override
Widget build(BuildContext context) {
onChanged(bool b) async {
await widget.setValue(b);
setState(() {
value = widget.getValue();
});
}
return GestureDetector(
child: Row(
children: [
Checkbox(
value: value,
onChanged: (_) => onChanged(!value),
).marginOnly(right: 5),
Expanded(
child: Text(translate(widget.label)),
)
],
).marginOnly(left: _kCheckBoxLeftMargin),
onTap: () => onChanged(!value),
);
}
}
class _Plugin extends StatefulWidget {
const _Plugin({Key? key}) : super(key: key);
@override
State<_Plugin> createState() => _PluginState();
}
class _PluginState extends State<_Plugin> {
@override
Widget build(BuildContext context) {
bind.pluginListReload();
final scrollController = ScrollController();
return DesktopScrollWrapper(
scrollController: scrollController,
child: ChangeNotifierProvider.value(
value: pluginManager,
child: Consumer<PluginManager>(builder: (context, model, child) {
return ListView(
physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: model.plugins.map((entry) => pluginCard(entry)).toList(),
).marginOnly(bottom: _kListViewBottomMargin);
}),
),
);
}
Widget pluginCard(PluginInfo plugin) {
return ChangeNotifierProvider.value(
value: plugin,
child: Consumer<PluginInfo>(
builder: (context, model, child) => DesktopSettingsCard(plugin: model),
),
);
}
Widget accountAction() {
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
: logOutConfirmDialog()
}));
}
}
class _About extends StatefulWidget {
const _About({Key? key}) : super(key: key);
@override
State<_About> createState() => _AboutState();
}
class _AboutState extends State<_About> {
@override
Widget build(BuildContext context) {
return futureBuilder(future: () async {
final license = await bind.mainGetLicense();
final version = await bind.mainGetVersion();
2022-11-17 16:36:07 +08:00
final buildDate = await bind.mainGetBuildDate();
final fingerprint = await bind.mainGetFingerprint();
return {
'license': license,
'version': version,
'buildDate': buildDate,
'fingerprint': fingerprint
};
}(), hasData: (data) {
final license = data['license'].toString();
final version = data['version'].toString();
2022-11-17 16:36:07 +08:00
final buildDate = data['buildDate'].toString();
final fingerprint = data['fingerprint'].toString();
const linkStyle = TextStyle(decoration: TextDecoration.underline);
2022-09-21 18:49:28 +08:00
final scrollController = ScrollController();
return DesktopScrollWrapper(
scrollController: scrollController,
child: SingleChildScrollView(
controller: scrollController,
2023-02-13 16:18:46 +08:00
physics: DraggableNeverScrollableScrollPhysics(),
2022-12-27 23:26:11 +08:00
child: _Card(title: '${translate('About')} RustDesk', children: [
2022-09-21 18:49:28 +08:00
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
SelectionArea(
child: Text('${translate('Version')}: $version')
.marginSymmetric(vertical: 4.0)),
SelectionArea(
child: Text('${translate('Build Date')}: $buildDate')
.marginSymmetric(vertical: 4.0)),
SelectionArea(
child: Text('${translate('Fingerprint')}: $fingerprint')
.marginSymmetric(vertical: 4.0)),
2022-09-21 18:49:28 +08:00
InkWell(
onTap: () {
2023-07-08 16:37:56 +08:00
launchUrlString('https://rustdesk.com/privacy.html');
2022-09-21 18:49:28 +08:00
},
2022-12-17 21:40:57 +08:00
child: Text(
translate('Privacy Statement'),
2022-09-21 18:49:28 +08:00
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
InkWell(
onTap: () {
launchUrlString('https://rustdesk.com');
2022-09-21 18:49:28 +08:00
},
2022-12-17 21:40:57 +08:00
child: Text(
translate('Website'),
2022-09-21 18:49:28 +08:00
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
Container(
decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
padding:
const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
child: SelectionArea(
child: Row(
2022-09-21 18:49:28 +08:00
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
2024-02-20 18:00:07 +08:00
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
2022-09-21 18:49:28 +08:00
style: const TextStyle(color: Colors.white),
),
2022-12-17 21:40:57 +08:00
Text(
translate('Slogan_tip'),
2022-09-21 18:49:28 +08:00
style: TextStyle(
fontWeight: FontWeight.w800,
color: Colors.white),
)
],
),
2022-09-21 18:49:28 +08:00
),
],
)),
2022-09-21 18:49:28 +08:00
).marginSymmetric(vertical: 4.0)
],
).marginOnly(left: _kContentHMargin)
]),
));
});
}
}
//#endregion
//#region components
// ignore: non_constant_identifier_names
Widget _Card(
{required String title,
required List<Widget> children,
List<Widget>? title_suffix}) {
2022-10-13 10:23:52 +08:00
return Row(
children: [
Flexible(
child: SizedBox(
width: _kCardFixedWidth,
child: Card(
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
translate(title),
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: _kTitleFontSize,
),
)),
...?title_suffix
],
).marginOnly(left: _kContentHMargin, top: 10, bottom: 10),
2022-10-13 10:23:52 +08:00
...children
.map((e) => e.marginOnly(top: 4, right: _kContentHMargin)),
],
).marginOnly(bottom: 10),
).marginOnly(left: _kCardLeftMargin, top: 15),
),
2022-10-13 10:23:52 +08:00
),
],
);
}
// ignore: non_constant_identifier_names
Widget _OptionCheckBox(BuildContext context, String label, String key,
{Function()? update,
bool reverse = false,
bool enabled = true,
Icon? checkedIcon,
bool? fakeValue,
bool isServer = true}) {
bool value =
isServer ? mainGetBoolOptionSync(key) : mainGetLocalBoolOptionSync(key);
final isOptFixed = isOptionFixed(key);
if (reverse) value = !value;
var ref = value.obs;
onChanged(option) async {
if (option != null) {
if (reverse) option = !option;
isServer
? await mainSetBoolOption(key, option)
: await mainSetLocalBoolOption(key, option);
final readOption = isServer
? mainGetBoolOptionSync(key)
: mainGetLocalBoolOptionSync(key);
if (reverse) {
ref.value = !readOption;
} else {
ref.value = readOption;
}
update?.call();
}
}
if (fakeValue != null) {
ref.value = fakeValue;
enabled = false;
}
return GestureDetector(
child: Obx(
() => Row(
children: [
Checkbox(
value: ref.value,
onChanged: enabled && !isOptFixed ? onChanged : null)
.marginOnly(right: 5),
Offstage(
offstage: !ref.value || checkedIcon == null,
child: checkedIcon?.marginOnly(right: 5),
),
Expanded(
child: Text(
translate(label),
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
style: TextStyle(color: disabledTextColor(context, enabled)),
))
],
),
).marginOnly(left: _kCheckBoxLeftMargin),
onTap: enabled && !isOptFixed
? () {
onChanged(!ref.value);
}
: null,
);
}
// ignore: non_constant_identifier_names
Widget _Radio<T>(BuildContext context,
{required T value,
required T groupValue,
required String label,
required Function(T value)? onChanged,
bool autoNewLine = true}) {
final onChange2 = onChanged != null
? (T? value) {
if (value != null) {
onChanged(value);
}
}
: null;
return GestureDetector(
child: Row(
children: [
Radio<T>(value: value, groupValue: groupValue, onChanged: onChange2),
Expanded(
child: Text(translate(label),
overflow: autoNewLine ? null : TextOverflow.ellipsis,
style: TextStyle(
fontSize: _kContentFontSize,
color: disabledTextColor(context, onChange2 != null)))
.marginOnly(left: 5),
),
],
).marginOnly(left: _kRadioLeftMargin),
onTap: () => onChange2?.call(value),
);
}
class WaylandCard extends StatefulWidget {
const WaylandCard({Key? key}) : super(key: key);
@override
State<WaylandCard> createState() => _WaylandCardState();
}
class _WaylandCardState extends State<WaylandCard> {
final restoreTokenKey = 'wayland-restore-token';
@override
Widget build(BuildContext context) {
return futureBuilder(
future: bind.mainHandleWaylandScreencastRestoreToken(
key: restoreTokenKey, value: "get"),
hasData: (restoreToken) {
final children = [
if (restoreToken.isNotEmpty)
_buildClearScreenSelection(context, restoreToken),
];
return Offstage(
offstage: children.isEmpty,
child: _Card(title: 'Wayland', children: children),
);
},
);
}
Widget _buildClearScreenSelection(BuildContext context, String restoreToken) {
onConfirm() async {
final msg = await bind.mainHandleWaylandScreencastRestoreToken(
key: restoreTokenKey, value: "clear");
gFFI.dialogManager.dismissAll();
if (msg.isNotEmpty) {
msgBox(gFFI.sessionId, 'custom-nocancel', 'Error', msg, '',
gFFI.dialogManager);
} else {
setState(() {});
}
}
showConfirmMsgBox() => msgBoxCommon(
gFFI.dialogManager,
'Confirmation',
Text(
translate('confirm_clear_Wayland_screen_selection_tip'),
),
[
dialogButton('OK', onPressed: onConfirm),
dialogButton('Cancel',
onPressed: () => gFFI.dialogManager.dismissAll())
]);
return _Button(
'Clear Wayland screen selection',
showConfirmMsgBox,
tip: 'clear_Wayland_screen_selection_tip',
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
Theme.of(context).colorScheme.error.withOpacity(0.75)),
),
);
}
}
// ignore: non_constant_identifier_names
Widget _Button(String label, Function() onPressed,
{bool enabled = true, String? tip, ButtonStyle? style}) {
var button = ElevatedButton(
onPressed: enabled ? onPressed : null,
child: Text(
translate(label),
).marginSymmetric(horizontal: 15),
style: style,
);
StatefulWidget child;
if (tip == null) {
child = button;
} else {
child = Tooltip(message: translate(tip), child: button);
}
return Row(children: [
child,
]).marginOnly(left: _kContentHMargin);
}
// ignore: non_constant_identifier_names
Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) {
return Row(
children: [
ElevatedButton(
onPressed: enabled ? onPressed : null,
child: Text(
translate(label),
).marginSymmetric(horizontal: 15),
),
],
).marginOnly(left: _kContentHSubMargin);
}
// ignore: non_constant_identifier_names
2023-02-12 19:59:51 +08:00
Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
{bool enabled = true}) {
return Row(
children: [
2023-02-12 19:59:51 +08:00
Text(
'${translate(label)}: ',
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
style: TextStyle(color: disabledTextColor(context, enabled)),
2023-02-12 19:59:51 +08:00
),
SizedBox(
width: 10,
),
child,
],
).marginOnly(left: _kContentHSubMargin);
}
Widget _lock(
bool locked,
String label,
Function() onUnlock,
) {
return Offstage(
offstage: !locked,
child: Row(
children: [
Flexible(
child: SizedBox(
width: _kCardFixedWidth,
child: Card(
child: ElevatedButton(
child: SizedBox(
height: 25,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.security_sharp,
size: 20,
),
Text(translate(label)).marginOnly(left: 5),
]).marginSymmetric(vertical: 2)),
onPressed: () async {
bool checked = await callMainCheckSuperUserPermission();
if (checked) {
onUnlock();
}
},
).marginSymmetric(horizontal: 2, vertical: 4),
).marginOnly(left: _kCardLeftMargin),
).marginOnly(top: 10),
),
],
));
}
_LabeledTextField(
BuildContext context,
String label,
TextEditingController controller,
String errorText,
bool enabled,
bool secure) {
return Row(
children: [
2023-02-20 01:00:58 +08:00
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate(label)}:',
textAlign: TextAlign.right,
style: TextStyle(
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
fontSize: 16, color: disabledTextColor(context, enabled)),
2023-02-20 01:00:58 +08:00
).marginOnly(right: 10)),
Expanded(
child: TextField(
controller: controller,
enabled: enabled,
obscureText: secure,
decoration: InputDecoration(
errorText: errorText.isNotEmpty ? errorText : null),
style: TextStyle(
refactor windows specific session (#7170) 1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same . 2. Always show physical console session on the top 3. Show running session and distinguish sessions with the same name 4. Not sub service until correct session id is ensured 5. Fix switch sides not work for multisession session 6. Remove all session string join/split except get_available_sessions in windows.rs 7. Fix prelogin, when share rdp is enabled and there is a rdp session, the console is in login screen, get_active_username will be the rdp's username and prelogin will be false, cm can't be created an that causes disconnection in a loop 8. Rename all user session to windows session Known issue: 1. Use current process session id for `run_as_user`, sahil says it can be wrong but I didn't reproduce. 2. Have not change tray process to current session 3. File transfer doesn't update home directory when session changed 4. When it's in login screen, remote file directory is empty, because cm have not start up Signed-off-by: 21pages <pages21@163.com>
2024-02-18 22:08:25 +08:00
color: disabledTextColor(context, enabled),
)),
),
],
2023-02-20 01:00:58 +08:00
).marginOnly(bottom: 8);
}
class _CountDownButton extends StatefulWidget {
_CountDownButton({
Key? key,
required this.text,
required this.second,
required this.onPressed,
}) : super(key: key);
final String text;
final VoidCallback? onPressed;
final int second;
@override
State<_CountDownButton> createState() => _CountDownButtonState();
}
class _CountDownButtonState extends State<_CountDownButton> {
bool _isButtonDisabled = false;
late int _countdownSeconds = widget.second;
Timer? _timer;
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startCountdownTimer() {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_countdownSeconds <= 0) {
setState(() {
_isButtonDisabled = false;
});
timer.cancel();
} else {
setState(() {
_countdownSeconds--;
});
}
});
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _isButtonDisabled
? null
: () {
widget.onPressed?.call();
setState(() {
_isButtonDisabled = true;
_countdownSeconds = widget.second;
});
_startCountdownTimer();
},
child: Text(
_isButtonDisabled ? '$_countdownSeconds s' : translate(widget.text),
),
);
}
}
//#endregion
//#region dialogs
void changeSocks5Proxy() async {
var socks = await bind.mainGetSocks();
String proxy = '';
String proxyMsg = '';
String username = '';
String password = '';
if (socks.length == 3) {
proxy = socks[0];
username = socks[1];
password = socks[2];
}
var proxyController = TextEditingController(text: proxy);
var userController = TextEditingController(text: username);
var pwdController = TextEditingController(text: password);
2023-02-16 23:23:46 +08:00
RxBool obscure = true.obs;
var isInProgress = false;
2023-05-08 12:34:19 +08:00
gFFI.dialogManager.show((setState, close, context) {
submit() async {
setState(() {
proxyMsg = '';
isInProgress = true;
});
cancel() {
setState(() {
isInProgress = false;
});
}
proxy = proxyController.text.trim();
username = userController.text.trim();
password = pwdController.text.trim();
if (proxy.isNotEmpty) {
String domainPort = proxy;
if (domainPort.contains('://')) {
domainPort = domainPort.split('://')[1];
}
proxyMsg = translate(await bind.mainTestIfValidServer(
server: domainPort, testWithProxy: false));
if (proxyMsg.isEmpty) {
// ignore
} else {
cancel();
return;
}
}
await bind.mainSetSocks(
proxy: proxy, username: username, password: password);
close();
}
return CustomAlertDialog(
title: Text(translate('Socks5/Http(s) Proxy')),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Align(
alignment: Alignment.centerRight,
child: Row(
children: [
Text(
translate('Server'),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
message: translate("default_proxy_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
],
)).marginOnly(right: 10),
),
Expanded(
child: TextField(
decoration: InputDecoration(
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
),
controller: proxyController,
2023-02-10 16:31:43 +08:00
autofocus: true,
),
),
],
2023-02-15 20:43:38 +08:00
).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
2023-02-15 20:43:38 +08:00
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Username")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
Expanded(
child: TextField(
controller: userController,
),
),
],
2023-02-15 20:43:38 +08:00
).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
2023-02-15 20:43:38 +08:00
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Password")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
Expanded(
2023-02-16 23:23:46 +08:00
child: Obx(() => TextField(
obscureText: obscure.value,
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () => obscure.value = !obscure.value,
icon: Icon(obscure.value
? Icons.visibility_off
: Icons.visibility))),
controller: pwdController,
)),
),
],
),
// NOT use Offstage to wrap LinearProgressIndicator
if (isInProgress)
const LinearProgressIndicator().marginOnly(top: 8),
],
),
),
actions: [
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});
}
//#endregion