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/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; 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(); } @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) { // 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); 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]; await widget.repository.moveNote(movedNote, newIndex); await _loadNotes(); } 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() { 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(); } Future _handleMenuItemTapped(String item) async { _closeMenu(); if (item == 'settings') { widget.onOpenSettings(); return; } if (item == 'deleted_notes') { setState(() { _showDeletedNotes = true; _searchQuery = ''; _isLoading = true; }); await _loadNotes(); return; } if (item == 'all_notes') { setState(() { _showDeletedNotes = false; _searchQuery = ''; _isLoading = true; }); await _loadNotes(); } } @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 ? _EmptyState(showDeletedNotes: _showDeletedNotes) : RefreshIndicator( onRefresh: () async { await widget.onRequestSync(); }, child: 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; 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, 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, ), ), ) : 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 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: _showDeletedNotes ? 'deleted_notes' : 'all_notes', ), ), ), ], ), ), ), ], ), ), ), 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}); final bool showDeletedNotes; @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( showDeletedNotes ? 'No hay notas borradas' : 'Aún no hay notas', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Text( showDeletedNotes ? 'Las notas borradas aparecerán aquí para poder restaurarlas.' : 'Pulsa el botón + para crear la primera.', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white70), ), ], ), ); } }