feat: Refactor category dialog to improve UI and functionality for creating and editing categories
This commit is contained in:
+258
-200
@@ -292,210 +292,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showCreateCategoryDialog([Category? category]) async {
|
Future<void> _showCreateCategoryDialog([Category? category]) async {
|
||||||
final TextEditingController controller = TextEditingController(text: category?.name ?? '');
|
await showDialog<void>(
|
||||||
Color? selectedColor = category != null && category.colorValue != null
|
|
||||||
? Color(category.colorValue!)
|
|
||||||
: null;
|
|
||||||
IconData? selectedIcon;
|
|
||||||
|
|
||||||
final List<Color> palette = [
|
|
||||||
Colors.amber,
|
|
||||||
Colors.blue,
|
|
||||||
Colors.green,
|
|
||||||
Colors.purple,
|
|
||||||
Colors.red,
|
|
||||||
Colors.teal,
|
|
||||||
Colors.orange,
|
|
||||||
Colors.grey,
|
|
||||||
];
|
|
||||||
|
|
||||||
final List<IconData> icons = [
|
|
||||||
Icons.folder,
|
|
||||||
Icons.work,
|
|
||||||
Icons.star,
|
|
||||||
Icons.home,
|
|
||||||
Icons.school,
|
|
||||||
Icons.book,
|
|
||||||
Icons.music_note,
|
|
||||||
Icons.lightbulb,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (category != null && category.iconCodePoint != null) {
|
|
||||||
selectedIcon = icons.firstWhere(
|
|
||||||
(i) => i.codePoint == category.iconCodePoint,
|
|
||||||
orElse: () => icons.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final bool? result = await showDialog<bool>(
|
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return StatefulBuilder(
|
return _CategoryDialog(
|
||||||
builder: (BuildContext ctx, StateSetter setState) {
|
category: category,
|
||||||
return AlertDialog(
|
repository: widget.repository,
|
||||||
title: Text(category == null ? 'Crear categoría' : 'Editar categoría'),
|
onCategoriesChanged: _loadCategories,
|
||||||
content: SingleChildScrollView(
|
onRequestSync: widget.onRequestSync,
|
||||||
child: Column(
|
onCategoryDeleted: () => _handleMenuItemTapped('all_notes'),
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
hintText: 'Nombre de la categoría',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
'Color',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.white70,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
children: palette.map((Color color) {
|
|
||||||
final bool isSelected = selectedColor == color;
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => setState(() => selectedColor = color),
|
|
||||||
child: Container(
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
border: isSelected
|
|
||||||
? Border.all(color: Colors.white, width: 2)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
'Icono',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.white70,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
children: icons.map((IconData icon) {
|
|
||||||
final bool isSelected = selectedIcon == icon;
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => setState(() => selectedIcon = icon),
|
|
||||||
child: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected
|
|
||||||
? Colors.white10
|
|
||||||
: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
icon,
|
|
||||||
color: isSelected ? Colors.white : Colors.white70,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
if (category != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final bool? confirm = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('Borrar categoría'),
|
|
||||||
content: const Text('¿Seguro que quieres borrar esta categoría?'),
|
|
||||||
actions: [
|
|
||||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancelar')),
|
|
||||||
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Borrar')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirm == true) {
|
|
||||||
try {
|
|
||||||
await widget.repository.deleteCategory(category.id);
|
|
||||||
await _loadCategories();
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Categoría borrada')));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await widget.onRequestSync();
|
|
||||||
} catch (_) {}
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error al borrar categoría: $e')));
|
|
||||||
}
|
|
||||||
Navigator.pop(context, false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text('Borrar', style: TextStyle(color: Colors.red)),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
child: const Text('Cancelar'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
child: Text(category == null ? 'Crear' : 'Guardar'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != true || controller.text.trim().isEmpty) {
|
|
||||||
controller.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final Category newCategory = Category(
|
|
||||||
id: category?.id,
|
|
||||||
name: controller.text.trim(),
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
colorValue: selectedColor?.toARGB32(),
|
|
||||||
iconCodePoint: selectedIcon?.codePoint,
|
|
||||||
);
|
|
||||||
if (category == null) {
|
|
||||||
debugPrint('Creating category: ${newCategory.name}, color: ${newCategory.colorValue}, icon: ${newCategory.iconCodePoint}');
|
|
||||||
} else {
|
|
||||||
debugPrint('Updating category: ${newCategory.name}, color: ${newCategory.colorValue}, icon: ${newCategory.iconCodePoint}');
|
|
||||||
}
|
|
||||||
await widget.repository.createCategory(newCategory);
|
|
||||||
await _loadCategories();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await widget.onRequestSync();
|
|
||||||
} catch (_) {}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('ERROR creating category: $e');
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Error al crear categoría: $e')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
controller.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -919,7 +727,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
? 'deleted_notes'
|
? 'deleted_notes'
|
||||||
: 'all_notes'),
|
: 'all_notes'),
|
||||||
categories: _categories,
|
categories: _categories,
|
||||||
onEditCategory: (Category c) => _showCreateCategoryDialog(c),
|
onEditCategory: (Category c) =>
|
||||||
|
_showCreateCategoryDialog(c),
|
||||||
onCreateCategory: _showCreateCategoryDialog,
|
onCreateCategory: _showCreateCategoryDialog,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -995,3 +804,252 @@ class _EmptyState extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CategoryDialog extends StatefulWidget {
|
||||||
|
const _CategoryDialog({
|
||||||
|
required this.category,
|
||||||
|
required this.repository,
|
||||||
|
required this.onCategoriesChanged,
|
||||||
|
required this.onRequestSync,
|
||||||
|
required this.onCategoryDeleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Category? category;
|
||||||
|
final NoteRepository repository;
|
||||||
|
final Future<void> Function() onCategoriesChanged;
|
||||||
|
final Future<void> Function() onRequestSync;
|
||||||
|
final Future<void> Function() onCategoryDeleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CategoryDialog> createState() => _CategoryDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CategoryDialogState extends State<_CategoryDialog> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
Color? _selectedColor;
|
||||||
|
IconData? _selectedIcon;
|
||||||
|
|
||||||
|
final List<Color> _palette = [
|
||||||
|
Colors.amber,
|
||||||
|
Colors.blue,
|
||||||
|
Colors.green,
|
||||||
|
Colors.purple,
|
||||||
|
Colors.red,
|
||||||
|
Colors.teal,
|
||||||
|
Colors.orange,
|
||||||
|
Colors.grey,
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<IconData> _icons = [
|
||||||
|
Icons.folder,
|
||||||
|
Icons.work,
|
||||||
|
Icons.star,
|
||||||
|
Icons.home,
|
||||||
|
Icons.school,
|
||||||
|
Icons.book,
|
||||||
|
Icons.music_note,
|
||||||
|
Icons.lightbulb,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.category?.name ?? '');
|
||||||
|
_selectedColor =
|
||||||
|
widget.category != null && widget.category!.colorValue != null
|
||||||
|
? Color(widget.category!.colorValue!)
|
||||||
|
: null;
|
||||||
|
if (widget.category != null && widget.category!.iconCodePoint != null) {
|
||||||
|
_selectedIcon = _icons.firstWhere(
|
||||||
|
(IconData icon) => icon.codePoint == widget.category!.iconCodePoint,
|
||||||
|
orElse: () => _icons.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveCategory() async {
|
||||||
|
final String name = _controller.text.trim();
|
||||||
|
if (name.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Category newCategory = Category(
|
||||||
|
id: widget.category?.id,
|
||||||
|
name: name,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
colorValue: _selectedColor?.toARGB32(),
|
||||||
|
iconCodePoint: _selectedIcon?.codePoint,
|
||||||
|
);
|
||||||
|
if (widget.category == null) {
|
||||||
|
debugPrint(
|
||||||
|
'Creating category: ${newCategory.name}, color: ${newCategory.colorValue}, icon: ${newCategory.iconCodePoint}',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'Updating category: ${newCategory.name}, color: ${newCategory.colorValue}, icon: ${newCategory.iconCodePoint}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await widget.repository.createCategory(newCategory);
|
||||||
|
await widget.onCategoriesChanged();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await widget.onRequestSync();
|
||||||
|
} catch (_) {}
|
||||||
|
if (mounted) {
|
||||||
|
await widget.onCategoryDeleted();
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('ERROR creating category: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Error al crear categoría: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteCategory() async {
|
||||||
|
final bool? confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => AlertDialog(
|
||||||
|
title: const Text('Borrar categoría'),
|
||||||
|
content: const Text('¿Seguro que quieres borrar esta categoría?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: const Text('Borrar'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirm != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await widget.repository.deleteCategory(widget.category!.id);
|
||||||
|
await widget.onCategoriesChanged();
|
||||||
|
try {
|
||||||
|
await widget.onRequestSync();
|
||||||
|
} catch (_) {}
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error al borrar categoría: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(
|
||||||
|
widget.category == null ? 'Crear categoría' : 'Editar categoría',
|
||||||
|
),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Nombre de la categoría',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'Color',
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
children: _palette.map((Color color) {
|
||||||
|
final bool isSelected = _selectedColor == color;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _selectedColor = color),
|
||||||
|
child: Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: isSelected
|
||||||
|
? Border.all(color: Colors.white, width: 2)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'Icono',
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.white70),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
children: _icons.map((IconData icon) {
|
||||||
|
final bool isSelected = _selectedIcon == icon;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _selectedIcon = icon),
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? Colors.white10 : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: isSelected ? Colors.white : Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (widget.category != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: _deleteCategory,
|
||||||
|
child: const Text('Borrar', style: TextStyle(color: Colors.red)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _saveCategory,
|
||||||
|
child: Text(widget.category == null ? 'Crear' : 'Guardar'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user