817851cec3
- 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.
230 lines
8.9 KiB
Dart
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();
|
|
}
|
|
} |