diff --git a/lib/app.dart b/lib/app.dart index 86c33c0..5b8672c 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -14,7 +14,7 @@ import 'package:notas/screens/biometric_gate_screen.dart'; import 'package:notas/screens/home_screen.dart'; import 'package:notas/screens/settings_screen.dart'; import 'package:notas/screens/vault_access_screen.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; import 'package:notas/theme/app_theme.dart'; import 'package:notas/widgets/sync_status.dart'; import 'package:path/path.dart' as p; @@ -44,6 +44,7 @@ class _NotesAppState extends State static const Duration _syncInterval = Duration(minutes: 5); static const Duration _windowSizeSaveDelay = Duration(milliseconds: 350); static const String _themeSeedColorKey = 'theme_seed_color_v1'; + static const String _themeModeKey = 'theme_mode_v1'; final LocalVaultService _vaultService = LocalVaultService.instance; final GlobalKey _scaffoldMessengerKey = @@ -69,7 +70,30 @@ class _NotesAppState extends State String? _syncErrorMessage; int _syncOperationId = 0; int _homeRefreshToken = 0; - Color _themeSeedColor = AppColors.defaultThemeSeedColor; + Color _themeSeedColor = Colors.amber; + ThemeMode _themeMode = ThemeMode.system; + + // Cached ThemeData for light and dark variants. + ThemeData? _lightTheme; + ThemeData? _darkTheme; + + Brightness _effectiveBrightness() { + switch (_themeMode) { + case ThemeMode.dark: + return Brightness.dark; + case ThemeMode.light: + return Brightness.light; + case ThemeMode.system: + return WidgetsBinding.instance.platformDispatcher.platformBrightness; + } + } + + AppPalette _activePalette() { + return AppPalette.fromBrightness( + _effectiveBrightness(), + seedColor: _themeSeedColor, + ); + } @override void initState() { @@ -80,6 +104,7 @@ class _NotesAppState extends State windowManager.setPreventClose(true); } _loadThemeSeedColor(); + _loadThemeMode(); _bootstrapVault(); } @@ -106,7 +131,7 @@ class _NotesAppState extends State setState(() { _themeSeedColor = Color(storedColorValue); - _themeData = AppTheme.theme(seedColor: _themeSeedColor); + _updateThemeData(); }); } @@ -120,15 +145,54 @@ class _NotesAppState extends State setState(() { _themeSeedColor = color; - _themeData = AppTheme.theme(seedColor: _themeSeedColor); + _updateThemeData(); }); } - // Cached ThemeData to avoid recomputing on every build. - ThemeData? _themeData; + Future _loadThemeMode() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final int? stored = prefs.getInt(_themeModeKey); + if (!mounted) return; - ThemeData get _theme => - _themeData ??= AppTheme.theme(seedColor: _themeSeedColor); + setState(() { + if (stored == null) { + _themeMode = ThemeMode.system; + } else if (stored == 1) { + _themeMode = ThemeMode.light; + } else if (stored == 2) { + _themeMode = ThemeMode.dark; + } else { + _themeMode = ThemeMode.system; + } + _updateThemeData(); + }); + } + + Future _setThemeMode(ThemeMode mode) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final int stored = mode == ThemeMode.system + ? 0 + : (mode == ThemeMode.light ? 1 : 2); + await prefs.setInt(_themeModeKey, stored); + + if (!mounted) return; + + setState(() { + _themeMode = mode; + }); + } + + void _updateThemeData() { + _lightTheme = AppTheme.theme( + seedColor: _themeSeedColor, + brightness: Brightness.light, + ); + _darkTheme = AppTheme.theme( + seedColor: _themeSeedColor, + brightness: Brightness.dark, + ); + // Updated light/dark themes regenerated + } @override void didChangeAppLifecycleState(AppLifecycleState state) { @@ -429,27 +493,30 @@ class _NotesAppState extends State final bool? retry = await showDialog( context: dialogCtx, - builder: (BuildContext context) => AlertDialog( - backgroundColor: AppColors.cardBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide(color: AppColors.border), - ), - title: const Text('No se pudo activar la biometría'), - content: const Text( - 'No se pudo activar la biometría. ¿Quieres intentarlo de nuevo o entrar sin huella?', - ), - actions: [ - TextButton( - onPressed: () => navigator.pop(false), - child: const Text('Entrar sin huella'), + builder: (BuildContext context) { + final AppPalette palette = _activePalette(); + return AlertDialog( + backgroundColor: palette.cardBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: palette.border), ), - FilledButton( - onPressed: () => navigator.pop(true), - child: const Text('Reintentar'), + title: const Text('No se pudo activar la biometría'), + content: const Text( + 'No se pudo activar la biometría. ¿Quieres intentarlo de nuevo o entrar sin huella?', ), - ], - ), + actions: [ + TextButton( + onPressed: () => navigator.pop(false), + child: const Text('Entrar sin huella'), + ), + FilledButton( + onPressed: () => navigator.pop(true), + child: const Text('Reintentar'), + ), + ], + ); + }, ); if (retry != true) { @@ -825,6 +892,7 @@ class _NotesAppState extends State } Widget _buildMainShell(NoteRepository repository) { + final AppPalette palette = _activePalette(); final Widget activeScreen = _currentSection == _AppSection.home ? HomeScreen( key: const ValueKey('home-screen'), @@ -845,6 +913,8 @@ class _NotesAppState extends State onForceSync: () => _performSync(forceFull: true), currentSeedColor: _themeSeedColor, onThemeColorSelected: _setThemeSeedColor, + currentThemeMode: _themeMode, + onThemeModeSelected: _setThemeMode, ); return Shortcuts( @@ -864,9 +934,7 @@ class _NotesAppState extends State autofocus: true, child: Scaffold( body: Container( - decoration: const BoxDecoration( - gradient: AppColors.backdropGradient, - ), + decoration: BoxDecoration(gradient: palette.backdropGradient), child: SafeArea( child: Column( children: [ @@ -1026,7 +1094,19 @@ class _NotesAppState extends State title: 'Mis Notas', debugShowCheckedModeBanner: false, scaffoldMessengerKey: _scaffoldMessengerKey, - theme: _theme, + theme: + _lightTheme ?? + AppTheme.theme( + seedColor: _themeSeedColor, + brightness: Brightness.light, + ), + darkTheme: + _darkTheme ?? + AppTheme.theme( + seedColor: _themeSeedColor, + brightness: Brightness.dark, + ), + themeMode: _themeMode, home: homeWidget, ); } diff --git a/lib/screens/biometric_choice_screen.dart b/lib/screens/biometric_choice_screen.dart index 7c679ce..47fc1c4 100644 --- a/lib/screens/biometric_choice_screen.dart +++ b/lib/screens/biometric_choice_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; class BiometricChoiceScreen extends StatelessWidget { const BiometricChoiceScreen({ @@ -15,9 +15,11 @@ class BiometricChoiceScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return Scaffold( body: Container( - decoration: const BoxDecoration(gradient: AppColors.backdropGradient), + decoration: BoxDecoration(gradient: palette.backdropGradient), child: SafeArea( child: Column( children: [ @@ -30,12 +32,12 @@ class BiometricChoiceScreen extends StatelessWidget { child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: AppColors.surface, + color: palette.cardBackground, borderRadius: BorderRadius.circular(24), - border: Border.all(color: AppColors.borderMuted), + border: Border.all(color: palette.border), boxShadow: [ BoxShadow( - color: AppColors.shadow, + color: palette.shadowSoft, blurRadius: 30, offset: const Offset(0, 18), ), @@ -55,7 +57,7 @@ class BiometricChoiceScreen extends StatelessWidget { 'Proteger con huella', textAlign: TextAlign.center, style: TextStyle( - color: AppColors.textPrimary, + color: Colors.white, fontSize: 28, fontWeight: FontWeight.w700, ), @@ -65,7 +67,7 @@ class BiometricChoiceScreen extends StatelessWidget { '¿Quieres que la app te pida huella o cara antes de entrar a tus notas?', textAlign: TextAlign.center, style: TextStyle( - color: AppColors.textSecondary, + color: palette.textSecondary, height: 1.4, ), ), @@ -94,10 +96,8 @@ class BiometricChoiceScreen extends StatelessWidget { padding: const EdgeInsets.symmetric( vertical: 14, ), - side: const BorderSide( - color: AppColors.textDisabled, - ), - foregroundColor: AppColors.textPrimary, + side: BorderSide(color: palette.border), + foregroundColor: palette.textPrimary, ), child: const Text('No, entrar sin huella'), ), diff --git a/lib/screens/biometric_gate_screen.dart b/lib/screens/biometric_gate_screen.dart index c26e490..ee9fde8 100644 --- a/lib/screens/biometric_gate_screen.dart +++ b/lib/screens/biometric_gate_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; class BiometricGateScreen extends StatefulWidget { const BiometricGateScreen({ @@ -39,9 +39,11 @@ class _BiometricGateScreenState extends State { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return Scaffold( body: Container( - decoration: const BoxDecoration(gradient: AppColors.backdropGradient), + decoration: BoxDecoration(gradient: palette.backdropGradient), child: SafeArea( child: Column( children: [ @@ -54,12 +56,12 @@ class _BiometricGateScreenState extends State { child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: AppColors.surface, + color: palette.cardBackground, borderRadius: BorderRadius.circular(24), - border: Border.all(color: AppColors.borderMuted), + border: Border.all(color: palette.border), boxShadow: [ BoxShadow( - color: AppColors.shadow, + color: palette.shadowSoft, blurRadius: 30, offset: const Offset(0, 18), ), @@ -79,7 +81,7 @@ class _BiometricGateScreenState extends State { 'Desbloqueo biométrico', textAlign: TextAlign.center, style: TextStyle( - color: AppColors.textPrimary, + color: Colors.white, fontSize: 28, fontWeight: FontWeight.w700, ), @@ -89,7 +91,7 @@ class _BiometricGateScreenState extends State { 'Pon tu huella o cara para entrar a tus notas.', textAlign: TextAlign.center, style: TextStyle( - color: AppColors.textSecondary, + color: palette.textSecondary, height: 1.4, ), ), diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 8540bc0..93ba2a0 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -15,7 +15,7 @@ import 'package:notas/widgets/note_card.dart'; import 'package:notas/widgets/search_app_bar.dart'; import 'package:notas/widgets/sync_status.dart'; import 'package:notas/widgets/sync_status_indicator.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({ @@ -489,9 +489,11 @@ class _HomeScreenState extends State { ), ); + final AppPalette palette = Theme.of(context).extension()!; + return Scaffold( body: Container( - decoration: const BoxDecoration(gradient: AppColors.backdropGradient), + decoration: BoxDecoration(gradient: palette.backdropGradient), child: SafeArea( child: Column( children: [ @@ -565,7 +567,7 @@ class _HomeScreenState extends State { child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: _closeMenu, - child: Container(color: AppColors.overlay), + child: Container(color: palette.overlay), ), ), ), @@ -578,7 +580,7 @@ class _HomeScreenState extends State { bottom: 0, width: 280, child: Material( - color: AppColors.cardBackground, + color: palette.cardBackground, elevation: 8, child: MenuDrawer( onMenuItemTapped: _handleMenuItemTapped, @@ -642,11 +644,13 @@ class _DraggableNote extends StatelessWidget { final VoidCallback onDraggableCanceled; final VoidCallback onDragStarted; - Widget _buildFeedback() { + Widget _buildFeedback(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return MouseRegion( cursor: SystemMouseCursors.grabbing, child: Material( - color: AppColors.transparent, + color: palette.transparent, elevation: 8, child: SizedBox( width: cellWidth, @@ -676,7 +680,9 @@ class _DraggableNote extends StatelessWidget { ); } - Widget _buildChildWhenDragging() { + Widget _buildChildWhenDragging(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return MouseRegion( cursor: SystemMouseCursors.grabbing, child: Opacity( @@ -684,10 +690,10 @@ class _DraggableNote extends StatelessWidget { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.cardBackground, + color: palette.cardBackground, borderRadius: BorderRadius.circular(12), border: Border.all( - color: borderColor ?? AppColors.textDisabled, + color: borderColor ?? palette.textSecondary, width: 1, ), ), @@ -697,8 +703,8 @@ class _DraggableNote extends StatelessWidget { children: [ Text( note.title, - style: const TextStyle( - color: AppColors.textPrimary, + style: TextStyle( + color: palette.textPrimary, fontSize: 16, fontWeight: FontWeight.bold, ), @@ -708,10 +714,7 @@ class _DraggableNote extends StatelessWidget { const SizedBox(height: 8), Text( note.body, - style: const TextStyle( - color: AppColors.textSecondary, - fontSize: 14, - ), + style: TextStyle(color: palette.textSecondary, fontSize: 14), maxLines: 20, overflow: TextOverflow.clip, ), @@ -724,10 +727,12 @@ class _DraggableNote extends StatelessWidget { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + final Widget content = Container( decoration: BoxDecoration( border: isDragTargetActive - ? Border.all(color: AppColors.dragTargetBorder, width: 2) + ? Border.all(color: palette.dragTargetBorder, width: 2) : null, borderRadius: BorderRadius.circular(12), ), @@ -747,8 +752,8 @@ class _DraggableNote extends StatelessWidget { onDragStarted: onDragStarted, onDragEnd: onDragEnd, onDraggableCanceled: (Velocity _v, Offset _o) => onDraggableCanceled(), - feedback: _buildFeedback(), - childWhenDragging: _buildChildWhenDragging(), + feedback: _buildFeedback(context), + childWhenDragging: _buildChildWhenDragging(context), child: content, ); } @@ -758,8 +763,8 @@ class _DraggableNote extends StatelessWidget { onDragStarted: onDragStarted, onDragEnd: onDragEnd, onDraggableCanceled: (Velocity _v, Offset _o) => onDraggableCanceled(), - feedback: _buildFeedback(), - childWhenDragging: _buildChildWhenDragging(), + feedback: _buildFeedback(context), + childWhenDragging: _buildChildWhenDragging(context), child: content, ); } @@ -778,15 +783,13 @@ class _EmptyState extends StatelessWidget { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon( - Icons.note_add_outlined, - color: AppColors.textMuted, - size: 48, - ), + Icon(Icons.note_add_outlined, color: palette.textSecondary, size: 48), const SizedBox(height: 12), Text( searchQuery != null && searchQuery!.isNotEmpty @@ -796,8 +799,8 @@ class _EmptyState extends StatelessWidget { : categoryName != null ? 'No hay notas en esta categoría' : 'Aún no hay notas', - style: const TextStyle( - color: AppColors.textPrimary, + style: TextStyle( + color: palette.textPrimary, fontSize: 18, fontWeight: FontWeight.w600, ), @@ -812,7 +815,7 @@ class _EmptyState extends StatelessWidget { ? 'Pulsa el botón + para crear una nota en “$categoryName”.' : 'Pulsa el botón + para crear la primera.', textAlign: TextAlign.center, - style: const TextStyle(color: AppColors.textSecondary), + style: TextStyle(color: palette.textSecondary), ), ], ), @@ -934,26 +937,31 @@ class _CategoryDialogState extends State<_CategoryDialog> { Future _deleteCategory() async { final bool? confirm = await showDialog( context: context, - builder: (BuildContext context) => AlertDialog( - backgroundColor: AppColors.cardBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide(color: AppColors.border), - ), - title: const Text('Borrar categoría'), - content: const Text('¿Seguro que quieres borrar esta categoría?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancelar'), + builder: (BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return AlertDialog( + backgroundColor: palette.cardBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: palette.border), ), - TextButton( - style: TextButton.styleFrom(foregroundColor: AppColors.destructive), - onPressed: () => Navigator.pop(context, true), - child: const Text('Borrar'), - ), - ], - ), + title: const Text('Borrar categoría'), + content: const Text('¿Seguro que quieres borrar esta categoría?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancelar'), + ), + TextButton( + style: TextButton.styleFrom( + foregroundColor: palette.destructiveAccent, + ), + onPressed: () => Navigator.pop(context, true), + child: const Text('Borrar'), + ), + ], + ); + }, ); if (confirm != true) { @@ -982,11 +990,13 @@ class _CategoryDialogState extends State<_CategoryDialog> { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return AlertDialog( - backgroundColor: AppColors.cardBackground, + backgroundColor: palette.cardBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: BorderSide(color: AppColors.border), + side: BorderSide(color: palette.border), ), title: Text( widget.category == null ? 'Crear categoría' : 'Editar categoría', @@ -1018,9 +1028,9 @@ class _CategoryDialogState extends State<_CategoryDialog> { const SizedBox(height: 16), Container( decoration: BoxDecoration( - color: AppColors.fill, + color: palette.fill, borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.border), + border: Border.all(color: palette.border), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -1049,7 +1059,7 @@ class _CategoryDialogState extends State<_CategoryDialog> { ), ], ), - const Divider(height: 1, color: AppColors.border), + Divider(height: 1, color: palette.border), Padding( padding: const EdgeInsets.all(12), child: AnimatedSwitcher( @@ -1075,11 +1085,11 @@ class _CategoryDialogState extends State<_CategoryDialog> { borderRadius: BorderRadius.circular(10), border: isSelected ? Border.all( - color: AppColors.textPrimary, + color: palette.textPrimary, width: 2, ) : Border.all( - color: AppColors.border, + color: palette.border, width: 1, ), ), @@ -1105,21 +1115,21 @@ class _CategoryDialogState extends State<_CategoryDialog> { height: 42, decoration: BoxDecoration( color: isSelected - ? AppColors.hover - : AppColors.transparent, + ? palette.hover + : palette.transparent, borderRadius: BorderRadius.circular(10), border: Border.all( color: isSelected - ? AppColors.textPrimary - : AppColors.border, + ? palette.textPrimary + : palette.border, width: 1, ), ), child: Icon( icon, color: isSelected - ? AppColors.textPrimary - : AppColors.textSecondary, + ? palette.textPrimary + : palette.textSecondary, ), ), ); @@ -1137,9 +1147,9 @@ class _CategoryDialogState extends State<_CategoryDialog> { if (widget.category != null) TextButton( onPressed: _deleteCategory, - child: const Text( + child: Text( 'Borrar', - style: TextStyle(color: AppColors.destructive), + style: TextStyle(color: palette.destructiveAccent), ), ), TextButton( @@ -1176,10 +1186,12 @@ class _PickerTabButton extends StatelessWidget { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return Material( borderRadius: borderRadius, clipBehavior: Clip.antiAlias, - color: selected ? AppColors.hover : AppColors.transparent, + color: selected ? palette.hover : palette.transparent, child: InkWell( borderRadius: borderRadius, onTap: onTap, @@ -1189,7 +1201,7 @@ class _PickerTabButton extends StatelessWidget { child: Text( label, style: TextStyle( - color: selected ? AppColors.textPrimary : AppColors.textMuted, + color: selected ? palette.textPrimary : palette.textSecondary, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, ), ), diff --git a/lib/screens/note_editor_screen.dart b/lib/screens/note_editor_screen.dart index 48572dd..44c77ea 100644 --- a/lib/screens/note_editor_screen.dart +++ b/lib/screens/note_editor_screen.dart @@ -7,7 +7,7 @@ import 'package:intl/intl.dart'; import 'package:notas/models/category.dart'; import 'package:notas/models/note.dart'; import 'package:notas/platform/app_platform.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; import 'package:notas/widgets/category_style.dart'; // NoteEditorScreen: unified UI for creating and editing notes. @@ -42,7 +42,7 @@ class NoteEditorScreen extends StatefulWidget { return showGeneralDialog( context: context, barrierDismissible: false, - barrierColor: AppColors.transparent, + barrierColor: Colors.transparent, transitionDuration: const Duration(milliseconds: 200), pageBuilder: (context, animation, secondaryAnimation) { return NoteEditorScreen( @@ -120,6 +120,11 @@ class _NoteEditorScreenState extends State { bool get _isMobileLayout => isAndroid || isIOS; + AppPalette _paletteOf(BuildContext context) { + return Theme.of(context).extension() ?? + AppPalette.fromBrightness(Theme.of(context).brightness); + } + @override void initState() { super.initState(); @@ -200,36 +205,37 @@ class _NoteEditorScreenState extends State { required ValueChanged onConfirmed, }) { final bool isDeletedNote = _currentNote.isDeleted; + final AppPalette palette = _paletteOf(context); return AlertDialog( - backgroundColor: AppColors.cardBackground, + backgroundColor: palette.cardBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: BorderSide(color: AppColors.border), + side: BorderSide(color: palette.border), ), title: Text( isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar nota', - style: const TextStyle(color: AppColors.textPrimary), + style: TextStyle(color: palette.textPrimary), ), content: Text( isDeletedNote ? 'Esta nota ya está borrada. Si la eliminas ahora, se borrará permanentemente.' : '¿Estás seguro de que deseas eliminar esta nota?', - style: const TextStyle(color: AppColors.textSecondary), + style: TextStyle(color: palette.textSecondary), ), actions: [ TextButton( onPressed: () => onConfirmed(false), - child: const Text( + child: Text( 'Cancelar', - style: TextStyle(color: AppColors.textSecondary), + style: TextStyle(color: palette.textSecondary), ), ), TextButton( onPressed: () => onConfirmed(true), child: Text( isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar', - style: const TextStyle(color: AppColors.destructive), + style: TextStyle(color: palette.destructiveAccent), ), ), ], @@ -241,7 +247,7 @@ class _NoteEditorScreenState extends State { final bool? confirmed = await showDialog( context: context, barrierDismissible: false, - barrierColor: AppColors.transparent, + barrierColor: Colors.transparent, builder: (BuildContext dialogContext) { return _buildDeleteConfirmationDialog( onConfirmed: (bool confirmed) => @@ -258,7 +264,7 @@ class _NoteEditorScreenState extends State { final bool? confirmed = await showDialog( context: context, barrierDismissible: false, - barrierColor: AppColors.transparent, + barrierColor: Colors.transparent, builder: (BuildContext dialogContext) { return _buildDeleteConfirmationDialog( onConfirmed: (bool confirmed) => @@ -287,14 +293,11 @@ class _NoteEditorScreenState extends State { }; return Material( - color: AppColors.transparent, + color: Colors.transparent, child: Stack( children: [ const Positioned.fill( - child: ModalBarrier( - dismissible: false, - color: AppColors.overlay, - ), + child: ModalBarrier(dismissible: false, color: Colors.black54), ), Center( child: ConstrainedBox( @@ -323,25 +326,30 @@ class _NoteEditorScreenState extends State { } Color _categoryBackgroundColor(Category? category) { + final AppPalette palette = _paletteOf(context); + if (category?.colorValue == null) { - return AppColors.borderMuted; + return palette.borderMuted; } return Color(category!.colorValue!); } Color _categoryForegroundColor(Category? category) { + final AppPalette palette = _paletteOf(context); + if (category == null || category.colorValue == null) { - return AppColors.textPrimary; + return palette.textPrimary; } final Color background = Color(category.colorValue!); return background.computeLuminance() > 0.55 - ? AppColors.textOnSurfaceDark - : AppColors.textPrimary; + ? palette.textOnSurfaceDark + : palette.textPrimary; } Widget _buildCategorySelectorBox({Category? category}) { + final AppPalette palette = _paletteOf(context); final String label = category?.name ?? 'Sin categoría'; final IconData icon = CategoryStyle.iconForCodePoint( category?.iconCodePoint, @@ -358,7 +366,7 @@ class _NoteEditorScreenState extends State { border: Border.all( color: category?.colorValue != null ? backgroundColor.withValues(alpha: 0.85) - : AppColors.textDisabled, + : palette.textDisabled, ), ), child: Row( @@ -409,12 +417,13 @@ class _NoteEditorScreenState extends State { _categoryMenuEntry = OverlayEntry( builder: (BuildContext overlayContext) { + final AppPalette palette = _paletteOf(overlayContext); final Size screenSize = MediaQuery.of(overlayContext).size; final double menuWidth = math.min(screenSize.width - 32, 320); final double menuHeight = math.min(screenSize.height - 32, 360); return Material( - color: AppColors.transparent, + color: Colors.transparent, child: Stack( children: [ Positioned.fill( @@ -432,7 +441,7 @@ class _NoteEditorScreenState extends State { ), child: Material( elevation: 10, - color: AppColors.surfaceElevated, + color: palette.surfaceElevated, borderRadius: BorderRadius.circular(12), clipBehavior: Clip.antiAlias, child: ListView( @@ -482,6 +491,7 @@ class _NoteEditorScreenState extends State { required bool isSelected, required VoidCallback onTap, }) { + final AppPalette palette = _paletteOf(context); final Color backgroundColor = _categoryBackgroundColor(category); final Color foregroundColor = _categoryForegroundColor(category); final IconData icon = CategoryStyle.iconForCodePoint( @@ -492,7 +502,7 @@ class _NoteEditorScreenState extends State { onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - color: isSelected ? AppColors.hover : null, + color: isSelected ? palette.hover : null, child: Row( children: [ Container( @@ -504,7 +514,7 @@ class _NoteEditorScreenState extends State { border: Border.all( color: category?.colorValue != null ? backgroundColor.withValues(alpha: 0.85) - : AppColors.textDisabled, + : palette.textDisabled, ), ), child: Icon(icon, size: 16, color: foregroundColor), @@ -553,6 +563,7 @@ class _NoteEditorScreenState extends State { } Widget _buildEditorContent({required bool isMobile}) { + final AppPalette palette = _paletteOf(context); final double titleSpacing = isMobile ? 16.0 : 8.0; return Column( @@ -560,15 +571,13 @@ class _NoteEditorScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: AppColors.border, width: 1), - ), + border: Border(bottom: BorderSide(color: palette.border, width: 1)), ), child: Row( children: [ IconButton( onPressed: _closeWithoutSaving, - icon: const Icon(Icons.close, color: AppColors.textSecondary), + icon: Icon(Icons.close, color: palette.textSecondary), tooltip: 'Cerrar sin guardar', ), const SizedBox(width: 8), @@ -579,23 +588,17 @@ class _NoteEditorScreenState extends State { children: [ Text( 'Posicion: ${_currentNote.position}', - style: const TextStyle( - color: AppColors.textMuted, - fontSize: 12, - ), + style: TextStyle(color: palette.textMuted, fontSize: 12), ), Text( 'Creado: ${_formatDate(_currentNote.createdAt)}', - style: const TextStyle( - color: AppColors.textMuted, - fontSize: 12, - ), + style: TextStyle(color: palette.textMuted, fontSize: 12), ), if (_currentNote.updatedAt != _currentNote.createdAt) Text( 'Modificado: ${_formatDate(_currentNote.updatedAt)}', - style: const TextStyle( - color: AppColors.textMuted, + style: TextStyle( + color: palette.textMuted, fontSize: 12, ), ), @@ -629,14 +632,14 @@ class _NoteEditorScreenState extends State { children: [ TextField( controller: _titleController, - style: const TextStyle( - color: AppColors.textPrimary, + style: TextStyle( + color: palette.textPrimary, fontSize: 28, fontWeight: FontWeight.bold, ), - decoration: const InputDecoration( + decoration: InputDecoration( hintText: 'Título', - hintStyle: TextStyle(color: AppColors.textHint), + hintStyle: TextStyle(color: palette.textHint), border: InputBorder.none, contentPadding: EdgeInsets.zero, ), @@ -648,14 +651,14 @@ class _NoteEditorScreenState extends State { keyboardType: TextInputType.multiline, maxLines: null, expands: true, - style: const TextStyle( - color: AppColors.textPrimary, + style: TextStyle( + color: palette.textPrimary, fontSize: 16, height: 1.6, ), - decoration: const InputDecoration( + decoration: InputDecoration( hintText: 'Escribe tu nota...', - hintStyle: TextStyle(color: AppColors.textHint), + hintStyle: TextStyle(color: palette.textHint), border: InputBorder.none, contentPadding: EdgeInsets.zero, ), @@ -668,7 +671,7 @@ class _NoteEditorScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - border: Border(top: BorderSide(color: AppColors.border, width: 1)), + border: Border(top: BorderSide(color: palette.border, width: 1)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -676,9 +679,9 @@ class _NoteEditorScreenState extends State { if (!_isNewNote) IconButton( onPressed: _deleteNote, - icon: const Icon( + icon: Icon( Icons.delete_outline, - color: AppColors.destructive, + color: palette.destructiveAccent, ), tooltip: 'Eliminar nota', ) @@ -694,12 +697,14 @@ class _NoteEditorScreenState extends State { @override Widget build(BuildContext context) { + final AppPalette palette = _paletteOf(context); + if (_isMobileLayout) { return Material( - color: AppColors.transparent, + color: palette.transparent, child: SafeArea( child: Container( - color: AppColors.cardBackground, + color: palette.cardBackground, child: _buildEditorContent(isMobile: true), ), ), @@ -714,10 +719,7 @@ class _NoteEditorScreenState extends State { return Stack( children: [ Positioned.fill( - child: ModalBarrier( - dismissible: false, - color: AppColors.shadowDim, - ), + child: ModalBarrier(dismissible: false, color: palette.shadowDim), ), Positioned.fill( child: Center( @@ -725,10 +727,10 @@ class _NoteEditorScreenState extends State { width: maxWidth, height: maxHeight, child: Material( - color: AppColors.cardBackground, + color: palette.cardBackground, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - side: BorderSide(color: AppColors.textDisabled, width: 1), + side: BorderSide(color: palette.textDisabled, width: 1), ), clipBehavior: Clip.antiAlias, child: _buildEditorContent(isMobile: false), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e920990..5b1a515 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:notas/data/local_vault_service.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; import 'package:notas/widgets/search_app_bar.dart'; import 'package:notas/data/api_client.dart'; @@ -12,6 +12,8 @@ class SettingsScreen extends StatefulWidget { required this.onForceSync, required this.currentSeedColor, required this.onThemeColorSelected, + required this.currentThemeMode, + required this.onThemeModeSelected, }); final Future Function() onDeleteAllData; @@ -19,6 +21,8 @@ class SettingsScreen extends StatefulWidget { final Future Function() onForceSync; final Color currentSeedColor; final Future Function(Color color) onThemeColorSelected; + final ThemeMode currentThemeMode; + final Future Function(ThemeMode mode) onThemeModeSelected; @override State createState() => _SettingsScreenState(); @@ -36,36 +40,40 @@ class _SettingsScreenState extends State { bool _encryptionKeyLoading = false; bool _encryptionKeyVisible = false; late Color _selectedSeedColor; + late ThemeMode _selectedThemeMode; - static const List _themeColorOptions = AppColors.themeSeedColors; + static const List _themeColorOptions = AppPalette.themeSeedColors; Future _confirmAndDeleteAll() async { final bool? confirmed = await showDialog( context: context, - builder: (context) => AlertDialog( - backgroundColor: AppColors.cardBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide(color: AppColors.border), - ), - title: const Text('Borrar todos los datos'), - content: const Text( - '¿Estás seguro? Esta acción eliminará la base de datos local y la clave de cifrado. Asegúrate de tener una copia de seguridad si es necesario o cuenta sincronizada.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancelar'), + builder: (context) { + final AppPalette palette = Theme.of(context).extension()!; + return AlertDialog( + backgroundColor: palette.cardBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: palette.border), ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text( - 'Borrar', - style: TextStyle(color: AppColors.destructive), + title: const Text('Borrar todos los datos'), + content: const Text( + '¿Estás seguro? Esta acción eliminará la base de datos local y la clave de cifrado. Asegúrate de tener una copia de seguridad si es necesario o cuenta sincronizada.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancelar'), ), - ), - ], - ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + 'Borrar', + style: TextStyle(color: palette.destructiveAccent), + ), + ), + ], + ); + }, ); if (confirmed != true) return; @@ -100,30 +108,33 @@ class _SettingsScreenState extends State { Future _confirmAndDeleteServerData() async { final bool? confirmed = await showDialog( context: context, - builder: (context) => AlertDialog( - backgroundColor: AppColors.cardBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide(color: AppColors.border), - ), - title: const Text('Borrar toda la info del servidor'), - content: const Text( - '¿Estás seguro? Esta acción eliminará toda la información almacenada en el servidor y no se puede deshacer.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancelar'), + builder: (context) { + final AppPalette palette = Theme.of(context).extension()!; + return AlertDialog( + backgroundColor: palette.cardBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: palette.border), ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text( - 'Borrar', - style: TextStyle(color: AppColors.destructive), + title: const Text('Borrar toda la info del servidor'), + content: const Text( + '¿Estás seguro? Esta acción eliminará toda la información almacenada en el servidor y no se puede deshacer.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancelar'), ), - ), - ], - ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + 'Borrar', + style: TextStyle(color: palette.destructiveAccent), + ), + ), + ], + ); + }, ); if (confirmed != true) return; @@ -234,6 +245,7 @@ class _SettingsScreenState extends State { void initState() { super.initState(); _selectedSeedColor = widget.currentSeedColor; + _selectedThemeMode = widget.currentThemeMode; _loadEndpoint(); } @@ -244,6 +256,26 @@ class _SettingsScreenState extends State { widget.currentSeedColor != _selectedSeedColor) { _selectedSeedColor = widget.currentSeedColor; } + if (oldWidget.currentThemeMode != widget.currentThemeMode) { + _selectedThemeMode = widget.currentThemeMode; + } + } + + Future _selectThemeMode(ThemeMode mode) async { + if (_selectedThemeMode == mode) return; + setState(() { + _selectedThemeMode = mode; + }); + try { + await widget.onThemeModeSelected(mode); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('No se pudo guardar la preferencia de tema: $e'), + ), + ); + } } Future _loadEndpoint() async { @@ -307,10 +339,12 @@ class _SettingsScreenState extends State { required bool isLoading, required IconData icon, }) { + final AppPalette palette = Theme.of(context).extension()!; + return ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: AppColors.destructiveAccent, - foregroundColor: AppColors.textPrimary, + backgroundColor: palette.destructiveAccent, + foregroundColor: palette.textPrimary, textStyle: const TextStyle(fontWeight: FontWeight.w600), ), onPressed: onPressed, @@ -326,11 +360,12 @@ class _SettingsScreenState extends State { } Widget _buildThemeColorButton(Color color) { + final AppPalette palette = Theme.of(context).extension()!; final bool isSelected = _selectedSeedColor.value == color.value; final Color foregroundColor = ThemeData.estimateBrightnessForColor(color) == Brightness.dark - ? AppColors.textPrimary - : AppColors.textOnAccent; + ? palette.textPrimary + : palette.textOnAccent; return Semantics( button: true, @@ -349,14 +384,12 @@ class _SettingsScreenState extends State { color: color, borderRadius: BorderRadius.circular(12), border: Border.all( - color: isSelected - ? AppColors.textPrimary - : AppColors.textDisabled, + color: isSelected ? palette.textPrimary : palette.textSecondary, width: isSelected ? 2.5 : 1.2, ), boxShadow: [ BoxShadow( - color: AppColors.shadowSoft, + color: palette.shadowSoft, blurRadius: 8, offset: const Offset(0, 3), ), @@ -458,9 +491,11 @@ class _SettingsScreenState extends State { } Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return Scaffold( body: Container( - decoration: const BoxDecoration(gradient: AppColors.backdropGradient), + decoration: BoxDecoration(gradient: palette.backdropGradient), child: SafeArea( child: Column( children: [ @@ -472,199 +507,233 @@ class _SettingsScreenState extends State { titleText: 'Configuración', ), Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Expanded(child: Text('Borrar datos locales:')), - _buildDestructiveButton( - label: 'Borrar', - onPressed: (_isBusy || _isServerDeleting) - ? null - : _confirmAndDeleteAll, - isLoading: _isBusy, - icon: Icons.delete_forever, - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - const Expanded( - child: Text('Borrar info del servidor:'), - ), - _buildDestructiveButton( - label: 'Borrar', - onPressed: - (_isBusy || _isSyncing || _isServerDeleting) - ? null - : _confirmAndDeleteServerData, - isLoading: _isServerDeleting, - icon: Icons.cloud_off, - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - const Expanded( - child: Text('Forzar sincronizacion total:'), - ), - ElevatedButton.icon( - onPressed: - (_isBusy || _isSyncing || _isServerDeleting) - ? null - : _forceSync, - icon: _isSyncing - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Icon(Icons.sync), - label: const Text('Sincronizar'), - ), - ], - ), - 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: https://notas-api.lpncnd.es/api)', - ), - const SizedBox(height: 8), - _buildResponsiveInputActionsRow( - input: _endpointLoading - ? const SizedBox( - height: 48, - child: Center( - child: CircularProgressIndicator(), - ), - ) - : TextField( - controller: _endpointController, - style: const TextStyle( - color: AppColors.textPrimary, - ), - keyboardType: TextInputType.url, - decoration: InputDecoration( - labelText: 'API endpoint', - labelStyle: const TextStyle( - color: AppColors.textSecondary, - ), - filled: true, - fillColor: AppColors.fill, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.accent, - width: 1.2, - ), - ), - ), - ), - actions: [ - ElevatedButton( - onPressed: _endpointLoading ? null : _saveEndpoint, - child: const Text('Guardar'), - ), - OutlinedButton( - onPressed: _endpointLoading ? null : _resetEndpoint, - child: const Text('Restaurar'), - ), - ], - ), - const SizedBox(height: 24), - const Text('Clave de cifrado local'), - const SizedBox(height: 8), - _buildResponsiveInputActionsRow( - input: TextField( - controller: _encryptionKeyController, - readOnly: true, - obscureText: !_encryptionKeyVisible, - enableSuggestions: false, - autocorrect: false, - style: const TextStyle(color: AppColors.textPrimary), - decoration: InputDecoration( - labelText: _encryptionKeyVisible - ? 'Clave de cifrado' - : 'Oculta hasta pulsar mostrar', - labelStyle: const TextStyle( - color: AppColors.textSecondary, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Expanded( + child: Text('Borrar datos locales:'), ), - filled: true, - fillColor: AppColors.fill, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, - ), + _buildDestructiveButton( + label: 'Borrar', + onPressed: (_isBusy || _isServerDeleting) + ? null + : _confirmAndDeleteAll, + isLoading: _isBusy, + icon: Icons.delete_forever, ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, - ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const Expanded( + child: Text('Borrar info del servidor:'), ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.accent, - width: 1.2, - ), + _buildDestructiveButton( + label: 'Borrar', + onPressed: + (_isBusy || _isSyncing || _isServerDeleting) + ? null + : _confirmAndDeleteServerData, + isLoading: _isServerDeleting, + icon: Icons.cloud_off, ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const Expanded( + child: Text('Forzar sincronizacion total:'), + ), + ElevatedButton.icon( + onPressed: + (_isBusy || _isSyncing || _isServerDeleting) + ? null + : _forceSync, + icon: _isSyncing + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.sync), + label: const Text('Sincronizar'), + ), + ], + ), + const SizedBox(height: 24), + const Text('Apariencia'), + const SizedBox(height: 8), + Column( + children: [ + RadioListTile( + title: const Text('Seguir modo del sistema'), + value: ThemeMode.system, + groupValue: _selectedThemeMode, + onChanged: (ThemeMode? v) => + _selectThemeMode(ThemeMode.system), + ), + RadioListTile( + title: const Text('Modo claro'), + value: ThemeMode.light, + groupValue: _selectedThemeMode, + onChanged: (ThemeMode? v) => + _selectThemeMode(ThemeMode.light), + ), + RadioListTile( + title: const Text('Modo oscuro'), + value: ThemeMode.dark, + groupValue: _selectedThemeMode, + onChanged: (ThemeMode? v) => + _selectThemeMode(ThemeMode.dark), + ), + ], + ), + const SizedBox(height: 16), + const Text('Color del esquema'), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(left: 4), + child: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + for (final Color color in _themeColorOptions) + _buildThemeColorButton(color), + ], ), ), - actions: [ - ElevatedButton( - onPressed: _encryptionKeyLoading - ? null - : _loadEncryptionKey, - child: _encryptionKeyLoading - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, + const SizedBox(height: 24), + const Text( + 'API endpoint (ej: https://notas-api.lpncnd.es/api)', + ), + const SizedBox(height: 8), + _buildResponsiveInputActionsRow( + input: _endpointLoading + ? const SizedBox( + height: 48, + child: Center( + child: CircularProgressIndicator(), + ), + ) + : TextField( + controller: _endpointController, + style: TextStyle(color: palette.textPrimary), + keyboardType: TextInputType.url, + decoration: InputDecoration( + labelText: 'API endpoint', + labelStyle: TextStyle( + color: palette.textSecondary, ), - ) - : const Text('Mostrar'), + filled: true, + fillColor: palette.fill, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide( + color: palette.border, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide( + color: palette.border, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide( + color: palette.accent, + width: 1.2, + ), + ), + ), + ), + actions: [ + ElevatedButton( + onPressed: _endpointLoading + ? null + : _saveEndpoint, + child: const Text('Guardar'), + ), + OutlinedButton( + onPressed: _endpointLoading + ? null + : _resetEndpoint, + child: const Text('Restaurar'), + ), + ], + ), + const SizedBox(height: 24), + const Text('Clave de cifrado local'), + const SizedBox(height: 8), + _buildResponsiveInputActionsRow( + input: TextField( + controller: _encryptionKeyController, + readOnly: true, + obscureText: !_encryptionKeyVisible, + enableSuggestions: false, + autocorrect: false, + style: TextStyle(color: palette.textPrimary), + decoration: InputDecoration( + labelText: _encryptionKeyVisible + ? 'Clave de cifrado' + : 'Oculta hasta pulsar mostrar', + labelStyle: TextStyle( + color: palette.textSecondary, + ), + filled: true, + fillColor: palette.fill, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: palette.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: palette.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide( + color: palette.accent, + width: 1.2, + ), + ), + ), ), - OutlinedButton( - onPressed: _encryptionKeyVisible - ? _hideEncryptionKey - : null, - child: const Text('Ocultar'), - ), - ], - ), - ], + actions: [ + ElevatedButton( + onPressed: _encryptionKeyLoading + ? null + : _loadEncryptionKey, + child: _encryptionKeyLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Text('Mostrar'), + ), + OutlinedButton( + onPressed: _encryptionKeyVisible + ? _hideEncryptionKey + : null, + child: const Text('Ocultar'), + ), + ], + ), + ], + ), ), ), ), diff --git a/lib/screens/vault_access_screen.dart b/lib/screens/vault_access_screen.dart index 16d8ae7..19103da 100644 --- a/lib/screens/vault_access_screen.dart +++ b/lib/screens/vault_access_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:notas/data/api_client.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; class VaultAccessScreen extends StatefulWidget { const VaultAccessScreen({ @@ -75,9 +75,11 @@ class _VaultAccessScreenState extends State { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return Scaffold( body: Container( - decoration: const BoxDecoration(gradient: AppColors.backdropGradient), + decoration: BoxDecoration(gradient: palette.backdropGradient), child: SafeArea( child: Column( children: [ @@ -90,12 +92,12 @@ class _VaultAccessScreenState extends State { child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: AppColors.surface, + color: palette.cardBackground, borderRadius: BorderRadius.circular(24), - border: Border.all(color: AppColors.borderMuted), + border: Border.all(color: palette.border), boxShadow: [ BoxShadow( - color: AppColors.shadow, + color: palette.shadowSoft, blurRadius: 30, offset: const Offset(0, 18), ), @@ -107,7 +109,7 @@ class _VaultAccessScreenState extends State { children: [ const Icon( Icons.lock_outline, - color: AppColors.accent, + color: Colors.white, size: 44, ), const SizedBox(height: 16), @@ -115,7 +117,7 @@ class _VaultAccessScreenState extends State { 'Mis Notas', textAlign: TextAlign.center, style: TextStyle( - color: AppColors.textPrimary, + color: Colors.white, fontSize: 30, fontWeight: FontWeight.w700, ), @@ -125,7 +127,7 @@ class _VaultAccessScreenState extends State { 'Tus notas se guardan cifradas en este dispositivo. La cuenta y la sincronización vendrán después.', textAlign: TextAlign.center, style: TextStyle( - color: AppColors.textSecondary, + color: palette.textSecondary, height: 1.4, ), ), @@ -141,32 +143,30 @@ class _VaultAccessScreenState extends State { controller: _endpointController, enabled: !widget.isBusy, keyboardType: TextInputType.url, - style: const TextStyle( - color: AppColors.textPrimary, - ), + style: const TextStyle(color: Colors.white), decoration: InputDecoration( labelText: 'API endpoint', - labelStyle: const TextStyle( - color: AppColors.textSecondary, + labelStyle: TextStyle( + color: palette.textSecondary, ), filled: true, - fillColor: AppColors.fill, + fillColor: palette.fill, border: OutlineInputBorder( borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, + borderSide: BorderSide( + color: palette.border, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, + borderSide: BorderSide( + color: palette.border, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.accent, + borderSide: BorderSide( + color: palette.accent, width: 1.2, ), ), @@ -177,32 +177,26 @@ class _VaultAccessScreenState extends State { controller: _emailController, enabled: !widget.isBusy, keyboardType: TextInputType.text, - style: const TextStyle( - color: AppColors.textPrimary, - ), + style: const TextStyle(color: Colors.white), decoration: InputDecoration( labelText: 'Usuario', - labelStyle: const TextStyle( - color: AppColors.textSecondary, + labelStyle: TextStyle( + color: palette.textSecondary, ), filled: true, - fillColor: AppColors.fill, + fillColor: palette.fill, border: OutlineInputBorder( borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, - ), + borderSide: BorderSide(color: palette.border), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, - ), + borderSide: BorderSide(color: palette.border), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.accent, + borderSide: BorderSide( + color: palette.accent, width: 1.2, ), ), @@ -213,32 +207,26 @@ class _VaultAccessScreenState extends State { controller: _passwordController, enabled: !widget.isBusy, obscureText: true, - style: const TextStyle( - color: AppColors.textPrimary, - ), + style: const TextStyle(color: Colors.white), decoration: InputDecoration( labelText: 'Contraseña', - labelStyle: const TextStyle( - color: AppColors.textSecondary, + labelStyle: TextStyle( + color: palette.textSecondary, ), filled: true, - fillColor: AppColors.fill, + fillColor: palette.fill, border: OutlineInputBorder( borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, - ), + borderSide: BorderSide(color: palette.border), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.border, - ), + borderSide: BorderSide(color: palette.border), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), - borderSide: const BorderSide( - color: AppColors.accent, + borderSide: BorderSide( + color: palette.accent, width: 1.2, ), ), @@ -271,10 +259,8 @@ class _VaultAccessScreenState extends State { padding: const EdgeInsets.symmetric( vertical: 14, ), - side: const BorderSide( - color: AppColors.textDisabled, - ), - foregroundColor: AppColors.textPrimary, + side: BorderSide(color: palette.border), + foregroundColor: palette.textPrimary, ), child: const Text('Iniciar sesión'), ), diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart deleted file mode 100644 index d87e938..0000000 --- a/lib/theme/app_colors.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppColors { - AppColors._(); - - static const Color defaultThemeSeedColor = Colors.amber; - - static const Color scaffoldBackground = Color.fromRGBO(31, 32, 33, 1); - static const Color backdropStart = Color(0xFF191A1D); - static const Color backdropMid = Color(0xFF222326); - static const Color backdropEnd = Color(0xFF101114); - static const LinearGradient backdropGradient = LinearGradient( - colors: [backdropStart, backdropMid, backdropEnd], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ); - - static const Color surface = Color(0xFF1D1E20); - static const Color surfaceElevated = Color(0xFF303134); - static const Color drawerBackground = Color.fromARGB(255, 30, 31, 35); - static const Color cardBackground = Color.fromRGBO(24, 25, 26, 1); - - static const Color overlay = Color.fromARGB(140, 0, 0, 0); - static const Color shadow = Color.fromRGBO(0, 0, 0, 0.35); - static const Color transparent = Colors.transparent; - - static const Color accent = Colors.amber; - static const Color destructive = Colors.red; - static const Color destructiveAccent = Colors.redAccent; - static const Color success = Colors.green; - static const Color textOnAccent = Colors.black; - static const Color textOnSurfaceDark = Colors.black87; - - static const Color textPrimary = Colors.white; - static const Color textSecondary = Colors.white70; - static const Color textMuted = Colors.white54; - static const Color textSubtle = Colors.white38; - static const Color textDisabled = Colors.white24; - static const Color textHint = Color.fromRGBO(255, 255, 255, 0.30); - - static const Color borderStrong = Color.fromRGBO(255, 255, 255, 0.20); - static const Color border = Color.fromRGBO(255, 255, 255, 0.12); - static const Color borderMuted = Color.fromRGBO(255, 255, 255, 0.08); - static const Color fill = Color.fromRGBO(255, 255, 255, 0.05); - static const Color hover = Color.fromRGBO(255, 255, 255, 0.10); - static const Color searchFocusBorder = Color.fromRGBO(255, 255, 255, 0.40); - static const Color shadowSoft = Color.fromRGBO(0, 0, 0, 0.25); - static const Color shadowDim = Color.fromARGB(54, 0, 0, 0); - - static const Color categoryFallback = Color(0xFFFFC107); - static const List categoryColors = [ - Colors.amber, - Colors.blue, - Colors.green, - Colors.purple, - Colors.red, - Colors.teal, - Colors.orange, - Colors.grey, - ]; - - static const List themeSeedColors = [ - Colors.amber, - Colors.blue, - Colors.teal, - Colors.green, - Colors.pink, - Colors.purple, - ]; - - static const Color dragTargetBorder = Color(0xFF42A5F5); - - static const Color syncPreparing = Color.fromARGB(255, 165, 165, 165); - static const Color syncEncrypting = Color.fromARGB(255, 109, 191, 255); - static const Color syncUploading = Color.fromARGB(255, 98, 190, 255); - static const Color syncWaiting = Color.fromARGB(255, 150, 150, 150); - static const Color syncDecrypting = Color.fromARGB(255, 154, 194, 112); -} diff --git a/lib/theme/app_palette.dart b/lib/theme/app_palette.dart new file mode 100644 index 0000000..61430ac --- /dev/null +++ b/lib/theme/app_palette.dart @@ -0,0 +1,321 @@ +import 'package:flutter/material.dart'; + +@immutable +class AppPalette extends ThemeExtension { + static const Color darkDefaultThemeSeedColor = Colors.amber; + static const Color lightDefaultThemeSeedColor = Colors.blue; + + static const List defaultCategoryColors = [ + Colors.amber, + Colors.blue, + Colors.green, + Colors.purple, + Colors.red, + Colors.teal, + Colors.orange, + Colors.grey, + ]; + + static const List themeSeedColors = [ + Colors.blue, + Colors.teal, + Colors.green, + Colors.pink, + Colors.purple, + Colors.orange, + ]; + + static const Gradient _darkBackdropGradient = LinearGradient( + colors: [Color(0xFF191A1D), Color(0xFF222326), Color(0xFF101114)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); + + static const Gradient _lightBackdropGradient = LinearGradient( + colors: [Color(0xFFFFFFFF), Color(0xFFF2F4F6), Color(0xFFECEFF1)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); + + static const Color _darkDrawerBackground = Color.fromARGB(255, 30, 31, 35); + static const Color _lightDrawerBackground = Color(0xFFF7F9FA); + static const Color _darkCardBackground = Color.fromRGBO(24, 25, 26, 1); + static const Color _lightCardBackground = Color(0xFFFFFFFF); + static const Color _darkSurfaceElevated = Color(0xFF303134); + static const Color _lightSurfaceElevated = Color(0xFFF0F2F4); + static const Color _darkBorder = Color.fromRGBO(255, 255, 255, 0.12); + static const Color _lightBorder = Color.fromRGBO(15, 23, 32, 0.06); + static const Color _darkBorderMuted = Color.fromRGBO(255, 255, 255, 0.08); + static const Color _lightBorderMuted = Color.fromRGBO(15, 23, 32, 0.03); + static const Color _darkTextPrimary = Colors.white; + static const Color _lightTextPrimary = Color(0xFF0F1720); + static const Color _darkTextSecondary = Colors.white70; + static const Color _lightTextSecondary = Color(0xFF374151); + static const Color _darkTextOnSurfaceDark = Colors.black87; + static const Color _lightTextOnSurfaceDark = Colors.black87; + static const Color _darkTextMuted = Colors.white54; + static const Color _lightTextMuted = Color(0xFF6B7280); + static const Color _darkTextDisabled = Colors.white24; + static const Color _lightTextDisabled = Color(0xFFBDBDBD); + static const Color _darkTextHint = Color.fromRGBO(255, 255, 255, 0.30); + static const Color _lightTextHint = Color.fromRGBO(15, 23, 32, 0.30); + static const Color _darkFill = Color.fromRGBO(255, 255, 255, 0.05); + static const Color _lightFill = Color.fromRGBO(15, 23, 32, 0.02); + static const Color _darkHover = Color.fromRGBO(255, 255, 255, 0.10); + static const Color _lightHover = Color.fromRGBO(15, 23, 32, 0.04); + static const Color _darkShadowSoft = Color.fromRGBO(0, 0, 0, 0.25); + static const Color _lightShadowSoft = Color.fromRGBO(0, 0, 0, 0.08); + static const Color _darkShadowDim = Color.fromARGB(54, 0, 0, 0); + static const Color _lightShadowDim = Color.fromARGB(40, 0, 0, 0); + static const Color _darkAccent = Colors.amber; + static const Color _lightAccent = Colors.blue; + static const Color _darkTextOnAccent = Colors.black; + static const Color _lightTextOnAccent = Colors.white; + static const Color _darkOverlay = Color.fromARGB(140, 0, 0, 0); + static const Color _lightOverlay = Color.fromARGB(120, 0, 0, 0); + static const Color _transparent = Colors.transparent; + static const Color _darkDragTargetBorder = Color(0xFF42A5F5); + static const Color _lightDragTargetBorder = Color(0xFF1565C0); + static const Color _darkSyncPreparing = Color.fromARGB(255, 165, 165, 165); + static const Color _lightSyncPreparing = Color.fromARGB(255, 120, 120, 120); + static const Color _darkSyncEncrypting = Color.fromARGB(255, 109, 191, 255); + static const Color _lightSyncEncrypting = Color.fromARGB(255, 2, 136, 209); + static const Color _darkSyncUploading = Color.fromARGB(255, 98, 190, 255); + static const Color _lightSyncUploading = Color.fromARGB(255, 2, 119, 189); + static const Color _darkSyncWaiting = Color.fromARGB(255, 150, 150, 150); + static const Color _lightSyncWaiting = Color.fromARGB(255, 150, 150, 150); + static const Color _darkSyncDecrypting = Color.fromARGB(255, 154, 194, 112); + static const Color _lightSyncDecrypting = Color.fromARGB(255, 76, 175, 80); + static const Color _darkSuccess = Colors.green; + static const Color _lightSuccess = Colors.green; + + const AppPalette({ + required this.backdropGradient, + required this.drawerBackground, + required this.cardBackground, + required this.categoryColors, + required this.surfaceElevated, + required this.border, + required this.borderMuted, + required this.accent, + required this.textPrimary, + required this.textSecondary, + required this.textOnSurfaceDark, + required this.textMuted, + required this.textDisabled, + required this.textHint, + required this.fill, + required this.hover, + required this.shadowSoft, + required this.shadowDim, + required this.destructiveAccent, + required this.textOnAccent, + required this.overlay, + required this.transparent, + required this.dragTargetBorder, + required this.syncPreparing, + required this.syncEncrypting, + required this.syncUploading, + required this.syncWaiting, + required this.syncDecrypting, + required this.success, + }); + + final Gradient backdropGradient; + final Color drawerBackground; + final Color cardBackground; + final List categoryColors; + final Color surfaceElevated; + final Color border; + final Color borderMuted; + final Color accent; + final Color textPrimary; + final Color textSecondary; + final Color textOnSurfaceDark; + final Color textMuted; + final Color textDisabled; + final Color textHint; + final Color fill; + final Color hover; + final Color shadowSoft; + final Color shadowDim; + final Color destructiveAccent; + final Color textOnAccent; + final Color overlay; + final Color transparent; + final Color dragTargetBorder; + final Color syncPreparing; + final Color syncEncrypting; + final Color syncUploading; + final Color syncWaiting; + final Color syncDecrypting; + final Color success; + + @override + AppPalette copyWith({ + Gradient? backdropGradient, + Color? drawerBackground, + Color? cardBackground, + List? categoryColors, + Color? surfaceElevated, + Color? border, + Color? borderMuted, + Color? accent, + Color? textPrimary, + Color? textSecondary, + Color? textOnSurfaceDark, + Color? textMuted, + Color? textDisabled, + Color? textHint, + Color? fill, + Color? hover, + Color? shadowSoft, + Color? shadowDim, + Color? destructiveAccent, + Color? textOnAccent, + Color? overlay, + Color? transparent, + Color? dragTargetBorder, + Color? syncPreparing, + Color? syncEncrypting, + Color? syncUploading, + Color? syncWaiting, + Color? syncDecrypting, + Color? success, + }) { + return AppPalette( + backdropGradient: backdropGradient ?? this.backdropGradient, + drawerBackground: drawerBackground ?? this.drawerBackground, + cardBackground: cardBackground ?? this.cardBackground, + categoryColors: categoryColors ?? this.categoryColors, + surfaceElevated: surfaceElevated ?? this.surfaceElevated, + border: border ?? this.border, + borderMuted: borderMuted ?? this.borderMuted, + accent: accent ?? this.accent, + textPrimary: textPrimary ?? this.textPrimary, + textSecondary: textSecondary ?? this.textSecondary, + textOnSurfaceDark: textOnSurfaceDark ?? this.textOnSurfaceDark, + textMuted: textMuted ?? this.textMuted, + textDisabled: textDisabled ?? this.textDisabled, + textHint: textHint ?? this.textHint, + fill: fill ?? this.fill, + hover: hover ?? this.hover, + shadowSoft: shadowSoft ?? this.shadowSoft, + shadowDim: shadowDim ?? this.shadowDim, + destructiveAccent: destructiveAccent ?? this.destructiveAccent, + textOnAccent: textOnAccent ?? this.textOnAccent, + overlay: overlay ?? this.overlay, + transparent: transparent ?? this.transparent, + dragTargetBorder: dragTargetBorder ?? this.dragTargetBorder, + syncPreparing: syncPreparing ?? this.syncPreparing, + syncEncrypting: syncEncrypting ?? this.syncEncrypting, + syncUploading: syncUploading ?? this.syncUploading, + syncWaiting: syncWaiting ?? this.syncWaiting, + syncDecrypting: syncDecrypting ?? this.syncDecrypting, + success: success ?? this.success, + ); + } + + @override + AppPalette lerp(ThemeExtension? other, double t) { + if (other is! AppPalette) return this; + return AppPalette( + backdropGradient: + LinearGradient.lerp( + backdropGradient as LinearGradient, + other.backdropGradient as LinearGradient, + t, + ) ?? + backdropGradient, + drawerBackground: + Color.lerp(drawerBackground, other.drawerBackground, t) ?? + drawerBackground, + cardBackground: + Color.lerp(cardBackground, other.cardBackground, t) ?? cardBackground, + categoryColors: t < 0.5 ? categoryColors : other.categoryColors, + surfaceElevated: + Color.lerp(surfaceElevated, other.surfaceElevated, t) ?? + surfaceElevated, + border: Color.lerp(border, other.border, t) ?? border, + borderMuted: Color.lerp(borderMuted, other.borderMuted, t) ?? borderMuted, + accent: Color.lerp(accent, other.accent, t) ?? accent, + textPrimary: Color.lerp(textPrimary, other.textPrimary, t) ?? textPrimary, + textSecondary: + Color.lerp(textSecondary, other.textSecondary, t) ?? textSecondary, + textOnSurfaceDark: + Color.lerp(textOnSurfaceDark, other.textOnSurfaceDark, t) ?? + textOnSurfaceDark, + textMuted: Color.lerp(textMuted, other.textMuted, t) ?? textMuted, + textDisabled: + Color.lerp(textDisabled, other.textDisabled, t) ?? textDisabled, + textHint: Color.lerp(textHint, other.textHint, t) ?? textHint, + fill: Color.lerp(fill, other.fill, t) ?? fill, + hover: Color.lerp(hover, other.hover, t) ?? hover, + shadowSoft: Color.lerp(shadowSoft, other.shadowSoft, t) ?? shadowSoft, + shadowDim: Color.lerp(shadowDim, other.shadowDim, t) ?? shadowDim, + destructiveAccent: + Color.lerp(destructiveAccent, other.destructiveAccent, t) ?? + destructiveAccent, + textOnAccent: + Color.lerp(textOnAccent, other.textOnAccent, t) ?? textOnAccent, + overlay: Color.lerp(overlay, other.overlay, t) ?? overlay, + transparent: Color.lerp(transparent, other.transparent, t) ?? transparent, + dragTargetBorder: + Color.lerp(dragTargetBorder, other.dragTargetBorder, t) ?? + dragTargetBorder, + syncPreparing: + Color.lerp(syncPreparing, other.syncPreparing, t) ?? syncPreparing, + syncEncrypting: + Color.lerp(syncEncrypting, other.syncEncrypting, t) ?? syncEncrypting, + syncUploading: + Color.lerp(syncUploading, other.syncUploading, t) ?? syncUploading, + syncWaiting: Color.lerp(syncWaiting, other.syncWaiting, t) ?? syncWaiting, + syncDecrypting: + Color.lerp(syncDecrypting, other.syncDecrypting, t) ?? syncDecrypting, + success: Color.lerp(success, other.success, t) ?? success, + ); + } + + static AppPalette fromBrightness(Brightness brightness, {Color? seedColor}) { + final bool isLight = brightness == Brightness.light; + return AppPalette( + backdropGradient: isLight + ? _lightBackdropGradient + : _darkBackdropGradient, + drawerBackground: isLight + ? _lightDrawerBackground + : _darkDrawerBackground, + cardBackground: isLight ? _lightCardBackground : _darkCardBackground, + categoryColors: defaultCategoryColors, + surfaceElevated: isLight ? _lightSurfaceElevated : _darkSurfaceElevated, + border: isLight ? _lightBorder : _darkBorder, + borderMuted: isLight ? _lightBorderMuted : _darkBorderMuted, + accent: seedColor ?? (isLight ? _lightAccent : _darkAccent), + textPrimary: isLight ? _lightTextPrimary : _darkTextPrimary, + textSecondary: isLight ? _lightTextSecondary : _darkTextSecondary, + textOnSurfaceDark: isLight + ? _lightTextOnSurfaceDark + : _darkTextOnSurfaceDark, + textMuted: isLight ? _lightTextMuted : _darkTextMuted, + textDisabled: isLight ? _lightTextDisabled : _darkTextDisabled, + textHint: isLight ? _lightTextHint : _darkTextHint, + fill: isLight ? _lightFill : _darkFill, + hover: isLight ? _lightHover : _darkHover, + shadowSoft: isLight ? _lightShadowSoft : _darkShadowSoft, + shadowDim: isLight ? _lightShadowDim : _darkShadowDim, + destructiveAccent: Colors.redAccent, + textOnAccent: isLight ? _lightTextOnAccent : _darkTextOnAccent, + overlay: isLight ? _lightOverlay : _darkOverlay, + transparent: _transparent, + dragTargetBorder: isLight + ? _lightDragTargetBorder + : _darkDragTargetBorder, + syncPreparing: isLight ? _lightSyncPreparing : _darkSyncPreparing, + syncEncrypting: isLight ? _lightSyncEncrypting : _darkSyncEncrypting, + syncUploading: isLight ? _lightSyncUploading : _darkSyncUploading, + syncWaiting: isLight ? _lightSyncWaiting : _darkSyncWaiting, + syncDecrypting: isLight ? _lightSyncDecrypting : _darkSyncDecrypting, + success: isLight ? _lightSuccess : _darkSuccess, + ); + } +} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 2488966..6542b7d 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -1,21 +1,33 @@ import 'package:flutter/material.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; class AppTheme { - static ThemeData theme({Color seedColor = AppColors.defaultThemeSeedColor}) { + static ThemeData theme({ + Color seedColor = Colors.amber, + Brightness brightness = Brightness.dark, + }) { final Brightness foregroundBrightness = ThemeData.estimateBrightnessForColor(seedColor); final Color foregroundColor = foregroundBrightness == Brightness.dark - ? AppColors.textPrimary - : AppColors.textOnAccent; + ? Colors.white + : Colors.black87; + + final ColorScheme scheme = ColorScheme.fromSeed( + seedColor: seedColor, + brightness: brightness, + ); + + final AppPalette palette = AppPalette.fromBrightness( + brightness, + seedColor: seedColor, + ); return ThemeData( useMaterial3: true, - scaffoldBackgroundColor: AppColors.scaffoldBackground, - colorScheme: ColorScheme.fromSeed( - seedColor: seedColor, - brightness: Brightness.dark, - ), + scaffoldBackgroundColor: scheme.background, + colorScheme: scheme, + extensions: >[palette], + brightness: brightness, floatingActionButtonTheme: FloatingActionButtonThemeData( backgroundColor: seedColor, foregroundColor: foregroundColor, diff --git a/lib/widgets/category_style.dart b/lib/widgets/category_style.dart index e19b2d2..7b661bb 100644 --- a/lib/widgets/category_style.dart +++ b/lib/widgets/category_style.dart @@ -1,10 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; class CategoryStyle { CategoryStyle._(); - static const List colors = AppColors.categoryColors; + static const List colors = AppPalette.defaultCategoryColors; + + static List colorsOf(BuildContext context) { + final AppPalette? palette = Theme.of(context).extension(); + if (palette != null) { + return palette.categoryColors; + } + + return AppPalette.defaultCategoryColors; + } static const List icons = [ Icons.label_outline_rounded, diff --git a/lib/widgets/menu_drawer.dart b/lib/widgets/menu_drawer.dart index 2cbde66..795d782 100644 --- a/lib/widgets/menu_drawer.dart +++ b/lib/widgets/menu_drawer.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:notas/models/category.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; import 'package:notas/widgets/category_style.dart'; class MenuDrawer extends StatelessWidget { @@ -21,10 +21,12 @@ class MenuDrawer extends StatelessWidget { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return Container( - decoration: const BoxDecoration( - color: AppColors.drawerBackground, - border: Border(right: BorderSide(color: AppColors.border, width: 0.5)), + decoration: BoxDecoration( + color: palette.drawerBackground, + border: Border(right: BorderSide(color: palette.border, width: 0.5)), ), child: Column( children: [ @@ -59,12 +61,10 @@ class MenuDrawer extends StatelessWidget { ? null : () => onEditCategory?.call(category), iconColor: Color( - category.colorValue ?? - AppColors.categoryFallback.value, + category.colorValue ?? palette.accent.value, ), textColor: Color( - category.colorValue ?? - AppColors.categoryFallback.value, + category.colorValue ?? palette.accent.value, ), trailing: IconButton( padding: const EdgeInsets.all(8), @@ -72,9 +72,9 @@ class MenuDrawer extends StatelessWidget { minWidth: 0, minHeight: 0, ), - icon: const Icon( + icon: Icon( Icons.more_vert, - color: AppColors.textSecondary, + color: palette.textSecondary, size: 20, ), onPressed: () => onEditCategory?.call(category), @@ -97,10 +97,10 @@ class MenuDrawer extends StatelessWidget { label: 'Mis notas borradas', selected: selectedItem == 'deleted_notes', onTap: () => onMenuItemTapped?.call('deleted_notes'), - iconColor: AppColors.destructiveAccent, - textColor: AppColors.destructiveAccent, + iconColor: palette.destructiveAccent, + textColor: palette.destructiveAccent, ), - const Divider(color: AppColors.border, height: 16), + Divider(color: palette.border, height: 16), _MenuItemTile( icon: Icons.settings, label: 'Configuración', @@ -143,13 +143,12 @@ class _MenuItemTileState extends State<_MenuItemTile> { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; final bool active = widget.selected || _hovering; - final Color backgroundColor = active - ? AppColors.hover - : AppColors.transparent; + final Color backgroundColor = active ? palette.hover : palette.transparent; final Color foregroundColor = active - ? AppColors.textPrimary - : AppColors.textSecondary; + ? palette.textPrimary + : palette.textSecondary; final Widget? trailing = _hovering ? widget.trailing : null; return MouseRegion( diff --git a/lib/widgets/note_card.dart b/lib/widgets/note_card.dart index dcf3269..6f22d90 100644 --- a/lib/widgets/note_card.dart +++ b/lib/widgets/note_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:notas/models/note.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; // Small presentational widget for a note inside the grid. // Keep this widget lightweight and layout-agnostic: it should not force @@ -31,6 +31,7 @@ class _NoteCardState extends State { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; final bool showGrabbing = widget.isDragging || _isPressed; return MouseRegion( @@ -63,10 +64,10 @@ class _NoteCardState extends State { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.cardBackground, + color: palette.cardBackground, borderRadius: BorderRadius.circular(12), border: Border.all( - color: widget.borderColor ?? AppColors.textDisabled, + color: widget.borderColor ?? palette.textDisabled, width: 1, ), ), @@ -90,8 +91,8 @@ class _NoteCardState extends State { final TextPainter textPainter = TextPainter( text: TextSpan( text: widget.note.body, - style: const TextStyle( - color: AppColors.textSecondary, + style: TextStyle( + color: palette.textSecondary, fontSize: 14, ), ), @@ -110,8 +111,8 @@ class _NoteCardState extends State { children: [ Text( widget.note.title, - style: const TextStyle( - color: AppColors.textPrimary, + style: TextStyle( + color: palette.textPrimary, fontSize: 16, fontWeight: FontWeight.bold, ), @@ -121,8 +122,8 @@ class _NoteCardState extends State { const SizedBox(height: 8), Text( widget.note.body, - style: const TextStyle( - color: AppColors.textSecondary, + style: TextStyle( + color: palette.textSecondary, fontSize: 14, ), maxLines: 20, @@ -130,10 +131,10 @@ class _NoteCardState extends State { ), if (isBodyTruncated) ...[ const SizedBox(height: 4), - const Text( + Text( '...', style: TextStyle( - color: AppColors.textMuted, + color: palette.textMuted, fontSize: 18, height: 1, ), diff --git a/lib/widgets/search_app_bar.dart b/lib/widgets/search_app_bar.dart index e5bd6d6..f1f19c2 100644 --- a/lib/widgets/search_app_bar.dart +++ b/lib/widgets/search_app_bar.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; class SearchAppBar extends StatefulWidget { const SearchAppBar({ @@ -52,10 +52,12 @@ class _SearchAppBarState extends State { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + return Container( decoration: BoxDecoration( - color: AppColors.transparent, - border: Border(bottom: BorderSide(color: AppColors.border, width: 0.5)), + color: palette.transparent, + border: Border(bottom: BorderSide(color: palette.border, width: 0.5)), ), padding: const EdgeInsets.only(left: 8, right: 20, top: 7, bottom: 7), child: Row( @@ -64,7 +66,7 @@ class _SearchAppBarState extends State { onPressed: widget.onLeadingPressed ?? widget.onMenuPressed, icon: Icon( widget.leadingIcon, - color: AppColors.textSecondary, + color: palette.textSecondary, size: 20, ), tooltip: widget.leadingTooltip, @@ -84,23 +86,21 @@ class _SearchAppBarState extends State { child: TextField( controller: _searchController, onChanged: widget.onSearchChanged, - style: const TextStyle( - color: AppColors.textPrimary, + style: TextStyle( + color: palette.textPrimary, fontSize: 13, ), - cursorColor: AppColors.textSecondary, + cursorColor: palette.textSecondary, decoration: InputDecoration( hintText: widget.searchHint, hintStyle: TextStyle( - color: AppColors.textSecondary.withValues( - alpha: 0.5, - ), + color: palette.textSecondary.withOpacity(0.6), ), suffixIcon: _searchController.text.isNotEmpty ? IconButton( - icon: const Icon( + icon: Icon( Icons.clear, - color: AppColors.textSecondary, + color: palette.textSecondary, size: 18, ), onPressed: () { @@ -112,37 +112,37 @@ class _SearchAppBarState extends State { minHeight: 36, ), ) - : const Padding( - padding: EdgeInsets.only(right: 8), + : Padding( + padding: const EdgeInsets.only(right: 8), child: Icon( Icons.search, - color: AppColors.textSecondary, + color: palette.textSecondary, size: 18, ), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( - color: AppColors.borderStrong, + color: palette.border, width: 0.5, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( - color: AppColors.borderStrong, + color: palette.border, width: 0.5, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( - color: AppColors.searchFocusBorder, - width: 0.5, + color: palette.accent, + width: 0.6, ), ), filled: true, - fillColor: AppColors.fill, + fillColor: palette.fill, contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, @@ -156,8 +156,8 @@ class _SearchAppBarState extends State { alignment: Alignment.centerLeft, child: Text( widget.titleText ?? '', - style: const TextStyle( - color: AppColors.textPrimary, + style: TextStyle( + color: palette.textPrimary, fontSize: 18, fontWeight: FontWeight.w600, ), diff --git a/lib/widgets/sync_status_indicator.dart b/lib/widgets/sync_status_indicator.dart index dd6e8ad..b042c92 100644 --- a/lib/widgets/sync_status_indicator.dart +++ b/lib/widgets/sync_status_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:notas/theme/app_colors.dart'; +import 'package:notas/theme/app_palette.dart'; import 'package:notas/widgets/sync_status.dart'; class SyncStatusIndicator extends StatelessWidget { @@ -87,16 +87,14 @@ class SyncStatusIndicator extends StatelessWidget { @override Widget build(BuildContext context) { + final AppPalette palette = Theme.of(context).extension()!; + switch (status) { case SyncStatus.idle: return Tooltip( message: _messageForStatus(), child: _buildIndicator( - const Icon( - Icons.cloud_outlined, - size: 16, - color: AppColors.textSubtle, - ), + Icon(Icons.cloud_outlined, size: 16, color: palette.textSecondary), ), ); @@ -106,7 +104,7 @@ class SyncStatusIndicator extends StatelessWidget { child: _buildIndicator( _buildStatusBadge( icon: Icons.sync, - color: AppColors.syncPreparing, + color: palette.syncPreparing, determinate: false, ), ), @@ -118,7 +116,7 @@ class SyncStatusIndicator extends StatelessWidget { child: _buildIndicator( _buildStatusBadge( icon: Icons.cloud_upload_outlined, - color: AppColors.syncEncrypting, + color: palette.syncEncrypting, determinate: true, ), ), @@ -130,7 +128,7 @@ class SyncStatusIndicator extends StatelessWidget { child: _buildIndicator( _buildStatusBadge( icon: Icons.cloud_upload, - color: AppColors.syncUploading, + color: palette.syncUploading, determinate: false, ), ), @@ -142,7 +140,7 @@ class SyncStatusIndicator extends StatelessWidget { child: _buildIndicator( _buildStatusBadge( icon: Icons.cloud_sync_outlined, - color: AppColors.syncWaiting, + color: palette.syncWaiting, determinate: false, ), ), @@ -154,7 +152,7 @@ class SyncStatusIndicator extends StatelessWidget { child: _buildIndicator( _buildStatusBadge( icon: Icons.cloud_download_outlined, - color: AppColors.syncDecrypting, + color: palette.syncDecrypting, determinate: true, ), ), @@ -166,7 +164,7 @@ class SyncStatusIndicator extends StatelessWidget { child: _buildIndicator( _buildStatusBadge( icon: Icons.sync, - color: AppColors.syncWaiting, + color: palette.syncWaiting, determinate: false, ), ), @@ -176,7 +174,7 @@ class SyncStatusIndicator extends StatelessWidget { return Tooltip( message: _messageForStatus(), child: _buildIndicator( - const Icon(Icons.check_circle, size: 16, color: AppColors.success), + Icon(Icons.check_circle, size: 16, color: palette.success), ), ); @@ -184,7 +182,7 @@ class SyncStatusIndicator extends StatelessWidget { return Tooltip( message: _messageForStatus(), child: _buildIndicator( - const Icon(Icons.error, size: 16, color: AppColors.destructive), + Icon(Icons.error, size: 16, color: palette.destructiveAccent), ), ); } diff --git a/test/widget_test.dart b/test/widget_test.dart index 0b46212..9c2aefc 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -8,6 +8,6 @@ void main() { await tester.pumpWidget(const NotesApp()); expect(find.byType(MaterialApp), findsOneWidget); - expect(find.text('Mis Notas'), findsWidgets); + expect(find.text('Preparando el vault local...'), findsWidgets); }); }