From 0c407994cd67191ec04d8325ca91ec03429be38f Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 19 Sep 2022 17:03:12 +0800 Subject: [PATCH 1/5] fix android deps build --- flutter/android/gradle/wrapper/gradle-wrapper.properties | 2 +- flutter/pubspec.lock | 2 +- flutter/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0..cc5527d78 100644 --- a/flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 7c0b90e54..bb27d243e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1138,7 +1138,7 @@ packages: name: wakelock url: "https://pub.dartlang.org" source: hosted - version: "0.5.6" + version: "0.6.2" wakelock_macos: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 09025ad0e..91c3b4164 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: external_path: ^1.0.1 provider: ^6.0.3 tuple: ^2.0.0 - wakelock: ^0.5.2 + wakelock: ^0.6.2 device_info_plus: ^4.1.2 #firebase_analytics: ^9.1.5 package_info_plus: ^1.4.2 From 9e6e842247070662cdeb208f4fe9f37c51c2ad6c Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 19 Sep 2022 20:26:39 +0800 Subject: [PATCH 2/5] refactor: move peer_widget / peercard_widget / peer_tab_page & move connect new address_book class; add peer tab onPageChanged android settings_page.dart add dark mode opt peer_tab_page search bar, add mobile peer_tab support --- flutter/lib/common.dart | 26 + flutter/lib/common/widgets/address_book.dart | 415 ++++++++++++ flutter/lib/common/widgets/peer_tab_page.dart | 271 ++++++++ .../widgets/peer_widget.dart | 67 +- .../widgets/peercard_widget.dart | 53 +- flutter/lib/consts.dart | 4 + .../lib/desktop/pages/connection_page.dart | 638 +----------------- .../lib/desktop/widgets/remote_menubar.dart | 1 - flutter/lib/desktop/widgets/utils.dart | 28 - flutter/lib/mobile/pages/connection_page.dart | 274 +++----- flutter/lib/mobile/pages/settings_page.dart | 16 +- flutter/lib/models/ab_model.dart | 2 +- 12 files changed, 927 insertions(+), 868 deletions(-) create mode 100644 flutter/lib/common/widgets/address_book.dart create mode 100644 flutter/lib/common/widgets/peer_tab_page.dart rename flutter/lib/{desktop => common}/widgets/peer_widget.dart (80%) rename flutter/lib/{desktop => common}/widgets/peercard_widget.dart (94%) delete mode 100644 flutter/lib/desktop/widgets/utils.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c07764e32..70e10a9da 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1007,3 +1007,29 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { } return false; } + +/// Connect to a peer with [id]. +/// If [isFileTransfer], starts a session only for file transfer. +/// If [isTcpTunneling], starts a session only for tcp tunneling. +/// If [isRDP], starts a session only for rdp. +void connect(BuildContext context, String id, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false}) async { + if (id == '') return; + id = id.replaceAll(' ', ''); + 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); + } else { + await rustDeskWinManager.newRemoteDesktop(id); + } + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart new file mode 100644 index 000000000..beecf47f2 --- /dev/null +++ b/flutter/lib/common/widgets/address_book.dart @@ -0,0 +1,415 @@ +import 'package:contextmenu/contextmenu.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peer_widget.dart'; +import 'package:flutter_hbb/models/ab_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; + +import '../../common.dart'; +import '../../desktop/pages/desktop_home_page.dart'; +import '../../models/platform_model.dart'; + +class AddressBook extends StatefulWidget { + const AddressBook({Key? key}) : super(key: key); + + @override + State createState() { + return _AddressBookState(); + } +} + +class _AddressBookState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.getAb()); + } + + @override + Widget build(BuildContext context) => FutureBuilder( + future: buildAddressBook(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const Offstage(); + } + }); + + handleLogin() { + loginDialog().then((success) { + if (success) { + setState(() {}); + } + }); + } + + Future buildAddressBook(BuildContext context) async { + final token = await bind.mainGetLocalOption(key: 'access_token'); + if (token.trim().isEmpty) { + return Center( + child: InkWell( + onTap: handleLogin, + child: Text( + translate("Login"), + style: const TextStyle(decoration: TextDecoration.underline), + ), + ), + ); + } + final model = gFFI.abModel; + return FutureBuilder( + future: model.getAb(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return _buildAddressBook(context); + } else if (snapshot.hasError) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate("${snapshot.error}")), + TextButton( + onPressed: () { + setState(() {}); + }, + child: Text(translate("Retry"))) + ], + ); + } else { + if (model.abLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (model.abError.isNotEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate(model.abError)), + TextButton( + onPressed: () { + setState(() {}); + }, + child: Text(translate("Retry"))) + ], + ), + ); + } else { + return const Offstage(); + } + } + }); + } + + Widget _buildAddressBook(BuildContext context) { + return Consumer( + builder: (context, model, child) => Row( + children: [ + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: MyTheme.grayBg)), + child: Container( + width: 200, + height: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(translate('Tags')), + InkWell( + child: PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + value: 'add-id', + child: Text(translate("Add ID")), + ), + PopupMenuItem( + value: 'add-tag', + child: Text(translate("Add Tag")), + ), + PopupMenuItem( + value: 'unset-all-tag', + child: Text( + translate("Unselect all tags")), + ), + ], + onSelected: handleAbOp, + child: const Icon(Icons.more_vert_outlined)), + ) + ], + ), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray)), + child: Obx( + () => Wrap( + children: gFFI.abModel.tags + .map((e) => + buildTag(e, gFFI.abModel.selectedTags, + onTap: () { + // + if (gFFI.abModel.selectedTags + .contains(e)) { + gFFI.abModel.selectedTags.remove(e); + } else { + gFFI.abModel.selectedTags.add(e); + } + })) + .toList(), + ), + ), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: AddressBookPeerWidget()), + ) + ], + )); + } + + Widget buildTag(String tagName, RxList rxTags, {Function()? onTap}) { + return ContextMenuArea( + width: 100, + builder: (context) => [ + ListTile( + title: Text(translate("Delete")), + onTap: () { + gFFI.abModel.deleteTag(tagName); + gFFI.abModel.updateAb(); + Future.delayed(Duration.zero, () => Get.back()); + }, + ) + ], + child: GestureDetector( + onTap: onTap, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: rxTags.contains(tagName) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(10)), + margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text( + tagName, + style: TextStyle( + color: rxTags.contains(tagName) ? MyTheme.white : null), + ), + ), + ), + ), + ); + } + + /// tag operation + void handleAbOp(String value) { + if (value == 'add-id') { + abAddId(); + } else if (value == 'add-tag') { + abAddTag(); + } else if (value == 'unset-all-tag') { + gFFI.abModel.unsetSelectedTags(); + } + } + + void abAddId() async { + var field = ""; + var msg = ""; + var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); + + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final ids = field.trim().split(RegExp(r"[\s,;\n]+")); + field = ids.join(','); + for (final newId in ids) { + if (gFFI.abModel.idContainBy(newId)) { + continue; + } + gFFI.abModel.addId(newId); + } + await gFFI.abModel.updateAb(); + this.setState(() {}); + // final currentPeers + } + close(); + } + + return CustomAlertDialog( + title: Text(translate("Add ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + focusNode: FocusNode()..requestFocus()), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + void abAddTag() async { + var field = ""; + var msg = ""; + var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final tags = field.trim().split(RegExp(r"[\s,;\n]+")); + field = tags.join(','); + for (final tag in tags) { + gFFI.abModel.addTag(tag); + } + await gFFI.abModel.updateAb(); + // final currentPeers + } + close(); + } + + return CustomAlertDialog( + title: Text(translate("Add Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + focusNode: FocusNode()..requestFocus(), + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + void abEditTag(String id) { + var isInProgress = false; + + final tags = List.of(gFFI.abModel.tags); + var selectedTag = gFFI.abModel.getPeerTags(id).obs; + + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + } + + return CustomAlertDialog( + title: Text(translate("Edit Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Wrap( + children: tags + .map((e) => buildTag(e, selectedTag, onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + })) + .toList(growable: false), + ), + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } +} diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart new file mode 100644 index 000000000..fbda91649 --- /dev/null +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -0,0 +1,271 @@ +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/consts.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; +import '../../models/platform_model.dart'; + +class PeerTabPage extends StatefulWidget { + final List tabs; + final List children; + const PeerTabPage({required this.tabs, required this.children, Key? key}) + : super(key: key); + @override + State createState() => _PeerTabPageState(); +} + +class _PeerTabPageState extends State + with SingleTickerProviderStateMixin { + late final PageController _pageController = PageController(); + final RxInt _tabIndex = 0.obs; + + @override + void initState() { + super.initState(); + } + + // hard code for now + void _handleTabSelection(int index) { + // reset search text + peerSearchText.value = ""; + peerSearchTextController.clear(); + _tabIndex.value = index; + _pageController.jumpToPage(index); + switch (index) { + case 0: + bind.mainLoadRecentPeers(); + break; + case 1: + bind.mainLoadFavPeers(); + break; + case 2: + bind.mainDiscover(); + break; + case 3: + gFFI.abModel.getAb(); + break; + } + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + textBaseline: TextBaseline.ideographic, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 28, + child: Container( + constraints: isDesktop ? null : kMobilePageConstraints, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: _createTabBar(context)), + const SizedBox(width: 10), + const PeerSearchBar(), + Offstage( + offstage: !isDesktop, + child: _createPeerViewTypeSwitch(context) + .marginOnly(left: 13)), + ], + )), + ), + _createTabBarView(), + ], + ); + } + + Widget _createTabBar(BuildContext context) { + return ListView( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + controller: ScrollController(), + children: super.widget.tabs.asMap().entries.map((t) { + return Obx(() => GestureDetector( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: _tabIndex.value == t.key + ? MyTheme.color(context).bg + : null, + borderRadius: BorderRadius.circular(2), + ), + child: Align( + alignment: Alignment.center, + child: Text( + t.value, + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: _tabIndex.value == t.key + ? MyTheme.color(context).text + : MyTheme.color(context).lightText), + ), + )), + onTap: () => _handleTabSelection(t.key), + )); + }).toList()); + } + + Widget _createTabBarView() { + final verticalMargin = isDesktop ? 12.0 : 6.0; + return Expanded( + child: PageView( + physics: const BouncingScrollPhysics(), + controller: _pageController, + children: super.widget.children, + onPageChanged: (to) => _tabIndex.value = to) + .marginSymmetric(vertical: verticalMargin)); + } + + Widget _createPeerViewTypeSwitch(BuildContext context) { + final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); + return Row( + children: [ + Obx( + () => Container( + padding: const EdgeInsets.all(4.0), + decoration: + peerCardUiType.value == PeerUiType.grid ? activeDeco : null, + child: InkWell( + onTap: () { + peerCardUiType.value = PeerUiType.grid; + }, + child: Icon( + Icons.grid_view_rounded, + size: 18, + color: peerCardUiType.value == PeerUiType.grid + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + )), + ), + ), + Obx( + () => Container( + padding: const EdgeInsets.all(4.0), + decoration: + peerCardUiType.value == PeerUiType.list ? activeDeco : null, + child: InkWell( + onTap: () { + peerCardUiType.value = PeerUiType.list; + }, + child: Icon( + Icons.list, + size: 18, + color: peerCardUiType.value == PeerUiType.list + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + )), + ), + ), + ], + ); + } +} + +class PeerSearchBar extends StatefulWidget { + const PeerSearchBar({Key? key}) : super(key: key); + + @override + State createState() => _PeerSearchBarState(); +} + +class _PeerSearchBarState extends State { + var drawer = false; + + @override + Widget build(BuildContext context) { + return drawer + ? _buildSearchBar() + : IconButton( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 2), + onPressed: () { + setState(() { + drawer = true; + }); + }, + icon: const Icon( + Icons.search_rounded, + color: MyTheme.dark, + )); + } + + Widget _buildSearchBar() { + RxBool focused = false.obs; + FocusNode focusNode = FocusNode(); + focusNode.addListener(() => focused.value = focusNode.hasFocus); + return Container( + width: 120, + decoration: BoxDecoration( + color: MyTheme.color(context).bg, + borderRadius: BorderRadius.circular(6), + ), + child: Obx(() => Row( + children: [ + Expanded( + child: Row( + children: [ + Icon( + Icons.search_rounded, + color: MyTheme.color(context).placeholder, + ).marginSymmetric(horizontal: 4), + Expanded( + child: TextField( + autofocus: true, + controller: peerSearchTextController, + onChanged: (searchText) { + peerSearchText.value = searchText; + }, + focusNode: focusNode, + textAlign: TextAlign.start, + maxLines: 1, + cursorColor: MyTheme.color(context).lightText, + cursorHeight: 18, + cursorWidth: 1, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(vertical: 6), + hintText: + focused.value ? null : translate("Search ID"), + hintStyle: TextStyle( + fontSize: 14, + color: MyTheme.color(context).placeholder), + border: InputBorder.none, + isDense: true, + ), + ), + ), + // Icon(Icons.close), + IconButton( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 2), + onPressed: () { + setState(() { + peerSearchTextController.clear(); + peerSearchText.value = ""; + drawer = false; + }); + }, + icon: const Icon( + Icons.close, + color: MyTheme.dark, + )), + ], + ), + ) + ], + )), + ); + } +} diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/common/widgets/peer_widget.dart similarity index 80% rename from flutter/lib/desktop/widgets/peer_widget.dart rename to flutter/lib/common/widgets/peer_widget.dart index f137241a9..9ae1a6340 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/common/widgets/peer_widget.dart @@ -39,7 +39,7 @@ class _PeerWidget extends StatefulWidget { /// State for the peer widget. class _PeerWidgetState extends State<_PeerWidget> with WindowListener { static const int _maxQueryCount = 3; - + final space = isDesktop ? 12.0 : 8.0; final _curPeers = {}; var _lastChangeTime = DateTime.now(); var _lastQueryPeers = {}; @@ -47,6 +47,17 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { var _queryCoun = 0; var _exit = false; + late final mobileWidth = () { + const minWidth = 320.0; + final windowWidth = MediaQuery.of(context).size.width; + var width = windowWidth - 2 * space; + if (windowWidth > minWidth + 2 * space) { + final n = (windowWidth / (minWidth + 2 * space)).floor(); + width = windowWidth / n - 2 * space; + } + return width; + }(); + _PeerWidgetState() { _startCheckOnlines(); } @@ -76,7 +87,6 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { @override Widget build(BuildContext context) { - const space = 12.0; return ChangeNotifierProvider( create: (context) => widget.peers, child: Consumer( @@ -93,32 +103,36 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { final peers = snapshot.data!; final cards = []; for (final peer in peers) { + final visibilityChild = VisibilityDetector( + key: ValueKey(peer.id), + onVisibilityChanged: (info) { + final peerId = (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: widget.peerCardWidgetFunc(peer), + ); cards.add(Offstage( key: ValueKey("off${peer.id}"), offstage: widget.offstageFunc(peer), - child: Obx( - () => SizedBox( - width: 220, - height: - peerCardUiType.value == PeerUiType.grid - ? 140 - : 42, - child: VisibilityDetector( - key: ValueKey(peer.id), - onVisibilityChanged: (info) { - final peerId = - (info.key as ValueKey).value; - if (info.visibleFraction > 0.00001) { - _curPeers.add(peerId); - } else { - _curPeers.remove(peerId); - } - _lastChangeTime = DateTime.now(); - }, - child: widget.peerCardWidgetFunc(peer), - ), - ), - ))); + child: isDesktop + ? Obx( + () => SizedBox( + width: 220, + height: peerCardUiType.value == + PeerUiType.grid + ? 140 + : 42, + child: visibilityChild, + ), + ) + : SizedBox( + width: mobileWidth, + child: visibilityChild))); } return Wrap( spacing: space, runSpacing: space, children: cards); @@ -273,6 +287,7 @@ class AddressBookPeerWidget extends BasePeerWidget { ); static List _loadPeers() { + debugPrint("_loadPeers : ${gFFI.abModel.peers.toString()}"); return gFFI.abModel.peers.map((e) { return Peer.fromJson(e['id'], e); }).toList(); @@ -296,7 +311,7 @@ class AddressBookPeerWidget extends BasePeerWidget { @override Widget build(BuildContext context) { final widget = super.build(context); - gFFI.abModel.updateAb(); + // gFFI.abModel.updateAb(); return widget; } } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peercard_widget.dart similarity index 94% rename from flutter/lib/desktop/widgets/peercard_widget.dart rename to flutter/lib/common/widgets/peercard_widget.dart index fc93c59c6..2cb451974 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peercard_widget.dart @@ -8,9 +8,8 @@ import '../../common/formatter/id_formatter.dart'; import '../../models/model.dart'; import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; -import './material_mod_popup_menu.dart' as mod_menu; -import './popup_menu.dart'; -import './utils.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import '../../desktop/widgets/popup_menu.dart'; class _PopupMenuTheme { static const Color commonColor = MyTheme.accent; @@ -55,6 +54,50 @@ class _PeerCardState extends State<_PeerCard> @override Widget build(BuildContext context) { super.build(context); + if (isDesktop) { + return _buildDesktop(); + } else { + return _buildMobile(); + } + } + + Widget _buildMobile() { + final peer = super.widget.peer; + return Card( + margin: EdgeInsets.zero, + child: GestureDetector( + onTap: !isWebDesktop ? () => connect(context, peer.id) : null, + onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null, + onLongPressStart: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + _showPeerMenu(peer.id); + }, + child: ListTile( + contentPadding: const EdgeInsets.only(left: 12), + subtitle: Text('${peer.username}@${peer.hostname}'), + title: Text(peer.id), + leading: Container( + padding: const EdgeInsets.all(6), + color: str2color('${peer.id}${peer.platform}', 0x7f), + child: getPlatformImage(peer.platform)), + trailing: InkWell( + child: const Padding( + padding: EdgeInsets.all(12), + child: Icon(Icons.more_vert)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(peer.id); + }), + ))); + } + + Widget _buildDesktop() { final peer = super.widget.peer; var deco = Rx(BoxDecoration( border: Border.all(color: Colors.transparent, width: _borderWidth), @@ -264,7 +307,7 @@ class _PeerCardState extends State<_PeerCard> final y = e.position.dy; _menuPos = RelativeRect.fromLTRB(x, y, x, y); }, - onPointerUp: (_) => _showPeerMenu(context, peer.id), + onPointerUp: (_) => _showPeerMenu(peer.id), child: MouseRegion( onEnter: (_) => _iconMoreHover.value = true, onExit: (_) => _iconMoreHover.value = false, @@ -281,7 +324,7 @@ class _PeerCardState extends State<_PeerCard> /// Show the peer menu and handle user's choice. /// User might remove the peer or send a file to the peer. - void _showPeerMenu(BuildContext context, String id) async { + void _showPeerMenu(String id) async { await mod_menu.showMenu( context: context, position: _menuPos, diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 8986f05ec..4f710ea2c 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + const double kDesktopRemoteTabBarHeight = 28.0; /// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page' @@ -17,6 +19,8 @@ const int kDesktopDefaultDisplayHeight = 720; const kInvalidValueStr = "InvalidValueStr"; +const kMobilePageConstraints = BoxConstraints(maxWidth: 600); + /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] const Map logicalKeyMap = { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 9024c996f..faf798f0d 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,17 +1,15 @@ import 'dart:async'; import 'dart:convert'; -import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; -import 'package:flutter_hbb/desktop/widgets/peer_widget.dart'; -import 'package:flutter_hbb/desktop/widgets/peercard_widget.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:get/get.dart'; 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 '../../models/platform_model.dart'; /// Connection page for connecting to a remote peer. @@ -66,7 +64,7 @@ class _ConnectionPageState extends State { SizedBox(height: 12), Divider(), Expanded( - child: _PeerTabbedPage( + child: PeerTabPage( tabs: [ translate('Recent Sessions'), translate('Favorites'), @@ -77,15 +75,7 @@ class _ConnectionPageState extends State { RecentPeerWidget(), FavoritePeerWidget(), DiscoveredPeerWidget(), - FutureBuilder( - future: buildAddressBook(context), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return const Offstage(); - } - }), + const AddressBook(), ], )), ], @@ -102,23 +92,7 @@ class _ConnectionPageState extends State { /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { final id = _idController.id; - connect(id, isFileTransfer: isFileTransfer); - } - - /// 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) { - await rustDeskWinManager.newFileTransfer(id); - } else { - await rustDeskWinManager.newRemoteDesktop(id); - } - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } + connect(context, id, isFileTransfer: isFileTransfer); } /// UI for the search bar. @@ -372,604 +346,4 @@ class _ConnectionPageState extends State { svcStatusCode.value = status["status_num"]; svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer(); } - - handleLogin() { - loginDialog().then((success) { - if (success) { - setState(() {}); - } - }); - } - - Future buildAddressBook(BuildContext context) async { - final token = await bind.mainGetLocalOption(key: 'access_token'); - if (token.trim().isEmpty) { - return Center( - child: InkWell( - onTap: handleLogin, - child: Text( - translate("Login"), - style: TextStyle(decoration: TextDecoration.underline), - ), - ), - ); - } - final model = gFFI.abModel; - return FutureBuilder( - future: model.getAb(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return _buildAddressBook(context); - } else if (snapshot.hasError) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(translate("${snapshot.error}")), - TextButton( - onPressed: () { - setState(() {}); - }, - child: Text(translate("Retry"))) - ], - ); - } else { - if (model.abLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (model.abError.isNotEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(translate("${model.abError}")), - TextButton( - onPressed: () { - setState(() {}); - }, - child: Text(translate("Retry"))) - ], - ), - ); - } else { - return Offstage(); - } - } - }); - } - - Widget _buildAddressBook(BuildContext context) { - return Row( - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide(color: MyTheme.grayBg)), - child: Container( - width: 200, - height: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(translate('Tags')), - InkWell( - child: PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Text(translate("Add ID")), - value: 'add-id', - ), - PopupMenuItem( - child: Text(translate("Add Tag")), - value: 'add-tag', - ), - PopupMenuItem( - child: Text(translate("Unselect all tags")), - value: 'unset-all-tag', - ), - ], - onSelected: handleAbOp, - child: Icon(Icons.more_vert_outlined)), - ) - ], - ), - Expanded( - child: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - border: Border.all(color: MyTheme.darkGray)), - child: Obx( - () => Wrap( - children: gFFI.abModel.tags - .map((e) => buildTag(e, gFFI.abModel.selectedTags, - onTap: () { - // - if (gFFI.abModel.selectedTags.contains(e)) { - gFFI.abModel.selectedTags.remove(e); - } else { - gFFI.abModel.selectedTags.add(e); - } - })) - .toList(), - ), - ), - ).marginSymmetric(vertical: 8.0), - ) - ], - ), - ), - ).marginOnly(right: 8.0), - Expanded( - child: Align( - alignment: Alignment.topLeft, child: AddressBookPeerWidget()), - ) - ], - ); - } - - Widget buildTag(String tagName, RxList rxTags, {Function()? onTap}) { - return ContextMenuArea( - width: 100, - builder: (context) => [ - ListTile( - title: Text(translate("Delete")), - onTap: () { - gFFI.abModel.deleteTag(tagName); - gFFI.abModel.updateAb(); - Future.delayed(Duration.zero, () => Get.back()); - }, - ) - ], - child: GestureDetector( - onTap: onTap, - child: Obx( - () => Container( - decoration: BoxDecoration( - color: rxTags.contains(tagName) ? Colors.blue : null, - border: Border.all(color: MyTheme.darkGray), - borderRadius: BorderRadius.circular(10)), - margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), - padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), - child: Text( - tagName, - style: TextStyle( - color: rxTags.contains(tagName) ? MyTheme.white : null), - ), - ), - ), - ), - ); - } - - /// tag operation - void handleAbOp(String value) { - if (value == 'add-id') { - abAddId(); - } else if (value == 'add-tag') { - abAddTag(); - } else if (value == 'unset-all-tag') { - gFFI.abModel.unsetSelectedTags(); - } - } - - void abAddId() async { - var field = ""; - var msg = ""; - var isInProgress = false; - TextEditingController controller = TextEditingController(text: field); - - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - msg = ""; - isInProgress = true; - }); - field = controller.text.trim(); - if (field.isEmpty) { - // pass - } else { - final ids = field.trim().split(RegExp(r"[\s,;\n]+")); - field = ids.join(','); - for (final newId in ids) { - if (gFFI.abModel.idContainBy(newId)) { - continue; - } - gFFI.abModel.addId(newId); - } - await gFFI.abModel.updateAb(); - this.setState(() {}); - // final currentPeers - } - close(); - } - - return CustomAlertDialog( - title: Text(translate("Add ID")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(translate("whitelist_sep")), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - Expanded( - child: TextField( - maxLines: null, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: controller, - focusNode: FocusNode()..requestFocus()), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } - - void abAddTag() async { - var field = ""; - var msg = ""; - var isInProgress = false; - TextEditingController controller = TextEditingController(text: field); - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - msg = ""; - isInProgress = true; - }); - field = controller.text.trim(); - if (field.isEmpty) { - // pass - } else { - final tags = field.trim().split(RegExp(r"[\s,;\n]+")); - field = tags.join(','); - for (final tag in tags) { - gFFI.abModel.addTag(tag); - } - await gFFI.abModel.updateAb(); - // final currentPeers - } - close(); - } - - return CustomAlertDialog( - title: Text(translate("Add Tag")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(translate("whitelist_sep")), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - Expanded( - child: TextField( - maxLines: null, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: controller, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } - - void abEditTag(String id) { - var isInProgress = false; - - final tags = List.of(gFFI.abModel.tags); - var selectedTag = gFFI.abModel.getPeerTags(id).obs; - - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - isInProgress = true; - }); - gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.updateAb(); - close(); - } - - return CustomAlertDialog( - title: Text(translate("Edit Tag")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Wrap( - children: tags - .map((e) => buildTag(e, selectedTag, onTap: () { - if (selectedTag.contains(e)) { - selectedTag.remove(e); - } else { - selectedTag.add(e); - } - })) - .toList(growable: false), - ), - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } -} - -class _PeerTabbedPage extends StatefulWidget { - final List tabs; - final List children; - const _PeerTabbedPage({required this.tabs, required this.children, Key? key}) - : super(key: key); - @override - _PeerTabbedPageState createState() => _PeerTabbedPageState(); -} - -class _PeerTabbedPageState extends State<_PeerTabbedPage> - with SingleTickerProviderStateMixin { - late PageController _pageController = PageController(); - RxInt _tabIndex = 0.obs; - - @override - void initState() { - super.initState(); - } - - // hard code for now - void _handleTabSelection(int index) { - // reset search text - peerSearchText.value = ""; - peerSearchTextController.clear(); - _tabIndex.value = index; - _pageController.jumpToPage(index); - switch (index) { - case 0: - bind.mainLoadRecentPeers(); - break; - case 1: - bind.mainLoadFavPeers(); - break; - case 2: - bind.mainDiscover(); - break; - case 3: - gFFI.abModel.updateAb(); - break; - } - } - - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - textBaseline: TextBaseline.ideographic, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 28, - child: Row( - children: [ - Expanded(child: _createTabBar(context)), - _createSearchBar(context), - _createPeerViewTypeSwitch(context), - ], - ), - ), - _createTabBarView(), - ], - ); - } - - Widget _createTabBar(BuildContext context) { - return ListView( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - controller: ScrollController(), - children: super.widget.tabs.asMap().entries.map((t) { - return Obx(() => GestureDetector( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: _tabIndex.value == t.key - ? MyTheme.color(context).bg - : null, - borderRadius: BorderRadius.circular(2), - ), - child: Align( - alignment: Alignment.center, - child: Text( - t.value, - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: _tabIndex.value == t.key - ? MyTheme.color(context).text - : MyTheme.color(context).lightText), - ), - )), - onTap: () => _handleTabSelection(t.key), - )); - }).toList()); - } - - Widget _createTabBarView() { - return Expanded( - child: PageView( - controller: _pageController, children: super.widget.children) - .marginSymmetric(vertical: 12)); - } - - _createSearchBar(BuildContext context) { - RxBool focused = false.obs; - FocusNode focusNode = FocusNode(); - focusNode.addListener(() => focused.value = focusNode.hasFocus); - RxBool rowHover = false.obs; - RxBool clearHover = false.obs; - return Container( - width: 120, - height: 25, - margin: EdgeInsets.only(right: 13), - decoration: BoxDecoration(color: MyTheme.color(context).bg), - child: Obx(() => Row( - children: [ - Expanded( - child: MouseRegion( - onEnter: (_) => rowHover.value = true, - onExit: (_) => rowHover.value = false, - child: Row( - children: [ - Icon( - IconFont.search, - size: 16, - color: MyTheme.color(context).placeholder, - ).marginSymmetric(horizontal: 4), - Expanded( - child: TextField( - controller: peerSearchTextController, - onChanged: (searchText) { - peerSearchText.value = searchText; - }, - focusNode: focusNode, - textAlign: TextAlign.start, - maxLines: 1, - cursorColor: MyTheme.color(context).lightText, - cursorHeight: 18, - cursorWidth: 1, - style: TextStyle(fontSize: 14), - decoration: InputDecoration( - contentPadding: EdgeInsets.symmetric(vertical: 6), - hintText: - focused.value ? null : translate("Search ID"), - hintStyle: TextStyle( - fontSize: 14, - color: MyTheme.color(context).placeholder), - border: InputBorder.none, - isDense: true, - ), - ), - ), - ], - ), - ), - ), - Offstage( - offstage: !(peerSearchText.value.isNotEmpty && - (rowHover.value || clearHover.value)), - child: InkWell( - onHover: (value) => clearHover.value = value, - child: Icon( - IconFont.round_close, - size: 16, - color: clearHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).placeholder, - ).marginSymmetric(horizontal: 4), - onTap: () { - peerSearchTextController.clear(); - peerSearchText.value = ""; - }), - ) - ], - )), - ); - } - - _createPeerViewTypeSwitch(BuildContext context) { - final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); - return Row( - children: [ - Obx( - () => Container( - padding: EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.grid ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.grid; - }, - child: Icon( - Icons.grid_view_rounded, - size: 18, - color: peerCardUiType.value == PeerUiType.grid - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - Obx( - () => Container( - padding: EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.list ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.list; - }, - child: Icon( - Icons.list, - size: 18, - color: peerCardUiType.value == PeerUiType.list - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - ], - ); - } } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 092ea7da8..070ad217b 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -15,7 +15,6 @@ import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import './popup_menu.dart'; import './material_mod_popup_menu.dart' as mod_menu; -import './utils.dart'; class _MenubarTheme { static const Color commonColor = MyTheme.accent; diff --git a/flutter/lib/desktop/widgets/utils.dart b/flutter/lib/desktop/widgets/utils.dart deleted file mode 100644 index 2f555c239..000000000 --- a/flutter/lib/desktop/widgets/utils.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; - -/// Connect to a peer with [id]. -/// If [isFileTransfer], starts a session only for file transfer. -/// If [isTcpTunneling], starts a session only for tcp tunneling. -/// If [isRDP], starts a session only for rdp. -void connect(BuildContext context, String id, - {bool isFileTransfer = false, - bool isTcpTunneling = false, - bool isRDP = false}) async { - if (id == '') return; - id = id.replaceAll(' ', ''); - 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); - } else { - await rustDeskWinManager.newRemoteDesktop(id); - } - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } -} diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index e6eddd087..83e26547a 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -2,12 +2,16 @@ import 'dart:async'; import 'package:flutter/material.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'; import '../../common.dart'; +import '../../common/widgets/address_book.dart'; +import '../../common/widgets/peer_tab_page.dart'; +import '../../common/widgets/peer_widget.dart'; +import '../../consts.dart'; import '../../models/model.dart'; -import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; import 'remote_page.dart'; @@ -19,16 +23,16 @@ class ConnectionPage extends StatefulWidget implements PageShape { ConnectionPage({Key? key}) : super(key: key); @override - final icon = Icon(Icons.connected_tv); + final icon = const Icon(Icons.connected_tv); @override final title = translate("Connection"); @override - final appBarActions = !isAndroid ? [WebMenu()] : []; + final appBarActions = !isAndroid ? [const WebMenu()] : []; @override - _ConnectionPageState createState() => _ConnectionPageState(); + State createState() => _ConnectionPageState(); } /// State for the connection page. @@ -38,7 +42,6 @@ class _ConnectionPageState extends State { /// Update url. If it's not null, means an update is available. var _updateUrl = ''; - var _menuPos; @override void initState() { @@ -54,9 +57,8 @@ class _ConnectionPageState extends State { }(); } if (isAndroid) { - Timer(Duration(seconds: 5), () async { + Timer(const Duration(seconds: 5), () async { _updateUrl = await bind.mainGetSoftwareUpdateUrl(); - ; if (_updateUrl.isNotEmpty) setState(() {}); }); } @@ -65,19 +67,29 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { Provider.of(context); - return SingleChildScrollView( - controller: ScrollController(), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - getUpdateUI(), - getSearchBarUI(), - Container(height: 12), - getPeers(), - ]), - ); + return Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + getUpdateUI(), + getSearchBarUI(), + Expanded( + child: PeerTabPage( + tabs: [ + translate('Recent Sessions'), + translate('Favorites'), + translate('Discovered'), + translate('Address Book') + ], + children: [ + RecentPeerWidget(), + FavoritePeerWidget(), + DiscoveredPeerWidget(), + const AddressBook(), + ], + )), + ]).marginOnly(top: 2, left: 12, right: 12); } /// Callback for the connect button. @@ -122,10 +134,10 @@ class _ConnectionPageState extends State { /// If [_updateUrl] is not empty, shows a button to update the software. Widget getUpdateUI() { return _updateUrl.isEmpty - ? SizedBox(height: 0) + ? const SizedBox(height: 0) : InkWell( onTap: () async { - final url = _updateUrl + '.apk'; + final url = '$_updateUrl.apk'; if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url)); } @@ -134,79 +146,77 @@ class _ConnectionPageState extends State { alignment: AlignmentDirectional.center, width: double.infinity, color: Colors.pinkAccent, - padding: EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric(vertical: 12), child: Text(translate('Download new version'), - style: TextStyle( + style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold)))); } /// UI for the search bar. /// Search for a peer and connect to it if the id exists. Widget getSearchBarUI() { - var w = Padding( - padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), - child: Container( - height: 84, - child: Padding( - padding: const EdgeInsets.only(top: 8, bottom: 8), - child: Ink( - decoration: BoxDecoration( - color: MyTheme.white, - borderRadius: const BorderRadius.all(Radius.circular(13)), - ), - child: Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.only(left: 16, right: 16), - child: TextField( - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - // keyboardType: TextInputType.number, - style: TextStyle( - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 30, - color: MyTheme.idColor, - ), - decoration: InputDecoration( - labelText: translate('Remote ID'), - // hintText: 'Enter your remote ID', - border: InputBorder.none, - helperStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: MyTheme.darkGray, - ), - labelStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - letterSpacing: 0.2, - color: MyTheme.darkGray, - ), - ), - controller: _idController, + final w = SizedBox( + height: 84, + child: Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: Ink( + decoration: const BoxDecoration( + color: MyTheme.white, + borderRadius: BorderRadius.all(Radius.circular(13)), + ), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 16, right: 16), + child: TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + // keyboardType: TextInputType.number, + style: const TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 30, + color: MyTheme.idColor, ), + decoration: InputDecoration( + labelText: translate('Remote ID'), + // hintText: 'Enter your remote ID', + border: InputBorder.none, + helperStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: MyTheme.darkGray, + ), + labelStyle: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + letterSpacing: 0.2, + color: MyTheme.darkGray, + ), + ), + controller: _idController, ), ), - SizedBox( - width: 60, - height: 60, - child: IconButton( - icon: Icon(Icons.arrow_forward, - color: MyTheme.darkGray, size: 45), - onPressed: onConnect, - ), + ), + SizedBox( + width: 60, + height: 60, + child: IconButton( + icon: const Icon(Icons.arrow_forward, + color: MyTheme.darkGray, size: 45), + onPressed: onConnect, ), - ], - ), + ), + ], ), ), ), ); - return Center( - child: Container(constraints: BoxConstraints(maxWidth: 600), child: w)); + return Align( + alignment: Alignment.topLeft, + child: Container(constraints: kMobilePageConstraints, child: w)); } @override @@ -214,97 +224,13 @@ class _ConnectionPageState extends State { _idController.dispose(); super.dispose(); } - - /// Get all the saved peers. - Widget getPeers() { - final windowWidth = MediaQuery.of(context).size.width; - final space = 8.0; - var width = windowWidth - 2 * space; - final minWidth = 320.0; - if (windowWidth > minWidth + 2 * space) { - final n = (windowWidth / (minWidth + 2 * space)).floor(); - width = windowWidth / n - 2 * space; - } - return FutureBuilder>( - future: gFFI.peers(), - builder: (context, snapshot) { - final cards = []; - if (snapshot.hasData) { - final peers = snapshot.data!; - peers.forEach((p) { - cards.add(Container( - width: width, - child: Card( - child: GestureDetector( - onTap: - !isWebDesktop ? () => connect('${p.id}') : null, - onDoubleTap: - isWebDesktop ? () => connect('${p.id}') : null, - onLongPressStart: (details) { - final x = details.globalPosition.dx; - final y = details.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - showPeerMenu(context, p.id); - }, - child: ListTile( - contentPadding: const EdgeInsets.only(left: 12), - subtitle: Text('${p.username}@${p.hostname}'), - title: Text('${p.id}'), - leading: Container( - padding: const EdgeInsets.all(6), - child: getPlatformImage('${p.platform}'), - color: str2color('${p.id}${p.platform}', 0x7f)), - trailing: InkWell( - child: Padding( - padding: const EdgeInsets.all(12), - child: Icon(Icons.more_vert)), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - showPeerMenu(context, p.id); - }), - ))))); - }); - } - return Wrap(children: cards, spacing: space, runSpacing: space); - }); - } - - /// Show the peer menu and handle user's choice. - /// User might remove the peer or send a file to the peer. - void showPeerMenu(BuildContext context, String id) async { - var value = await showMenu( - context: context, - position: this._menuPos, - items: [ - PopupMenuItem( - child: Text(translate('Remove')), value: 'remove') - ] + - (!isAndroid - ? [] - : [ - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file') - ]), - elevation: 8, - ); - if (value == 'remove') { - setState(() => bind.mainRemovePeer(id: id)); - () async { - removePreference(id); - }(); - } else if (value == 'file') { - connect(id, isFileTransfer: true); - } - } } class WebMenu extends StatefulWidget { + const WebMenu({Key? key}) : super(key: key); + @override - _WebMenuState createState() => _WebMenuState(); + State createState() => _WebMenuState(); } class _WebMenuState extends State { @@ -337,36 +263,36 @@ class _WebMenuState extends State { Widget build(BuildContext context) { Provider.of(context); return PopupMenuButton( - icon: Icon(Icons.more_vert), + icon: const Icon(Icons.more_vert), itemBuilder: (context) { return (isIOS ? [ - PopupMenuItem( - child: Icon(Icons.qr_code_scanner, color: Colors.black), + const PopupMenuItem( value: "scan", + child: Icon(Icons.qr_code_scanner, color: Colors.black), ) ] : >[]) + [ PopupMenuItem( - child: Text(translate('ID/Relay Server')), value: "server", + child: Text(translate('ID/Relay Server')), ) ] + (url.contains('admin.rustdesk.com') ? >[] : [ PopupMenuItem( + value: "login", child: Text(username == null ? translate("Login") - : translate("Logout") + ' ($username)'), - value: "login", + : '${translate("Logout")} ($username)'), ) ]) + [ PopupMenuItem( - child: Text(translate('About') + ' RustDesk'), value: "about", + child: Text('${translate('About')} RustDesk'), ) ]; }, diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index be8403427..dc3a153d7 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -32,6 +32,7 @@ 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; @@ -59,6 +60,8 @@ class _SettingsState extends State with WidgetsBindingObserver { _enableAbr = enableAbrRes; } + _enableAbr = isDarkTheme(); + if (update) { setState(() {}); } @@ -173,7 +176,18 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.translate), onPressed: (context) { showLanguageSettings(gFFI.dialogManager); - }) + }), + SettingsTile.switchTile( + title: Text(translate('Dark Theme')), + leading: Icon(Icons.dark_mode), + initialValue: _isDarkMode, + onToggle: (v) { + setState(() { + _isDarkMode = !_isDarkMode; + MyTheme.changeTo(_isDarkMode); + }); + }, + ) ]), SettingsSection( title: Text(translate("Enhancements")), diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 2749e972f..e0467565c 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -101,7 +101,7 @@ class AbModel with ChangeNotifier { final resp = await http.post(Uri.parse(api), headers: authHeaders, body: body); abLoading = false; - await getAb(); + // await getAb(); // TODO notifyListeners(); debugPrint("resp: ${resp.body}"); } From 5625a061a4b15c5606c525c855186b6d9929fc85 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Sep 2022 14:56:01 +0800 Subject: [PATCH 3/5] merge master peer_tab_page.dart peer_widget.dart --- flutter/lib/common/widgets/peer_tab_page.dart | 88 ++++++------ flutter/lib/common/widgets/peer_widget.dart | 128 ++++++++++-------- 2 files changed, 115 insertions(+), 101 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index fbda91649..c1151088d 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -23,15 +23,31 @@ class _PeerTabPageState extends State @override void initState() { + () async { + await bind.mainGetLocalOption(key: 'peer-tab-index').then((value) { + if (value == '') return; + final tab = int.parse(value); + _tabIndex.value = tab; + _pageController.jumpToPage(tab); + }); + await bind.mainGetLocalOption(key: 'peer-card-ui-type').then((value) { + if (value == '') return; + final tab = int.parse(value); + peerCardUiType.value = + tab == PeerUiType.list.index ? PeerUiType.list : PeerUiType.grid; + }); + }(); super.initState(); } // hard code for now - void _handleTabSelection(int index) { + Future _handleTabSelection(int index) async { // reset search text peerSearchText.value = ""; peerSearchTextController.clear(); _tabIndex.value = index; + await bind.mainSetLocalOption( + key: 'peer-tab-index', value: index.toString()); _pageController.jumpToPage(index); switch (index) { case 0: @@ -89,7 +105,7 @@ class _PeerTabPageState extends State shrinkWrap: true, controller: ScrollController(), children: super.widget.tabs.asMap().entries.map((t) { - return Obx(() => GestureDetector( + return Obx(() => InkWell( child: Container( padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( @@ -111,7 +127,7 @@ class _PeerTabPageState extends State : MyTheme.color(context).lightText), ), )), - onTap: () => _handleTabSelection(t.key), + onTap: () async => await _handleTabSelection(t.key), )); }).toList()); } @@ -120,7 +136,9 @@ class _PeerTabPageState extends State final verticalMargin = isDesktop ? 12.0 : 6.0; return Expanded( child: PageView( - physics: const BouncingScrollPhysics(), + physics: isDesktop + ? NeverScrollableScrollPhysics() + : BouncingScrollPhysics(), controller: _pageController, children: super.widget.children, onPageChanged: (to) => _tabIndex.value = to) @@ -130,44 +148,30 @@ class _PeerTabPageState extends State Widget _createPeerViewTypeSwitch(BuildContext context) { final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); return Row( - children: [ - Obx( - () => Container( - padding: const EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.grid ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.grid; - }, - child: Icon( - Icons.grid_view_rounded, - size: 18, - color: peerCardUiType.value == PeerUiType.grid - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - Obx( - () => Container( - padding: const EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.list ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.list; - }, - child: Icon( - Icons.list, - size: 18, - color: peerCardUiType.value == PeerUiType.list - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - ], + children: [PeerUiType.grid, PeerUiType.list] + .map((type) => Obx( + () => Container( + padding: EdgeInsets.all(4.0), + decoration: peerCardUiType.value == type ? activeDeco : null, + child: InkWell( + onTap: () async { + await bind.mainSetLocalOption( + key: 'peer-card-ui-type', + value: type.index.toString()); + peerCardUiType.value = type; + }, + child: Icon( + type == PeerUiType.grid + ? Icons.grid_view_rounded + : Icons.list, + size: 18, + color: peerCardUiType.value == type + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + )), + ), + )) + .toList(), ); } } diff --git a/flutter/lib/common/widgets/peer_widget.dart b/flutter/lib/common/widgets/peer_widget.dart index f32b8a2f1..bdfbe53e5 100644 --- a/flutter/lib/common/widgets/peer_widget.dart +++ b/flutter/lib/common/widgets/peer_widget.dart @@ -92,68 +92,78 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { return ChangeNotifierProvider( create: (context) => widget.peers, child: Consumer( - builder: (context, peers, child) => peers.peers.isEmpty - ? Center( - child: Text(translate("Empty")), - ) - : DesktopScrollWrapper( - scrollController: _scrollController, - child: SingleChildScrollView( - physics: NeverScrollableScrollPhysics(), - controller: _scrollController, - child: ObxValue((searchText) { - return FutureBuilder>( - builder: (context, snapshot) { - if (snapshot.hasData) { - final peers = snapshot.data!; - final cards = []; - for (final peer in peers) { - cards.add(Offstage( - key: ValueKey("off${peer.id}"), - offstage: widget.offstageFunc(peer), - child: Obx( - () => SizedBox( - width: 220, - height: - peerCardUiType.value == PeerUiType.grid - ? 140 - : 42, - child: VisibilityDetector( - key: ValueKey(peer.id), - onVisibilityChanged: (info) { - final peerId = - (info.key as ValueKey).value; - if (info.visibleFraction > 0.00001) { - _curPeers.add(peerId); - } else { - _curPeers.remove(peerId); - } - _lastChangeTime = DateTime.now(); - }, - child: widget.peerCardWidgetFunc(peer), - ), - ), - ))); - } - return Wrap( - spacing: space, - runSpacing: space, - children: cards); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - future: matchPeers(searchText.value, peers.peers), - ); - }, peerSearchText), - ), - ), - ), + builder: (context, peers, child) => peers.peers.isEmpty + ? Center( + child: Text(translate("Empty")), + ) + : _buildPeersView(peers)), ); } + Widget _buildPeersView(Peers peers) { + final body = ObxValue((searchText) { + return FutureBuilder>( + builder: (context, snapshot) { + if (snapshot.hasData) { + final peers = snapshot.data!; + final cards = []; + for (final peer in peers) { + final visibilityChild = VisibilityDetector( + key: ValueKey(peer.id), + onVisibilityChanged: (info) { + final peerId = (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: widget.peerCardWidgetFunc(peer), + ); + cards.add(Offstage( + key: ValueKey("off${peer.id}"), + offstage: widget.offstageFunc(peer), + child: isDesktop + ? Obx( + () => SizedBox( + width: 220, + height: peerCardUiType.value == PeerUiType.grid + ? 140 + : 42, + child: visibilityChild, + ), + ) + : SizedBox(width: mobileWidth, child: visibilityChild))); + } + return Wrap(spacing: space, runSpacing: space, children: cards); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + future: matchPeers(searchText.value, peers.peers), + ); + }, peerSearchText); + + if (isDesktop) { + return DesktopScrollWrapper( + scrollController: _scrollController, + child: SingleChildScrollView( + physics: NeverScrollableScrollPhysics(), + controller: _scrollController, + child: body), + ); + } else { + return SingleChildScrollView( + physics: BouncingScrollPhysics(), + controller: _scrollController, + child: body, + ); + } + } + // ignore: todo // TODO: variables walk through async tasks? void _startCheckOnlines() { From 285d415a5aed2ac86906675e479ad0dba269a776 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Sep 2022 17:16:09 +0800 Subject: [PATCH 4/5] mobile peers tab padding --- flutter/lib/common/widgets/peer_tab_page.dart | 1 + flutter/lib/common/widgets/peercard_widget.dart | 2 +- flutter/lib/mobile/pages/connection_page.dart | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index c1151088d..fefe74671 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -80,6 +80,7 @@ class _PeerTabPageState extends State SizedBox( height: 28, child: Container( + padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2), constraints: isDesktop ? null : kMobilePageConstraints, child: Row( crossAxisAlignment: CrossAxisAlignment.center, diff --git a/flutter/lib/common/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peercard_widget.dart index 2cb451974..2e9c1c080 100644 --- a/flutter/lib/common/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peercard_widget.dart @@ -64,7 +64,7 @@ class _PeerCardState extends State<_PeerCard> Widget _buildMobile() { final peer = super.widget.peer; return Card( - margin: EdgeInsets.zero, + margin: EdgeInsets.symmetric(horizontal: 2), child: GestureDetector( onTap: !isWebDesktop ? () => connect(context, peer.id) : null, onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 83e26547a..1377088c7 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -89,7 +89,7 @@ class _ConnectionPageState extends State { const AddressBook(), ], )), - ]).marginOnly(top: 2, left: 12, right: 12); + ]).marginOnly(top: 2, left: 10, right: 10); } /// Callback for the connect button. @@ -158,7 +158,7 @@ class _ConnectionPageState extends State { final w = SizedBox( height: 84, child: Padding( - padding: const EdgeInsets.only(top: 8, bottom: 8), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2), child: Ink( decoration: const BoxDecoration( color: MyTheme.white, From 725c0689e275878dde23fdec51ddf57953c8cbd7 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Sep 2022 17:54:47 +0800 Subject: [PATCH 5/5] mobile id text format --- .../lib/common/widgets/peercard_widget.dart | 2 +- .../lib/desktop/pages/connection_page.dart | 6 +++--- flutter/lib/mobile/pages/connection_page.dart | 20 ++++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/flutter/lib/common/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peercard_widget.dart index 2e9c1c080..e3bc81af2 100644 --- a/flutter/lib/common/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peercard_widget.dart @@ -77,7 +77,7 @@ class _PeerCardState extends State<_PeerCard> child: ListTile( contentPadding: const EdgeInsets.only(left: 12), subtitle: Text('${peer.username}@${peer.hostname}'), - title: Text(peer.id), + title: Text(formatID(peer.id)), leading: Container( padding: const EdgeInsets.all(6), color: str2color('${peer.id}${peer.platform}', 0x7f), diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 6e1eac814..ad8e430f4 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -60,7 +60,7 @@ class _ConnectionPageState extends State { children: [ Row( children: [ - getSearchBarUI(context), + _buildRemoteIDTextField(context), ], ).marginOnly(top: 22), SizedBox(height: 12), @@ -97,9 +97,9 @@ class _ConnectionPageState extends State { connect(context, id, isFileTransfer: isFileTransfer); } - /// UI for the search bar. + /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. - Widget getSearchBarUI(BuildContext context) { + Widget _buildRemoteIDTextField(BuildContext context) { RxBool ftHover = false.obs; RxBool ftPressed = false.obs; RxBool connHover = false.obs; diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 1377088c7..edc2f5f6d 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -1,6 +1,7 @@ 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'; @@ -38,7 +39,7 @@ class ConnectionPage extends StatefulWidget implements PageShape { /// State for the connection page. class _ConnectionPageState extends State { /// Controller for the id input bar. - final _idController = TextEditingController(); + final _idController = IDTextEditingController(); /// Update url. If it's not null, means an update is available. var _updateUrl = ''; @@ -49,9 +50,9 @@ class _ConnectionPageState extends State { if (_idController.text.isEmpty) { () async { final lastRemoteId = await bind.mainGetLastRemoteId(); - if (lastRemoteId != _idController.text) { + if (lastRemoteId != _idController.id) { setState(() { - _idController.text = lastRemoteId; + _idController.id = lastRemoteId; }); } }(); @@ -72,8 +73,8 @@ class _ConnectionPageState extends State { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ - getUpdateUI(), - getSearchBarUI(), + _buildUpdateUI(), + _buildRemoteIDTextField(), Expanded( child: PeerTabPage( tabs: [ @@ -95,7 +96,7 @@ class _ConnectionPageState extends State { /// Callback for the connect button. /// Connects to the selected peer. void onConnect() { - var id = _idController.text.trim(); + var id = _idController.id; connect(id); } @@ -132,7 +133,7 @@ class _ConnectionPageState extends State { /// UI for software update. /// If [_updateUrl] is not empty, shows a button to update the software. - Widget getUpdateUI() { + Widget _buildUpdateUI() { return _updateUrl.isEmpty ? const SizedBox(height: 0) : InkWell( @@ -152,9 +153,9 @@ class _ConnectionPageState extends State { color: Colors.white, fontWeight: FontWeight.bold)))); } - /// UI for the search bar. + /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. - Widget getSearchBarUI() { + Widget _buildRemoteIDTextField() { final w = SizedBox( height: 84, child: Padding( @@ -197,6 +198,7 @@ class _ConnectionPageState extends State { ), ), controller: _idController, + inputFormatters: [IDTextInputFormatter()], ), ), ),