import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:nostr/nostr.dart' as nostr; import '../../shared/auth/auth.dart'; import '../../shared/relay/relay.dart'; import '../../shared/theme/theme.dart'; class SettingsPage extends HookConsumerWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final config = ref.watch(relayConfigProvider); final selectedAccent = ref.watch(accentProvider); return Scaffold( appBar: AppBar(title: const Text('Settings')), body: ListView( padding: const EdgeInsets.all(Grid.xs), children: [ // Connection info Text('Connection', style: context.textTheme.titleMedium), const SizedBox(height: Grid.twelve), ListTile( leading: const Icon(LucideIcons.server), title: const Text('Connected to'), subtitle: Text( config.baseUrl, style: context.textTheme.bodySmall?.copyWith( color: context.colors.onSurfaceVariant, ), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(Radii.md), side: BorderSide(color: context.colors.outlineVariant), ), ), if (config.nsec != null && config.nsec!.isNotEmpty) ...[ const SizedBox(height: Grid.xxs), Builder( builder: (context) { final privHex = nostr.Nip19.decodePrivkey(config.nsec!); final pubkey = privHex.isNotEmpty ? nostr.Keychain(privHex).public : 'unknown'; return ListTile( leading: const Icon(LucideIcons.key), title: const Text('Identity (pubkey)'), subtitle: Text( pubkey, style: context.textTheme.bodySmall?.copyWith( color: context.colors.onSurfaceVariant, fontFamily: 'GeistMono', fontSize: 11, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), trailing: IconButton( icon: const Icon(LucideIcons.copy, size: 16), onPressed: () { Clipboard.setData(ClipboardData(text: pubkey)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Pubkey copied'), duration: Duration(seconds: 2), ), ); }, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(Radii.md), side: BorderSide(color: context.colors.outlineVariant), ), ); }, ), ], const SizedBox(height: Grid.twelve), OutlinedButton.icon( onPressed: () => _confirmSignOut(context, ref), icon: const Icon(LucideIcons.logOut), label: const Text('Sign Out'), style: OutlinedButton.styleFrom( foregroundColor: context.colors.error, ), ), const SizedBox(height: Grid.sm), // Appearance Text('Appearance', style: context.textTheme.titleMedium), const SizedBox(height: Grid.twelve), SegmentedButton( segments: const [ ButtonSegment( value: ThemeMode.light, icon: Icon(LucideIcons.sun), label: Text('Light'), ), ButtonSegment( value: ThemeMode.system, icon: Icon(LucideIcons.monitor), label: Text('System'), ), ButtonSegment( value: ThemeMode.dark, icon: Icon(LucideIcons.moon), label: Text('Dark'), ), ], selected: {ref.watch(themeProvider)}, onSelectionChanged: (modes) { ref.read(themeProvider.notifier).setThemeMode(modes.first); }, ), const SizedBox(height: Grid.xs), // Accent color picker Text('Accent Color', style: context.textTheme.titleSmall), const SizedBox(height: Grid.xxs), Wrap( spacing: Grid.xxs, runSpacing: Grid.xxs, children: [ // Default (Mauve) swatch _AccentSwatch( color: context.colors.brightness == Brightness.light ? const Color(0xFF8839EF) : const Color(0xFFC6A0F6), label: 'Mauve', selected: selectedAccent == defaultAccentIndex, onTap: () => ref .read(accentProvider.notifier) .setAccent(defaultAccentIndex), ), // 8 accent colors from desktop for (var i = 0; i < accentColors.length; i++) _AccentSwatch( color: context.colors.brightness == Brightness.light ? accentColors[i].light : accentColors[i].dark, label: accentColors[i].name, selected: selectedAccent == i, onTap: () => ref.read(accentProvider.notifier).setAccent(i), ), ], ), ], ), ); } void _confirmSignOut(BuildContext context, WidgetRef ref) { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Sign Out'), content: const Text( 'You will need to scan a new pairing code from your ' 'desktop app to reconnect.', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { Navigator.of(ctx).pop(); // close dialog // Pop all pushed routes back to root so MaterialApp.home // rebuilds to PairingPage when auth state changes. Navigator.of(context).popUntil((route) => route.isFirst); ref.read(authProvider.notifier).signOut(); }, style: FilledButton.styleFrom( backgroundColor: Theme.of(ctx).colorScheme.error, ), child: const Text('Sign Out'), ), ], ), ); } } class _AccentSwatch extends StatelessWidget { const _AccentSwatch({ required this.color, required this.label, required this.selected, required this.onTap, }); final Color color; final String label; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { return Tooltip( message: label, child: GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: 36, height: 36, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(Radii.md), border: selected ? Border.all(color: context.colors.onSurface, width: 2.5) : Border.all(color: color.withValues(alpha: 0.4), width: 1), ), child: selected ? Icon(LucideIcons.check, size: 16, color: _contrastColor(color)) : null, ), ), ); } static Color _contrastColor(Color bg) { final lum = bg.computeLuminance(); final contrastWithBlack = (lum + 0.05) / 0.05; final contrastWithWhite = 1.05 / (lum + 0.05); return contrastWithBlack >= contrastWithWhite ? const Color(0xFF000000) : const Color(0xFFFFFFFF); } }