feat: Enhance NoteEditorScreen with completion callback and improved mobile UI

This commit is contained in:
2026-05-19 17:09:33 +02:00
parent f550476177
commit 48cd1b2403
+311 -151
View File
@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:notas/models/note.dart'; import 'package:notas/models/note.dart';
import 'package:notas/platform/app_platform.dart';
// NoteEditorScreen: unified UI for creating and editing notes. // NoteEditorScreen: unified UI for creating and editing notes.
// - Use `NoteEditorScreen.showDialog(context, note: existing)` to edit. // - 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. // the user confirmed deletion. `null` indicates the user closed without saving.
class NoteEditorScreen extends StatefulWidget { class NoteEditorScreen extends StatefulWidget {
const NoteEditorScreen({super.key, required this.note}); const NoteEditorScreen({super.key, required this.note, this.onComplete});
final Note? note; final Note? note;
final ValueChanged<dynamic>? onComplete;
@override @override
State<NoteEditorScreen> createState() => _NoteEditorScreenState(); State<NoteEditorScreen> createState() => _NoteEditorScreenState();
static Future<dynamic> showDialog(BuildContext context, {Note? note}) { static Future<dynamic> showDialog(BuildContext context, {Note? note}) {
return showGeneralDialog<dynamic>( if (isAndroid || isIOS) {
context: context, return showGeneralDialog<dynamic>(
barrierDismissible: false, context: context,
barrierColor: Colors.transparent, barrierDismissible: false,
transitionDuration: const Duration(milliseconds: 200), barrierColor: Colors.transparent,
pageBuilder: (context, animation, secondaryAnimation) { transitionDuration: const Duration(milliseconds: 200),
return NoteEditorScreen(note: note); pageBuilder: (context, animation, secondaryAnimation) {
}, return NoteEditorScreen(note: note);
transitionBuilder: (context, animation, secondaryAnimation, child) { },
return ScaleTransition(scale: animation, child: child); transitionBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(scale: animation, child: child);
},
);
}
final OverlayState? overlayState = Overlay.of(context, rootOverlay: true);
if (overlayState == null) {
return showGeneralDialog<dynamic>(
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<dynamic> completer = Completer<dynamic>();
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<NoteEditorScreen> {
late Note _currentNote; late Note _currentNote;
late bool _isNewNote; late bool _isNewNote;
bool get _isMobilePlatform => isAndroid || isIOS;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -70,8 +113,19 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
super.dispose(); super.dispose();
} }
void _complete(dynamic result) {
final ValueChanged<dynamic>? callback = widget.onComplete;
if (callback != null) {
callback(result);
return;
}
Navigator.of(context).pop(result);
}
void _closeWithoutSaving() { void _closeWithoutSaving() {
Navigator.of(context).pop(); _complete(null);
} }
void _saveNote() { void _saveNote() {
@@ -79,7 +133,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
final String body = _bodyController.text.trim(); final String body = _bodyController.text.trim();
if (title.isEmpty && body.isEmpty) { if (title.isEmpty && body.isEmpty) {
Navigator.of(context).pop(); _complete(null);
return; return;
} }
@@ -89,7 +143,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
Navigator.of(context).pop(updatedNote); _complete(updatedNote);
} }
void _deleteNote() { void _deleteNote() {
@@ -120,7 +174,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pop('delete'); _complete('delete');
}, },
child: Text( child: Text(
isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar', isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar',
@@ -150,160 +204,266 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( if (_isMobilePlatform) {
color: Colors.transparent, return Material(
child: LayoutBuilder( color: Colors.transparent,
builder: (BuildContext context, BoxConstraints constraints) { child: SafeArea(
final double maxWidth = math.min(600, constraints.maxWidth - 48); child: Container(
final double maxHeight = math.min(constraints.maxHeight * 0.88, 720); color: const Color.fromARGB(255, 24, 25, 26),
final double overlayTop = MediaQuery.paddingOf(context).top + 32; child: Column(
children: [
return Stack( Container(
children: [ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
Positioned.fill( decoration: BoxDecoration(
top: overlayTop, border: Border(
child: Container( bottom: BorderSide(color: Colors.white12, width: 1),
color: const Color.fromARGB(127, 0, 0, 0).withValues(alpha: 0.5), ),
), ),
), child: Row(
SafeArea( children: [
child: Center( IconButton(
child: Padding( onPressed: _closeWithoutSaving,
padding: EdgeInsets.only(top: overlayTop), icon: const Icon(Icons.close, color: Colors.white70),
child: ConstrainedBox( tooltip: 'Cerrar sin guardar',
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
), ),
child: Container( const SizedBox(width: 8),
decoration: BoxDecoration( Expanded(
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),
),
],
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Text(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), 'Creado: ${_formatDate(_currentNote.createdAt)}',
decoration: BoxDecoration( style: const TextStyle(color: Colors.white54, fontSize: 12),
border: Border( ),
bottom: BorderSide(color: Colors.white12, width: 1), 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: [ children: [
IconButton( TextField(
onPressed: _closeWithoutSaving, controller: _titleController,
icon: const Icon(Icons.close, color: Colors.white70), style: const TextStyle(
tooltip: 'Cerrar sin guardar', 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), const SizedBox(height: 16),
Expanded( TextField(
child: Column( controller: _bodyController,
crossAxisAlignment: CrossAxisAlignment.start, maxLines: null,
mainAxisSize: MainAxisSize.min, style: const TextStyle(
children: [ color: Colors.white,
Text( fontSize: 16,
'Creado: ${_formatDate(_currentNote.createdAt)}', height: 1.6,
style: const TextStyle(color: Colors.white54, fontSize: 12), ),
), decoration: const InputDecoration(
if (_currentNote.updatedAt != _currentNote.createdAt) hintText: 'Escribe tu nota...',
Text( hintStyle: TextStyle(color: Colors.white30),
'Modificado: ${_formatDate(_currentNote.updatedAt)}', border: InputBorder.none,
style: const TextStyle(color: Colors.white54, fontSize: 12), 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'),
),
],
),
),
],
), ),
), ),
), ),
), ),
], ),
); ],
}, );
), },
); );
} }
} }