From cb455d379c11782fc054c428891ef5ec9aff2019 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:58:41 +0200 Subject: [PATCH] feat: inline discovery in remote screen --- .../mobile_remote_screen.dart | 27 +--- .../companion_remote/discovery_view.dart} | 122 +++++------------- 2 files changed, 33 insertions(+), 116 deletions(-) rename lib/{screens/companion_remote/pairing_screen.dart => widgets/companion_remote/discovery_view.dart} (72%) diff --git a/lib/screens/companion_remote/mobile_remote_screen.dart b/lib/screens/companion_remote/mobile_remote_screen.dart index a113ece2..8046314b 100644 --- a/lib/screens/companion_remote/mobile_remote_screen.dart +++ b/lib/screens/companion_remote/mobile_remote_screen.dart @@ -8,9 +8,9 @@ import '../../i18n/strings.g.dart'; import '../../providers/companion_remote_provider.dart'; import '../../utils/platform_detector.dart'; import '../../utils/app_logger.dart'; +import '../../widgets/companion_remote/discovery_view.dart'; import '../../widgets/overlay_sheet.dart'; import '../../widgets/pill_input_decoration.dart'; -import 'pairing_screen.dart'; class MobileRemoteScreen extends StatefulWidget { const MobileRemoteScreen({super.key}); @@ -90,30 +90,7 @@ class _MobileRemoteScreenState extends State { } if (!provider.isConnected) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.phonelink_off, size: 64, color: Colors.grey), - const SizedBox(height: 16), - Text( - provider.status == RemoteSessionStatus.error - ? provider.session?.errorMessage ?? t.companionRemote.remote.connectionError - : t.companionRemote.remote.notConnected, - style: const TextStyle(fontSize: 20, color: Colors.grey), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - FilledButton.icon( - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => const PairingScreen())); - }, - icon: const Icon(Icons.link), - label: Text(t.companionRemote.connectToDevice), - ), - ], - ), - ); + return const DiscoveryView(); } return const _RemoteControlLayout(); diff --git a/lib/screens/companion_remote/pairing_screen.dart b/lib/widgets/companion_remote/discovery_view.dart similarity index 72% rename from lib/screens/companion_remote/pairing_screen.dart rename to lib/widgets/companion_remote/discovery_view.dart index bc33e2f4..47f646a9 100644 --- a/lib/screens/companion_remote/pairing_screen.dart +++ b/lib/widgets/companion_remote/discovery_view.dart @@ -8,14 +8,15 @@ import '../../providers/companion_remote_provider.dart'; import '../../providers/user_profile_provider.dart'; import '../../utils/app_logger.dart'; -class PairingScreen extends StatefulWidget { - const PairingScreen({super.key}); +/// Discovers LAN hosts and provides UI to connect to them. +class DiscoveryView extends StatefulWidget { + const DiscoveryView({super.key}); @override - State createState() => _PairingScreenState(); + State createState() => _DiscoveryViewState(); } -class _PairingScreenState extends State { +class _DiscoveryViewState extends State { final _hostAddressController = TextEditingController(); final _formKey = GlobalKey(); bool _isConnecting = false; @@ -23,6 +24,7 @@ class _PairingScreenState extends State { bool _showManualEntry = false; bool _cryptoReady = false; + late final CompanionRemoteProvider _provider; StreamSubscription>? _discoverySubscription; List _hosts = []; bool _isSearching = true; @@ -31,16 +33,16 @@ class _PairingScreenState extends State { @override void initState() { super.initState(); + _provider = context.read(); _initCryptoAndDiscover(); } Future _initCryptoAndDiscover() async { - final provider = context.read(); final home = context.read().home; - await provider.ensureCryptoReady(home); + await _provider.ensureCryptoReady(home); if (!mounted) return; - if (provider.isCryptoReady) { + if (_provider.isCryptoReady) { setState(() => _cryptoReady = true); _startDiscovery(); } else { @@ -53,8 +55,7 @@ class _PairingScreenState extends State { } void _startDiscovery() { - final provider = context.read(); - final stream = provider.discoverHosts(); + final stream = _provider.discoverHosts(); if (stream == null) return; _discoverySubscription = stream.listen((hosts) { @@ -66,7 +67,6 @@ class _PairingScreenState extends State { } }); - // Show "no devices found" after 10 seconds of no results _searchTimeout = Timer(const Duration(seconds: 10), () { if (mounted && _hosts.isEmpty) { setState(() => _isSearching = false); @@ -79,55 +79,25 @@ class _PairingScreenState extends State { _hostAddressController.dispose(); _discoverySubscription?.cancel(); _searchTimeout?.cancel(); - context.read().stopDiscovery(); + _provider.stopDiscovery(); super.dispose(); } - Future _connectToHost(DiscoveredHost host) async { + Future _connect(Future Function() action) async { setState(() { _isConnecting = true; _errorMessage = null; }); try { - final provider = context.read(); - await provider.connectToDiscoveredHost(host); - - if (mounted) { - Navigator.of(context).pop(); - } + _provider.stopDiscovery(); + await action(); } catch (e) { - appLogger.e('Failed to connect to host', error: e); + appLogger.e('Failed to connect', error: e); if (!mounted) return; - setState(() { - _isConnecting = false; - _errorMessage = _parseErrorMessage(e.toString()); - }); - } - } - - Future _connectManual() async { - if (!_formKey.currentState!.validate()) return; - - setState(() { - _isConnecting = true; - _errorMessage = null; - }); - - try { - final provider = context.read(); - await provider.connectToManualHost(_hostAddressController.text.trim()); - - if (mounted) { - Navigator.of(context).pop(); - } - } catch (e) { - appLogger.e('Failed to connect to manual host', error: e); - if (!mounted) return; - setState(() { - _isConnecting = false; - _errorMessage = _parseErrorMessage(e.toString()); - }); + setState(() => _errorMessage = _parseErrorMessage(e.toString())); + } finally { + if (mounted) setState(() => _isConnecting = false); } } @@ -157,28 +127,11 @@ class _PairingScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(t.companionRemote.connectToDevice), - ), - body: _buildBody(), - ); - } - - Widget _buildBody() { return SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Icon(Icons.devices, size: 64, color: Theme.of(context).colorScheme.primary), - const SizedBox(height: 16), - Text( - t.companionRemote.pairing.pairWithDesktop, - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), Text( t.companionRemote.pairing.discoveryDescription, style: Theme.of(context).textTheme.bodyMedium, @@ -208,14 +161,12 @@ class _PairingScreenState extends State { const SizedBox(height: 16), ], - // Discovered hosts _buildDiscoverySection(), const SizedBox(height: 24), const Divider(), const SizedBox(height: 16), - // Manual entry fallback _buildManualEntrySection(), ], ), @@ -231,10 +182,7 @@ class _PairingScreenState extends State { children: [ const Icon(Icons.warning_amber, size: 48, color: Colors.orange), const SizedBox(height: 12), - Text( - t.companionRemote.pairing.cryptoInitFailed, - textAlign: TextAlign.center, - ), + Text(t.companionRemote.pairing.cryptoInitFailed, textAlign: TextAlign.center), ], ), ), @@ -247,16 +195,9 @@ class _PairingScreenState extends State { padding: const EdgeInsets.all(24.0), child: Column( children: [ - const SizedBox( - width: 32, - height: 32, - child: CircularProgressIndicator(strokeWidth: 3), - ), + const SizedBox(width: 32, height: 32, child: CircularProgressIndicator(strokeWidth: 3)), const SizedBox(height: 16), - Text( - t.companionRemote.pairing.searchingForDevices, - style: Theme.of(context).textTheme.bodyMedium, - ), + Text(t.companionRemote.pairing.searchingForDevices, style: Theme.of(context).textTheme.bodyMedium), ], ), ), @@ -291,10 +232,7 @@ class _PairingScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - t.companionRemote.pairing.availableDevices, - style: Theme.of(context).textTheme.titleMedium, - ), + Text(t.companionRemote.pairing.availableDevices, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), ..._hosts.map((host) => Card( child: ListTile( @@ -304,7 +242,7 @@ class _PairingScreenState extends State { trailing: _isConnecting ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.arrow_forward), - onTap: _isConnecting ? null : () => _connectToHost(host), + onTap: _isConnecting ? null : () => _connect(() => _provider.connectToDiscoveredHost(host)), ), )), ], @@ -326,9 +264,7 @@ class _PairingScreenState extends State { const SizedBox(width: 8), Text( t.companionRemote.pairing.manualConnection, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.primary), ), ], ), @@ -352,8 +288,7 @@ class _PairingScreenState extends State { if (value == null || value.isEmpty) { return t.companionRemote.pairing.validationHostRequired; } - final parts = value.split(':'); - if (parts.length != 2) { + if (value.split(':').length != 2) { return t.companionRemote.pairing.validationHostFormat; } return null; @@ -362,7 +297,12 @@ class _PairingScreenState extends State { ), const SizedBox(height: 16), FilledButton.icon( - onPressed: _isConnecting ? null : _connectManual, + onPressed: _isConnecting + ? null + : () { + if (!_formKey.currentState!.validate()) return; + _connect(() => _provider.connectToManualHost(_hostAddressController.text.trim())); + }, icon: _isConnecting ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.link),