flutter desktop: ui changes

Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
fufesou 2022-09-01 22:36:40 -07:00
parent 2dc8c02d15
commit 722a4d3de7
4 changed files with 88 additions and 441 deletions

View File

@ -6,6 +6,9 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';

View File

@ -17,7 +17,6 @@ import '../../mobile/widgets/dialog.dart';
import '../../mobile/widgets/overlay.dart'; import '../../mobile/widgets/overlay.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../../models/chat_model.dart';
import '../../common/shared_state.dart'; import '../../common/shared_state.dart';
final initText = '\1' * 1024; final initText = '\1' * 1024;
@ -39,7 +38,6 @@ class RemotePage extends StatefulWidget {
class _RemotePageState extends State<RemotePage> class _RemotePageState extends State<RemotePage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
Timer? _timer; Timer? _timer;
bool _showBar = !isWebDesktop;
String _value = ''; String _value = '';
final _cursorOverImage = false.obs; final _cursorOverImage = false.obs;
@ -131,7 +129,7 @@ class _RemotePageState extends State<RemotePage>
common < oldValue.length && common < oldValue.length &&
common < newValue.length && common < newValue.length &&
newValue[common] == oldValue[common]; newValue[common] == oldValue[common];
++common); ++common) {}
for (i = 0; i < oldValue.length - common; ++i) { for (i = 0; i < oldValue.length - common; ++i) {
_ffi.inputKey('VK_BACK'); _ffi.inputKey('VK_BACK');
} }
@ -145,8 +143,8 @@ class _RemotePageState extends State<RemotePage>
} }
return; return;
} }
if (oldValue.length > 0 && if (oldValue.isNotEmpty &&
newValue.length > 0 && newValue.isNotEmpty &&
oldValue[0] == '\1' && oldValue[0] == '\1' &&
newValue[0] != '\1') { newValue[0] != '\1') {
// clipboard // clipboard
@ -155,7 +153,7 @@ class _RemotePageState extends State<RemotePage>
if (newValue.length == oldValue.length) { if (newValue.length == oldValue.length) {
// ? // ?
} else if (newValue.length < oldValue.length) { } else if (newValue.length < oldValue.length) {
final char = 'VK_BACK'; const char = 'VK_BACK';
_ffi.inputKey(char); _ffi.inputKey(char);
} else { } else {
final content = newValue.substring(oldValue.length); final content = newValue.substring(oldValue.length);
@ -200,24 +198,9 @@ class _RemotePageState extends State<RemotePage>
} }
Widget buildBody(BuildContext context, FfiModel ffiModel) { Widget buildBody(BuildContext context, FfiModel ffiModel) {
final hasDisplays = ffiModel.pi.displays.length > 0;
final keyboard = ffiModel.permissions['keyboard'] != false; final keyboard = ffiModel.permissions['keyboard'] != false;
return Scaffold( return Scaffold(
backgroundColor: MyTheme.color(context).bg, backgroundColor: MyTheme.color(context).bg,
// resizeToAvoidBottomInset: true,
// floatingActionButton: _showBar
// ? null
// : FloatingActionButton(
// mini: true,
// child: Icon(Icons.expand_less),
// backgroundColor: MyTheme.accent,
// onPressed: () {
// setState(() {
// _showBar = !_showBar;
// });
// }),
// bottomNavigationBar:
// _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null,
body: Overlay( body: Overlay(
initialEntries: [ initialEntries: [
OverlayEntry(builder: (context) { OverlayEntry(builder: (context) {
@ -249,7 +232,7 @@ class _RemotePageState extends State<RemotePage>
ChangeNotifierProvider.value(value: _ffi.canvasModel), ChangeNotifierProvider.value(value: _ffi.canvasModel),
], ],
child: Consumer<FfiModel>( child: Consumer<FfiModel>(
builder: (context, ffiModel, _child) => builder: (context, ffiModel, child) =>
buildBody(context, ffiModel)))); buildBody(context, ffiModel))));
} }
@ -307,100 +290,6 @@ class _RemotePageState extends State<RemotePage>
child: child)))); child: child))));
} }
Widget? getBottomAppBar(FfiModel ffiModel) {
final RxBool fullscreen = Get.find(tag: 'fullscreen');
return MouseRegion(
cursor: SystemMouseCursors.basic,
child: BottomAppBar(
elevation: 10,
color: MyTheme.accent,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.clear),
onPressed: () {
clientClose(_ffi.dialogManager);
},
)
] +
<Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.tv),
onPressed: () {
_ffi.dialogManager.dismissAll();
showOptions(widget.id);
},
)
] +
(isWebDesktop
? []
: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(fullscreen.isTrue
? Icons.fullscreen
: Icons.close_fullscreen),
onPressed: () {
fullscreen.value = !fullscreen.value;
},
)
]) +
(isWebDesktop
? []
: _ffi.ffiModel.isPeerAndroid
? [
IconButton(
color: Colors.white,
icon: Icon(Icons.build),
onPressed: () {
if (mobileActionsOverlayEntry == null) {
showMobileActionsOverlay();
} else {
hideMobileActionsOverlay();
}
},
)
]
: []) +
(isWeb
? []
: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.message),
onPressed: () {
_ffi.chatModel
.changeCurrentID(ChatModel.clientModeID);
_ffi.chatModel.toggleChatOverlay();
},
)
]) +
[
IconButton(
color: Colors.white,
icon: Icon(Icons.more_vert),
onPressed: () {
showActions(widget.id, ffiModel);
},
),
]),
IconButton(
color: Colors.white,
icon: Icon(Icons.expand_more),
onPressed: () {
setState(() => _showBar = !_showBar);
}),
],
),
));
}
/// touchMode only: /// touchMode only:
/// LongPress -> right click /// LongPress -> right click
/// OneFingerPan -> start/end -> left down start/end /// OneFingerPan -> start/end -> left down start/end
@ -458,12 +347,16 @@ class _RemotePageState extends State<RemotePage>
if (e is PointerScrollEvent) { if (e is PointerScrollEvent) {
var dx = e.scrollDelta.dx.toInt(); var dx = e.scrollDelta.dx.toInt();
var dy = e.scrollDelta.dy.toInt(); var dy = e.scrollDelta.dy.toInt();
if (dx > 0) if (dx > 0) {
dx = -1; dx = -1;
else if (dx < 0) dx = 1; } else if (dx < 0) {
if (dy > 0) dx = 1;
}
if (dy > 0) {
dy = -1; dy = -1;
else if (dy < 0) dy = 1; } else if (dy < 0) {
dy = 1;
}
bind.sessionSendMouse( bind.sessionSendMouse(
id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}');
} }
@ -546,106 +439,6 @@ class _RemotePageState extends State<RemotePage>
return out; return out;
} }
void showActions(String id, FfiModel ffiModel) async {
final size = MediaQuery.of(context).size;
final x = 120.0;
final y = size.height - super.widget.tabBarHeight;
final more = <PopupMenuItem<String>>[];
final pi = _ffi.ffiModel.pi;
final perms = _ffi.ffiModel.permissions;
if (pi.version.isNotEmpty) {
more.add(PopupMenuItem<String>(
child: Text(translate('Refresh')), value: 'refresh'));
}
more.add(PopupMenuItem<String>(
child: Row(
children: ([
Text(translate('OS Password')),
TextButton(
style: flatButtonStyle,
onPressed: () {
showSetOSPassword(widget.id, false, _ffi.dialogManager);
},
child: Icon(Icons.edit, color: MyTheme.accent),
)
])),
value: 'enter_os_password'));
if (!isWebDesktop) {
if (perms['keyboard'] != false && perms['clipboard'] != false) {
more.add(PopupMenuItem<String>(
child: Text(translate('Paste')), value: 'paste'));
}
more.add(PopupMenuItem<String>(
child: Text(translate('Reset canvas')), value: 'reset_canvas'));
}
if (perms['keyboard'] != false) {
if (pi.platform == 'Linux' || pi.sasEnabled) {
more.add(PopupMenuItem<String>(
child: Text(translate('Insert') + ' Ctrl + Alt + Del'),
value: 'cad'));
}
more.add(PopupMenuItem<String>(
child: Text(translate('Insert Lock')), value: 'lock'));
if (pi.platform == 'Windows' &&
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
true) {
more.add(PopupMenuItem<String>(
child: Text(translate(
(ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')),
value: 'block-input'));
}
}
if (gFFI.ffiModel.permissions["restart"] != false &&
(pi.platform == "Linux" ||
pi.platform == "Windows" ||
pi.platform == "Mac OS")) {
more.add(PopupMenuItem<String>(
child: Text(translate('Restart Remote Device')), value: 'restart'));
}
() async {
var value = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
items: more,
elevation: 8,
);
if (value == 'cad') {
bind.sessionCtrlAltDel(id: widget.id);
} else if (value == 'lock') {
bind.sessionLockScreen(id: widget.id);
} else if (value == 'block-input') {
bind.sessionToggleOption(
id: widget.id,
value: (_ffi.ffiModel.inputBlocked ? 'un' : '') + 'block-input');
_ffi.ffiModel.inputBlocked = !_ffi.ffiModel.inputBlocked;
} else if (value == 'refresh') {
bind.sessionRefresh(id: widget.id);
} else if (value == 'paste') {
() async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
bind.sessionInputString(id: widget.id, value: data.text ?? "");
}
}();
} else if (value == 'enter_os_password') {
// FIXME:
// TODO icon diff
// null means no session of id
// empty string means no password
var password = await bind.sessionGetOption(id: id, arg: "os-password");
if (password != null) {
bind.sessionInputOsPassword(id: widget.id, value: password);
} else {
showSetOSPassword(widget.id, true, _ffi.dialogManager);
}
} else if (value == 'reset_canvas') {
_ffi.cursorModel.reset();
} else if (value == 'restart') {
showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager);
}
}();
}
@override @override
void onWindowEvent(String eventName) { void onWindowEvent(String eventName) {
print("window event: $eventName"); print("window event: $eventName");
@ -676,7 +469,7 @@ class ImagePaint extends StatelessWidget {
{Key? key, {Key? key,
required this.id, required this.id,
required this.cursorOverImage, required this.cursorOverImage,
this.listenerBuilder = null}) this.listenerBuilder})
: super(key: key); : super(key: key);
@override @override
@ -855,177 +648,7 @@ class QualityMonitor extends StatelessWidget {
], ],
), ),
) )
: SizedBox.shrink()))); : const SizedBox.shrink())));
}
void showOptions(String id) async {
final _ffi = ffi(id);
String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced';
if (quality == '') quality = 'balanced';
String viewStyle =
await bind.sessionGetOption(id: id, arg: 'view-style') ?? '';
String scrollStyle =
await bind.sessionGetOption(id: id, arg: 'scroll-style') ?? '';
var displays = <Widget>[];
final pi = _ffi.ffiModel.pi;
final image = _ffi.ffiModel.getConnectionImage();
if (image != null)
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
if (pi.displays.length > 1) {
final cur = pi.currentDisplay;
final children = <Widget>[];
for (var i = 0; i < pi.displays.length; ++i)
children.add(InkWell(
onTap: () {
if (i == cur) return;
bind.sessionSwitchDisplay(id: id, value: i);
_ffi.dialogManager.dismissAll();
},
child: Ink(
width: 40,
height: 40,
decoration: BoxDecoration(
border: Border.all(color: Colors.black87),
color: i == cur ? Colors.black87 : Colors.white),
child: Center(
child: Text((i + 1).toString(),
style: TextStyle(
color: i == cur ? Colors.white : Colors.black87))))));
displays.add(Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 8,
children: children,
)));
}
if (displays.isNotEmpty) {
displays.add(Divider(color: MyTheme.border));
}
final perms = _ffi.ffiModel.permissions;
_ffi.dialogManager.show((setState, close) {
final more = <Widget>[];
if (perms['audio'] != false) {
more.add(getToggle(id, setState, 'disable-audio', 'Mute'));
}
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false)
more.add(
getToggle(id, setState, 'disable-clipboard', 'Disable clipboard'));
more.add(getToggle(
id, setState, 'lock-after-session-end', 'Lock after session end'));
if (pi.platform == 'Windows') {
more.add(Consumer<FfiModel>(
builder: (_context, _ffiModel, _child) => () {
return getToggle(
id, setState, 'privacy-mode', 'Privacy mode');
}()));
}
}
var setQuality = (String? value) {
if (value == null) return;
setState(() {
quality = value;
bind.sessionSetImageQuality(id: id, value: value);
});
};
var setViewStyle = (String? value) {
if (value == null) return;
setState(() {
viewStyle = value;
bind.sessionPeerOption(id: id, name: "view-style", value: value);
_ffi.canvasModel.updateViewStyle();
});
};
var setScrollStyle = (String? value) {
if (value == null) return;
setState(() {
scrollStyle = value;
bind.sessionPeerOption(id: id, name: "scroll-style", value: value);
_ffi.canvasModel.updateScrollStyle();
});
};
return CustomAlertDialog(
title: SizedBox.shrink(),
content: Column(
mainAxisSize: MainAxisSize.min,
children: displays +
<Widget>[
getRadio('Original', 'original', viewStyle, setViewStyle),
getRadio('Shrink', 'shrink', viewStyle, setViewStyle),
getRadio('Stretch', 'stretch', viewStyle, setViewStyle),
Divider(color: MyTheme.border),
getRadio(
'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle),
getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle),
Divider(color: MyTheme.border),
getRadio('Good image quality', 'best', quality, setQuality),
getRadio('Balanced', 'balanced', quality, setQuality),
getRadio('Optimize reaction time', 'low', quality, setQuality),
Divider(color: MyTheme.border),
getToggle(
id, setState, 'show-remote-cursor', 'Show remote cursor'),
getToggle(id, setState, 'show-quality-monitor',
'Show quality monitor',
ffi: _ffi),
] +
more),
actions: [],
contentPadding: 0,
);
}, clickMaskDismiss: true, backDismiss: true);
}
void showSetOSPassword(
String id, bool login, OverlayDialogManager dialogManager) async {
final controller = TextEditingController();
var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? "";
var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != "";
controller.text = password;
dialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('OS Password')),
content: Column(mainAxisSize: MainAxisSize.min, children: [
PasswordWidget(controller: controller),
CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate('Auto Login'),
),
value: autoLogin,
onChanged: (v) {
if (v == null) return;
setState(() => autoLogin = v);
},
),
]),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () {
close();
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: () {
var text = controller.text.trim();
bind.sessionPeerOption(id: id, name: "os-password", value: text);
bind.sessionPeerOption(
id: id, name: "auto-login", value: autoLogin ? 'Y' : '');
if (text != "" && login) {
bind.sessionInputOsPassword(id: id, value: text);
}
close();
},
child: Text(translate('OK')),
),
]);
});
} }
void sendPrompt(String id, bool isMac, String key) { void sendPrompt(String id, bool isMac, String key) {

View File

@ -15,7 +15,7 @@ class _PopupMenuTheme {
static const Color commonColor = MyTheme.accent; static const Color commonColor = MyTheme.accent;
// kMinInteractiveDimension // kMinInteractiveDimension
static const double height = 25.0; static const double height = 25.0;
static const double dividerHeight = 12.0; static const double dividerHeight = 3.0;
} }
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>> typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
@ -46,6 +46,7 @@ class _PeerCard extends StatefulWidget {
/// State for the connection page. /// State for the connection page.
class _PeerCardState extends State<_PeerCard> class _PeerCardState extends State<_PeerCard>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
var _menuPos = RelativeRect.fill;
final double _cardRadis = 20; final double _cardRadis = 20;
final double _borderWidth = 2; final double _borderWidth = 2;
final RxBool _iconMoreHover = false.obs; final RxBool _iconMoreHover = false.obs;
@ -253,36 +254,36 @@ class _PeerCardState extends State<_PeerCard>
); );
} }
Widget _actionMore(Peer peer) { Widget _actionMore(Peer peer) => Listener(
return FutureBuilder( onPointerDown: (e) {
future: widget.popupMenuEntryBuilder(context), final x = e.position.dx;
initialData: const <mod_menu.PopupMenuEntry<String>>[], final y = e.position.dy;
builder: (BuildContext context, _menuPos = RelativeRect.fromLTRB(x, y, x, y);
AsyncSnapshot<List<mod_menu.PopupMenuEntry<String>>> snapshot) { },
if (snapshot.hasData) { onPointerUp: (_) => _showPeerMenu(context, peer.id),
return Listener( child: MouseRegion(
child: MouseRegion( onEnter: (_) => _iconMoreHover.value = true,
onEnter: (_) => _iconMoreHover.value = true, onExit: (_) => _iconMoreHover.value = false,
onExit: (_) => _iconMoreHover.value = false, child: CircleAvatar(
child: CircleAvatar( radius: 14,
radius: 14, backgroundColor: _iconMoreHover.value
backgroundColor: _iconMoreHover.value ? MyTheme.color(context).grayBg!
? MyTheme.color(context).grayBg! : MyTheme.color(context).bg!,
: MyTheme.color(context).bg!, child: Icon(Icons.more_vert,
child: mod_menu.PopupMenuButton( size: 18,
padding: EdgeInsets.zero, color: _iconMoreHover.value
icon: Icon(Icons.more_vert, ? MyTheme.color(context).text
size: 18, : MyTheme.color(context).lightText))));
color: _iconMoreHover.value
? MyTheme.color(context).text /// Show the peer menu and handle user's choice.
: MyTheme.color(context).lightText), /// User might remove the peer or send a file to the peer.
position: mod_menu.PopupMenuPosition.over, void _showPeerMenu(BuildContext context, String id) async {
itemBuilder: (BuildContext context) => snapshot.data!, await mod_menu.showMenu(
)))); context: context,
} else { position: _menuPos,
return Container(); items: await super.widget.popupMenuEntryBuilder(context),
} elevation: 8,
}); );
} }
/// Get the image for the current [platform]. /// Get the image for the current [platform].
@ -411,19 +412,26 @@ abstract class BasePeerCard extends StatelessWidget {
@protected @protected
MenuEntryBase<String> _rdpAction(BuildContext context, String id) { MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
return MenuEntryButton<String>( return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Row( childBuilder: (TextStyle? style) => Container(
children: [ alignment: AlignmentDirectional.center,
Text( height: _PopupMenuTheme.height,
translate('RDP'), child: Row(
style: style, children: [
), Text(
SizedBox(width: 20), translate('RDP'),
IconButton( style: style,
icon: Icon(Icons.edit), ),
onPressed: () => _rdpDialog(id), Expanded(
) child: Align(
], alignment: Alignment.centerRight,
), child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(Icons.edit),
onPressed: () => _rdpDialog(id),
),
))
],
)),
proc: () { proc: () {
_connect(context, id, isRDP: true); _connect(context, id, isRDP: true);
}, },
@ -614,6 +622,7 @@ class RecentPeerCard extends BasePeerCard {
if (peer.platform == 'Windows') { if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id)); menuItems.add(_rdpAction(context, peer.id));
} }
menuItems.add(MenuEntryDivider());
menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id));
menuItems.add(_renameAction(peer.id, false)); menuItems.add(_renameAction(peer.id, false));
menuItems.add(_removeAction(peer.id, () async { menuItems.add(_removeAction(peer.id, () async {

View File

@ -472,9 +472,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}); });
final quality = final quality =
await bind.sessionGetCustomImageQuality(id: widget.id); await bind.sessionGetCustomImageQuality(id: widget.id);
final double initValue = quality != null && quality.isNotEmpty double initValue = quality != null && quality.isNotEmpty
? quality[0].toDouble() ? quality[0].toDouble()
: 50.0; : 50.0;
const minValue = 10.0;
const maxValue = 100.0;
if (initValue < minValue) {
initValue = minValue;
}
if (initValue > maxValue) {
initValue = maxValue;
}
final RxDouble sliderValue = RxDouble(initValue); final RxDouble sliderValue = RxDouble(initValue);
final rxReplay = rxdart.ReplaySubject<double>(); final rxReplay = rxdart.ReplaySubject<double>();
rxReplay rxReplay
@ -489,10 +497,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
final slider = Obx(() { final slider = Obx(() {
return Slider( return Slider(
value: sliderValue.value, value: sliderValue.value,
min: 10.0, min: minValue,
max: 100.0, max: maxValue,
divisions: 90, divisions: 90,
// label: sliderValue.value.round().toString(),
onChanged: (double value) { onChanged: (double value) {
sliderValue.value = value; sliderValue.value = value;
rxReplay.add(value); rxReplay.add(value);
@ -502,7 +509,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
final content = Row( final content = Row(
children: [ children: [
slider, slider,
Obx(() => Text('${sliderValue.value.round()}% Bitrate')) SizedBox(
width: 90,
child: Obx(() => Text(
'${sliderValue.value.round()}% Bitrate',
style: const TextStyle(fontSize: 15),
)))
], ],
); );
msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality',