refactor: Remove category handling from note editor and simplify note card options
This commit is contained in:
+266
-71
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@@ -40,6 +42,7 @@ class HomeScreen extends StatefulWidget {
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
static const double _desktopBreakpoint = 900;
|
||||
static const String _createCategoryMenuValue = '__create_category__';
|
||||
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final GlobalKey _filterButtonKey = GlobalKey();
|
||||
@@ -182,6 +185,190 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
return 'Última sincronización: ${DateFormat('dd/MM/yyyy HH:mm').format(timestamp)}';
|
||||
}
|
||||
|
||||
Future<String?> _promptCategoryName({
|
||||
required String title,
|
||||
required String confirmLabel,
|
||||
String? initialValue,
|
||||
}) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: initialValue ?? '',
|
||||
);
|
||||
|
||||
try {
|
||||
final String? result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
final AppPalette palette = _paletteOf(dialogContext);
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: palette.surfaceElevated,
|
||||
title: Text(title),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Nombre de la categoría',
|
||||
),
|
||||
onSubmitted: (String value) {
|
||||
Navigator.of(dialogContext).pop(value.trim());
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop(controller.text.trim());
|
||||
},
|
||||
child: Text(confirmLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final String name = (result ?? '').trim();
|
||||
if (name.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return name;
|
||||
} finally {
|
||||
controller.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Category?> _saveCategory({Category? existingCategory}) async {
|
||||
final String? categoryName = await _promptCategoryName(
|
||||
title: existingCategory == null ? 'Crear categoría' : 'Editar categoría',
|
||||
confirmLabel: existingCategory == null ? 'Crear' : 'Guardar',
|
||||
initialValue: existingCategory?.name,
|
||||
);
|
||||
|
||||
if (categoryName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final Category category = existingCategory == null
|
||||
? Category(name: categoryName, updatedAt: now)
|
||||
: existingCategory.copyWith(
|
||||
name: categoryName,
|
||||
updatedAt: now,
|
||||
isDirty: true,
|
||||
);
|
||||
|
||||
await widget.repository.createCategory(category);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_categories = <Category>[
|
||||
for (final Category item in _categories)
|
||||
if (item.id == category.id) category else item,
|
||||
];
|
||||
|
||||
if (!_categories.any((Category item) => item.id == category.id)) {
|
||||
_categories = <Category>[..._categories, category];
|
||||
}
|
||||
});
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
Future<void> _updateNoteCategory(Note note, String? categoryId) async {
|
||||
try {
|
||||
final Note updated = await widget.repository.updateNote(
|
||||
note.copyWith(
|
||||
categoryId: categoryId,
|
||||
updatedAt: DateTime.now(),
|
||||
isDirty: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_notes = <Note>[
|
||||
for (final Note item in _notes)
|
||||
if (item.id == updated.id) updated else item,
|
||||
];
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('No se pudo cambiar la categoría: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteNoteAfterConfirmation(Note note) async {
|
||||
final bool? confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
final AppPalette palette = _paletteOf(dialogContext);
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: palette.surfaceElevated,
|
||||
title: const Text('Eliminar nota'),
|
||||
content: const Text(
|
||||
'Esta acción eliminará la nota. ¿Quieres continuar?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Confirmar'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await widget.repository.deleteNote(note);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_notes = _notes.where((Note item) => item.id != note.id).toList();
|
||||
if (_selectedNoteId == note.id) {
|
||||
_selectedNoteId = null;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('No se pudo eliminar la nota: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RelativeRect _menuRectFromContext(BuildContext anchorContext) {
|
||||
final RenderBox button = anchorContext.findRenderObject()! as RenderBox;
|
||||
final RenderBox overlay =
|
||||
@@ -224,7 +411,28 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
for (final Category category in _categories)
|
||||
PopupMenuItem<String?>(
|
||||
value: category.id,
|
||||
child: Text(category.name),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(category.name)),
|
||||
const SizedBox(width: 8),
|
||||
Builder(
|
||||
builder: (BuildContext menuContext) {
|
||||
return IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
icon: const Icon(Icons.more_vert, size: 18),
|
||||
onPressed: () {
|
||||
Navigator.of(menuContext).pop();
|
||||
unawaited(_saveCategory(existingCategory: category));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -248,80 +456,67 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
BuildContext anchorContext,
|
||||
Note note,
|
||||
) async {
|
||||
final Category? selected = await _showAnchoredCategoryMenu<Category?>(
|
||||
final String? selectedCategoryId = await _showAnchoredCategoryMenu<String?>(
|
||||
anchorContext: anchorContext,
|
||||
items: <PopupMenuEntry<Category?>>[
|
||||
const PopupMenuItem<Category?>(
|
||||
value: null,
|
||||
child: Text('Sin categoría'),
|
||||
),
|
||||
items: <PopupMenuEntry<String?>>[
|
||||
const PopupMenuItem<String?>(value: '', child: Text('Sin categoría')),
|
||||
const PopupMenuDivider(),
|
||||
for (final Category category in _categories)
|
||||
PopupMenuItem<Category?>(value: category, child: Text(category.name)),
|
||||
PopupMenuItem<String?>(
|
||||
value: category.id,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(category.name)),
|
||||
const SizedBox(width: 8),
|
||||
Builder(
|
||||
builder: (BuildContext menuContext) {
|
||||
return IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
icon: const Icon(Icons.more_vert, size: 18),
|
||||
onPressed: () {
|
||||
Navigator.of(menuContext).pop();
|
||||
unawaited(_saveCategory(existingCategory: category));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem<String?>(
|
||||
value: _createCategoryMenuValue,
|
||||
child: Text('Crear categoría'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
if (!mounted || selectedCategoryId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String? categoryId = selected?.id;
|
||||
if (selectedCategoryId == _createCategoryMenuValue) {
|
||||
final Category? createdCategory = await _saveCategory();
|
||||
if (createdCategory == null || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _updateNoteCategory(note, createdCategory.id);
|
||||
return;
|
||||
}
|
||||
|
||||
final String? categoryId = selectedCategoryId.isEmpty
|
||||
? null
|
||||
: selectedCategoryId;
|
||||
if (categoryId == note.categoryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final Note updated = await widget.repository.updateNote(
|
||||
note.copyWith(
|
||||
categoryId: categoryId,
|
||||
updatedAt: DateTime.now(),
|
||||
isDirty: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_notes = <Note>[
|
||||
for (final Note item in _notes)
|
||||
if (item.id == updated.id) updated else item,
|
||||
];
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('No se pudo cambiar la categoría: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteNote(Note note) async {
|
||||
try {
|
||||
await widget.repository.deleteNote(note);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_notes = _notes.where((Note item) => item.id != note.id).toList();
|
||||
if (_selectedNoteId == note.id) {
|
||||
_selectedNoteId = null;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('No se pudo eliminar la nota: $error')),
|
||||
);
|
||||
}
|
||||
await _updateNoteCategory(note, categoryId);
|
||||
}
|
||||
|
||||
Future<void> _createNote({required bool openEditor}) async {
|
||||
@@ -387,7 +582,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
key: ValueKey<String>(note.id),
|
||||
repository: widget.repository,
|
||||
note: note,
|
||||
categories: _categories,
|
||||
embedded: embedded,
|
||||
onSaved: (Note saved) {
|
||||
if (!mounted) {
|
||||
@@ -578,22 +772,24 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
await _loadData(keepSelection: true);
|
||||
},
|
||||
child: ReorderableListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 20),
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 10, 14),
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: visibleNotes.length,
|
||||
onReorder: _handleReorder,
|
||||
footer: Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 88),
|
||||
child: Text(
|
||||
_formatLastSyncAt(),
|
||||
style: TextStyle(color: palette.textSecondary, fontSize: 12),
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 72),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_formatLastSyncAt(),
|
||||
style: TextStyle(color: palette.textSecondary, fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final Note note = visibleNotes[index];
|
||||
return Padding(
|
||||
key: ValueKey<String>(note.id),
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: ReorderableDelayedDragStartListener(
|
||||
index: index,
|
||||
child: NoteCard(
|
||||
@@ -601,7 +797,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
isSelected: note.id == _selectedNoteId,
|
||||
showSelectionBorder: isDesktop,
|
||||
onTap: () => _handleNoteTap(note, isDesktop),
|
||||
onDelete: () => _deleteNote(note),
|
||||
onDelete: () => _deleteNoteAfterConfirmation(note),
|
||||
onChangeCategory: (BuildContext buttonContext) =>
|
||||
_changeNoteCategory(buttonContext, note),
|
||||
),
|
||||
@@ -682,7 +878,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
key: ValueKey<String>(selectedNote.id),
|
||||
repository: widget.repository,
|
||||
note: selectedNote,
|
||||
categories: _categories,
|
||||
embedded: true,
|
||||
onSaved: (Note saved) {
|
||||
if (!mounted) {
|
||||
|
||||
Reference in New Issue
Block a user