import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/server_config.dart'; import '../providers/providers.dart'; import '../services/mqtt_service.dart' show ConnectionStatus; import '../services/wol_service.dart'; import '../theme/app_theme.dart'; import '../widgets/status_dot.dart'; class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState { final _formKey = GlobalKey(); late final TextEditingController _localHostController; late final TextEditingController _vpnHostController; late final TextEditingController _remoteHostController; late final TextEditingController _portController; late final TextEditingController _macController; late final TextEditingController _mqttTokenController; bool _isWaking = false; @override void initState() { super.initState(); final config = ref.read(serverConfigProvider); _localHostController = TextEditingController(text: config?.localHost ?? ''); _vpnHostController = TextEditingController(text: config?.vpnHost ?? ''); _remoteHostController = TextEditingController(text: config?.host ?? ''); _portController = TextEditingController(text: '${config?.port ?? 8765}'); _macController = TextEditingController(text: config?.macAddress ?? ''); _mqttTokenController = TextEditingController(text: config?.mqttToken ?? ''); } @override void dispose() { _localHostController.dispose(); _vpnHostController.dispose(); _remoteHostController.dispose(); _portController.dispose(); _macController.dispose(); _mqttTokenController.dispose(); super.dispose(); } Future _save() async { if (!_formKey.currentState!.validate()) return; final config = ServerConfig( host: _remoteHostController.text.trim(), port: int.tryParse(_portController.text.trim()) ?? 8765, localHost: _localHostController.text.trim().isEmpty ? null : _localHostController.text.trim(), vpnHost: _vpnHostController.text.trim().isEmpty ? null : _vpnHostController.text.trim(), macAddress: _macController.text.trim().isEmpty ? null : _macController.text.trim(), mqttToken: _mqttTokenController.text.trim().isEmpty ? null : _mqttTokenController.text.trim(), ); await ref.read(serverConfigProvider.notifier).save(config); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Server configuration saved'), duration: Duration(seconds: 2), ), ); } } Future _wakeMac() async { final mac = _macController.text.trim(); if (mac.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No MAC address configured')), ); return; } setState(() => _isWaking = true); try { await WolService.wake(mac, localHost: _localHostController.text.trim()); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Wake-on-LAN packet sent')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('WoL failed: $e')), ); } } finally { if (mounted) setState(() => _isWaking = false); } } @override Widget build(BuildContext context) { final wsStatus = ref.watch(wsStatusProvider); return Scaffold( appBar: AppBar( title: const Text('Settings'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Connection status card Card( child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ StatusDot(status: wsStatus), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _statusText(wsStatus), style: Theme.of(context).textTheme.bodyLarge, ), if (ref.watch(connectedViaProvider).isNotEmpty) Text( 'via ${ref.watch(connectedViaProvider)}', style: TextStyle( fontSize: 12, color: Colors.grey.shade500, ), ), ], ), ), ], ), ), ), const SizedBox(height: 24), // Local address Text('Local Address', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _localHostController, decoration: const InputDecoration( hintText: '192.168.1.100', ), keyboardType: TextInputType.url, validator: (v) { if (v == null || v.trim().isEmpty) return null; final ip = RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'); return ip.hasMatch(v.trim()) ? null : 'Enter a valid IP address'; }, ), const SizedBox(height: 16), // VPN address Text('VPN Address', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _vpnHostController, decoration: const InputDecoration( hintText: '10.8.0.1 (OpenVPN static IP)', ), keyboardType: TextInputType.url, validator: (v) { if (v == null || v.trim().isEmpty) return null; final ip = RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'); return ip.hasMatch(v.trim()) ? null : 'Enter a valid IP address'; }, ), const SizedBox(height: 16), // Remote address Text('Remote Address', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _remoteHostController, decoration: const InputDecoration( hintText: 'your-server.example.com', ), keyboardType: TextInputType.url, validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null, ), const SizedBox(height: 16), // Port Text('Port', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _portController, decoration: const InputDecoration( hintText: '8765', ), keyboardType: TextInputType.number, validator: (v) { if (v == null || v.trim().isEmpty) return 'Required'; final port = int.tryParse(v.trim()); if (port == null || port < 1 || port > 65535) { return 'Port must be 1–65535'; } return null; }, ), const SizedBox(height: 16), // MAC address Text('MAC Address (for Wake-on-LAN)', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _macController, decoration: const InputDecoration( hintText: 'AA:BB:CC:DD:EE:FF', ), validator: (v) { if (v == null || v.trim().isEmpty) return null; final mac = RegExp( r'^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$'); return mac.hasMatch(v.trim()) ? null : 'Enter a valid MAC address (AA:BB:CC:DD:EE:FF)'; }, ), const SizedBox(height: 16), // MQTT Token Text('MQTT Token', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), TextFormField( controller: _mqttTokenController, decoration: const InputDecoration( hintText: 'Shared secret for MQTT auth', ), obscureText: true, ), const SizedBox(height: 24), // Save button ElevatedButton( onPressed: _save, style: ElevatedButton.styleFrom( backgroundColor: AppColors.accent, foregroundColor: Colors.white, ), child: const Text('Save Configuration'), ), const SizedBox(height: 12), // Wake Mac button OutlinedButton.icon( onPressed: _isWaking ? null : _wakeMac, icon: _isWaking ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.power_settings_new), label: const Text('Wake Mac'), ), const SizedBox(height: 12), // Reset TLS Trust button OutlinedButton.icon( onPressed: () async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Reset Server Trust?'), content: const Text( 'This clears the saved server certificate fingerprint. ' 'Use this if you reinstalled AIBroker or changed servers. ' 'The app will trust the next server it connects to.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Reset', style: TextStyle(color: AppColors.error)), ), ], ), ); if (confirmed == true && mounted) { // Access MqttService through the provider and reset trust final prefs = await SharedPreferences.getInstance(); await prefs.remove('trustedCertFingerprint'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Server trust reset. Reconnect to trust the new server.')), ); } }, icon: const Icon(Icons.shield_outlined), label: const Text('Reset Server Trust'), ), const SizedBox(height: 12), ], ), ), ), ); } String _statusText(ConnectionStatus status) { switch (status) { case ConnectionStatus.connected: return 'Connected'; case ConnectionStatus.connecting: return 'Connecting...'; case ConnectionStatus.reconnecting: return 'Reconnecting...'; case ConnectionStatus.disconnected: return 'Disconnected'; } } }