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 {
|
||||
final TextEditingController controller = TextEditingController(text: category?.name ?? '');
|
||||
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>(
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext ctx, StateSetter setState) {
|
||||
return AlertDialog(
|
||||
title: Text(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 (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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
return _CategoryDialog(
|
||||
category: category,
|
||||
repository: widget.repository,
|
||||
onCategoriesChanged: _loadCategories,
|
||||
onRequestSync: widget.onRequestSync,
|
||||
onCategoryDeleted: () => _handleMenuItemTapped('all_notes'),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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
|
||||
@@ -919,7 +727,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
? 'deleted_notes'
|
||||
: 'all_notes'),
|
||||
categories: _categories,
|
||||
onEditCategory: (Category c) => _showCreateCategoryDialog(c),
|
||||
onEditCategory: (Category c) =>
|
||||
_showCreateCategoryDialog(c),
|
||||
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