feat: Add color and icon properties to categories, enhance category management in UI

This commit is contained in:
2026-05-20 17:10:44 +02:00
parent def755e1c5
commit 3ff4efb738
8 changed files with 517 additions and 45 deletions
+264 -14
View File
@@ -7,6 +7,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:notas/data/note_repository.dart';
import 'package:notas/models/note.dart';
import 'package:notas/screens/note_editor_screen.dart';
import 'package:notas/models/category.dart';
import 'package:notas/widgets/menu_drawer.dart';
import 'package:notas/widgets/note_card.dart';
import 'package:notas/widgets/search_app_bar.dart';
@@ -49,6 +50,8 @@ class _HomeScreenState extends State<HomeScreen> {
bool _isMenuOpen = false;
bool _showDeletedNotes = false;
PointerDeviceKind _lastPointerKind = PointerDeviceKind.mouse;
List<Category> _categories = <Category>[];
String? _selectedCategoryId;
void _openMenu() {
if (_isMenuOpen) {
@@ -80,6 +83,34 @@ class _HomeScreenState extends State<HomeScreen> {
void initState() {
super.initState();
_loadNotes();
_loadCategories();
}
Future<void> _loadCategories() async {
try {
final List<Category> cats = await widget.repository.loadCategories();
if (!mounted) return;
setState(() {
_categories = cats;
});
} catch (e) {
debugPrint('Failed to load categories: $e');
}
}
Category? _currentCategory() {
final String? selectedCategoryId = _selectedCategoryId;
if (selectedCategoryId == null) {
return null;
}
for (final Category category in _categories) {
if (category.id == selectedCategoryId) {
return category;
}
}
return null;
}
@override
@@ -113,7 +144,10 @@ class _HomeScreenState extends State<HomeScreen> {
}
Future<void> _openNoteComposer() async {
final dynamic result = await NoteEditorScreen.showDialog(context);
final dynamic result = await NoteEditorScreen.showDialog(
context,
categoryId: _showDeletedNotes ? null : _selectedCategoryId,
);
if (result == null) {
return;
@@ -188,12 +222,20 @@ class _HomeScreenState extends State<HomeScreen> {
}
List<Note> _getFilteredNotes() {
Iterable<Note> notes = _notes;
if (_selectedCategoryId != null) {
notes = notes.where(
(Note note) => note.categoryId == _selectedCategoryId,
);
}
if (_searchQuery.isEmpty) {
return _notes;
return notes.toList();
}
final String query = _searchQuery.toLowerCase();
return _notes
return notes
.where(
(Note note) =>
note.title.toLowerCase().contains(query) ||
@@ -205,6 +247,23 @@ class _HomeScreenState extends State<HomeScreen> {
Future<void> _handleMenuItemTapped(String item) async {
_closeMenu();
if (item == 'create_category') {
await _showCreateCategoryDialog();
return;
}
if (item.startsWith('category_')) {
final String id = item.substring('category_'.length);
setState(() {
_selectedCategoryId = id;
_showDeletedNotes = false;
_searchQuery = '';
_isLoading = true;
});
await _loadNotes();
return;
}
if (item == 'settings') {
widget.onOpenSettings();
return;
@@ -213,6 +272,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (item == 'deleted_notes') {
setState(() {
_showDeletedNotes = true;
_selectedCategoryId = null;
_searchQuery = '';
_isLoading = true;
});
@@ -223,6 +283,7 @@ class _HomeScreenState extends State<HomeScreen> {
if (item == 'all_notes') {
setState(() {
_showDeletedNotes = false;
_selectedCategoryId = null;
_searchQuery = '';
_isLoading = true;
});
@@ -230,15 +291,185 @@ class _HomeScreenState extends State<HomeScreen> {
}
}
Future<void> _showCreateCategoryDialog() async {
final TextEditingController controller = TextEditingController();
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,
];
final bool? result = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext ctx, StateSetter setState) {
return AlertDialog(
title: const Text('Crear 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: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Crear'),
),
],
);
},
);
},
);
if (result != true || controller.text.trim().isEmpty) {
controller.dispose();
return;
}
try {
final Category newCategory = Category(
name: controller.text.trim(),
updatedAt: DateTime.now(),
colorValue: selectedColor?.toARGB32(),
iconCodePoint: selectedIcon?.codePoint,
);
debugPrint('Creating category: ${newCategory.name}, color: ${newCategory.colorValue}, icon: ${newCategory.iconCodePoint}');
await widget.repository.createCategory(newCategory);
await _loadCategories();
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Categoría creada')));
}
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
Widget build(BuildContext context) {
final double width = MediaQuery.of(context).size.width;
final int crossAxisCount = math.max((width / 250).floor().round(), 2);
final List<Note> visibleNotes = _getFilteredNotes();
final Category? currentCategory = _currentCategory();
final Widget body = _isLoading
? const Center(child: CircularProgressIndicator())
: _notes.isEmpty
? _EmptyState(showDeletedNotes: _showDeletedNotes)
: visibleNotes.isEmpty
? _EmptyState(
showDeletedNotes: _showDeletedNotes,
categoryName: currentCategory?.name,
searchQuery: _searchQuery,
)
: RefreshIndicator(
onRefresh: () async {
await widget.onRequestSync();
@@ -251,9 +482,9 @@ class _HomeScreenState extends State<HomeScreen> {
crossAxisCount: crossAxisCount,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
itemCount: _getFilteredNotes().length,
itemCount: visibleNotes.length,
itemBuilder: (BuildContext context, int index) {
final List<Note> filteredNotes = _getFilteredNotes();
final List<Note> filteredNotes = visibleNotes;
return DragTarget<int>(
onAcceptWithDetails: (DragTargetDetails<int> details) {
final Note targetNote = filteredNotes[index];
@@ -357,7 +588,6 @@ class _HomeScreenState extends State<HomeScreen> {
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
@@ -640,9 +870,13 @@ class _HomeScreenState extends State<HomeScreen> {
elevation: 8,
child: MenuDrawer(
onMenuItemTapped: _handleMenuItemTapped,
selectedItem: _showDeletedNotes
? 'deleted_notes'
: 'all_notes',
selectedItem: _selectedCategoryId != null
? 'category_${_selectedCategoryId}'
: (_showDeletedNotes
? 'deleted_notes'
: 'all_notes'),
categories: _categories,
onCreateCategory: _showCreateCategoryDialog,
),
),
),
@@ -668,9 +902,15 @@ class _HomeScreenState extends State<HomeScreen> {
}
class _EmptyState extends StatelessWidget {
const _EmptyState({required this.showDeletedNotes});
const _EmptyState({
required this.showDeletedNotes,
this.categoryName,
this.searchQuery,
});
final bool showDeletedNotes;
final String? categoryName;
final String? searchQuery;
@override
Widget build(BuildContext context) {
@@ -681,7 +921,13 @@ class _EmptyState extends StatelessWidget {
const Icon(Icons.note_add_outlined, color: Colors.white54, size: 48),
const SizedBox(height: 12),
Text(
showDeletedNotes ? 'No hay notas borradas' : 'Aún no hay notas',
searchQuery != null && searchQuery!.isNotEmpty
? 'No hay resultados'
: showDeletedNotes
? 'No hay notas borradas'
: categoryName != null
? 'No hay notas en esta categoría'
: 'Aún no hay notas',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
@@ -690,8 +936,12 @@ class _EmptyState extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
showDeletedNotes
searchQuery != null && searchQuery!.isNotEmpty
? 'Prueba a buscar con otro término o crea una nota nueva.'
: showDeletedNotes
? 'Las notas borradas aparecerán aquí para poder restaurarlas.'
: categoryName != null
? 'Pulsa el botón + para crear una nota en “$categoryName”.'
: 'Pulsa el botón + para crear la primera.',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white70),