Files
notas/lib/widgets/category_menu_button.dart
Marcos 817851cec3 feat: Enhance SearchAppBar with debounce and customizable padding
- Added debounce functionality for search input in SearchAppBar to improve performance.
- Introduced a controller parameter to allow external control of the search input.
- Made padding customizable via a new padding parameter.
- Improved leading button handling with an option to show/hide it.

feat: Implement Category Editor Dialog

- Created a new CategoryEditorDialog widget for editing category details.
- Allows users to set category name, color, and icon with a preview.
- Supports creating new categories with a confirmation dialog.

feat: Add Category Menu Button

- Introduced CategoryMenuButton for selecting categories with an option to create new ones.
- Displays a menu with existing categories and allows editing.
- Supports dynamic background color based on active state.

feat: Create Note List View

- Added NoteListView widget for displaying a list of notes with reordering capability.
- Integrates refresh functionality and displays last sync time.
- Supports selection and deletion of notes with visual feedback.
2026-07-11 15:01:09 +02:00

244 lines
7.5 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:notas/models/category.dart';
import 'package:notas/theme/app_palette.dart';
import 'package:notas/widgets/category_style.dart';
const String kCreateCategoryMenuValue = '__create_category__';
class CategoryMenuButton extends StatelessWidget {
const CategoryMenuButton({
super.key,
required this.categories,
required this.selectedCategoryId,
required this.onCategorySelected,
this.onCreateCategory,
this.onEditCategory,
this.icon = Icons.filter_alt_outlined,
this.tooltip = 'Filtrar por categorías',
this.isActive = false,
this.allowCreateCategory = false,
this.emptyLabel = 'Todas las categorías',
this.createLabel = 'Crear categoría',
this.backgroundColor,
});
final List<Category> categories;
final String? selectedCategoryId;
final ValueChanged<String?> onCategorySelected;
final FutureOr<void> Function()? onCreateCategory;
final FutureOr<void> Function(Category category)? onEditCategory;
final IconData icon;
final String tooltip;
final bool isActive;
final bool allowCreateCategory;
final String emptyLabel;
final String createLabel;
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
final AppPalette palette = Theme.of(context).extension<AppPalette>() ??
AppPalette.fromBrightness(Theme.of(context).brightness);
return IconButton(
onPressed: () async {
final String? selectedValue = await showCategorySelectionMenu(
anchorContext: context,
categories: categories,
selectedCategoryId: selectedCategoryId,
allowCreateCategory: allowCreateCategory,
emptyLabel: emptyLabel,
createLabel: createLabel,
onEditCategory: onEditCategory,
);
if (selectedValue == null) {
return;
}
if (selectedValue == kCreateCategoryMenuValue) {
await onCreateCategory?.call();
return;
}
onCategorySelected(selectedValue.isEmpty ? null : selectedValue);
},
tooltip: tooltip,
iconSize: 24,
style: IconButton.styleFrom(
backgroundColor: backgroundColor ??
(isActive ? palette.accent.withValues(alpha: 0.08) : Colors.transparent),
shape: const CircleBorder(),
),
icon: Stack(
clipBehavior: Clip.none,
children: [
Icon(icon, color: palette.textSecondary),
if (isActive)
Positioned(
right: -1,
top: -1,
child: Container(
width: 7,
height: 7,
decoration: BoxDecoration(
color: palette.accent,
shape: BoxShape.circle,
),
),
),
],
),
);
}
}
Future<String?> showCategorySelectionMenu({
required BuildContext anchorContext,
required List<Category> categories,
required String? selectedCategoryId,
required bool allowCreateCategory,
required String emptyLabel,
required String createLabel,
FutureOr<void> Function(Category category)? onEditCategory,
}) {
return showMenu<String?>(
context: anchorContext,
position: _menuRectFromContext(anchorContext),
elevation: 10,
color: _paletteOf(anchorContext).surfaceElevated,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
items: <PopupMenuEntry<String?>>[
PopupMenuItem<String?>(
value: '',
child: _buildCategoryMenuItem(
context: anchorContext,
label: emptyLabel,
icon: allowCreateCategory
? Icons.folder_outlined
: Icons.filter_alt_outlined,
color: _paletteOf(anchorContext).textSecondary,
selected: selectedCategoryId == null,
),
),
const PopupMenuDivider(),
for (final Category category in categories)
PopupMenuItem<String?>(
value: category.id,
child: Builder(
builder: (BuildContext menuContext) {
final Color categoryColor = Color(
category.colorValue ?? _paletteOf(menuContext).accent.toARGB32(),
);
return _buildCategoryMenuItem(
context: menuContext,
label: category.name,
icon: CategoryStyle.iconForCodePoint(category.iconCodePoint),
color: categoryColor,
selected: selectedCategoryId == category.id,
onEditPressed: onEditCategory == null
? null
: () {
Navigator.of(menuContext).pop();
final FutureOr<void> result = onEditCategory(category);
if (result is Future<void>) {
unawaited(result);
}
},
);
},
),
),
if (allowCreateCategory) ...[
const PopupMenuDivider(),
PopupMenuItem<String?>(
value: kCreateCategoryMenuValue,
child: _buildCategoryMenuItem(
context: anchorContext,
label: createLabel,
icon: Icons.add_circle_outline,
color: _paletteOf(anchorContext).textSecondary,
selected: false,
),
),
],
],
);
}
RelativeRect _menuRectFromContext(BuildContext anchorContext) {
final RenderBox button = anchorContext.findRenderObject()! as RenderBox;
final RenderBox overlay =
Overlay.of(anchorContext).context.findRenderObject()! as RenderBox;
final Offset topLeft = button.localToGlobal(Offset.zero, ancestor: overlay);
final Offset bottomRight = button.localToGlobal(
button.size.bottomRight(Offset.zero),
ancestor: overlay,
);
return RelativeRect.fromRect(
Rect.fromLTRB(topLeft.dx, topLeft.dy, bottomRight.dx, bottomRight.dy),
Offset.zero & overlay.size,
);
}
Widget _buildCategoryMenuItem({
required BuildContext context,
required String label,
required IconData icon,
required Color color,
required bool selected,
VoidCallback? onEditPressed,
}) {
final AppPalette palette = _paletteOf(context);
return AnimatedContainer(
duration: const Duration(milliseconds: 160),
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: selected ? color.withValues(alpha: 0.05) : Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: selected ? color.withValues(alpha: 0.42) : Colors.transparent,
width: 1,
),
),
child: Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: selected ? palette.textPrimary : palette.textSecondary,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
),
),
),
const SizedBox(width: 8),
if (onEditPressed != null)
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
icon: Icon(
Icons.more_vert,
size: 18,
color: palette.textSecondary,
),
onPressed: onEditPressed,
),
],
),
);
}
AppPalette _paletteOf(BuildContext context) {
return Theme.of(context).extension<AppPalette>() ??
AppPalette.fromBrightness(Theme.of(context).brightness);
}