From 4faf0a3d35874cce32283ab71566ce07b47a98a0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 19 Aug 2022 15:44:19 +0800 Subject: [PATCH 1/2] check super permission: win && linux Signed-off-by: 21pages --- flutter/lib/common.dart | 2 + .../desktop/pages/desktop_setting_page.dart | 235 +++++++++++++----- .../lib/desktop/widgets/tabbar_widget.dart | 1 - src/flutter_ffi.rs | 16 +- src/platform/linux.rs | 6 + src/platform/windows.rs | 19 +- src/ui_interface.rs | 7 + 7 files changed, 208 insertions(+), 78 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 63be444e1..6e3ec7020 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -52,6 +52,8 @@ 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, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 65c7ae819..9be269370 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -253,28 +253,47 @@ class _Safety extends StatefulWidget { class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + bool locked = true; @override Widget build(BuildContext context) { super.build(context); return ListView( children: [ - permissions(), - password(), - whitelist(), + Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + permissions(), + password(), + whitelist(), + ]), + ), + ], + ) ], ).marginOnly(bottom: _kListViewBottomMargin); } Widget permissions() { + bool enabled = !locked; return _Card(title: 'Permissions', children: [ - _OptionCheckBox('Enable Keyboard/Mouse', 'enable-keyboard'), - _OptionCheckBox('Enable Clipboard', 'enable-clipboard'), - _OptionCheckBox('Enable File Transfer', 'enable-file-transfer'), - _OptionCheckBox('Enable Audio', 'enable-audio'), - _OptionCheckBox('Enable Remote Restart', 'enable-remote-restart'), + _OptionCheckBox('Enable Keyboard/Mouse', 'enable-keyboard', + enabled: enabled), + _OptionCheckBox('Enable Clipboard', 'enable-clipboard', enabled: enabled), + _OptionCheckBox('Enable File Transfer', 'enable-file-transfer', + enabled: enabled), + _OptionCheckBox('Enable Audio', 'enable-audio', enabled: enabled), + _OptionCheckBox('Enable Remote Restart', 'enable-remote-restart', + enabled: enabled), _OptionCheckBox('Enable remote configuration modification', - 'allow-remote-config-modification'), + 'allow-remote-config-modification', + enabled: enabled), ]); } @@ -297,15 +316,17 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { String currentValue = values[keys.indexOf(model.verificationMethod)]; List radios = values .map((value) => _Radio( - value: value, - groupValue: currentValue, - label: value, - onChanged: ((value) { - model.verificationMethod = keys[values.indexOf(value)]; - }))) + value: value, + groupValue: currentValue, + label: value, + onChanged: ((value) { + model.verificationMethod = keys[values.indexOf(value)]; + }), + enabled: !locked, + )) .toList(); - var onChanged = tmp_enabled + var onChanged = tmp_enabled && !locked ? (value) { if (value != null) model.temporaryPasswordLength = value.toString(); @@ -319,7 +340,11 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { value: value, groupValue: model.temporaryPasswordLength, onChanged: onChanged), - Text(value), + Text( + value, + style: TextStyle( + color: _disabledTextColor(onChanged != null)), + ), ], ).paddingSymmetric(horizontal: 10), onTap: () => onChanged?.call(value), @@ -335,10 +360,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ...lengthRadios, ], ), - enabled: tmp_enabled), + enabled: tmp_enabled && !locked), radios[1], - _SubButton( - 'Set permanent password', setPasswordDialog, perm_enabled), + _SubButton('Set permanent password', setPasswordDialog, + perm_enabled && !locked), radios[2], ]); }))); @@ -346,7 +371,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget whitelist() { return _Card(title: 'IP Whitelisting', children: [ - _Button('IP Whitelisting', changeWhiteList, tip: 'whitelist_tip') + _Button('IP Whitelisting', changeWhiteList, + tip: 'whitelist_tip', enabled: !locked) ]); } } @@ -362,31 +388,46 @@ class _ConnectionState extends State<_Connection> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + bool locked = true; @override Widget build(BuildContext context) { super.build(context); - return ListView( - children: [ - _Card(title: 'Server', children: [ - _Button('ID/Relay Server', changeServer), - ]), - _Card(title: 'Service', children: [ - _OptionCheckBox('Enable Service', 'stop-service', reverse: true), - // TODO: Not implemented - // _option_check('Always connected via relay', 'allow-always-relay'), - // _option_check('Start ID/relay service', 'stop-rendezvous-service', - // reverse: true), - ]), - _Card(title: 'TCP Tunneling', children: [ - _OptionCheckBox('Enable TCP Tunneling', 'enable-tunnel'), - ]), - direct_ip(), - _Card(title: 'Proxy', children: [ - _Button('Socks5 Proxy', changeSocks5Proxy), - ]), - ], - ).marginOnly(bottom: _kListViewBottomMargin); + bool enabled = !locked; + return ListView(children: [ + Column( + children: [ + _lock(locked, 'Unlock Connection Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + _Card(title: 'Server', children: [ + _Button('ID/Relay Server', changeServer, enabled: enabled), + ]), + _Card(title: 'Service', children: [ + _OptionCheckBox('Enable Service', 'stop-service', + reverse: true, enabled: enabled), + // TODO: Not implemented + // _option_check('Always connected via relay', 'allow-always-relay', enabled: enabled), + // _option_check('Start ID/relay service', 'stop-rendezvous-service', + // reverse: true, enabled: enabled), + ]), + _Card(title: 'TCP Tunneling', children: [ + _OptionCheckBox('Enable TCP Tunneling', 'enable-tunnel', + enabled: enabled), + ]), + direct_ip(), + _Card(title: 'Proxy', children: [ + _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), + ]), + ]), + ), + ], + ) + ]).marginOnly(bottom: _kListViewBottomMargin); } Widget direct_ip() { @@ -395,7 +436,7 @@ class _ConnectionState extends State<_Connection> RxBool apply_enabled = false.obs; return _Card(title: 'Direct IP Access', children: [ _OptionCheckBox('Enable Direct IP Access', 'direct-server', - update: update), + update: update, enabled: !locked), _futureBuilder( future: () async { String enabled = await bind.mainGetOption(key: 'direct-server'); @@ -414,7 +455,7 @@ class _ConnectionState extends State<_Connection> width: 80, child: TextField( controller: controller, - enabled: enabled, + enabled: enabled && !locked, onChanged: (_) => apply_enabled.value = true, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp( @@ -429,10 +470,10 @@ class _ConnectionState extends State<_Connection> ), ), ), - enabled: enabled, - ), + enabled: enabled && !locked, + ).marginOnly(left: 5), Obx(() => ElevatedButton( - onPressed: apply_enabled.value && enabled + onPressed: apply_enabled.value && enabled && !locked ? () async { apply_enabled.value = false; await bind.mainSetOption( @@ -440,7 +481,9 @@ class _ConnectionState extends State<_Connection> value: controller.text); } : null, - child: Text(translate('Apply')), + child: Text( + translate('Apply'), + ), ).marginOnly(left: 20)) ]); }, @@ -700,8 +743,16 @@ Widget _Card({required String title, required List children}) { ); } +Color? _disabledTextColor(bool enabled) { + return enabled + ? null + : isDarkTheme() + ? MyTheme.disabledTextDark + : MyTheme.disabledTextLight; +} + Widget _OptionCheckBox(String label, String key, - {Function()? update = null, bool reverse = false}) { + {Function()? update = null, bool reverse = false, bool enabled = true}) { return _futureBuilder( future: bind.mainGetOption(key: key), hasData: (data) { @@ -721,9 +772,14 @@ Widget _OptionCheckBox(String label, String key, child: Obx( () => Row( children: [ - Checkbox(value: ref.value, onChanged: onChanged) + Checkbox( + value: ref.value, onChanged: enabled ? onChanged : null) .marginOnly(right: 10), - Expanded(child: Text(translate(label))) + Expanded( + child: Text( + translate(label), + style: TextStyle(color: _disabledTextColor(enabled)), + )) ], ), ).marginOnly(left: _kCheckBoxLeftMargin), @@ -734,29 +790,33 @@ Widget _OptionCheckBox(String label, String key, }); } -Widget _Radio({ - required T value, - required T groupValue, - required String label, - required Function(T value) onChanged, -}) { - var on_change = (T? value) { - if (value != null) { - onChanged(value); - } - }; +Widget _Radio( + {required T value, + required T groupValue, + required String label, + required Function(T value) onChanged, + bool enabled = true}) { + var on_change = enabled + ? (T? value) { + if (value != null) { + onChanged(value); + } + } + : null; return GestureDetector( child: Row( children: [ Radio(value: value, groupValue: groupValue, onChanged: on_change), Expanded( child: Text(translate(label), - style: TextStyle(fontSize: _kContentFontSize)) + style: TextStyle( + fontSize: _kContentFontSize, + color: _disabledTextColor(enabled))) .marginOnly(left: 5), ), ], ).marginOnly(left: _kRadioLeftMargin), - onTap: () => on_change(value), + onTap: () => on_change?.call(value), ); } @@ -808,19 +868,19 @@ Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { decoration: BoxDecoration( border: Border.all( color: hover.value && enabled - ? Colors.grey.withOpacity(0.8) - : Colors.grey.withOpacity(0.5), + ? Color(0xFFD7D7D7) + : Color(0xFFCBCBCB), width: hover.value && enabled ? 2 : 1)), child: Row( children: [ Container( height: 28, color: (hover.value && enabled) - ? Colors.grey.withOpacity(0.8) - : Colors.grey.withOpacity(0.5), + ? Color(0xFFD7D7D7) + : Color(0xFFCBCBCB), child: Text( label + ': ', - style: TextStyle(), + style: TextStyle(fontWeight: FontWeight.w300), ), alignment: Alignment.center, padding: @@ -851,6 +911,43 @@ Widget _futureBuilder( }); } +Widget _lock( + bool locked, + String label, + Function() onUnlock, +) { + return Offstage( + offstage: !locked, + child: Row( + children: [ + Container( + width: _kCardFixedWidth, + child: Card( + child: ElevatedButton( + child: Container( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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 keys; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index d2acb87ad..094659251 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -70,7 +70,6 @@ class DesktopTabBar extends StatelessWidget { super(key: key) { scrollController.itemCount = tabs.length; WidgetsBinding.instance.addPostFrameCallback((_) { - debugPrint("callback"); scrollController.scrollToItem(selected.value, center: true, animate: true); }); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d3560ba4a..53e3f1ff8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -22,12 +22,12 @@ use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ - discover, forget_password, get_api_server, get_app_name, get_async_job_status, - get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option, - get_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, - get_version, has_hwcodec, has_rendezvous_service, post_request, set_local_option, set_option, - set_options, set_peer_option, set_permanent_password, set_socks, store_fav, - test_if_valid_server, update_temporary_password, using_public_server, + check_super_user_permission, discover, forget_password, get_api_server, get_app_name, + get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, + get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, + get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request, + set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, + store_fav, test_if_valid_server, update_temporary_password, using_public_server, }; fn initialize(app_dir: &str) { @@ -735,6 +735,10 @@ pub fn main_set_permanent_password(password: String) { set_permanent_password(password); } +pub fn main_check_super_user_permission() -> bool { + check_super_user_permission() +} + pub fn cm_send_chat(conn_id: i32, msg: String) { connection_manager::send_chat(conn_id, msg); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 85947a143..0ead52f31 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -629,3 +629,9 @@ extern "C" { pub fn quit_gui() { unsafe { gtk_main_quit() }; } + +pub fn check_super_user_permission() -> ResultType { + // TODO: replace echo with a rustdesk's program, which is location-fixed and non-gui. + let status = std::process::Command::new("pkexec").arg("echo").status()?; + Ok(status.success() && status.code() == Some(0)) +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index cb0fd778f..fa9fb5b10 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -8,7 +8,7 @@ use hbb_common::{ }; use std::io::prelude::*; use std::{ - ffi::OsString, + ffi::{CString, OsString}, fs, io, mem, sync::{Arc, Mutex}, time::{Duration, Instant}, @@ -17,7 +17,8 @@ use winapi::{ shared::{minwindef::*, ntdef::NULL, windef::*}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, minwinbase::STILL_ACTIVE, - processthreadsapi::GetExitCodeProcess, winbase::*, wingdi::*, winnt::HANDLE, winuser::*, + processthreadsapi::GetExitCodeProcess, shellapi::ShellExecuteA, winbase::*, wingdi::*, + winnt::HANDLE, winuser::*, }, }; use windows_service::{ @@ -1418,3 +1419,17 @@ pub fn get_user_token(session_id: u32, as_user: bool) -> HANDLE { } } } + +pub fn check_super_user_permission() -> ResultType { + unsafe { + let ret = ShellExecuteA( + NULL as _, + CString::new("runas")?.as_ptr() as _, + CString::new("cmd")?.as_ptr() as _, + CString::new("/c /q")?.as_ptr() as _, + NULL as _, + SW_SHOWNORMAL, + ); + return Ok(ret as i32 > 32); + } +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index d45b83b75..f59f96090 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -676,6 +676,13 @@ pub fn has_hwcodec() -> bool { return true; } +pub fn check_super_user_permission() -> bool { + #[cfg(any(windows, target_os = "linux"))] + return crate::platform::check_super_user_permission().unwrap_or(false); + #[cfg(not(any(windows, target_os = "linux")))] + true +} + pub fn check_zombie(childs: Childs) { let mut deads = Vec::new(); loop { From a10487c8401c4592650e2bd22e8b7821a3a04603 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 20 Aug 2022 19:57:16 +0800 Subject: [PATCH 2/2] native style Signed-off-by: 21pages --- flutter/assets/tabbar.ttf | Bin 0 -> 2288 bytes flutter/lib/common.dart | 115 ++++- flutter/lib/consts.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 315 ++++++++------ .../desktop/pages/desktop_setting_page.dart | 85 ++-- .../lib/desktop/pages/desktop_tab_page.dart | 53 +-- .../lib/desktop/pages/file_manager_page.dart | 1 + flutter/lib/desktop/pages/remote_page.dart | 6 +- .../lib/desktop/widgets/tabbar_widget.dart | 399 ++++++++++-------- flutter/pubspec.yaml | 3 + 10 files changed, 603 insertions(+), 376 deletions(-) create mode 100644 flutter/assets/tabbar.ttf diff --git a/flutter/assets/tabbar.ttf b/flutter/assets/tabbar.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a9220f348fb303a4c064717c2b0543a5a05a44ae GIT binary patch literal 2288 zcmd^BOK%%h6h3#xk9gcXoN?U5DPP zz2AA<^SbjeF(UF)l|&kwzqE8#xz>M`h&=`A;tQ8X6VtsHfZ6-tUrQHLr3*j3vq2=Z zK(?JNY_3$V&4!4?KOl?c)Kn&--S`u>4?t5n2)y^jcY(hK#d1Ys;}`(;{*LKbK~JaH zkHQ)3e*nItnA#|@AWcBu4PMbwMYZF`^lc(f5dO0zy<&WN^Vcj<%Nx+YPRL!XHmdZp z3oH?4Ud{gDwIq-}85@7))%NMX$Zy;JGWf+m+|XVd_go|ngKZ(f+0H$AX%KzkgG(h` zL=DbjaJ@uhbcTdy=N1;Jt&lP_3iBka!S(wwE9A2&__ud5=(v_a!pP%DT`poDk{jXP z6TXA&1AApVlwjB?klW4%s)xsmE9WzG$Ml#vs~;r(OjTI$9QKC510F_DA+A;JN!yIQ z>FnQtlzTdf6&--wMGN4MSlx1&UejluFf(SZf!-j8eH^Up!C>9yTs#RkYGx=1^)O!j z|9u>>8t{_S26V)9h##2gwTYjQ>9dL7Gv*1K`01D#oA{k-=4`S~le;`kIr@aYVZ=IZ znfS5r7M}W}6h;3deFMp$^U=hd=x1ZDU8gE{4#>E`$z50{y6>3nid`C?Yu!9T%5t zi^A>NLU%OUEqvJ%jrIuJwI$)!OtfcDXgz6-d*Us;bwrYB;PKv0!TS?zVlonlc1_p; zggdouVYv2vxW7Lvj9~tV;~8OS=4?0?6K0?tn&}Vk8GIfK&puPzh79I0r(fG_3vV}) zaq7`XU-CI|Rbi2TuSeL{>@4qUC*8L#fn|`@Wy~$$Y%E}ZuY*O%`yA{79&)f7_gKQg z9>|||utdXj-obu&ETIR?g*zL4c-z5@I_NV83wU#Ut{KhsqMBVRq{>aUxh|{aN?zBL@%UI%G_PuE zx#^~|o=q6Win3DHi^?4QsfB{Bl*;<5nl|D&!zi5|9p!PjJf7BzbdJiTQi=@B8B*vn zZDP(-nsm}=#hQ&r_=~X4(i#=8v;2tm;O=Eum#G3f?o*+0isN&2&}tr5nq_$~n+mNX zS^_y5$VdT|;i(8T=j5j%lLD}U1LV3@_({V$j@J#}Rl { + 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.