import 'package:flutter/material.dart'; import 'relay_management_controller.dart'; import '../../data/nostr/models/nostr_relay.dart'; import '../../core/service_locator.dart'; import '../../data/local/models/item.dart'; /// Screen for managing Nostr relays. /// /// Allows users to view, add, remove, test, and toggle relays. class RelayManagementScreen extends StatefulWidget { /// Controller for managing relay state. final RelayManagementController controller; /// Creates a [RelayManagementScreen] instance. const RelayManagementScreen({ super.key, required this.controller, }); @override State createState() => _RelayManagementScreenState(); } class _RelayManagementScreenState extends State { final TextEditingController _urlController = TextEditingController(); bool _useNip05RelaysAutomatically = false; bool _isDarkMode = false; bool _isLoadingSetting = true; @override void initState() { super.initState(); _loadSetting(); } @override void dispose() { _urlController.dispose(); super.dispose(); } Future _loadSetting() async { try { final localStorage = ServiceLocator.instance.localStorageService; if (localStorage == null) { setState(() { _isLoadingSetting = false; }); return; } final settingsItem = await localStorage.getItem('app_settings'); if (settingsItem != null) { final useNip05 = settingsItem.data['use_nip05_relays_automatically'] == true; final isDark = settingsItem.data['dark_mode'] == true; setState(() { _useNip05RelaysAutomatically = useNip05; _isDarkMode = isDark; _isLoadingSetting = false; }); // Update theme notifier if available final themeNotifier = ServiceLocator.instance.themeNotifier; if (themeNotifier != null) { themeNotifier.setThemeMode(isDark ? ThemeMode.dark : ThemeMode.light); } } else { setState(() { _isLoadingSetting = false; }); } } catch (e) { setState(() { _isLoadingSetting = false; }); } } Future _saveSetting(String key, bool value) async { try { final localStorage = ServiceLocator.instance.localStorageService; if (localStorage == null) return; final settingsItem = await localStorage.getItem('app_settings'); final data = settingsItem?.data ?? {}; data[key] = value; await localStorage.insertItem(Item( id: 'app_settings', data: data, )); setState(() { if (key == 'use_nip05_relays_automatically') { _useNip05RelaysAutomatically = value; } else if (key == 'dark_mode') { _isDarkMode = value; // Notify the app to update theme _updateAppTheme(value); } }); } catch (e) { // Log error but don't show to user - setting will just not persist } } void _updateAppTheme(bool isDark) { // Update the theme notifier, which MyApp is listening to final themeNotifier = ServiceLocator.instance.themeNotifier; if (themeNotifier != null) { themeNotifier.setThemeMode(isDark ? ThemeMode.dark : ThemeMode.light); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Nostr Relay Management'), ), body: ListenableBuilder( listenable: widget.controller, builder: (context, child) { return Column( children: [ // Settings section Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).cardColor, border: Border( bottom: BorderSide( color: Theme.of(context).dividerColor, width: 1, ), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Settings', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), if (_isLoadingSetting) const Row( children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), SizedBox(width: 12), Text('Loading settings...'), ], ) else SwitchListTile( title: const Text('Use NIP-05 relays automatically'), subtitle: const Text( 'Automatically replace relays with NIP-05 preferred relays upon login', style: TextStyle(fontSize: 12), ), value: _useNip05RelaysAutomatically, onChanged: (value) { _saveSetting('use_nip05_relays_automatically', value); }, contentPadding: EdgeInsets.zero, ), const SizedBox(height: 8), SwitchListTile( title: const Text('Dark Mode'), subtitle: const Text( 'Enable dark theme for the app', style: TextStyle(fontSize: 12), ), value: _isDarkMode, onChanged: (value) { _saveSetting('dark_mode', value); }, contentPadding: EdgeInsets.zero, ), ], ), ), // Error message if (widget.controller.error != null) Container( width: double.infinity, padding: const EdgeInsets.all(16), color: Colors.red.shade100, child: Row( children: [ Icon(Icons.error, color: Colors.red.shade700), const SizedBox(width: 8), Expanded( child: Text( widget.controller.error!, style: TextStyle(color: Colors.red.shade700), ), ), IconButton( icon: const Icon(Icons.close), onPressed: widget.controller.clearError, color: Colors.red.shade700, ), ], ), ), // Top action buttons Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Test All and Toggle All buttons Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: widget.controller.isCheckingHealth ? null : widget.controller.checkRelayHealth, icon: widget.controller.isCheckingHealth ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.network_check), label: const Text('Test All'), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton.icon( onPressed: widget.controller.relays.isEmpty ? null : () async { await widget.controller.toggleAllRelays(); }, icon: const Icon(Icons.power_settings_new), label: Text( widget.controller.relays.isNotEmpty && widget.controller.relays.every((r) => r.isEnabled) ? 'Turn All Off' : 'Turn All On', ), ), ), ], ), const SizedBox(height: 16), // Add relay input Row( children: [ Expanded( child: TextField( controller: _urlController, decoration: InputDecoration( labelText: 'Relay URL', hintText: 'wss://relay.example.com', border: const OutlineInputBorder(), ), keyboardType: TextInputType.url, ), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: () async { final url = _urlController.text.trim(); if (url.isNotEmpty) { final success = await widget.controller.addRelay(url); if (mounted) { if (success) { _urlController.clear(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Relay added and connected successfully'), backgroundColor: Colors.green, duration: Duration(seconds: 1), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( widget.controller.error ?? 'Failed to connect to relay', ), backgroundColor: Colors.orange, duration: const Duration(seconds: 3), ), ); } } } }, icon: const Icon(Icons.add), label: const Text('Add'), ), ], ), ], ), ), const Divider(), // Relay list Expanded( child: widget.controller.relays.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.cloud_off, size: 64, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( 'No relays configured', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Colors.grey, ), ), const SizedBox(height: 8), Text( 'Add a relay to get started', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey, ), ), ], ), ) : ListView.builder( itemCount: widget.controller.relays.length, itemBuilder: (context, index) { final relay = widget.controller.relays[index]; return _RelayListItem( relay: relay, onToggle: () async { await widget.controller.toggleRelay(relay.url); }, onRemove: () { widget.controller.removeRelay(relay.url); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Relay ${relay.url} removed'), duration: const Duration(seconds: 2), ), ); }, ); }, ), ), ], ); }, ), ); } } /// Widget for displaying a single relay in the list. class _RelayListItem extends StatelessWidget { /// The relay to display. final NostrRelay relay; /// Callback when toggle is pressed. final VoidCallback onToggle; /// Callback when remove is pressed. final VoidCallback onRemove; const _RelayListItem({ required this.relay, required this.onToggle, required this.onRemove, }); @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ // Status indicator Container( width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: relay.isConnected && relay.isEnabled ? Colors.green : Colors.grey, ), ), const SizedBox(width: 8), // URL and status text Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( relay.url, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), const SizedBox(height: 2), Text( relay.isConnected && relay.isEnabled ? 'Connected' : 'Disabled', style: TextStyle( fontSize: 11, color: relay.isConnected && relay.isEnabled ? Colors.green : Colors.grey, ), ), ], ), ), const SizedBox(width: 8), // Toggle switch Row( mainAxisSize: MainAxisSize.min, children: [ Text( relay.isEnabled ? 'ON' : 'OFF', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: relay.isEnabled ? Colors.green : Colors.grey[600], ), ), const SizedBox(width: 4), Transform.scale( scale: 0.8, child: Switch( value: relay.isEnabled, onChanged: (_) => onToggle(), ), ), ], ), const SizedBox(width: 4), // Remove button IconButton( icon: const Icon(Icons.delete, size: 18), color: Colors.red, tooltip: 'Remove', onPressed: onRemove, padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 32, minHeight: 32, ), ), ], ), ), ); } }