diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index ce4b68ae0..056dc714f 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -1223,76 +1223,9 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId); qualityInitValue = quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; - const qualityMinValue = 10.0; - const qualityMoreThresholdValue = 100.0; - const qualityMaxValue = 2000.0; - if (qualityInitValue < qualityMinValue) { - qualityInitValue = qualityMinValue; + if (qualityInitValue < 10 || qualityInitValue > 2000) { + qualityInitValue = 50; } - if (qualityInitValue > qualityMaxValue) { - qualityInitValue = qualityMaxValue; - } - final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final moreQualityInitValue = qualityInitValue > qualityMoreThresholdValue; - final RxBool moreQualityChecked = RxBool(moreQualityInitValue); - final debouncerQuality = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(quality: v); - }, - initialValue: qualityInitValue, - ); - final qualitySlider = Obx(() => Row( - children: [ - Expanded( - flex: 3, - child: Slider( - value: qualitySliderValue.value, - min: qualityMinValue, - max: moreQualityChecked.value - ? qualityMaxValue - : qualityMoreThresholdValue, - divisions: 18, - onChanged: (double value) { - qualitySliderValue.value = value; - debouncerQuality.value = value; - }, - )), - Expanded( - flex: 1, - child: Text( - '${qualitySliderValue.value.round()}%', - style: const TextStyle(fontSize: 15), - )), - Expanded( - flex: 1, - child: Text( - translate('Bitrate'), - style: const TextStyle(fontSize: 15), - )), - Expanded( - flex: 1, - child: Row( - children: [ - Checkbox( - value: moreQualityChecked.value, - onChanged: (bool? value) { - moreQualityChecked.value = value!; - if (!value && - qualitySliderValue.value > - qualityMoreThresholdValue) { - qualitySliderValue.value = qualityMoreThresholdValue; - debouncerQuality.value = qualityMoreThresholdValue; - } - }, - ).marginOnly(right: 5), - Expanded( - child: Text(translate('More')), - ) - ], - )), - ], - )); // fps final fpsOption = await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps'); @@ -1300,55 +1233,20 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { if (fpsInitValue < 5 || fpsInitValue > 120) { fpsInitValue = 30; } - final RxDouble fpsSliderValue = RxDouble(fpsInitValue); - final debouncerFps = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(fps: v); - }, - initialValue: qualityInitValue, - ); bool? direct; try { direct = ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect; } catch (_) {} - final fpsSlider = Offstage( - offstage: (await bind.mainIsUsingPublicServer() && direct != true) || - version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0, - child: Row( - children: [ - Expanded( - flex: 3, - child: Obx((() => Slider( - value: fpsSliderValue.value, - min: 5, - max: 120, - divisions: 23, - onChanged: (double value) { - fpsSliderValue.value = value; - debouncerFps.value = value; - }, - )))), - Expanded( - flex: 1, - child: Obx(() => Text( - '${fpsSliderValue.value.round()}', - style: const TextStyle(fontSize: 15), - ))), - Expanded( - flex: 2, - child: Text( - translate('FPS'), - style: const TextStyle(fontSize: 15), - )) - ], - ), - ); + bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) || + version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0; - final content = Column( - children: [qualitySlider, fpsSlider], - ); + final content = customImageQualityWidget( + initQuality: qualityInitValue, + initFps: fpsInitValue, + setQuality: (v) => setCustomValues(quality: v), + setFps: (v) => setCustomValues(fps: v), + showFps: !notShowFps); msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); } diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart new file mode 100644 index 000000000..771b65ab5 --- /dev/null +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -0,0 +1,277 @@ +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; + +customImageQualityWidget( + {required double initQuality, + required double initFps, + required Function(double) setQuality, + required Function(double) setFps, + required bool showFps}) { + final qualityValue = initQuality.obs; + final fpsValue = initFps.obs; + + final RxBool moreQualityChecked = RxBool(qualityValue.value > 100); + final debouncerQuality = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setQuality(v); + }, + initialValue: qualityValue.value, + ); + final debouncerFps = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setFps(v); + }, + initialValue: fpsValue.value, + ); + + onMoreChanged(bool? value) { + if (value == null) return; + moreQualityChecked.value = value; + if (!value && qualityValue.value > 100) { + qualityValue.value = 100; + } + debouncerQuality.value = qualityValue.value; + } + + return Column( + children: [ + Obx(() => Row( + children: [ + Expanded( + flex: 3, + child: Slider( + value: qualityValue.value, + min: 10.0, + max: moreQualityChecked.value ? 2000 : 100, + divisions: moreQualityChecked.value ? 199 : 18, + onChanged: (double value) async { + qualityValue.value = value; + debouncerQuality.value = value; + }, + ), + ), + Expanded( + flex: 1, + child: Text( + '${qualityValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + Expanded( + flex: isMobile ? 2 : 1, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )), + // mobile doesn't have enough space + if (!isMobile) + Expanded( + flex: 1, + child: Row( + children: [ + Checkbox( + value: moreQualityChecked.value, + onChanged: onMoreChanged, + ), + Expanded( + child: Text(translate('More')), + ) + ], + )) + ], + )), + if (isMobile) + Obx(() => Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Checkbox( + value: moreQualityChecked.value, + onChanged: onMoreChanged, + ), + ), + ), + Expanded( + child: Text(translate('More')), + ) + ], + )), + if (showFps) + Obx(() => Row( + children: [ + Expanded( + flex: 3, + child: Slider( + value: fpsValue.value, + min: 5.0, + max: 120.0, + divisions: 23, + onChanged: (double value) async { + fpsValue.value = value; + debouncerFps.value = value; + }, + ), + ), + Expanded( + flex: 1, + child: Text( + '${fpsValue.value.round()}', + style: const TextStyle(fontSize: 15), + )), + Expanded( + flex: 2, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + )), + ], + ); +} + +customImageQualitySetting() { + final qualityKey = 'custom_image_quality'; + final fpsKey = 'custom-fps'; + + var initQuality = + (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? 50.0); + if (initQuality < 10 || initQuality > 2000) { + initQuality = 50; + } + var initFps = + (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0); + if (initFps < 5 || initFps > 120) { + initFps = 30; + } + + return customImageQualityWidget( + initQuality: initQuality, + initFps: initFps, + setQuality: (v) { + bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString()); + }, + setFps: (v) { + bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString()); + }, + showFps: true); +} + +Future setServerConfig( + List controllers, + List errMsgs, + ServerConfig config, +) async { + config.idServer = config.idServer.trim(); + config.relayServer = config.relayServer.trim(); + config.apiServer = config.apiServer.trim(); + config.key = config.key.trim(); + // id + if (config.idServer.isNotEmpty) { + errMsgs[0].value = + translate(await bind.mainTestIfValidServer(server: config.idServer)); + if (errMsgs[0].isNotEmpty) { + return false; + } + } + // relay + if (config.relayServer.isNotEmpty) { + errMsgs[1].value = + translate(await bind.mainTestIfValidServer(server: config.relayServer)); + if (errMsgs[1].isNotEmpty) { + return false; + } + } + // api + if (config.apiServer.isNotEmpty) { + if (!config.apiServer.startsWith('http://') && + !config.apiServer.startsWith('https://')) { + errMsgs[2].value = + '${translate("API Server")}: ${translate("invalid_http")}'; + return false; + } + } + final oldApiServer = await bind.mainGetApiServer(); + + // should set one by one + await bind.mainSetOption( + key: 'custom-rendezvous-server', value: config.idServer); + await bind.mainSetOption(key: 'relay-server', value: config.relayServer); + await bind.mainSetOption(key: 'api-server', value: config.apiServer); + await bind.mainSetOption(key: 'key', value: config.key); + + final newApiServer = await bind.mainGetApiServer(); + if (oldApiServer.isNotEmpty && + oldApiServer != newApiServer && + gFFI.userModel.isLogin) { + gFFI.userModel.logOut(apiServer: oldApiServer); + } + return true; +} + +List ServerConfigImportExportWidgets( + List controllers, + List errMsgs, +) { + import() { + Clipboard.getData(Clipboard.kTextPlain).then((value) { + final text = value?.text; + if (text != null && text.isNotEmpty) { + try { + final sc = ServerConfig.decode(text); + if (sc.idServer.isNotEmpty) { + controllers[0].text = sc.idServer; + controllers[1].text = sc.relayServer; + controllers[2].text = sc.apiServer; + controllers[3].text = sc.key; + Future success = setServerConfig(controllers, errMsgs, sc); + success.then((value) { + if (value) { + showToast( + translate('Import server configuration successfully')); + } else { + showToast(translate('Invalid server configuration')); + } + }); + } else { + showToast(translate('Invalid server configuration')); + } + } catch (e) { + showToast(translate('Invalid server configuration')); + } + } else { + showToast(translate('Clipboard is empty')); + } + }); + } + + export() { + final text = ServerConfig( + idServer: controllers[0].text.trim(), + relayServer: controllers[1].text.trim(), + apiServer: controllers[2].text.trim(), + key: controllers[3].text.trim()) + .encode(); + debugPrint("ServerConfig export: $text"); + Clipboard.setData(ClipboardData(text: text)); + showToast(translate('Export server configuration successfully')); + } + + return [ + Tooltip( + message: translate('Import Server Config'), + child: IconButton( + icon: Icon(Icons.paste, color: Colors.grey), onPressed: import), + ), + Tooltip( + message: translate('Export Server Config'), + child: IconButton( + icon: Icon(Icons.copy, color: Colors.grey), onPressed: export)) + ]; +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index f158efb82..50052f241 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; @@ -966,54 +967,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { var relayController = TextEditingController(text: old('relay-server')); var apiController = TextEditingController(text: old('api-server')); var keyController = TextEditingController(text: old('key')); - - set(String idServer, String relayServer, String apiServer, - String key) async { - idServer = idServer.trim(); - relayServer = relayServer.trim(); - apiServer = apiServer.trim(); - key = key.trim(); - if (idServer.isNotEmpty) { - idErrMsg.value = - translate(await bind.mainTestIfValidServer(server: idServer)); - if (idErrMsg.isNotEmpty) { - return false; - } - } - if (relayServer.isNotEmpty) { - relayErrMsg.value = - translate(await bind.mainTestIfValidServer(server: relayServer)); - if (relayErrMsg.isNotEmpty) { - return false; - } - } - if (apiServer.isNotEmpty) { - if (!apiServer.startsWith('http://') && - !apiServer.startsWith('https://')) { - apiErrMsg.value = - '${translate("API Server")}: ${translate("invalid_http")}'; - return false; - } - } - final oldApiServer = await bind.mainGetApiServer(); - - // should set one by one - await bind.mainSetOption( - key: 'custom-rendezvous-server', value: idServer); - await bind.mainSetOption(key: 'relay-server', value: relayServer); - await bind.mainSetOption(key: 'api-server', value: apiServer); - await bind.mainSetOption(key: 'key', value: key); - - final newApiServer = await bind.mainGetApiServer(); - if (oldApiServer.isNotEmpty && oldApiServer != newApiServer) { - await gFFI.userModel.logOut(apiServer: oldApiServer); - } - return true; - } + final controllers = [ + idController, + relayController, + apiController, + keyController, + ]; + final errMsgs = [ + idErrMsg, + relayErrMsg, + apiErrMsg, + ]; submit() async { - bool result = await set(idController.text, relayController.text, - apiController.text, keyController.text); + bool result = await setServerConfig( + controllers, + errMsgs, + ServerConfig( + idServer: idController.text, + relayServer: relayController.text, + apiServer: apiController.text, + key: keyController.text)); if (result) { setState(() {}); showToast(translate('Successful')); @@ -1022,83 +996,28 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } } - import() { - Clipboard.getData(Clipboard.kTextPlain).then((value) { - final text = value?.text; - if (text != null && text.isNotEmpty) { - try { - final sc = ServerConfig.decode(text); - if (sc.idServer.isNotEmpty) { - idController.text = sc.idServer; - relayController.text = sc.relayServer; - apiController.text = sc.apiServer; - keyController.text = sc.key; - Future success = - set(sc.idServer, sc.relayServer, sc.apiServer, sc.key); - success.then((value) { - if (value) { - showToast( - translate('Import server configuration successfully')); - } else { - showToast(translate('Invalid server configuration')); - } - }); - } else { - showToast(translate('Invalid server configuration')); - } - } catch (e) { - showToast(translate('Invalid server configuration')); - } - } else { - showToast(translate('Clipboard is empty')); - } - }); - } - - export() { - final text = ServerConfig( - idServer: idController.text, - relayServer: relayController.text, - apiServer: apiController.text, - key: keyController.text) - .encode(); - debugPrint("ServerConfig export: $text"); - - Clipboard.setData(ClipboardData(text: text)); - showToast(translate('Export server configuration successfully')); - } - bool secure = !enabled; - return _Card(title: 'ID/Relay Server', title_suffix: [ - Tooltip( - message: translate('Import Server Config'), - child: IconButton( - icon: Icon(Icons.paste, color: Colors.grey), - onPressed: enabled ? import : null), - ), - Tooltip( - message: translate('Export Server Config'), - child: IconButton( - icon: Icon(Icons.copy, color: Colors.grey), - onPressed: enabled ? export : null)), - ], children: [ - Column( + return _Card( + title: 'ID/Relay Server', + title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs), children: [ - Obx(() => _LabeledTextField(context, 'ID Server', idController, - idErrMsg.value, enabled, secure)), - Obx(() => _LabeledTextField(context, 'Relay Server', - relayController, relayErrMsg.value, enabled, secure)), - Obx(() => _LabeledTextField(context, 'API Server', apiController, - apiErrMsg.value, enabled, secure)), - _LabeledTextField( - context, 'Key', keyController, '', enabled, secure), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [_Button('Apply', submit, enabled: enabled)], - ).marginOnly(top: 10), - ], - ) - ]); + Column( + children: [ + Obx(() => _LabeledTextField(context, 'ID Server', idController, + idErrMsg.value, enabled, secure)), + Obx(() => _LabeledTextField(context, 'Relay Server', + relayController, relayErrMsg.value, enabled, secure)), + Obx(() => _LabeledTextField(context, 'API Server', + apiController, apiErrMsg.value, enabled, secure)), + _LabeledTextField( + context, 'Key', keyController, '', enabled, secure), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [_Button('Apply', submit, enabled: enabled)], + ).marginOnly(top: 10), + ], + ) + ]); } return tmpWrapper(); @@ -1182,15 +1101,6 @@ class _DisplayState extends State<_Display> { } final groupValue = bind.mainGetUserDefaultOption(key: key); - final qualityKey = 'custom_image_quality'; - final qualityValue = - (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? - 50.0) - .obs; - final fpsKey = 'custom-fps'; - final fpsValue = - (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0) - .obs; return _Card(title: 'Default Image Quality', children: [ _Radio(context, value: kRemoteImageQualityBest, @@ -1214,64 +1124,7 @@ class _DisplayState extends State<_Display> { onChanged: onChanged), Offstage( offstage: groupValue != kRemoteImageQualityCustom, - child: Column( - children: [ - Obx(() => Row( - children: [ - Slider( - value: qualityValue.value, - min: 10.0, - max: 100.0, - divisions: 18, - onChanged: (double value) async { - qualityValue.value = value; - await bind.mainSetUserDefaultOption( - key: qualityKey, value: value.toString()); - }, - ), - SizedBox( - width: 40, - child: Text( - '${qualityValue.value.round()}%', - style: const TextStyle(fontSize: 15), - )), - SizedBox( - width: 50, - child: Text( - translate('Bitrate'), - style: const TextStyle(fontSize: 15), - )) - ], - )), - Obx(() => Row( - children: [ - Slider( - value: fpsValue.value, - min: 5.0, - max: 120.0, - divisions: 23, - onChanged: (double value) async { - fpsValue.value = value; - await bind.mainSetUserDefaultOption( - key: fpsKey, value: value.toString()); - }, - ), - SizedBox( - width: 40, - child: Text( - '${fpsValue.value.round()}', - style: const TextStyle(fontSize: 15), - )), - SizedBox( - width: 50, - child: Text( - translate('FPS'), - style: const TextStyle(fontSize: 15), - )) - ], - )), - ], - ), + child: customImageQualitySetting(), ) ]); } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 84426a307..d10095381 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -466,7 +466,7 @@ class _RemotePageState extends State { gFFI.ffiModel.toggleTouchMode(); final v = gFFI.ffiModel.touchMode ? 'Y' : ''; bind.sessionPeerOption( - sessionId: sessionId, name: "touch", value: v); + sessionId: sessionId, name: "touch-mode", value: v); }))); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 95bca0b41..e2120a05f 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:settings_ui/settings_ui.dart'; @@ -458,6 +458,7 @@ class _SettingsState extends State with WidgetsBindingObserver { title: Text(translate("Share Screen")), tiles: shareScreenTiles, ), + defaultDisplaySection(), if (isAndroid) SettingsSection( title: Text(translate("Enhancements")), @@ -513,6 +514,23 @@ class _SettingsState extends State with WidgetsBindingObserver { } return true; } + + defaultDisplaySection() { + return SettingsSection( + title: Text(translate("Display Settings")), + tiles: [ + SettingsTile( + title: Text(translate('Display Settings')), + leading: Icon(Icons.desktop_windows_outlined), + trailing: Icon(Icons.arrow_forward_ios), + onPressed: (context) { + Navigator.push(context, MaterialPageRoute(builder: (context) { + return _DisplayPage(); + })); + }) + ], + ); + } } void showServerSettings(OverlayDialogManager dialogManager) async { @@ -623,3 +641,181 @@ class ScanButton extends StatelessWidget { ); } } + +class _DisplayPage extends StatefulWidget { + const _DisplayPage({super.key}); + + @override + State<_DisplayPage> createState() => __DisplayPageState(); +} + +class __DisplayPageState extends State<_DisplayPage> { + @override + Widget build(BuildContext context) { + final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings()); + final h264 = codecsJson['h264'] ?? false; + final h265 = codecsJson['h265'] ?? false; + var codecList = [ + _RadioEntry('Auto', 'auto'), + _RadioEntry('VP8', 'vp8'), + _RadioEntry('VP9', 'vp9'), + _RadioEntry('AV1', 'av1'), + if (h264) _RadioEntry('H264', 'h264'), + if (h265) _RadioEntry('H265', 'h265') + ]; + RxBool showCustomImageQuality = false.obs; + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.arrow_back_ios)), + title: Text(translate('Display Settings')), + centerTitle: true, + ), + body: SettingsList(sections: [ + SettingsSection( + tiles: [ + _getPopupDialogRadioEntry( + title: 'Default View Style', + list: [ + _RadioEntry('Scale original', kRemoteViewStyleOriginal), + _RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive) + ], + getter: () => bind.mainGetUserDefaultOption(key: 'view_style'), + asyncSetter: (value) async { + await bind.mainSetUserDefaultOption( + key: 'view_style', value: value); + }, + ), + _getPopupDialogRadioEntry( + title: 'Default Image Quality', + list: [ + _RadioEntry('Good image quality', kRemoteImageQualityBest), + _RadioEntry('Balanced', kRemoteImageQualityBalanced), + _RadioEntry('Optimize reaction time', kRemoteImageQualityLow), + _RadioEntry('Custom', kRemoteImageQualityCustom), + ], + getter: () { + final v = bind.mainGetUserDefaultOption(key: 'image_quality'); + showCustomImageQuality.value = v == kRemoteImageQualityCustom; + return v; + }, + asyncSetter: (value) async { + await bind.mainSetUserDefaultOption( + key: 'image_quality', value: value); + showCustomImageQuality.value = + value == kRemoteImageQualityCustom; + }, + tail: customImageQualitySetting(), + showTail: showCustomImageQuality, + notCloseValue: kRemoteImageQualityCustom, + ), + _getPopupDialogRadioEntry( + title: 'Default Codec', + list: codecList, + getter: () => + bind.mainGetUserDefaultOption(key: 'codec-preference'), + asyncSetter: (value) async { + await bind.mainSetUserDefaultOption( + key: 'codec-preference', value: value); + }, + ), + ], + ), + SettingsSection( + title: Text(translate('Other Default Options')), + tiles: [ + otherRow('Show remote cursor', 'show_remote_cursor'), + otherRow('Show quality monitor', 'show_quality_monitor'), + otherRow('Mute', 'disable_audio'), + otherRow('Disable clipboard', 'disable_clipboard'), + otherRow('Lock after session end', 'lock_after_session_end'), + otherRow('Privacy mode', 'privacy_mode'), + otherRow('Touch mode', 'touch-mode'), + ], + ), + ]), + ); + } + + otherRow(String label, String key) { + final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; + return SettingsTile.switchTile( + initialValue: value, + title: Text(translate(label)), + onToggle: (b) async { + await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : ''); + setState(() {}); + }, + ); + } +} + +class _RadioEntry { + final String label; + final String value; + _RadioEntry(this.label, this.value); +} + +typedef _RadioEntryGetter = String Function(); +typedef _RadioEntrySetter = Future Function(String); + +_getPopupDialogRadioEntry({ + required String title, + required List<_RadioEntry> list, + required _RadioEntryGetter getter, + required _RadioEntrySetter asyncSetter, + Widget? tail, + RxBool? showTail, + String? notCloseValue, +}) { + RxString groupValue = ''.obs; + RxString valueText = ''.obs; + + init() { + groupValue.value = getter(); + final e = list.firstWhereOrNull((e) => e.value == groupValue.value); + if (e != null) { + valueText.value = e.label; + } + } + + init(); + + void showDialog() async { + gFFI.dialogManager.show((setState, close, context) { + onChanged(String? value) async { + if (value == null) return; + await asyncSetter(value); + init(); + if (value != notCloseValue) { + close(); + } + } + + return CustomAlertDialog( + content: Obx( + () => Column(children: [ + ...list + .map((e) => getRadio(Text(translate(e.label)), e.value, + groupValue.value, (String? value) => onChanged(value))) + .toList(), + Offstage( + offstage: + !(tail != null && showTail != null && showTail.value == true), + child: tail, + ), + ]), + )); + }, backDismiss: true, clickMaskDismiss: true); + } + + return SettingsTile( + title: Text(translate(title)), + onPressed: (context) => showDialog(), + value: Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Obx(() => Text(translate(valueText.value))), + ), + ); +} diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 550b37d72..3e745ecce 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -147,59 +147,72 @@ void setTemporaryPasswordLengthDialog( void showServerSettingsWithValue( ServerConfig serverConfig, OverlayDialogManager dialogManager) async { - Map oldOptions = jsonDecode(await bind.mainGetOptions()); - final oldCfg = ServerConfig.fromOptions(oldOptions); - var isInProgress = false; final idCtrl = TextEditingController(text: serverConfig.idServer); final relayCtrl = TextEditingController(text: serverConfig.relayServer); final apiCtrl = TextEditingController(text: serverConfig.apiServer); final keyCtrl = TextEditingController(text: serverConfig.key); - String? idServerMsg; - String? relayServerMsg; - String? apiServerMsg; + RxString idServerMsg = ''.obs; + RxString relayServerMsg = ''.obs; + RxString apiServerMsg = ''.obs; + + final controllers = [idCtrl, relayCtrl, apiCtrl, keyCtrl]; + final errMsgs = [ + idServerMsg, + relayServerMsg, + apiServerMsg, + ]; dialogManager.show((setState, close, context) { - Future validate() async { - if (idCtrl.text != oldCfg.idServer) { - final res = await validateAsync(idCtrl.text); - setState(() => idServerMsg = res); - if (idServerMsg != null) return false; - } - if (relayCtrl.text != oldCfg.relayServer) { - relayServerMsg = await validateAsync(relayCtrl.text); - if (relayServerMsg != null) return false; - } - if (apiCtrl.text != oldCfg.apiServer) { - if (apiServerMsg != null) return false; - } - return true; + Future submit() async { + setState(() { + isInProgress = true; + }); + bool ret = await setServerConfig( + controllers, + errMsgs, + ServerConfig( + idServer: idCtrl.text.trim(), + relayServer: relayCtrl.text.trim(), + apiServer: apiCtrl.text.trim(), + key: keyCtrl.text.trim())); + setState(() { + isInProgress = false; + }); + return ret; } return CustomAlertDialog( - title: Text(translate('ID/Relay Server')), + title: Row( + children: [ + Expanded(child: Text(translate('ID/Relay Server'))), + ...ServerConfigImportExportWidgets(controllers, errMsgs), + ], + ), content: Form( - child: Column( + child: Obx(() => Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: idCtrl, decoration: InputDecoration( labelText: translate('ID Server'), - errorText: idServerMsg), + errorText: idServerMsg.value.isEmpty + ? null + : idServerMsg.value), + ) + ] + + [ + TextFormField( + controller: relayCtrl, + decoration: InputDecoration( + labelText: translate('Relay Server'), + errorText: relayServerMsg.value.isEmpty + ? null + : relayServerMsg.value), ) ] + - (isAndroid - ? [ - TextFormField( - controller: relayCtrl, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg), - ) - ] - : []) + [ TextFormField( controller: apiCtrl, @@ -214,7 +227,7 @@ void showServerSettingsWithValue( return translate("invalid_http"); } } - return apiServerMsg; + return null; }, ), TextFormField( @@ -225,7 +238,7 @@ void showServerSettingsWithValue( ), // NOT use Offstage to wrap LinearProgressIndicator if (isInProgress) const LinearProgressIndicator(), - ])), + ]))), actions: [ dialogButton('Cancel', onPressed: () { close(); @@ -233,35 +246,12 @@ void showServerSettingsWithValue( dialogButton( 'OK', onPressed: () async { - setState(() { - idServerMsg = null; - relayServerMsg = null; - apiServerMsg = null; - isInProgress = true; - }); - if (await validate()) { - if (idCtrl.text != oldCfg.idServer) { - if (oldCfg.idServer.isNotEmpty) { - await gFFI.userModel.logOut(); - } - bind.mainSetOption( - key: "custom-rendezvous-server", value: idCtrl.text); - } - if (relayCtrl.text != oldCfg.relayServer) { - bind.mainSetOption(key: "relay-server", value: relayCtrl.text); - } - if (keyCtrl.text != oldCfg.key) { - bind.mainSetOption(key: "key", value: keyCtrl.text); - } - if (apiCtrl.text != oldCfg.apiServer) { - bind.mainSetOption(key: "api-server", value: apiCtrl.text); - } + if (await submit()) { close(); showToast(translate('Successful')); + } else { + showToast(translate('Failed')); } - setState(() { - isInProgress = false; - }); }, ), ], diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index c0f14cbf6..113c11df6 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1179,6 +1179,10 @@ impl PeerConfig { if !mp.contains_key(key) { mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); } + key = "touch-mode"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); + } } }