mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-12-15 20:19:05 +08:00
72ec86b58d
* refact: texture render as an option Signed-off-by: fufesou <linlong1266@gmail.com> * refact: texture render, translation Signed-off-by: fufesou <linlong1266@gmail.com> * refact: texture render as option Signed-off-by: fufesou <linlong1266@gmail.com> * Update ui_interface.rs --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2339 lines
74 KiB
Dart
2339 lines
74 KiB
Dart
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';
|
|
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
|
|
|
import '../../common/widgets/dialog.dart';
|
|
import '../../common/widgets/login.dart';
|
|
|
|
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(),
|
|
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(
|
|
backgroundColor: Theme.of(context).colorScheme.background,
|
|
body: Row(
|
|
children: <Widget>[
|
|
SizedBox(
|
|
width: _kTabWidth,
|
|
child: Column(
|
|
children: [
|
|
_header(),
|
|
Flexible(child: _listView(tabs: _settingTabs())),
|
|
],
|
|
),
|
|
),
|
|
const VerticalDivider(width: 1),
|
|
Expanded(
|
|
child: Container(
|
|
color: Theme.of(context).scaffoldBackgroundColor,
|
|
child: DesktopScrollWrapper(
|
|
scrollController: controller,
|
|
child: PageView(
|
|
controller: controller,
|
|
physics: NeverScrollableScrollPhysics(),
|
|
children: _children(),
|
|
)),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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}) {
|
|
final scrollController = ScrollController();
|
|
return DesktopScrollWrapper(
|
|
scrollController: scrollController,
|
|
child: ListView(
|
|
physics: DraggableNeverScrollableScrollPhysics(),
|
|
controller: scrollController,
|
|
children: tabs.map((tab) => _listItem(tab: tab)).toList(),
|
|
));
|
|
}
|
|
|
|
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> {
|
|
final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service');
|
|
RxBool serviceBtnEnabled = true.obs;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scrollController = ScrollController();
|
|
return DesktopScrollWrapper(
|
|
scrollController: scrollController,
|
|
child: ListView(
|
|
physics: DraggableNeverScrollableScrollPhysics(),
|
|
controller: scrollController,
|
|
children: [
|
|
service(),
|
|
theme(),
|
|
hwcodec(),
|
|
audio(context),
|
|
record(context),
|
|
WaylandCard(),
|
|
_Card(title: 'Language', children: [language()]),
|
|
other()
|
|
],
|
|
).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),
|
|
]);
|
|
}
|
|
|
|
Widget service() {
|
|
if (bind.isOutgoingOnly()) {
|
|
return const Offstage();
|
|
}
|
|
|
|
return _Card(title: 'Service', children: [
|
|
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
|
|
() async {
|
|
serviceBtnEnabled.value = false;
|
|
await start_service(serviceStop.value);
|
|
// enable the button after 1 second
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
serviceBtnEnabled.value = true;
|
|
});
|
|
}();
|
|
}, enabled: serviceBtnEnabled.value))
|
|
]);
|
|
}
|
|
|
|
Widget other() {
|
|
final children = <Widget>[
|
|
if (!bind.isIncomingOnly())
|
|
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
|
kOptionEnableConfirmClosingTabs,
|
|
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
|
|
if (isLinux)
|
|
Tooltip(
|
|
message: translate('software_render_tip'),
|
|
child: _OptionCheckBox(
|
|
context,
|
|
"Always use software rendering",
|
|
kOptionAllowAlwaysSoftwareRender,
|
|
),
|
|
),
|
|
Tooltip(
|
|
message: translate('texture_render_tip'),
|
|
child: _OptionCheckBox(
|
|
context,
|
|
"Use texture rendering",
|
|
kOptionTextureRender,
|
|
optGetter: bind.mainGetUseTextureRender,
|
|
optSetter: (k, v) async =>
|
|
await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
|
|
),
|
|
),
|
|
if (!bind.isCustomClient())
|
|
_OptionCheckBox(
|
|
context,
|
|
'Check for software update on startup',
|
|
kOptionEnableCheckUpdate,
|
|
isServer: false,
|
|
)
|
|
],
|
|
];
|
|
if (bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
|
|
children.add(_OptionCheckBox(
|
|
context, 'Allow linux headless', kOptionAllowLinuxHeadless));
|
|
}
|
|
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) {
|
|
bool value = mainGetBoolOptionSync(kOptionAllowRemoveWallpaper);
|
|
return Row(
|
|
children: [
|
|
Flexible(
|
|
child: _OptionCheckBox(
|
|
context,
|
|
'Remove wallpaper during incoming sessions',
|
|
kOptionAllowRemoveWallpaper,
|
|
update: (bool v) {
|
|
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();
|
|
return Offstage(
|
|
offstage: !(hwcodec || vram),
|
|
child: _Card(title: 'Hardware Codec', children: [
|
|
_OptionCheckBox(
|
|
context,
|
|
'Enable hardware codec',
|
|
kOptionEnableHwcodec,
|
|
update: (bool v) {
|
|
if (v) {
|
|
bind.mainCheckHwcodec();
|
|
}
|
|
},
|
|
)
|
|
]),
|
|
);
|
|
}
|
|
|
|
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);
|
|
values.insert(0, translate('Default'));
|
|
String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang);
|
|
if (!keys.contains(currentKey)) {
|
|
currentKey = defaultOptionLang;
|
|
}
|
|
final isOptFixed = isOptionFixed(kCommConfKeyLang);
|
|
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();
|
|
final scrollController = ScrollController();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return DesktopScrollWrapper(
|
|
scrollController: scrollController,
|
|
child: SingleChildScrollView(
|
|
physics: DraggableNeverScrollableScrollPhysics(),
|
|
controller: scrollController,
|
|
child: Column(
|
|
children: [
|
|
_lock(locked, 'Unlock Security Settings', () {
|
|
locked = false;
|
|
setState(() => {});
|
|
}),
|
|
AbsorbPointer(
|
|
absorbing: locked,
|
|
child: Column(children: [
|
|
permissions(context),
|
|
password(context),
|
|
_Card(title: '2FA', children: [tfa()]),
|
|
_Card(title: 'ID', children: [changeId()]),
|
|
more(context),
|
|
]),
|
|
),
|
|
],
|
|
)).marginOnly(bottom: _kListViewBottomMargin));
|
|
}
|
|
|
|
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:
|
|
TextStyle(color: disabledTextColor(context, enabled)),
|
|
))
|
|
],
|
|
)),
|
|
),
|
|
onTap: () {
|
|
onChanged(!has2fa.value);
|
|
},
|
|
).marginOnly(left: _kCheckBoxLeftMargin);
|
|
}
|
|
|
|
return tmpWrapper();
|
|
}
|
|
|
|
Widget changeId() {
|
|
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: [
|
|
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(
|
|
color: disabledTextColor(
|
|
context, onChanged != null)),
|
|
),
|
|
],
|
|
).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: [
|
|
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(
|
|
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],
|
|
]);
|
|
})));
|
|
}
|
|
|
|
Widget more(BuildContext context) {
|
|
bool enabled = !locked;
|
|
return _Card(title: 'Security', children: [
|
|
shareRdp(context, enabled),
|
|
_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:
|
|
TextStyle(color: disabledTextColor(context, enabled))),
|
|
)
|
|
],
|
|
).marginOnly(left: _kCheckBoxLeftMargin),
|
|
onTap: enabled ? () => onChanged(!value) : null),
|
|
);
|
|
}
|
|
|
|
List<Widget> directIp(BuildContext context) {
|
|
TextEditingController controller = TextEditingController();
|
|
update(bool v) => 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:
|
|
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(
|
|
color: disabledTextColor(
|
|
context, enabled && enableHideCm)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
));
|
|
}));
|
|
}
|
|
|
|
List<Widget> autoDisconnect(BuildContext context) {
|
|
TextEditingController controller = TextEditingController();
|
|
update(bool v) => 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;
|
|
final scrollController = ScrollController();
|
|
return DesktopScrollWrapper(
|
|
scrollController: scrollController,
|
|
child: ListView(
|
|
controller: scrollController,
|
|
physics: DraggableNeverScrollableScrollPhysics(),
|
|
children: [
|
|
_lock(locked, 'Unlock Network Settings', () {
|
|
locked = false;
|
|
setState(() => {});
|
|
}),
|
|
AbsorbPointer(
|
|
absorbing: locked,
|
|
child: Column(children: [
|
|
server(enabled),
|
|
_Card(title: 'Proxy', children: [
|
|
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
|
|
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,
|
|
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',
|
|
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) {
|
|
final scrollController = ScrollController();
|
|
return DesktopScrollWrapper(
|
|
scrollController: scrollController,
|
|
child: ListView(
|
|
physics: DraggableNeverScrollableScrollPhysics(),
|
|
controller: scrollController,
|
|
children: [
|
|
_Card(title: 'Account', children: [accountAction(), useInfo()]),
|
|
],
|
|
).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();
|
|
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();
|
|
final buildDate = data['buildDate'].toString();
|
|
final fingerprint = data['fingerprint'].toString();
|
|
const linkStyle = TextStyle(decoration: TextDecoration.underline);
|
|
final scrollController = ScrollController();
|
|
return DesktopScrollWrapper(
|
|
scrollController: scrollController,
|
|
child: SingleChildScrollView(
|
|
controller: scrollController,
|
|
physics: DraggableNeverScrollableScrollPhysics(),
|
|
child: _Card(title: '${translate('About')} RustDesk', children: [
|
|
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)),
|
|
InkWell(
|
|
onTap: () {
|
|
launchUrlString('https://rustdesk.com/privacy.html');
|
|
},
|
|
child: Text(
|
|
translate('Privacy Statement'),
|
|
style: linkStyle,
|
|
).marginSymmetric(vertical: 4.0)),
|
|
InkWell(
|
|
onTap: () {
|
|
launchUrlString('https://rustdesk.com');
|
|
},
|
|
child: Text(
|
|
translate('Website'),
|
|
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(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
|
|
style: const TextStyle(color: Colors.white),
|
|
),
|
|
Text(
|
|
translate('Slogan_tip'),
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
],
|
|
)),
|
|
).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}) {
|
|
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),
|
|
...children
|
|
.map((e) => e.marginOnly(top: 4, right: _kContentHMargin)),
|
|
],
|
|
).marginOnly(bottom: 10),
|
|
).marginOnly(left: _kCardLeftMargin, top: 15),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ignore: non_constant_identifier_names
|
|
Widget _OptionCheckBox(
|
|
BuildContext context,
|
|
String label,
|
|
String key, {
|
|
Function(bool)? update,
|
|
bool reverse = false,
|
|
bool enabled = true,
|
|
Icon? checkedIcon,
|
|
bool? fakeValue,
|
|
bool isServer = true,
|
|
bool Function()? optGetter,
|
|
Future<void> Function(String, bool)? optSetter,
|
|
}) {
|
|
getOpt() => optGetter != null
|
|
? optGetter()
|
|
: (isServer
|
|
? mainGetBoolOptionSync(key)
|
|
: mainGetLocalBoolOptionSync(key));
|
|
bool value = getOpt();
|
|
final isOptFixed = isOptionFixed(key);
|
|
if (reverse) value = !value;
|
|
var ref = value.obs;
|
|
onChanged(option) async {
|
|
if (option != null) {
|
|
if (reverse) option = !option;
|
|
final setter =
|
|
optSetter ?? (isServer ? mainSetBoolOption : mainSetLocalBoolOption);
|
|
await setter(key, option);
|
|
final readOption = getOpt();
|
|
if (reverse) {
|
|
ref.value = !readOption;
|
|
} else {
|
|
ref.value = readOption;
|
|
}
|
|
update?.call(readOption);
|
|
}
|
|
}
|
|
|
|
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),
|
|
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
|
|
Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
|
|
{bool enabled = true}) {
|
|
return Row(
|
|
children: [
|
|
Text(
|
|
'${translate(label)}: ',
|
|
style: TextStyle(color: disabledTextColor(context, enabled)),
|
|
),
|
|
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: [
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 140),
|
|
child: Text(
|
|
'${translate(label)}:',
|
|
textAlign: TextAlign.right,
|
|
style: TextStyle(
|
|
fontSize: 16, color: disabledTextColor(context, enabled)),
|
|
).marginOnly(right: 10)),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: controller,
|
|
enabled: enabled,
|
|
obscureText: secure,
|
|
decoration: InputDecoration(
|
|
errorText: errorText.isNotEmpty ? errorText : null),
|
|
style: TextStyle(
|
|
color: disabledTextColor(context, enabled),
|
|
)),
|
|
),
|
|
],
|
|
).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);
|
|
RxBool obscure = true.obs;
|
|
|
|
// proxy settings
|
|
// The following option is a not real key, it is just used for custom client advanced settings.
|
|
const String optionProxyUrl = "proxy-url";
|
|
final isOptFixed = isOptionFixed(optionProxyUrl);
|
|
|
|
var isInProgress = false;
|
|
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,
|
|
autofocus: true,
|
|
enabled: !isOptFixed,
|
|
),
|
|
),
|
|
],
|
|
).marginOnly(bottom: 8),
|
|
Row(
|
|
children: [
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 140),
|
|
child: Text(
|
|
'${translate("Username")}:',
|
|
textAlign: TextAlign.right,
|
|
).marginOnly(right: 10)),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: userController,
|
|
enabled: !isOptFixed,
|
|
),
|
|
),
|
|
],
|
|
).marginOnly(bottom: 8),
|
|
Row(
|
|
children: [
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 140),
|
|
child: Text(
|
|
'${translate("Password")}:',
|
|
textAlign: TextAlign.right,
|
|
).marginOnly(right: 10)),
|
|
Expanded(
|
|
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,
|
|
enabled: !isOptFixed,
|
|
)),
|
|
),
|
|
],
|
|
),
|
|
// NOT use Offstage to wrap LinearProgressIndicator
|
|
if (isInProgress)
|
|
const LinearProgressIndicator().marginOnly(top: 8),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
dialogButton('Cancel', onPressed: close, isOutline: true),
|
|
if (!isOptFixed) dialogButton('OK', onPressed: submit),
|
|
],
|
|
onSubmit: submit,
|
|
onCancel: close,
|
|
);
|
|
});
|
|
}
|
|
|
|
//#endregion
|