diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index cc6b816..0ad7934 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -69,21 +69,21 @@ class _HomeScreenState extends State { final recent = allRecipes.take(6).toList(); if (mounted) { - setState(() { + setState(() { _allRecipes = allRecipes; _favoriteRecipes = favorites; _recentRecipes = recent; _tagCounts = Map.fromEntries(sortedTags); - _isLoading = false; - }); + _isLoading = false; + }); } } catch (e) { Logger.error('Failed to load home data', e); if (mounted) { - setState(() { - _isLoading = false; - }); - } + setState(() { + _isLoading = false; + }); + } } } @@ -420,7 +420,7 @@ class _HomeScreenState extends State { ], if (recipe.isFavourite) ...[ if (recipe.rating > 0) const SizedBox(width: 8), - Icon( + Icon( Icons.favorite, size: 16, color: Colors.red, @@ -443,8 +443,8 @@ class _HomeScreenState extends State { child: Icon( Icons.restaurant_menu, size: 48, - color: Colors.grey, - ), + color: Colors.grey, + ), ), ); } @@ -583,14 +583,14 @@ class _HomeScreenState extends State { ), ], ), - ), + ), Icon( Icons.favorite, color: Colors.red, size: 20, - ), - ], - ), + ), + ], + ), ), ), ); @@ -680,7 +680,7 @@ class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> { ), ), ], - ); + ); } return Stack( diff --git a/lib/ui/relay_management/relay_management_screen.dart b/lib/ui/relay_management/relay_management_screen.dart index 0598ed0..550dde8 100644 --- a/lib/ui/relay_management/relay_management_screen.dart +++ b/lib/ui/relay_management/relay_management_screen.dart @@ -28,6 +28,7 @@ class RelayManagementScreen extends StatefulWidget { class _RelayManagementScreenState extends State { final TextEditingController _urlController = TextEditingController(); + final FocusNode _urlFocusNode = FocusNode(); bool _useNip05RelaysAutomatically = false; bool _isDarkMode = false; bool _isLoadingSetting = true; @@ -37,8 +38,6 @@ class _RelayManagementScreenState extends State { MultiMediaService? _multiMediaService; List _mediaServers = []; MediaServerConfig? _defaultMediaServer; - bool _mediaServersExpanded = false; - bool _settingsExpanded = true; // Settings expanded by default // Store original values to detect changes List _originalMediaServers = []; @@ -51,6 +50,16 @@ class _RelayManagementScreenState extends State { super.initState(); _initializeMultiMediaService(); _loadSetting(); + _urlFocusNode.addListener(_onUrlFocusChanged); + } + + void _onUrlFocusChanged() { + if (_urlFocusNode.hasFocus && _urlController.text.isEmpty) { + _urlController.text = 'wss://'; + _urlController.selection = TextSelection.fromPosition( + TextPosition(offset: _urlController.text.length), + ); + } } Future _initializeMultiMediaService() async { @@ -71,7 +80,6 @@ class _RelayManagementScreenState extends State { _defaultMediaServer = _multiMediaService!.getDefaultServer(); _originalMediaServers = List.from(_mediaServers); _originalDefaultServerId = _defaultMediaServer?.id; - _mediaServersExpanded = _mediaServers.isNotEmpty; // Auto-expand if servers exist }); } } @@ -133,6 +141,8 @@ class _RelayManagementScreenState extends State { @override void dispose() { _urlController.dispose(); + _urlFocusNode.removeListener(_onUrlFocusChanged); + _urlFocusNode.dispose(); super.dispose(); } @@ -399,7 +409,7 @@ class _RelayManagementScreenState extends State { (s) => s.isDefault, orElse: () => updatedServers.isNotEmpty ? updatedServers.first : result, ); - _mediaServersExpanded = true; // Expand to show the newly added server + // Server added successfully }); Logger.info('Media server added. Total servers: ${_mediaServers.length}, Default: ${_defaultMediaServer?.baseUrl}'); @@ -495,369 +505,471 @@ class _RelayManagementScreenState extends State { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return Scaffold( appBar: AppBar( title: const Text('Advanced Settings'), + actions: _hasUnsavedChanges + ? [ + TextButton.icon( + onPressed: _saveAllSettings, + icon: const Icon(Icons.save, size: 18), + label: const Text('Save'), + style: TextButton.styleFrom( + foregroundColor: Colors.green, + ), + ), + ] + : null, ), body: ListenableBuilder( listenable: widget.controller, builder: (context, child) { return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Settings section - Now using ExpansionTile for uniformity - ExpansionTile( - title: const Text('Settings'), - subtitle: _hasUnsavedChanges - ? const Text('You have unsaved changes', style: TextStyle(color: Colors.orange)) - : Text('${_useNip05RelaysAutomatically ? "NIP-05" : "Manual"} relays • ${_isDarkMode ? "Dark" : "Light"} mode'), - initiallyExpanded: _settingsExpanded, - trailing: _hasUnsavedChanges - ? ElevatedButton.icon( - onPressed: _saveAllSettings, - icon: const Icon(Icons.save, size: 18), - label: const Text('Save'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + // Preferences Section + _buildSectionCard( + context: context, + title: 'Preferences', + icon: Icons.settings_outlined, + child: _isLoadingSetting + ? const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Center(child: CircularProgressIndicator(strokeWidth: 2)), + ) + : Column( + children: [ + _buildSettingTile( + context: context, + title: 'Dark Mode', + subtitle: 'Enable dark theme for the app', + value: _isDarkMode, + onChanged: (value) { + _saveSetting('dark_mode', value); + }, + icon: Icons.dark_mode_outlined, + ), + ], ), - ) - : null, - onExpansionChanged: (expanded) { - setState(() { - _settingsExpanded = expanded; - }); - }, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_isLoadingSetting) - const Row( - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(height: 8), + + // Media Server Section + Builder( + builder: (context) { + final immichEnabled = dotenv.env['IMMICH_ENABLE']?.toLowerCase() != 'false'; + final defaultBlossomServer = dotenv.env['BLOSSOM_SERVER'] ?? 'https://media.based21.com'; + + return _buildSectionCard( + context: context, + title: 'Media Server', + icon: Icons.storage_outlined, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (_defaultMediaServer != null) + _buildMediaServerTile( + context: context, + server: _defaultMediaServer!, + isDefault: true, + onEdit: () => _editMediaServer(_defaultMediaServer!), + onRemove: () => _removeMediaServer(_defaultMediaServer!), + ) + else + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + 'No media server configured', + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.grey, + fontSize: 12, + ), ), - 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), + OutlinedButton.icon( + onPressed: () => _addMediaServer(immichEnabled, defaultBlossomServer), + icon: const Icon(Icons.add, size: 16), + label: const Text('Add More', style: TextStyle(fontSize: 13)), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + minimumSize: const Size(0, 36), ), - value: _isDarkMode, - onChanged: (value) { - _saveSetting('dark_mode', value); - }, - contentPadding: EdgeInsets.zero, - ), - const SizedBox(height: 16), - // Media Server Settings - Multiple Servers - Builder( - builder: (context) { - final immichEnabled = dotenv.env['IMMICH_ENABLE']?.toLowerCase() != 'false'; - final defaultBlossomServer = dotenv.env['BLOSSOM_SERVER'] ?? 'https://media.based21.com'; - - return ExpansionTile( - key: ValueKey('media-servers-${_mediaServers.length}-${_mediaServersExpanded}'), - title: const Text('Media Servers'), - subtitle: Text(_mediaServers.isEmpty - ? 'No servers configured' - : '${_mediaServers.length} server(s)${_defaultMediaServer != null ? " • Default: ${_defaultMediaServer!.name ?? _defaultMediaServer!.type}" : ""}'), - initiallyExpanded: _mediaServersExpanded, - onExpansionChanged: (expanded) { - setState(() { - _mediaServersExpanded = expanded; - }); - }, - children: [ - // Server list - if (_mediaServers.isEmpty) - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'No media servers configured. Add one to get started.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - ) - else - ...List.generate(_mediaServers.length, (index) { - final server = _mediaServers[index]; - return ListTile( - key: ValueKey(server.id), - leading: Icon( - server.isDefault ? Icons.star : Icons.star_border, - color: server.isDefault ? Colors.amber : Colors.grey, - ), - title: Text(server.name ?? '${server.type.toUpperCase()} Server'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(server.baseUrl), - if (server.isDefault) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Chip( - label: const Text('Default'), - labelStyle: const TextStyle(fontSize: 10), - padding: EdgeInsets.zero, - ), - ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: () => _editMediaServer(server), - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => _removeMediaServer(server), - ), - ], - ), - onTap: () => _setDefaultServer(server), - ); - }), - // Add server button - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton.icon( - onPressed: () => _addMediaServer(immichEnabled, defaultBlossomServer), - icon: const Icon(Icons.add), - label: const Text('Add Media Server'), - ), - ), - ], - ); - }, ), ], - ], - ), - ), - ], - ), - // 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, - ), - ], - ), + ); + }, ), - - // Relay Management Section (Expandable) - ExpansionTile( - title: const Text('Nostr Relays'), - subtitle: Text('${widget.controller.relays.length} relay(s) configured'), - initiallyExpanded: false, - children: [ - 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 { - final hasEnabled = widget.controller.relays.any((r) => r.isEnabled); - if (hasEnabled) { - await widget.controller.turnAllOff(); - } else { - await widget.controller.turnAllOn(); - } - }, - icon: const Icon(Icons.power_settings_new), - label: Text( - widget.controller.relays.isNotEmpty && - widget.controller.relays.any((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(height: 8), + + // Relays Section + _buildSectionCard( + context: context, + title: 'Relays', + icon: Icons.cloud_outlined, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Add relay input + TextField( + controller: _urlController, + focusNode: _urlFocusNode, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + labelText: 'Relay URL', + hintText: 'wss://relay.example.com', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.link, size: 18), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + isDense: true, ), - 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) { + keyboardType: TextInputType.url, + ), + const SizedBox(height: 8), + ElevatedButton.icon( + onPressed: () async { + String url = _urlController.text.trim(); + // Remove wss:// prefix if user added it manually, we'll add it properly + if (url.startsWith('wss://')) { + url = url.substring(6); + } + if (url.isNotEmpty) { + final fullUrl = url.startsWith('wss://') ? url : 'wss://$url'; + final success = await widget.controller.addRelay(fullUrl); + 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), + 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'), + } + }, + icon: const Icon(Icons.add, size: 16), + label: const Text('Add Relay', style: TextStyle(fontSize: 13)), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + minimumSize: const Size(0, 36), ), - ], - ), - const SizedBox(height: 16), - // Relay list - SizedBox( - height: 300, - child: widget.controller.relays.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.cloud_off, - size: 48, - color: Colors.grey.shade400, - ), - const SizedBox(height: 16), - Text( - 'No relays configured', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Colors.grey, + ), + const SizedBox(height: 12), + // Relay list + if (widget.controller.relays.isEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + Icon( + Icons.cloud_off, + size: 36, + color: Colors.grey.shade400, + ), + const SizedBox(height: 8), + Text( + 'No relays configured', + style: theme.textTheme.bodyMedium?.copyWith( + color: Colors.grey, + fontSize: 13, + ), + ), + const SizedBox(height: 4), + Text( + 'Add a relay to get started', + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.grey, + fontSize: 11, + ), + ), + ], + ), + ) + else + ...widget.controller.relays.map((relay) { + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: _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), ), + ); + }, ), - const SizedBox(height: 8), - Text( - 'Add a relay to get started', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.grey, - ), + ); + }).toList(), + ], + ), + ), + + // Error message + if (widget.controller.error != null) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.red.shade200), + ), + child: Row( + children: [ + Icon(Icons.error_outline, size: 16, color: Colors.red.shade700), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.controller.error!, + style: TextStyle( + color: Colors.red.shade700, + fontSize: 12, ), - ], + ), ), - ) - : ListView.builder( - shrinkWrap: true, - 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), - ), - ); - }, - ); - }, - ), - ), - ], + IconButton( + icon: const Icon(Icons.close, size: 16), + onPressed: widget.controller.clearError, + color: Colors.red.shade700, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 24, minHeight: 24), + ), + ], ), ), ], - ), - ], + ], ), ); }, ), ); } + + Widget _buildSectionCard({ + required BuildContext context, + required String title, + required IconData icon, + required Widget child, + }) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Card( + elevation: 0, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: isDark ? Colors.grey.shade800 : Colors.grey.shade200, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: theme.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Icon(icon, color: theme.primaryColor, size: 16), + ), + const SizedBox(width: 8), + Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + ], + ), + const SizedBox(height: 12), + child, + ], + ), + ), + ); + } + + Widget _buildSettingTile({ + required BuildContext context, + required String title, + required String subtitle, + required bool value, + required ValueChanged onChanged, + required IconData icon, + }) { + final theme = Theme.of(context); + + return Row( + children: [ + Icon(icon, size: 16, color: theme.primaryColor), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.grey, + fontSize: 11, + ), + ), + ], + ), + ), + Transform.scale( + scale: 0.85, + child: Switch( + value: value, + onChanged: onChanged, + ), + ), + ], + ); + } + + Widget _buildMediaServerTile({ + required BuildContext context, + required MediaServerConfig server, + required bool isDefault, + required VoidCallback onEdit, + required VoidCallback onRemove, + }) { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isDefault ? theme.primaryColor.withValues(alpha: 0.3) : Colors.grey.shade300, + width: isDefault ? 1.5 : 1, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: theme.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Icon( + Icons.storage, + color: theme.primaryColor, + size: 16, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + server.name ?? '${server.type.toUpperCase()} Server', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 13, + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (isDefault) ...[ + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: theme.primaryColor.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(3), + ), + child: Text( + 'Default', + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w600, + color: theme.primaryColor, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 2), + Text( + server.baseUrl, + style: theme.textTheme.bodySmall?.copyWith( + color: Colors.grey, + fontSize: 11, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.edit, size: 16), + onPressed: onEdit, + tooltip: 'Edit', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 28, minHeight: 28), + ), + IconButton( + icon: const Icon(Icons.delete_outline, size: 16), + onPressed: onRemove, + tooltip: 'Remove', + color: Colors.red, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 28, minHeight: 28), + ), + ], + ), + ); + } } /// Widget for displaying a single relay in the list. @@ -879,94 +991,102 @@ class _RelayListItem extends StatelessWidget { @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, - ), - ), - ], - ), + final theme = Theme.of(context); + final isConnected = relay.isConnected && relay.isEnabled; + + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isConnected + ? Colors.green.withValues(alpha: 0.3) + : Colors.grey.shade300, + width: isConnected ? 1.5 : 1, + ), + ), + child: Row( + children: [ + // Status indicator + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isConnected ? Colors.green : Colors.grey, + boxShadow: isConnected + ? [ + BoxShadow( + color: Colors.green.withValues(alpha: 0.5), + blurRadius: 3, + spreadRadius: 1, + ), + ] + : null, ), - const SizedBox(width: 8), - // Toggle switch - Row( + ), + const SizedBox(width: 10), + // URL and status text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( - relay.isEnabled ? 'ON' : 'OFF', - style: TextStyle( - fontSize: 11, + relay.url, + style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, - color: relay.isEnabled - ? Colors.green - : Colors.grey[600], - ), + fontSize: 13, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const SizedBox(height: 2), + Row( + children: [ + Icon( + isConnected ? Icons.check_circle : Icons.cancel, + size: 12, + color: isConnected ? Colors.green : Colors.grey, ), - const SizedBox(width: 4), - Transform.scale( - scale: 0.8, - child: Switch( - value: relay.isEnabled, - onChanged: (_) => onToggle(), + const SizedBox(width: 3), + Text( + isConnected ? 'Connected' : 'Disabled', + style: theme.textTheme.bodySmall?.copyWith( + color: isConnected ? Colors.green : Colors.grey, + fontWeight: FontWeight.w500, + fontSize: 11, + ), ), + ], ), ], ), - 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, - ), + ), + const SizedBox(width: 8), + // Toggle switch + Transform.scale( + scale: 0.85, + child: Switch( + value: relay.isEnabled, + onChanged: (_) => onToggle(), ), - ], - ), + ), + const SizedBox(width: 4), + // Remove button + IconButton( + icon: const Icon(Icons.delete_outline, size: 16), + color: Colors.red, + tooltip: 'Remove', + onPressed: onRemove, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 28, + minHeight: 28, + ), + ), + ], ), ); }