* refact: web desktop, web_id_input_tip Signed-off-by: fufesou <linlong1266@gmail.com> * Update en.rs * Update cn.rs * Update en.rs --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
		
			
				
	
	
		
			377 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			377 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| 
 | |
| import 'package:auto_size_text_field/auto_size_text_field.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hbb/common/formatter/id_formatter.dart';
 | |
| import 'package:flutter_hbb/common/widgets/connection_page_title.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:provider/provider.dart';
 | |
| import 'package:url_launcher/url_launcher.dart';
 | |
| import 'package:flutter_hbb/models/peer_model.dart';
 | |
| 
 | |
| import '../../common.dart';
 | |
| import '../../common/widgets/peer_tab_page.dart';
 | |
| import '../../common/widgets/autocomplete.dart';
 | |
| import '../../consts.dart';
 | |
| import '../../models/model.dart';
 | |
| import '../../models/platform_model.dart';
 | |
| import 'home_page.dart';
 | |
| 
 | |
| /// Connection page for connecting to a remote peer.
 | |
| class ConnectionPage extends StatefulWidget implements PageShape {
 | |
|   ConnectionPage({Key? key, required this.appBarActions}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   final icon = const Icon(Icons.connected_tv);
 | |
| 
 | |
|   @override
 | |
|   final title = translate("Connection");
 | |
| 
 | |
|   @override
 | |
|   final List<Widget> appBarActions;
 | |
| 
 | |
|   @override
 | |
|   State<ConnectionPage> createState() => _ConnectionPageState();
 | |
| }
 | |
| 
 | |
| /// State for the connection page.
 | |
| class _ConnectionPageState extends State<ConnectionPage> {
 | |
|   /// Controller for the id input bar.
 | |
|   final _idController = IDTextEditingController();
 | |
|   final RxBool _idEmpty = true.obs;
 | |
| 
 | |
|   /// Update url. If it's not null, means an update is available.
 | |
|   var _updateUrl = '';
 | |
|   List<Peer> peers = [];
 | |
| 
 | |
|   bool isPeersLoading = false;
 | |
|   bool isPeersLoaded = false;
 | |
|   StreamSubscription? _uniLinksSubscription;
 | |
| 
 | |
|   _ConnectionPageState() {
 | |
|     if (!isWeb) _uniLinksSubscription = listenUniLinks();
 | |
|     _idController.addListener(() {
 | |
|       _idEmpty.value = _idController.text.isEmpty;
 | |
|     });
 | |
|     Get.put<IDTextEditingController>(_idController);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     if (_idController.text.isEmpty) {
 | |
|       WidgetsBinding.instance.addPostFrameCallback((_) async {
 | |
|         final lastRemoteId = await bind.mainGetLastRemoteId();
 | |
|         if (lastRemoteId != _idController.id) {
 | |
|           setState(() {
 | |
|             _idController.id = lastRemoteId;
 | |
|           });
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|     if (isAndroid) {
 | |
|       if (!bind.isCustomClient()) {
 | |
|         platformFFI.registerEventHandler(
 | |
|             kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
 | |
|             (Map<String, dynamic> evt) async {
 | |
|           if (evt['url'] is String) {
 | |
|             setState(() {
 | |
|               _updateUrl = evt['url'];
 | |
|             });
 | |
|           }
 | |
|         });
 | |
|         Timer(const Duration(seconds: 1), () async {
 | |
|           bind.mainGetSoftwareUpdateUrl();
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     Provider.of<FfiModel>(context);
 | |
|     return CustomScrollView(
 | |
|       slivers: [
 | |
|         SliverList(
 | |
|             delegate: SliverChildListDelegate([
 | |
|           if (!bind.isCustomClient()) _buildUpdateUI(),
 | |
|           _buildRemoteIDTextField(),
 | |
|         ])),
 | |
|         SliverFillRemaining(
 | |
|           hasScrollBody: true,
 | |
|           child: PeerTabPage(),
 | |
|         )
 | |
|       ],
 | |
|     ).marginOnly(top: 2, left: 10, right: 10);
 | |
|   }
 | |
| 
 | |
|   /// Callback for the connect button.
 | |
|   /// Connects to the selected peer.
 | |
|   void onConnect() {
 | |
|     var id = _idController.id;
 | |
|     connect(context, id);
 | |
|   }
 | |
| 
 | |
|   /// UI for software update.
 | |
|   /// If [_updateUrl] is not empty, shows a button to update the software.
 | |
|   Widget _buildUpdateUI() {
 | |
|     return _updateUrl.isEmpty
 | |
|         ? const SizedBox(height: 0)
 | |
|         : InkWell(
 | |
|             onTap: () async {
 | |
|               final url = 'https://rustdesk.com/download';
 | |
|               if (await canLaunchUrl(Uri.parse(url))) {
 | |
|                 await launchUrl(Uri.parse(url));
 | |
|               }
 | |
|             },
 | |
|             child: Container(
 | |
|                 alignment: AlignmentDirectional.center,
 | |
|                 width: double.infinity,
 | |
|                 color: Colors.pinkAccent,
 | |
|                 padding: const EdgeInsets.symmetric(vertical: 12),
 | |
|                 child: Text(translate('Download new version'),
 | |
|                     style: const TextStyle(
 | |
|                         color: Colors.white, fontWeight: FontWeight.bold))));
 | |
|   }
 | |
| 
 | |
|   Future<void> _fetchPeers() async {
 | |
|     setState(() {
 | |
|       isPeersLoading = true;
 | |
|     });
 | |
|     await Future.delayed(Duration(milliseconds: 100));
 | |
|     peers = await getAllPeers();
 | |
|     setState(() {
 | |
|       isPeersLoading = false;
 | |
|       isPeersLoaded = true;
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /// UI for the remote ID TextField.
 | |
|   /// Search for a peer and connect to it if the id exists.
 | |
|   Widget _buildRemoteIDTextField() {
 | |
|     final w = SizedBox(
 | |
|       height: 84,
 | |
|       child: Padding(
 | |
|         padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2),
 | |
|         child: Ink(
 | |
|           decoration: BoxDecoration(
 | |
|             color: Theme.of(context).cardColor,
 | |
|             borderRadius: BorderRadius.all(Radius.circular(13)),
 | |
|           ),
 | |
|           child: Row(
 | |
|             children: <Widget>[
 | |
|               Expanded(
 | |
|                 child: Container(
 | |
|                   padding: const EdgeInsets.only(left: 16, right: 16),
 | |
|                   child: Autocomplete<Peer>(
 | |
|                     optionsBuilder: (TextEditingValue textEditingValue) {
 | |
|                       if (textEditingValue.text == '') {
 | |
|                         return const Iterable<Peer>.empty();
 | |
|                       } else if (peers.isEmpty && !isPeersLoaded) {
 | |
|                         Peer emptyPeer = Peer(
 | |
|                           id: '',
 | |
|                           username: '',
 | |
|                           hostname: '',
 | |
|                           alias: '',
 | |
|                           platform: '',
 | |
|                           tags: [],
 | |
|                           hash: '',
 | |
|                           password: '',
 | |
|                           forceAlwaysRelay: false,
 | |
|                           rdpPort: '',
 | |
|                           rdpUsername: '',
 | |
|                           loginName: '',
 | |
|                         );
 | |
|                         return [emptyPeer];
 | |
|                       } else {
 | |
|                         String textWithoutSpaces =
 | |
|                             textEditingValue.text.replaceAll(" ", "");
 | |
|                         if (int.tryParse(textWithoutSpaces) != null) {
 | |
|                           textEditingValue = TextEditingValue(
 | |
|                             text: textWithoutSpaces,
 | |
|                             selection: textEditingValue.selection,
 | |
|                           );
 | |
|                         }
 | |
|                         String textToFind = textEditingValue.text.toLowerCase();
 | |
| 
 | |
|                         return peers
 | |
|                             .where((peer) =>
 | |
|                                 peer.id.toLowerCase().contains(textToFind) ||
 | |
|                                 peer.username
 | |
|                                     .toLowerCase()
 | |
|                                     .contains(textToFind) ||
 | |
|                                 peer.hostname
 | |
|                                     .toLowerCase()
 | |
|                                     .contains(textToFind) ||
 | |
|                                 peer.alias.toLowerCase().contains(textToFind))
 | |
|                             .toList();
 | |
|                       }
 | |
|                     },
 | |
|                     fieldViewBuilder: (BuildContext context,
 | |
|                         TextEditingController fieldTextEditingController,
 | |
|                         FocusNode fieldFocusNode,
 | |
|                         VoidCallback onFieldSubmitted) {
 | |
|                       fieldTextEditingController.text = _idController.text;
 | |
|                       fieldFocusNode.addListener(() async {
 | |
|                         _idEmpty.value =
 | |
|                             fieldTextEditingController.text.isEmpty;
 | |
|                         if (fieldFocusNode.hasFocus && !isPeersLoading) {
 | |
|                           _fetchPeers();
 | |
|                         }
 | |
|                       });
 | |
|                       final textLength =
 | |
|                           fieldTextEditingController.value.text.length;
 | |
|                       // select all to facilitate removing text, just following the behavior of address input of chrome
 | |
|                       fieldTextEditingController.selection = TextSelection(
 | |
|                           baseOffset: 0, extentOffset: textLength);
 | |
|                       return AutoSizeTextField(
 | |
|                         controller: fieldTextEditingController,
 | |
|                         focusNode: fieldFocusNode,
 | |
|                         minFontSize: 18,
 | |
|                         autocorrect: false,
 | |
|                         enableSuggestions: false,
 | |
|                         keyboardType: TextInputType.visiblePassword,
 | |
|                         // keyboardType: TextInputType.number,
 | |
|                         onChanged: (String text) {
 | |
|                           _idController.id = text;
 | |
|                         },
 | |
|                         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,
 | |
|                           ),
 | |
|                         ),
 | |
|                         inputFormatters: [IDTextInputFormatter()],
 | |
|                         onSubmitted: (_) {
 | |
|                           onConnect();
 | |
|                         },
 | |
|                       );
 | |
|                     },
 | |
|                     onSelected: (option) {
 | |
|                       setState(() {
 | |
|                         _idController.id = option.id;
 | |
|                         FocusScope.of(context).unfocus();
 | |
|                       });
 | |
|                     },
 | |
|                     optionsViewBuilder: (BuildContext context,
 | |
|                         AutocompleteOnSelected<Peer> onSelected,
 | |
|                         Iterable<Peer> options) {
 | |
|                       double maxHeight = options.length * 50;
 | |
|                       if (options.length == 1) {
 | |
|                         maxHeight = 52;
 | |
|                       } else if (options.length == 3) {
 | |
|                         maxHeight = 146;
 | |
|                       } else if (options.length == 4) {
 | |
|                         maxHeight = 193;
 | |
|                       }
 | |
|                       maxHeight = maxHeight.clamp(0, 200);
 | |
|                       return Align(
 | |
|                           alignment: Alignment.topLeft,
 | |
|                           child: Container(
 | |
|                               decoration: BoxDecoration(
 | |
|                                 boxShadow: [
 | |
|                                   BoxShadow(
 | |
|                                     color: Colors.black.withOpacity(0.3),
 | |
|                                     blurRadius: 5,
 | |
|                                     spreadRadius: 1,
 | |
|                                   ),
 | |
|                                 ],
 | |
|                               ),
 | |
|                               child: ClipRRect(
 | |
|                                   borderRadius: BorderRadius.circular(5),
 | |
|                                   child: Material(
 | |
|                                       elevation: 4,
 | |
|                                       child: ConstrainedBox(
 | |
|                                           constraints: BoxConstraints(
 | |
|                                             maxHeight: maxHeight,
 | |
|                                             maxWidth: 320,
 | |
|                                           ),
 | |
|                                           child: peers.isEmpty && isPeersLoading
 | |
|                                               ? Container(
 | |
|                                                   height: 80,
 | |
|                                                   child: Center(
 | |
|                                                       child:
 | |
|                                                           CircularProgressIndicator(
 | |
|                                                     strokeWidth: 2,
 | |
|                                                   )))
 | |
|                                               : ListView(
 | |
|                                                   padding:
 | |
|                                                       EdgeInsets.only(top: 5),
 | |
|                                                   children: options
 | |
|                                                       .map((peer) =>
 | |
|                                                           AutocompletePeerTile(
 | |
|                                                               onSelect: () =>
 | |
|                                                                   onSelected(
 | |
|                                                                       peer),
 | |
|                                                               peer: peer))
 | |
|                                                       .toList(),
 | |
|                                                 ))))));
 | |
|                     },
 | |
|                   ),
 | |
|                 ),
 | |
|               ),
 | |
|               Obx(() => Offstage(
 | |
|                     offstage: _idEmpty.value,
 | |
|                     child: IconButton(
 | |
|                         onPressed: () {
 | |
|                           setState(() {
 | |
|                             _idController.clear();
 | |
|                           });
 | |
|                         },
 | |
|                         icon: Icon(Icons.clear, color: MyTheme.darkGray)),
 | |
|                   )),
 | |
|               SizedBox(
 | |
|                 width: 60,
 | |
|                 height: 60,
 | |
|                 child: IconButton(
 | |
|                   icon: const Icon(Icons.arrow_forward,
 | |
|                       color: MyTheme.darkGray, size: 45),
 | |
|                   onPressed: onConnect,
 | |
|                 ),
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|     final child = Column(children: [
 | |
|       if (isWebDesktop)
 | |
|         getConnectionPageTitle(context, true).marginOnly(bottom: 10, top: 15, left: 12),
 | |
|       w
 | |
|     ]);
 | |
|     return Align(
 | |
|         alignment: Alignment.topCenter,
 | |
|         child: Container(constraints: kMobilePageConstraints, child: child));
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     _uniLinksSubscription?.cancel();
 | |
|     _idController.dispose();
 | |
|     if (Get.isRegistered<IDTextEditingController>()) {
 | |
|       Get.delete<IDTextEditingController>();
 | |
|     }
 | |
|     if (!bind.isCustomClient()) {
 | |
|       platformFFI.unregisterEventHandler(
 | |
|           kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
 | |
|     }
 | |
|     super.dispose();
 | |
|   }
 | |
| }
 |