import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:notas/models/category.dart'; import 'package:notas/models/note.dart'; import 'package:notas/platform/app_platform.dart'; import 'package:notas/widgets/category_style.dart'; // NoteEditorScreen: unified UI for creating and editing notes. // - Use `NoteEditorScreen.showDialog(context, note: existing)` to edit. // - Use `NoteEditorScreen.showDialog(context)` to create a new note. // The screen returns either a `Note` (saved) or the string `'delete'` when // the user confirmed deletion. `null` indicates the user closed without saving. class NoteEditorScreen extends StatefulWidget { const NoteEditorScreen({ super.key, required this.note, this.categoryId, this.categories = const [], this.onComplete, }); final Note? note; final String? categoryId; final List categories; final ValueChanged? onComplete; @override State createState() => _NoteEditorScreenState(); static Future _showGeneralEditorDialog( BuildContext context, { Note? note, String? categoryId, List categories = const [], }) { return showGeneralDialog( context: context, barrierDismissible: false, barrierColor: Colors.transparent, transitionDuration: const Duration(milliseconds: 200), pageBuilder: (context, animation, secondaryAnimation) { return NoteEditorScreen( note: note, categoryId: categoryId, categories: categories, ); }, transitionBuilder: (context, animation, secondaryAnimation, child) { return ScaleTransition(scale: animation, child: child); }, ); } static Future showDialog( BuildContext context, { Note? note, String? categoryId, List categories = const [], }) { if (isAndroid || isIOS) { return _showGeneralEditorDialog( context, note: note, categoryId: categoryId, categories: categories, ); } final OverlayState? overlayState = Overlay.of(context, rootOverlay: true); if (overlayState == null) { return _showGeneralEditorDialog( context, note: note, categoryId: categoryId, categories: categories, ); } final Completer completer = Completer(); late final OverlayEntry entry; entry = OverlayEntry( builder: (BuildContext overlayContext) { return NoteEditorScreen( note: note, categoryId: categoryId, categories: categories, onComplete: (dynamic result) { if (!completer.isCompleted) { completer.complete(result); } if (entry.mounted) { entry.remove(); } }, ); }, ); overlayState.insert(entry); return completer.future; } } class _NoteEditorScreenState extends State { late TextEditingController _titleController; late TextEditingController _bodyController; late Note _currentNote; late bool _isNewNote; String? _selectedCategoryId; final GlobalKey _categorySelectorKey = GlobalKey(); OverlayEntry? _categoryMenuEntry; bool _didComplete = false; bool get _isMobileLayout => isAndroid || isIOS; @override void initState() { super.initState(); _isNewNote = widget.note == null; if (_isNewNote) { final DateTime now = DateTime.now(); _currentNote = Note( title: '', body: '', createdAt: now, updatedAt: now, position: 0, categoryId: widget.categoryId, ); } else { _currentNote = widget.note!; } _selectedCategoryId = _currentNote.categoryId ?? widget.categoryId; _titleController = TextEditingController(text: _currentNote.title); _bodyController = TextEditingController(text: _currentNote.body); } @override void dispose() { _closeCategoryMenu(); _titleController.dispose(); _bodyController.dispose(); super.dispose(); } void _complete(dynamic result) { if (_didComplete) { return; } _didComplete = true; _closeCategoryMenu(); final ValueChanged? callback = widget.onComplete; if (callback != null) { callback(result); return; } Navigator.of(context).pop(result); } void _closeWithoutSaving() { _complete(null); } void _saveNote() { final String title = _titleController.text.trim(); final String body = _bodyController.text.trim(); final bool categoryChanged = _selectedCategoryId != _currentNote.categoryId; if (title.isEmpty && body.isEmpty && !categoryChanged) { _complete(null); return; } final Note updatedNote = _currentNote.copyWith( title: title.isEmpty ? 'Sin título' : title, body: body, categoryId: _selectedCategoryId, updatedAt: DateTime.now(), isDirty: true, ); _complete(updatedNote); } Widget _buildDeleteConfirmationDialog({ required ValueChanged onConfirmed, }) { final bool isDeletedNote = _currentNote.isDeleted; return AlertDialog( backgroundColor: const Color(0xFF303134), title: Text( isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar nota', style: const TextStyle(color: Colors.white), ), content: Text( isDeletedNote ? 'Esta nota ya está borrada. Si la eliminas ahora, se borrará permanentemente.' : '¿Estás seguro de que deseas eliminar esta nota?', style: const TextStyle(color: Colors.white70), ), actions: [ TextButton( onPressed: () => onConfirmed(false), child: const Text( 'Cancelar', style: TextStyle(color: Colors.white70), ), ), TextButton( onPressed: () => onConfirmed(true), child: Text( isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar', style: const TextStyle(color: Colors.red), ), ), ], ); } Future _showDeleteConfirmation() async { if (_isMobileLayout) { final bool? confirmed = await showDialog( context: context, barrierDismissible: false, barrierColor: Colors.transparent, builder: (BuildContext dialogContext) { return _buildDeleteConfirmationDialog( onConfirmed: (bool confirmed) => Navigator.of(dialogContext).pop(confirmed), ); }, ); return confirmed ?? false; } final OverlayState? overlayState = Overlay.of(context, rootOverlay: true); if (overlayState == null) { final bool? confirmed = await showDialog( context: context, barrierDismissible: false, barrierColor: Colors.transparent, builder: (BuildContext dialogContext) { return _buildDeleteConfirmationDialog( onConfirmed: (bool confirmed) => Navigator.of(dialogContext).pop(confirmed), ); }, ); return confirmed ?? false; } final Completer completer = Completer(); late final OverlayEntry entry; bool didRemove = false; entry = OverlayEntry( builder: (BuildContext overlayContext) { final ValueChanged close = (bool confirmed) { if (!completer.isCompleted) { completer.complete(confirmed); } if (!didRemove && entry.mounted) { didRemove = true; entry.remove(); } }; return Material( color: Colors.transparent, child: Stack( children: [ const Positioned.fill( child: ModalBarrier( dismissible: false, color: Color.fromARGB(140, 0, 0, 0), ), ), Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: _buildDeleteConfirmationDialog(onConfirmed: close), ), ), ], ), ); }, ); overlayState.insert(entry); return completer.future; } Category? _categoryById(String? id) { for (final Category category in widget.categories) { if (category.id == id) { return category; } } return null; } Color _categoryBackgroundColor(Category? category) { if (category?.colorValue == null) { return Colors.white.withValues(alpha: 0.08); } return Color(category!.colorValue!); } Color _categoryForegroundColor(Category? category) { if (category == null || category.colorValue == null) { return Colors.white; } final Color background = Color(category.colorValue!); return background.computeLuminance() > 0.55 ? Colors.black87 : Colors.white; } Widget _buildCategorySelectorBox({Category? category}) { final String label = category?.name ?? 'Sin categoría'; final IconData icon = CategoryStyle.iconForCodePoint(category?.iconCodePoint); final Color backgroundColor = _categoryBackgroundColor(category); final Color foregroundColor = _categoryForegroundColor(category); return Container( key: _categorySelectorKey, padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(10), border: Border.all( color: category?.colorValue != null ? backgroundColor.withValues(alpha: 0.85) : Colors.white24, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: foregroundColor, size: 15), const SizedBox(width: 6), Expanded( child: Text( label, overflow: TextOverflow.ellipsis, style: TextStyle( color: foregroundColor, fontSize: 12, fontWeight: FontWeight.w600, ), ), ), const SizedBox(width: 6), Icon( Icons.arrow_drop_down, color: foregroundColor.withValues(alpha: 0.9), size: 16, ), ], ), ); } void _closeCategoryMenu() { final OverlayEntry? entry = _categoryMenuEntry; if (entry != null && entry.mounted) { entry.remove(); } _categoryMenuEntry = null; } void _toggleCategoryMenu() { if (_categoryMenuEntry != null) { _closeCategoryMenu(); return; } final OverlayState? overlayState = Overlay.of(context, rootOverlay: true); if (overlayState == null) { return; } _categoryMenuEntry = OverlayEntry( builder: (BuildContext overlayContext) { final Size screenSize = MediaQuery.of(overlayContext).size; final double menuWidth = math.min(screenSize.width - 32, 320); final double menuHeight = math.min(screenSize.height - 32, 360); return Material( color: Colors.transparent, child: Stack( children: [ Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: _closeCategoryMenu, child: const SizedBox.expand(), ), ), Center( child: ConstrainedBox( constraints: BoxConstraints( maxWidth: menuWidth, maxHeight: menuHeight, ), child: Material( elevation: 10, color: const Color(0xFF303134), borderRadius: BorderRadius.circular(12), clipBehavior: Clip.antiAlias, child: ListView( padding: const EdgeInsets.symmetric(vertical: 8), shrinkWrap: true, children: [ _buildCategoryMenuItem( category: null, label: 'Sin categoría', isSelected: _selectedCategoryId == null, onTap: () { setState(() { _selectedCategoryId = null; }); _closeCategoryMenu(); }, ), for (final Category category in widget.categories) _buildCategoryMenuItem( category: category, label: category.name, isSelected: _selectedCategoryId == category.id, onTap: () { setState(() { _selectedCategoryId = category.id; }); _closeCategoryMenu(); }, ), ], ), ), ), ), ], ), ); }, ); overlayState.insert(_categoryMenuEntry!); } Widget _buildCategoryMenuItem({ required Category? category, required String label, required bool isSelected, required VoidCallback onTap, }) { final Color backgroundColor = _categoryBackgroundColor(category); final Color foregroundColor = _categoryForegroundColor(category); final IconData icon = CategoryStyle.iconForCodePoint(category?.iconCodePoint); return InkWell( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), color: isSelected ? Colors.white.withValues(alpha: 0.08) : null, child: Row( children: [ Container( width: 28, height: 28, decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(8), border: Border.all( color: category?.colorValue != null ? backgroundColor.withValues(alpha: 0.85) : Colors.white24, ), ), child: Icon(icon, size: 16, color: foregroundColor), ), const SizedBox(width: 12), Expanded( child: Text( label, overflow: TextOverflow.ellipsis, style: TextStyle( color: foregroundColor, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), if (isSelected) Icon(Icons.check, color: foregroundColor, size: 18), ], ), ), ); } Future _deleteNote() async { final bool confirmed = await _showDeleteConfirmation(); if (!mounted || !confirmed) { return; } _complete('delete'); } String _formatDate(DateTime date) { final DateTime now = DateTime.now(); final DateTime today = DateTime(now.year, now.month, now.day); final DateTime yesterday = today.subtract(const Duration(days: 1)); final DateTime noteDate = DateTime(date.year, date.month, date.day); if (noteDate == today) { return 'Hoy ${DateFormat('HH:mm').format(date)}'; } else if (noteDate == yesterday) { return 'Ayer ${DateFormat('HH:mm').format(date)}'; } else { return DateFormat('dd/MM/yyyy HH:mm').format(date); } } Widget _buildEditorContent({required bool isMobile}) { final double titleSpacing = isMobile ? 16.0 : 8.0; return Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: Colors.white12, width: 1)), ), child: Row( children: [ IconButton( onPressed: _closeWithoutSaving, icon: const Icon(Icons.close, color: Colors.white70), tooltip: 'Cerrar sin guardar', ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'Creado: ${_formatDate(_currentNote.createdAt)}', style: const TextStyle( color: Colors.white54, fontSize: 12, ), ), if (_currentNote.updatedAt != _currentNote.createdAt) Text( 'Modificado: ${_formatDate(_currentNote.updatedAt)}', style: const TextStyle( color: Colors.white54, fontSize: 12, ), ), ], ), ), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 150), child: SizedBox( width: 150, child: KeyedSubtree( key: const ValueKey('category_selector'), child: InkWell( onTap: _toggleCategoryMenu, borderRadius: BorderRadius.circular(12), child: _buildCategorySelectorBox( category: _categoryById(_selectedCategoryId), ), ), ), ), ), ], ), ), Expanded( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _titleController, style: const TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, ), decoration: const InputDecoration( hintText: 'Título', hintStyle: TextStyle(color: Colors.white30), border: InputBorder.none, contentPadding: EdgeInsets.zero, ), ), SizedBox(height: titleSpacing), Expanded( child: TextField( controller: _bodyController, keyboardType: TextInputType.multiline, maxLines: null, expands: true, style: const TextStyle( color: Colors.white, fontSize: 16, height: 1.6, ), decoration: const InputDecoration( hintText: 'Escribe tu nota...', hintStyle: TextStyle(color: Colors.white30), border: InputBorder.none, contentPadding: EdgeInsets.zero, ), ), ), ], ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( border: Border(top: BorderSide(color: Colors.white12, width: 1)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (!_isNewNote) IconButton( onPressed: _deleteNote, icon: const Icon(Icons.delete_outline, color: Colors.red), tooltip: 'Eliminar nota', ) else const SizedBox(width: 48), FilledButton(onPressed: _saveNote, child: const Text('Guardar')), ], ), ), ], ); } @override Widget build(BuildContext context) { if (_isMobileLayout) { return Material( color: Colors.transparent, child: SafeArea( child: Container( color: const Color.fromARGB(255, 24, 25, 26), child: _buildEditorContent(isMobile: true), ), ), ); } return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final double maxWidth = math.min(constraints.maxWidth - 32, 600); final double maxHeight = math.min(constraints.maxHeight - 32, 720); return Stack( children: [ Positioned.fill( child: ModalBarrier( dismissible: false, color: const Color.fromARGB(54, 0, 0, 0).withValues(alpha: 0.5), ), ), Positioned.fill( child: Center( child: SizedBox( width: maxWidth, height: maxHeight, child: Material( color: const Color.fromRGBO(24, 25, 26, 1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide(color: Colors.white24, width: 1), ), clipBehavior: Clip.antiAlias, child: _buildEditorContent(isMobile: false), ), ), ), ), ], ); }, ); } }