diff --git a/lib/app.dart b/lib/app.dart index 08e7371..858688c 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -19,6 +19,7 @@ import 'package:notas/widgets/app_title_bar.dart'; import 'package:notas/widgets/sync_status_indicator.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; enum _AppSection { @@ -50,6 +51,7 @@ class _NotesAppState extends State static const Duration _screenTransitionDuration = Duration(milliseconds: 280); static const Duration _biometricInactivityTimeout = Duration(minutes: 5); static const Duration _syncInterval = Duration(minutes: 5); + static const String _themeSeedColorKey = 'theme_seed_color_v1'; final LocalVaultService _vaultService = LocalVaultService.instance; final GlobalKey _scaffoldMessengerKey = @@ -71,6 +73,7 @@ class _NotesAppState extends State SyncStatus _syncStatus = SyncStatus.idle; String? _syncErrorMessage; int _homeRefreshToken = 0; + Color _themeSeedColor = Colors.amber; @override void initState() { @@ -80,6 +83,7 @@ class _NotesAppState extends State windowManager.addListener(this); windowManager.setPreventClose(true); } + _loadThemeSeedColor(); _bootstrapVault(); } @@ -96,6 +100,33 @@ class _NotesAppState extends State super.dispose(); } + Future _loadThemeSeedColor() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final int? storedColorValue = prefs.getInt(_themeSeedColorKey); + if (storedColorValue == null || !mounted) { + return; + } + + setState(() { + _themeSeedColor = Color(storedColorValue); + }); + } + + Future _setThemeSeedColor(Color color) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_themeSeedColorKey, color.value); + + if (!mounted) { + return; + } + + setState(() { + _themeSeedColor = color; + }); + } + + ThemeData get _theme => AppTheme.theme(seedColor: _themeSeedColor); + @override void didChangeAppLifecycleState(AppLifecycleState state) { if (_isUnlocking) { @@ -669,7 +700,7 @@ class _NotesAppState extends State navigatorKey: _navigatorKey, title: 'Mis Notas', debugShowCheckedModeBanner: false, - theme: AppTheme.theme, + theme: _theme, home: const Scaffold( body: SafeArea( child: Column( @@ -700,7 +731,7 @@ class _NotesAppState extends State title: 'Mis Notas', debugShowCheckedModeBanner: false, scaffoldMessengerKey: _scaffoldMessengerKey, - theme: AppTheme.theme, + theme: _theme, home: home, ); } @@ -722,6 +753,8 @@ class _NotesAppState extends State onDeleteAllData: _resetLocalVaultData, onBackToHome: _openHome, onForceSync: () => _performSync(forceFull: true), + currentSeedColor: _themeSeedColor, + onThemeColorSelected: _setThemeSeedColor, ); return MaterialApp( @@ -729,7 +762,7 @@ class _NotesAppState extends State title: 'Mis Notas', debugShowCheckedModeBanner: false, scaffoldMessengerKey: _scaffoldMessengerKey, - theme: AppTheme.theme, + theme: _theme, home: Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.f5): const PerformSyncIntent(), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 4a6abb8..93af161 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -9,11 +9,15 @@ class SettingsScreen extends StatefulWidget { required this.onDeleteAllData, required this.onBackToHome, required this.onForceSync, + required this.currentSeedColor, + required this.onThemeColorSelected, }); final Future Function() onDeleteAllData; final VoidCallback onBackToHome; final Future Function() onForceSync; + final Color currentSeedColor; + final Future Function(Color color) onThemeColorSelected; @override State createState() => _SettingsScreenState(); @@ -22,11 +26,22 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { bool _isBusy = false; bool _isSyncing = false; + bool _isThemeSaving = false; final TextEditingController _endpointController = TextEditingController(); final TextEditingController _encryptionKeyController = TextEditingController(); bool _endpointLoading = true; bool _encryptionKeyLoading = false; bool _encryptionKeyVisible = false; + late Color _selectedSeedColor; + + static const List _themeColorOptions = [ + Colors.amber, + Colors.blue, + Colors.teal, + Colors.green, + Colors.pink, + Colors.purple, + ]; Future _confirmAndDeleteAll() async { final bool? confirmed = await showDialog( @@ -100,12 +115,56 @@ class _SettingsScreenState extends State { } } + Future _selectThemeColor(Color color) async { + if (_isThemeSaving || _selectedSeedColor == color) { + return; + } + + final Color previousColor = _selectedSeedColor; + setState(() { + _selectedSeedColor = color; + _isThemeSaving = true; + }); + + try { + await widget.onThemeColorSelected(color); + } catch (error) { + if (!mounted) { + return; + } + + setState(() { + _selectedSeedColor = previousColor; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('No se pudo guardar el color: $error')), + ); + } finally { + if (mounted) { + setState(() { + _isThemeSaving = false; + }); + } + } + } + @override void initState() { super.initState(); + _selectedSeedColor = widget.currentSeedColor; _loadEndpoint(); } + @override + void didUpdateWidget(covariant SettingsScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.currentSeedColor != widget.currentSeedColor && + widget.currentSeedColor != _selectedSeedColor) { + _selectedSeedColor = widget.currentSeedColor; + } + } + Future _loadEndpoint() async { final String endpoint = await ApiConfig.getEndpoint(); if (!mounted) return; @@ -160,6 +219,58 @@ class _SettingsScreenState extends State { }); } + Widget _buildThemeColorButton(Color color) { + final bool isSelected = _selectedSeedColor.value == color.value; + final Color foregroundColor = + ThemeData.estimateBrightnessForColor(color) == Brightness.dark + ? Colors.white + : Colors.black; + + return Semantics( + button: true, + selected: isSelected, + label: 'Color ${color.value.toRadixString(16)}', + child: Tooltip( + message: isSelected ? 'Color actual' : 'Usar este color', + child: InkWell( + onTap: _isThemeSaving ? null : () => _selectThemeColor(color), + borderRadius: BorderRadius.circular(12), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + width: 44, + height: 44, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? Colors.white : Colors.white24, + width: isSelected ? 2.5 : 1.2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.25), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + if (isSelected) + Icon( + Icons.check, + size: 22, + color: foregroundColor, + ), + ], + ), + ), + ), + ), + ); + } + @override void dispose() { _endpointController.dispose(); @@ -296,11 +407,6 @@ class _SettingsScreenState extends State { child: Text('Forzar sincronizacion total:'), ), ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.amber, - foregroundColor: Colors.black, - textStyle: const TextStyle(fontWeight: FontWeight.w700), - ), onPressed: (_isBusy || _isSyncing) ? null : _forceSync, icon: _isSyncing ? const SizedBox( @@ -314,6 +420,17 @@ class _SettingsScreenState extends State { ], ), const SizedBox(height: 24), + const Text('Color del esquema'), + const SizedBox(height: 8), + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + for (final Color color in _themeColorOptions) + _buildThemeColorButton(color), + ], + ), + const SizedBox(height: 24), const Text('API endpoint (ej: http://localhost:3000/api)'), const SizedBox(height: 8), _buildResponsiveInputActionsRow( diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index e407235..b820eb4 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -1,17 +1,22 @@ import 'package:flutter/material.dart'; class AppTheme { - static ThemeData get theme { + static ThemeData theme({Color seedColor = Colors.amber}) { + final Brightness foregroundBrightness = + ThemeData.estimateBrightnessForColor(seedColor); + final Color foregroundColor = + foregroundBrightness == Brightness.dark ? Colors.white : Colors.black; + return ThemeData( useMaterial3: true, scaffoldBackgroundColor: const Color.fromRGBO(31, 32, 33, 1), colorScheme: ColorScheme.fromSeed( - seedColor: Colors.amber, + seedColor: seedColor, brightness: Brightness.dark, ), - floatingActionButtonTheme: const FloatingActionButtonThemeData( - backgroundColor: Colors.amber, - foregroundColor: Colors.black, + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: seedColor, + foregroundColor: foregroundColor, ), ); }