Files
notas/lib/widgets/category_editor_dialog.dart
T
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

230 lines
8.9 KiB
Dart

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();
}
}