import 'dart:math' as math; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:notas/data/note_repository.dart'; import 'package:notas/models/note.dart'; import 'package:notas/screens/note_editor_screen.dart'; import 'package:notas/models/category.dart'; import 'package:notas/widgets/menu_drawer.dart'; 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'; class HomeScreen extends StatefulWidget { const HomeScreen({ super.key, required this.repository, required this.onOpenSettings, required this.onRequestSync, this.onVaultInvalid, this.syncStatus = SyncStatus.idle, this.syncProgress, this.syncDetailMessage, this.syncErrorMessage, this.refreshToken = 0, }); final NoteRepository repository; final VoidCallback onOpenSettings; final Future Function() onRequestSync; final Future Function()? onVaultInvalid; final SyncStatus syncStatus; final double? syncProgress; final String? syncDetailMessage; final String? syncErrorMessage; final int refreshToken; @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { List _notes = []; String _searchQuery = ''; bool _isLoading = true; bool _isDragging = false; bool _isMenuOpen = false; bool _showDeletedNotes = false; PointerDeviceKind _lastPointerKind = PointerDeviceKind.mouse; List _categories = []; String? _selectedCategoryId; void _openMenu() { if (_isMenuOpen) { return; } setState(() { _isMenuOpen = true; }); } void _closeMenu() { if (!_isMenuOpen) { return; } setState(() { _isMenuOpen = false; }); } bool _requiresLongPressToDrag(PointerDeviceKind kind) { return kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus || kind == PointerDeviceKind.invertedStylus; } @override void initState() { super.initState(); _loadNotes(); _loadCategories(); } Future _loadCategories() async { try { final List cats = await widget.repository.loadCategories(); if (!mounted) return; setState(() { _categories = cats; }); } catch (e) { debugPrint('Failed to load categories: $e'); } } Category? _currentCategory() { final String? selectedCategoryId = _selectedCategoryId; if (selectedCategoryId == null) { return null; } for (final Category category in _categories) { if (category.id == selectedCategoryId) { return category; } } return null; } @override void didUpdateWidget(covariant HomeScreen oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.refreshToken != widget.refreshToken) { _loadNotes(); } } Future _loadNotes() async { try { final List storedNotes = _showDeletedNotes ? await widget.repository.loadDeletedNotes() : await widget.repository.loadNotes(); if (!mounted) return; setState(() { _notes = storedNotes; _isLoading = false; }); } catch (e, st) { // Log the error so we can inspect the root cause before resetting the vault. debugPrint('Failed to load notes: $e\n$st'); // If loading notes fails (e.g., DB corrupt), notify the app to reset the vault. if (widget.onVaultInvalid != null) { await widget.onVaultInvalid!(); } } } Future _openNoteComposer() async { final dynamic result = await NoteEditorScreen.showDialog( context, categoryId: _showDeletedNotes ? null : _selectedCategoryId, ); if (result == null) { return; } if (result is Note) { await widget.repository.createNote(result); await _loadNotes(); // Trigger sync after creating a note. try { await widget.onRequestSync(); } catch (_) {} } } Future _deleteNote(Note note) async { await widget.repository.deleteNote(note); await _loadNotes(); // Trigger sync after deleting a note. try { await widget.onRequestSync(); } catch (_) {} } Future _reorderNote(int oldIndex, int newIndex) async { if (oldIndex == newIndex) { return; } final Note movedNote = _notes[oldIndex]; try { await widget.repository.moveNote(movedNote, newIndex); await _loadNotes(); } catch (e, st) { // Don't let DB errors cause the app to reset the vault automatically. debugPrint('Failed to move note: $e\n$st'); if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Error al reordenar la nota: $e'))); } } Future _openNoteEditor(Note note) async { final dynamic result = await NoteEditorScreen.showDialog( context, note: note, ); if (result == null) { return; } if (result == 'delete') { await _deleteNote(note); return; } if (result is Note) { if (_notes.any((Note item) => item == note)) { await widget.repository.updateNote(result); await _loadNotes(); // Trigger sync after editing a note. try { await widget.onRequestSync(); } catch (_) {} } } } List _getFilteredNotes() { Iterable notes = _notes; if (_selectedCategoryId != null) { notes = notes.where( (Note note) => note.categoryId == _selectedCategoryId, ); } if (_searchQuery.isEmpty) { return notes.toList(); } final String query = _searchQuery.toLowerCase(); return notes .where( (Note note) => note.title.toLowerCase().contains(query) || note.body.toLowerCase().contains(query), ) .toList(); } Future _handleMenuItemTapped(String item) async { _closeMenu(); if (item == 'create_category') { await _showCreateCategoryDialog(); return; } if (item.startsWith('category_')) { final String id = item.substring('category_'.length); setState(() { _selectedCategoryId = id; _showDeletedNotes = false; _searchQuery = ''; _isLoading = true; }); await _loadNotes(); return; } if (item == 'settings') { widget.onOpenSettings(); return; } if (item == 'deleted_notes') { setState(() { _showDeletedNotes = true; _selectedCategoryId = null; _searchQuery = ''; _isLoading = true; }); await _loadNotes(); return; } if (item == 'all_notes') { setState(() { _showDeletedNotes = false; _selectedCategoryId = null; _searchQuery = ''; _isLoading = true; }); await _loadNotes(); } } Future _showCreateCategoryDialog() async { final TextEditingController controller = TextEditingController(); Color? selectedColor; IconData? selectedIcon; final List palette = [ Colors.amber, Colors.blue, Colors.green, Colors.purple, Colors.red, Colors.teal, Colors.orange, Colors.grey, ]; final List icons = [ Icons.folder, Icons.work, Icons.star, Icons.home, Icons.school, Icons.book, Icons.music_note, Icons.lightbulb, ]; final bool? result = await showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( builder: (BuildContext ctx, StateSetter setState) { return AlertDialog( title: const Text('Crear categoría'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: controller, decoration: const InputDecoration( hintText: 'Nombre de la categoría', ), ), const SizedBox(height: 12), Align( alignment: Alignment.centerLeft, child: Text( 'Color', style: const TextStyle( fontSize: 14, color: Colors.white70, ), ), ), const SizedBox(height: 8), Wrap( spacing: 8, children: palette.map((Color color) { final bool isSelected = selectedColor == color; return GestureDetector( onTap: () => setState(() => selectedColor = color), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(6), border: isSelected ? Border.all(color: Colors.white, width: 2) : null, ), ), ); }).toList(), ), const SizedBox(height: 12), Align( alignment: Alignment.centerLeft, child: Text( 'Icono', style: const TextStyle( fontSize: 14, color: Colors.white70, ), ), ), const SizedBox(height: 8), Wrap( spacing: 8, children: icons.map((IconData icon) { final bool isSelected = selectedIcon == icon; return GestureDetector( onTap: () => setState(() => selectedIcon = icon), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: isSelected ? Colors.white10 : Colors.transparent, borderRadius: BorderRadius.circular(6), ), child: Icon( icon, color: isSelected ? Colors.white : Colors.white70, ), ), ); }).toList(), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancelar'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('Crear'), ), ], ); }, ); }, ); if (result != true || controller.text.trim().isEmpty) { controller.dispose(); return; } try { final Category newCategory = Category( name: controller.text.trim(), updatedAt: DateTime.now(), colorValue: selectedColor?.toARGB32(), iconCodePoint: selectedIcon?.codePoint, ); debugPrint('Creating category: ${newCategory.name}, color: ${newCategory.colorValue}, icon: ${newCategory.iconCodePoint}'); await widget.repository.createCategory(newCategory); await _loadCategories(); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Categoría creada'))); } try { await widget.onRequestSync(); } catch (_) {} } catch (e) { debugPrint('ERROR creating category: $e'); if (mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Error al crear categoría: $e'))); } } controller.dispose(); } @override Widget build(BuildContext context) { final double width = MediaQuery.of(context).size.width; final int crossAxisCount = math.max((width / 250).floor().round(), 2); final List visibleNotes = _getFilteredNotes(); final Category? currentCategory = _currentCategory(); final Widget body = _isLoading ? const Center(child: CircularProgressIndicator()) : visibleNotes.isEmpty ? _EmptyState( showDeletedNotes: _showDeletedNotes, categoryName: currentCategory?.name, searchQuery: _searchQuery, ) : RefreshIndicator( onRefresh: () async { await widget.onRequestSync(); }, child: MouseRegion( cursor: _isDragging ? SystemMouseCursors.grabbing : SystemMouseCursors.basic, child: MasonryGridView.count( crossAxisCount: crossAxisCount, mainAxisSpacing: 10, crossAxisSpacing: 10, itemCount: visibleNotes.length, itemBuilder: (BuildContext context, int index) { final List filteredNotes = visibleNotes; return DragTarget( onAcceptWithDetails: (DragTargetDetails details) { final Note targetNote = filteredNotes[index]; final int originalTargetIndex = _notes.indexOf( targetNote, ); _reorderNote(details.data, originalTargetIndex); }, builder: (context, candidateData, rejectedData) { return LayoutBuilder( builder: (context, constraints) { final double cellWidth = constraints.maxWidth; final bool requiresLongPressToDrag = _requiresLongPressToDrag(_lastPointerKind); final Widget draggableNote = requiresLongPressToDrag ? LongPressDraggable( data: _notes.indexOf(filteredNotes[index]), delay: const Duration(milliseconds: 280), onDragStarted: () { if (!mounted) return; setState(() { _isDragging = true; }); }, onDragEnd: (_) { if (!mounted) return; setState(() { _isDragging = false; }); }, onDraggableCanceled: (_, _) { if (!mounted) return; setState(() { _isDragging = false; }); }, feedback: MouseRegion( cursor: SystemMouseCursors.grabbing, child: Material( color: Colors.transparent, elevation: 8, child: SizedBox( width: cellWidth, child: TweenAnimationBuilder( tween: Tween(begin: 0.97, end: 1.0), duration: const Duration( milliseconds: 180, ), curve: Curves.easeOutCubic, builder: (context, scale, child) { return Transform.scale( scale: scale, alignment: Alignment.topLeft, child: child, ); }, child: Opacity( opacity: 0.95, child: NoteCard( note: filteredNotes[index], onTap: () {}, isDragging: true, ), ), ), ), ), ), childWhenDragging: MouseRegion( cursor: SystemMouseCursors.grabbing, child: Opacity( opacity: 0.3, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color.fromRGBO( 24, 25, 26, 1, ), borderRadius: BorderRadius.circular( 12, ), border: Border.all( color: Colors.white24, width: 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( filteredNotes[index].title, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), maxLines: 2, ), const SizedBox(height: 8), Text( filteredNotes[index].body, style: const TextStyle( color: Colors.white70, fontSize: 14, ), maxLines: 20, overflow: TextOverflow.clip, ), ], ), ), ), ), child: Container( decoration: BoxDecoration( border: candidateData.isNotEmpty ? Border.all( color: Colors.blue.shade400, width: 2, ) : null, borderRadius: BorderRadius.circular(12), ), child: NoteCard( key: ValueKey( filteredNotes[index].id, ), note: filteredNotes[index], onTap: () => _openNoteEditor(filteredNotes[index]), isDragging: _isDragging, ), ), ) : Draggable( data: _notes.indexOf(filteredNotes[index]), onDragStarted: () { if (!mounted) return; setState(() { _isDragging = true; }); }, onDragEnd: (_) { if (!mounted) return; setState(() { _isDragging = false; }); }, onDraggableCanceled: (_, _) { if (!mounted) return; setState(() { _isDragging = false; }); }, feedback: MouseRegion( cursor: SystemMouseCursors.grabbing, child: Material( color: Colors.transparent, elevation: 8, child: SizedBox( width: cellWidth, child: TweenAnimationBuilder( tween: Tween(begin: 0.97, end: 1.0), duration: const Duration( milliseconds: 180, ), curve: Curves.easeOutCubic, builder: (context, scale, child) { return Transform.scale( scale: scale, alignment: Alignment.topLeft, child: child, ); }, child: Opacity( opacity: 0.95, child: NoteCard( note: filteredNotes[index], onTap: () {}, isDragging: true, ), ), ), ), ), ), childWhenDragging: MouseRegion( cursor: SystemMouseCursors.grabbing, child: Opacity( opacity: 0.3, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color.fromRGBO( 24, 25, 26, 1, ), borderRadius: BorderRadius.circular( 12, ), border: Border.all( color: Colors.white24, width: 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( filteredNotes[index].title, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Text( filteredNotes[index].body, style: const TextStyle( color: Colors.white70, fontSize: 14, ), maxLines: 20, overflow: TextOverflow.clip, ), ], ), ), ), ), child: Container( decoration: BoxDecoration( border: candidateData.isNotEmpty ? Border.all( color: Colors.blue.shade400, width: 2, ) : null, borderRadius: BorderRadius.circular(12), ), child: NoteCard( key: ValueKey( filteredNotes[index].id, ), note: filteredNotes[index], onTap: () => _openNoteEditor(filteredNotes[index]), isDragging: _isDragging, ), ), ); return Listener( onPointerDown: (PointerDownEvent event) { if (_lastPointerKind == event.kind) { return; } setState(() { _lastPointerKind = event.kind; }); }, child: draggableNote, ); }, ); }, ); }, ), ), ); return Scaffold( body: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFF191A1D), Color(0xFF222326), Color(0xFF101114)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: SafeArea( child: Column( children: [ SearchAppBar( onMenuPressed: () { setState(() { _isMenuOpen = !_isMenuOpen; }); }, trailingWidget: SyncStatusIndicator( status: widget.syncStatus, progress: widget.syncProgress, detailMessage: widget.syncDetailMessage, errorMessage: widget.syncErrorMessage, onTap: widget.onRequestSync, ), onSearchChanged: (String query) { setState(() { _searchQuery = query; }); }, ), Expanded( child: Listener( onPointerDown: (PointerDownEvent event) { if (_lastPointerKind == event.kind) { return; } setState(() { _lastPointerKind = event.kind; }); }, child: Stack( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 12.0, vertical: 8.0, ), child: body, ), Positioned( left: 0, top: 0, bottom: 0, width: 28, child: IgnorePointer( ignoring: _isMenuOpen || !_requiresLongPressToDrag(_lastPointerKind), child: GestureDetector( behavior: HitTestBehavior.translucent, onHorizontalDragUpdate: (DragUpdateDetails details) { if ((details.primaryDelta ?? 0) > 6) { _openMenu(); } }, child: const SizedBox.expand(), ), ), ), Positioned.fill( child: IgnorePointer( ignoring: !_isMenuOpen, child: AnimatedOpacity( duration: const Duration(milliseconds: 300), opacity: _isMenuOpen ? 0.5 : 0.0, curve: Curves.easeOutCubic, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: _closeMenu, child: Container(color: Colors.black), ), ), ), ), AnimatedPositioned( duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, left: _isMenuOpen ? 0 : -280, top: 0, bottom: 0, width: 280, child: Material( color: const Color.fromRGBO(24, 25, 26, 1), elevation: 8, child: MenuDrawer( onMenuItemTapped: _handleMenuItemTapped, selectedItem: _selectedCategoryId != null ? 'category_${_selectedCategoryId}' : (_showDeletedNotes ? 'deleted_notes' : 'all_notes'), categories: _categories, onCreateCategory: _showCreateCategoryDialog, ), ), ), ], ), ), ), ], ), ), ), floatingActionButton: _showDeletedNotes ? null : FloatingActionButton( onPressed: _openNoteComposer, child: const MouseRegion( cursor: SystemMouseCursors.click, child: Icon(Icons.add), ), ), ); } } class _EmptyState extends StatelessWidget { const _EmptyState({ required this.showDeletedNotes, this.categoryName, this.searchQuery, }); final bool showDeletedNotes; final String? categoryName; final String? searchQuery; @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.note_add_outlined, color: Colors.white54, size: 48), const SizedBox(height: 12), Text( searchQuery != null && searchQuery!.isNotEmpty ? 'No hay resultados' : showDeletedNotes ? 'No hay notas borradas' : categoryName != null ? 'No hay notas en esta categoría' : 'Aún no hay notas', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Text( searchQuery != null && searchQuery!.isNotEmpty ? 'Prueba a buscar con otro término o crea una nota nueva.' : showDeletedNotes ? 'Las notas borradas aparecerán aquí para poder restaurarlas.' : categoryName != null ? '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: Colors.white70), ), ], ), ); } }