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,230 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:notas/theme/app_palette.dart';
|
||||
import 'package:notas/widgets/category_style.dart';
|
||||
|
||||
class CategoryDraft {
|
||||
const CategoryDraft({
|
||||
required this.name,
|
||||
required this.colorValue,
|
||||
required this.iconCodePoint,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final int colorValue;
|
||||
final int iconCodePoint;
|
||||
}
|
||||
|
||||
Future<CategoryDraft?> showCategoryEditorDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String confirmLabel,
|
||||
String? initialName,
|
||||
int? initialColorValue,
|
||||
int? initialIconCodePoint,
|
||||
}) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: initialName ?? '',
|
||||
);
|
||||
final List<Color> colorOptions = CategoryStyle.colorsOf(context);
|
||||
final List<IconData> iconOptions = CategoryStyle.icons;
|
||||
final int fallbackColorValue =
|
||||
initialColorValue ?? colorOptions.first.toARGB32();
|
||||
final int fallbackIconCodePoint =
|
||||
initialIconCodePoint ?? iconOptions.first.codePoint;
|
||||
|
||||
try {
|
||||
final CategoryDraft? result = await showDialog<CategoryDraft>(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
final AppPalette palette = Theme.of(dialogContext).extension<AppPalette>() ??
|
||||
AppPalette.fromBrightness(Theme.of(dialogContext).brightness);
|
||||
final List<Color> dialogColorOptions = CategoryStyle.colorsOf(
|
||||
dialogContext,
|
||||
);
|
||||
final List<IconData> dialogIconOptions = CategoryStyle.icons;
|
||||
|
||||
int selectedColorValue = fallbackColorValue;
|
||||
int selectedIconCodePoint = fallbackIconCodePoint;
|
||||
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setDialogState) {
|
||||
final Color previewColor = Color(selectedColorValue);
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: palette.surfaceElevated,
|
||||
title: Text(title),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Nombre de la categoría',
|
||||
),
|
||||
onSubmitted: (String value) {
|
||||
final String name = value.trim();
|
||||
if (name.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(dialogContext).pop(
|
||||
CategoryDraft(
|
||||
name: name,
|
||||
colorValue: selectedColorValue,
|
||||
iconCodePoint: selectedIconCodePoint,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Color',
|
||||
style: TextStyle(
|
||||
color: palette.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
for (final Color color in dialogColorOptions)
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
onTap: () {
|
||||
setDialogState(() {
|
||||
selectedColorValue = color.toARGB32();
|
||||
});
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
border: Border.all(
|
||||
color: selectedColorValue == color.toARGB32()
|
||||
? palette.textPrimary
|
||||
: palette.border,
|
||||
width: selectedColorValue == color.toARGB32()
|
||||
? 2.5
|
||||
: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.0),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: selectedColorValue == color.toARGB32()
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 18,
|
||||
color: color.computeLuminance() > 0.5
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Icono',
|
||||
style: TextStyle(
|
||||
color: palette.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final IconData icon in dialogIconOptions)
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: () {
|
||||
setDialogState(() {
|
||||
selectedIconCodePoint = icon.codePoint;
|
||||
});
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedIconCodePoint == icon.codePoint
|
||||
? previewColor.withValues(alpha: 0.14)
|
||||
: palette.fill,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: selectedIconCodePoint == icon.codePoint
|
||||
? previewColor
|
||||
: palette.border,
|
||||
width: selectedIconCodePoint == icon.codePoint
|
||||
? 2
|
||||
: 1,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: selectedIconCodePoint == icon.codePoint
|
||||
? previewColor
|
||||
: palette.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final String name = controller.text.trim();
|
||||
if (name.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(dialogContext).pop(
|
||||
CategoryDraft(
|
||||
name: name,
|
||||
colorValue: selectedColorValue,
|
||||
iconCodePoint: selectedIconCodePoint,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(confirmLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == null || result.name.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
controller.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user