From aeeffad33bf3eb4e98e732a56b66edeb033a7c32 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 22 Sep 2022 15:59:51 +0800 Subject: [PATCH 01/30] fix peer widget overflow and tile bug, add more sync ffi --- Cargo.lock | 96 ++++++++++++++++++- .../lib/common/widgets/peercard_widget.dart | 59 ++++-------- src/flutter_ffi.rs | 53 ++++++++-- 3 files changed, 160 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5627a861f..7dedfde5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,21 @@ dependencies = [ "atomic", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "alsa" version = "0.6.0" @@ -420,6 +435,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.11.0" @@ -1359,6 +1395,19 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "embed-resource" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0" +dependencies = [ + "cc", + "rustc_version 0.4.0", + "toml", + "vswhom", + "winreg 0.10.1", +] + [[package]] name = "encoding_rs" version = "0.8.31" @@ -1548,7 +1597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" dependencies = [ "memoffset", - "rustc_version", + "rustc_version 0.3.3", ] [[package]] @@ -2822,6 +2871,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memalloc" version = "0.1.0" @@ -4230,6 +4285,15 @@ dependencies = [ "semver 0.11.0", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.13", +] + [[package]] name = "rustdesk" version = "1.2.0" @@ -4305,6 +4369,16 @@ dependencies = [ "wol-rs", ] +[[package]] +name = "rustdesk-portable-packer" +version = "0.1.0" +dependencies = [ + "brotli", + "dirs", + "embed-resource", + "md5", +] + [[package]] name = "rustfft" version = "6.0.1" @@ -5374,6 +5448,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/flutter/lib/common/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peercard_widget.dart index ecf89283a..1f30c89df 100644 --- a/flutter/lib/common/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peercard_widget.dart @@ -128,7 +128,8 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerTile( BuildContext context, Peer peer, Rx deco) { final greyStyle = - TextStyle(fontSize: 12, color: MyTheme.color(context).lighterText); + TextStyle(fontSize: 11, color: MyTheme.color(context).lighterText); + final alias = bind.mainGetPeerOptionSync(id: peer.id, key: 'alias'); return Obx( () => Container( foregroundDecoration: deco.value, @@ -150,7 +151,6 @@ class _PeerCardState extends State<_PeerCard> children: [ Expanded( child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row(children: [ Padding( @@ -160,42 +160,21 @@ class _PeerCardState extends State<_PeerCard> backgroundColor: peer.online ? Colors.green : Colors.yellow)), - Text( - formatID(peer.id), + Expanded( + child: Text( + alias.isEmpty ? formatID(peer.id) : alias, style: const TextStyle(fontWeight: FontWeight.w400), - ), + overflow: TextOverflow.ellipsis, + )), ]), Align( alignment: Alignment.centerLeft, - child: FutureBuilder( - future: bind.mainGetPeerOption( - id: peer.id, key: 'alias'), - builder: (_, snapshot) { - if (snapshot.hasData) { - final name = snapshot.data!.isEmpty - ? '${peer.username}@${peer.hostname}' - : snapshot.data!; - return Tooltip( - message: name, - waitDuration: const Duration(seconds: 1), - child: Text( - name, - style: greyStyle, - textAlign: TextAlign.start, - overflow: TextOverflow.ellipsis, - ), - ); - } else { - // alias has not arrived - return Text( - '${peer.username}@${peer.hostname}', - style: greyStyle, - textAlign: TextAlign.start, - overflow: TextOverflow.ellipsis, - ); - } - }, + child: Text( + '${peer.username}@${peer.hostname}', + style: greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, ), ), ], @@ -203,7 +182,7 @@ class _PeerCardState extends State<_PeerCard> ), _actionMore(peer), ], - ).paddingSymmetric(horizontal: 4.0), + ).paddingOnly(left: 10.0, top: 3.0), ), ) ], @@ -272,7 +251,8 @@ class _PeerCardState extends State<_PeerCard> child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row(children: [ + Expanded( + child: Row(children: [ Padding( padding: const EdgeInsets.fromLTRB(0, 4, 8, 4), child: CircleAvatar( @@ -280,9 +260,12 @@ class _PeerCardState extends State<_PeerCard> backgroundColor: peer.online ? Colors.green : Colors.yellow)), - Text( - peer.alias.isEmpty ? formatID(peer.id) : peer.alias) - ]).paddingSymmetric(vertical: 8), + Expanded( + child: Text( + peer.alias.isEmpty ? formatID(peer.id) : peer.alias, + overflow: TextOverflow.ellipsis, + )), + ]).paddingSymmetric(vertical: 8)), _actionMore(peer), ], ).paddingSymmetric(horizontal: 12.0), diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f41108895..f859125bd 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -15,17 +15,18 @@ use hbb_common::{message_proto::Hash, ResultType}; use crate::flutter::{self, SESSIONS}; use crate::start_server; -use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::get_sound_inputs; use crate::ui_interface::{ - change_id, check_mouse_time, check_super_user_permission, discover, forget_password, + self, change_id, check_mouse_time, 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_mouse_time, get_option, get_options, get_peer, get_peer_option, get_socks, get_uuid, get_version, has_hwcodec, - has_rendezvous_service, post_request, send_to_cm, 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, + has_rendezvous_service, is_can_screen_recording, is_installed, is_installed_daemon, + is_installed_lower_version, is_process_trusted, is_rdp_service_open, is_share_rdp, + post_request, send_to_cm, 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, }; use crate::{ client::file_trait::FileManager, @@ -557,10 +558,19 @@ pub fn main_get_peer_option(id: String, key: String) -> String { get_peer_option(id, key) } +pub fn main_get_peer_option_sync(id: String, key: String) -> SyncReturn { + SyncReturn(get_peer_option(id, key)) +} + pub fn main_set_peer_option(id: String, key: String, value: String) { set_peer_option(id, key, value) } +pub fn main_set_peer_option_sync(id: String, key: String, value: String) -> SyncReturn { + set_peer_option(id, key, value); + SyncReturn(true) +} + pub fn main_forget_password(id: String) { forget_password(id) } @@ -693,10 +703,7 @@ fn main_broadcast_message(data: &HashMap<&str, &str>) { } pub fn main_change_theme(dark: String) { - main_broadcast_message(&HashMap::from([ - ("name", "theme"), - ("dark", &dark), - ])); + main_broadcast_message(&HashMap::from([("name", "theme"), ("dark", &dark)])); send_to_cm(&crate::ipc::Data::Theme(dark)); } @@ -972,6 +979,34 @@ pub fn query_onlines(ids: Vec) { crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } +pub fn main_is_installed() -> SyncReturn { + SyncReturn(is_installed()) +} + +pub fn main_is_installed_lower_version() -> SyncReturn { + SyncReturn(is_installed_lower_version()) +} + +pub fn main_is_installed_daemon(prompt: bool) -> SyncReturn { + SyncReturn(is_installed_daemon(prompt)) +} + +pub fn main_is_process_trusted(prompt: bool) -> SyncReturn { + SyncReturn(is_process_trusted(prompt)) +} + +pub fn main_is_can_screen_recording(prompt: bool) -> SyncReturn { + SyncReturn(is_can_screen_recording(prompt)) +} + +pub fn main_is_share_rdp() -> SyncReturn { + SyncReturn(is_share_rdp()) +} + +pub fn main_is_rdp_service_open() -> SyncReturn { + SyncReturn(is_rdp_service_open()) +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ From 78efa66378c53e0ec33eefa83e8ee48d3c1fe741 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 22 Sep 2022 16:18:06 +0800 Subject: [PATCH 02/30] locked only if installed, to-do: need refine here --- flutter/lib/desktop/pages/desktop_setting_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 5ab3b9a51..60bc96b47 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -331,7 +331,7 @@ class _Safety extends StatefulWidget { class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; - bool locked = true; + bool locked = bind.mainIsInstalled(); final scrollController = ScrollController(); @override From 36cd2622273345b31badf91245700f9015c340c5 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 22 Sep 2022 15:12:23 +0800 Subject: [PATCH 03/30] mobile dark theme options --- flutter/lib/mobile/pages/settings_page.dart | 56 ++++++++++++++------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 6f986ee78..93120427a 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -25,14 +25,13 @@ class SettingsPage extends StatefulWidget implements PageShape { final appBarActions = [ScanButton()]; @override - _SettingsState createState() => _SettingsState(); + State createState() => _SettingsState(); } const url = 'https://rustdesk.com/'; final _hasIgnoreBattery = androidVersion >= 26; var _ignoreBatteryOpt = false; var _enableAbr = false; -var _isDarkMode = false; class _SettingsState extends State with WidgetsBindingObserver { String? username; @@ -60,8 +59,6 @@ class _SettingsState extends State with WidgetsBindingObserver { _enableAbr = enableAbrRes; } - // _isDarkMode = MyTheme.currentDarkMode(); // TODO - if (update) { setState(() {}); } @@ -100,7 +97,7 @@ class _SettingsState extends State with WidgetsBindingObserver { Provider.of(context); final enhancementsTiles = [ SettingsTile.switchTile( - title: Text(translate('Adaptive Bitrate') + ' (beta)'), + title: Text('${translate('Adaptive Bitrate')} (beta)'), initialValue: _enableAbr, onToggle: (v) { bind.mainSetOption(key: "enable-abr", value: v ? "" : "N"); @@ -152,7 +149,7 @@ class _SettingsState extends State with WidgetsBindingObserver { SettingsTile.navigation( title: Text(username == null ? translate("Login") - : translate("Logout") + ' ($username)'), + : '${translate("Logout")} ($username)'), leading: Icon(Icons.person), onPressed: (context) { if (username == null) { @@ -177,15 +174,11 @@ class _SettingsState extends State with WidgetsBindingObserver { onPressed: (context) { showLanguageSettings(gFFI.dialogManager); }), - SettingsTile.switchTile( + SettingsTile.navigation( title: Text(translate('Dark Theme')), leading: Icon(Icons.dark_mode), - initialValue: _isDarkMode, - onToggle: (v) { - setState(() { - _isDarkMode = !_isDarkMode; - // MyTheme.changeDarkMode(_isDarkMode); // TODO - }); + onPressed: (context) { + showThemeSettings(gFFI.dialogManager); }, ) ]), @@ -232,7 +225,7 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async { final langs = json.decode(await bind.mainGetLangs()) as List; var lang = await bind.mainGetLocalOption(key: "lang"); dialogManager.show((setState, close) { - final setLang = (v) { + setLang(v) { if (lang != v) { setState(() { lang = v; @@ -241,7 +234,8 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async { HomePage.homeKey.currentState?.refreshPages(); Future.delayed(Duration(milliseconds: 200), close); } - }; + } + return CustomAlertDialog( title: SizedBox.shrink(), content: Column( @@ -257,13 +251,41 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async { ), actions: []); }, backDismiss: true, clickMaskDismiss: true); - } catch (_e) {} + } catch (e) { + // + } +} + +void showThemeSettings(OverlayDialogManager dialogManager) async { + var themeMode = MyTheme.getThemeModePreference(); + + dialogManager.show((setState, close) { + setTheme(v) { + if (themeMode != v) { + setState(() { + themeMode = v; + }); + MyTheme.changeDarkMode(themeMode); + Future.delayed(Duration(milliseconds: 200), close); + } + } + + return CustomAlertDialog( + title: SizedBox.shrink(), + contentPadding: 10, + content: Column(children: [ + getRadio('Light', ThemeMode.light, themeMode, setTheme), + getRadio('Dark', ThemeMode.dark, themeMode, setTheme), + getRadio('Follow System', ThemeMode.system, themeMode, setTheme) + ]), + actions: []); + }, backDismiss: true, clickMaskDismiss: true); } void showAbout(OverlayDialogManager dialogManager) { dialogManager.show((setState, close) { return CustomAlertDialog( - title: Text(translate('About') + ' RustDesk'), + title: Text('${translate('About')} RustDesk'), content: Wrap(direction: Axis.vertical, spacing: 12, children: [ Text('Version: $version'), InkWell( From 9bbc3376a42ca3112293818ec3873206c75659b0 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 22 Sep 2022 15:35:46 +0800 Subject: [PATCH 04/30] refactor: rename to peer_card.dart and peers_view.dart --- flutter/lib/common/widgets/address_book.dart | 4 +- .../{peercard_widget.dart => peer_card.dart} | 1 - flutter/lib/common/widgets/peer_tab_page.dart | 4 +- .../{peer_widget.dart => peers_view.dart} | 56 +++++++++---------- .../lib/desktop/pages/connection_page.dart | 8 +-- flutter/lib/mobile/pages/connection_page.dart | 8 +-- 6 files changed, 40 insertions(+), 41 deletions(-) rename flutter/lib/common/widgets/{peercard_widget.dart => peer_card.dart} (99%) rename flutter/lib/common/widgets/{peer_widget.dart => peers_view.dart} (86%) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index beecf47f2..ca5e85f56 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -1,6 +1,6 @@ import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/common/widgets/peer_widget.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -174,7 +174,7 @@ class _AddressBookState extends State { Expanded( child: Align( alignment: Alignment.topLeft, - child: AddressBookPeerWidget()), + child: AddressBookPeersView()), ) ], )); diff --git a/flutter/lib/common/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peer_card.dart similarity index 99% rename from flutter/lib/common/widgets/peercard_widget.dart rename to flutter/lib/common/widgets/peer_card.dart index 1f30c89df..bf0c93de5 100644 --- a/flutter/lib/common/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -489,7 +489,6 @@ abstract class BasePeerCard extends StatelessWidget { await bind.mainRemovePeer(id: id); removePreference(id); await reloadFunc(); - // Get.forceAppUpdate(); // TODO use inner model / state }(); }, dismissOnClicked: true, diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index fefe74671..2a1fe9909 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hbb/common/widgets/peer_widget.dart'; -import 'package:flutter_hbb/common/widgets/peercard_widget.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; diff --git a/flutter/lib/common/widgets/peer_widget.dart b/flutter/lib/common/widgets/peers_view.dart similarity index 86% rename from flutter/lib/common/widgets/peer_widget.dart rename to flutter/lib/common/widgets/peers_view.dart index e6236ff4e..cf9c4299a 100644 --- a/flutter/lib/common/widgets/peer_widget.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -11,34 +11,34 @@ import 'package:window_manager/window_manager.dart'; import '../../common.dart'; import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; -import 'peercard_widget.dart'; +import 'peer_card.dart'; typedef OffstageFunc = bool Function(Peer peer); -typedef PeerCardWidgetFunc = Widget Function(Peer peer); +typedef PeerCardBuilder = BasePeerCard Function(Peer peer); /// for peer search text, global obs value final peerSearchText = "".obs; final peerSearchTextController = TextEditingController(text: peerSearchText.value); -class _PeerWidget extends StatefulWidget { +class _PeersView extends StatefulWidget { final Peers peers; final OffstageFunc offstageFunc; - final PeerCardWidgetFunc peerCardWidgetFunc; + final PeerCardBuilder peerCardBuilder; - const _PeerWidget( + const _PeersView( {required this.peers, required this.offstageFunc, - required this.peerCardWidgetFunc, + required this.peerCardBuilder, Key? key}) : super(key: key); @override - _PeerWidgetState createState() => _PeerWidgetState(); + _PeersViewState createState() => _PeersViewState(); } /// State for the peer widget. -class _PeerWidgetState extends State<_PeerWidget> with WindowListener { +class _PeersViewState extends State<_PeersView> with WindowListener { static const int _maxQueryCount = 3; final space = isDesktop ? 12.0 : 8.0; final _curPeers = {}; @@ -60,7 +60,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { return width; }(); - _PeerWidgetState() { + _PeersViewState() { _startCheckOnlines(); } @@ -119,7 +119,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { } _lastChangeTime = DateTime.now(); }, - child: widget.peerCardWidgetFunc(peer), + child: widget.peerCardBuilder(peer), ); cards.add(Offstage( key: ValueKey("off${peer.id}"), @@ -198,39 +198,39 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { } } -abstract class BasePeerWidget extends StatelessWidget { +abstract class BasePeersView extends StatelessWidget { final String name; final String loadEvent; final OffstageFunc offstageFunc; - final PeerCardWidgetFunc peerCardWidgetFunc; + final PeerCardBuilder peerCardBuilder; final List initPeers; - const BasePeerWidget({ + const BasePeersView({ Key? key, required this.name, required this.loadEvent, required this.offstageFunc, - required this.peerCardWidgetFunc, + required this.peerCardBuilder, required this.initPeers, }) : super(key: key); @override Widget build(BuildContext context) { - return _PeerWidget( + return _PeersView( peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), offstageFunc: offstageFunc, - peerCardWidgetFunc: peerCardWidgetFunc); + peerCardBuilder: peerCardBuilder); } } -class RecentPeerWidget extends BasePeerWidget { - RecentPeerWidget({Key? key}) +class RecentPeersView extends BasePeersView { + RecentPeersView({Key? key}) : super( key: key, name: 'recent peer', loadEvent: 'load_recent_peers', offstageFunc: (Peer peer) => false, - peerCardWidgetFunc: (Peer peer) => RecentPeerCard( + peerCardBuilder: (Peer peer) => RecentPeerCard( peer: peer, ), initPeers: [], @@ -244,14 +244,14 @@ class RecentPeerWidget extends BasePeerWidget { } } -class FavoritePeerWidget extends BasePeerWidget { - FavoritePeerWidget({Key? key}) +class FavoritePeersView extends BasePeersView { + FavoritePeersView({Key? key}) : super( key: key, name: 'favorite peer', loadEvent: 'load_fav_peers', offstageFunc: (Peer peer) => false, - peerCardWidgetFunc: (Peer peer) => FavoritePeerCard( + peerCardBuilder: (Peer peer) => FavoritePeerCard( peer: peer, ), initPeers: [], @@ -265,14 +265,14 @@ class FavoritePeerWidget extends BasePeerWidget { } } -class DiscoveredPeerWidget extends BasePeerWidget { - DiscoveredPeerWidget({Key? key}) +class DiscoveredPeersView extends BasePeersView { + DiscoveredPeersView({Key? key}) : super( key: key, name: 'discovered peer', loadEvent: 'load_lan_peers', offstageFunc: (Peer peer) => false, - peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard( + peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peer: peer, ), initPeers: [], @@ -286,15 +286,15 @@ class DiscoveredPeerWidget extends BasePeerWidget { } } -class AddressBookPeerWidget extends BasePeerWidget { - AddressBookPeerWidget({Key? key}) +class AddressBookPeersView extends BasePeersView { + AddressBookPeersView({Key? key}) : super( key: key, name: 'address book peer', loadEvent: 'load_address_book_peers', offstageFunc: (Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags), - peerCardWidgetFunc: (Peer peer) => AddressBookPeerCard( + peerCardBuilder: (Peer peer) => AddressBookPeerCard( peer: peer, ), initPeers: _loadPeers(), diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index ad8e430f4..6a8c58f7b 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -11,7 +11,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peer_widget.dart'; +import '../../common/widgets/peers_view.dart'; import '../../models/platform_model.dart'; /// Connection page for connecting to a remote peer. @@ -74,9 +74,9 @@ class _ConnectionPageState extends State { translate('Address Book') ], children: [ - RecentPeerWidget(), - FavoritePeerWidget(), - DiscoveredPeerWidget(), + RecentPeersView(), + FavoritePeersView(), + DiscoveredPeersView(), const AddressBook(), ], )), diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index edc2f5f6d..0778bec4e 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -10,7 +10,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/address_book.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peer_widget.dart'; +import '../../common/widgets/peers_view.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -84,9 +84,9 @@ class _ConnectionPageState extends State { translate('Address Book') ], children: [ - RecentPeerWidget(), - FavoritePeerWidget(), - DiscoveredPeerWidget(), + RecentPeersView(), + FavoritePeersView(), + DiscoveredPeersView(), const AddressBook(), ], )), From 00077676f4d0fac4b56eb8cb6a9a3767f81b9bba Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 22 Sep 2022 16:45:14 +0800 Subject: [PATCH 05/30] 1. new mobile connect. 2. _forceAlwaysRelayAction dismissOnClicked: false. 3. no tcp tunneling on mobile 4. adjust peer tab border on mobile --- flutter/lib/common.dart | 38 ++++++++++++--- flutter/lib/common/widgets/peer_card.dart | 48 ++++++++----------- flutter/lib/common/widgets/peer_tab_page.dart | 2 +- flutter/lib/mobile/pages/connection_page.dart | 35 +------------- 4 files changed, 55 insertions(+), 68 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 365ce3dd5..5cd54a0a8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -17,6 +17,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; import 'common/widgets/overlay.dart'; +import 'mobile/pages/file_manager_page.dart'; +import 'mobile/pages/remote_page.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -1071,14 +1073,38 @@ void connect(BuildContext context, String id, assert(!(isFileTransfer && isTcpTunneling && isRDP), "more than one connect type"); - FocusScopeNode currentFocus = FocusScope.of(context); - if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); - } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); + if (isDesktop) { + if (isFileTransfer) { + await rustDeskWinManager.newFileTransfer(id); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.newPortForward(id, isRDP); + } else { + await rustDeskWinManager.newRemoteDesktop(id); + } } else { - await rustDeskWinManager.newRemoteDesktop(id); + if (isFileTransfer) { + if (!await PermissionManager.check("file")) { + if (!await PermissionManager.request("file")) { + return; + } + } + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FileManagerPage(id: id), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => RemotePage(id: id), + ), + ); + } } + + FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index bf0c93de5..7eae2aa5f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -77,8 +77,11 @@ class _PeerCardState extends State<_PeerCard> subtitle: Text('${peer.username}@${peer.hostname}'), title: Text(peer.alias.isEmpty ? formatID(peer.id) : peer.alias), leading: Container( + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + borderRadius: BorderRadius.circular(4), + ), padding: const EdgeInsets.all(6), - color: str2color('${peer.id}${peer.platform}', 0x7f), child: getPlatformImage(peer.platform)), trailing: InkWell( child: const Padding( @@ -458,7 +461,7 @@ abstract class BasePeerCard extends StatelessWidget { } await bind.mainSetPeerOption(id: id, key: option, value: value); }, - dismissOnClicked: true, + dismissOnClicked: false, ); } @@ -543,7 +546,6 @@ abstract class BasePeerCard extends StatelessWidget { if (favs.remove(id)) { await bind.mainStoreFav(favs: favs); await reloadFunc(); - // Get.forceAppUpdate(); // TODO use inner model / state } }(); }, @@ -624,15 +626,13 @@ class RecentPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context, peer), _transferFileAction(context, peer.id), - _tcpTunnelingAction(context, peer.id), ]; - MenuEntryBase? rdpAction; - if (peer.platform == 'Windows') { - rdpAction = _rdpAction(context, peer.id); + if (isDesktop) { + menuItems.add(_tcpTunnelingAction(context, peer.id)); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); - if (rdpAction != null) { - menuItems.add(rdpAction); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); } menuItems.add(_wolAction(peer.id)); menuItems.add(MenuEntryDivider()); @@ -656,15 +656,13 @@ class FavoritePeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context, peer), _transferFileAction(context, peer.id), - _tcpTunnelingAction(context, peer.id), ]; - MenuEntryBase? rdpAction; - if (peer.platform == 'Windows') { - rdpAction = _rdpAction(context, peer.id); + if (isDesktop) { + menuItems.add(_tcpTunnelingAction(context, peer.id)); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); - if (rdpAction != null) { - menuItems.add(rdpAction); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); } menuItems.add(_wolAction(peer.id)); menuItems.add(MenuEntryDivider()); @@ -690,15 +688,13 @@ class DiscoveredPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context, peer), _transferFileAction(context, peer.id), - _tcpTunnelingAction(context, peer.id), ]; - MenuEntryBase? rdpAction; - if (peer.platform == 'Windows') { - rdpAction = _rdpAction(context, peer.id); + if (isDesktop) { + menuItems.add(_tcpTunnelingAction(context, peer.id)); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); - if (rdpAction != null) { - menuItems.add(rdpAction); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); } menuItems.add(_wolAction(peer.id)); menuItems.add(MenuEntryDivider()); @@ -721,15 +717,13 @@ class AddressBookPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context, peer), _transferFileAction(context, peer.id), - _tcpTunnelingAction(context, peer.id), ]; - MenuEntryBase? rdpAction; - if (peer.platform == 'Windows') { - rdpAction = _rdpAction(context, peer.id); + if (isDesktop) { + menuItems.add(_tcpTunnelingAction(context, peer.id)); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); - if (rdpAction != null) { - menuItems.add(rdpAction); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); } menuItems.add(_wolAction(peer.id)); menuItems.add(MenuEntryDivider()); diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 2a1fe9909..3ed3dc11d 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -113,7 +113,7 @@ class _PeerTabPageState extends State color: _tabIndex.value == t.key ? MyTheme.color(context).bg : null, - borderRadius: BorderRadius.circular(2), + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), ), child: Align( alignment: Alignment.center, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 0778bec4e..6156223b5 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; -import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -15,7 +14,6 @@ import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; -import 'remote_page.dart'; import 'scan_page.dart'; import 'settings_page.dart'; @@ -97,38 +95,7 @@ class _ConnectionPageState extends State { /// Connects to the selected peer. void onConnect() { var id = _idController.id; - connect(id); - } - - /// Connect to a peer with [id]. - /// If [isFileTransfer], starts a session only for file transfer. - void connect(String id, {bool isFileTransfer = false}) async { - if (id == '') return; - id = id.replaceAll(' ', ''); - if (isFileTransfer) { - if (!await PermissionManager.check("file")) { - if (!await PermissionManager.request("file")) { - return; - } - } - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => FileManagerPage(id: id), - ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => RemotePage(id: id), - ), - ); - } - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } + connect(context, id); } /// UI for software update. From 51b02353c9177a73944cc71d8e42943791f64f17 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 22 Sep 2022 17:38:18 +0800 Subject: [PATCH 06/30] 1. mobile ab login. 2. typos 3. del rename dialog body padding --- flutter/lib/common/widgets/address_book.dart | 16 ++++++---- flutter/lib/common/widgets/peer_card.dart | 2 -- .../lib/desktop/pages/desktop_home_page.dart | 7 +++-- .../desktop/pages/desktop_setting_page.dart | 30 ++++++++++--------- flutter/lib/mobile/pages/settings_page.dart | 2 +- flutter/lib/mobile/widgets/dialog.dart | 18 ++++++----- src/lang/cn.rs | 2 +- src/lang/cs.rs | 2 +- src/lang/da.rs | 2 +- src/lang/de.rs | 2 +- src/lang/eo.rs | 2 +- src/lang/es.rs | 2 +- src/lang/fr.rs | 2 +- src/lang/hu.rs | 2 +- src/lang/id.rs | 2 +- src/lang/it.rs | 2 +- src/lang/ja.rs | 2 +- src/lang/ko.rs | 2 +- src/lang/pl.rs | 2 +- src/lang/pt_PT.rs | 2 +- src/lang/ptbr.rs | 2 +- src/lang/ru.rs | 2 +- src/lang/sk.rs | 2 +- src/lang/template.rs | 2 +- src/lang/tr.rs | 2 +- src/lang/tw.rs | 2 +- src/lang/vn.rs | 2 +- 27 files changed, 64 insertions(+), 53 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index ca5e85f56..9fac81723 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '../../common.dart'; import '../../desktop/pages/desktop_home_page.dart'; +import '../../mobile/pages/settings_page.dart'; import '../../models/platform_model.dart'; class AddressBook extends StatefulWidget { @@ -37,11 +38,16 @@ class _AddressBookState extends State { }); handleLogin() { - loginDialog().then((success) { - if (success) { - setState(() {}); - } - }); + // TODO refactor login dialog for desktop and mobile + if (isDesktop) { + loginDialog().then((success) { + if (success) { + setState(() {}); + } + }); + } else { + showLogin(gFFI.dialogManager); + } } Future buildAddressBook(BuildContext context) async { diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 7eae2aa5f..9c0c997bc 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -590,8 +590,6 @@ abstract class BasePeerCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Form( child: TextFormField( controller: controller, diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index edae7deeb..833a914cd 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -331,7 +331,7 @@ Future loginDialog() async { var userNameMsg = ""; String pass = ""; var passMsg = ""; - var userContontroller = TextEditingController(text: userName); + var userController = TextEditingController(text: userName); var pwdController = TextEditingController(text: pass); var isInProgress = false; @@ -349,7 +349,7 @@ Future loginDialog() async { }); } - userName = userContontroller.text; + userName = userController.text; pass = pwdController.text; if (userName.isEmpty) { userNameMsg = translate("Username missed"); @@ -385,6 +385,7 @@ Future loginDialog() async { close(); } + // 登录dialog return CustomAlertDialog( title: Text(translate("Login")), content: ConstrainedBox( @@ -411,7 +412,7 @@ Future loginDialog() async { decoration: InputDecoration( border: const OutlineInputBorder(), errorText: userNameMsg.isNotEmpty ? userNameMsg : null), - controller: userContontroller, + controller: userController, focusNode: FocusNode()..requestFocus(), ), ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 60bc96b47..0918fc59b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -48,7 +48,7 @@ class _DesktopSettingPageState extends State _TabInfo('Security', Icons.enhanced_encryption_outlined, Icons.enhanced_encryption), _TabInfo('Network', Icons.link_outlined, Icons.link), - _TabInfo('Acount', Icons.person_outline, Icons.person), + _TabInfo('Account', Icons.person_outline, Icons.person), _TabInfo('About', Icons.info_outline, Icons.info) ]; @@ -92,7 +92,7 @@ class _DesktopSettingPageState extends State _General(), _Safety(), _Network(), - _Acount(), + _Account(), _About(), ], )), @@ -641,14 +641,14 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } } -class _Acount extends StatefulWidget { - const _Acount({Key? key}) : super(key: key); +class _Account extends StatefulWidget { + const _Account({Key? key}) : super(key: key); @override - State<_Acount> createState() => _AcountState(); + State<_Account> createState() => _AccountState(); } -class _AcountState extends State<_Acount> { +class _AccountState extends State<_Account> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); @@ -658,12 +658,12 @@ class _AcountState extends State<_Acount> { physics: NeverScrollableScrollPhysics(), controller: scrollController, children: [ - _Card(title: 'Acount', children: [login()]), + _Card(title: 'Account', children: [accountAction()]), ], ).marginOnly(bottom: _kListViewBottomMargin)); } - Widget login() { + Widget accountAction() { return _futureBuilder(future: () async { return await gFFI.userModel.getUserName(); }(), hasData: (data) { @@ -671,12 +671,14 @@ class _AcountState extends State<_Acount> { return _Button( username.isEmpty ? 'Login' : 'Logout', () => { - loginDialog().then((success) { - if (success) { - // refresh frame - setState(() {}); - } - }) + username.isEmpty + ? loginDialog().then((success) { + if (success) { + // refresh frame + setState(() {}); + } + }) + : gFFI.userModel.logOut() }); }); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 93120427a..985fe2df0 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -451,7 +451,7 @@ void showLogin(OverlayDialogManager dialogManager) { ), controller: nameController, ), - PasswordWidget(controller: passwordController), + PasswordWidget(controller: passwordController, autoFocus: false), ]), actions: (loading ? [CircularProgressIndicator()] diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 503b82c50..c17045236 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -6,8 +6,7 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; void clientClose(OverlayDialogManager dialogManager) { - msgBox('', 'Close', 'Are you sure to close the connection?', - dialogManager); + msgBox('', 'Close', 'Are you sure to close the connection?', dialogManager); } void showSuccess() { @@ -131,7 +130,7 @@ void setTemporaryPasswordLengthDialog( if (index < 0) index = 0; length = lengths[index]; dialogManager.show((setState, close) { - final setLength = (newValue) { + setLength(newValue) { final oldValue = length; if (oldValue == newValue) return; setState(() { @@ -143,7 +142,8 @@ void setTemporaryPasswordLengthDialog( close(); showSuccess(); }); - }; + } + return CustomAlertDialog( title: Text(translate("Set temporary password length")), content: Column( @@ -230,12 +230,14 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { } class PasswordWidget extends StatefulWidget { - PasswordWidget({Key? key, required this.controller}) : super(key: key); + PasswordWidget({Key? key, required this.controller, this.autoFocus = true}) + : super(key: key); final TextEditingController controller; + final bool autoFocus; @override - _PasswordWidgetState createState() => _PasswordWidgetState(); + State createState() => _PasswordWidgetState(); } class _PasswordWidgetState extends State { @@ -245,7 +247,9 @@ class _PasswordWidgetState extends State { @override void initState() { super.initState(); - Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); + if (widget.autoFocus) { + Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); + } } @override diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 664d6f05b..47f3c0870 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "适应窗口"), ("General", "常规"), ("Security", "安全"), - ("Acount", "账户"), + ("Account", "账户"), ("Theme", "主题"), ("Dark Theme", "暗黑主题"), ("Enable hardware codec", "使用硬件编解码"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ace56788f..9d203f9ce 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Měřítko adaptivní"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 27724f7b3..a07539719 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptiv"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 8d90be381..fa589a564 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptiv skalieren"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 6c7bb5aa8..cc28525e5 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skalo adapta"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index c8296ced5..9704a3f84 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -337,7 +337,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptable a escala"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index d9a42e934..8276a54f2 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Échelle adaptative"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b35224c03..e322053ac 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skála adaptív"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 657014141..a285e15de 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -337,7 +337,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptif"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index 8f6dfb3d9..917d5e9b2 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -323,7 +323,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Translate mode", ""), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6d0a2a2f7..446bbc944 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -321,7 +321,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "フィットウィンドウ"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index ca939e2b8..cb223f77d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -318,7 +318,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "맞는 창"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fe45ddf3e..e6696fed5 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -322,7 +322,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptacyjna"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 858afd8a1..783d93635 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -318,7 +318,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptável"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index af4f0b52e..ac1688d13 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", ""), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 04cfed485..a005dc6ad 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Масштаб адаптивный"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8ae17b1ad..837e491ce 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Prispôsobivá mierka"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index 914b103df..5c68cef37 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", ""), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b1b029b39..a0cd3ed8d 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -337,7 +337,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Ölçek uyarlanabilir"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 764f666e7..c3c0849f0 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "適應窗口"), ("General", "常規"), ("Security", "安全"), - ("Acount", "賬戶"), + ("Account", "賬戶"), ("Theme", "主題"), ("Dark Theme", "暗黑主題"), ("Enable hardware codec", "使用硬件編解碼"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f177581f9..88aa79dbf 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Quy mô thích ứng"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), From 204eab4b81baa681f77aa570bcc0c6ba052f4d40 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 22 Sep 2022 23:22:31 +0800 Subject: [PATCH 07/30] add margin to app icon --- .../AppIcon.appiconset/app_icon_1024.png | Bin 15146 -> 15849 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 1975 -> 1570 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 383 -> 354 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 3938 -> 3330 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 624 -> 569 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 8587 -> 6852 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 1023 -> 909 bytes flutter/pubspec.lock | 10 +++++----- res/icon-margin.png | Bin 0 -> 15172 bytes 9 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 res/icon-margin.png diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 6b4737dd5d7abd614f667696c8fbc698ae90556c..9a3310e01c49f8b36849b6a24d12dc4325827e2a 100644 GIT binary patch literal 15849 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

YV5r>{ zuNJ*Q0X$FkB(j+b5>X{{W+mOY8A|G0rsOx%nRm`L^S)Rgb1Ie01JAIz;tug|~3=*AyK<@>$t)EZ=8>k0fV!0$(3DB*(0^8!Dy?5sfb}EyB4wq2h7BTWf z5;FgI!X91(%%-6fxSr}&>@<=aOvM_9_0i=0ZM+}PHODny0@eZUtm^AuTJZw(cMZuj z*bohAQYJU{4DTgYNYY0Qh9IiiHtLt$#gP04oa}*nh5t+P3%?Wk5p^qSilku%FWImi zxb~l597Z@ErovWtQ-ev6FaeWv@+85u3#@QSZlan4EH}ZFTLpr4uH#>$3HTBWjlmS8uAA|mXKR(xed^ya+-z`)!rsTzlcspq0^wQEWoqIquGcN z8<7P3$>KE@mRfdsoGvZa{=QG1I&9@D={!E(*0u;ikLPj5LbZwL-9~+i<-%_005wzn z2@1WnKSgzi<=fOTq;V3?rEqwXlk>5Tt$B$m1md|wq6Bldl1>&>HP2w~Ov(B@)L0AS zOx|d;!?YEg$j7?XDbd|={1&u#l5>4j)vlDchUxsh=@q&=>J8a~0%qE~@X=`NPlo#I z!w-Lv)RcUVgxFLX?Mki6pou@M`*4dlq9;(69XJ`9{D?V$63W(@ z5Ed`BHzy#D2|8T7-+3zS?bo{$%&&Yoh;IVXH;eI~z(n8btulW)=YLgs22!?q&MFrg z;luf?k+2XW?&sGw?2|-Vos#a8OMcGu-F@*Zvr(3u_eE-Q;w40PuCB0cD`xMr)tRIPd@f literal 15146 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 v+}mnSJ(2Y4Epz4PxKDgp&l+LfBy`4 z|MvR-6MX+5gZ~eD|G3!yH<14@jQ`E#|5Bg-?e+iP?f=y0|0sz6>GS`q(f`8U|GwM* zv(^8Axc^+F|4p3#Jd^(gbpOcV|D?_Tm&N~MsQ=^e|Bk@_dA0vRmH+Sd|JmyQq09eB zf0_SsvHxwZ|D4GGXsiEJp#O-y|5l;@PC?{@0000KbW%=J0QNuo$#oa;@8!{%OEn`7 z2v&$!FX9(?000E&NklMvuH47QzW+y3CmwGa zAT}xJ&pv;$L7pUU(lCo z#TTw~Aer2Cfm`8S9#SFyj;r_&g}0CrZwntd%Kw;y)X052ZcxZUisTAM`~NMZ%G)Dx zaFK_U$zL4q07#v}!3Ma7)VV&?1ot^eo!tEafg3mhHwT_Sc{l<2{ZFR5D>wmHe|LKY zZZF{kT;A>#c=H-g!0R{S0>-pGT-mUrHyW*b#_0r%r%hlHLh3t%PK^+v1)C@16auQZ z4T|JDEBJ=}K)zE9#%@1l0oC2q$WQ7jd?oztlge<~Pg5Wq*T`>_m6r(6$ZyOxUTOkL zG$)9L313-S1o3%NN=Lw16ba_xf2E`@{jpy5(h!)w#2Qotn(f9T2)u*0VYO!7V88@SRkTXw&}rMy0LI=iwYR@6QFLu z0lWd-T%n-AqfV7?!$Cq?xGg4NQ#EQhaD-l!iULDOz%Swkb>S$Hh3CU(5rO^?|JWlO zM_EvFfhr&n;v}GD0tX)ce^S*E5QuP=u>>x*xY_*V0_qSqfC5~2`1w3eE?~^5aN?t1 z$1|6bSYT>V1B#PK$3c)-U`6FWJj8P(t(RC}gHo&x@M7ZWtCLi~Y@#)n2i)MPV3I^2 z9)nc_xM}0QODYi7s8t9z5j7Vk6mT_aG;!nWX#VIY6zFNxDukP5e-W!E6qs6x1uQ87 zJu7J!bmJ``9s`3CaPzFAwo4iV9!kjP25_^+Xf z6Hmnr>UVgtacUeT6;OvH**J933dx!#6@UreehYz<75-krd?3F6wFW+vGPRqSNFY88 zNYex^dNbjikX;q4e+xXQBb*gKB@~DU0V$hY4N#G{F7PV2G7%i7)RMR|Qm7xBdvKJA zPWOO-0Cb83(X!zP{*2nrwWTi4$>vBHEa@%fmE;WyeN;MFf4cm@5frc+grXk8KFmpl zP`+#7O)B3a6hTJ;CT>e76b)NgKv@tPtJN(rPSenN5?as;e+fVskhnr);-Y9Y{6&q# z8LJWmz}E^B9lFx)Pq{(VP)=OqdwEB<+1Om zz{TaQ_*JgH9eTfNYkIDu05?~6fA3|svtQ>s|Aof0MPKPW`=!?NU+X>l#pd&0?LPnI z_OoB_KmQ93f59srsl4QJfAMd<=97_;k&*Eq@dfZRM$1E3`Gf!f002ovPDHLkV1gyQ B;DP`E delta 1901 zcmV-z2a@=r47U%EBna?OOjJbx005CeCy`Jae*ka)1atrU{r?Mh|Jv*S2zCG8?f)l- z|2L5T4|@M^um2f;{}g@y>-7K6<^RIn|9Z9mB82}Qg8%*g|MB?$@Am&wpa19b|EbUa zWU2o#jsGo*|MmL+N*%|7ffKTBQF&mjA}z|GnD(jllm* zf1LlM&HsqK|2vZZy4nA&(f^#t|B}N0guDNKxc@(t|CYu7VW|H|ng5{5|DS`#;s5{u z6?9TgQvi$_^_ThT;@;7~XHYXL6bJN{udb$kpRfP`24P7=K~#9!)thNw8Zi)u(c*2j z+D`tv-1jB7g2*8#sNnhizfJ2=R3@8le*$IO=biqPeMlygV>7-_tnRLCZ+xM@#+Qxl zmEBdJ6JJ-h_`?@3v9{QIAR?>jA> z?;nD3e;Lo`2K&?g_me01f5*Yj8hOIn&fECgB#+p99gTkS4F601TPM$0f6j?3f8-%6 zzf!;!dC1nPZ}$s%$d}z^_Wz!MHhtn!inoVf94fHFDRVo zzm5W{DIB@VVLk!$!eR!N2dE;P>E|$y0HQ(R%#de7PBQ^>0YuWmnZ^a13uIizU@CxQ zQz)nee$R$&p`b9dOa-j5aHc#?VA@bOW#P>61Iz?aZ3qWa%~t(GvWHb~^^3xZa1AB` zsD+(AaLAS&Kp;AF4A|2cfAj?qxE4}QCOLpOKrZW`P>7odRBdVKU)J9r&lSc+d7IUt zD}el&^y^oZ6Ab7=K5%a=?G~6wvwsw%203Y7EcT!$fcl9ntL5Ng?KHLp=Q^a>0s~nv zyISNRFenJd(H(RI5D5!LhRv-`1~;-OAT5EcU<7L*cOY>k3Ev3Ze+wX)=51I4F?4cR zTt}e9<7i-!dl2Uz&=0s5ur9?c={$4$$Gi{R3v>%?1jImE zgid)Xuv~Wm8%70<25=0cU>@!SkYv-qSwdTY52WLsTY=G$WcjHJ20RGd3UnOYHN&vM zlA+rexMyQ9X&{gffAih6I|07edm;luS1bw>a1>Ch$3gmF%u-R^oj|lWn}sfn1mY}| z?%WAn*t7ExTL+nC0=wi+pvv|j5hH;S%f?lA0xhfLxd4N2OIldKY%K0cA)wxwUImDu zzcJ`QA)sDP7Pb2TwKD6-kxD?lGZO>S1h+!9xlqS|#al0|u_obXTzoLA1<(?V327jW zqe2y~1!^gNf51_L)=U_7J|a;IIK2rJm5_S_1S-mf{ ztT+-B5`raCD=zW(W&&`f9wuOS0L6GQqm7e2L(`MP!>{} zF>L|DT}nu?+pUPj9Dq=C5EgREBalMCiEKeQaB|)G!U4NQWFUapwQ%O- zCQ4JR$4XiKhErF7zqsX;tJCM-9t#EO9LPw3&?^fCsXM&=8QX)g6mBL!=qf*Y0)8aP zHA@9=0-OKF&0=eEp=P`*7CkmZx*YIlfhROAQyug29!nXGX4xX2IcwXeVd(J)lX%=hs!Vf{kICAf3bJCywdN( ny??RzeOms1K!1$`UjF|R?7e%eE*Oe`00000NkvXXu0mjfS$C?{ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index c78490fc442c3640a519ee94a3faa81d087cacf5..a529bd927a24075adffda5dc990b1ba4ce54914e 100644 GIT binary patch delta 327 zcmey*^oVJKN_|CuPlzi65ETALLHQsq5PbXff9<9JycPeSef;0D`+v>$|3w@A2hIO) zJ?X!3-+zVX|LkS|&p!UY`O5z}C;mq){;%EnU#i_o?)C=YS?Pe4=6I()5S4F z;&N{POTHrt94+CwjamL%pWplbd;h~lf8O}{@W@Y)Kt?(uN<}!iF)& z5S2ewL8$VRbP0l+XkKqj9Vp delta 356 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 diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 883dbd709db9f3298e1af6c1bc676525cb19f260..2ce17295714ca1c69895b842b8245aa2d84c8fed 100644 GIT binary patch delta 544 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 literal 8587 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 diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 056497e14981860529b98b67d73d953dcdabd9fd..c9bf7b6bb844a1cce7ca10f364d964aa68aec5e7 100644 GIT binary patch delta 863 zcmV-l1EBo>2aN}iBnY=qOjJbx005Ce6_HQ|kx&MI0CE5S|Nj7Q{}g@y@b~`?dH?$S z{{(aY33mVW`v0@l|Bb-^*y{g0lm97*|K;)j(&qn6oBuYC|KRTb&gB0rivJ{q|LODp ztkM6X%>RG5|7EKG8i4=A-~Xx4|Ao8%LYDtAjQ^Fy|A@W+bF%+%u>WnY|5Bd+9)kb; z{{QTM^#8uw|G3!yo5=rPrvF)@|97(&Ky zuRYllYpZt!5GXSM053oT2v>j>Wx4G5Du^)O2Qm%OVgf*S?2)N0MNqAyGO`^5>Ht7= z7K(JyNCh`aekhZb2;2aW<98|5K!$ZfN)WaToB#+d3Dr*lsE;vl0l%tpE6VeUXS_yze638C&qf~<%GD(0&s^o zb>RY#s0N?-Gzy@?0vl&;RJaOkzOgU11rVYE6I@RSb9Ale0u%6x?j!CdgcZIIZ%lxJ zdyUJI#LioRuVulR0|S76lPsN59S;30Cju4#U9hB85aS~q(|i`dCXS*A zop53SHrbpR74U_FOaR-0YQUD^13jw}$1{f$7qlqIgLKSXu>gA#>oOov@Eu_R#!(ik zGX)S!sa{kDAdj_V8BltU2HB9!PNwf{mUbIRlINz+2Dl!kqXXBCVrG&Z@D-GQ{_jQt z#!f-gPEb+O!afqBWtbrV3}>R@Q%i-aqIx!qqP#bfS~RmrZ}9@C>M@4fDlpN;sw|CL8%xnv!xq9{i_yA~x zKOJgmbCpMz%@4Ng3zh{CO1Yy(hx<5AGS9JhE7~DRs5<6BB62OO*Z82>^r+kTu-){y pPy66_s`0_m=#!_{x%=1O(hoT9j-;$0e%$~7002ovPDHLkV1g4MvxEQu delta 966 zcmV;%13CPS2mc3;K2#|H0k=x7Yu&)c=^q|BJu>g1Y}dl>aG+|JUjNrq2J5!T)Nk|6Zm4Lze&Y zfB66D^Z$jr|7NQHE{p#kg8y={|0{|AP@ey^RavtD000hjQchC<`|8ke9tXXVVPFov zkm~p0Hvj+v+DSw~R9M5cm*;w_KoEvOO)NQQXDOlxBG`M2HHjv<|C>652!nA4jXdZ3 zwQPB}4LdVZpod&OBeP#f&iu(eNTD8!e_47o&BzuXr9e*w9R52*;VGEE!0uMzYYj#D zet=x`&o9eTDElDgi6h04;EYxn8Gd;0%1r=!2$9;1X=VJJe|+2k za6LE!C}lvEcUJ*h9b#(ufMLACv#tQ#v+?x>V|)x}ghe92_8=yGruubOxf-uDI|%@7 zgdMA9%i!O);;vo*wNh!oD|Q=fG_2=FzmDWL5@6s&%^!xx-2q<3kpS>Mgz77o=7HDQ zA^?US%`Y;!g%>TMA_mN&4jm@ae_Jg)1y=+>o2_k`Q0h3^p%7q&6ZBLj)@RtGtcd`x zXuo2iRqyRf04~t$FiLAUX)6F!o==Tql>}@?ftXsS() zfQw6L(SS}je473TU>9y-O#=>k!WIk%8!7E-^daK%Oxg==60Q;7f5#>PfBhsw;w!jv zifC(&Apo%V5MXKqpi#vhmjFD)YMb!aLqKB{0MuUepbFh6kAByhB?RCKVWNpPun(Ju ziWs1o`1W5Actelg^#=8<;}W1}{B-%~7)A;OP@52`J2UWIIBraAaV*+19R`5Xd+kcF zcx(C$F}-pcz+4C7>U|N~e|3m!q9nTa_Zq}h{bWoXLR_N`O9b%N*?f(Y{o?rLwFrf` zbwF)=K>S>q#~Xqy%cO!VeXiUT<{j5BqA;&E-rX8#a(mkSK13_}MVzrK_7~z{n=d!b zXJRaC_&+{=%{qKpE}`4o${nlx};K?Fto3fKJoPR^fz7V o>#w17U-DfF=D#=n&!5u&f9hKM9u7#i&;S4c07*qoM6N<$f(|b48UO$Q diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 79c25ee5b..f1921779e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -140,7 +140,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -161,7 +161,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -588,7 +588,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -602,7 +602,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -679,7 +679,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: diff --git a/res/icon-margin.png b/res/icon-margin.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc650e9de412b3dd87a0941713d3787b971ce4c GIT binary patch literal 15172 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 z≧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 Date: Fri, 23 Sep 2022 10:18:30 +0800 Subject: [PATCH 08/30] opt: center/align topright when toggle chat --- flutter/lib/models/chat_model.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index e9952db1e..dbf5ac753 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -167,8 +167,10 @@ class ChatModel with ChangeNotifier { _isShowChatPage = !_isShowChatPage; notifyListeners(); await windowManager.setSize(Size(400, 600)); + await windowManager.setAlignment(Alignment.topRight); } else { await windowManager.setSize(Size(800, 600)); + await windowManager.center(); await Future.delayed(Duration(milliseconds: 100)); _isShowChatPage = !_isShowChatPage; notifyListeners(); From d939fdac7258b42efe1915c860b840e09beb63b1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 23 Sep 2022 11:01:33 +0800 Subject: [PATCH 09/30] opt: hide home button when it only exists on tab --- .../lib/desktop/widgets/tabbar_widget.dart | 74 +++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index dc9bdccb8..1b82b6b55 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -490,6 +490,15 @@ class _ListView extends StatelessWidget { this.tabBuilder, this.labelGetter}); + /// Check whether to show ListView + /// + /// Conditions: + /// - hide single item when only has one item (home) on [DesktopTabPage]. + bool isHideSingleItem() { + return state.value.tabs.length == 1 && + controller.tabType == DesktopTabType.main; + } + @override Widget build(BuildContext context) { return Obx(() => ListView( @@ -497,38 +506,41 @@ class _ListView extends StatelessWidget { scrollDirection: Axis.horizontal, shrinkWrap: true, physics: const BouncingScrollPhysics(), - children: state.value.tabs.asMap().entries.map((e) { - final index = e.key; - final tab = e.value; - return _Tab( - index: index, - label: labelGetter == null - ? Rx(tab.label) - : labelGetter!(tab.label), - selectedIcon: tab.selectedIcon, - unselectedIcon: tab.unselectedIcon, - closable: tab.closable, - selected: state.value.selected, - onClose: () { - if (tab.onTabCloseButton != null) { - tab.onTabCloseButton!(); - } else { - controller.remove(index); - } - }, - onSelected: () => controller.jumpTo(index), - tabBuilder: tabBuilder == null - ? null - : (Widget icon, Widget labelWidget, TabThemeConf themeConf) { - return tabBuilder!( - tab.label, - icon, - labelWidget, - themeConf, - ); + children: isHideSingleItem() + ? List.empty() + : state.value.tabs.asMap().entries.map((e) { + final index = e.key; + final tab = e.value; + return _Tab( + index: index, + label: labelGetter == null + ? Rx(tab.label) + : labelGetter!(tab.label), + selectedIcon: tab.selectedIcon, + unselectedIcon: tab.unselectedIcon, + closable: tab.closable, + selected: state.value.selected, + onClose: () { + if (tab.onTabCloseButton != null) { + tab.onTabCloseButton!(); + } else { + controller.remove(index); + } }, - ); - }).toList())); + onSelected: () => controller.jumpTo(index), + tabBuilder: tabBuilder == null + ? null + : (Widget icon, Widget labelWidget, + TabThemeConf themeConf) { + return tabBuilder!( + tab.label, + icon, + labelWidget, + themeConf, + ); + }, + ); + }).toList())); } } From b8a382a0d837fe3116d0d2dfa5a8f18f4806fac4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 23 Sep 2022 12:20:40 +0800 Subject: [PATCH 10/30] flutter_desktop: remove animation & adjust popup menu Signed-off-by: fufesou --- flutter/lib/common.dart | 12 + flutter/lib/common/widgets/address_book.dart | 7 +- flutter/lib/common/widgets/peer_card.dart | 52 +- flutter/lib/common/widgets/peers_view.dart | 12 +- .../lib/desktop/pages/connection_page.dart | 16 +- .../widgets/material_mod_popup_menu.dart | 5 +- flutter/lib/desktop/widgets/popup_menu.dart | 147 +++-- .../lib/desktop/widgets/remote_menubar.dart | 506 ++++++++++-------- 8 files changed, 478 insertions(+), 279 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 5cd54a0a8..8f8220db1 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -177,6 +177,12 @@ class MyTheme { ), splashColor: Colors.transparent, highlightColor: Colors.transparent, + splashFactory: isDesktop ? NoSplash.splashFactory : null, + textButtonTheme: isDesktop + ? TextButtonThemeData( + style: ButtonStyle(splashFactory: NoSplash.splashFactory), + ) + : null, ).copyWith( extensions: >[ ColorThemeExtension.light, @@ -192,6 +198,12 @@ class MyTheme { ), splashColor: Colors.transparent, highlightColor: Colors.transparent, + splashFactory: isDesktop ? NoSplash.splashFactory : null, + textButtonTheme: isDesktop + ? TextButtonThemeData( + style: ButtonStyle(splashFactory: NoSplash.splashFactory), + ) + : null, ).copyWith( extensions: >[ ColorThemeExtension.dark, diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 9fac81723..ab5f924dd 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -11,7 +11,8 @@ import '../../mobile/pages/settings_page.dart'; import '../../models/platform_model.dart'; class AddressBook extends StatefulWidget { - const AddressBook({Key? key}) : super(key: key); + final EdgeInsets? menuPadding; + const AddressBook({Key? key, this.menuPadding}) : super(key: key); @override State createState() { @@ -180,7 +181,9 @@ class _AddressBookState extends State { Expanded( child: Align( alignment: Alignment.topLeft, - child: AddressBookPeersView()), + child: AddressBookPeersView( + menuPadding: widget.menuPadding, + )), ) ], )); diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 9c0c997bc..2f6421880 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -14,7 +14,7 @@ import '../../desktop/widgets/popup_menu.dart'; class _PopupMenuTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension - static const double height = 25.0; + static const double height = 20.0; static const double dividerHeight = 3.0; } @@ -319,8 +319,10 @@ class _PeerCardState extends State<_PeerCard> abstract class BasePeerCard extends StatelessWidget { final Peer peer; + final EdgeInsets? menuPadding; - BasePeerCard({required this.peer, Key? key}) : super(key: key); + BasePeerCard({required this.peer, this.menuPadding, Key? key}) + : super(key: key); @override Widget build(BuildContext context) { @@ -365,6 +367,7 @@ abstract class BasePeerCard extends StatelessWidget { isRDP: isRDP, ); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -414,17 +417,25 @@ abstract class BasePeerCard extends StatelessWidget { Expanded( child: Align( alignment: Alignment.centerRight, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.edit), - onPressed: () => _rdpDialog(id), - ), + child: Transform.scale( + scale: 0.8, + child: IconButton( + icon: const Icon(Icons.edit), + padding: EdgeInsets.zero, + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + _rdpDialog(id); + }, + )), )) ], )), proc: () { connect(context, id, isRDP: true); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -439,6 +450,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { bind.mainWol(id: id); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -447,6 +459,7 @@ abstract class BasePeerCard extends StatelessWidget { Future> _forceAlwaysRelayAction(String id) async { const option = 'force-always-relay'; return MenuEntrySwitch( + switchType: SwitchType.scheckbox, text: translate('Always connect via relay'), getter: () async { return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; @@ -461,7 +474,8 @@ abstract class BasePeerCard extends StatelessWidget { } await bind.mainSetPeerOption(id: id, key: option, value: value); }, - dismissOnClicked: false, + padding: menuPadding, + dismissOnClicked: true, ); } @@ -475,6 +489,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { _rename(id, isAddressBook); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -494,6 +509,7 @@ abstract class BasePeerCard extends StatelessWidget { await reloadFunc(); }(); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -508,6 +524,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { bind.mainForgetPassword(id: id); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -528,6 +545,7 @@ abstract class BasePeerCard extends StatelessWidget { } }(); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -549,6 +567,7 @@ abstract class BasePeerCard extends StatelessWidget { } }(); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -616,7 +635,8 @@ abstract class BasePeerCard extends StatelessWidget { } class RecentPeerCard extends BasePeerCard { - RecentPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); + RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -645,8 +665,8 @@ class RecentPeerCard extends BasePeerCard { } class FavoritePeerCard extends BasePeerCard { - FavoritePeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key); + FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -677,8 +697,8 @@ class FavoritePeerCard extends BasePeerCard { } class DiscoveredPeerCard extends BasePeerCard { - DiscoveredPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key); + DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -706,8 +726,8 @@ class DiscoveredPeerCard extends BasePeerCard { } class AddressBookPeerCard extends BasePeerCard { - AddressBookPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key); + AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -748,6 +768,7 @@ class AddressBookPeerCard extends BasePeerCard { await gFFI.abModel.updateAb(); }(); }, + padding: super.menuPadding, dismissOnClicked: true, ); } @@ -762,6 +783,7 @@ class AddressBookPeerCard extends BasePeerCard { proc: () { _abEditTag(id); }, + padding: super.menuPadding, dismissOnClicked: true, ); } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index cf9c4299a..63c29af6d 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -224,7 +224,7 @@ abstract class BasePeersView extends StatelessWidget { } class RecentPeersView extends BasePeersView { - RecentPeersView({Key? key}) + RecentPeersView({Key? key, EdgeInsets? menuPadding}) : super( key: key, name: 'recent peer', @@ -232,6 +232,7 @@ class RecentPeersView extends BasePeersView { offstageFunc: (Peer peer) => false, peerCardBuilder: (Peer peer) => RecentPeerCard( peer: peer, + menuPadding: menuPadding, ), initPeers: [], ); @@ -245,7 +246,7 @@ class RecentPeersView extends BasePeersView { } class FavoritePeersView extends BasePeersView { - FavoritePeersView({Key? key}) + FavoritePeersView({Key? key, EdgeInsets? menuPadding}) : super( key: key, name: 'favorite peer', @@ -253,6 +254,7 @@ class FavoritePeersView extends BasePeersView { offstageFunc: (Peer peer) => false, peerCardBuilder: (Peer peer) => FavoritePeerCard( peer: peer, + menuPadding: menuPadding, ), initPeers: [], ); @@ -266,7 +268,7 @@ class FavoritePeersView extends BasePeersView { } class DiscoveredPeersView extends BasePeersView { - DiscoveredPeersView({Key? key}) + DiscoveredPeersView({Key? key, EdgeInsets? menuPadding}) : super( key: key, name: 'discovered peer', @@ -274,6 +276,7 @@ class DiscoveredPeersView extends BasePeersView { offstageFunc: (Peer peer) => false, peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peer: peer, + menuPadding: menuPadding, ), initPeers: [], ); @@ -287,7 +290,7 @@ class DiscoveredPeersView extends BasePeersView { } class AddressBookPeersView extends BasePeersView { - AddressBookPeersView({Key? key}) + AddressBookPeersView({Key? key, EdgeInsets? menuPadding}) : super( key: key, name: 'address book peer', @@ -296,6 +299,7 @@ class AddressBookPeersView extends BasePeersView { !_hitTag(gFFI.abModel.selectedTags, peer.tags), peerCardBuilder: (Peer peer) => AddressBookPeerCard( peer: peer, + menuPadding: menuPadding, ), initPeers: _loadPeers(), ); diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 6a8c58f7b..9ff7befd7 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -74,10 +74,18 @@ class _ConnectionPageState extends State { translate('Address Book') ], children: [ - RecentPeersView(), - FavoritePeersView(), - DiscoveredPeersView(), - const AddressBook(), + RecentPeersView( + menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + ), + FavoritePeersView( + menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + ), + DiscoveredPeersView( + menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + ), + const AddressBook( + menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + ), ], )), ], diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 1345f72f1..776a2b756 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -14,7 +14,8 @@ import 'package:flutter/material.dart'; // void setState(VoidCallback fn) { } // enum Menu { itemOne, itemTwo, itemThree, itemFour } -const Duration _kMenuDuration = Duration(milliseconds: 300); +// const Duration _kMenuDuration = Duration(milliseconds: 300); +const Duration _kMenuDuration = Duration(milliseconds: 0); const double _kMenuCloseIntervalEnd = 2.0 / 3.0; const double _kMenuHorizontalPadding = 16.0; const double _kMenuDividerHeight = 16.0; @@ -22,7 +23,7 @@ const double _kMenuDividerHeight = 16.0; const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; const double _kMenuMaxWidth = double.infinity; // const double _kMenuVerticalPadding = 8.0; -const double _kMenuVerticalPadding = 0.0; +const double _kMenuVerticalPadding = 8.0; const double _kMenuWidthStep = 0.0; //const double _kMenuScreenPadding = 8.0; const double _kMenuScreenPadding = 0.0; diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 3814561ee..84fd69b0e 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -78,7 +78,8 @@ class MyPopupMenuItemState> duration: kThemeChangeDuration, child: Container( alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: widget.height), + constraints: BoxConstraints( + minHeight: widget.height, maxHeight: widget.height), padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), child: widget.child, @@ -156,12 +157,14 @@ class MenuEntryRadios extends MenuEntryBase { final RadioCurOptionGetter curOptionGetter; final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; + final EdgeInsets? padding; MenuEntryRadios({ required this.text, required this.optionsGetter, required this.curOptionGetter, required this.optionSetter, + this.padding, dismissOnClicked = false, RxBool? enabled, }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) { @@ -189,8 +192,10 @@ class MenuEntryRadios extends MenuEntryBase { height: conf.height, child: TextButton( child: Container( + padding: padding, alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), + constraints: + BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: Row( children: [ Text( @@ -202,17 +207,22 @@ class MenuEntryRadios extends MenuEntryBase { ), Expanded( child: Align( - alignment: Alignment.centerRight, - child: SizedBox( - width: 20.0, - height: 20.0, - child: Obx(() => opt.value == curOption.value - ? Icon( - Icons.check, - color: conf.commonColor, - ) - : const SizedBox.shrink())), - )), + alignment: Alignment.centerRight, + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() => opt.value == curOption.value + ? IconButton( + padding: const EdgeInsets.fromLTRB( + 8.0, 0.0, 8.0, 0.0), + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + onPressed: () {}, + icon: Icon( + Icons.check, + color: conf.commonColor, + )) + : const SizedBox.shrink()), + ))), ], ), ), @@ -239,12 +249,14 @@ class MenuEntrySubRadios extends MenuEntryBase { final RadioCurOptionGetter curOptionGetter; final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; + final EdgeInsets? padding; MenuEntrySubRadios({ required this.text, required this.optionsGetter, required this.curOptionGetter, required this.optionSetter, + this.padding, dismissOnClicked = false, RxBool? enabled, }) : super( @@ -275,8 +287,10 @@ class MenuEntrySubRadios extends MenuEntryBase { height: conf.height, child: TextButton( child: Container( + padding: padding, alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), + constraints: + BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: Row( children: [ Text( @@ -289,14 +303,18 @@ class MenuEntrySubRadios extends MenuEntryBase { Expanded( child: Align( alignment: Alignment.centerRight, - child: SizedBox( - width: 20.0, - height: 20.0, + child: Transform.scale( + scale: MenuConfig.iconScale, child: Obx(() => opt.value == curOption.value - ? Icon( - Icons.check, - color: conf.commonColor, - ) + ? IconButton( + padding: EdgeInsets.zero, + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + onPressed: () {}, + icon: Icon( + Icons.check, + color: conf.commonColor, + )) : const SizedBox.shrink())), )), ], @@ -318,7 +336,7 @@ class MenuEntrySubRadios extends MenuEntryBase { return [ PopupMenuChildrenItem( enabled: super.enabled, - padding: EdgeInsets.zero, + padding: padding, height: conf.height, itemBuilder: (BuildContext context) => options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(), @@ -345,22 +363,31 @@ class MenuEntrySubRadios extends MenuEntryBase { } } +enum SwitchType { + sswitch, + scheckbox, +} + typedef SwitchGetter = Future Function(); typedef SwitchSetter = Future Function(bool); abstract class MenuEntrySwitchBase extends MenuEntryBase { + final SwitchType switchType; final String text; + final EdgeInsets? padding; Rx? textStyle; MenuEntrySwitchBase({ + required this.switchType, required this.text, required dismissOnClicked, this.textStyle, + this.padding, RxBool? enabled, }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled); RxBool get curOption; - Future setOption(bool option); + Future setOption(bool? option); @override List> build( @@ -376,6 +403,7 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { height: conf.height, child: TextButton( child: Container( + padding: padding, alignment: AlignmentDirectional.centerStart, height: conf.height, child: Row(children: [ @@ -386,16 +414,33 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { Expanded( child: Align( alignment: Alignment.centerRight, - child: Obx(() => Switch( - value: curOption.value, - onChanged: (v) { - if (super.dismissOnClicked && - Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(v); - }, - )), + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() { + if (switchType == SwitchType.sswitch) { + return Switch( + value: curOption.value, + onChanged: (v) { + if (super.dismissOnClicked && + Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(v); + }, + ); + } else { + return Checkbox( + value: curOption.value, + onChanged: (v) { + if (super.dismissOnClicked && + Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(v); + }, + ); + } + })), )) ])), onPressed: () { @@ -416,15 +461,19 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { final RxBool _curOption = false.obs; MenuEntrySwitch({ + required SwitchType switchType, required String text, required this.getter, required this.setter, Rx? textStyle, + EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, }) : super( + switchType: switchType, text: text, textStyle: textStyle, + padding: padding, dismissOnClicked: dismissOnClicked, enabled: enabled, ) { @@ -436,11 +485,13 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { @override RxBool get curOption => _curOption; @override - setOption(bool option) async { - await setter(option); - final opt = await getter(); - if (_curOption.value != opt) { - _curOption.value = opt; + setOption(bool? option) async { + if (option != null) { + await setter(option); + final opt = await getter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } } } } @@ -453,32 +504,40 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { final SwitchSetter setter; MenuEntrySwitch2({ + required SwitchType switchType, required String text, required this.getter, required this.setter, Rx? textStyle, + EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, }) : super( + switchType: switchType, text: text, textStyle: textStyle, + padding: padding, dismissOnClicked: dismissOnClicked); @override RxBool get curOption => getter(); @override - setOption(bool option) async { - await setter(option); + setOption(bool? option) async { + if (option != null) { + await setter(option); + } } } class MenuEntrySubMenu extends MenuEntryBase { final String text; final List> entries; + final EdgeInsets? padding; MenuEntrySubMenu({ required this.text, required this.entries, + this.padding, RxBool? enabled, }) : super(enabled: enabled); @@ -490,7 +549,7 @@ class MenuEntrySubMenu extends MenuEntryBase { PopupMenuChildrenItem( enabled: super.enabled, height: conf.height, - padding: EdgeInsets.zero, + padding: padding, position: mod_menu.PopupMenuPosition.overSide, itemBuilder: (BuildContext context) => entries .map((entry) => entry.build(context, conf)) @@ -522,10 +581,12 @@ class MenuEntrySubMenu extends MenuEntryBase { class MenuEntryButton extends MenuEntryBase { final Widget Function(TextStyle? style) childBuilder; Function() proc; + final EdgeInsets? padding; MenuEntryButton({ required this.childBuilder, required this.proc, + this.padding, dismissOnClicked = false, RxBool? enabled, }) : super( @@ -553,8 +614,10 @@ class MenuEntryButton extends MenuEntryBase { } : null, child: Container( + padding: padding, alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), + constraints: + BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: childBuilder( super.enabled!.value ? enabledStyle : disabledStyle), ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 070ad217b..67e69a1d9 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -19,7 +19,7 @@ import './material_mod_popup_menu.dart' as mod_menu; class _MenubarTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension - static const double height = 25.0; + static const double height = 20.0; static const double dividerHeight = 12.0; } @@ -367,31 +367,41 @@ class _RemoteMenubarState extends State { List> _getControlMenu(BuildContext context) { final pi = widget.ffi.ffiModel.pi; final perms = widget.ffi.ffiModel.permissions; - + const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); final List> displayMenu = []; displayMenu.addAll([ MenuEntryButton( - childBuilder: (TextStyle? style) => Row( - children: [ - Text( - translate('OS Password'), - style: style, - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.edit), - onPressed: () => showSetOSPassword( - widget.id, false, widget.ffi.dialogManager), - ), - )) - ], - ), + childBuilder: (TextStyle? style) => Container( + alignment: AlignmentDirectional.center, + height: _MenubarTheme.height, + child: Row( + children: [ + Text( + translate('OS Password'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.edit), + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + showSetOSPassword( + widget.id, false, widget.ffi.dialogManager); + })), + )) + ], + )), proc: () { showSetOSPassword(widget.id, false, widget.ffi.dialogManager); }, + padding: padding, dismissOnClicked: true, ), MenuEntryButton( @@ -402,6 +412,7 @@ class _RemoteMenubarState extends State { proc: () { connect(context, widget.id, isFileTransfer: true); }, + padding: padding, dismissOnClicked: true, ), MenuEntryButton( @@ -409,6 +420,7 @@ class _RemoteMenubarState extends State { translate('TCP Tunneling'), style: style, ), + padding: padding, proc: () { connect(context, widget.id, isTcpTunneling: true); }, @@ -427,6 +439,7 @@ class _RemoteMenubarState extends State { proc: () { showAuditDialog(widget.id, widget.ffi.dialogManager); }, + padding: padding, dismissOnClicked: true, ), ); @@ -443,6 +456,7 @@ class _RemoteMenubarState extends State { proc: () { bind.sessionCtrlAltDel(id: widget.id); }, + padding: padding, dismissOnClicked: true, )); } @@ -459,6 +473,7 @@ class _RemoteMenubarState extends State { proc: () { showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); }, + padding: padding, dismissOnClicked: true, )); } @@ -472,6 +487,7 @@ class _RemoteMenubarState extends State { proc: () { bind.sessionLockScreen(id: widget.id); }, + padding: padding, dismissOnClicked: true, )); @@ -489,6 +505,7 @@ class _RemoteMenubarState extends State { value: '${blockInput.value ? "un" : ""}block-input'); blockInput.value = !blockInput.value; }, + padding: padding, dismissOnClicked: true, )); } @@ -503,6 +520,7 @@ class _RemoteMenubarState extends State { proc: () { bind.sessionRefresh(id: widget.id); }, + padding: padding, dismissOnClicked: true, )); } @@ -523,6 +541,7 @@ class _RemoteMenubarState extends State { // } // }(); // }, + // padding: padding, // dismissOnClicked: true, // )); // } @@ -535,6 +554,7 @@ class _RemoteMenubarState extends State { proc: () { widget.ffi.cursorModel.reset(); }, + padding: padding, dismissOnClicked: true, )); } @@ -543,125 +563,155 @@ class _RemoteMenubarState extends State { } List> _getDisplayMenu(dynamic futureData) { + const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); final displayMenu = [ MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Scale original'), value: 'original'), - MenuEntryRadioOption( - text: translate('Scale adaptive'), value: 'adaptive'), - ], - curOptionGetter: () async { - return await bind.sessionGetOption( - id: widget.id, arg: 'view-style') ?? - 'adaptive'; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: "view-style", value: newValue); - widget.ffi.canvasModel.updateViewStyle(); - }), + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Scale original'), + value: 'original', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Scale adaptive'), + value: 'adaptive', + dismissOnClicked: true, + ), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'view-style') ?? + 'adaptive'; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: widget.id, name: "view-style", value: newValue); + widget.ffi.canvasModel.updateViewStyle(); + }, + padding: padding, + dismissOnClicked: true, + ), MenuEntryDivider(), MenuEntryRadios( - text: translate('Scroll Style'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('ScrollAuto'), value: 'scrollauto'), - MenuEntryRadioOption( - text: translate('Scrollbar'), value: 'scrollbar'), - ], - curOptionGetter: () async { - return await bind.sessionGetOption( - id: widget.id, arg: 'scroll-style') ?? - ''; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: "scroll-style", value: newValue); - widget.ffi.canvasModel.updateScrollStyle(); - }), + text: translate('Scroll Style'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('ScrollAuto'), + value: 'scrollauto', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Scrollbar'), + value: 'scrollbar', + dismissOnClicked: true, + ), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'scroll-style') ?? + ''; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: widget.id, name: "scroll-style", value: newValue); + widget.ffi.canvasModel.updateScrollStyle(); + }, + padding: padding, + dismissOnClicked: true, + ), MenuEntryDivider(), MenuEntryRadios( - text: translate('Image Quality'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Good image quality'), value: 'best'), - MenuEntryRadioOption( - text: translate('Balanced'), value: 'balanced'), - MenuEntryRadioOption( - text: translate('Optimize reaction time'), value: 'low'), - MenuEntryRadioOption( - text: translate('Custom'), - value: 'custom', - dismissOnClicked: true), - ], - curOptionGetter: () async { - String quality = - await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced'; - if (quality == '') quality = 'balanced'; - return quality; - }, - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetImageQuality(id: widget.id, value: newValue); - } + text: translate('Image Quality'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Good image quality'), + value: 'best', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Balanced'), + value: 'balanced', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Optimize reaction time'), + value: 'low', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Custom'), + value: 'custom', + dismissOnClicked: true), + ], + curOptionGetter: () async { + String quality = + await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced'; + if (quality == '') quality = 'balanced'; + return quality; + }, + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetImageQuality(id: widget.id, value: newValue); + } - if (newValue == 'custom') { - final btnCancel = msgBoxButton(translate('Close'), () { - widget.ffi.dialogManager.dismissAll(); - }); - final quality = - await bind.sessionGetCustomImageQuality(id: widget.id); - double initValue = quality != null && quality.isNotEmpty - ? quality[0].toDouble() - : 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 rxReplay = rxdart.ReplaySubject(); - rxReplay - .throttleTime(const Duration(milliseconds: 1000), - trailing: true, leading: false) - .listen((double v) { - () async { - await bind.sessionSetCustomImageQuality( - id: widget.id, value: v.toInt()); - }(); - }); - final slider = Obx(() { - return Slider( - value: sliderValue.value, - min: minValue, - max: maxValue, - divisions: 90, - onChanged: (double value) { - sliderValue.value = value; - rxReplay.add(value); - }, - ); - }); - final content = Row( - children: [ - slider, - SizedBox( - width: 90, - child: Obx(() => Text( - '${sliderValue.value.round()}% Bitrate', - style: const TextStyle(fontSize: 15), - ))) - ], - ); - msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - content, [btnCancel]); + if (newValue == 'custom') { + final btnCancel = msgBoxButton(translate('Close'), () { + widget.ffi.dialogManager.dismissAll(); + }); + final quality = + await bind.sessionGetCustomImageQuality(id: widget.id); + double initValue = quality != null && quality.isNotEmpty + ? quality[0].toDouble() + : 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 rxReplay = rxdart.ReplaySubject(); + rxReplay + .throttleTime(const Duration(milliseconds: 1000), + trailing: true, leading: false) + .listen((double v) { + () async { + await bind.sessionSetCustomImageQuality( + id: widget.id, value: v.toInt()); + }(); + }); + final slider = Obx(() { + return Slider( + value: sliderValue.value, + min: minValue, + max: maxValue, + divisions: 90, + onChanged: (double value) { + sliderValue.value = value; + rxReplay.add(value); + }, + ); + }); + final content = Row( + children: [ + slider, + SizedBox( + width: 90, + child: Obx(() => Text( + '${sliderValue.value.round()}% Bitrate', + style: const TextStyle(fontSize: 15), + ))) + ], + ); + msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', + content, [btnCancel]); + } + }, + padding: padding, + ), MenuEntryDivider(), ]; @@ -677,30 +727,49 @@ class _RemoteMenubarState extends State { } finally {} if (codecs.length == 2 && (codecs[0] || codecs[1])) { displayMenu.add(MenuEntryRadios( - text: translate('Codec Preference'), - optionsGetter: () { - final list = [ - MenuEntryRadioOption(text: translate('Auto'), value: 'auto'), - MenuEntryRadioOption(text: 'VP9', value: 'vp9'), - ]; - if (codecs[0]) { - list.add(MenuEntryRadioOption(text: 'H264', value: 'h264')); - } - if (codecs[1]) { - list.add(MenuEntryRadioOption(text: 'H265', value: 'h265')); - } - return list; - }, - curOptionGetter: () async { - return await bind.sessionGetOption( - id: widget.id, arg: 'codec-preference') ?? - 'auto'; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: "codec-preference", value: newValue); - bind.sessionChangePreferCodec(id: widget.id); - })); + text: translate('Codec Preference'), + optionsGetter: () { + final list = [ + MenuEntryRadioOption( + text: translate('Auto'), + value: 'auto', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: 'VP9', + value: 'vp9', + dismissOnClicked: true, + ), + ]; + if (codecs[0]) { + list.add(MenuEntryRadioOption( + text: 'H264', + value: 'h264', + dismissOnClicked: true, + )); + } + if (codecs[1]) { + list.add(MenuEntryRadioOption( + text: 'H265', + value: 'h265', + dismissOnClicked: true, + )); + } + return list; + }, + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'codec-preference') ?? + 'auto'; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: widget.id, name: "codec-preference", value: newValue); + bind.sessionChangePreferCodec(id: widget.id); + }, + padding: padding, + dismissOnClicked: true, + )); } } @@ -708,62 +777,74 @@ class _RemoteMenubarState extends State { displayMenu.add(() { final state = ShowRemoteCursorState.find(widget.id); return MenuEntrySwitch2( - text: translate('Show remote cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: widget.id, value: 'show-remote-cursor'); - }); + switchType: SwitchType.scheckbox, + text: translate('Show remote cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption( + id: widget.id, value: 'show-remote-cursor'); + }, + padding: padding, + dismissOnClicked: true, + ); }()); /// Show quality monitor displayMenu.add(MenuEntrySwitch( - text: translate('Show quality monitor'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-quality-monitor'); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'show-quality-monitor'); - widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); - })); + switchType: SwitchType.scheckbox, + text: translate('Show quality monitor'), + getter: () async { + return bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-quality-monitor'); + }, + setter: (bool v) async { + await bind.sessionToggleOption( + id: widget.id, value: 'show-quality-monitor'); + widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + }, + padding: padding, + dismissOnClicked: true, + )); final perms = widget.ffi.ffiModel.permissions; final pi = widget.ffi.ffiModel.pi; if (perms['audio'] != false) { - displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio')); + displayMenu + .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); } if (Platform.isWindows && pi.platform == 'Windows' && perms['file'] != false) { displayMenu.add(_createSwitchMenuEntry( - 'Allow file copy and paste', 'enable-file-transfer')); + 'Allow file copy and paste', 'enable-file-transfer', padding, true)); } if (perms['keyboard'] != false) { if (perms['clipboard'] != false) { - displayMenu.add( - _createSwitchMenuEntry('Disable clipboard', 'disable-clipboard')); + displayMenu.add(_createSwitchMenuEntry( + 'Disable clipboard', 'disable-clipboard', padding, true)); } displayMenu.add(_createSwitchMenuEntry( - 'Lock after session end', 'lock-after-session-end')); + 'Lock after session end', 'lock-after-session-end', padding, true)); if (pi.platform == 'Windows') { displayMenu.add(MenuEntrySwitch2( - dismissOnClicked: true, - text: translate('Privacy mode'), - getter: () { - return PrivacyModeState.find(widget.id); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'privacy-mode'); - })); + switchType: SwitchType.scheckbox, + text: translate('Privacy mode'), + getter: () { + return PrivacyModeState.find(widget.id); + }, + setter: (bool v) async { + await bind.sessionToggleOption( + id: widget.id, value: 'privacy-mode'); + }, + padding: padding, + dismissOnClicked: true, + )); } } return displayMenu; @@ -772,34 +853,39 @@ class _RemoteMenubarState extends State { List> _getKeyboardMenu() { final keyboardMenu = [ MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Legacy mode'), value: 'legacy'), - MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), - ], - curOptionGetter: () async { - return await bind.sessionGetKeyboardName(id: widget.id); - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetKeyboardMode( - id: widget.id, keyboardMode: newValue); - widget.ffi.canvasModel.updateViewStyle(); - }) + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'), + MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), + ], + curOptionGetter: () async { + return await bind.sessionGetKeyboardName(id: widget.id); + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionSetKeyboardMode( + id: widget.id, keyboardMode: newValue); + widget.ffi.canvasModel.updateViewStyle(); + }, + ) ]; return keyboardMenu; } - MenuEntrySwitch _createSwitchMenuEntry(String text, String option) { + MenuEntrySwitch _createSwitchMenuEntry( + String text, String option, EdgeInsets? padding, bool dismissOnClicked) { return MenuEntrySwitch( - text: translate(text), - getter: () async { - return bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: option); - }); + switchType: SwitchType.scheckbox, + text: translate(text), + getter: () async { + return bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: widget.id, value: option); + }, + padding: padding, + dismissOnClicked: dismissOnClicked, + ); } } From 4f9255539914518a8cff7711d61bd48b4d844f1e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 23 Sep 2022 15:12:50 +0800 Subject: [PATCH 11/30] fix connect status colors --- flutter/lib/common/widgets/peer_card.dart | 27 +++++++++---------- flutter/lib/consts.dart | 2 ++ .../lib/desktop/pages/connection_page.dart | 7 ++++- .../desktop/pages/desktop_setting_page.dart | 3 ++- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 2f6421880..f00a6864d 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -1,6 +1,7 @@ import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -156,13 +157,7 @@ class _PeerCardState extends State<_PeerCard> child: Column( children: [ Row(children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 4, 4, 4), - child: CircleAvatar( - radius: 5, - backgroundColor: peer.online - ? Colors.green - : Colors.yellow)), + getOnline(4, peer.online), Expanded( child: Text( alias.isEmpty ? formatID(peer.id) : alias, @@ -256,13 +251,7 @@ class _PeerCardState extends State<_PeerCard> children: [ Expanded( child: Row(children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 4, 8, 4), - child: CircleAvatar( - radius: 5, - backgroundColor: peer.online - ? Colors.green - : Colors.yellow)), + getOnline(4, peer.online), Expanded( child: Text( peer.alias.isEmpty ? formatID(peer.id) : peer.alias, @@ -991,3 +980,13 @@ void _rdpDialog(String id) async { ); }); } + +Widget getOnline(int rightMargin, bool online) { + return Tooltip( + message: translate(online ? 'Online' : 'Offline'), + waitDuration: const Duration(seconds: 1), + child: Padding( + padding: const EdgeInsets.fromLTRB(0, 4, 8, 4), + child: CircleAvatar( + radius: 3, backgroundColor: online ? Colors.green : kColorWarn))); +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index ff15173d3..ccacab5fb 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -11,6 +11,8 @@ const String kAppTypeDesktopPortForward = "port forward"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; +const Color kColorWarn = Color.fromARGB(255, 245, 133, 59); + const int kMobileDefaultDisplayWidth = 720; const int kMobileDefaultDisplayHeight = 1280; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 9ff7befd7..261b23f53 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -297,7 +298,11 @@ class _ConnectionPageState extends State { width: 8, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - color: svcStopped.value ? Colors.redAccent : Colors.green, + color: svcStopped.value || svcStatusCode.value == 0 + ? kColorWarn + : (svcStatusCode.value == 1 + ? Color.fromARGB(255, 50, 190, 166) + : Color.fromARGB(255, 224, 79, 95)), ), ).paddingSymmetric(horizontal: 12.0); if (svcStopped.value) { diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0918fc59b..02c2b5354 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; @@ -474,7 +475,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox(context, 'Deny remote access', 'stop-service', checkedIcon: const Icon( Icons.warning_amber_rounded, - color: Color.fromARGB(255, 255, 204, 0), + color: kColorWarn, ), enabled: enabled), Offstage( From e8587436d6e19b0493b697c38fc3d2becd76ae37 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 23 Sep 2022 16:31:50 +0800 Subject: [PATCH 12/30] refactor ThemeData --- flutter/lib/common.dart | 58 +++++-------------- flutter/lib/common/widgets/address_book.dart | 6 +- flutter/lib/common/widgets/overlay.dart | 16 ++--- flutter/lib/common/widgets/peer_card.dart | 32 ++++++---- flutter/lib/common/widgets/peer_tab_page.dart | 31 +++++----- .../lib/desktop/pages/connection_page.dart | 21 ++++--- .../lib/desktop/pages/desktop_home_page.dart | 41 +++++++------ .../desktop/pages/desktop_setting_page.dart | 8 ++- .../lib/desktop/pages/desktop_tab_page.dart | 2 +- .../lib/desktop/pages/file_manager_page.dart | 2 +- .../desktop/pages/file_manager_tab_page.dart | 2 +- .../lib/desktop/pages/port_forward_page.dart | 33 ++++++----- .../desktop/pages/port_forward_tab_page.dart | 2 +- flutter/lib/desktop/pages/remote_page.dart | 15 +++-- .../lib/desktop/pages/remote_tab_page.dart | 2 +- flutter/lib/desktop/pages/server_page.dart | 4 +- flutter/lib/desktop/widgets/popup_menu.dart | 19 +++--- flutter/lib/mobile/pages/chat_page.dart | 16 +++-- .../lib/mobile/pages/file_manager_page.dart | 12 ++-- flutter/lib/mobile/pages/home_page.dart | 6 +- flutter/lib/mobile/pages/remote_page.dart | 14 +++-- 21 files changed, 178 insertions(+), 164 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8f8220db1..170ac597b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -76,59 +76,22 @@ class IconFont { class ColorThemeExtension extends ThemeExtension { const ColorThemeExtension({ - required this.bg, - required this.grayBg, - required this.text, - required this.lightText, - required this.lighterText, - required this.placeholder, required this.border, }); - final Color? bg; - final Color? grayBg; - final Color? text; - final Color? lightText; - final Color? lighterText; - final Color? placeholder; final Color? border; static const light = ColorThemeExtension( - bg: Color(0xFFFFFFFF), - grayBg: Color(0xFFEEEEEE), - text: Color(0xFF222222), - lightText: Color(0xFF666666), - lighterText: Color(0xFF888888), - placeholder: Color(0xFFAAAAAA), border: Color(0xFFCCCCCC), ); static const dark = ColorThemeExtension( - bg: Color(0xFF252525), - grayBg: Color(0xFF141414), - text: Color(0xFFFFFFFF), - lightText: Color(0xFF999999), - lighterText: Color(0xFF777777), - placeholder: Color(0xFF555555), border: Color(0xFF555555), ); @override - ThemeExtension copyWith( - {Color? bg, - Color? grayBg, - Color? text, - Color? lightText, - Color? lighterText, - Color? placeholder, - Color? border}) { + ThemeExtension copyWith({Color? border}) { return ColorThemeExtension( - bg: bg ?? this.bg, - grayBg: grayBg ?? this.grayBg, - text: text ?? this.text, - lightText: lightText ?? this.lightText, - lighterText: lighterText ?? this.lighterText, - placeholder: placeholder ?? this.placeholder, border: border ?? this.border, ); } @@ -140,12 +103,6 @@ class ColorThemeExtension extends ThemeExtension { 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), - placeholder: Color.lerp(placeholder, other.placeholder, t), border: Color.lerp(border, other.border, t), ); } @@ -170,6 +127,13 @@ class MyTheme { static ThemeData lightTheme = ThemeData( brightness: Brightness.light, + backgroundColor: Color(0xFFFFFFFF), + scaffoldBackgroundColor: Color(0xFFEEEEEE), + textTheme: const TextTheme( + titleLarge: TextStyle(fontSize: 19, color: Colors.black87), + bodySmall: + TextStyle(fontSize: 12, color: Colors.black54, height: 1.25)), + hintColor: Color(0xFFAAAAAA), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( @@ -191,6 +155,12 @@ class MyTheme { ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, + backgroundColor: Color(0xFF252525), + scaffoldBackgroundColor: Color(0xFF141414), + textTheme: const TextTheme( + titleLarge: TextStyle(fontSize: 19), + bodySmall: TextStyle(fontSize: 12, height: 1.25)), + hintColor: Color(0xFF555555), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index ab5f924dd..47a992bd3 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -115,7 +115,8 @@ class _AddressBookState extends State { Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), - side: const BorderSide(color: MyTheme.grayBg)), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), child: Container( width: 200, height: double.infinity, @@ -215,7 +216,8 @@ class _AddressBookState extends State { child: Text( tagName, style: TextStyle( - color: rxTags.contains(tagName) ? MyTheme.white : null), + color: + rxTags.contains(tagName) ? Colors.white : null), // TODO ), ), ), diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 0e0a6ce2d..89dbe2ea6 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -26,7 +26,7 @@ class DraggableChatWindow extends StatelessWidget { position: position, width: width, height: height, - builder: (_, onPanUpdate) { + builder: (context, onPanUpdate) { return isIOS ? ChatPage(chatModel: chatModel) : Scaffold( @@ -35,16 +35,16 @@ class DraggableChatWindow extends StatelessWidget { onPanUpdate: onPanUpdate, appBar: isDesktop ? _buildDesktopAppBar() - : _buildMobileAppBar(), + : _buildMobileAppBar(context), ), body: ChatPage(chatModel: chatModel), ); }); } - Widget _buildMobileAppBar() { + Widget _buildMobileAppBar(BuildContext context) { return Container( - color: MyTheme.accent50, + color: Theme.of(context).colorScheme.primary, height: 50, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -169,17 +169,17 @@ class DraggableMobileActions extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton( - color: MyTheme.white, + color: Colors.white, onPressed: onBackPressed, splashRadius: 20, icon: const Icon(Icons.arrow_back)), IconButton( - color: MyTheme.white, + color: Colors.white, onPressed: onHomePressed, splashRadius: 20, icon: const Icon(Icons.home)), IconButton( - color: MyTheme.white, + color: Colors.white, onPressed: onRecentPressed, splashRadius: 20, icon: const Icon(Icons.more_horiz)), @@ -190,7 +190,7 @@ class DraggableMobileActions extends StatelessWidget { endIndent: 10, ), IconButton( - color: MyTheme.white, + color: Colors.white, onPressed: onHidePressed, splashRadius: 20, icon: const Icon(Icons.keyboard_arrow_down)), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 2f6421880..169e10428 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -108,7 +108,9 @@ class _PeerCardState extends State<_PeerCard> return MouseRegion( onEnter: (evt) { deco.value = BoxDecoration( - border: Border.all(color: MyTheme.button, width: _borderWidth), + border: Border.all( + color: Theme.of(context).colorScheme.secondary, + width: _borderWidth), borderRadius: peerCardUiType.value == PeerUiType.grid ? BorderRadius.circular(_cardRadis) : null); @@ -130,8 +132,9 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerTile( BuildContext context, Peer peer, Rx deco) { - final greyStyle = - TextStyle(fontSize: 11, color: MyTheme.color(context).lighterText); + final greyStyle = TextStyle( + fontSize: 11, + color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); final alias = bind.mainGetPeerOptionSync(id: peer.id, key: 'alias'); return Obx( () => Container( @@ -148,7 +151,8 @@ class _PeerCardState extends State<_PeerCard> ), Expanded( child: Container( - decoration: BoxDecoration(color: MyTheme.color(context).bg), + decoration: + BoxDecoration(color: Theme.of(context).backgroundColor), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -250,7 +254,7 @@ class _PeerCardState extends State<_PeerCard> ), ), Container( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -294,13 +298,21 @@ class _PeerCardState extends State<_PeerCard> child: CircleAvatar( radius: 14, backgroundColor: _iconMoreHover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).backgroundColor, + // ? Theme.of(context).scaffoldBackgroundColor! + // : Theme.of(context).backgroundColor!, child: Icon(Icons.more_vert, size: 18, color: _iconMoreHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText)))); + ? Theme.of(context).textTheme.titleLarge?.color + : Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5))))); + // ? MyTheme.color(context).text + // : MyTheme.color(context).lightText)))); /// Show the peer menu and handle user's choice. /// User might remove the peer or send a file to the peer. @@ -865,7 +877,7 @@ class AddressBookPeerCard extends BasePeerCard { child: Text( tagName, style: TextStyle( - color: rxTags.contains(tagName) ? MyTheme.white : null), + color: rxTags.contains(tagName) ? Colors.white : null), ), ), ), diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 3ed3dc11d..089e6acb5 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -101,6 +101,7 @@ class _PeerTabPageState extends State } Widget _createTabBar(BuildContext context) { + final textColor = Theme.of(context).textTheme.titleLarge?.color; return ListView( scrollDirection: Axis.horizontal, shrinkWrap: true, @@ -111,7 +112,7 @@ class _PeerTabPageState extends State padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: _tabIndex.value == t.key - ? MyTheme.color(context).bg + ? Theme.of(context).backgroundColor : null, borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), ), @@ -123,9 +124,9 @@ class _PeerTabPageState extends State style: TextStyle( height: 1, fontSize: 14, - color: _tabIndex.value == t.key - ? MyTheme.color(context).text - : MyTheme.color(context).lightText), + color: + _tabIndex.value == t.key ? textColor : textColor + ?..withOpacity(0.5)), ), )), onTap: () async => await _handleTabSelection(t.key), @@ -147,7 +148,8 @@ class _PeerTabPageState extends State } Widget _createPeerViewTypeSwitch(BuildContext context) { - final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); + final textColor = Theme.of(context).textTheme.titleLarge?.color; + final activeDeco = BoxDecoration(color: Theme.of(context).backgroundColor); return Row( children: [PeerUiType.grid, PeerUiType.list] .map((type) => Obx( @@ -166,9 +168,9 @@ class _PeerTabPageState extends State ? Icons.grid_view_rounded : Icons.list, size: 18, - color: peerCardUiType.value == type - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, + color: + peerCardUiType.value == type ? textColor : textColor + ?..withOpacity(0.5), )), ), )) @@ -212,7 +214,7 @@ class _PeerSearchBarState extends State { return Container( width: 120, decoration: BoxDecoration( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, borderRadius: BorderRadius.circular(6), ), child: Obx(() => Row( @@ -222,7 +224,7 @@ class _PeerSearchBarState extends State { children: [ Icon( Icons.search_rounded, - color: MyTheme.color(context).placeholder, + color: Theme.of(context).hintColor, ).marginSymmetric(horizontal: 4), Expanded( child: TextField( @@ -234,7 +236,11 @@ class _PeerSearchBarState extends State { focusNode: focusNode, textAlign: TextAlign.start, maxLines: 1, - cursorColor: MyTheme.color(context).lightText, + cursorColor: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5), cursorHeight: 18, cursorWidth: 1, style: const TextStyle(fontSize: 14), @@ -244,8 +250,7 @@ class _PeerSearchBarState extends State { hintText: focused.value ? null : translate("Search ID"), hintStyle: TextStyle( - fontSize: 14, - color: MyTheme.color(context).placeholder), + fontSize: 14, color: Theme.of(context).hintColor), border: InputBorder.none, isDense: true, ), diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 9ff7befd7..cdd4cc286 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -121,7 +121,7 @@ class _ConnectionPageState extends State { width: 320 + 20 * 2, padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), decoration: BoxDecoration( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, borderRadius: const BorderRadius.all(Radius.circular(13)), ), child: Ink( @@ -131,7 +131,10 @@ class _ConnectionPageState extends State { children: [ Text( translate('Control Remote Desktop'), - style: const TextStyle(fontSize: 19, height: 1), + style: Theme.of(context) + .textTheme + .titleLarge + ?.merge(TextStyle(height: 1)), ), ], ).marginOnly(bottom: 15), @@ -150,13 +153,12 @@ class _ConnectionPageState extends State { height: 1, ), maxLines: 1, - cursorColor: MyTheme.color(context).text!, + cursorColor: + Theme.of(context).textTheme.titleLarge?.color, decoration: InputDecoration( hintText: inputFocused.value ? null : translate('Enter Remote ID'), - hintStyle: TextStyle( - color: MyTheme.color(context).placeholder), border: OutlineInputBorder( borderRadius: BorderRadius.zero, borderSide: BorderSide( @@ -219,8 +221,11 @@ class _ConnectionPageState extends State { style: TextStyle( fontSize: 12, color: ftPressed.value - ? MyTheme.color(context).bg - : MyTheme.color(context).text), + ? Theme.of(context).backgroundColor + : Theme.of(context) + .textTheme + .titleLarge + ?.color), ).marginSymmetric(horizontal: 12), ), )), @@ -260,7 +265,7 @@ class _ConnectionPageState extends State { ), style: TextStyle( fontSize: 12, - color: MyTheme.color(context).bg), + color: Theme.of(context).backgroundColor), ), ).marginSymmetric(horizontal: 12), )), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 833a914cd..6c4af5cf8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -68,7 +68,7 @@ class _DesktopHomePageState extends State value: gFFI.serverModel, child: Container( width: 200, - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, child: Column( children: [ buildTip(context), @@ -82,7 +82,7 @@ class _DesktopHomePageState extends State buildRightPane(BuildContext context) { return Container( - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: ConnectionPage(), ); } @@ -116,7 +116,11 @@ class _DesktopHomePageState extends State translate("ID"), style: TextStyle( fontSize: 14, - color: MyTheme.color(context).lightText), + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5)), ).marginOnly(top: 5), buildPopupMenu(context) ], @@ -152,6 +156,7 @@ class _DesktopHomePageState extends State } Widget buildPopupMenu(BuildContext context) { + final textColor = Theme.of(context).textTheme.titleLarge?.color; RxBool hover = false.obs; return InkWell( onTap: () async {}, @@ -159,14 +164,12 @@ class _DesktopHomePageState extends State () => CircleAvatar( radius: 15, backgroundColor: hover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).backgroundColor, child: Icon( Icons.more_vert_outlined, size: 20, - color: hover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, + color: hover.value ? textColor : textColor?.withOpacity(0.5), ), ), ), @@ -178,6 +181,7 @@ class _DesktopHomePageState extends State final model = gFFI.serverModel; RxBool refreshHover = false.obs; RxBool editHover = false.obs; + final textColor = Theme.of(context).textTheme.titleLarge?.color; return Container( margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), child: Row( @@ -198,7 +202,7 @@ class _DesktopHomePageState extends State Text( translate("Password"), style: TextStyle( - fontSize: 14, color: MyTheme.color(context).lightText), + fontSize: 14, color: textColor?.withOpacity(0.5)), ), Row( children: [ @@ -228,8 +232,8 @@ class _DesktopHomePageState extends State () => Icon( Icons.refresh, color: refreshHover.value - ? MyTheme.color(context).text - : Color(0xFFDDDDDD), + ? textColor + : Color(0xFFDDDDDD), // TODO size: 22, ).marginOnly(right: 8, bottom: 2), ), @@ -241,8 +245,8 @@ class _DesktopHomePageState extends State () => Icon( Icons.edit, color: editHover.value - ? MyTheme.color(context).text - : Color(0xFFDDDDDD), + ? textColor + : Color(0xFFDDDDDD), // TODO size: 22, ).marginOnly(right: 8, bottom: 2), ), @@ -270,7 +274,11 @@ class _DesktopHomePageState extends State children: [ Text( translate("Your Desktop"), - style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), + style: Theme.of(context).textTheme.titleLarge, + // style: TextStyle( + // // color: MyTheme.color(context).text, + // fontWeight: FontWeight.normal, + // fontSize: 19), ), SizedBox( height: 10.0, @@ -278,10 +286,7 @@ class _DesktopHomePageState extends State Text( translate("desk_tip"), overflow: TextOverflow.clip, - style: TextStyle( - fontSize: 12, - color: MyTheme.color(context).lighterText, - height: 1.25), + style: Theme.of(context).textTheme.bodySmall, ) ], ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0918fc59b..ec386a8e2 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -68,7 +68,7 @@ class _DesktopSettingPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: Row( children: [ SizedBox( @@ -83,7 +83,7 @@ class _DesktopSettingPageState extends State const VerticalDivider(thickness: 1, width: 1), Expanded( child: Container( - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: DesktopScrollWrapper( scrollController: controller, child: PageView( @@ -802,7 +802,9 @@ Widget _Card({required String title, required List children}) { } Color? _disabledTextColor(BuildContext context, bool enabled) { - return enabled ? null : MyTheme.color(context).lighterText; + return enabled + ? null + : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6); } // ignore: non_constant_identifier_names diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 2f24ddbde..cd79e127b 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -42,7 +42,7 @@ class _DesktopTabPageState extends State { OverlayEntry(builder: (context) { gFFI.dialogManager.setOverlayState(Overlay.of(context)); return Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, tail: ActionIcon( diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index ced61e8d9..f44088d6e 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -104,7 +104,7 @@ class _FileManagerPageState extends State return false; }, child: Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: Row( children: [ Flexible(flex: 3, child: body(isLocal: true)), diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 9b8060bb7..24a36eddb 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -72,7 +72,7 @@ class _FileManagerTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 28aa8d3cf..ccbf1805e 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -70,7 +70,7 @@ class _PortForwardPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( - backgroundColor: MyTheme.color(context).grayBg, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: FutureBuilder(future: () async { if (!widget.isRDP) { refreshTunnelConfig(); @@ -80,7 +80,8 @@ class _PortForwardPageState extends State return Container( decoration: BoxDecoration( border: Border.all( - width: 20, color: MyTheme.color(context).grayBg!)), + width: 20, + color: Theme.of(context).scaffoldBackgroundColor)), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -88,7 +89,7 @@ class _PortForwardPageState extends State Flexible( child: Container( decoration: BoxDecoration( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, border: Border.all(width: 1, color: MyTheme.border)), child: widget.isRDP ? buildRdp(context) : buildTunnel(context), @@ -131,7 +132,7 @@ class _PortForwardPageState extends State return Theme( data: Theme.of(context) - .copyWith(backgroundColor: MyTheme.color(context).bg), + .copyWith(backgroundColor: Theme.of(context).backgroundColor), child: Obx(() => ListView.builder( controller: ScrollController(), itemCount: pfs.length + 2, @@ -139,7 +140,7 @@ class _PortForwardPageState extends State if (index == 0) { return Container( height: 25, - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: Row(children: [ text('Local Port'), const SizedBox(width: _kColumn1Width), @@ -166,7 +167,7 @@ class _PortForwardPageState extends State return Container( height: _kRowHeight, - decoration: BoxDecoration(color: MyTheme.color(context).bg), + decoration: BoxDecoration(color: Theme.of(context).backgroundColor), child: Row(children: [ buildTunnelInputCell(context, controller: localPortController, @@ -216,11 +217,12 @@ class _PortForwardPageState extends State {required TextEditingController controller, List? inputFormatters, String? hint}) { + final textColor = Theme.of(context).textTheme.titleLarge?.color; return Expanded( child: TextField( controller: controller, inputFormatters: inputFormatters, - cursorColor: MyTheme.color(context).text, + cursorColor: textColor, cursorHeight: 20, cursorWidth: 1, decoration: InputDecoration( @@ -228,12 +230,12 @@ class _PortForwardPageState extends State borderSide: BorderSide(color: MyTheme.color(context).border!)), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: MyTheme.color(context).border!)), - fillColor: MyTheme.color(context).bg, + fillColor: Theme.of(context).backgroundColor, contentPadding: const EdgeInsets.all(10), hintText: hint, - hintStyle: TextStyle( - color: MyTheme.color(context).placeholder, fontSize: 16)), - style: TextStyle(color: MyTheme.color(context).text, fontSize: 16), + hintStyle: + TextStyle(color: Theme.of(context).hintColor, fontSize: 16)), + style: TextStyle(color: textColor, fontSize: 16), ).marginAll(10), ); } @@ -250,7 +252,7 @@ class _PortForwardPageState extends State ? MyTheme.currentThemeMode() == ThemeMode.dark ? const Color(0xFF202020) : const Color(0xFFF4F5F6) - : MyTheme.color(context).bg), + : Theme.of(context).backgroundColor), child: Row(children: [ text(pf.localPort.toString()), const SizedBox(width: _kColumn1Width), @@ -292,7 +294,7 @@ class _PortForwardPageState extends State ).marginOnly(left: _kTextLeftMargin)); return Theme( data: Theme.of(context) - .copyWith(backgroundColor: MyTheme.color(context).bg), + .copyWith(backgroundColor: Theme.of(context).backgroundColor), child: ListView.builder( controller: ScrollController(), itemCount: 2, @@ -300,7 +302,7 @@ class _PortForwardPageState extends State if (index == 0) { return Container( height: 25, - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: Row(children: [ text1('Local Port'), const SizedBox(width: _kColumn1Width), @@ -311,7 +313,8 @@ class _PortForwardPageState extends State } else { return Container( height: _kRowHeight, - decoration: BoxDecoration(color: MyTheme.color(context).bg), + decoration: + BoxDecoration(color: Theme.of(context).backgroundColor), child: Row(children: [ Expanded( child: Align( diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index d4f17aaef..6c6865718 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -80,7 +80,7 @@ class _PortForwardTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, onWindowCloseButton: () async { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dad286ee7..74ff9b7af 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -11,7 +11,6 @@ import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; -import '../../consts.dart'; import '../widgets/remote_menubar.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; @@ -45,7 +44,6 @@ class _RemotePageState extends State late RxBool _keyboardEnabled; final FocusNode _rawKeyFocusNode = FocusNode(); - var _isPhysicalMouse = false; var _imageFocused = false; Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -138,7 +136,7 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -443,6 +441,7 @@ class ImagePainter extends CustomPainter { } class QualityMonitor extends StatelessWidget { + static const textStyle = TextStyle(color: MyTheme.grayBg); final QualityMonitorModel qualityMonitorModel; QualityMonitor(this.qualityMonitorModel); @@ -462,23 +461,23 @@ class QualityMonitor extends StatelessWidget { children: [ Text( "Speed: ${qualityMonitorModel.data.speed ?? ''}", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), Text( "FPS: ${qualityMonitorModel.data.fps ?? ''}", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), Text( "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), Text( "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), Text( "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), ], ), diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index b086a2e35..e84b574fd 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -91,7 +91,7 @@ class _ConnectionTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, showTabBar: fullscreen.isFalse, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 9ca446dcb..68d5c3b33 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -69,7 +69,7 @@ class _DesktopServerPageState extends State OverlayEntry(builder: (context) { gFFI.dialogManager.setOverlayState(Overlay.of(context)); return Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -145,7 +145,7 @@ class ConnectionManagerState extends State { windowManager.startDragging(); }, child: Container( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, ), ), ), diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 84fd69b0e..71d1ec417 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -1,7 +1,6 @@ import 'dart:core'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/common.dart'; import 'package:get/get.dart'; import './material_mod_popup_menu.dart' as mod_menu; @@ -201,7 +200,7 @@ class MenuEntryRadios extends MenuEntryBase { Text( opt.text, style: TextStyle( - color: MyTheme.color(context).text, + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -296,7 +295,7 @@ class MenuEntrySubRadios extends MenuEntryBase { Text( opt.text, style: TextStyle( - color: MyTheme.color(context).text, + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -345,7 +344,7 @@ class MenuEntrySubRadios extends MenuEntryBase { Text( text, style: TextStyle( - color: MyTheme.color(context).text, + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -392,8 +391,8 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { @override List> build( BuildContext context, MenuConfig conf) { - textStyle ??= const TextStyle( - color: Colors.black, + textStyle ??= TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal) .obs; @@ -560,7 +559,9 @@ class MenuEntrySubMenu extends MenuEntryBase { Obx(() => Text( text, style: TextStyle( - color: super.enabled!.value ? Colors.black : Colors.grey, + color: super.enabled!.value + ? Theme.of(context).textTheme.titleLarge?.color + : Colors.grey, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), )), @@ -595,8 +596,8 @@ class MenuEntryButton extends MenuEntryBase { ); Widget _buildChild(BuildContext context, MenuConfig conf) { - const enabledStyle = TextStyle( - color: Colors.black, + final enabledStyle = TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal); const disabledStyle = TextStyle( diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index 2151f17be..8ac5ce313 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -45,7 +45,7 @@ class ChatPage extends StatelessWidget implements PageShape { return ChangeNotifierProvider.value( value: chatModel, child: Container( - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: Consumer(builder: (context, chatModel, child) { final currentUser = chatModel.currentUser; return Stack( @@ -62,11 +62,17 @@ class ChatPage extends StatelessWidget implements PageShape { inputOptions: InputOptions( sendOnEnter: true, inputDecoration: defaultInputDecoration( - fillColor: MyTheme.color(context).bg), + fillColor: Theme.of(context).backgroundColor), sendButtonBuilder: defaultSendButton( - color: MyTheme.color(context).text!), - inputTextStyle: - TextStyle(color: MyTheme.color(context).text)), + color: Theme.of(context) + .textTheme + .titleLarge! + .color!), + inputTextStyle: TextStyle( + color: Theme.of(context) + .textTheme + .titleLarge + ?.color)), messageOptions: MessageOptions( showOtherUsersAvatar: false, showTime: true, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index dd1cbb83f..13059d188 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -58,7 +58,7 @@ class _FileManagerPageState extends State { return false; }, child: Scaffold( - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, appBar: AppBar( leading: Row(children: [ IconButton( @@ -69,7 +69,7 @@ class _FileManagerPageState extends State { title: ToggleSwitch( initialLabelIndex: model.isLocal ? 0 : 1, activeBgColor: [MyTheme.idColor], - inactiveBgColor: MyTheme.grayBg, + // inactiveBgColor: MyTheme.grayBg, inactiveFgColor: Colors.black54, totalSwitches: 2, minWidth: 100, @@ -465,6 +465,9 @@ class _FileManagerPageState extends State { ); case JobState.none: break; + case JobState.paused: + // TODO: Handle this case. + break; } return null; } @@ -530,8 +533,7 @@ class BottomSheetBody extends StatelessWidget { children: [ Text(title, style: TextStyle(fontSize: 18)), Text(text, - style: TextStyle( - fontSize: 14, color: MyTheme.grayBg)) + style: TextStyle(fontSize: 14)) // TODO color ], ) ], @@ -548,7 +550,7 @@ class BottomSheetBody extends StatelessWidget { )); }, onClosing: () {}, - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, enableDrag: false, ); } diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 05a6d6b51..31240b895 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -59,7 +59,7 @@ class _HomePageState extends State { return false; }, child: Scaffold( - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, appBar: AppBar( centerTitle: true, title: Text("RustDesk"), @@ -73,7 +73,7 @@ class _HomePageState extends State { .toList(), currentIndex: _selectedIndex, type: BottomNavigationBarType.fixed, - selectedItemColor: MyTheme.accent, + selectedItemColor: MyTheme.accent, // unselectedItemColor: MyTheme.darkGray, onTap: (index) => setState(() { // close chat overlay when go chat page @@ -95,7 +95,7 @@ class WebHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, appBar: AppBar( centerTitle: true, title: Text("RustDesk" + (isWeb ? " (Beta) " : "")), diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 94f584109..e16035175 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -752,7 +752,7 @@ class _RemotePageState extends State { void changeTouchMode() { setState(() => _showEdit = false); showModalBottomSheet( - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, isScrollControlled: true, context: context, shape: const RoundedRectangleBorder( @@ -968,7 +968,9 @@ class ImagePainter extends CustomPainter { } } +// TODO global widget class QualityMonitor extends StatelessWidget { + static final textColor = Colors.grey.shade200; @override Widget build(BuildContext context) => ChangeNotifierProvider.value( value: gFFI.qualityMonitorModel, @@ -985,23 +987,23 @@ class QualityMonitor extends StatelessWidget { children: [ Text( "Speed: ${qualityMonitorModel.data.speed ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), Text( "FPS: ${qualityMonitorModel.data.fps ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), Text( "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), Text( "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), Text( "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), ], ), From 02adf7104dee54766863c368e21fa5218b223edf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 23 Sep 2022 16:37:17 +0800 Subject: [PATCH 13/30] feat: add shadow on linux --- flutter/linux/my_application.cc | 7 ++++++- flutter/pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 6d101687b..05d420342 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -59,7 +59,12 @@ static void my_application_activate(GApplication* application) { FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + auto border_frame = gtk_frame_new(nullptr); + gtk_frame_set_shadow_type(GTK_FRAME(border_frame), GTK_SHADOW_ETCHED_IN); + gtk_container_add(GTK_CONTAINER(border_frame), GTK_WIDGET(view)); + gtk_widget_show(GTK_WIDGET(border_frame)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(border_frame)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 05c711dcf..c87bb02fb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 + ref: 1818097611168f6148317f4c527aa45ff29d5850 freezed_annotation: ^2.0.3 tray_manager: git: From cf31ec3a53d3946e9ee630c3497f73075aeb3b00 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 23 Sep 2022 16:56:28 +0800 Subject: [PATCH 14/30] fix mobile build --- src/ui_interface.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index dc3a02c7a..3d076255f 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -338,10 +338,13 @@ pub fn set_socks(proxy: String, username: String, password: String) { .ok(); } -#[cfg(not(any(target_os = "android", target_os = "ios")))] #[inline] pub fn is_installed() -> bool { - crate::platform::is_installed() + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + return crate::platform::is_installed(); + } + false } #[inline] From d2d531516aa2bd5813a8518472f55dc5bf90a017 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 23 Sep 2022 17:16:25 +0800 Subject: [PATCH 15/30] opt mobile dark theme --- flutter/lib/common.dart | 2 +- flutter/lib/common/widgets/peer_tab_page.dart | 8 +++---- flutter/lib/mobile/pages/connection_page.dart | 4 ++-- flutter/lib/mobile/pages/server_page.dart | 21 +++++++++++-------- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 170ac597b..dcdc191ff 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -160,7 +160,7 @@ class MyTheme { textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19), bodySmall: TextStyle(fontSize: 12, height: 1.25)), - hintColor: Color(0xFF555555), + cardColor: Color(0xFF252525), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 089e6acb5..835849ae4 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -201,9 +201,9 @@ class _PeerSearchBarState extends State { drawer = true; }); }, - icon: const Icon( + icon: Icon( Icons.search_rounded, - color: MyTheme.dark, + color: Theme.of(context).hintColor, )); } @@ -267,9 +267,9 @@ class _PeerSearchBarState extends State { drawer = false; }); }, - icon: const Icon( + icon: Icon( Icons.close, - color: MyTheme.dark, + color: Theme.of(context).hintColor, )), ], ), diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 6156223b5..af84a8a47 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -128,8 +128,8 @@ class _ConnectionPageState extends State { child: Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2), child: Ink( - decoration: const BoxDecoration( - color: MyTheme.white, + decoration: BoxDecoration( + color: Theme.of(context).cardColor, borderRadius: BorderRadius.all(Radius.circular(13)), ), child: Row( diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 4fdd00ede..bed043b25 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -170,7 +170,7 @@ class ServerInfo extends StatelessWidget { icon: const Icon(Icons.perm_identity), labelText: translate("ID"), labelStyle: const TextStyle( - fontWeight: FontWeight.bold, color: MyTheme.accent50), + fontWeight: FontWeight.bold, color: MyTheme.accent80), ), onSaved: (String? value) {}, ), @@ -185,7 +185,7 @@ class ServerInfo extends StatelessWidget { icon: const Icon(Icons.lock), labelText: translate("Password"), labelStyle: const TextStyle( - fontWeight: FontWeight.bold, color: MyTheme.accent50), + fontWeight: FontWeight.bold, color: MyTheme.accent80), suffix: isPermanent ? null : IconButton( @@ -213,7 +213,7 @@ class ServerInfo extends StatelessWidget { fontFamily: 'WorkSans', fontWeight: FontWeight.bold, fontSize: 18, - color: MyTheme.accent80, + color: MyTheme.accent, ), )) ], @@ -304,7 +304,8 @@ class _PermissionCheckerState extends State { softWrap: true, style: const TextStyle( fontSize: 14.0, - color: MyTheme.accent50))) + fontWeight: FontWeight.w500, + color: MyTheme.accent80))) ], ) : const SizedBox.shrink()) @@ -334,7 +335,9 @@ class PermissionRow extends StatelessWidget { alignment: Alignment.centerLeft, child: Text(name, style: const TextStyle( - fontSize: 16.0, color: MyTheme.accent50)))), + fontSize: 16.0, + fontWeight: FontWeight.bold, + color: MyTheme.accent80)))), Expanded( flex: 2, child: FittedBox( @@ -398,7 +401,7 @@ class ConnectionManager extends StatelessWidget { }, icon: const Icon( Icons.chat, - color: MyTheme.accent80, + color: MyTheme.accent, ))) ], ), @@ -460,8 +463,8 @@ class PaddingCard extends StatelessWidget { titleIcon != null ? Padding( padding: const EdgeInsets.only(right: 10), - child: Icon(titleIcon, - color: MyTheme.accent80, size: 30)) + child: + Icon(titleIcon, color: MyTheme.accent, size: 30)) : const SizedBox.shrink(), Text( title!, @@ -469,7 +472,7 @@ class PaddingCard extends StatelessWidget { fontFamily: 'WorkSans', fontWeight: FontWeight.bold, fontSize: 20, - color: MyTheme.accent80, + color: MyTheme.accent, ), ) ], From 2e9ff13ed488c695ef8153b21289e4f28f9ed949 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 23 Sep 2022 17:28:22 +0800 Subject: [PATCH 16/30] button widget and preparing help cards --- .../lib/desktop/pages/connection_page.dart | 88 ++----------------- .../lib/desktop/pages/desktop_home_page.dart | 46 ++++++++++ flutter/lib/desktop/widgets/button.dart | 73 +++++++++++++++ 3 files changed, 128 insertions(+), 79 deletions(-) create mode 100644 flutter/lib/desktop/widgets/button.dart diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 261b23f53..0b17c5f47 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -14,6 +14,7 @@ import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/peers_view.dart'; import '../../models/platform_model.dart'; +import '../widgets/button.dart'; /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget { @@ -109,10 +110,6 @@ class _ConnectionPageState extends State { /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. Widget _buildRemoteIDTextField(BuildContext context) { - RxBool ftHover = false.obs; - RxBool ftPressed = false.obs; - RxBool connHover = false.obs; - RxBool connPressed = false.obs; RxBool inputFocused = false.obs; FocusNode focusNode = FocusNode(); focusNode.addListener(() { @@ -189,84 +186,17 @@ class _ConnectionPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Obx(() => InkWell( - onTapDown: (_) => ftPressed.value = true, - onTapUp: (_) => ftPressed.value = false, - onTapCancel: () => ftPressed.value = false, - onHover: (value) => ftHover.value = value, - onTap: () { - onConnect(isFileTransfer: true); - }, - child: Container( - height: 27, - alignment: Alignment.center, - decoration: BoxDecoration( - color: ftPressed.value - ? MyTheme.accent - : Colors.transparent, - border: Border.all( - color: ftPressed.value - ? MyTheme.accent - : ftHover.value - ? MyTheme.hoverBorder - : MyTheme.border, - ), - borderRadius: BorderRadius.circular(5), - ), - child: Text( - translate( - "Transfer File", - ), - style: TextStyle( - fontSize: 12, - color: ftPressed.value - ? MyTheme.color(context).bg - : MyTheme.color(context).text), - ).marginSymmetric(horizontal: 12), - ), - )), + Button( + isOutline: true, + onTap: () { + onConnect(isFileTransfer: true); + }, + text: "Transfer File", + ), const SizedBox( width: 17, ), - Obx( - () => InkWell( - onTapDown: (_) => connPressed.value = true, - onTapUp: (_) => connPressed.value = false, - onTapCancel: () => connPressed.value = false, - onHover: (value) => connHover.value = value, - onTap: onConnect, - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: 80.0, - ), - child: Container( - height: 27, - decoration: BoxDecoration( - color: connPressed.value - ? MyTheme.accent - : MyTheme.button, - border: Border.all( - color: connPressed.value - ? MyTheme.accent - : connHover.value - ? MyTheme.hoverBorder - : MyTheme.button, - ), - borderRadius: BorderRadius.circular(5), - ), - child: Center( - child: Text( - translate( - "Connect", - ), - style: TextStyle( - fontSize: 12, - color: MyTheme.color(context).bg), - ), - ).marginSymmetric(horizontal: 12), - )), - ), - ), + Button(onTap: onConnect, text: "Connect"), ], ), ) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 833a914cd..e39e4f372 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -26,6 +26,7 @@ class _DesktopHomePageState extends State with TrayListener, WindowListener, AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + var updateUrl = ''; @override void onWindowClose() async { @@ -74,6 +75,7 @@ class _DesktopHomePageState extends State buildTip(context), buildIDBoard(context), buildPasswordBoard(context), + buildHelpCards(), ], ), ), @@ -288,6 +290,46 @@ class _DesktopHomePageState extends State ); } + Widget buildHelpCards() { + if (Platform.isWindows) { + if (!bind.mainIsInstalled()) { + return buildInstallCard(); + } else if (bind.mainIsInstalledLowerVersion()) { + return buildUpgradeCard(); + } + } + if (updateUrl.isNotEmpty) { + return buildUpdateCard(); + } + if (Platform.isMacOS) {} + if (bind.mainIsInstalledLowerVersion()) {} + return Container(); + } + + Widget buildUpdateCard() { + return Container(); + } + + Widget buildUpgradeCard() { + return Container(); + } + + Widget buildInstallCard() { + return Container( + margin: EdgeInsets.only(top: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("install_tip"), + style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), + ), + ], + ), + ); + } + @override void onTrayMenuItemClick(MenuItem menuItem) { debugPrint('click ${menuItem.key}'); @@ -305,6 +347,10 @@ class _DesktopHomePageState extends State @override void initState() { super.initState(); + Timer(const Duration(seconds: 5), () async { + updateUrl = await bind.mainGetSoftwareUpdateUrl(); + if (updateUrl.isNotEmpty) setState(() {}); + }); trayManager.addListener(this); windowManager.addListener(this); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { diff --git a/flutter/lib/desktop/widgets/button.dart b/flutter/lib/desktop/widgets/button.dart new file mode 100644 index 000000000..dc0cc6a2c --- /dev/null +++ b/flutter/lib/desktop/widgets/button.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class Button extends StatefulWidget { + GestureTapCallback onTap; + String text; + double? minWidth; + bool isOutline; + + Button({ + Key? key, + this.minWidth, + this.isOutline = false, + required this.onTap, + required this.text, + }) : super(key: key); + + @override + State