import 'dart:math' as math; 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/widgets/app_title_bar.dart'; import 'package:notas/widgets/menu_drawer.dart'; import 'package:notas/widgets/note_card.dart'; import 'package:notas/widgets/search_app_bar.dart'; // HomeScreen: main entry showing notes in a responsive masonry grid. // Key behaviors implemented here: // - Load/save notes via `NoteRepository` (SQLite through Drift) // - Open `NoteEditorScreen` for create/edit // - Drag & drop reordering (updates `index` in the database) class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { final NoteRepository _repository = NoteRepository(); List _notes = []; String _searchQuery = ''; bool _isLoading = true; bool _isDragging = false; bool _isMenuOpen = false; @override void initState() { super.initState(); _loadNotes(); } Future _loadNotes() async { final List storedNotes = await _repository.loadNotes(); if (!mounted) { return; } setState(() { _notes = storedNotes; _isLoading = false; }); } Future _openNoteComposer() async { final dynamic result = await NoteEditorScreen.showDialog(context); if (result == null) { return; } if (result is Note) { final Note createdNote = await _repository.createNote(result); final List updatedNotes = _normalizeNotes([createdNote, ..._notes]); if (!mounted) { return; } setState(() { _notes = updatedNotes; }); } } Future _deleteNote(Note note) async { await _repository.deleteNote(note); final List updatedNotes = _normalizeNotes( _notes.where((Note item) => item.id != note.id).toList(), ); if (!mounted) { return; } setState(() { _notes = updatedNotes; }); } Future _reorderNote(int oldIndex, int newIndex) async { if (oldIndex == newIndex) { return; } final List updatedNotes = [..._notes]; final Note movedNote = updatedNotes.removeAt(oldIndex); updatedNotes.insert(newIndex, movedNote); await _repository.moveNote(movedNote, newIndex); if (!mounted) { return; } setState(() { _notes = _normalizeNotes(updatedNotes); }); } 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) { final int noteIndex = _notes.indexWhere((Note item) => item == note); if (noteIndex != -1) { final Note savedNote = await _repository.updateNote(result); final List updatedNotes = [..._notes]; updatedNotes[noteIndex] = savedNote; if (!mounted) { return; } setState(() { _notes = _normalizeNotes(updatedNotes); }); } } } List _normalizeNotes(List notes) { return notes.asMap().entries.map((MapEntry entry) { return entry.value.copyWith(index: entry.key); }).toList(); } List _getFilteredNotes() { if (_searchQuery.isEmpty) { return _notes; } final String query = _searchQuery.toLowerCase(); return _notes .where((Note note) => note.title.toLowerCase().contains(query) || note.body.toLowerCase().contains(query)) .toList(); } @override Widget build(BuildContext context) { final double width = MediaQuery.of(context).size.width; final int crossAxisCount = math.max((width / 250).floor().round(), 2); final Widget body = _isLoading ? const Center(child: CircularProgressIndicator()) : _notes.isEmpty ? const _EmptyState() : MouseRegion( cursor: _isDragging ? SystemMouseCursors.grabbing : SystemMouseCursors.basic, child: MasonryGridView.count( crossAxisCount: crossAxisCount, mainAxisSpacing: 10, crossAxisSpacing: 10, itemCount: _getFilteredNotes().length, itemBuilder: (BuildContext context, int index) { final List filteredNotes = _getFilteredNotes(); 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; return 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 ?? filteredNotes[index].index), note: filteredNotes[index], onTap: () => _openNoteEditor(filteredNotes[index]), isDragging: _isDragging, ), ), ); }, ); }, ); }, ), ); return Scaffold( body: SafeArea( child: Column( children: [ const AppTitleBar(), SearchAppBar( onMenuPressed: () { setState(() { _isMenuOpen = !_isMenuOpen; }); }, onSearchChanged: (String query) { setState(() { _searchQuery = query; }); }, ), Expanded( child: Stack( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), child: body, ), // Dark overlay when menu is open; clicking it closes the menu. 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: () { setState(() { _isMenuOpen = false; }); }, 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: (String item) { setState(() { _isMenuOpen = false; }); }, ), ), ), ], ), ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _openNoteComposer, child: const MouseRegion( cursor: SystemMouseCursors.click, child: Icon(Icons.add), ), ), ); } } class _EmptyState extends StatelessWidget { const _EmptyState(); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: const [ Icon(Icons.note_add_outlined, color: Colors.white54, size: 48), SizedBox(height: 12), Text( 'Aún no hay notas', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), ), SizedBox(height: 8), Text( 'Pulsa el botón + para crear la primera.', textAlign: TextAlign.center, style: TextStyle(color: Colors.white70), ), ], ), ); } }