refactor: Remove category handling from note editor and simplify note card options
This commit is contained in:
+263
-68
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ class HomeScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
static const double _desktopBreakpoint = 900;
|
static const double _desktopBreakpoint = 900;
|
||||||
|
static const String _createCategoryMenuValue = '__create_category__';
|
||||||
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final GlobalKey _filterButtonKey = GlobalKey();
|
final GlobalKey _filterButtonKey = GlobalKey();
|
||||||
@@ -182,6 +185,190 @@ 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({
|
||||||
|
required String title,
|
||||||
|
required String confirmLabel,
|
||||||
|
String? initialValue,
|
||||||
|
}) async {
|
||||||
|
final TextEditingController controller = TextEditingController(
|
||||||
|
text: initialValue ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final String? result = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext dialogContext) {
|
||||||
|
final AppPalette palette = _paletteOf(dialogContext);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: palette.surfaceElevated,
|
||||||
|
title: Text(title),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Nombre de la categoría',
|
||||||
|
),
|
||||||
|
onSubmitted: (String value) {
|
||||||
|
Navigator.of(dialogContext).pop(value.trim());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(dialogContext).pop(controller.text.trim());
|
||||||
|
},
|
||||||
|
child: Text(confirmLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final String name = (result ?? '').trim();
|
||||||
|
if (name.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
} finally {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Category?> _saveCategory({Category? existingCategory}) async {
|
||||||
|
final String? categoryName = await _promptCategoryName(
|
||||||
|
title: existingCategory == null ? 'Crear categoría' : 'Editar categoría',
|
||||||
|
confirmLabel: existingCategory == null ? 'Crear' : 'Guardar',
|
||||||
|
initialValue: existingCategory?.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (categoryName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
final Category category = existingCategory == null
|
||||||
|
? Category(name: categoryName, updatedAt: now)
|
||||||
|
: existingCategory.copyWith(
|
||||||
|
name: categoryName,
|
||||||
|
updatedAt: now,
|
||||||
|
isDirty: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await widget.repository.createCategory(category);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_categories = <Category>[
|
||||||
|
for (final Category item in _categories)
|
||||||
|
if (item.id == category.id) category else item,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!_categories.any((Category item) => item.id == category.id)) {
|
||||||
|
_categories = <Category>[..._categories, category];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateNoteCategory(Note note, String? categoryId) async {
|
||||||
|
try {
|
||||||
|
final Note updated = await widget.repository.updateNote(
|
||||||
|
note.copyWith(
|
||||||
|
categoryId: categoryId,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
isDirty: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_notes = <Note>[
|
||||||
|
for (final Note item in _notes)
|
||||||
|
if (item.id == updated.id) updated else item,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('No se pudo cambiar la categoría: $error')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteNoteAfterConfirmation(Note note) async {
|
||||||
|
final bool? confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext dialogContext) {
|
||||||
|
final AppPalette palette = _paletteOf(dialogContext);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: palette.surfaceElevated,
|
||||||
|
title: const Text('Eliminar nota'),
|
||||||
|
content: const Text(
|
||||||
|
'Esta acción eliminará la nota. ¿Quieres continuar?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||||
|
child: const Text('Cancelar'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Confirmar'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await widget.repository.deleteNote(note);
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_notes = _notes.where((Note item) => item.id != note.id).toList();
|
||||||
|
if (_selectedNoteId == note.id) {
|
||||||
|
_selectedNoteId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('No se pudo eliminar la nota: $error')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RelativeRect _menuRectFromContext(BuildContext anchorContext) {
|
RelativeRect _menuRectFromContext(BuildContext anchorContext) {
|
||||||
final RenderBox button = anchorContext.findRenderObject()! as RenderBox;
|
final RenderBox button = anchorContext.findRenderObject()! as RenderBox;
|
||||||
final RenderBox overlay =
|
final RenderBox overlay =
|
||||||
@@ -224,7 +411,28 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
for (final Category category in _categories)
|
for (final Category category in _categories)
|
||||||
PopupMenuItem<String?>(
|
PopupMenuItem<String?>(
|
||||||
value: category.id,
|
value: category.id,
|
||||||
child: Text(category.name),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(category.name)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Builder(
|
||||||
|
builder: (BuildContext menuContext) {
|
||||||
|
return IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.more_vert, size: 18),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(menuContext).pop();
|
||||||
|
unawaited(_saveCategory(existingCategory: category));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -248,80 +456,67 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
BuildContext anchorContext,
|
BuildContext anchorContext,
|
||||||
Note note,
|
Note note,
|
||||||
) async {
|
) async {
|
||||||
final Category? selected = await _showAnchoredCategoryMenu<Category?>(
|
final String? selectedCategoryId = await _showAnchoredCategoryMenu<String?>(
|
||||||
anchorContext: anchorContext,
|
anchorContext: anchorContext,
|
||||||
items: <PopupMenuEntry<Category?>>[
|
items: <PopupMenuEntry<String?>>[
|
||||||
const PopupMenuItem<Category?>(
|
const PopupMenuItem<String?>(value: '', child: Text('Sin categoría')),
|
||||||
value: null,
|
|
||||||
child: Text('Sin categoría'),
|
|
||||||
),
|
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
for (final Category category in _categories)
|
for (final Category category in _categories)
|
||||||
PopupMenuItem<Category?>(value: category, child: Text(category.name)),
|
PopupMenuItem<String?>(
|
||||||
|
value: category.id,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(category.name)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Builder(
|
||||||
|
builder: (BuildContext menuContext) {
|
||||||
|
return IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.more_vert, size: 18),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(menuContext).pop();
|
||||||
|
unawaited(_saveCategory(existingCategory: category));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuDivider(),
|
||||||
|
const PopupMenuItem<String?>(
|
||||||
|
value: _createCategoryMenuValue,
|
||||||
|
child: Text('Crear categoría'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted || selectedCategoryId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String? categoryId = selected?.id;
|
if (selectedCategoryId == _createCategoryMenuValue) {
|
||||||
|
final Category? createdCategory = await _saveCategory();
|
||||||
|
if (createdCategory == null || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _updateNoteCategory(note, createdCategory.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? categoryId = selectedCategoryId.isEmpty
|
||||||
|
? null
|
||||||
|
: selectedCategoryId;
|
||||||
if (categoryId == note.categoryId) {
|
if (categoryId == note.categoryId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await _updateNoteCategory(note, categoryId);
|
||||||
final Note updated = await widget.repository.updateNote(
|
|
||||||
note.copyWith(
|
|
||||||
categoryId: categoryId,
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
isDirty: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_notes = <Note>[
|
|
||||||
for (final Note item in _notes)
|
|
||||||
if (item.id == updated.id) updated else item,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('No se pudo cambiar la categoría: $error')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _deleteNote(Note note) async {
|
|
||||||
try {
|
|
||||||
await widget.repository.deleteNote(note);
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_notes = _notes.where((Note item) => item.id != note.id).toList();
|
|
||||||
if (_selectedNoteId == note.id) {
|
|
||||||
_selectedNoteId = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('No se pudo eliminar la nota: $error')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _createNote({required bool openEditor}) async {
|
Future<void> _createNote({required bool openEditor}) async {
|
||||||
@@ -387,7 +582,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
key: ValueKey<String>(note.id),
|
key: ValueKey<String>(note.id),
|
||||||
repository: widget.repository,
|
repository: widget.repository,
|
||||||
note: note,
|
note: note,
|
||||||
categories: _categories,
|
|
||||||
embedded: embedded,
|
embedded: embedded,
|
||||||
onSaved: (Note saved) {
|
onSaved: (Note saved) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
@@ -578,22 +772,24 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
await _loadData(keepSelection: true);
|
await _loadData(keepSelection: true);
|
||||||
},
|
},
|
||||||
child: ReorderableListView.builder(
|
child: ReorderableListView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 20),
|
padding: const EdgeInsets.fromLTRB(10, 10, 10, 14),
|
||||||
buildDefaultDragHandles: false,
|
buildDefaultDragHandles: false,
|
||||||
itemCount: visibleNotes.length,
|
itemCount: visibleNotes.length,
|
||||||
onReorder: _handleReorder,
|
onReorder: _handleReorder,
|
||||||
footer: Padding(
|
footer: Padding(
|
||||||
padding: const EdgeInsets.only(top: 8, bottom: 88),
|
padding: const EdgeInsets.only(top: 4, bottom: 72),
|
||||||
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
_formatLastSyncAt(),
|
_formatLastSyncAt(),
|
||||||
style: TextStyle(color: palette.textSecondary, fontSize: 12),
|
style: TextStyle(color: palette.textSecondary, fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final Note note = visibleNotes[index];
|
final Note note = visibleNotes[index];
|
||||||
return Padding(
|
return Padding(
|
||||||
key: ValueKey<String>(note.id),
|
key: ValueKey<String>(note.id),
|
||||||
padding: const EdgeInsets.only(bottom: 10),
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
child: ReorderableDelayedDragStartListener(
|
child: ReorderableDelayedDragStartListener(
|
||||||
index: index,
|
index: index,
|
||||||
child: NoteCard(
|
child: NoteCard(
|
||||||
@@ -601,7 +797,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
isSelected: note.id == _selectedNoteId,
|
isSelected: note.id == _selectedNoteId,
|
||||||
showSelectionBorder: isDesktop,
|
showSelectionBorder: isDesktop,
|
||||||
onTap: () => _handleNoteTap(note, isDesktop),
|
onTap: () => _handleNoteTap(note, isDesktop),
|
||||||
onDelete: () => _deleteNote(note),
|
onDelete: () => _deleteNoteAfterConfirmation(note),
|
||||||
onChangeCategory: (BuildContext buttonContext) =>
|
onChangeCategory: (BuildContext buttonContext) =>
|
||||||
_changeNoteCategory(buttonContext, note),
|
_changeNoteCategory(buttonContext, note),
|
||||||
),
|
),
|
||||||
@@ -682,7 +878,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
key: ValueKey<String>(selectedNote.id),
|
key: ValueKey<String>(selectedNote.id),
|
||||||
repository: widget.repository,
|
repository: widget.repository,
|
||||||
note: selectedNote,
|
note: selectedNote,
|
||||||
categories: _categories,
|
|
||||||
embedded: true,
|
embedded: true,
|
||||||
onSaved: (Note saved) {
|
onSaved: (Note saved) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import 'package:flutter_quill/flutter_quill.dart';
|
|||||||
|
|
||||||
import 'package:notas/data/note_body.dart';
|
import 'package:notas/data/note_body.dart';
|
||||||
import 'package:notas/data/note_repository.dart';
|
import 'package:notas/data/note_repository.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 NoteEditorScreen extends StatefulWidget {
|
class NoteEditorScreen extends StatefulWidget {
|
||||||
const NoteEditorScreen({
|
const NoteEditorScreen({
|
||||||
@@ -16,7 +14,6 @@ class NoteEditorScreen extends StatefulWidget {
|
|||||||
this.repository,
|
this.repository,
|
||||||
this.saveNote,
|
this.saveNote,
|
||||||
required this.note,
|
required this.note,
|
||||||
this.categories = const <Category>[],
|
|
||||||
this.embedded = false,
|
this.embedded = false,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
});
|
});
|
||||||
@@ -24,7 +21,6 @@ class NoteEditorScreen extends StatefulWidget {
|
|||||||
final NoteRepository? repository;
|
final NoteRepository? repository;
|
||||||
final Future<Note> Function(Note note)? saveNote;
|
final Future<Note> Function(Note note)? saveNote;
|
||||||
final Note note;
|
final Note note;
|
||||||
final List<Category> categories;
|
|
||||||
final bool embedded;
|
final bool embedded;
|
||||||
final ValueChanged<Note>? onSaved;
|
final ValueChanged<Note>? onSaved;
|
||||||
|
|
||||||
@@ -34,7 +30,6 @@ class NoteEditorScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||||
static const Duration _debounceDuration = Duration(seconds: 1);
|
static const Duration _debounceDuration = Duration(seconds: 1);
|
||||||
final GlobalKey _categorySelectorKey = GlobalKey();
|
|
||||||
|
|
||||||
late final TextEditingController _titleController;
|
late final TextEditingController _titleController;
|
||||||
late final QuillController _bodyController;
|
late final QuillController _bodyController;
|
||||||
@@ -45,7 +40,6 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
bool _saveQueued = false;
|
bool _saveQueued = false;
|
||||||
late Note _baselineNote;
|
late Note _baselineNote;
|
||||||
String? _selectedCategoryId;
|
|
||||||
|
|
||||||
AppPalette _paletteOf(BuildContext context) {
|
AppPalette _paletteOf(BuildContext context) {
|
||||||
return Theme.of(context).extension<AppPalette>() ??
|
return Theme.of(context).extension<AppPalette>() ??
|
||||||
@@ -56,7 +50,6 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_baselineNote = widget.note;
|
_baselineNote = widget.note;
|
||||||
_selectedCategoryId = widget.note.categoryId;
|
|
||||||
_titleController = TextEditingController(text: widget.note.title)
|
_titleController = TextEditingController(text: widget.note.title)
|
||||||
..addListener(_scheduleSave);
|
..addListener(_scheduleSave);
|
||||||
_bodyController = QuillController(
|
_bodyController = QuillController(
|
||||||
@@ -81,55 +74,6 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
return noteDocumentToStorageJson(_bodyController.document);
|
return noteDocumentToStorageJson(_bodyController.document);
|
||||||
}
|
}
|
||||||
|
|
||||||
Category? _categoryById(String? id) {
|
|
||||||
for (final Category category in widget.categories) {
|
|
||||||
if (category.id == id) {
|
|
||||||
return category;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _categoryBackgroundColor(Category? category) {
|
|
||||||
final AppPalette palette = _paletteOf(context);
|
|
||||||
|
|
||||||
if (category?.colorValue == null) {
|
|
||||||
return palette.borderMuted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Color(category!.colorValue!);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _categoryForegroundColor(Category? category) {
|
|
||||||
final AppPalette palette = _paletteOf(context);
|
|
||||||
|
|
||||||
if (category == null || category.colorValue == null) {
|
|
||||||
return palette.textPrimary;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Color background = Color(category.colorValue!);
|
|
||||||
return background.computeLuminance() > 0.55
|
|
||||||
? palette.textOnSurfaceDark
|
|
||||||
: palette.textPrimary;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scheduleSave() {
|
void _scheduleSave() {
|
||||||
_debounceTimer?.cancel();
|
_debounceTimer?.cancel();
|
||||||
_debounceTimer = Timer(_debounceDuration, () {
|
_debounceTimer = Timer(_debounceDuration, () {
|
||||||
@@ -147,7 +91,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
final Note draft = _baselineNote.copyWith(
|
final Note draft = _baselineNote.copyWith(
|
||||||
title: title.isEmpty ? 'Sin título' : title,
|
title: title.isEmpty ? 'Sin título' : title,
|
||||||
body: body,
|
body: body,
|
||||||
categoryId: _selectedCategoryId,
|
categoryId: _baselineNote.categoryId,
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
);
|
);
|
||||||
@@ -188,89 +132,8 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectCategory(BuildContext anchorContext) async {
|
Widget _buildEditorBody() {
|
||||||
final Category? selected = await showMenu<Category?>(
|
|
||||||
context: anchorContext,
|
|
||||||
position: _menuRectFromContext(anchorContext),
|
|
||||||
elevation: 10,
|
|
||||||
color: _paletteOf(anchorContext).surfaceElevated,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
|
||||||
items: <PopupMenuEntry<Category?>>[
|
|
||||||
const PopupMenuItem<Category?>(
|
|
||||||
value: null,
|
|
||||||
child: Text('Sin categoría'),
|
|
||||||
),
|
|
||||||
const PopupMenuDivider(),
|
|
||||||
for (final Category category in widget.categories)
|
|
||||||
PopupMenuItem<Category?>(value: category, child: Text(category.name)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_selectedCategoryId = selected?.id;
|
|
||||||
});
|
|
||||||
_scheduleSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCategorySelector() {
|
|
||||||
final Category? category = _categoryById(_selectedCategoryId);
|
|
||||||
final AppPalette palette = _paletteOf(context);
|
final AppPalette palette = _paletteOf(context);
|
||||||
final Color backgroundColor = _categoryBackgroundColor(category);
|
|
||||||
final Color foregroundColor = _categoryForegroundColor(category);
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
key: const ValueKey<String>('category_selector'),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
onTap: () => _selectCategory(context),
|
|
||||||
child: Container(
|
|
||||||
key: _categorySelectorKey,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: category?.colorValue != null
|
|
||||||
? backgroundColor.withValues(alpha: 0.9)
|
|
||||||
: palette.textDisabled,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
CategoryStyle.iconForCodePoint(category?.iconCodePoint),
|
|
||||||
color: foregroundColor,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
category?.name ?? 'Sin categoría',
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
|
||||||
color: foregroundColor,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Icon(Icons.arrow_drop_down, color: foregroundColor, size: 18),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEditorBody({required bool embedded}) {
|
|
||||||
final AppPalette palette = _paletteOf(context);
|
|
||||||
final BoxBorder? bodyBorder = embedded
|
|
||||||
? null
|
|
||||||
: Border.all(color: palette.border);
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -278,11 +141,13 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _titleController,
|
controller: _titleController,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: palette.textPrimary,
|
color: palette.textPrimary,
|
||||||
fontSize: 28,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -292,22 +157,13 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 240),
|
|
||||||
child: _buildCategorySelector(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(8),
|
||||||
color: palette.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: bodyBorder,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(14),
|
|
||||||
child: QuillEditor.basic(
|
child: QuillEditor.basic(
|
||||||
controller: _bodyController,
|
controller: _bodyController,
|
||||||
focusNode: _bodyFocusNode,
|
focusNode: _bodyFocusNode,
|
||||||
@@ -323,7 +179,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 8),
|
||||||
QuillSimpleToolbar(
|
QuillSimpleToolbar(
|
||||||
controller: _bodyController,
|
controller: _bodyController,
|
||||||
config: const QuillSimpleToolbarConfig(
|
config: const QuillSimpleToolbarConfig(
|
||||||
@@ -367,8 +223,8 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||||||
final AppPalette palette = _paletteOf(context);
|
final AppPalette palette = _paletteOf(context);
|
||||||
|
|
||||||
final Widget editor = Padding(
|
final Widget editor = Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(8),
|
||||||
child: _buildEditorBody(embedded: widget.embedded),
|
child: _buildEditorBody(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.embedded) {
|
if (widget.embedded) {
|
||||||
|
|||||||
+27
-14
@@ -49,7 +49,7 @@ class NoteCard extends StatelessWidget {
|
|||||||
highlightColor:
|
highlightColor:
|
||||||
Colors.transparent, // Desactiva el brillo al mantener pulsado
|
Colors.transparent, // Desactiva el brillo al mantener pulsado
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -75,39 +75,52 @@ class NoteCard extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: palette.textSecondary,
|
color: palette.textSecondary,
|
||||||
fontSize: 13,
|
fontSize: 14,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
PopupMenuButton<String>(
|
Builder(
|
||||||
tooltip: 'Más opciones',
|
builder: (BuildContext buttonContext) {
|
||||||
|
return PopupMenuButton<String>(
|
||||||
icon: Icon(Icons.more_vert, color: palette.textSecondary),
|
icon: Icon(Icons.more_vert, color: palette.textSecondary),
|
||||||
onOpened: () {},
|
|
||||||
onSelected: (String value) {
|
onSelected: (String value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
case 'category':
|
||||||
|
onChangeCategory?.call(buttonContext);
|
||||||
|
return;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
onDelete?.call();
|
onDelete?.call();
|
||||||
return;
|
return;
|
||||||
case 'category':
|
|
||||||
onChangeCategory?.call(context);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
itemBuilder: (BuildContext context) =>
|
||||||
const PopupMenuItem<String>(
|
<PopupMenuEntry<String>>[
|
||||||
value: 'delete',
|
|
||||||
child: Text('Eliminar nota'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
const PopupMenuItem<String>(
|
||||||
value: 'category',
|
value: 'category',
|
||||||
child: Text('Cambiar categoría'),
|
child: Text('Modificar categoría'),
|
||||||
|
),
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.delete_outline, color: Colors.red),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
'Eliminar',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -33,13 +33,6 @@ void main() {
|
|||||||
body: NoteEditorScreen(
|
body: NoteEditorScreen(
|
||||||
repository: null,
|
repository: null,
|
||||||
note: initialNote,
|
note: initialNote,
|
||||||
categories: <Category>[
|
|
||||||
Category(
|
|
||||||
id: 'work',
|
|
||||||
name: 'Trabajo',
|
|
||||||
updatedAt: DateTime(2026, 5, 21),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
saveNote: (Note note) async => note,
|
saveNote: (Note note) async => note,
|
||||||
onSaved: (Note result) {
|
onSaved: (Note result) {
|
||||||
savedNote = result;
|
savedNote = result;
|
||||||
@@ -88,7 +81,6 @@ void main() {
|
|||||||
body: NoteEditorScreen(
|
body: NoteEditorScreen(
|
||||||
repository: null,
|
repository: null,
|
||||||
note: initialNote,
|
note: initialNote,
|
||||||
categories: <Category>[],
|
|
||||||
saveNote: (Note note) async {
|
saveNote: (Note note) async {
|
||||||
saveCount += 1;
|
saveCount += 1;
|
||||||
return note;
|
return note;
|
||||||
|
|||||||
Reference in New Issue
Block a user