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.
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user