Reestructuracion de la app
This commit is contained in:
+87
-127
@@ -4,150 +4,110 @@ import 'package:notas/data/note_body.dart';
|
||||
import 'package:notas/models/note.dart';
|
||||
import 'package:notas/theme/app_palette.dart';
|
||||
|
||||
// Small presentational widget for a note inside the grid.
|
||||
// Keep this widget lightweight and layout-agnostic: it should not force
|
||||
// width/height constraints (so it works inside different parent layouts
|
||||
// like MasonryGridView or Draggable feedback). Visual styling only.
|
||||
|
||||
class NoteCard extends StatefulWidget {
|
||||
class NoteCard extends StatelessWidget {
|
||||
const NoteCard({
|
||||
super.key,
|
||||
required this.note,
|
||||
this.onTap,
|
||||
this.isDragging = false,
|
||||
this.isSelected = false,
|
||||
this.borderColor,
|
||||
this.onTap,
|
||||
this.onDelete,
|
||||
this.onChangeCategory,
|
||||
});
|
||||
|
||||
final Note note;
|
||||
final VoidCallback? onTap;
|
||||
final bool isDragging;
|
||||
final bool isSelected;
|
||||
final Color? borderColor;
|
||||
|
||||
@override
|
||||
State<NoteCard> createState() => _NoteCardState();
|
||||
}
|
||||
|
||||
class _NoteCardState extends State<NoteCard> {
|
||||
bool _isPressed = false;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onDelete;
|
||||
final ValueChanged<BuildContext>? onChangeCategory;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AppPalette palette = Theme.of(context).extension<AppPalette>()!;
|
||||
final bool showGrabbing = widget.isDragging || _isPressed;
|
||||
final String bodyText = noteBodyToPlainText(note.body).trim();
|
||||
|
||||
return MouseRegion(
|
||||
cursor: showGrabbing
|
||||
? SystemMouseCursors.grabbing
|
||||
: SystemMouseCursors.grab,
|
||||
child: GestureDetector(
|
||||
onTapDown: widget.onTap == null
|
||||
? null
|
||||
: (_) {
|
||||
setState(() {
|
||||
_isPressed = true;
|
||||
});
|
||||
},
|
||||
onTapUp: widget.onTap == null
|
||||
? null
|
||||
: (_) {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
onTapCancel: widget.onTap == null
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
onTap: widget.onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.cardBackground,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: widget.borderColor ?? palette.textDisabled,
|
||||
width: 1,
|
||||
return Material(
|
||||
color: Colors.transparent, // 1. Fondo completamente transparente
|
||||
shape: BorderDirectional(
|
||||
start: BorderSide(
|
||||
color: isSelected ? palette.accent : Colors.transparent,
|
||||
width: isSelected ? 1.6 : 1.0,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: onTap,
|
||||
hoverColor: Colors.transparent, // 2. Desactiva el efecto hover (pasar el ratón)
|
||||
splashColor: Colors.transparent, // 3. Desactiva el efecto de onda al hacer clic
|
||||
highlightColor: Colors.transparent, // Desactiva el brillo al mantener pulsado
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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),
|
||||
Text(
|
||||
bodyText.isEmpty ? ' ' : bodyText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: palette.textSecondary,
|
||||
fontSize: 13,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
// Estimate whether the body will exceed 20 lines without always
|
||||
// running the expensive TextPainter layout. This heuristic counts
|
||||
// newline characters and estimates wrapped lines based on an
|
||||
// average characters-per-line to handle many short lines well.
|
||||
final String bodyText = noteBodyToPlainText(widget.note.body);
|
||||
final List<String> rawLines = bodyText.split('\n');
|
||||
const int avgCharsPerLine = 40;
|
||||
int estimatedLines = 0;
|
||||
for (final String line in rawLines) {
|
||||
estimatedLines += (line.trim().length ~/ avgCharsPerLine) + 1;
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<String>(
|
||||
tooltip: 'Más opciones',
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
color: palette.textSecondary,
|
||||
),
|
||||
onOpened: () {},
|
||||
onSelected: (String value) {
|
||||
switch (value) {
|
||||
case 'delete':
|
||||
onDelete?.call();
|
||||
return;
|
||||
case 'category':
|
||||
onChangeCategory?.call(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final bool needsPreciseMeasurement = estimatedLines > 15;
|
||||
final bool isBodyTruncated;
|
||||
|
||||
if (needsPreciseMeasurement) {
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: bodyText,
|
||||
style: TextStyle(
|
||||
color: palette.textSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
maxLines: 15,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout(maxWidth: constraints.maxWidth);
|
||||
|
||||
isBodyTruncated = textPainter.didExceedMaxLines;
|
||||
} else {
|
||||
isBodyTruncated = false;
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.note.title,
|
||||
style: TextStyle(
|
||||
color: palette.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
bodyText,
|
||||
style: TextStyle(
|
||||
color: palette.textSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 15,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
if (isBodyTruncated) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'...',
|
||||
style: TextStyle(
|
||||
color: palette.textMuted,
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||
const PopupMenuItem<String>(
|
||||
value: 'delete',
|
||||
child: Text('Eliminar nota'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'category',
|
||||
child: Text('Cambiar categoría'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user