import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../data/sources/localiser/onboarding_client.dart'; import '../../data/sources/mdns/mdns_discovery.dart'; import '../../domain/models/server_config.dart'; import '../../providers.dart'; class ServerDiscoveryScreen extends ConsumerStatefulWidget { const ServerDiscoveryScreen({super.key}); @override ConsumerState createState() => _ServerDiscoveryScreenState(); } class _ServerDiscoveryScreenState extends ConsumerState { final _hostController = TextEditingController(); final _portController = TextEditingController(text: '4000'); bool _connecting = false; String? _error; final _discovery = MdnsDiscovery(); final _discoveredServers = []; StreamSubscription? _discoverySub; @override void initState() { super.initState(); _discoverySub = _discovery.discover().listen( (server) { if (!_discoveredServers.contains(server)) { setState(() => _discoveredServers.add(server)); } }, onError: (_) {}, ); } @override void dispose() { _discoverySub?.cancel(); _hostController.dispose(); _portController.dispose(); _discovery.stop(); super.dispose(); } Future _connect(ServerConfig config) async { setState(() { _connecting = true; _error = null; }); try { final checklist = await OnboardingClient(config: config).getChecklist(); ref.read(serverConfigProvider.notifier).state = config; await ref.read(credentialStoreProvider).saveServer(config); if (!mounted) return; if (!checklist.hasAdmin) { context.push('/onboarding'); } else { context.push('/login'); } } catch (e) { setState(() => _error = e.toString()); } finally { if (mounted) setState(() => _connecting = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Connect to Server')), body: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text('Discovered servers'), const SizedBox(height: 8), if (_discoveredServers.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 32), child: Center( child: Text( 'Scanning…', style: TextStyle(color: Colors.grey), ), ), ) else ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200), child: ListView.builder( shrinkWrap: true, itemCount: _discoveredServers.length, itemBuilder: (context, i) { final server = _discoveredServers[i]; return ListTile( title: Text(server.host), subtitle: Text('Port ${server.port}'), trailing: const Icon(Icons.chevron_right), onTap: _connecting ? null : () => _connect(server), ); }, ), ), const Divider(), const SizedBox(height: 16), const Text('Manual entry'), const SizedBox(height: 8), TextField( controller: _hostController, decoration: const InputDecoration( labelText: 'Host / IP', hintText: '192.168.1.100', border: OutlineInputBorder(), ), keyboardType: TextInputType.url, ), const SizedBox(height: 12), TextField( controller: _portController, decoration: const InputDecoration( labelText: 'Port', border: OutlineInputBorder(), ), keyboardType: TextInputType.number, ), const SizedBox(height: 16), if (_error != null) Padding( padding: const EdgeInsets.only(bottom: 12), child: Text( _error!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), FilledButton( onPressed: _connecting ? null : () => _connect(ServerConfig( host: _hostController.text.trim(), port: int.tryParse(_portController.text) ?? 4000, )), child: _connecting ? const SizedBox.square( dimension: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Connect'), ), ], ), ), ); } }