2022-08-13 12:43:35 +08:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:io' show Platform;
|
|
|
|
import 'dart:ui';
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_hbb/common.dart';
|
|
|
|
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
|
|
|
|
import 'package:flutter_hbb/models/platform_model.dart';
|
|
|
|
import 'package:flutter_hbb/models/server_model.dart';
|
|
|
|
import 'package:get/get.dart';
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
|
|
|
|
const double _kCardFixedWidth = 600;
|
|
|
|
const double _kCardLeftPadding = 20;
|
|
|
|
const double _kContentLeftPadding = 30;
|
|
|
|
const double _kListViewBottomPadding = 30;
|
2022-08-11 16:03:04 +08:00
|
|
|
|
|
|
|
class DesktopSettingPage extends StatefulWidget {
|
|
|
|
DesktopSettingPage({Key? key}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<DesktopSettingPage> createState() => _DesktopSettingPageState();
|
|
|
|
}
|
|
|
|
|
2022-08-13 12:43:35 +08:00
|
|
|
class _DesktopSettingPageState extends State<DesktopSettingPage>
|
|
|
|
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
|
|
|
final List<NavigationRailDestination> _destinations =
|
|
|
|
<NavigationRailDestination>[
|
|
|
|
_destination('Display', Icons.palette_outlined, Icons.palette),
|
|
|
|
_destination(
|
|
|
|
'Security', Icons.health_and_safety_outlined, Icons.health_and_safety),
|
|
|
|
_destination(
|
|
|
|
'Connection', Icons.settings_remote_outlined, Icons.settings_remote),
|
|
|
|
_destination('Video', Icons.videocam_outlined, Icons.videocam),
|
|
|
|
_destination('Audio', Icons.volume_up_outlined, Icons.volume_up),
|
|
|
|
];
|
|
|
|
|
|
|
|
late TabController controller;
|
|
|
|
int _selectedIndex = 0;
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool get wantKeepAlive => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
controller = TabController(length: _destinations.length, vsync: this);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
super.build(context);
|
|
|
|
return Scaffold(
|
|
|
|
body: Row(
|
|
|
|
children: <Widget>[
|
|
|
|
NavigationRail(
|
|
|
|
selectedIndex: _selectedIndex,
|
|
|
|
onDestinationSelected: (int index) {
|
|
|
|
setState(() {
|
|
|
|
_selectedIndex = index;
|
|
|
|
});
|
|
|
|
controller.animateTo(index);
|
|
|
|
},
|
|
|
|
labelType: NavigationRailLabelType.all,
|
|
|
|
destinations: _destinations,
|
|
|
|
),
|
|
|
|
const VerticalDivider(thickness: 1, width: 1),
|
|
|
|
Expanded(
|
|
|
|
child: TabBarView(
|
|
|
|
controller: controller,
|
|
|
|
children: [
|
|
|
|
_Display(),
|
|
|
|
_Safety(),
|
|
|
|
_Connection(),
|
|
|
|
_Video(),
|
|
|
|
_Audio(),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
)
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static NavigationRailDestination _destination(
|
|
|
|
String label, IconData selected, IconData unSelected) {
|
|
|
|
return NavigationRailDestination(
|
|
|
|
icon: Icon(unSelected),
|
|
|
|
selectedIcon: Icon(selected),
|
|
|
|
label: Text(translate(label)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//#region pages
|
|
|
|
|
|
|
|
class _Display extends StatefulWidget {
|
|
|
|
_Display({Key? key}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<_Display> createState() => _DisplayState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin {
|
|
|
|
@override
|
|
|
|
bool get wantKeepAlive => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
super.build(context);
|
|
|
|
return ListView(
|
|
|
|
children: [
|
|
|
|
_Card(title: translate('Display'), children: [language(), theme()]),
|
|
|
|
],
|
|
|
|
).paddingOnly(bottom: _kListViewBottomPadding);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget language() {
|
|
|
|
return _futureBuilder(future: () async {
|
|
|
|
String langs = await bind.mainGetLangs();
|
|
|
|
String lang = await bind.mainGetLocalOption(key: "lang");
|
|
|
|
return {"langs": langs, "lang": lang};
|
|
|
|
}(), 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, "default");
|
|
|
|
values.insert(0, "Default");
|
|
|
|
String currentKey = data["lang"]!;
|
|
|
|
if (!keys.contains(currentKey)) {
|
|
|
|
currentKey = "default";
|
|
|
|
}
|
|
|
|
return _row(
|
|
|
|
'Language',
|
|
|
|
_ComboBox(
|
|
|
|
keys: keys,
|
|
|
|
values: values,
|
|
|
|
initialKey: currentKey,
|
|
|
|
onChanged: (key) async {
|
|
|
|
await bind.mainSetLocalOption(key: "lang", value: key);
|
|
|
|
Get.forceAppUpdate();
|
|
|
|
},
|
|
|
|
));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget theme() {
|
|
|
|
return _row(
|
|
|
|
'Dark Theme',
|
|
|
|
Switch(
|
|
|
|
value: isDarkTheme(),
|
|
|
|
onChanged: ((dark) async {
|
|
|
|
Get.changeTheme(dark ? MyTheme.darkTheme : MyTheme.lightTheme);
|
|
|
|
Get.find<SharedPreferences>()
|
|
|
|
.setString("darkTheme", dark ? "Y" : "");
|
|
|
|
Get.forceAppUpdate();
|
|
|
|
})));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
super.build(context);
|
|
|
|
return ListView(
|
|
|
|
children: [
|
|
|
|
permissions(),
|
|
|
|
password(),
|
|
|
|
whitelist(),
|
|
|
|
],
|
|
|
|
).paddingOnly(bottom: _kListViewBottomPadding);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget permissions() {
|
|
|
|
return _Card(title: 'Permissions', children: [
|
|
|
|
_option_check('Enable Keyboard/Mouse', 'enable-keyboard'),
|
|
|
|
_option_check('Enable Clipboard', 'enable-clipboard'),
|
|
|
|
_option_check('Enable File Transfer', 'enable-file-transfer'),
|
|
|
|
_option_check('Enable Audio', 'enable-audio'),
|
|
|
|
_option_check('Enable Remote Restart', 'enable-remote-restart'),
|
|
|
|
_option_check('Enable remote configuration modification',
|
|
|
|
'allow-remote-config-modification'),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget password() {
|
|
|
|
return ChangeNotifierProvider.value(
|
|
|
|
value: gFFI.serverModel,
|
|
|
|
child: Consumer<ServerModel>(
|
|
|
|
builder: ((context, model, child) =>
|
|
|
|
_Card(title: 'Password', children: [
|
|
|
|
_row(
|
|
|
|
'Verification Method',
|
|
|
|
_ComboBox(
|
|
|
|
keys: [
|
|
|
|
kUseTemporaryPassword,
|
|
|
|
kUsePermanentPassword,
|
|
|
|
kUseBothPasswords,
|
|
|
|
],
|
|
|
|
values: [
|
|
|
|
translate("Use temporary password"),
|
|
|
|
translate("Use permanent password"),
|
|
|
|
translate("Use both passwords"),
|
|
|
|
],
|
|
|
|
initialKey: model.verificationMethod,
|
|
|
|
onChanged: (key) => model.verificationMethod = key)),
|
|
|
|
_row(
|
|
|
|
'Temporary Password Length',
|
|
|
|
_ComboBox(
|
|
|
|
keys: ['6', '8', '10'],
|
|
|
|
values: ['6', '8', '10'],
|
|
|
|
initialKey: model.temporaryPasswordLength,
|
|
|
|
onChanged: (key) => model.temporaryPasswordLength = key,
|
|
|
|
enabled:
|
|
|
|
model.verificationMethod != kUsePermanentPassword,
|
|
|
|
)),
|
|
|
|
_button(
|
|
|
|
'permanent_password_tip',
|
|
|
|
'Set permanent password',
|
|
|
|
setPasswordDialog,
|
|
|
|
model.verificationMethod != kUseTemporaryPassword)
|
|
|
|
]))));
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget whitelist() {
|
|
|
|
return _Card(title: 'IP Whitelisting', children: [
|
|
|
|
_button('whitelist_tip', 'IP Whitelisting', changeWhiteList)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _Connection extends StatefulWidget {
|
|
|
|
const _Connection({Key? key}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<_Connection> createState() => _ConnectionState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ConnectionState extends State<_Connection>
|
|
|
|
with AutomaticKeepAliveClientMixin {
|
|
|
|
final TextEditingController controller = TextEditingController();
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool get wantKeepAlive => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
super.build(context);
|
|
|
|
return ListView(
|
|
|
|
children: [
|
|
|
|
_Card(title: 'Server', children: [
|
|
|
|
_button('self-hosting_tip', 'ID/Relay Server', changeServer),
|
|
|
|
]),
|
|
|
|
_Card(title: 'Service', children: [
|
|
|
|
_option_check('Enable Service', 'stop-service', reverse: true),
|
|
|
|
// TODO: Not implemented
|
|
|
|
// _option_check('Always connected via relay', 'allow-always-relay'),
|
|
|
|
// _option_check('Start ID/relay service', 'stop-rendezvous-service',
|
|
|
|
// reverse: true),
|
|
|
|
]),
|
|
|
|
_Card(title: 'TCP Tunneling', children: [
|
|
|
|
_option_check('Enable TCP Tunneling', 'enable-tunnel'),
|
|
|
|
]),
|
|
|
|
direct_ip(),
|
|
|
|
_Card(title: 'Proxy', children: [
|
|
|
|
_button('socks5_proxy_tip', 'Socks5 Proxy', changeSocks5Proxy),
|
|
|
|
]),
|
|
|
|
],
|
|
|
|
).paddingOnly(bottom: _kListViewBottomPadding);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget direct_ip() {
|
|
|
|
var update = () => setState(() {});
|
|
|
|
return _Card(title: 'Direct IP Access', children: [
|
|
|
|
_option_check('Enable Direct IP Access', 'direct-server', update: update),
|
|
|
|
_row(
|
|
|
|
'Port',
|
|
|
|
_futureBuilder(
|
|
|
|
future: () async {
|
|
|
|
String enabled = await bind.mainGetOption(key: 'direct-server');
|
|
|
|
String port = await bind.mainGetOption(key: 'direct-access-port');
|
|
|
|
return {'enabled': enabled, 'port': port};
|
|
|
|
}(),
|
|
|
|
hasData: (data) {
|
|
|
|
bool enabled =
|
|
|
|
option2bool('direct-server', data['enabled'].toString());
|
|
|
|
String port = data['port'].toString();
|
|
|
|
int? iport = int.tryParse(port);
|
|
|
|
if (iport == null || iport < 1 || iport > 65535) {
|
|
|
|
port = '';
|
|
|
|
}
|
|
|
|
controller.text = port;
|
|
|
|
return TextField(
|
|
|
|
controller: controller,
|
|
|
|
enabled: enabled,
|
|
|
|
onChanged: (value) async {
|
|
|
|
await bind.mainSetOption(
|
|
|
|
key: 'direct-access-port', value: controller.text);
|
|
|
|
},
|
|
|
|
decoration: InputDecoration(
|
|
|
|
hintText: '21118',
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _Video extends StatefulWidget {
|
|
|
|
const _Video({Key? key}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<_Video> createState() => _VideoState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _VideoState extends State<_Video> with AutomaticKeepAliveClientMixin {
|
|
|
|
@override
|
|
|
|
bool get wantKeepAlive => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
super.build(context);
|
|
|
|
return ListView(
|
|
|
|
children: [
|
|
|
|
_Card(title: 'Adaptive Bitrate', children: [
|
|
|
|
_option_check('Adaptive Bitrate', 'enable-abr'),
|
|
|
|
]),
|
|
|
|
],
|
|
|
|
).paddingOnly(bottom: _kListViewBottomPadding);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _Audio extends StatefulWidget {
|
|
|
|
const _Audio({Key? key}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<_Audio> createState() => _AudioState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin {
|
|
|
|
@override
|
|
|
|
bool get wantKeepAlive => true;
|
|
|
|
|
2022-08-11 16:03:04 +08:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2022-08-13 12:43:35 +08:00
|
|
|
super.build(context);
|
|
|
|
var update = () => setState(() {});
|
|
|
|
return ListView(children: [
|
|
|
|
_Card(
|
|
|
|
title: 'Audio Input',
|
|
|
|
children: [
|
|
|
|
_option_check('Mute', 'enable-audio', reverse: true, update: update),
|
|
|
|
_row(
|
|
|
|
'Audio device',
|
|
|
|
_futureBuilder(future: () async {
|
|
|
|
List<String> all = await bind.mainGetSoundInputs();
|
|
|
|
String current = await bind.mainGetOption(key: 'audio-input');
|
|
|
|
String enabled = await bind.mainGetOption(key: 'enable-audio');
|
|
|
|
return {'all': all, 'current': current, 'enabled': enabled};
|
|
|
|
}(), hasData: (data) {
|
|
|
|
List<String> keys = (data['all'] as List<String>).toList();
|
|
|
|
List<String> values = keys.toList();
|
|
|
|
if (Platform.isWindows) {
|
|
|
|
keys.insert(0, '');
|
|
|
|
values.insert(0, 'System Sound');
|
|
|
|
} else {
|
|
|
|
keys.insert(0, ''); // TODO
|
|
|
|
values.insert(0, 'None');
|
|
|
|
}
|
|
|
|
String initialKey = data['current'];
|
|
|
|
if (!keys.contains(initialKey)) {
|
|
|
|
initialKey = '';
|
|
|
|
}
|
|
|
|
return _ComboBox(
|
|
|
|
keys: keys,
|
|
|
|
values: values,
|
|
|
|
initialKey: initialKey,
|
|
|
|
onChanged: (key) {
|
|
|
|
bind.mainSetOption(key: 'audio-input', value: key);
|
|
|
|
},
|
|
|
|
enabled:
|
|
|
|
option2bool('enable-audio', data['enabled'].toString()),
|
|
|
|
);
|
|
|
|
})),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
]).paddingOnly(bottom: _kListViewBottomPadding);
|
2022-08-11 16:03:04 +08:00
|
|
|
}
|
|
|
|
}
|
2022-08-13 12:43:35 +08:00
|
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
//#region components
|
|
|
|
|
|
|
|
Widget _Card({required String title, required List<Widget> children}) {
|
|
|
|
return Row(
|
|
|
|
children: [
|
|
|
|
Container(
|
|
|
|
width: _kCardFixedWidth,
|
|
|
|
child: Card(
|
|
|
|
child: Column(
|
|
|
|
children: [
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Text(
|
|
|
|
translate(title),
|
|
|
|
textAlign: TextAlign.start,
|
|
|
|
style: TextStyle(
|
|
|
|
fontSize: 25,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Spacer(),
|
|
|
|
],
|
|
|
|
).paddingOnly(left: _kContentLeftPadding, top: 10, bottom: 20),
|
|
|
|
...children.map((e) => e.paddingOnly(top: 2)),
|
|
|
|
],
|
|
|
|
).paddingOnly(bottom: 10),
|
|
|
|
).paddingOnly(left: _kCardLeftPadding, top: 20),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _option_switch(String label, String key,
|
|
|
|
{Function()? update = null, bool reverse = false}) {
|
|
|
|
return _row(
|
|
|
|
label,
|
|
|
|
_futureBuilder(
|
|
|
|
future: bind.mainGetOption(key: key),
|
|
|
|
hasData: (data) {
|
|
|
|
bool value = option2bool(key, data.toString());
|
|
|
|
if (reverse) value = !value;
|
|
|
|
var ref = value.obs;
|
|
|
|
return Obx((() => Switch(
|
|
|
|
value: ref.value,
|
|
|
|
onChanged: ((option) async {
|
|
|
|
ref.value = option;
|
|
|
|
if (reverse) option = !option;
|
|
|
|
String value = bool2option(key, option);
|
|
|
|
bind.mainSetOption(key: key, value: value);
|
|
|
|
update?.call();
|
|
|
|
}))));
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _option_check(String label, String key,
|
|
|
|
{Function()? update = null, bool reverse = false}) {
|
|
|
|
return Row(children: [
|
|
|
|
_futureBuilder(
|
|
|
|
future: bind.mainGetOption(key: key),
|
|
|
|
hasData: (data) {
|
|
|
|
bool value = option2bool(key, data.toString());
|
|
|
|
if (reverse) value = !value;
|
|
|
|
var ref = value.obs;
|
|
|
|
return Obx((() => Checkbox(
|
|
|
|
value: ref.value,
|
|
|
|
onChanged: ((option) async {
|
|
|
|
if (option != null) {
|
|
|
|
ref.value = option;
|
|
|
|
if (reverse) option = !option;
|
|
|
|
String value = bool2option(key, option);
|
|
|
|
bind.mainSetOption(key: key, value: value);
|
|
|
|
update?.call();
|
|
|
|
}
|
|
|
|
}))));
|
|
|
|
}).paddingOnly(right: 10),
|
|
|
|
Text(translate(label)),
|
|
|
|
]).paddingOnly(left: _kContentLeftPadding);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _button(String tip, String label, Function() onPressed,
|
|
|
|
[bool enabled = true]) {
|
|
|
|
return _row(
|
|
|
|
translate(tip),
|
|
|
|
OutlinedButton(
|
|
|
|
onPressed: enabled ? onPressed : null,
|
|
|
|
child: Text(
|
|
|
|
translate(label),
|
|
|
|
)));
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _row(String label, Widget widget) {
|
|
|
|
return Row(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: Text(
|
|
|
|
translate(label),
|
|
|
|
)),
|
|
|
|
SizedBox(
|
|
|
|
width: 40,
|
|
|
|
),
|
|
|
|
Expanded(child: widget),
|
|
|
|
],
|
|
|
|
).paddingSymmetric(horizontal: _kContentLeftPadding);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget _futureBuilder(
|
|
|
|
{required Future? future, required Widget Function(dynamic data) hasData}) {
|
|
|
|
return FutureBuilder(
|
|
|
|
future: future,
|
|
|
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
|
|
|
if (snapshot.hasData) {
|
|
|
|
return hasData(snapshot.data!);
|
|
|
|
} else {
|
|
|
|
if (snapshot.hasError) {
|
|
|
|
print(snapshot.error.toString());
|
|
|
|
}
|
|
|
|
return Container();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ComboBox extends StatelessWidget {
|
|
|
|
late final List<String> keys;
|
|
|
|
late final List<String> values;
|
|
|
|
late final String initialKey;
|
|
|
|
late final Function(String key) onChanged;
|
|
|
|
late final bool enabled;
|
|
|
|
|
|
|
|
_ComboBox({
|
|
|
|
Key? key,
|
|
|
|
required this.keys,
|
|
|
|
required this.values,
|
|
|
|
required this.initialKey,
|
|
|
|
required this.onChanged,
|
|
|
|
this.enabled = true,
|
|
|
|
}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
var index = keys.indexOf(initialKey);
|
|
|
|
if (index < 0) {
|
|
|
|
assert(false);
|
|
|
|
index = 0;
|
|
|
|
}
|
|
|
|
var ref = values[index].obs;
|
|
|
|
return Container(
|
|
|
|
child: SizedBox(
|
|
|
|
child: Obx((() => DropdownButton<String>(
|
|
|
|
isExpanded: true,
|
|
|
|
value: ref.value,
|
|
|
|
elevation: 16,
|
|
|
|
underline: Container(
|
|
|
|
height: 40,
|
|
|
|
),
|
|
|
|
icon: Icon(
|
|
|
|
Icons.arrow_drop_down_sharp,
|
|
|
|
size: 35,
|
|
|
|
),
|
|
|
|
onChanged: enabled
|
|
|
|
? (String? newValue) {
|
|
|
|
if (newValue != null && newValue != ref.value) {
|
|
|
|
ref.value = newValue;
|
|
|
|
onChanged(keys[values.indexOf(newValue)]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
: null,
|
|
|
|
items: values.map<DropdownMenuItem<String>>((String value) {
|
|
|
|
return DropdownMenuItem<String>(
|
|
|
|
value: value,
|
|
|
|
child: Text(value),
|
|
|
|
);
|
|
|
|
}).toList(),
|
|
|
|
)))),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
//#region dialogs
|
|
|
|
|
|
|
|
void changeServer() async {
|
|
|
|
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
|
|
|
print("${oldOptions}");
|
|
|
|
String idServer = oldOptions['custom-rendezvous-server'] ?? "";
|
|
|
|
var idServerMsg = "";
|
|
|
|
String relayServer = oldOptions['relay-server'] ?? "";
|
|
|
|
var relayServerMsg = "";
|
|
|
|
String apiServer = oldOptions['api-server'] ?? "";
|
|
|
|
var apiServerMsg = "";
|
|
|
|
var key = oldOptions['key'] ?? "";
|
|
|
|
|
|
|
|
var isInProgress = false;
|
|
|
|
gFFI.dialogManager.show((setState, close) {
|
|
|
|
return CustomAlertDialog(
|
|
|
|
title: Text(translate("ID/Relay Server")),
|
|
|
|
content: ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(minWidth: 500),
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
SizedBox(
|
|
|
|
height: 8.0,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(minWidth: 100),
|
|
|
|
child: Text("${translate('ID Server')}:")
|
|
|
|
.marginOnly(bottom: 16.0)),
|
|
|
|
SizedBox(
|
|
|
|
width: 24.0,
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: TextField(
|
|
|
|
onChanged: (s) {
|
|
|
|
idServer = s;
|
|
|
|
},
|
|
|
|
decoration: InputDecoration(
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
|
|
|
|
controller: TextEditingController(text: idServer),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
height: 8.0,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(minWidth: 100),
|
|
|
|
child: Text("${translate('Relay Server')}:")
|
|
|
|
.marginOnly(bottom: 16.0)),
|
|
|
|
SizedBox(
|
|
|
|
width: 24.0,
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: TextField(
|
|
|
|
onChanged: (s) {
|
|
|
|
relayServer = s;
|
|
|
|
},
|
|
|
|
decoration: InputDecoration(
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
errorText:
|
|
|
|
relayServerMsg.isNotEmpty ? relayServerMsg : null),
|
|
|
|
controller: TextEditingController(text: relayServer),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
height: 8.0,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(minWidth: 100),
|
|
|
|
child: Text("${translate('API Server')}:")
|
|
|
|
.marginOnly(bottom: 16.0)),
|
|
|
|
SizedBox(
|
|
|
|
width: 24.0,
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: TextField(
|
|
|
|
onChanged: (s) {
|
|
|
|
apiServer = s;
|
|
|
|
},
|
|
|
|
decoration: InputDecoration(
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
errorText:
|
|
|
|
apiServerMsg.isNotEmpty ? apiServerMsg : null),
|
|
|
|
controller: TextEditingController(text: apiServer),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
height: 8.0,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(minWidth: 100),
|
|
|
|
child:
|
|
|
|
Text("${translate('Key')}:").marginOnly(bottom: 16.0)),
|
|
|
|
SizedBox(
|
|
|
|
width: 24.0,
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: TextField(
|
|
|
|
onChanged: (s) {
|
|
|
|
key = s;
|
|
|
|
},
|
|
|
|
decoration: InputDecoration(
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
),
|
|
|
|
controller: TextEditingController(text: key),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
height: 4.0,
|
|
|
|
),
|
|
|
|
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
actions: [
|
|
|
|
TextButton(
|
|
|
|
onPressed: () {
|
|
|
|
close();
|
|
|
|
},
|
|
|
|
child: Text(translate("Cancel"))),
|
|
|
|
TextButton(
|
|
|
|
onPressed: () async {
|
|
|
|
setState(() {
|
|
|
|
[idServerMsg, relayServerMsg, apiServerMsg].forEach((element) {
|
|
|
|
element = "";
|
|
|
|
});
|
|
|
|
isInProgress = true;
|
|
|
|
});
|
|
|
|
final cancel = () {
|
|
|
|
setState(() {
|
|
|
|
isInProgress = false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
idServer = idServer.trim();
|
|
|
|
relayServer = relayServer.trim();
|
|
|
|
apiServer = apiServer.trim();
|
|
|
|
key = key.trim();
|
|
|
|
|
|
|
|
if (idServer.isNotEmpty) {
|
|
|
|
idServerMsg = translate(
|
|
|
|
await bind.mainTestIfValidServer(server: idServer));
|
|
|
|
if (idServerMsg.isEmpty) {
|
|
|
|
oldOptions['custom-rendezvous-server'] = idServer;
|
|
|
|
} else {
|
|
|
|
cancel();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
oldOptions['custom-rendezvous-server'] = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (relayServer.isNotEmpty) {
|
|
|
|
relayServerMsg = translate(
|
|
|
|
await bind.mainTestIfValidServer(server: relayServer));
|
|
|
|
if (relayServerMsg.isEmpty) {
|
|
|
|
oldOptions['relay-server'] = relayServer;
|
|
|
|
} else {
|
|
|
|
cancel();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
oldOptions['relay-server'] = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (apiServer.isNotEmpty) {
|
|
|
|
if (apiServer.startsWith('http://') ||
|
|
|
|
apiServer.startsWith("https://")) {
|
|
|
|
oldOptions['api-server'] = apiServer;
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
apiServerMsg = translate("invalid_http");
|
|
|
|
cancel();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
oldOptions['api-server'] = "";
|
|
|
|
}
|
|
|
|
// ok
|
|
|
|
oldOptions['key'] = key;
|
|
|
|
await bind.mainSetOptions(json: jsonEncode(oldOptions));
|
|
|
|
close();
|
|
|
|
},
|
|
|
|
child: Text(translate("OK"))),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void changeWhiteList() async {
|
|
|
|
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
|
|
|
|
var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(',');
|
|
|
|
var newWhiteListField = newWhiteList.join('\n');
|
|
|
|
var msg = "";
|
|
|
|
var isInProgress = false;
|
|
|
|
gFFI.dialogManager.show((setState, close) {
|
|
|
|
return CustomAlertDialog(
|
|
|
|
title: Text(translate("IP Whitelisting")),
|
|
|
|
content: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Text(translate("whitelist_sep")),
|
|
|
|
SizedBox(
|
|
|
|
height: 8.0,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: TextField(
|
|
|
|
onChanged: (s) {
|
|
|
|
newWhiteListField = s;
|
|
|
|
},
|
|
|
|
maxLines: null,
|
|
|
|
decoration: InputDecoration(
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
errorText: msg.isEmpty ? null : translate(msg),
|
|
|
|
),
|
|
|
|
controller: TextEditingController(text: newWhiteListField),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
height: 4.0,
|
|
|
|
),
|
|
|
|
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
|
|
|
],
|
|
|
|
),
|
|
|
|
actions: [
|
|
|
|
TextButton(
|
|
|
|
onPressed: () {
|
|
|
|
close();
|
|
|
|
},
|
|
|
|
child: Text(translate("Cancel"))),
|
|
|
|
TextButton(
|
|
|
|
onPressed: () async {
|
|
|
|
setState(() {
|
|
|
|
msg = "";
|
|
|
|
isInProgress = true;
|
|
|
|
});
|
|
|
|
newWhiteListField = newWhiteListField.trim();
|
|
|
|
var newWhiteList = "";
|
|
|
|
if (newWhiteListField.isEmpty) {
|
|
|
|
// pass
|
|
|
|
} else {
|
|
|
|
final ips =
|
|
|
|
newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
|
|
|
|
// test ip
|
|
|
|
final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$");
|
|
|
|
for (final ip in ips) {
|
|
|
|
if (!ipMatch.hasMatch(ip)) {
|
|
|
|
msg = translate("Invalid IP") + " $ip";
|
|
|
|
setState(() {
|
|
|
|
isInProgress = false;
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newWhiteList = ips.join(',');
|
|
|
|
}
|
|
|
|
oldOptions['whitelist'] = newWhiteList;
|
|
|
|
await bind.mainSetOptions(json: jsonEncode(oldOptions));
|
|
|
|
close();
|
|
|
|
},
|
|
|
|
child: Text(translate("OK"))),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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 isInProgress = false;
|
|
|
|
gFFI.dialogManager.show((setState, close) {
|
|
|
|
return CustomAlertDialog(
|
|
|
|
title: Text(translate("Socks5 Proxy")),
|
|
|
|
content: ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(minWidth: 500),
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
SizedBox(
|
|
|
|
height: 8.0,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(minWidth: 100),
|
|
|
|
child: Text("${translate('Hostname')}:")
|
|
|
|
.marginOnly(bottom: 16.0)),
|
|
|
|
SizedBox(
|
|
|
|
width: 24.0,
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: TextField(
|
|
|
|
onChanged: (s) {
|
|
|
|
proxy = s;
|
|
|
|
},
|
|
|
|
decoration: InputDecoration(
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
|
|
|
|
controller: TextEditingController(text: proxy),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
height: 8.0,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(minWidth: 100),
|
|
|
|
child: Text("${translate('Username')}:")
|
|
|
|
.marginOnly(bottom: 16.0)),
|
|
|
|
SizedBox(
|
|
|
|
width: 24.0,
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: TextField(
|
|
|
|
onChanged: (s) {
|
|
|
|
username = s;
|
|
|
|
},
|
|
|
|
decoration: InputDecoration(
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
),
|
|
|
|
controller: TextEditingController(text: username),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
height: 8.0,
|
|
|
|
),
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
ConstrainedBox(
|
|
|
|
constraints: BoxConstraints(minWidth: 100),
|
|
|
|
child: Text("${translate('Password')}:")
|
|
|
|
.marginOnly(bottom: 16.0)),
|
|
|
|
SizedBox(
|
|
|
|
width: 24.0,
|
|
|
|
),
|
|
|
|
Expanded(
|
|
|
|
child: TextField(
|
|
|
|
onChanged: (s) {
|
|
|
|
password = s;
|
|
|
|
},
|
|
|
|
decoration: InputDecoration(
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
),
|
|
|
|
controller: TextEditingController(text: password),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
SizedBox(
|
|
|
|
height: 8.0,
|
|
|
|
),
|
|
|
|
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
actions: [
|
|
|
|
TextButton(
|
|
|
|
onPressed: () {
|
|
|
|
close();
|
|
|
|
},
|
|
|
|
child: Text(translate("Cancel"))),
|
|
|
|
TextButton(
|
|
|
|
onPressed: () async {
|
|
|
|
setState(() {
|
|
|
|
proxyMsg = "";
|
|
|
|
isInProgress = true;
|
|
|
|
});
|
|
|
|
final cancel = () {
|
|
|
|
setState(() {
|
|
|
|
isInProgress = false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
proxy = proxy.trim();
|
|
|
|
username = username.trim();
|
|
|
|
password = password.trim();
|
|
|
|
|
|
|
|
if (proxy.isNotEmpty) {
|
|
|
|
proxyMsg =
|
|
|
|
translate(await bind.mainTestIfValidServer(server: proxy));
|
|
|
|
if (proxyMsg.isEmpty) {
|
|
|
|
// ignore
|
|
|
|
} else {
|
|
|
|
cancel();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await bind.mainSetSocks(
|
|
|
|
proxy: proxy, username: username, password: password);
|
|
|
|
close();
|
|
|
|
},
|
|
|
|
child: Text(translate("OK"))),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
//#endregion
|