import 'dart:async'; import 'package:flutter/material.dart'; import 'package:intl/intl.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_style.dart'; import 'package:notas/widgets/note_card.dart'; import 'package:notas/widgets/sync_status.dart'; class _CategoryDraft { const _CategoryDraft({ required this.name, required this.colorValue, required this.iconCodePoint, }); final String name; final int colorValue; final int iconCodePoint; } 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; static const String _createCategoryMenuValue = '__create_category__'; final TextEditingController _searchController = TextEditingController(); final GlobalKey _filterButtonKey = GlobalKey(); 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'); } } 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; } String _formatLastSyncAt() { final DateTime? timestamp = _lastSyncAt; if (timestamp == null) { return 'Última sincronización: nunca'; } return 'Última sincronización: ${DateFormat('dd/MM/yyyy HH:mm').format(timestamp)}'; } Future<_CategoryDraft?> _promptCategoryDetails({ required String title, required String confirmLabel, String? initialName, int? initialColorValue, int? initialIconCodePoint, }) async { final TextEditingController controller = TextEditingController( text: initialName ?? '', ); final List colorOptions = CategoryStyle.colorsOf(context); final List iconOptions = CategoryStyle.icons; final int fallbackColorValue = initialColorValue ?? colorOptions.first.toARGB32(); final int fallbackIconCodePoint = initialIconCodePoint ?? iconOptions.first.codePoint; try { final _CategoryDraft? result = await showDialog<_CategoryDraft>( context: context, builder: (BuildContext dialogContext) { final AppPalette palette = _paletteOf(dialogContext); final List dialogColorOptions = CategoryStyle.colorsOf( dialogContext, ); final List dialogIconOptions = CategoryStyle.icons; int selectedColorValue = fallbackColorValue; int selectedIconCodePoint = fallbackIconCodePoint; return StatefulBuilder( builder: (BuildContext context, StateSetter setDialogState) { final Color previewColor = Color(selectedColorValue); return AlertDialog( backgroundColor: palette.surfaceElevated, title: Text(title), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: controller, autofocus: true, textInputAction: TextInputAction.done, decoration: const InputDecoration( hintText: 'Nombre de la categoría', ), onSubmitted: (String value) { final String name = value.trim(); if (name.isEmpty) { return; } Navigator.of(dialogContext).pop( _CategoryDraft( name: name, colorValue: selectedColorValue, iconCodePoint: selectedIconCodePoint, ), ); }, ), const SizedBox(height: 20), Text( 'Color', style: TextStyle( color: palette.textSecondary, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 10), Wrap( spacing: 10, runSpacing: 10, children: [ for (final Color color in dialogColorOptions) InkWell( borderRadius: BorderRadius.circular(999), onTap: () { setDialogState(() { selectedColorValue = color.toARGB32(); }); }, child: AnimatedContainer( duration: const Duration(milliseconds: 160), width: 42, height: 42, decoration: BoxDecoration( shape: BoxShape.circle, color: color, border: Border.all( color: selectedColorValue == color.toARGB32() ? palette.textPrimary : palette.border, width: selectedColorValue == color.toARGB32() ? 2.5 : 1, ), boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.0), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: selectedColorValue == color.toARGB32() ? Icon( Icons.check, size: 18, color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white, ) : null, ), ), ], ), const SizedBox(height: 20), Text( 'Icono', style: TextStyle( color: palette.textSecondary, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 10), Wrap( spacing: 8, runSpacing: 8, children: [ for (final IconData icon in dialogIconOptions) InkWell( borderRadius: BorderRadius.circular(14), onTap: () { setDialogState(() { selectedIconCodePoint = icon.codePoint; }); }, child: AnimatedContainer( duration: const Duration(milliseconds: 160), width: 50, height: 50, decoration: BoxDecoration( color: selectedIconCodePoint == icon.codePoint ? previewColor.withValues(alpha: 0.14) : palette.fill, borderRadius: BorderRadius.circular(14), border: Border.all( color: selectedIconCodePoint == icon.codePoint ? previewColor : palette.border, width: selectedIconCodePoint == icon.codePoint ? 2 : 1, ), ), child: Icon( icon, color: selectedIconCodePoint == icon.codePoint ? previewColor : palette.textSecondary, ), ), ), ], ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Cancelar'), ), FilledButton( onPressed: () { final String name = controller.text.trim(); if (name.isEmpty) { return; } Navigator.of(dialogContext).pop( _CategoryDraft( name: name, colorValue: selectedColorValue, iconCodePoint: selectedIconCodePoint, ), ); }, child: Text(confirmLabel), ), ], ); }, ); }, ); if (result == null || result.name.trim().isEmpty) { return null; } return result; } finally { controller.dispose(); } } Future _saveCategory({Category? existingCategory}) async { final _CategoryDraft? categoryDraft = await _promptCategoryDetails( 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')), ); } } RelativeRect _menuRectFromContext(BuildContext anchorContext) { final RenderBox button = anchorContext.findRenderObject()! as RenderBox; final RenderBox overlay = Overlay.of(anchorContext).context.findRenderObject()! as RenderBox; final Offset topLeft = button.localToGlobal(Offset.zero, ancestor: overlay); final Offset bottomRight = button.localToGlobal( button.size.bottomRight(Offset.zero), ancestor: overlay, ); return RelativeRect.fromRect( Rect.fromLTRB(topLeft.dx, topLeft.dy, bottomRight.dx, bottomRight.dy), Offset.zero & overlay.size, ); } Future _showAnchoredCategoryMenu({ required BuildContext anchorContext, required List> items, }) { return showMenu( context: anchorContext, position: _menuRectFromContext(anchorContext), items: items, elevation: 10, color: _paletteOf(anchorContext).surfaceElevated, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ); } Widget _buildCategoryMenuItem({ required BuildContext context, required String label, required IconData icon, required Color color, required bool selected, VoidCallback? onEditPressed, }) { final AppPalette palette = _paletteOf(context); return AnimatedContainer( duration: const Duration(milliseconds: 160), padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: selected ? color.withValues(alpha: 0.05) : Colors.transparent, borderRadius: BorderRadius.circular(12), border: Border.all( color: selected ? color.withValues(alpha: 0.42) : Colors.transparent, width: selected ? 1 : 1, ), ), child: Row( children: [ Icon(icon, color: color, size: 20), const SizedBox(width: 10), Expanded( child: Text( label, overflow: TextOverflow.ellipsis, style: TextStyle( color: selected ? palette.textPrimary : palette.textSecondary, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, ), ), ), const SizedBox(width: 8), if (onEditPressed != null) IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), icon: Icon( Icons.more_vert, size: 18, color: palette.textSecondary, ), onPressed: onEditPressed, ), ], ), ); } Future _openCategoryFilter(BuildContext anchorContext) async { final String? selectedCategoryId = await _showAnchoredCategoryMenu( anchorContext: anchorContext, items: >[ PopupMenuItem( value: '', child: _buildCategoryMenuItem( context: anchorContext, label: 'Todas las categorías', icon: Icons.filter_alt_outlined, color: _paletteOf(anchorContext).textSecondary, selected: _selectedCategoryId == null, ), ), const PopupMenuDivider(), for (final Category category in _categories) PopupMenuItem( value: category.id, child: Builder( builder: (BuildContext menuContext) { final Color categoryColor = Color( category.colorValue ?? _paletteOf(menuContext).accent.toARGB32(), ); return _buildCategoryMenuItem( context: menuContext, label: category.name, icon: CategoryStyle.iconForCodePoint(category.iconCodePoint), color: categoryColor, selected: _selectedCategoryId == category.id, onEditPressed: () { Navigator.of(menuContext).pop(); unawaited(_saveCategory(existingCategory: category)); }, ); }, ), ), ], ); if (!mounted || selectedCategoryId == null) { return; } setState(() { _selectedCategoryId = selectedCategoryId.isEmpty ? null : selectedCategoryId; if (_selectedNoteId != null && !_visibleNotes().any((Note note) => note.id == _selectedNoteId)) { _selectedNoteId = null; } }); } Future _changeNoteCategory( BuildContext anchorContext, Note note, ) async { final String? selectedCategoryId = await _showAnchoredCategoryMenu( anchorContext: anchorContext, items: >[ PopupMenuItem( value: '', child: _buildCategoryMenuItem( context: anchorContext, label: 'Sin categoría', icon: Icons.folder_outlined, color: _paletteOf(anchorContext).textSecondary, selected: note.categoryId == null, ), ), const PopupMenuDivider(), for (final Category category in _categories) PopupMenuItem( value: category.id, child: Builder( builder: (BuildContext menuContext) { final Color categoryColor = Color( category.colorValue ?? _paletteOf(menuContext).accent.toARGB32(), ); return _buildCategoryMenuItem( context: menuContext, label: category.name, icon: CategoryStyle.iconForCodePoint(category.iconCodePoint), color: categoryColor, selected: note.categoryId == category.id, onEditPressed: () { Navigator.of(menuContext).pop(); unawaited(_saveCategory(existingCategory: category)); }, ); }, ), ), const PopupMenuDivider(), PopupMenuItem( value: _createCategoryMenuValue, child: _buildCategoryMenuItem( context: anchorContext, label: 'Crear categoría', icon: Icons.add_circle_outline, color: _paletteOf(anchorContext).textSecondary, selected: false, ), ), ], ); if (!mounted || selectedCategoryId == null) { return; } if (selectedCategoryId == _createCategoryMenuValue) { 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 _buildHeader(BuildContext context) { final AppPalette palette = _paletteOf(context); return Container( decoration: BoxDecoration( color: palette.transparent, border: Border(bottom: BorderSide(color: palette.border, width: 0.5)), ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ Builder( builder: (BuildContext buttonContext) { return IconButton( key: _filterButtonKey, onPressed: () => _openCategoryFilter(buttonContext), tooltip: 'Filtrar por categorías', iconSize: 24, style: IconButton.styleFrom( backgroundColor: _selectedCategoryId == null ? Colors.transparent : palette.accent.withValues(alpha: 0.08), shape: const CircleBorder(), ), icon: Stack( clipBehavior: Clip.none, children: [ Icon( Icons.filter_alt_outlined, color: palette.textSecondary, ), if (_selectedCategoryId != null) Positioned( right: -1, top: -1, child: Container( width: 7, height: 7, decoration: BoxDecoration( color: palette.accent, shape: BoxShape.circle, ), ), ), ], ), ); }, ), const SizedBox(width: 8), Expanded( child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 640), child: TextField( controller: _searchController, onChanged: (String value) { setState(() { _searchQuery = value.trim(); }); }, decoration: InputDecoration( hintText: 'Buscar notas...', hintStyle: TextStyle(color: palette.textSecondary), prefixIcon: Icon( Icons.search, color: palette.textSecondary, ), suffixIcon: _searchQuery.isEmpty ? null : IconButton( onPressed: () { _searchController.clear(); setState(() { _searchQuery = ''; }); }, icon: Icon( Icons.clear, 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), ), isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 14, vertical: 12, ), ), ), ), ), ), const SizedBox(width: 8), IconButton( onPressed: widget.onOpenSettings, icon: Icon(Icons.settings_outlined, color: palette.textSecondary), tooltip: 'Ajustes', ), ], ), ); } Widget _buildNoteList(BuildContext context, {required bool isDesktop}) { final AppPalette palette = _paletteOf(context); final List visibleNotes = _visibleNotes(); if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (visibleNotes.isEmpty) { return Center( child: Text( 'No hay notas para mostrar', style: TextStyle(color: palette.textSecondary), ), ); } return RefreshIndicator( onRefresh: () async { await widget.onRequestSync(); await _loadData(keepSelection: true); }, child: ReorderableListView.builder( padding: const EdgeInsets.fromLTRB(10, 10, 10, 14), buildDefaultDragHandles: false, itemCount: visibleNotes.length, onReorderItem: _handleReorder, footer: Padding( padding: const EdgeInsets.only(top: 4, bottom: 72), child: Center( child: Text( _formatLastSyncAt(), style: TextStyle(color: palette.textSecondary, fontSize: 12), ), ), ), itemBuilder: (BuildContext context, int index) { final Note note = visibleNotes[index]; return Padding( key: ValueKey(note.id), padding: const EdgeInsets.only(bottom: 6), child: ReorderableDelayedDragStartListener( index: index, child: NoteCard( note: note, category: _categoryById(note.categoryId), isSelected: note.id == _selectedNoteId, showSelectionBorder: isDesktop, onTap: () => _handleNoteTap(note, isDesktop), onDelete: () => _deleteNoteAfterConfirmation(note), onChangeCategory: (BuildContext buttonContext) => _changeNoteCategory(buttonContext, note), ), ), ); }, ), ); } 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: [ _buildHeader(context), Expanded(child: _buildNoteList(context, isDesktop: true)), ], ), 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) { return Stack( children: [ Column( children: [ _buildHeader(context), Expanded(child: _buildNoteList(context, isDesktop: false)), ], ), 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); }, ), ), ); } }