import 'dart:async'; import 'package:flutter/material.dart'; import 'package:notas/data/note_body.dart'; import 'package:notas/data/note_repository.dart'; import 'package:notas/models/category.dart'; import 'package:notas/models/note.dart'; import 'package:notas/screens/note_editor_screen.dart'; import 'package:notas/theme/app_palette.dart'; import 'package:notas/widgets/category_editor_dialog.dart'; import 'package:notas/widgets/category_menu_button.dart'; import 'package:notas/widgets/category_style.dart'; import 'package:notas/widgets/note_list_view.dart'; import 'package:notas/widgets/search_app_bar.dart'; import 'package:notas/widgets/sync_status.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 { static const double _desktopBreakpoint = 900; final TextEditingController _searchController = TextEditingController(); List _notes = []; List _categories = []; bool _isLoading = true; String _searchQuery = ''; String? _selectedCategoryId; String? _selectedNoteId; DateTime? _lastSyncAt; AppPalette _paletteOf(BuildContext context) { return Theme.of(context).extension() ?? AppPalette.fromBrightness(Theme.of(context).brightness); } @override void initState() { super.initState(); _loadData(); } @override void dispose() { _searchController.dispose(); super.dispose(); } @override void didUpdateWidget(covariant HomeScreen oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.refreshToken != widget.refreshToken) { _loadData(keepSelection: true); } } Future _loadData({bool keepSelection = false}) async { if (mounted) { setState(() { _isLoading = true; }); } try { final List notes = await widget.repository.loadNotes(); final List categories = await widget.repository.loadCategories(); final DateTime? lastSyncAt = await widget.repository.getLastSyncAt(); if (!mounted) { return; } setState(() { _notes = notes; _categories = categories; _lastSyncAt = lastSyncAt; _isLoading = false; if (!keepSelection) { _selectedNoteId = null; } else if (_selectedNoteId != null && !_notes.any((Note note) => note.id == _selectedNoteId)) { _selectedNoteId = null; } }); } catch (error, stackTrace) { debugPrint('Failed to load home data: $error\n$stackTrace'); if (widget.onVaultInvalid != null) { await widget.onVaultInvalid!(); } } } Future _reloadNotes({bool keepSelection = true}) async { try { final List notes = await widget.repository.loadNotes(); if (!mounted) { return; } setState(() { _notes = notes; if (!keepSelection || (_selectedNoteId != null && !_notes.any((Note note) => note.id == _selectedNoteId))) { _selectedNoteId = null; } }); } catch (error) { debugPrint('Failed to reload notes: $error'); } } void _setSearchQuery(String value) { if (!mounted) { return; } setState(() { _searchQuery = value.trim(); }); } List _visibleNotes() { 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) || noteBodyToPlainText(note.body).toLowerCase().contains(query), ) .toList(); } Note? _selectedNote() { final String? selectedId = _selectedNoteId; if (selectedId == null) { return null; } for (final Note note in _notes) { if (note.id == selectedId) { return note; } } return null; } Category? _categoryById(String? categoryId) { if (categoryId == null) { return null; } for (final Category category in _categories) { if (category.id == categoryId) { return category; } } return null; } void _selectCategoryFilter(String? categoryId) { setState(() { _selectedCategoryId = categoryId; if (_selectedNoteId != null && !_visibleNotes().any((Note note) => note.id == _selectedNoteId)) { _selectedNoteId = null; } }); } Future _saveCategory({Category? existingCategory}) async { final CategoryDraft? categoryDraft = await showCategoryEditorDialog( context: context, title: existingCategory == null ? 'Crear categoría' : 'Editar categoría', confirmLabel: existingCategory == null ? 'Crear' : 'Guardar', initialName: existingCategory?.name, initialColorValue: existingCategory?.colorValue ?? CategoryStyle.colorsOf(context).first.toARGB32(), initialIconCodePoint: existingCategory?.iconCodePoint ?? CategoryStyle.icons.first.codePoint, ); if (categoryDraft == null) { return null; } final DateTime now = DateTime.now(); final Category category = existingCategory == null ? Category( name: categoryDraft.name, updatedAt: now, colorValue: categoryDraft.colorValue, iconCodePoint: categoryDraft.iconCodePoint, ) : existingCategory.copyWith( name: categoryDraft.name, updatedAt: now, isDirty: true, colorValue: categoryDraft.colorValue, iconCodePoint: categoryDraft.iconCodePoint, ); await widget.repository.createCategory(category); if (!mounted) { return null; } setState(() { _categories = [ for (final Category item in _categories) if (item.id == category.id) category else item, ]; if (!_categories.any((Category item) => item.id == category.id)) { _categories = [..._categories, category]; } }); return category; } Future _updateNoteCategory(Note note, String? categoryId) async { try { final Note updated = await widget.repository.updateNote( note.copyWith( categoryId: categoryId, updatedAt: DateTime.now(), isDirty: true, ), ); if (!mounted) { return; } setState(() { _notes = [ for (final Note item in _notes) if (item.id == updated.id) updated else item, ]; }); } catch (error) { if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('No se pudo cambiar la categoría: $error')), ); } } Future _deleteNoteAfterConfirmation(Note note) async { final bool? confirmed = await showDialog( context: context, builder: (BuildContext dialogContext) { final AppPalette palette = _paletteOf(dialogContext); return AlertDialog( backgroundColor: palette.surfaceElevated, title: const Text('Eliminar nota'), content: const Text( 'Esta acción eliminará la nota. ¿Quieres continuar?', ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancelar'), ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(true), style: FilledButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Confirmar'), ), ], ); }, ); if (confirmed != true) { return; } try { await widget.repository.deleteNote(note); if (!mounted) { return; } setState(() { _notes = _notes.where((Note item) => item.id != note.id).toList(); if (_selectedNoteId == note.id) { _selectedNoteId = null; } }); } catch (error) { if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('No se pudo eliminar la nota: $error')), ); } } Future _changeNoteCategory(BuildContext anchorContext, Note note) async { final String? selectedCategoryId = await showCategorySelectionMenu( anchorContext: anchorContext, categories: _categories, selectedCategoryId: note.categoryId, allowCreateCategory: true, emptyLabel: 'Sin categoría', createLabel: 'Crear categoría', onEditCategory: (Category category) { unawaited(_saveCategory(existingCategory: category)); }, ); if (!mounted || selectedCategoryId == null) { return; } if (selectedCategoryId == kCreateCategoryMenuValue) { final Category? createdCategory = await _saveCategory(); if (createdCategory == null || !mounted) { return; } await _updateNoteCategory(note, createdCategory.id); return; } final String? categoryId = selectedCategoryId.isEmpty ? null : selectedCategoryId; if (categoryId == note.categoryId) { return; } await _updateNoteCategory(note, categoryId); } Future _createNote({required bool openEditor}) async { final DateTime now = DateTime.now(); final Note draft = Note( title: 'Sin título', body: '', createdAt: now, updatedAt: now, position: 0, categoryId: _selectedCategoryId, ); try { if (_searchQuery.isNotEmpty) { _searchController.clear(); _searchQuery = ''; } final Note created = await widget.repository.createNote(draft); if (!mounted) { return; } await _reloadNotes(keepSelection: false); if (!mounted) { return; } setState(() { _selectedNoteId = created.id; }); if (openEditor) { await _openEditor(created, embedded: false); } } catch (error) { if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('No se pudo crear la nota: $error')), ); } } Future _openExistingNote(Note note, {required bool embedded}) async { setState(() { _selectedNoteId = note.id; }); if (embedded) { return; } await _openEditor(note, embedded: false); } Future _openEditor(Note note, {required bool embedded}) async { final Widget editor = NoteEditorScreen( key: ValueKey(note.id), repository: widget.repository, note: note, embedded: embedded, onSaved: (Note saved) { if (!mounted) { return; } setState(() { _notes = [ for (final Note item in _notes) if (item.id == saved.id) saved else item, ]; }); }, ); if (embedded) { return; } await Navigator.of(context).push( MaterialPageRoute(builder: (_) => editor), ); } Future _handleNoteTap(Note note, bool isDesktop) async { if (isDesktop) { await _openExistingNote(note, embedded: true); return; } await _openEditor(note, embedded: false); } Future _handleReorder(int oldIndex, int newIndex) async { final List visibleNotes = _visibleNotes(); if (oldIndex < 0 || oldIndex >= visibleNotes.length) { return; } final Note movedNote = visibleNotes[oldIndex]; final List remainingVisible = [...visibleNotes]..removeAt(oldIndex); final int clampedNewIndex = newIndex.clamp(0, remainingVisible.length); int targetFullIndex; if (remainingVisible.isEmpty) { targetFullIndex = 0; } else if (clampedNewIndex == 0) { targetFullIndex = 0; } else if (clampedNewIndex >= remainingVisible.length) { targetFullIndex = _notes.length - 1; } else { final Note afterNote = remainingVisible[clampedNewIndex]; targetFullIndex = _notes.indexWhere( (Note note) => note.id == afterNote.id, ); if (targetFullIndex < 0) { targetFullIndex = 0; } } try { await widget.repository.moveNote(movedNote, targetFullIndex); await _reloadNotes(keepSelection: true); } catch (error) { if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('No se pudo reordenar la nota: $error')), ); } } Widget _buildEmptyDetailPane(BuildContext context) { final AppPalette palette = _paletteOf(context); return Center( child: Text( 'Selecciona una nota o\ncrea una nueva para empezar.', textAlign: TextAlign.center, style: TextStyle( color: palette.textSecondary, fontSize: 18, fontWeight: FontWeight.w500, ), ), ); } Widget _buildDesktopLayout(BuildContext context, BoxConstraints constraints) { final AppPalette palette = _paletteOf(context); final double leftWidth = (constraints.maxWidth * 0.34).clamp(320, 440); final Note? selectedNote = _selectedNote(); final bool selectedIsVisible = selectedNote != null && _visibleNotes().any((Note note) => note.id == selectedNote.id); return Row( children: [ SizedBox( width: leftWidth, child: Container( decoration: BoxDecoration( color: palette.transparent, border: Border(right: BorderSide(color: palette.border)), ), child: Stack( children: [ Column( children: [ SearchAppBar( controller: _searchController, leadingWidget: CategoryMenuButton( categories: _categories, selectedCategoryId: _selectedCategoryId, onCategorySelected: _selectCategoryFilter, onEditCategory: (Category category) { unawaited(_saveCategory(existingCategory: category)); }, icon: Icons.filter_alt_outlined, tooltip: 'Filtrar por categorías', isActive: _selectedCategoryId != null, ), showLeadingButton: false, trailingWidget: IconButton( onPressed: widget.onOpenSettings, icon: Icon( Icons.settings_outlined, color: palette.textSecondary, ), tooltip: 'Ajustes', ), onSearchChanged: _setSearchQuery, searchMaxWidth: 640, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), ), Expanded( child: NoteListView( notes: _visibleNotes(), isLoading: _isLoading, selectedNoteId: _selectedNoteId, showSelectionBorder: true, categoryForNote: _categoryById, lastSyncAt: _lastSyncAt, onRefresh: () async { await widget.onRequestSync(); await _loadData(keepSelection: true); }, onReorder: _handleReorder, onNoteTap: (Note note) => _handleNoteTap(note, true), onDeleteNote: _deleteNoteAfterConfirmation, onChangeCategory: _changeNoteCategory, ), ), ], ), Positioned( right: 20, bottom: 20, child: FloatingActionButton( onPressed: () => _createNote(openEditor: false), child: const Icon(Icons.add), ), ), ], ), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(16), child: AnimatedSwitcher( duration: const Duration(milliseconds: 220), child: selectedIsVisible ? NoteEditorScreen( key: ValueKey(selectedNote.id), repository: widget.repository, note: selectedNote, embedded: true, onSaved: (Note saved) { if (!mounted) { return; } setState(() { _notes = [ for (final Note item in _notes) if (item.id == saved.id) saved else item, ]; }); }, ) : _buildEmptyDetailPane(context), ), ), ), ], ); } Widget _buildMobileLayout(BuildContext context) { final AppPalette palette = _paletteOf(context); return Stack( children: [ Column( children: [ SearchAppBar( controller: _searchController, leadingWidget: CategoryMenuButton( categories: _categories, selectedCategoryId: _selectedCategoryId, onCategorySelected: _selectCategoryFilter, onEditCategory: (Category category) { unawaited(_saveCategory(existingCategory: category)); }, icon: Icons.filter_alt_outlined, tooltip: 'Filtrar por categorías', isActive: _selectedCategoryId != null, ), showLeadingButton: false, trailingWidget: IconButton( onPressed: widget.onOpenSettings, icon: Icon(Icons.settings_outlined, color: palette.textSecondary), tooltip: 'Ajustes', ), onSearchChanged: _setSearchQuery, searchMaxWidth: 640, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), ), Expanded( child: NoteListView( notes: _visibleNotes(), isLoading: _isLoading, selectedNoteId: _selectedNoteId, showSelectionBorder: false, categoryForNote: _categoryById, lastSyncAt: _lastSyncAt, onRefresh: () async { await widget.onRequestSync(); await _loadData(keepSelection: true); }, onReorder: _handleReorder, onNoteTap: (Note note) => _handleNoteTap(note, false), onDeleteNote: _deleteNoteAfterConfirmation, onChangeCategory: _changeNoteCategory, ), ), ], ), Positioned( right: 20, bottom: 20, child: FloatingActionButton( onPressed: () => _createNote(openEditor: true), child: const Icon(Icons.add), ), ), ], ); } @override Widget build(BuildContext context) { final AppPalette palette = _paletteOf(context); return Scaffold( body: Container( decoration: BoxDecoration(gradient: palette.backdropGradient), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool isDesktop = constraints.maxWidth >= _desktopBreakpoint; return isDesktop ? _buildDesktopLayout(context, constraints) : _buildMobileLayout(context); }, ), ), ); } }