feat: Refactor note editor dialog and delete confirmation for improved readability and reusability

This commit is contained in:
2026-05-21 16:26:31 +02:00
parent 49fc33edc0
commit 28f4ede4aa
3 changed files with 518 additions and 458 deletions
+392 -288
View File
@@ -316,12 +316,6 @@ class _HomeScreenState extends State<HomeScreen> {
final Widget body = _isLoading
? const Center(child: CircularProgressIndicator())
: visibleNotes.isEmpty
? _EmptyState(
showDeletedNotes: _showDeletedNotes,
categoryName: currentCategory?.name,
searchQuery: _searchQuery,
)
: RefreshIndicator(
onRefresh: () async {
await widget.onRequestSync();
@@ -330,295 +324,405 @@ class _HomeScreenState extends State<HomeScreen> {
cursor: _isDragging
? SystemMouseCursors.grabbing
: SystemMouseCursors.basic,
child: MasonryGridView.count(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
itemCount: visibleNotes.length,
itemBuilder: (BuildContext context, int index) {
final List<Note> filteredNotes = visibleNotes;
return DragTarget<int>(
onAcceptWithDetails: (DragTargetDetails<int> details) {
final Note targetNote = filteredNotes[index];
final int originalTargetIndex = _notes.indexOf(
targetNote,
);
_reorderNote(details.data, originalTargetIndex);
},
builder: (context, candidateData, rejectedData) {
return LayoutBuilder(
builder: (context, constraints) {
final double cellWidth = constraints.maxWidth;
final bool requiresLongPressToDrag =
_requiresLongPressToDrag(_lastPointerKind);
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: visibleNotes.isEmpty
? [
SliverFillRemaining(
hasScrollBody: false,
child: _EmptyState(
showDeletedNotes: _showDeletedNotes,
categoryName: currentCategory?.name,
searchQuery: _searchQuery,
),
),
]
: [
SliverPadding(
padding: const EdgeInsets.only(bottom: 8),
sliver: SliverMasonryGrid.count(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childCount: visibleNotes.length,
itemBuilder: (BuildContext context, int index) {
final List<Note> filteredNotes = visibleNotes;
return DragTarget<int>(
onAcceptWithDetails:
(DragTargetDetails<int> details) {
final Note targetNote =
filteredNotes[index];
final int originalTargetIndex = _notes
.indexOf(targetNote);
_reorderNote(
details.data,
originalTargetIndex,
);
},
builder: (context, candidateData, rejectedData) {
return LayoutBuilder(
builder: (context, constraints) {
final double cellWidth =
constraints.maxWidth;
final bool requiresLongPressToDrag =
_requiresLongPressToDrag(
_lastPointerKind,
);
final Widget draggableNote = requiresLongPressToDrag
? LongPressDraggable<int>(
data: _notes.indexOf(filteredNotes[index]),
delay: const Duration(milliseconds: 280),
onDragStarted: () {
if (!mounted) return;
setState(() {
_isDragging = true;
});
},
onDragEnd: (_) {
if (!mounted) return;
setState(() {
_isDragging = false;
});
},
onDraggableCanceled: (_, _) {
if (!mounted) return;
setState(() {
_isDragging = false;
});
},
feedback: MouseRegion(
cursor: SystemMouseCursors.grabbing,
child: Material(
color: Colors.transparent,
elevation: 8,
child: SizedBox(
width: cellWidth,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0.97, end: 1.0),
duration: const Duration(
milliseconds: 180,
),
curve: Curves.easeOutCubic,
builder: (context, scale, child) {
return Transform.scale(
scale: scale,
alignment: Alignment.topLeft,
child: child,
);
},
child: Opacity(
opacity: 0.95,
child: NoteCard(
note: filteredNotes[index],
onTap: () {},
isDragging: true,
),
),
),
),
),
),
childWhenDragging: MouseRegion(
cursor: SystemMouseCursors.grabbing,
child: Opacity(
opacity: 0.3,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(
24,
25,
26,
1,
),
borderRadius: BorderRadius.circular(
12,
),
border: Border.all(
color: Colors.white24,
width: 1,
),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
filteredNotes[index].title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
final Widget draggableNote =
requiresLongPressToDrag
? LongPressDraggable<int>(
data: _notes.indexOf(
filteredNotes[index],
),
maxLines: 2,
),
const SizedBox(height: 8),
Text(
filteredNotes[index].body,
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
delay: const Duration(
milliseconds: 280,
),
onDragStarted: () {
if (!mounted) return;
setState(() {
_isDragging = true;
});
},
onDragEnd: (_) {
if (!mounted) return;
setState(() {
_isDragging = false;
});
},
onDraggableCanceled: (_, _) {
if (!mounted) return;
setState(() {
_isDragging = false;
});
},
feedback: MouseRegion(
cursor:
SystemMouseCursors.grabbing,
child: Material(
color: Colors.transparent,
elevation: 8,
child: SizedBox(
width: cellWidth,
child: TweenAnimationBuilder<double>(
tween: Tween(
begin: 0.97,
end: 1.0,
),
duration: const Duration(
milliseconds: 180,
),
curve:
Curves.easeOutCubic,
builder:
(
context,
scale,
child,
) {
return Transform.scale(
scale: scale,
alignment:
Alignment
.topLeft,
child: child,
);
},
child: Opacity(
opacity: 0.95,
child: NoteCard(
note:
filteredNotes[index],
onTap: () {},
isDragging: true,
),
),
),
),
),
),
childWhenDragging: MouseRegion(
cursor:
SystemMouseCursors.grabbing,
child: Opacity(
opacity: 0.3,
child: Container(
padding:
const EdgeInsets.all(
16,
),
decoration: BoxDecoration(
color:
const Color.fromRGBO(
24,
25,
26,
1,
),
borderRadius:
BorderRadius.circular(
12,
),
border: Border.all(
color: Colors.white24,
width: 1,
),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
mainAxisSize:
MainAxisSize.min,
children: [
Text(
filteredNotes[index]
.title,
style:
const TextStyle(
color: Colors
.white,
fontSize: 16,
fontWeight:
FontWeight
.bold,
),
maxLines: 2,
),
const SizedBox(
height: 8,
),
Text(
filteredNotes[index]
.body,
style:
const TextStyle(
color: Colors
.white70,
fontSize: 14,
),
maxLines: 20,
overflow:
TextOverflow.clip,
),
],
),
),
),
),
child: Container(
decoration: BoxDecoration(
border:
candidateData.isNotEmpty
? Border.all(
color: Colors
.blue
.shade400,
width: 2,
)
: null,
borderRadius:
BorderRadius.circular(12),
),
child: NoteCard(
key: ValueKey<String>(
filteredNotes[index].id,
),
note: filteredNotes[index],
onTap: () => _openNoteEditor(
filteredNotes[index],
),
isDragging: _isDragging,
),
),
maxLines: 20,
overflow: TextOverflow.clip,
),
],
),
),
),
),
child: Container(
decoration: BoxDecoration(
border: candidateData.isNotEmpty
? Border.all(
color: Colors.blue.shade400,
width: 2,
)
: null,
borderRadius: BorderRadius.circular(12),
),
child: NoteCard(
key: ValueKey<String>(
filteredNotes[index].id,
),
note: filteredNotes[index],
onTap: () =>
_openNoteEditor(filteredNotes[index]),
isDragging: _isDragging,
),
),
)
: Draggable<int>(
data: _notes.indexOf(filteredNotes[index]),
onDragStarted: () {
if (!mounted) return;
setState(() {
_isDragging = true;
});
},
onDragEnd: (_) {
if (!mounted) return;
setState(() {
_isDragging = false;
});
},
onDraggableCanceled: (_, _) {
if (!mounted) return;
setState(() {
_isDragging = false;
});
},
feedback: MouseRegion(
cursor: SystemMouseCursors.grabbing,
child: Material(
color: Colors.transparent,
elevation: 8,
child: SizedBox(
width: cellWidth,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0.97, end: 1.0),
duration: const Duration(
milliseconds: 180,
),
curve: Curves.easeOutCubic,
builder: (context, scale, child) {
return Transform.scale(
scale: scale,
alignment: Alignment.topLeft,
child: child,
: Draggable<int>(
data: _notes.indexOf(
filteredNotes[index],
),
onDragStarted: () {
if (!mounted) return;
setState(() {
_isDragging = true;
});
},
onDragEnd: (_) {
if (!mounted) return;
setState(() {
_isDragging = false;
});
},
onDraggableCanceled: (_, _) {
if (!mounted) return;
setState(() {
_isDragging = false;
});
},
feedback: MouseRegion(
cursor:
SystemMouseCursors.grabbing,
child: Material(
color: Colors.transparent,
elevation: 8,
child: SizedBox(
width: cellWidth,
child: TweenAnimationBuilder<double>(
tween: Tween(
begin: 0.97,
end: 1.0,
),
duration: const Duration(
milliseconds: 180,
),
curve:
Curves.easeOutCubic,
builder:
(
context,
scale,
child,
) {
return Transform.scale(
scale: scale,
alignment:
Alignment
.topLeft,
child: child,
);
},
child: Opacity(
opacity: 0.95,
child: NoteCard(
note:
filteredNotes[index],
onTap: () {},
isDragging: true,
),
),
),
),
),
),
childWhenDragging: MouseRegion(
cursor:
SystemMouseCursors.grabbing,
child: Opacity(
opacity: 0.3,
child: Container(
padding:
const EdgeInsets.all(
16,
),
decoration: BoxDecoration(
color:
const Color.fromRGBO(
24,
25,
26,
1,
),
borderRadius:
BorderRadius.circular(
12,
),
border: Border.all(
color: Colors.white24,
width: 1,
),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
mainAxisSize:
MainAxisSize.min,
children: [
Text(
filteredNotes[index]
.title,
style:
const TextStyle(
color: Colors
.white,
fontSize: 16,
fontWeight:
FontWeight
.bold,
),
maxLines: 2,
overflow: TextOverflow
.ellipsis,
),
const SizedBox(
height: 8,
),
Text(
filteredNotes[index]
.body,
style:
const TextStyle(
color: Colors
.white70,
fontSize: 14,
),
maxLines: 20,
overflow:
TextOverflow.clip,
),
],
),
),
),
),
child: Container(
decoration: BoxDecoration(
border:
candidateData.isNotEmpty
? Border.all(
color: Colors
.blue
.shade400,
width: 2,
)
: null,
borderRadius:
BorderRadius.circular(12),
),
child: NoteCard(
key: ValueKey<String>(
filteredNotes[index].id,
),
note: filteredNotes[index],
onTap: () => _openNoteEditor(
filteredNotes[index],
),
isDragging: _isDragging,
),
),
);
},
child: Opacity(
opacity: 0.95,
child: NoteCard(
note: filteredNotes[index],
onTap: () {},
isDragging: true,
),
),
),
),
),
),
childWhenDragging: MouseRegion(
cursor: SystemMouseCursors.grabbing,
child: Opacity(
opacity: 0.3,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color.fromRGBO(
24,
25,
26,
1,
),
borderRadius: BorderRadius.circular(
12,
),
border: Border.all(
color: Colors.white24,
width: 1,
),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
filteredNotes[index].title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
filteredNotes[index].body,
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
maxLines: 20,
overflow: TextOverflow.clip,
),
],
),
),
),
),
child: Container(
decoration: BoxDecoration(
border: candidateData.isNotEmpty
? Border.all(
color: Colors.blue.shade400,
width: 2,
)
: null,
borderRadius: BorderRadius.circular(12),
),
child: NoteCard(
key: ValueKey<String>(
filteredNotes[index].id,
),
note: filteredNotes[index],
onTap: () =>
_openNoteEditor(filteredNotes[index]),
isDragging: _isDragging,
),
),
);
return Listener(
onPointerDown: (PointerDownEvent event) {
if (_lastPointerKind == event.kind) {
return;
}
return Listener(
onPointerDown:
(PointerDownEvent event) {
if (_lastPointerKind ==
event.kind) {
return;
}
setState(() {
_lastPointerKind = event.kind;
});
setState(() {
_lastPointerKind = event.kind;
});
},
child: draggableNote,
);
},
);
},
);
},
child: draggableNote,
);
},
);
},
);
},
),
),
],
),
),
);
@@ -904,7 +1008,7 @@ class _CategoryDialogState extends State<_CategoryDialog> {
await widget.onRequestSync();
} catch (_) {}
if (mounted) {
await widget.onCategoryDeleted();
await widget.onCategoryDeleted();
Navigator.pop(context);
}
} catch (e) {
+87 -154
View File
@@ -28,39 +28,44 @@ class NoteEditorScreen extends StatefulWidget {
@override
State<NoteEditorScreen> createState() => _NoteEditorScreenState();
static Future<dynamic> _showGeneralEditorDialog(
BuildContext context, {
Note? note,
String? categoryId,
}) {
return showGeneralDialog<dynamic>(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (context, animation, secondaryAnimation) {
return NoteEditorScreen(note: note, categoryId: categoryId);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(scale: animation, child: child);
},
);
}
static Future<dynamic> showDialog(
BuildContext context, {
Note? note,
String? categoryId,
}) {
if (isAndroid || isIOS) {
return showGeneralDialog<dynamic>(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (context, animation, secondaryAnimation) {
return NoteEditorScreen(note: note, categoryId: categoryId);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(scale: animation, child: child);
},
return _showGeneralEditorDialog(
context,
note: note,
categoryId: categoryId,
);
}
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, categoryId: categoryId);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(scale: animation, child: child);
},
return _showGeneralEditorDialog(
context,
note: note,
categoryId: categoryId,
);
}
@@ -161,43 +166,52 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
_complete(updatedNote);
}
Future<bool> _showDeleteConfirmation() async {
Widget _buildDeleteConfirmationDialog({
required ValueChanged<bool> 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<bool> _showDeleteConfirmation() async {
if (_isMobileLayout) {
final bool? confirmed = await showDialog<bool>(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
builder: (BuildContext dialogContext) {
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: () => Navigator.of(dialogContext).pop(false),
child: const Text(
'Cancelar',
style: TextStyle(color: Colors.white70),
),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(
isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar',
style: const TextStyle(color: Colors.red),
),
),
],
return _buildDeleteConfirmationDialog(
onConfirmed: (bool confirmed) =>
Navigator.of(dialogContext).pop(confirmed),
);
},
);
@@ -212,34 +226,9 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
barrierDismissible: false,
barrierColor: Colors.transparent,
builder: (BuildContext dialogContext) {
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: () => Navigator.of(dialogContext).pop(false),
child: const Text(
'Cancelar',
style: TextStyle(color: Colors.white70),
),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(
isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar',
style: const TextStyle(color: Colors.red),
),
),
],
return _buildDeleteConfirmationDialog(
onConfirmed: (bool confirmed) =>
Navigator.of(dialogContext).pop(confirmed),
);
},
);
@@ -251,18 +240,18 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
late final OverlayEntry entry;
bool didRemove = false;
void close(bool confirmed) {
if (!completer.isCompleted) {
completer.complete(confirmed);
}
if (!didRemove && entry.mounted) {
didRemove = true;
entry.remove();
}
}
entry = OverlayEntry(
builder: (BuildContext overlayContext) {
final ValueChanged<bool> close = (bool confirmed) {
if (!completer.isCompleted) {
completer.complete(confirmed);
}
if (!didRemove && entry.mounted) {
didRemove = true;
entry.remove();
}
};
return Material(
color: Colors.transparent,
child: Stack(
@@ -276,39 +265,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: 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: () => close(false),
child: const Text(
'Cancelar',
style: TextStyle(color: Colors.white70),
),
),
TextButton(
onPressed: () => close(true),
child: Text(
isDeletedNote
? 'Eliminar permanentemente'
: 'Eliminar',
style: const TextStyle(color: Colors.red),
),
),
],
),
child: _buildDeleteConfirmationDialog(onConfirmed: close),
),
),
],
@@ -351,26 +308,15 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
return Column(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.white12,
width: 1,
),
),
border: Border(bottom: BorderSide(color: Colors.white12, width: 1)),
),
child: Row(
children: [
IconButton(
onPressed: _closeWithoutSaving,
icon: const Icon(
Icons.close,
color: Colors.white70,
),
icon: const Icon(Icons.close, color: Colors.white70),
tooltip: 'Cerrar sin guardar',
),
const SizedBox(width: 8),
@@ -434,9 +380,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
),
decoration: const InputDecoration(
hintText: 'Escribe tu nota...',
hintStyle: TextStyle(
color: Colors.white30,
),
hintStyle: TextStyle(color: Colors.white30),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
@@ -447,14 +391,9 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.white12, width: 1),
),
border: Border(top: BorderSide(color: Colors.white12, width: 1)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -462,18 +401,12 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
if (!_isNewNote)
IconButton(
onPressed: _deleteNote,
icon: const Icon(
Icons.delete_outline,
color: Colors.red,
),
icon: const Icon(Icons.delete_outline, color: Colors.red),
tooltip: 'Eliminar nota',
)
else
const SizedBox(width: 48),
FilledButton(
onPressed: _saveNote,
child: const Text('Guardar'),
),
FilledButton(onPressed: _saveNote, child: const Text('Guardar')),
],
),
),