From 48cd1b24031d32113406d61c0599659430c73884 Mon Sep 17 00:00:00 2001 From: Marcos Date: Tue, 19 May 2026 17:09:33 +0200 Subject: [PATCH] feat: Enhance NoteEditorScreen with completion callback and improved mobile UI --- lib/screens/note_editor_screen.dart | 462 +++++++++++++++++++--------- 1 file changed, 311 insertions(+), 151 deletions(-) diff --git a/lib/screens/note_editor_screen.dart b/lib/screens/note_editor_screen.dart index f906a43..255cb84 100644 --- a/lib/screens/note_editor_screen.dart +++ b/lib/screens/note_editor_screen.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:notas/models/note.dart'; +import 'package:notas/platform/app_platform.dart'; // NoteEditorScreen: unified UI for creating and editing notes. // - Use `NoteEditorScreen.showDialog(context, note: existing)` to edit. @@ -12,26 +14,65 @@ import 'package:notas/models/note.dart'; // the user confirmed deletion. `null` indicates the user closed without saving. class NoteEditorScreen extends StatefulWidget { - const NoteEditorScreen({super.key, required this.note}); + const NoteEditorScreen({super.key, required this.note, this.onComplete}); final Note? note; + final ValueChanged? onComplete; @override State createState() => _NoteEditorScreenState(); static Future showDialog(BuildContext context, {Note? note}) { - return showGeneralDialog( - context: context, - barrierDismissible: false, - barrierColor: Colors.transparent, - transitionDuration: const Duration(milliseconds: 200), - pageBuilder: (context, animation, secondaryAnimation) { - return NoteEditorScreen(note: note); - }, - transitionBuilder: (context, animation, secondaryAnimation, child) { - return ScaleTransition(scale: animation, child: child); + if (isAndroid || isIOS) { + return showGeneralDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.transparent, + transitionDuration: const Duration(milliseconds: 200), + pageBuilder: (context, animation, secondaryAnimation) { + return NoteEditorScreen(note: note); + }, + transitionBuilder: (context, animation, secondaryAnimation, child) { + return ScaleTransition(scale: animation, child: child); + }, + ); + } + + final OverlayState? overlayState = Overlay.of(context, rootOverlay: true); + if (overlayState == null) { + return showGeneralDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.transparent, + transitionDuration: const Duration(milliseconds: 200), + pageBuilder: (context, animation, secondaryAnimation) { + return NoteEditorScreen(note: note); + }, + transitionBuilder: (context, animation, secondaryAnimation, child) { + return ScaleTransition(scale: animation, child: child); + }, + ); + } + + final Completer completer = Completer(); + late final OverlayEntry entry; + + entry = OverlayEntry( + builder: (BuildContext overlayContext) { + return NoteEditorScreen( + note: note, + onComplete: (dynamic result) { + if (!completer.isCompleted) { + completer.complete(result); + } + entry.remove(); + }, + ); }, ); + + overlayState.insert(entry); + return completer.future; } } @@ -41,6 +82,8 @@ class _NoteEditorScreenState extends State { late Note _currentNote; late bool _isNewNote; + bool get _isMobilePlatform => isAndroid || isIOS; + @override void initState() { super.initState(); @@ -70,8 +113,19 @@ class _NoteEditorScreenState extends State { super.dispose(); } + void _complete(dynamic result) { + final ValueChanged? callback = widget.onComplete; + + if (callback != null) { + callback(result); + return; + } + + Navigator.of(context).pop(result); + } + void _closeWithoutSaving() { - Navigator.of(context).pop(); + _complete(null); } void _saveNote() { @@ -79,7 +133,7 @@ class _NoteEditorScreenState extends State { final String body = _bodyController.text.trim(); if (title.isEmpty && body.isEmpty) { - Navigator.of(context).pop(); + _complete(null); return; } @@ -89,7 +143,7 @@ class _NoteEditorScreenState extends State { updatedAt: DateTime.now(), ); - Navigator.of(context).pop(updatedNote); + _complete(updatedNote); } void _deleteNote() { @@ -120,7 +174,7 @@ class _NoteEditorScreenState extends State { TextButton( onPressed: () { Navigator.of(context).pop(); - Navigator.of(context).pop('delete'); + _complete('delete'); }, child: Text( isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar', @@ -150,160 +204,266 @@ class _NoteEditorScreenState extends State { @override Widget build(BuildContext context) { - return Material( - color: Colors.transparent, - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - final double maxWidth = math.min(600, constraints.maxWidth - 48); - final double maxHeight = math.min(constraints.maxHeight * 0.88, 720); - final double overlayTop = MediaQuery.paddingOf(context).top + 32; - - return Stack( - children: [ - Positioned.fill( - top: overlayTop, - child: Container( - color: const Color.fromARGB(127, 0, 0, 0).withValues(alpha: 0.5), - ), - ), - SafeArea( - child: Center( - child: Padding( - padding: EdgeInsets.only(top: overlayTop), - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: maxWidth, - maxHeight: maxHeight, + if (_isMobilePlatform) { + return Material( + color: Colors.transparent, + child: SafeArea( + child: Container( + color: const Color.fromARGB(255, 24, 25, 26), + child: 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', ), - child: Container( - decoration: BoxDecoration( - color: const Color.fromRGBO(24, 25, 26, 1), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white24, width: 1), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.5), - blurRadius: 24, - offset: const Offset(0, 8), - ), - ], - ), + const SizedBox(width: 8), + Expanded( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.white12, width: 1), + 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), + ), + ], + ), + ), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + 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, + ), + ), + const SizedBox(height: 16), + TextField( + controller: _bodyController, + maxLines: null, + 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'), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final double maxWidth = math.min(600, constraints.maxWidth - 48); + final double maxHeight = math.min(constraints.maxHeight * 0.88, 720); + final double overlayTop = MediaQuery.paddingOf(context).top + 32; + + return Stack( + children: [ + Positioned.fill( + top: overlayTop, + child: IgnorePointer( + child: Container( + color: const Color.fromARGB(54, 0, 0, 0).withValues(alpha: 0.5), + ), + ), + ), + Positioned.fill( + top: overlayTop, + 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: Column( + mainAxisSize: MainAxisSize.min, + 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), + ), + ], ), ), - child: Row( + ], + ), + ), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - IconButton( - onPressed: _closeWithoutSaving, - icon: const Icon(Icons.close, color: Colors.white70), - tooltip: 'Cerrar sin guardar', + 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, + ), ), - 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), - ), - ], + const SizedBox(height: 16), + TextField( + controller: _bodyController, + maxLines: null, + 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, ), ), ], ), ), - Expanded( - child: SingleChildScrollView( - 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, - ), - ), - const SizedBox(height: 16), - TextField( - controller: _bodyController, - maxLines: null, - 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'), - ), - ], - ), - ), - ], + ), ), - ), + 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'), + ), + ], + ), + ), + ], ), ), ), ), - ], - ); - }, - ), + ), + ], + ); + }, ); } }