1522 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			1522 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
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/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: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';
 | 
						|
 | 
						|
const double _kTabWidth = 235;
 | 
						|
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 _kSettingPageIndexTag = "settingPageIndex";
 | 
						|
 | 
						|
class _TabInfo {
 | 
						|
  late final String label;
 | 
						|
  late final IconData unselected;
 | 
						|
  late final IconData selected;
 | 
						|
  _TabInfo(this.label, this.unselected, this.selected);
 | 
						|
}
 | 
						|
 | 
						|
class DesktopSettingPage extends StatefulWidget {
 | 
						|
  final int initialPage;
 | 
						|
 | 
						|
  const DesktopSettingPage({Key? key, required this.initialPage})
 | 
						|
      : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  State<DesktopSettingPage> createState() => _DesktopSettingPageState();
 | 
						|
 | 
						|
  static void switch2page(int page) {
 | 
						|
    if (page >= 5) return;
 | 
						|
    try {
 | 
						|
      if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
 | 
						|
        DesktopTabPage.onAddSetting(initialPage: page);
 | 
						|
        PageController controller = Get.find(tag: _kSettingPageControllerTag);
 | 
						|
        RxInt selectedIndex = Get.find(tag: _kSettingPageIndexTag);
 | 
						|
        selectedIndex.value = page;
 | 
						|
        controller.jumpToPage(page);
 | 
						|
      } else {
 | 
						|
        DesktopTabPage.onAddSetting(initialPage: page);
 | 
						|
      }
 | 
						|
    } catch (e) {
 | 
						|
      debugPrint('$e');
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _DesktopSettingPageState extends State<DesktopSettingPage>
 | 
						|
    with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
 | 
						|
  final List<_TabInfo> settingTabs = <_TabInfo>[
 | 
						|
    _TabInfo('General', Icons.settings_outlined, Icons.settings),
 | 
						|
    _TabInfo('Security', Icons.enhanced_encryption_outlined,
 | 
						|
        Icons.enhanced_encryption),
 | 
						|
    _TabInfo('Network', Icons.link_outlined, Icons.link),
 | 
						|
    _TabInfo('Account', Icons.person_outline, Icons.person),
 | 
						|
    _TabInfo('About', Icons.info_outline, Icons.info)
 | 
						|
  ];
 | 
						|
 | 
						|
  late PageController controller;
 | 
						|
  late RxInt selectedIndex;
 | 
						|
 | 
						|
  @override
 | 
						|
  bool get wantKeepAlive => true;
 | 
						|
 | 
						|
  @override
 | 
						|
  void initState() {
 | 
						|
    super.initState();
 | 
						|
    selectedIndex = (widget.initialPage < 5 ? widget.initialPage : 0).obs;
 | 
						|
    Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag);
 | 
						|
    controller = PageController(initialPage: widget.initialPage);
 | 
						|
    Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void dispose() {
 | 
						|
    super.dispose();
 | 
						|
    Get.delete<PageController>(tag: _kSettingPageControllerTag);
 | 
						|
    Get.delete<RxInt>(tag: _kSettingPageIndexTag);
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    super.build(context);
 | 
						|
    return Scaffold(
 | 
						|
      backgroundColor: Theme.of(context).backgroundColor,
 | 
						|
      body: Row(
 | 
						|
        children: <Widget>[
 | 
						|
          SizedBox(
 | 
						|
            width: _kTabWidth,
 | 
						|
            child: Column(
 | 
						|
              children: [
 | 
						|
                _header(),
 | 
						|
                Flexible(child: _listView(tabs: settingTabs)),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          const VerticalDivider(thickness: 1, width: 1),
 | 
						|
          Expanded(
 | 
						|
            child: Container(
 | 
						|
              color: Theme.of(context).scaffoldBackgroundColor,
 | 
						|
              child: DesktopScrollWrapper(
 | 
						|
                  scrollController: controller,
 | 
						|
                  child: PageView(
 | 
						|
                    controller: controller,
 | 
						|
                    children: const [
 | 
						|
                      _General(),
 | 
						|
                      _Safety(),
 | 
						|
                      _Network(),
 | 
						|
                      _Account(),
 | 
						|
                      _About(),
 | 
						|
                    ],
 | 
						|
                  )),
 | 
						|
            ),
 | 
						|
          )
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  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: NeverScrollableScrollPhysics(),
 | 
						|
          controller: scrollController,
 | 
						|
          children: tabs
 | 
						|
              .asMap()
 | 
						|
              .entries
 | 
						|
              .map((tab) => _listItem(tab: tab.value, index: tab.key))
 | 
						|
              .toList(),
 | 
						|
        ));
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _listItem({required _TabInfo tab, required int index}) {
 | 
						|
    return Obx(() {
 | 
						|
      bool selected = index == selectedIndex.value;
 | 
						|
      return SizedBox(
 | 
						|
        width: _kTabWidth,
 | 
						|
        height: _kTabHeight,
 | 
						|
        child: InkWell(
 | 
						|
          onTap: () {
 | 
						|
            if (selectedIndex.value != index) {
 | 
						|
              controller.jumpToPage(index);
 | 
						|
            }
 | 
						|
            selectedIndex.value = index;
 | 
						|
          },
 | 
						|
          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> {
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final scrollController = ScrollController();
 | 
						|
    return DesktopScrollWrapper(
 | 
						|
        scrollController: scrollController,
 | 
						|
        child: ListView(
 | 
						|
          physics: NeverScrollableScrollPhysics(),
 | 
						|
          controller: scrollController,
 | 
						|
          children: [
 | 
						|
            theme(),
 | 
						|
            abr(),
 | 
						|
            hwcodec(),
 | 
						|
            audio(context),
 | 
						|
            record(context),
 | 
						|
            _Card(title: 'Language', children: [language()]),
 | 
						|
          ],
 | 
						|
        ).marginOnly(bottom: _kListViewBottomMargin));
 | 
						|
  }
 | 
						|
 | 
						|
  Widget theme() {
 | 
						|
    final current = MyTheme.getThemeModePreference().toShortString();
 | 
						|
    onChanged(String value) {
 | 
						|
      MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
 | 
						|
      setState(() {});
 | 
						|
    }
 | 
						|
 | 
						|
    return _Card(title: 'Theme', children: [
 | 
						|
      _Radio<String>(context,
 | 
						|
          value: "light",
 | 
						|
          groupValue: current,
 | 
						|
          label: "Light",
 | 
						|
          onChanged: onChanged),
 | 
						|
      _Radio<String>(context,
 | 
						|
          value: "dark",
 | 
						|
          groupValue: current,
 | 
						|
          label: "Dark",
 | 
						|
          onChanged: onChanged),
 | 
						|
      _Radio<String>(context,
 | 
						|
          value: "system",
 | 
						|
          groupValue: current,
 | 
						|
          label: "Follow System",
 | 
						|
          onChanged: onChanged),
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
 | 
						|
  Widget abr() {
 | 
						|
    return _Card(title: 'Adaptive Bitrate', children: [
 | 
						|
      _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'),
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
 | 
						|
  Widget hwcodec() {
 | 
						|
    return Offstage(
 | 
						|
      offstage: !bind.mainHasHwcodec(),
 | 
						|
      child: _Card(title: 'Hardware Codec', children: [
 | 
						|
        _OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec'),
 | 
						|
      ]),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget audio(BuildContext context) {
 | 
						|
    String getDefault() {
 | 
						|
      if (Platform.isWindows) return "System Sound";
 | 
						|
      return "";
 | 
						|
    }
 | 
						|
 | 
						|
    Future<String> getValue() async {
 | 
						|
      String device = await bind.mainGetOption(key: 'audio-input');
 | 
						|
      if (device.isNotEmpty) {
 | 
						|
        return device;
 | 
						|
      } else {
 | 
						|
        return getDefault();
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    setDevice(String device) {
 | 
						|
      if (device == getDefault()) device = "";
 | 
						|
      bind.mainSetOption(key: 'audio-input', value: device);
 | 
						|
    }
 | 
						|
 | 
						|
    return _futureBuilder(future: () async {
 | 
						|
      List<String> devices = (await bind.mainGetSoundInputs()).toList();
 | 
						|
      if (Platform.isWindows) {
 | 
						|
        devices.insert(0, 'System Sound');
 | 
						|
      }
 | 
						|
      String current = await getValue();
 | 
						|
      return {'devices': devices, 'current': current};
 | 
						|
    }(), hasData: (data) {
 | 
						|
      String currentDevice = data['current'];
 | 
						|
      List<String> devices = data['devices'] as List<String>;
 | 
						|
      if (devices.isEmpty) {
 | 
						|
        return const Offstage();
 | 
						|
      }
 | 
						|
      return _Card(title: 'Audio Input Device', children: [
 | 
						|
        ...devices.map((device) => _Radio<String>(context,
 | 
						|
                value: device,
 | 
						|
                groupValue: currentDevice,
 | 
						|
                label: device, onChanged: (value) {
 | 
						|
              setDevice(value);
 | 
						|
              setState(() {});
 | 
						|
            }))
 | 
						|
      ]);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  Widget record(BuildContext context) {
 | 
						|
    return _futureBuilder(future: () async {
 | 
						|
      String customDirectory =
 | 
						|
          await bind.mainGetOption(key: 'video-save-directory');
 | 
						|
      String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
 | 
						|
      String dir;
 | 
						|
      if (customDirectory.isNotEmpty) {
 | 
						|
        dir = customDirectory;
 | 
						|
      } else {
 | 
						|
        dir = defaultDirectory;
 | 
						|
      }
 | 
						|
      final canlaunch = await canLaunchUrl(Uri.file(dir));
 | 
						|
      return {'dir': dir, 'canlaunch': canlaunch};
 | 
						|
    }(), hasData: (data) {
 | 
						|
      Map<String, dynamic> map = data as Map<String, dynamic>;
 | 
						|
      String dir = map['dir']!;
 | 
						|
      bool canlaunch = map['canlaunch']! as bool;
 | 
						|
 | 
						|
      return _Card(title: 'Recording', children: [
 | 
						|
        _OptionCheckBox(context, 'Automatically record incoming sessions',
 | 
						|
            'allow-auto-record-incoming'),
 | 
						|
        Row(
 | 
						|
          children: [
 | 
						|
            Text('${translate('Directory')}:'),
 | 
						|
            Expanded(
 | 
						|
              child: GestureDetector(
 | 
						|
                  onTap: canlaunch ? () => launchUrl(Uri.file(dir)) : null,
 | 
						|
                  child: Text(
 | 
						|
                    dir,
 | 
						|
                    softWrap: true,
 | 
						|
                    style:
 | 
						|
                        const TextStyle(decoration: TextDecoration.underline),
 | 
						|
                  )).marginOnly(left: 10),
 | 
						|
            ),
 | 
						|
            ElevatedButton(
 | 
						|
                    onPressed: () async {
 | 
						|
                      String? selectedDirectory = await FilePicker.platform
 | 
						|
                          .getDirectoryPath(initialDirectory: dir);
 | 
						|
                      if (selectedDirectory != null) {
 | 
						|
                        await bind.mainSetOption(
 | 
						|
                            key: 'video-save-directory',
 | 
						|
                            value: selectedDirectory);
 | 
						|
                        setState(() {});
 | 
						|
                      }
 | 
						|
                    },
 | 
						|
                    child: Text(translate('Change')))
 | 
						|
                .marginOnly(left: 5),
 | 
						|
          ],
 | 
						|
        ).marginOnly(left: _kContentHMargin),
 | 
						|
      ]);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  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, "");
 | 
						|
      values.insert(0, "Default");
 | 
						|
      String currentKey = data["lang"]!;
 | 
						|
      if (!keys.contains(currentKey)) {
 | 
						|
        currentKey = "";
 | 
						|
      }
 | 
						|
      return _ComboBox(
 | 
						|
        keys: keys,
 | 
						|
        values: values,
 | 
						|
        initialKey: currentKey,
 | 
						|
        onChanged: (key) async {
 | 
						|
          await bind.mainSetLocalOption(key: "lang", value: key);
 | 
						|
          Get.forceAppUpdate();
 | 
						|
          bind.mainChangeLanguage(lang: key);
 | 
						|
        },
 | 
						|
      ).marginOnly(left: _kContentHMargin);
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
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: NeverScrollableScrollPhysics(),
 | 
						|
            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: 'ID', children: [changeId()]),
 | 
						|
                    more(context),
 | 
						|
                  ]),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            )).marginOnly(bottom: _kListViewBottomMargin));
 | 
						|
  }
 | 
						|
 | 
						|
  Widget changeId() {
 | 
						|
    return _Button('Change ID', changeIdDialog, enabled: !locked);
 | 
						|
  }
 | 
						|
 | 
						|
  Widget permissions(context) {
 | 
						|
    bool enabled = !locked;
 | 
						|
    return _Card(title: 'Permissions', children: [
 | 
						|
      _OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard',
 | 
						|
          enabled: enabled),
 | 
						|
      _OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard',
 | 
						|
          enabled: enabled),
 | 
						|
      _OptionCheckBox(context, 'Enable File Transfer', 'enable-file-transfer',
 | 
						|
          enabled: enabled),
 | 
						|
      _OptionCheckBox(context, 'Enable Audio', 'enable-audio',
 | 
						|
          enabled: enabled),
 | 
						|
      _OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel',
 | 
						|
          enabled: enabled),
 | 
						|
      _OptionCheckBox(context, 'Enable Remote Restart', 'enable-remote-restart',
 | 
						|
          enabled: enabled),
 | 
						|
      _OptionCheckBox(
 | 
						|
          context, 'Enable Recording Session', 'enable-record-session',
 | 
						|
          enabled: enabled),
 | 
						|
      _OptionCheckBox(context, 'Enable remote configuration modification',
 | 
						|
          'allow-remote-config-modification',
 | 
						|
          enabled: enabled),
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
 | 
						|
  Widget password(BuildContext context) {
 | 
						|
    return ChangeNotifierProvider.value(
 | 
						|
        value: gFFI.serverModel,
 | 
						|
        child: Consumer<ServerModel>(builder: ((context, model, child) {
 | 
						|
          List<String> keys = [
 | 
						|
            kUseTemporaryPassword,
 | 
						|
            kUsePermanentPassword,
 | 
						|
            kUseBothPasswords,
 | 
						|
          ];
 | 
						|
          List<String> values = [
 | 
						|
            translate("Use temporary password"),
 | 
						|
            translate("Use permanent password"),
 | 
						|
            translate("Use both passwords"),
 | 
						|
          ];
 | 
						|
          bool tmpEnabled = model.verificationMethod != kUsePermanentPassword;
 | 
						|
          bool permEnabled = model.verificationMethod != kUseTemporaryPassword;
 | 
						|
          String currentValue = values[keys.indexOf(model.verificationMethod)];
 | 
						|
          List<Widget> radios = values
 | 
						|
              .map((value) => _Radio<String>(
 | 
						|
                    context,
 | 
						|
                    value: value,
 | 
						|
                    groupValue: currentValue,
 | 
						|
                    label: value,
 | 
						|
                    onChanged: ((value) {
 | 
						|
                      () async {
 | 
						|
                        await model
 | 
						|
                            .setVerificationMethod(keys[values.indexOf(value)]);
 | 
						|
                        await model.updatePasswordModel();
 | 
						|
                      }();
 | 
						|
                    }),
 | 
						|
                    enabled: !locked,
 | 
						|
                  ))
 | 
						|
              .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)),
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ).paddingSymmetric(horizontal: 10),
 | 
						|
                    onTap: () => onChanged?.call(value),
 | 
						|
                  ))
 | 
						|
              .toList();
 | 
						|
 | 
						|
          return _Card(title: 'Password', children: [
 | 
						|
            radios[0],
 | 
						|
            _SubLabeledWidget(
 | 
						|
                'Temporary Password Length',
 | 
						|
                Row(
 | 
						|
                  children: [
 | 
						|
                    ...lengthRadios,
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
                enabled: tmpEnabled && !locked),
 | 
						|
            radios[1],
 | 
						|
            _SubButton('Set permanent password', setPasswordDialog,
 | 
						|
                permEnabled && !locked),
 | 
						|
            radios[2],
 | 
						|
          ]);
 | 
						|
        })));
 | 
						|
  }
 | 
						|
 | 
						|
  Widget more(BuildContext context) {
 | 
						|
    bool enabled = !locked;
 | 
						|
    return _Card(title: 'Security', children: [
 | 
						|
      _OptionCheckBox(context, 'Deny remote access', 'stop-service',
 | 
						|
          checkedIcon: const Icon(
 | 
						|
            Icons.warning_amber_rounded,
 | 
						|
            color: kColorWarn,
 | 
						|
          ),
 | 
						|
          enabled: enabled),
 | 
						|
      Offstage(
 | 
						|
        offstage: !Platform.isWindows,
 | 
						|
        child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp',
 | 
						|
            enabled: enabled),
 | 
						|
      ),
 | 
						|
      _OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
 | 
						|
          reverse: true, enabled: enabled),
 | 
						|
      ...directIp(context),
 | 
						|
      whitelist(),
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
 | 
						|
  List<Widget> directIp(BuildContext context) {
 | 
						|
    TextEditingController controller = TextEditingController();
 | 
						|
    update() => setState(() {});
 | 
						|
    RxBool applyEnabled = false.obs;
 | 
						|
    return [
 | 
						|
      _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server',
 | 
						|
          update: update, enabled: !locked),
 | 
						|
      _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());
 | 
						|
          if (!enabled) applyEnabled.value = false;
 | 
						|
          controller.text = data['port'].toString();
 | 
						|
          return Offstage(
 | 
						|
            offstage: !enabled,
 | 
						|
            child: Row(children: [
 | 
						|
              _SubLabeledWidget(
 | 
						|
                'Port',
 | 
						|
                SizedBox(
 | 
						|
                  width: 80,
 | 
						|
                  child: TextField(
 | 
						|
                    controller: controller,
 | 
						|
                    enabled: enabled && !locked,
 | 
						|
                    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])$')),
 | 
						|
                    ],
 | 
						|
                    textAlign: TextAlign.end,
 | 
						|
                    decoration: const InputDecoration(
 | 
						|
                      hintText: '21118',
 | 
						|
                      border: InputBorder.none,
 | 
						|
                      contentPadding: EdgeInsets.only(right: 5),
 | 
						|
                      isCollapsed: true,
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                enabled: enabled && !locked,
 | 
						|
              ).marginOnly(left: 5),
 | 
						|
              Obx(() => ElevatedButton(
 | 
						|
                    onPressed: applyEnabled.value && enabled && !locked
 | 
						|
                        ? () async {
 | 
						|
                            applyEnabled.value = false;
 | 
						|
                            await bind.mainSetOption(
 | 
						|
                                key: 'direct-access-port',
 | 
						|
                                value: controller.text);
 | 
						|
                          }
 | 
						|
                        : null,
 | 
						|
                    child: Text(
 | 
						|
                      translate('Apply'),
 | 
						|
                    ),
 | 
						|
                  ).marginOnly(left: 20))
 | 
						|
            ]),
 | 
						|
          );
 | 
						|
        },
 | 
						|
      ),
 | 
						|
    ];
 | 
						|
  }
 | 
						|
 | 
						|
  Widget whitelist() {
 | 
						|
    bool enabled = !locked;
 | 
						|
    return _futureBuilder(future: () async {
 | 
						|
      return await bind.mainGetOption(key: 'whitelist');
 | 
						|
    }(), hasData: (data) {
 | 
						|
      RxBool hasWhitelist = (data as String).isNotEmpty.obs;
 | 
						|
      update() async {
 | 
						|
        hasWhitelist.value =
 | 
						|
            (await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
 | 
						|
      }
 | 
						|
 | 
						|
      onChanged(bool? checked) async {
 | 
						|
        changeWhiteList(callback: update);
 | 
						|
      }
 | 
						|
 | 
						|
      return GestureDetector(
 | 
						|
        child: Tooltip(
 | 
						|
          message: translate('whitelist_tip'),
 | 
						|
          child: Obx(() => Row(
 | 
						|
                children: [
 | 
						|
                  Checkbox(
 | 
						|
                          value: hasWhitelist.value,
 | 
						|
                          onChanged: enabled ? onChanged : null)
 | 
						|
                      .marginOnly(right: 5),
 | 
						|
                  Offstage(
 | 
						|
                    offstage: !hasWhitelist.value,
 | 
						|
                    child: const Icon(Icons.warning_amber_rounded,
 | 
						|
                            color: Color.fromARGB(255, 255, 204, 0))
 | 
						|
                        .marginOnly(right: 5),
 | 
						|
                  ),
 | 
						|
                  Expanded(
 | 
						|
                      child: Text(
 | 
						|
                    translate('Use IP Whitelisting'),
 | 
						|
                    style:
 | 
						|
                        TextStyle(color: _disabledTextColor(context, enabled)),
 | 
						|
                  ))
 | 
						|
                ],
 | 
						|
              )),
 | 
						|
        ),
 | 
						|
        onTap: () {
 | 
						|
          onChanged(!hasWhitelist.value);
 | 
						|
        },
 | 
						|
      ).marginOnly(left: _kCheckBoxLeftMargin);
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
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 = true;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    super.build(context);
 | 
						|
    bool enabled = !locked;
 | 
						|
    final scrollController = ScrollController();
 | 
						|
    return DesktopScrollWrapper(
 | 
						|
        scrollController: scrollController,
 | 
						|
        child: ListView(
 | 
						|
            controller: scrollController,
 | 
						|
            physics: NeverScrollableScrollPhysics(),
 | 
						|
            children: [
 | 
						|
              _lock(locked, 'Unlock Network Settings', () {
 | 
						|
                locked = false;
 | 
						|
                setState(() => {});
 | 
						|
              }),
 | 
						|
              AbsorbPointer(
 | 
						|
                absorbing: locked,
 | 
						|
                child: Column(children: [
 | 
						|
                  _Card(title: 'Server', children: [
 | 
						|
                    _Button('ID/Relay Server', changeServer, enabled: enabled),
 | 
						|
                  ]),
 | 
						|
                  _Card(title: 'Proxy', children: [
 | 
						|
                    _Button('Socks5 Proxy', changeSocks5Proxy,
 | 
						|
                        enabled: enabled),
 | 
						|
                  ]),
 | 
						|
                ]),
 | 
						|
              ),
 | 
						|
            ]).marginOnly(bottom: _kListViewBottomMargin));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
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: NeverScrollableScrollPhysics(),
 | 
						|
          controller: scrollController,
 | 
						|
          children: [
 | 
						|
            _Card(title: 'Account', children: [accountAction()]),
 | 
						|
          ],
 | 
						|
        ).marginOnly(bottom: _kListViewBottomMargin));
 | 
						|
  }
 | 
						|
 | 
						|
  Widget accountAction() {
 | 
						|
    return _futureBuilder(future: () async {
 | 
						|
      return await gFFI.userModel.getUserName();
 | 
						|
    }(), hasData: (data) {
 | 
						|
      String username = data as String;
 | 
						|
      return _Button(
 | 
						|
          username.isEmpty ? 'Login' : 'Logout',
 | 
						|
          () => {
 | 
						|
                username.isEmpty
 | 
						|
                    ? loginDialog().then((success) {
 | 
						|
                        if (success) {
 | 
						|
                          // refresh frame
 | 
						|
                          setState(() {});
 | 
						|
                        }
 | 
						|
                      })
 | 
						|
                    : gFFI.userModel.logOut()
 | 
						|
              });
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
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();
 | 
						|
      return {'license': license, 'version': version};
 | 
						|
    }(), hasData: (data) {
 | 
						|
      final license = data['license'].toString();
 | 
						|
      final version = data['version'].toString();
 | 
						|
      const linkStyle = TextStyle(decoration: TextDecoration.underline);
 | 
						|
      final scrollController = ScrollController();
 | 
						|
      return DesktopScrollWrapper(
 | 
						|
          scrollController: scrollController,
 | 
						|
          child: SingleChildScrollView(
 | 
						|
            controller: scrollController,
 | 
						|
            physics: NeverScrollableScrollPhysics(),
 | 
						|
            child: _Card(title: "About RustDesk", children: [
 | 
						|
              Column(
 | 
						|
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                children: [
 | 
						|
                  const SizedBox(
 | 
						|
                    height: 8.0,
 | 
						|
                  ),
 | 
						|
                  Text("Version: $version").marginSymmetric(vertical: 4.0),
 | 
						|
                  InkWell(
 | 
						|
                      onTap: () {
 | 
						|
                        launchUrlString("https://rustdesk.com/privacy");
 | 
						|
                      },
 | 
						|
                      child: const Text(
 | 
						|
                        "Privacy Statement",
 | 
						|
                        style: linkStyle,
 | 
						|
                      ).marginSymmetric(vertical: 4.0)),
 | 
						|
                  InkWell(
 | 
						|
                      onTap: () {
 | 
						|
                        launchUrlString("https://rustdesk.com");
 | 
						|
                      },
 | 
						|
                      child: const Text(
 | 
						|
                        "Website",
 | 
						|
                        style: linkStyle,
 | 
						|
                      ).marginSymmetric(vertical: 4.0)),
 | 
						|
                  Container(
 | 
						|
                    decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
 | 
						|
                    padding:
 | 
						|
                        const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
 | 
						|
                    child: Row(
 | 
						|
                      children: [
 | 
						|
                        Expanded(
 | 
						|
                          child: Column(
 | 
						|
                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                            children: [
 | 
						|
                              Text(
 | 
						|
                                "Copyright © 2022 Purslane Ltd.\n$license",
 | 
						|
                                style: const TextStyle(color: Colors.white),
 | 
						|
                              ),
 | 
						|
                              const Text(
 | 
						|
                                "Made with heart in this chaotic world!",
 | 
						|
                                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}) {
 | 
						|
  return Row(
 | 
						|
    children: [
 | 
						|
      SizedBox(
 | 
						|
        width: _kCardFixedWidth,
 | 
						|
        child: Card(
 | 
						|
          child: Column(
 | 
						|
            children: [
 | 
						|
              Row(
 | 
						|
                children: [
 | 
						|
                  Text(
 | 
						|
                    translate(title),
 | 
						|
                    textAlign: TextAlign.start,
 | 
						|
                    style: const TextStyle(
 | 
						|
                      fontSize: _kTitleFontSize,
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                  const Spacer(),
 | 
						|
                ],
 | 
						|
              ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10),
 | 
						|
              ...children
 | 
						|
                  .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)),
 | 
						|
            ],
 | 
						|
          ).marginOnly(bottom: 10),
 | 
						|
        ).marginOnly(left: _kCardLeftMargin, top: 15),
 | 
						|
      ),
 | 
						|
    ],
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
Color? _disabledTextColor(BuildContext context, bool enabled) {
 | 
						|
  return enabled
 | 
						|
      ? null
 | 
						|
      : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
 | 
						|
}
 | 
						|
 | 
						|
// ignore: non_constant_identifier_names
 | 
						|
Widget _OptionCheckBox(BuildContext context, String label, String key,
 | 
						|
    {Function()? update,
 | 
						|
    bool reverse = false,
 | 
						|
    bool enabled = true,
 | 
						|
    Icon? checkedIcon}) {
 | 
						|
  return _futureBuilder(
 | 
						|
      future: bind.mainGetOption(key: key),
 | 
						|
      hasData: (data) {
 | 
						|
        bool value = option2bool(key, data.toString());
 | 
						|
        if (reverse) value = !value;
 | 
						|
        var ref = value.obs;
 | 
						|
        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();
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        return GestureDetector(
 | 
						|
          child: Obx(
 | 
						|
            () => Row(
 | 
						|
              children: [
 | 
						|
                Checkbox(
 | 
						|
                        value: ref.value, onChanged: enabled ? 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: () {
 | 
						|
            onChanged(!ref.value);
 | 
						|
          },
 | 
						|
        );
 | 
						|
      });
 | 
						|
}
 | 
						|
 | 
						|
// 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 enabled = true}) {
 | 
						|
  var onChange = enabled
 | 
						|
      ? (T? value) {
 | 
						|
          if (value != null) {
 | 
						|
            onChanged(value);
 | 
						|
          }
 | 
						|
        }
 | 
						|
      : null;
 | 
						|
  return GestureDetector(
 | 
						|
    child: Row(
 | 
						|
      children: [
 | 
						|
        Radio<T>(value: value, groupValue: groupValue, onChanged: onChange),
 | 
						|
        Expanded(
 | 
						|
          child: Text(translate(label),
 | 
						|
                  maxLines: 1,
 | 
						|
                  overflow: TextOverflow.ellipsis,
 | 
						|
                  style: TextStyle(
 | 
						|
                      fontSize: _kContentFontSize,
 | 
						|
                      color: _disabledTextColor(context, enabled)))
 | 
						|
              .marginOnly(left: 5),
 | 
						|
        ),
 | 
						|
      ],
 | 
						|
    ).marginOnly(left: _kRadioLeftMargin),
 | 
						|
    onTap: () => onChange?.call(value),
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
// ignore: non_constant_identifier_names
 | 
						|
Widget _Button(String label, Function() onPressed,
 | 
						|
    {bool enabled = true, String? tip}) {
 | 
						|
  var button = ElevatedButton(
 | 
						|
      onPressed: enabled ? onPressed : null,
 | 
						|
      child: Container(
 | 
						|
        child: Text(
 | 
						|
          translate(label),
 | 
						|
        ).marginSymmetric(horizontal: 15),
 | 
						|
      ));
 | 
						|
  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: Container(
 | 
						|
            child: Text(
 | 
						|
              translate(label),
 | 
						|
            ).marginSymmetric(horizontal: 15),
 | 
						|
          )),
 | 
						|
    ],
 | 
						|
  ).marginOnly(left: _kContentHSubMargin);
 | 
						|
}
 | 
						|
 | 
						|
// ignore: non_constant_identifier_names
 | 
						|
Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) {
 | 
						|
  RxBool hover = false.obs;
 | 
						|
  return Row(
 | 
						|
    children: [
 | 
						|
      MouseRegion(
 | 
						|
          onEnter: (_) => hover.value = true,
 | 
						|
          onExit: (_) => hover.value = false,
 | 
						|
          child: Obx(
 | 
						|
            () {
 | 
						|
              return Container(
 | 
						|
                  height: 32,
 | 
						|
                  decoration: BoxDecoration(
 | 
						|
                      border: Border.all(
 | 
						|
                          color: hover.value && enabled
 | 
						|
                              ? const Color(0xFFD7D7D7)
 | 
						|
                              : const Color(0xFFCBCBCB),
 | 
						|
                          width: hover.value && enabled ? 2 : 1)),
 | 
						|
                  child: Row(
 | 
						|
                    children: [
 | 
						|
                      Container(
 | 
						|
                        height: 28,
 | 
						|
                        color: (hover.value && enabled)
 | 
						|
                            ? const Color(0xFFD7D7D7)
 | 
						|
                            : const Color(0xFFCBCBCB),
 | 
						|
                        alignment: Alignment.center,
 | 
						|
                        padding: const EdgeInsets.symmetric(
 | 
						|
                            horizontal: 5, vertical: 2),
 | 
						|
                        child: Text(
 | 
						|
                          '${translate(label)}: ',
 | 
						|
                          style: const TextStyle(fontWeight: FontWeight.w300),
 | 
						|
                        ),
 | 
						|
                      ).paddingAll(2),
 | 
						|
                      child,
 | 
						|
                    ],
 | 
						|
                  ));
 | 
						|
            },
 | 
						|
          )),
 | 
						|
    ],
 | 
						|
  ).marginOnly(left: _kContentHSubMargin);
 | 
						|
}
 | 
						|
 | 
						|
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) {
 | 
						|
            debugPrint(snapshot.error.toString());
 | 
						|
          }
 | 
						|
          return Container();
 | 
						|
        }
 | 
						|
      });
 | 
						|
}
 | 
						|
 | 
						|
Widget _lock(
 | 
						|
  bool locked,
 | 
						|
  String label,
 | 
						|
  Function() onUnlock,
 | 
						|
) {
 | 
						|
  return Offstage(
 | 
						|
      offstage: !locked,
 | 
						|
      child: Row(
 | 
						|
        children: [
 | 
						|
          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 bind.mainCheckSuperUserPermission();
 | 
						|
                  if (checked) {
 | 
						|
                    onUnlock();
 | 
						|
                  }
 | 
						|
                },
 | 
						|
              ).marginSymmetric(horizontal: 2, vertical: 4),
 | 
						|
            ).marginOnly(left: _kCardLeftMargin),
 | 
						|
          ).marginOnly(top: 10),
 | 
						|
        ],
 | 
						|
      ));
 | 
						|
}
 | 
						|
 | 
						|
// ignore: must_be_immutable
 | 
						|
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;
 | 
						|
  late String current;
 | 
						|
 | 
						|
  _ComboBox({
 | 
						|
    Key? key,
 | 
						|
    required this.keys,
 | 
						|
    required this.values,
 | 
						|
    required this.initialKey,
 | 
						|
    required this.onChanged,
 | 
						|
    // ignore: unused_element
 | 
						|
    this.enabled = true,
 | 
						|
  }) : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    var index = keys.indexOf(initialKey);
 | 
						|
    if (index < 0) {
 | 
						|
      index = 0;
 | 
						|
    }
 | 
						|
    var ref = values[index].obs;
 | 
						|
    current = keys[index];
 | 
						|
    return Container(
 | 
						|
      decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
 | 
						|
      height: 30,
 | 
						|
      child: Obx(() => DropdownButton<String>(
 | 
						|
            isExpanded: true,
 | 
						|
            value: ref.value,
 | 
						|
            elevation: 16,
 | 
						|
            underline: Container(
 | 
						|
              height: 25,
 | 
						|
            ),
 | 
						|
            icon: const Icon(
 | 
						|
              Icons.expand_more_sharp,
 | 
						|
              size: 20,
 | 
						|
            ),
 | 
						|
            onChanged: enabled
 | 
						|
                ? (String? newValue) {
 | 
						|
                    if (newValue != null && newValue != ref.value) {
 | 
						|
                      ref.value = newValue;
 | 
						|
                      current = newValue;
 | 
						|
                      onChanged(keys[values.indexOf(newValue)]);
 | 
						|
                    }
 | 
						|
                  }
 | 
						|
                : null,
 | 
						|
            items: values.map<DropdownMenuItem<String>>((String value) {
 | 
						|
              return DropdownMenuItem<String>(
 | 
						|
                value: value,
 | 
						|
                child: Text(
 | 
						|
                  value,
 | 
						|
                  style: const TextStyle(fontSize: _kContentFontSize),
 | 
						|
                  overflow: TextOverflow.ellipsis,
 | 
						|
                ).marginOnly(left: 5),
 | 
						|
              );
 | 
						|
            }).toList(),
 | 
						|
          )),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
//#endregion
 | 
						|
 | 
						|
//#region dialogs
 | 
						|
 | 
						|
void changeServer() async {
 | 
						|
  Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
 | 
						|
  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 idController = TextEditingController(text: idServer);
 | 
						|
  var relayController = TextEditingController(text: relayServer);
 | 
						|
  var apiController = TextEditingController(text: apiServer);
 | 
						|
  var keyController = TextEditingController(text: key);
 | 
						|
 | 
						|
  var isInProgress = false;
 | 
						|
 | 
						|
  gFFI.dialogManager.show((setState, close) {
 | 
						|
    submit() async {
 | 
						|
      setState(() {
 | 
						|
        idServerMsg = "";
 | 
						|
        relayServerMsg = "";
 | 
						|
        apiServerMsg = "";
 | 
						|
        isInProgress = true;
 | 
						|
      });
 | 
						|
      cancel() {
 | 
						|
        setState(() {
 | 
						|
          isInProgress = false;
 | 
						|
        });
 | 
						|
      }
 | 
						|
 | 
						|
      idServer = idController.text.trim();
 | 
						|
      relayServer = relayController.text.trim();
 | 
						|
      apiServer = apiController.text.trim().toLowerCase();
 | 
						|
      key = keyController.text.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();
 | 
						|
    }
 | 
						|
 | 
						|
    return CustomAlertDialog(
 | 
						|
      title: Text(translate("ID/Relay Server")),
 | 
						|
      content: ConstrainedBox(
 | 
						|
        constraints: const BoxConstraints(minWidth: 500),
 | 
						|
        child: Column(
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
          children: [
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                ConstrainedBox(
 | 
						|
                    constraints: const BoxConstraints(minWidth: 100),
 | 
						|
                    child: Text("${translate('ID Server')}:")
 | 
						|
                        .marginOnly(bottom: 16.0)),
 | 
						|
                const SizedBox(
 | 
						|
                  width: 24.0,
 | 
						|
                ),
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    decoration: InputDecoration(
 | 
						|
                        border: const OutlineInputBorder(),
 | 
						|
                        errorText: idServerMsg.isNotEmpty ? idServerMsg : null),
 | 
						|
                    controller: idController,
 | 
						|
                    focusNode: FocusNode()..requestFocus(),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                ConstrainedBox(
 | 
						|
                    constraints: const BoxConstraints(minWidth: 100),
 | 
						|
                    child: Text("${translate('Relay Server')}:")
 | 
						|
                        .marginOnly(bottom: 16.0)),
 | 
						|
                const SizedBox(
 | 
						|
                  width: 24.0,
 | 
						|
                ),
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    decoration: InputDecoration(
 | 
						|
                        border: const OutlineInputBorder(),
 | 
						|
                        errorText:
 | 
						|
                            relayServerMsg.isNotEmpty ? relayServerMsg : null),
 | 
						|
                    controller: relayController,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                ConstrainedBox(
 | 
						|
                    constraints: const BoxConstraints(minWidth: 100),
 | 
						|
                    child: Text("${translate('API Server')}:")
 | 
						|
                        .marginOnly(bottom: 16.0)),
 | 
						|
                const SizedBox(
 | 
						|
                  width: 24.0,
 | 
						|
                ),
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    decoration: InputDecoration(
 | 
						|
                        border: const OutlineInputBorder(),
 | 
						|
                        errorText:
 | 
						|
                            apiServerMsg.isNotEmpty ? apiServerMsg : null),
 | 
						|
                    controller: apiController,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                ConstrainedBox(
 | 
						|
                    constraints: const BoxConstraints(minWidth: 100),
 | 
						|
                    child:
 | 
						|
                        Text("${translate('Key')}:").marginOnly(bottom: 16.0)),
 | 
						|
                const SizedBox(
 | 
						|
                  width: 24.0,
 | 
						|
                ),
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    decoration: const InputDecoration(
 | 
						|
                      border: OutlineInputBorder(),
 | 
						|
                    ),
 | 
						|
                    controller: keyController,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 4.0,
 | 
						|
            ),
 | 
						|
            Offstage(
 | 
						|
                offstage: !isInProgress, child: const LinearProgressIndicator())
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      actions: [
 | 
						|
        TextButton(onPressed: close, child: Text(translate("Cancel"))),
 | 
						|
        TextButton(onPressed: submit, child: Text(translate("OK"))),
 | 
						|
      ],
 | 
						|
      onSubmit: submit,
 | 
						|
      onCancel: close,
 | 
						|
    );
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
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);
 | 
						|
 | 
						|
  var isInProgress = false;
 | 
						|
  gFFI.dialogManager.show((setState, close) {
 | 
						|
    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) {
 | 
						|
        proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy));
 | 
						|
        if (proxyMsg.isEmpty) {
 | 
						|
          // ignore
 | 
						|
        } else {
 | 
						|
          cancel();
 | 
						|
          return;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      await bind.mainSetSocks(
 | 
						|
          proxy: proxy, username: username, password: password);
 | 
						|
      close();
 | 
						|
    }
 | 
						|
 | 
						|
    return CustomAlertDialog(
 | 
						|
      title: Text(translate("Socks5 Proxy")),
 | 
						|
      content: ConstrainedBox(
 | 
						|
        constraints: const BoxConstraints(minWidth: 500),
 | 
						|
        child: Column(
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
          children: [
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                ConstrainedBox(
 | 
						|
                    constraints: const BoxConstraints(minWidth: 100),
 | 
						|
                    child: Text("${translate('Hostname')}:")
 | 
						|
                        .marginOnly(bottom: 16.0)),
 | 
						|
                const SizedBox(
 | 
						|
                  width: 24.0,
 | 
						|
                ),
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    decoration: InputDecoration(
 | 
						|
                        border: const OutlineInputBorder(),
 | 
						|
                        errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
 | 
						|
                    controller: proxyController,
 | 
						|
                    focusNode: FocusNode()..requestFocus(),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                ConstrainedBox(
 | 
						|
                    constraints: const BoxConstraints(minWidth: 100),
 | 
						|
                    child: Text("${translate('Username')}:")
 | 
						|
                        .marginOnly(bottom: 16.0)),
 | 
						|
                const SizedBox(
 | 
						|
                  width: 24.0,
 | 
						|
                ),
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    decoration: const InputDecoration(
 | 
						|
                      border: OutlineInputBorder(),
 | 
						|
                    ),
 | 
						|
                    controller: userController,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                ConstrainedBox(
 | 
						|
                    constraints: const BoxConstraints(minWidth: 100),
 | 
						|
                    child: Text("${translate('Password')}:")
 | 
						|
                        .marginOnly(bottom: 16.0)),
 | 
						|
                const SizedBox(
 | 
						|
                  width: 24.0,
 | 
						|
                ),
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    decoration: const InputDecoration(
 | 
						|
                      border: OutlineInputBorder(),
 | 
						|
                    ),
 | 
						|
                    controller: pwdController,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Offstage(
 | 
						|
                offstage: !isInProgress, child: const LinearProgressIndicator())
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      actions: [
 | 
						|
        TextButton(onPressed: close, child: Text(translate("Cancel"))),
 | 
						|
        TextButton(onPressed: submit, child: Text(translate("OK"))),
 | 
						|
      ],
 | 
						|
      onSubmit: submit,
 | 
						|
      onCancel: close,
 | 
						|
    );
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
//#endregion
 |