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
+187 -27
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,14 +14,16 @@ 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}) {
if (isAndroid || isIOS) {
return showGeneralDialog<dynamic>( return showGeneralDialog<dynamic>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -33,6 +37,43 @@ class NoteEditorScreen extends StatefulWidget {
}, },
); );
} }
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;
}
} }
class _NoteEditorScreenState extends State<NoteEditorScreen> { class _NoteEditorScreenState extends State<NoteEditorScreen> {
@@ -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,9 +204,124 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isMobilePlatform) {
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
child: LayoutBuilder( 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',
),
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),
),
],
),
),
],
),
),
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) { builder: (BuildContext context, BoxConstraints constraints) {
final double maxWidth = math.min(600, constraints.maxWidth - 48); final double maxWidth = math.min(600, constraints.maxWidth - 48);
final double maxHeight = math.min(constraints.maxHeight * 0.88, 720); final double maxHeight = math.min(constraints.maxHeight * 0.88, 720);
@@ -162,32 +331,25 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
children: [ children: [
Positioned.fill( Positioned.fill(
top: overlayTop, top: overlayTop,
child: IgnorePointer(
child: Container( child: Container(
color: const Color.fromARGB(127, 0, 0, 0).withValues(alpha: 0.5), color: const Color.fromARGB(54, 0, 0, 0).withValues(alpha: 0.5),
), ),
), ),
SafeArea( ),
Positioned.fill(
top: overlayTop,
child: Center( child: Center(
child: Padding( child: SizedBox(
padding: EdgeInsets.only(top: overlayTop), width: maxWidth,
child: ConstrainedBox( height: maxHeight,
constraints: BoxConstraints( child: Material(
maxWidth: maxWidth,
maxHeight: maxHeight,
),
child: Container(
decoration: BoxDecoration(
color: const Color.fromRGBO(24, 25, 26, 1), color: const Color.fromRGBO(24, 25, 26, 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white24, width: 1), side: BorderSide(color: Colors.white24, width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
), ),
clipBehavior: Clip.antiAlias,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -299,11 +461,9 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
), ),
), ),
), ),
),
], ],
); );
}, },
),
); );
} }
} }