Files
notas/lib/widgets/note_card.dart
T
Marcos f4bb5104e2 Refactor theme colors and styles across the application
- Introduced AppColors class to centralize color definitions for better maintainability and consistency.
- Updated various screens (Settings, Vault Access, Note Card, etc.) to use AppColors for styling instead of hardcoded colors.
- Enhanced UI elements with improved color contrast and accessibility.
- Replaced gradient backgrounds with defined color schemes for a cohesive look.
- Refactored button styles and text colors to align with the new theme structure.
2026-05-23 09:38:26 +02:00

151 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:notas/models/note.dart';
import 'package:notas/theme/app_colors.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 {
const NoteCard({
super.key,
required this.note,
this.onTap,
this.isDragging = false,
this.borderColor,
});
final Note note;
final VoidCallback? onTap;
final bool isDragging;
final Color? borderColor;
@override
State<NoteCard> createState() => _NoteCardState();
}
class _NoteCardState extends State<NoteCard> {
bool _isPressed = false;
@override
Widget build(BuildContext context) {
final bool showGrabbing = widget.isDragging || _isPressed;
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: AppColors.cardBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: widget.borderColor ?? AppColors.textDisabled,
width: 1,
),
),
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 List<String> rawLines = widget.note.body.split('\n');
const int avgCharsPerLine = 40;
int estimatedLines = 0;
for (final String line in rawLines) {
estimatedLines += (line.trim().length ~/ avgCharsPerLine) + 1;
}
final bool needsPreciseMeasurement = estimatedLines > 20;
final bool isBodyTruncated;
if (needsPreciseMeasurement) {
final TextPainter textPainter = TextPainter(
text: TextSpan(
text: widget.note.body,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
),
),
maxLines: 20,
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: const TextStyle(
color: AppColors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
widget.note.body,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
),
maxLines: 20,
overflow: TextOverflow.clip,
),
if (isBodyTruncated) ...[
const SizedBox(height: 4),
const Text(
'...',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 18,
height: 1,
),
),
],
],
);
},
),
),
),
);
}
}