refactor: Enhance category handling in note editor and card components
This commit is contained in:
+381
-93
@@ -9,9 +9,22 @@ import 'package:notas/models/category.dart';
|
|||||||
import 'package:notas/models/note.dart';
|
import 'package:notas/models/note.dart';
|
||||||
import 'package:notas/screens/note_editor_screen.dart';
|
import 'package:notas/screens/note_editor_screen.dart';
|
||||||
import 'package:notas/theme/app_palette.dart';
|
import 'package:notas/theme/app_palette.dart';
|
||||||
|
import 'package:notas/widgets/category_style.dart';
|
||||||
import 'package:notas/widgets/note_card.dart';
|
import 'package:notas/widgets/note_card.dart';
|
||||||
import 'package:notas/widgets/sync_status.dart';
|
import 'package:notas/widgets/sync_status.dart';
|
||||||
|
|
||||||
|
class _CategoryDraft {
|
||||||
|
const _CategoryDraft({
|
||||||
|
required this.name,
|
||||||
|
required this.colorValue,
|
||||||
|
required this.iconCodePoint,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final int colorValue;
|
||||||
|
final int iconCodePoint;
|
||||||
|
}
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({
|
const HomeScreen({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -176,6 +189,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Category? _categoryById(String? categoryId) {
|
||||||
|
if (categoryId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Category category in _categories) {
|
||||||
|
if (category.id == categoryId) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String _formatLastSyncAt() {
|
String _formatLastSyncAt() {
|
||||||
final DateTime? timestamp = _lastSyncAt;
|
final DateTime? timestamp = _lastSyncAt;
|
||||||
if (timestamp == null) {
|
if (timestamp == null) {
|
||||||
@@ -185,80 +212,253 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
return 'Última sincronización: ${DateFormat('dd/MM/yyyy HH:mm').format(timestamp)}';
|
return 'Última sincronización: ${DateFormat('dd/MM/yyyy HH:mm').format(timestamp)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _promptCategoryName({
|
Future<_CategoryDraft?> _promptCategoryDetails({
|
||||||
required String title,
|
required String title,
|
||||||
required String confirmLabel,
|
required String confirmLabel,
|
||||||
String? initialValue,
|
String? initialName,
|
||||||
|
int? initialColorValue,
|
||||||
|
int? initialIconCodePoint,
|
||||||
}) async {
|
}) async {
|
||||||
final TextEditingController controller = TextEditingController(
|
final TextEditingController controller = TextEditingController(
|
||||||
text: initialValue ?? '',
|
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 {
|
try {
|
||||||
final String? result = await showDialog<String>(
|
final _CategoryDraft? result = await showDialog<_CategoryDraft>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext dialogContext) {
|
builder: (BuildContext dialogContext) {
|
||||||
final AppPalette palette = _paletteOf(dialogContext);
|
final AppPalette palette = _paletteOf(dialogContext);
|
||||||
|
final List<Color> dialogColorOptions = CategoryStyle.colorsOf(
|
||||||
|
dialogContext,
|
||||||
|
);
|
||||||
|
final List<IconData> dialogIconOptions = CategoryStyle.icons;
|
||||||
|
|
||||||
return AlertDialog(
|
int selectedColorValue = fallbackColorValue;
|
||||||
backgroundColor: palette.surfaceElevated,
|
int selectedIconCodePoint = fallbackIconCodePoint;
|
||||||
title: Text(title),
|
|
||||||
content: TextField(
|
return StatefulBuilder(
|
||||||
controller: controller,
|
builder: (BuildContext context, StateSetter setDialogState) {
|
||||||
autofocus: true,
|
final Color previewColor = Color(selectedColorValue);
|
||||||
textInputAction: TextInputAction.done,
|
|
||||||
decoration: const InputDecoration(
|
return AlertDialog(
|
||||||
hintText: 'Nombre de la categoría',
|
backgroundColor: palette.surfaceElevated,
|
||||||
),
|
title: Text(title),
|
||||||
onSubmitted: (String value) {
|
content: SingleChildScrollView(
|
||||||
Navigator.of(dialogContext).pop(value.trim());
|
child: Column(
|
||||||
},
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
actions: [
|
children: [
|
||||||
TextButton(
|
TextField(
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
controller: controller,
|
||||||
child: const Text('Cancelar'),
|
autofocus: true,
|
||||||
),
|
textInputAction: TextInputAction.done,
|
||||||
FilledButton(
|
decoration: const InputDecoration(
|
||||||
onPressed: () {
|
hintText: 'Nombre de la categoría',
|
||||||
Navigator.of(dialogContext).pop(controller.text.trim());
|
),
|
||||||
},
|
onSubmitted: (String value) {
|
||||||
child: Text(confirmLabel),
|
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.withOpacity(0.18),
|
||||||
|
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.withOpacity(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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final String name = (result ?? '').trim();
|
if (result == null || result.name.trim().isEmpty) {
|
||||||
if (name.isEmpty) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return name;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Category?> _saveCategory({Category? existingCategory}) async {
|
Future<Category?> _saveCategory({Category? existingCategory}) async {
|
||||||
final String? categoryName = await _promptCategoryName(
|
final _CategoryDraft? categoryDraft = await _promptCategoryDetails(
|
||||||
title: existingCategory == null ? 'Crear categoría' : 'Editar categoría',
|
title: existingCategory == null ? 'Crear categoría' : 'Editar categoría',
|
||||||
confirmLabel: existingCategory == null ? 'Crear' : 'Guardar',
|
confirmLabel: existingCategory == null ? 'Crear' : 'Guardar',
|
||||||
initialValue: existingCategory?.name,
|
initialName: existingCategory?.name,
|
||||||
|
initialColorValue:
|
||||||
|
existingCategory?.colorValue ??
|
||||||
|
CategoryStyle.colorsOf(context).first.toARGB32(),
|
||||||
|
initialIconCodePoint:
|
||||||
|
existingCategory?.iconCodePoint ??
|
||||||
|
CategoryStyle.icons.first.codePoint,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (categoryName == null) {
|
if (categoryDraft == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final DateTime now = DateTime.now();
|
final DateTime now = DateTime.now();
|
||||||
final Category category = existingCategory == null
|
final Category category = existingCategory == null
|
||||||
? Category(name: categoryName, updatedAt: now)
|
? Category(
|
||||||
|
name: categoryDraft.name,
|
||||||
|
updatedAt: now,
|
||||||
|
colorValue: categoryDraft.colorValue,
|
||||||
|
iconCodePoint: categoryDraft.iconCodePoint,
|
||||||
|
)
|
||||||
: existingCategory.copyWith(
|
: existingCategory.copyWith(
|
||||||
name: categoryName,
|
name: categoryDraft.name,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
|
colorValue: categoryDraft.colorValue,
|
||||||
|
iconCodePoint: categoryDraft.iconCodePoint,
|
||||||
);
|
);
|
||||||
|
|
||||||
await widget.repository.createCategory(category);
|
await widget.repository.createCategory(category);
|
||||||
@@ -399,39 +599,94 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.withOpacity(0.05) : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected ? color.withOpacity(0.42) : Colors.transparent,
|
||||||
|
width: selected ? 1 : 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _openCategoryFilter(BuildContext anchorContext) async {
|
Future<void> _openCategoryFilter(BuildContext anchorContext) async {
|
||||||
final String? selectedCategoryId = await _showAnchoredCategoryMenu<String?>(
|
final String? selectedCategoryId = await _showAnchoredCategoryMenu<String?>(
|
||||||
anchorContext: anchorContext,
|
anchorContext: anchorContext,
|
||||||
items: <PopupMenuEntry<String?>>[
|
items: <PopupMenuEntry<String?>>[
|
||||||
const PopupMenuItem<String?>(
|
PopupMenuItem<String?>(
|
||||||
value: '',
|
value: '',
|
||||||
child: Text('Todas las categorías'),
|
child: _buildCategoryMenuItem(
|
||||||
|
context: anchorContext,
|
||||||
|
label: 'Todas las categorías',
|
||||||
|
icon: Icons.filter_alt_outlined,
|
||||||
|
color: _paletteOf(anchorContext).textSecondary,
|
||||||
|
selected: _selectedCategoryId == null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
for (final Category category in _categories)
|
for (final Category category in _categories)
|
||||||
PopupMenuItem<String?>(
|
PopupMenuItem<String?>(
|
||||||
value: category.id,
|
value: category.id,
|
||||||
child: Row(
|
child: Builder(
|
||||||
children: [
|
builder: (BuildContext menuContext) {
|
||||||
Expanded(child: Text(category.name)),
|
final Color categoryColor = Color(
|
||||||
const SizedBox(width: 8),
|
category.colorValue ??
|
||||||
Builder(
|
_paletteOf(menuContext).accent.toARGB32(),
|
||||||
builder: (BuildContext menuContext) {
|
);
|
||||||
return IconButton(
|
return _buildCategoryMenuItem(
|
||||||
padding: EdgeInsets.zero,
|
context: menuContext,
|
||||||
constraints: const BoxConstraints(
|
label: category.name,
|
||||||
minWidth: 32,
|
icon: CategoryStyle.iconForCodePoint(category.iconCodePoint),
|
||||||
minHeight: 32,
|
color: categoryColor,
|
||||||
),
|
selected: _selectedCategoryId == category.id,
|
||||||
icon: const Icon(Icons.more_vert, size: 18),
|
onEditPressed: () {
|
||||||
onPressed: () {
|
Navigator.of(menuContext).pop();
|
||||||
Navigator.of(menuContext).pop();
|
unawaited(_saveCategory(existingCategory: category));
|
||||||
unawaited(_saveCategory(existingCategory: category));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -459,38 +714,50 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
final String? selectedCategoryId = await _showAnchoredCategoryMenu<String?>(
|
final String? selectedCategoryId = await _showAnchoredCategoryMenu<String?>(
|
||||||
anchorContext: anchorContext,
|
anchorContext: anchorContext,
|
||||||
items: <PopupMenuEntry<String?>>[
|
items: <PopupMenuEntry<String?>>[
|
||||||
const PopupMenuItem<String?>(value: '', child: Text('Sin categoría')),
|
PopupMenuItem<String?>(
|
||||||
|
value: '',
|
||||||
|
child: _buildCategoryMenuItem(
|
||||||
|
context: anchorContext,
|
||||||
|
label: 'Sin categoría',
|
||||||
|
icon: Icons.folder_outlined,
|
||||||
|
color: _paletteOf(anchorContext).textSecondary,
|
||||||
|
selected: note.categoryId == null,
|
||||||
|
),
|
||||||
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
for (final Category category in _categories)
|
for (final Category category in _categories)
|
||||||
PopupMenuItem<String?>(
|
PopupMenuItem<String?>(
|
||||||
value: category.id,
|
value: category.id,
|
||||||
child: Row(
|
child: Builder(
|
||||||
children: [
|
builder: (BuildContext menuContext) {
|
||||||
Expanded(child: Text(category.name)),
|
final Color categoryColor = Color(
|
||||||
const SizedBox(width: 8),
|
category.colorValue ??
|
||||||
Builder(
|
_paletteOf(menuContext).accent.toARGB32(),
|
||||||
builder: (BuildContext menuContext) {
|
);
|
||||||
return IconButton(
|
return _buildCategoryMenuItem(
|
||||||
padding: EdgeInsets.zero,
|
context: menuContext,
|
||||||
constraints: const BoxConstraints(
|
label: category.name,
|
||||||
minWidth: 32,
|
icon: CategoryStyle.iconForCodePoint(category.iconCodePoint),
|
||||||
minHeight: 32,
|
color: categoryColor,
|
||||||
),
|
selected: note.categoryId == category.id,
|
||||||
icon: const Icon(Icons.more_vert, size: 18),
|
onEditPressed: () {
|
||||||
onPressed: () {
|
Navigator.of(menuContext).pop();
|
||||||
Navigator.of(menuContext).pop();
|
unawaited(_saveCategory(existingCategory: category));
|
||||||
unawaited(_saveCategory(existingCategory: category));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
const PopupMenuItem<String?>(
|
PopupMenuItem<String?>(
|
||||||
value: _createCategoryMenuValue,
|
value: _createCategoryMenuValue,
|
||||||
child: Text('Crear categoría'),
|
child: _buildCategoryMenuItem(
|
||||||
|
context: anchorContext,
|
||||||
|
label: 'Crear categoría',
|
||||||
|
icon: Icons.add_circle_outline,
|
||||||
|
color: _paletteOf(anchorContext).textSecondary,
|
||||||
|
selected: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -673,11 +940,36 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
key: _filterButtonKey,
|
key: _filterButtonKey,
|
||||||
onPressed: () => _openCategoryFilter(buttonContext),
|
onPressed: () => _openCategoryFilter(buttonContext),
|
||||||
icon: Icon(
|
|
||||||
Icons.filter_alt_outlined,
|
|
||||||
color: palette.textSecondary,
|
|
||||||
),
|
|
||||||
tooltip: 'Filtrar por categorías',
|
tooltip: 'Filtrar por categorías',
|
||||||
|
iconSize: 24,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: _selectedCategoryId == null
|
||||||
|
? Colors.transparent
|
||||||
|
: palette.accent.withOpacity(0.08),
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
),
|
||||||
|
icon: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.filter_alt_outlined,
|
||||||
|
color: palette.textSecondary,
|
||||||
|
),
|
||||||
|
if (_selectedCategoryId != null)
|
||||||
|
Positioned(
|
||||||
|
right: -1,
|
||||||
|
top: -1,
|
||||||
|
child: Container(
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: palette.accent,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -794,6 +1086,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
index: index,
|
index: index,
|
||||||
child: NoteCard(
|
child: NoteCard(
|
||||||
note: note,
|
note: note,
|
||||||
|
category: _categoryById(note.categoryId),
|
||||||
isSelected: note.id == _selectedNoteId,
|
isSelected: note.id == _selectedNoteId,
|
||||||
showSelectionBorder: isDesktop,
|
showSelectionBorder: isDesktop,
|
||||||
onTap: () => _handleNoteTap(note, isDesktop),
|
onTap: () => _handleNoteTap(note, isDesktop),
|
||||||
@@ -812,14 +1105,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
final AppPalette palette = _paletteOf(context);
|
final AppPalette palette = _paletteOf(context);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: palette.surfaceElevated,
|
|
||||||
border: Border.all(color: palette.border),
|
|
||||||
borderRadius: BorderRadius.circular(18),
|
|
||||||
),
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Selecciona una nota o crea una nueva para empezar',
|
'Selecciona una nota o\ncrea una nueva para empezar.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: palette.textSecondary,
|
color: palette.textSecondary,
|
||||||
|
|||||||
@@ -231,10 +231,27 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
return editor;
|
return editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Container(
|
||||||
backgroundColor: palette.cardBackground,
|
decoration: BoxDecoration(gradient: palette.backdropGradient),
|
||||||
appBar: AppBar(title: const Text('Editar nota')),
|
child: Scaffold(
|
||||||
body: SafeArea(child: editor),
|
backgroundColor: Colors.transparent,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Editar nota'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(color: palette.border, width: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(child: editor),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:notas/data/note_body.dart';
|
import 'package:notas/data/note_body.dart';
|
||||||
|
import 'package:notas/models/category.dart';
|
||||||
import 'package:notas/models/note.dart';
|
import 'package:notas/models/note.dart';
|
||||||
import 'package:notas/theme/app_palette.dart';
|
import 'package:notas/theme/app_palette.dart';
|
||||||
|
import 'package:notas/widgets/category_style.dart';
|
||||||
|
|
||||||
class NoteCard extends StatelessWidget {
|
class NoteCard extends StatelessWidget {
|
||||||
const NoteCard({
|
const NoteCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.note,
|
required this.note,
|
||||||
|
this.category,
|
||||||
this.isSelected = false,
|
this.isSelected = false,
|
||||||
this.borderColor,
|
this.borderColor,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
@@ -17,6 +20,7 @@ class NoteCard extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final Note note;
|
final Note note;
|
||||||
|
final Category? category;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final Color? borderColor;
|
final Color? borderColor;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
@@ -28,6 +32,12 @@ class NoteCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||||
final String bodyText = noteBodyToPlainText(note.body).trim();
|
final String bodyText = noteBodyToPlainText(note.body).trim();
|
||||||
|
final Color? categoryColor = category?.colorValue == null
|
||||||
|
? null
|
||||||
|
: Color(category!.colorValue!);
|
||||||
|
final IconData? categoryIcon = category == null
|
||||||
|
? null
|
||||||
|
: CategoryStyle.iconForCodePoint(category!.iconCodePoint);
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent, // 1. Fondo completamente transparente
|
color: Colors.transparent, // 1. Fondo completamente transparente
|
||||||
@@ -58,15 +68,34 @@ class NoteCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
note.title.isEmpty ? 'Sin título' : note.title,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
maxLines: 1,
|
children: [
|
||||||
overflow: TextOverflow.ellipsis,
|
if (categoryIcon != null) ...[
|
||||||
style: TextStyle(
|
Container(
|
||||||
color: palette.textPrimary,
|
width: 18,
|
||||||
fontSize: 15,
|
height: 18,
|
||||||
fontWeight: FontWeight.w700,
|
child: Icon(
|
||||||
),
|
categoryIcon,
|
||||||
|
size: 18,
|
||||||
|
color: categoryColor ?? palette.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
note.title.isEmpty ? 'Sin título' : note.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: palette.textPrimary,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
@@ -88,6 +117,11 @@ class NoteCard extends StatelessWidget {
|
|||||||
builder: (BuildContext buttonContext) {
|
builder: (BuildContext buttonContext) {
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
icon: Icon(Icons.more_vert, color: palette.textSecondary),
|
icon: Icon(Icons.more_vert, color: palette.textSecondary),
|
||||||
|
color: palette.surfaceElevated,
|
||||||
|
elevation: 10,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
onSelected: (String value) {
|
onSelected: (String value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 'category':
|
case 'category':
|
||||||
|
|||||||
Reference in New Issue
Block a user