diff --git a/flutter/assets/tabbar.ttf b/flutter/assets/tabbar.ttf new file mode 100644 index 000000000..a9220f348 Binary files /dev/null and b/flutter/assets/tabbar.ttf differ diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6e3ec7020..8570e5b7e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -8,7 +8,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; -import 'package:get/instance_manager.dart'; +import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; @@ -38,6 +38,88 @@ final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( final iconFile = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); +class IconFont { + static const _family = 'iconfont'; + IconFont._(); + + static const IconData max = IconData(0xe606, fontFamily: _family); + static const IconData restore = IconData(0xe607, fontFamily: _family); + static const IconData close = IconData(0xe668, fontFamily: _family); + static const IconData min = IconData(0xe609, fontFamily: _family); + static const IconData add = IconData(0xe664, fontFamily: _family); + static const IconData menu = IconData(0xe628, fontFamily: _family); +} + +class ColorThemeExtension extends ThemeExtension { + const ColorThemeExtension({ + required this.bg, + required this.grayBg, + required this.text, + required this.lightText, + required this.lighterText, + required this.border, + }); + + final Color? bg; + final Color? grayBg; + final Color? text; + final Color? lightText; + final Color? lighterText; + final Color? border; + + static const light = ColorThemeExtension( + bg: Color(0xFFFFFFFF), + grayBg: Color(0xFFEEEEEE), + text: Color(0xFF222222), + lightText: Color(0xFF666666), + lighterText: Color(0xFF888888), + border: Color(0xFFCCCCCC), + ); + + static const dark = ColorThemeExtension( + bg: Color(0xFF252525), + grayBg: Color(0xFF141414), + text: Color(0xFFFFFFFF), + lightText: Color(0xFF999999), + lighterText: Color(0xFF777777), + border: Color(0xFF555555), + ); + + @override + ThemeExtension copyWith( + {Color? bg, + Color? grayBg, + Color? text, + Color? lightText, + Color? lighterText, + Color? border}) { + return ColorThemeExtension( + bg: bg ?? this.bg, + grayBg: grayBg ?? this.grayBg, + text: text ?? this.text, + lightText: lightText ?? this.lightText, + lighterText: lighterText ?? this.lighterText, + border: border ?? this.border, + ); + } + + @override + ThemeExtension lerp( + ThemeExtension? other, double t) { + if (other is! ColorThemeExtension) { + return this; + } + return ColorThemeExtension( + bg: Color.lerp(bg, other.bg, t), + grayBg: Color.lerp(grayBg, other.grayBg, t), + text: Color.lerp(text, other.text, t), + lightText: Color.lerp(lightText, other.lightText, t), + lighterText: Color.lerp(lighterText, other.lighterText, t), + border: Color.lerp(border, other.border, t), + ); + } +} + class MyTheme { MyTheme._(); @@ -52,20 +134,37 @@ class MyTheme { static const Color darkGray = Color(0xFFB9BABC); static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; - static const Color disabledTextLight = Color(0xFF888888); - static const Color disabledTextDark = Color(0xFF777777); static ThemeData lightTheme = ThemeData( brightness: Brightness.light, primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, - tabBarTheme: TabBarTheme(labelColor: Colors.black87), + tabBarTheme: TabBarTheme( + labelColor: Colors.black87, + ), + // backgroundColor: Color(0xFFFFFFFF), + ).copyWith( + extensions: >[ + ColorThemeExtension.light, + ], ); static ThemeData darkTheme = ThemeData( - brightness: Brightness.dark, - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - tabBarTheme: TabBarTheme(labelColor: Colors.white70)); + brightness: Brightness.dark, + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: TabBarTheme( + labelColor: Colors.white70, + ), + // backgroundColor: Color(0xFF252525) + ).copyWith( + extensions: >[ + ColorThemeExtension.dark, + ], + ); + + static ColorThemeExtension color(BuildContext context) { + return Theme.of(context).extension()!; + } } bool isDarkTheme() { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 7b61c5b48..09e80b482 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,4 +1,4 @@ -const double kDesktopRemoteTabBarHeight = 48.0; +const double kDesktopRemoteTabBarHeight = 28.0; const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 407d38958..30fec849b 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -48,15 +48,16 @@ class _DesktopHomePageState extends State @override Widget build(BuildContext context) { + super.build(context); return Row( children: [ - Flexible( - child: buildServerInfo(context), - flex: 1, + buildServerInfo(context), + VerticalDivider( + width: 1, + thickness: 1, ), - Flexible( + Expanded( child: buildServerBoard(context), - flex: 4, ), ], ); @@ -66,6 +67,8 @@ class _DesktopHomePageState extends State return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Container( + width: 200, + color: MyTheme.color(context).bg, child: Column( children: [ buildTip(context), @@ -78,44 +81,48 @@ class _DesktopHomePageState extends State } buildServerBoard(BuildContext context) { - return Column( - children: [ - Expanded(child: ConnectionPage()), - ], + return Container( + color: MyTheme.color(context).grayBg, + child: ConnectionPage(), ); } buildIDBoard(BuildContext context) { final model = gFFI.serverModel; return Container( - margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + margin: EdgeInsets.symmetric(horizontal: 16), + height: 52, child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Container( - width: 3, - height: 70, + width: 2, decoration: BoxDecoration(color: MyTheme.accent), ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.only(left: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - translate("ID"), - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.w500), - ), - buildPopupMenu(context) - ], + Container( + height: 15, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translate("ID"), + style: TextStyle( + fontSize: 14, + color: MyTheme.color(context).lightText), + ), + buildPopupMenu(context) + ], + ), ), - GestureDetector( + Flexible( + child: GestureDetector( onDoubleTap: () { Clipboard.setData( ClipboardData(text: model.serverId.text)); @@ -124,7 +131,15 @@ class _DesktopHomePageState extends State child: TextFormField( controller: model.serverId, readOnly: true, - )), + decoration: InputDecoration( + border: InputBorder.none, + ), + style: TextStyle( + fontSize: 22, + ), + ).marginOnly(bottom: 5), + ), + ) ], ), ), @@ -136,116 +151,143 @@ class _DesktopHomePageState extends State Widget buildPopupMenu(BuildContext context) { var position; - return GestureDetector( - onTapDown: (detail) { - final x = detail.globalPosition.dx; - final y = detail.globalPosition.dy; - position = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () async { - final userName = await gFFI.userModel.getUserName(); - final enabledInput = await bind.mainGetOption(key: 'enable-audio'); - final defaultInput = await gFFI.getDefaultAudioInput(); - var menu = [ - await genEnablePopupMenuItem( - translate("Enable Keyboard/Mouse"), - 'enable-keyboard', + RxBool hover = false.obs; + return InkWell( + onTapDown: (detail) { + final x = detail.globalPosition.dx; + final y = detail.globalPosition.dy; + position = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + final userName = await gFFI.userModel.getUserName(); + final enabledInput = await bind.mainGetOption(key: 'enable-audio'); + final defaultInput = await gFFI.getDefaultAudioInput(); + var menu = [ + await genEnablePopupMenuItem( + translate("Enable Keyboard/Mouse"), + 'enable-keyboard', + ), + await genEnablePopupMenuItem( + translate("Enable Clipboard"), + 'enable-clipboard', + ), + await genEnablePopupMenuItem( + translate("Enable File Transfer"), + 'enable-file-transfer', + ), + await genEnablePopupMenuItem( + translate("Enable TCP Tunneling"), + 'enable-tunnel', + ), + genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), + PopupMenuDivider(), + PopupMenuItem( + child: Text(translate("ID/Relay Server")), + value: 'custom-server', + ), + PopupMenuItem( + child: Text(translate("IP Whitelisting")), + value: 'whitelist', + ), + PopupMenuItem( + child: Text(translate("Socks5 Proxy")), + value: 'socks5-proxy', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Enable Service"), + 'stop-service', + ), + // TODO: direct server + await genEnablePopupMenuItem( + translate("Always connected via relay"), + 'allow-always-relay', + ), + await genEnablePopupMenuItem( + translate("Start ID/relay service"), + 'stop-rendezvous-service', + ), + PopupMenuDivider(), + userName.isEmpty + ? PopupMenuItem( + child: Text(translate("Login")), + value: 'login', + ) + : PopupMenuItem( + child: Text("${translate("Logout")} $userName"), + value: 'logout', + ), + PopupMenuItem( + child: Text(translate("Change ID")), + value: 'change-id', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Dark Theme"), + 'allow-darktheme', + ), + PopupMenuItem( + child: Text(translate("About")), + value: 'about', + ), + ]; + final v = + await showMenu(context: context, position: position, items: menu); + if (v != null) { + onSelectMenu(v); + } + }, + child: Obx( + () => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(90), + boxShadow: [ + BoxShadow( + color: hover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + spreadRadius: 2) + ], + ), + child: Center( + child: Icon( + Icons.more_vert_outlined, + size: 20, + color: hover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, ), - await genEnablePopupMenuItem( - translate("Enable Clipboard"), - 'enable-clipboard', - ), - await genEnablePopupMenuItem( - translate("Enable File Transfer"), - 'enable-file-transfer', - ), - await genEnablePopupMenuItem( - translate("Enable TCP Tunneling"), - 'enable-tunnel', - ), - genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), - PopupMenuDivider(), - PopupMenuItem( - child: Text(translate("ID/Relay Server")), - value: 'custom-server', - ), - PopupMenuItem( - child: Text(translate("IP Whitelisting")), - value: 'whitelist', - ), - PopupMenuItem( - child: Text(translate("Socks5 Proxy")), - value: 'socks5-proxy', - ), - PopupMenuDivider(), - await genEnablePopupMenuItem( - translate("Enable Service"), - 'stop-service', - ), - // TODO: direct server - await genEnablePopupMenuItem( - translate("Always connected via relay"), - 'allow-always-relay', - ), - await genEnablePopupMenuItem( - translate("Start ID/relay service"), - 'stop-rendezvous-service', - ), - PopupMenuDivider(), - userName.isEmpty - ? PopupMenuItem( - child: Text(translate("Login")), - value: 'login', - ) - : PopupMenuItem( - child: Text("${translate("Logout")} $userName"), - value: 'logout', - ), - PopupMenuItem( - child: Text(translate("Change ID")), - value: 'change-id', - ), - PopupMenuDivider(), - await genEnablePopupMenuItem( - translate("Dark Theme"), - 'allow-darktheme', - ), - PopupMenuItem( - child: Text(translate("About")), - value: 'about', - ), - ]; - final v = - await showMenu(context: context, position: position, items: menu); - if (v != null) { - onSelectMenu(v); - } - }, - child: Icon(Icons.more_vert_outlined)); + ), + ), + ), + onHover: (value) => hover.value = value, + ); } buildPasswordBoard(BuildContext context) { final model = gFFI.serverModel; + RxBool refreshHover = false.obs; return Container( - margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + margin: EdgeInsets.symmetric(vertical: 12, horizontal: 16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Container( - width: 3, - height: 70, + width: 2, + height: 52, decoration: BoxDecoration(color: MyTheme.accent), ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.only(left: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( translate("Password"), - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 14, color: MyTheme.color(context).lightText), ), Row( children: [ @@ -262,12 +304,25 @@ class _DesktopHomePageState extends State child: TextFormField( controller: model.serverPasswd, readOnly: true, + decoration: InputDecoration( + border: InputBorder.none, + ), + style: TextStyle(fontSize: 15), ), ), ), - IconButton( - icon: Icon(Icons.refresh), - onPressed: () => bind.mainUpdateTemporaryPassword(), + InkWell( + child: Obx( + () => Icon( + Icons.refresh, + color: refreshHover.value + ? MyTheme.color(context).text + : Color(0xFFDDDDDD), + size: 22, + ).marginOnly(right: 5), + ), + onTap: () => bind.mainUpdateTemporaryPassword(), + onHover: (value) => refreshHover.value = value, ), FutureBuilder( future: buildPasswordPopupMenu(context), @@ -282,7 +337,7 @@ class _DesktopHomePageState extends State } }) ], - ), + ).marginOnly(bottom: 20), ], ), ), @@ -294,7 +349,8 @@ class _DesktopHomePageState extends State Future buildPasswordPopupMenu(BuildContext context) async { var position; - return GestureDetector( + RxBool editHover = false.obs; + return InkWell( onTapDown: (detail) { final x = detail.globalPosition.dx; final y = detail.globalPosition.dy; @@ -365,7 +421,12 @@ class _DesktopHomePageState extends State setPasswordDialog(); } }, - child: Icon(Icons.edit)); + onHover: (value) => editHover.value = value, + child: Obx(() => Icon(Icons.edit, + size: 22, + color: editHover.value + ? MyTheme.color(context).text + : Color(0xFFDDDDDD)))); } buildTip(BuildContext context) { @@ -377,7 +438,7 @@ class _DesktopHomePageState extends State children: [ Text( translate("Your Desktop"), - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), ), SizedBox( height: 8.0, @@ -385,7 +446,8 @@ class _DesktopHomePageState extends State Text( translate("desk_tip"), overflow: TextOverflow.clip, - style: TextStyle(fontSize: 14), + style: TextStyle( + fontSize: 12, color: MyTheme.color(context).lighterText), ) ], ), @@ -394,13 +456,17 @@ class _DesktopHomePageState extends State buildControlPanel(BuildContext context) { return Container( + width: 320, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: MyTheme.white), padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(translate("Control Remote Desktop")), + Text( + translate("Control Remote Desktop"), + style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), + ), Form( child: Column( children: [ @@ -409,6 +475,7 @@ class _DesktopHomePageState extends State inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) ], + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w400), ) ], )) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9be269370..7c87d7cb0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -71,6 +71,7 @@ class _DesktopSettingPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( + backgroundColor: MyTheme.color(context).bg, body: Row( children: [ Container( @@ -88,16 +89,19 @@ class _DesktopSettingPageState extends State ), const VerticalDivider(thickness: 1, width: 1), Expanded( - child: PageView( - controller: controller, - children: [ - _UserInterface(), - _Safety(), - _Display(), - _Audio(), - _Connection(), - _About(), - ], + child: Container( + color: MyTheme.color(context).grayBg, + child: PageView( + controller: controller, + children: [ + _UserInterface(), + _Safety(), + _Display(), + _Audio(), + _Connection(), + _About(), + ], + ), ), ) ], @@ -269,8 +273,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { AbsorbPointer( absorbing: locked, child: Column(children: [ - permissions(), - password(), + permissions(context), + password(context), whitelist(), ]), ), @@ -280,24 +284,26 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ).marginOnly(bottom: _kListViewBottomMargin); } - Widget permissions() { + Widget permissions(context) { bool enabled = !locked; return _Card(title: 'Permissions', children: [ - _OptionCheckBox('Enable Keyboard/Mouse', 'enable-keyboard', + _OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard', enabled: enabled), - _OptionCheckBox('Enable Clipboard', 'enable-clipboard', enabled: enabled), - _OptionCheckBox('Enable File Transfer', 'enable-file-transfer', + _OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard', enabled: enabled), - _OptionCheckBox('Enable Audio', 'enable-audio', enabled: enabled), - _OptionCheckBox('Enable Remote Restart', 'enable-remote-restart', + _OptionCheckBox(context, 'Enable File Transfer', 'enable-file-transfer', enabled: enabled), - _OptionCheckBox('Enable remote configuration modification', + _OptionCheckBox(context, 'Enable Audio', 'enable-audio', + enabled: enabled), + _OptionCheckBox(context, 'Enable Remote Restart', 'enable-remote-restart', + enabled: enabled), + _OptionCheckBox(context, 'Enable remote configuration modification', 'allow-remote-config-modification', enabled: enabled), ]); } - Widget password() { + Widget password(BuildContext context) { return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Consumer(builder: ((context, model, child) { @@ -316,6 +322,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { String currentValue = values[keys.indexOf(model.verificationMethod)]; List radios = values .map((value) => _Radio( + context, value: value, groupValue: currentValue, label: value, @@ -343,7 +350,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Text( value, style: TextStyle( - color: _disabledTextColor(onChanged != null)), + color: _disabledTextColor( + context, onChanged != null)), ), ], ).paddingSymmetric(horizontal: 10), @@ -408,7 +416,7 @@ class _ConnectionState extends State<_Connection> _Button('ID/Relay Server', changeServer, enabled: enabled), ]), _Card(title: 'Service', children: [ - _OptionCheckBox('Enable Service', 'stop-service', + _OptionCheckBox(context, 'Enable Service', 'stop-service', reverse: true, enabled: enabled), // TODO: Not implemented // _option_check('Always connected via relay', 'allow-always-relay', enabled: enabled), @@ -416,10 +424,11 @@ class _ConnectionState extends State<_Connection> // reverse: true, enabled: enabled), ]), _Card(title: 'TCP Tunneling', children: [ - _OptionCheckBox('Enable TCP Tunneling', 'enable-tunnel', + _OptionCheckBox( + context, 'Enable TCP Tunneling', 'enable-tunnel', enabled: enabled), ]), - direct_ip(), + direct_ip(context), _Card(title: 'Proxy', children: [ _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), ]), @@ -430,12 +439,12 @@ class _ConnectionState extends State<_Connection> ]).marginOnly(bottom: _kListViewBottomMargin); } - Widget direct_ip() { + Widget direct_ip(BuildContext context) { TextEditingController controller = TextEditingController(); var update = () => setState(() {}); RxBool apply_enabled = false.obs; return _Card(title: 'Direct IP Access', children: [ - _OptionCheckBox('Enable Direct IP Access', 'direct-server', + _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', update: update, enabled: !locked), _futureBuilder( future: () async { @@ -509,7 +518,7 @@ class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { return ListView( children: [ _Card(title: 'Adaptive Bitrate', children: [ - _OptionCheckBox('Adaptive Bitrate', 'enable-abr'), + _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), ]), hwcodec(), ], @@ -523,7 +532,8 @@ class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { return Offstage( offstage: !(data as bool), child: _Card(title: 'Hardware Codec', children: [ - _OptionCheckBox('Enable hardware codec', 'enable-hwcodec'), + _OptionCheckBox( + context, 'Enable hardware codec', 'enable-hwcodec'), ]), ); }); @@ -592,6 +602,7 @@ class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { ); deviceWidget.addAll([ _Radio<_AudioInputType>( + context, value: _AudioInputType.Specify, groupValue: groupValue, label: 'Specify device', @@ -606,6 +617,7 @@ class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { } return Column(children: [ _Radio<_AudioInputType>( + context, value: _AudioInputType.Mute, groupValue: groupValue, label: 'Mute', @@ -615,6 +627,7 @@ class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { }, ), _Radio( + context, value: _AudioInputType.Standard, groupValue: groupValue, label: 'Use standard device', @@ -743,15 +756,11 @@ Widget _Card({required String title, required List children}) { ); } -Color? _disabledTextColor(bool enabled) { - return enabled - ? null - : isDarkTheme() - ? MyTheme.disabledTextDark - : MyTheme.disabledTextLight; +Color? _disabledTextColor(BuildContext context, bool enabled) { + return enabled ? null : MyTheme.color(context).lighterText; } -Widget _OptionCheckBox(String label, String key, +Widget _OptionCheckBox(BuildContext context, String label, String key, {Function()? update = null, bool reverse = false, bool enabled = true}) { return _futureBuilder( future: bind.mainGetOption(key: key), @@ -778,7 +787,7 @@ Widget _OptionCheckBox(String label, String key, Expanded( child: Text( translate(label), - style: TextStyle(color: _disabledTextColor(enabled)), + style: TextStyle(color: _disabledTextColor(context, enabled)), )) ], ), @@ -790,7 +799,7 @@ Widget _OptionCheckBox(String label, String key, }); } -Widget _Radio( +Widget _Radio(BuildContext context, {required T value, required T groupValue, required String label, @@ -811,7 +820,7 @@ Widget _Radio( child: Text(translate(label), style: TextStyle( fontSize: _kContentFontSize, - color: _disabledTextColor(enabled))) + color: _disabledTextColor(context, enabled))) .marginOnly(left: 5), ), ], diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 5cbc7aece..45722174e 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -30,30 +30,35 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - DesktopTabBar( - tabs: tabs, - dark: isDarkTheme(), - mainTab: true, - onAddSetting: onAddSetting, - ), - Obx((() => Expanded( - child: PageView( - controller: DesktopTabBar.controller.value, - children: tabs.map((tab) { - switch (tab.label) { - case kTabLabelHomePage: - return DesktopHomePage(key: ValueKey(tab.label)); - case kTabLabelSettingPage: - return DesktopSettingPage(key: ValueKey(tab.label)); - default: - return Container(); - } - }).toList()), - ))), - ], + return Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Column( + children: [ + DesktopTabBar( + tabs: tabs, + dark: isDarkTheme(), + mainTab: true, + onAddSetting: onAddSetting, + ), + Obx((() => Expanded( + child: PageView( + controller: DesktopTabBar.controller.value, + children: tabs.map((tab) { + switch (tab.label) { + case kTabLabelHomePage: + return DesktopHomePage(key: ValueKey(tab.label)); + case kTabLabelSettingPage: + return DesktopSettingPage(key: ValueKey(tab.label)); + default: + return Container(); + } + }).toList()), + ))), + ], + ), ), ); } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0111e5f90..2868d2d3b 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -98,6 +98,7 @@ class _FileManagerPageState extends State return false; }, child: Scaffold( + backgroundColor: MyTheme.color(context).bg, body: Row( children: [ Flexible(flex: 3, child: body(isLocal: true)), diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 8aba86d0f..025db279f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -181,10 +181,11 @@ class _RemotePageState extends State _ffi.inputKey(label, down: down, press: press ?? false); } - Widget buildBody(FfiModel ffiModel) { + Widget buildBody(BuildContext context, FfiModel ffiModel) { final hasDisplays = ffiModel.pi.displays.length > 0; final keyboard = ffiModel.permissions['keyboard'] != false; return Scaffold( + backgroundColor: MyTheme.color(context).bg, // resizeToAvoidBottomInset: true, floatingActionButton: _showBar ? null @@ -229,7 +230,8 @@ class _RemotePageState extends State ChangeNotifierProvider.value(value: _ffi.canvasModel), ], child: Consumer( - builder: (context, ffiModel, _child) => buildBody(ffiModel)))); + builder: (context, ffiModel, _child) => + buildBody(context, ffiModel)))); } Widget getRawPointerAndKeyBody(Widget child) { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 094659251..bf39f4dc6 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -13,7 +13,7 @@ import 'package:scroll_pos/scroll_pos.dart'; const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kIconSize = 18; const double _kDividerIndent = 10; -const double _kAddIconSize = _kTabBarHeight - 15; +const double _kActionIconSize = 12; final _tabBarKey = GlobalKey(); void closeTab(String? id) { @@ -79,63 +79,81 @@ class DesktopTabBar extends StatelessWidget { Widget build(BuildContext context) { return Container( height: _kTabBarHeight, - child: Row( + child: Column( children: [ - Expanded( + Container( + height: _kTabBarHeight - 1, child: Row( children: [ - Offstage( - offstage: !mainTab, - child: Row(children: [ - Image.asset('assets/logo.ico'), - Text("RustDesk").paddingOnly(left: 5), - ]).paddingSymmetric(horizontal: 12, vertical: 5), - ), Expanded( - child: GestureDetector( - onPanStart: (_) { - if (mainTab) { - windowManager.startDragging(); - } else { - WindowController.fromWindowId(windowId!) - .startDragging(); - } - }, - child: _ListView( - key: _tabBarKey, - controller: controller, - scrollController: scrollController, - tabInfos: tabs, - selected: selected, - onTabClose: onTabClose, - theme: _theme)), + child: Row( + children: [ + Offstage( + offstage: !mainTab, + child: Row(children: [ + Image.asset( + 'assets/logo.ico', + width: 20, + height: 20, + ), + Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2), + ]).marginOnly( + left: 5, + right: 10, + ), + ), + Expanded( + child: GestureDetector( + onPanStart: (_) { + if (mainTab) { + windowManager.startDragging(); + } else { + WindowController.fromWindowId(windowId!) + .startDragging(); + } + }, + child: _ListView( + key: _tabBarKey, + controller: controller, + scrollController: scrollController, + tabInfos: tabs, + selected: selected, + onTabClose: onTabClose, + theme: _theme)), + ), + Offstage( + offstage: mainTab, + child: _AddButton( + theme: _theme, + ).paddingOnly(left: 10), + ), + ], + ), ), Offstage( - offstage: mainTab, - child: _AddButton( + offstage: onAddSetting == null, + child: _ActionIcon( + message: 'Settings', + icon: IconFont.menu, theme: _theme, - ).paddingOnly(left: 10), + onTap: () => onAddSetting?.call(), + is_close: false, + ), ), + WindowActionPanel( + mainTab: mainTab, + theme: _theme, + ) ], ), ), - Offstage( - offstage: onAddSetting == null, - child: Tooltip( - message: translate("Settings"), - child: InkWell( - child: Icon( - Icons.menu, - color: _theme.unSelectedIconColor, - ), - onTap: () => onAddSetting?.call(), - ).paddingOnly(right: 10), - ), + Divider( + height: 1, + thickness: 1, ), - WindowActionPanel( - mainTab: mainTab, - color: _theme.unSelectedIconColor, - ) ], ), ); @@ -156,85 +174,88 @@ class DesktopTabBar extends StatelessWidget { class WindowActionPanel extends StatelessWidget { final bool mainTab; - final Color color; + final _Theme theme; const WindowActionPanel( - {Key? key, required this.mainTab, required this.color}) + {Key? key, required this.mainTab, required this.theme}) : super(key: key); @override Widget build(BuildContext context) { return Row( children: [ - Tooltip( - message: translate("Minimize"), - child: InkWell( - child: Icon( - Icons.minimize, - color: color, - ).paddingSymmetric(horizontal: 5), - onTap: () { - if (mainTab) { - windowManager.minimize(); - } else { - WindowController.fromWindowId(windowId!).minimize(); - } - }, - ), + _ActionIcon( + message: 'Minimize', + icon: IconFont.min, + theme: theme, + onTap: () { + if (mainTab) { + windowManager.minimize(); + } else { + WindowController.fromWindowId(windowId!).minimize(); + } + }, + is_close: false, ), - Tooltip( - message: translate("Maximize"), - child: InkWell( - child: Icon( - Icons.rectangle_outlined, - color: color, - size: 20, - ).paddingSymmetric(horizontal: 5), - onTap: () { - if (mainTab) { - windowManager.isMaximized().then((maximized) { - if (maximized) { + FutureBuilder(builder: (context, snapshot) { + RxBool is_maximized = false.obs; + if (mainTab) { + windowManager.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } else { + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } + return Obx( + () => _ActionIcon( + message: is_maximized.value ? "Restore" : "Maximize", + icon: is_maximized.value ? IconFont.restore : IconFont.max, + theme: theme, + onTap: () { + if (mainTab) { + if (is_maximized.value) { windowManager.unmaximize(); } else { windowManager.maximize(); } - }); - } else { - final wc = WindowController.fromWindowId(windowId!); - wc.isMaximized().then((maximized) { - if (maximized) { + } else { + final wc = WindowController.fromWindowId(windowId!); + if (is_maximized.value) { wc.unmaximize(); } else { wc.maximize(); } - }); - } - }, - ), + } + is_maximized.value = !is_maximized.value; + }, + is_close: false, + ), + ); + }), + _ActionIcon( + message: 'Close', + icon: IconFont.close, + theme: theme, + onTap: () { + if (mainTab) { + windowManager.close(); + } else { + WindowController.fromWindowId(windowId!).close(); + } + }, + is_close: true, ), - Tooltip( - message: translate("Close"), - child: InkWell( - child: Icon( - Icons.close, - color: color, - ).paddingSymmetric(horizontal: 5), - onTap: () { - if (mainTab) { - windowManager.close(); - } else { - WindowController.fromWindowId(windowId!).close(); - } - }, - ), - ) ], ); } } +// ignore: must_be_immutable class _ListView extends StatelessWidget { - late Rx controller; + final Rx controller; final ScrollPosController scrollController; final RxList tabInfos; final Rx selected; @@ -327,74 +348,60 @@ class _Tab extends StatelessWidget { Widget build(BuildContext context) { bool is_selected = index == selected; bool show_divider = index != selected - 1 && index != selected; - return Stack( - children: [ - Ink( - child: InkWell( - onHover: (hover) => _hover.value = hover, - onTap: () => onSelected(), - child: Row( - children: [ - Container( - height: _kTabBarHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + return Ink( + child: InkWell( + onHover: (hover) => _hover.value = hover, + onTap: () => onSelected(), + child: Row( + children: [ + Container( + height: _kTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - is_selected ? selectedIcon : unselectedIcon, - size: _kIconSize, + Icon( + is_selected ? selectedIcon : unselectedIcon, + size: _kIconSize, + color: is_selected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5), + Text( + translate(label), + textAlign: TextAlign.center, + style: TextStyle( color: is_selected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, - ).paddingOnly(right: 5), - Text( - translate(label), - textAlign: TextAlign.center, - style: TextStyle( - color: is_selected - ? theme.selectedTextColor - : theme.unSelectedTextColor), - ), - ], + ? theme.selectedTextColor + : theme.unSelectedTextColor), ), - Offstage( - offstage: !closable, - child: Obx((() => _CloseButton( - visiable: _hover.value, - tabSelected: is_selected, - onClose: () => onClose(), - theme: theme, - ))), - ) - ])).paddingSymmetric(horizontal: 10), - Offstage( - offstage: !show_divider, - child: VerticalDivider( - width: 1, - indent: _kDividerIndent, - endIndent: _kDividerIndent, - color: theme.dividerColor, - thickness: 1, - ), - ) - ], - ), - ), + ], + ), + Offstage( + offstage: !closable, + child: Obx((() => _CloseButton( + visiable: _hover.value, + tabSelected: is_selected, + onClose: () => onClose(), + theme: theme, + ))), + ) + ])).paddingSymmetric(horizontal: 10), + Offstage( + offstage: !show_divider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: theme.dividerColor, + thickness: 1, + ), + ) + ], ), - Positioned( - height: 2, - left: 0, - right: 0, - bottom: 0, - child: Center( - child: Container( - color: - is_selected ? theme.indicatorColor : Colors.transparent), - )) - ], + ), ); } } @@ -409,19 +416,13 @@ class _AddButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Ink( - height: _kTabBarHeight, - child: InkWell( - customBorder: const CircleBorder(), + return _ActionIcon( + message: 'New Connection', + icon: IconFont.add, + theme: theme, onTap: () => rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), - child: Icon( - Icons.add_sharp, - size: _kAddIconSize, - color: theme.unSelectedIconColor, - ), - ), - ); + is_close: false); } } @@ -460,6 +461,46 @@ class _CloseButton extends StatelessWidget { } } +class _ActionIcon extends StatelessWidget { + final String message; + final IconData icon; + final _Theme theme; + final Function() onTap; + final bool is_close; + const _ActionIcon({ + Key? key, + required this.message, + required this.icon, + required this.theme, + required this.onTap, + required this.is_close, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + RxBool hover = false.obs; + return Obx(() => Tooltip( + message: translate(message), + child: InkWell( + hoverColor: is_close ? Colors.red : theme.hoverColor, + onHover: (value) => hover.value = value, + child: Container( + height: _kTabBarHeight - 1, + width: _kTabBarHeight - 1, + child: Icon( + icon, + color: hover.value && is_close + ? Colors.white + : theme.unSelectedIconColor, + size: _kActionIconSize, + ), + ), + onTap: onTap, + ), + )); + } +} + class _Theme { late Color unSelectedtabIconColor; late Color selectedtabIconColor; @@ -468,7 +509,7 @@ class _Theme { late Color selectedIconColor; late Color unSelectedIconColor; late Color dividerColor; - late Color indicatorColor; + late Color hoverColor; _Theme.light() { unSelectedtabIconColor = Color.fromARGB(255, 162, 203, 241); @@ -478,7 +519,7 @@ class _Theme { selectedIconColor = Color.fromARGB(255, 26, 26, 26); unSelectedIconColor = Color.fromARGB(255, 96, 96, 96); dividerColor = Color.fromARGB(255, 238, 238, 238); - indicatorColor = MyTheme.accent; + hoverColor = Colors.grey.withOpacity(0.2); } _Theme.dark() { @@ -489,6 +530,6 @@ class _Theme { selectedIconColor = Color.fromARGB(255, 215, 215, 215); unSelectedIconColor = Color.fromARGB(255, 255, 255, 255); dividerColor = Color.fromARGB(255, 64, 64, 64); - indicatorColor = MyTheme.accent; + hoverColor = Colors.black26; } } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ddbbb32b7..5a5e55a05 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -108,6 +108,9 @@ flutter: - family: GestureIcons fonts: - asset: assets/gestures.ttf + - family: IconFont + fonts: + - asset: assets/tabbar.ttf # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware.