feat: Add color and icon properties to categories, enhance category management in UI
This commit is contained in:
+264
-14
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user