280 lines
8.7 KiB
Dart
280 lines
8.7 KiB
Dart
import "package:flutter/material.dart";
|
|
|
|
class TripDiagramEntry {
|
|
const TripDiagramEntry({required this.label, this.labelIcon, this.subtitle, this.time});
|
|
|
|
final String label;
|
|
final IconData? labelIcon;
|
|
final String? subtitle;
|
|
final String? time;
|
|
}
|
|
|
|
class TripDiagram extends StatelessWidget {
|
|
const TripDiagram({
|
|
required this.entries,
|
|
super.key,
|
|
this.leftOffset = 0,
|
|
this.rowHeight = 32,
|
|
this.lineWidth = 7,
|
|
this.lineColor,
|
|
this.highlightLastEntry = false,
|
|
this.labelOffset = 26,
|
|
this.rightPadding = 16,
|
|
this.oddRowColor,
|
|
this.terminalRowColor,
|
|
this.stationTextStyle,
|
|
this.timeTextStyle,
|
|
this.missingTimeTextStyle,
|
|
this.emptyMessage = "No stations found.",
|
|
this.emptyTextStyle,
|
|
});
|
|
|
|
final List<TripDiagramEntry> entries;
|
|
final double leftOffset;
|
|
final double rowHeight;
|
|
final double lineWidth;
|
|
final Color? lineColor;
|
|
final bool highlightLastEntry;
|
|
final double labelOffset;
|
|
final double rightPadding;
|
|
final Color? oddRowColor;
|
|
final Color? terminalRowColor;
|
|
final TextStyle? stationTextStyle;
|
|
final TextStyle? timeTextStyle;
|
|
final TextStyle? missingTimeTextStyle;
|
|
final String emptyMessage;
|
|
final TextStyle? emptyTextStyle;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final resolvedLineColor = lineColor ?? Theme.of(context).colorScheme.primary;
|
|
|
|
final deduped = entries;
|
|
|
|
if (deduped.isEmpty) {
|
|
return Text(
|
|
emptyMessage,
|
|
style:
|
|
emptyTextStyle ??
|
|
TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
|
);
|
|
}
|
|
|
|
final totalHeight = deduped.length * rowHeight;
|
|
|
|
return Stack(
|
|
children: [
|
|
SizedBox(
|
|
height: totalHeight,
|
|
width: double.infinity,
|
|
child: CustomPaint(
|
|
painter: _TripDiagramPainter(
|
|
count: deduped.length,
|
|
rowHeight: rowHeight,
|
|
lineWidth: lineWidth,
|
|
lineColor: resolvedLineColor,
|
|
leftOffset: leftOffset,
|
|
),
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
child: Column(
|
|
children: List.generate(deduped.length, (index) {
|
|
final entry = deduped[index];
|
|
final time = entry.time;
|
|
final isTerminalRow = highlightLastEntry && index == deduped.length - 1;
|
|
|
|
return Container(
|
|
height: rowHeight,
|
|
color: isTerminalRow
|
|
? (terminalRowColor ?? resolvedLineColor.withValues(alpha: 0.12))
|
|
: index.isOdd
|
|
? (oddRowColor ?? Colors.white.withValues(alpha: 0.03))
|
|
: null,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(
|
|
left: leftOffset + labelOffset,
|
|
right: rightPadding,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
|
|
Row(
|
|
children: [
|
|
Text(
|
|
entry.label,
|
|
style:
|
|
stationTextStyle ??
|
|
const TextStyle(
|
|
fontSize: 15,
|
|
color: Color(0xFFDDDDDD),
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.5,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
|
|
if (entry.labelIcon != null) ...[
|
|
const SizedBox(width: 4),
|
|
Icon(
|
|
entry.labelIcon,
|
|
size: 12,
|
|
color: const Color(0xFFBBBBBB),
|
|
),
|
|
]
|
|
],
|
|
),
|
|
if (entry.subtitle != null)
|
|
Text(
|
|
entry.subtitle!,
|
|
style:
|
|
stationTextStyle ??
|
|
const TextStyle(
|
|
fontSize: 11,
|
|
color: Color(0xFFDDDDDD),
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.5,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
time ?? "--:--",
|
|
style: time != null
|
|
? (timeTextStyle ??
|
|
const TextStyle(
|
|
fontSize: 15,
|
|
fontFamily: "monospace",
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFFEEEEEE),
|
|
))
|
|
: (missingTimeTextStyle ??
|
|
const TextStyle(
|
|
fontSize: 15,
|
|
fontFamily: "monospace",
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF555555),
|
|
)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TripDiagramPainter extends CustomPainter {
|
|
const _TripDiagramPainter({
|
|
required this.count,
|
|
required this.rowHeight,
|
|
required this.lineWidth,
|
|
required this.lineColor,
|
|
this.leftOffset = 0,
|
|
});
|
|
|
|
final int count;
|
|
final double rowHeight;
|
|
final double lineWidth;
|
|
final Color lineColor;
|
|
final double leftOffset;
|
|
|
|
static const double _linePad = 4.0;
|
|
static const double _blobPad = 4.0;
|
|
static const double _centerSize = 4.0;
|
|
static const double _cornerFactor = 0.3;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
if (count <= 0) return;
|
|
|
|
final strokeWidth = lineWidth / 2.0;
|
|
final ringSize = _centerSize + strokeWidth;
|
|
final paddingWidth = strokeWidth + _blobPad;
|
|
final centerX = leftOffset + ringSize / 2 + paddingWidth / 2 + 2;
|
|
final firstY = rowHeight / 2;
|
|
final lastY = (count - 1) * rowHeight + rowHeight / 2;
|
|
|
|
final nodeRects = List.generate(count, (index) {
|
|
final centerY = index * rowHeight + rowHeight / 2;
|
|
final rect = Rect.fromCenter(
|
|
center: Offset(centerX, centerY),
|
|
width: ringSize,
|
|
height: ringSize,
|
|
);
|
|
return RRect.fromRectAndRadius(
|
|
rect,
|
|
Radius.circular(ringSize * _cornerFactor),
|
|
);
|
|
});
|
|
|
|
for (final rect in nodeRects) {
|
|
canvas.drawRRect(
|
|
rect,
|
|
Paint()
|
|
..color = Colors.white
|
|
..strokeWidth = paddingWidth
|
|
..style = PaintingStyle.stroke,
|
|
);
|
|
}
|
|
|
|
canvas.drawLine(
|
|
Offset(centerX, firstY),
|
|
Offset(centerX, lastY),
|
|
Paint()
|
|
..color = Colors.white
|
|
..strokeWidth = lineWidth + _linePad
|
|
..strokeCap = StrokeCap.round
|
|
..style = PaintingStyle.stroke,
|
|
);
|
|
|
|
canvas.drawLine(
|
|
Offset(centerX, firstY),
|
|
Offset(centerX, lastY),
|
|
Paint()
|
|
..color = lineColor
|
|
..strokeWidth = lineWidth
|
|
..strokeCap = StrokeCap.round
|
|
..style = PaintingStyle.stroke,
|
|
);
|
|
|
|
for (final rect in nodeRects) {
|
|
final centerRect = rect.deflate(strokeWidth / 2);
|
|
canvas.drawRRect(
|
|
centerRect,
|
|
Paint()
|
|
..color = Colors.white
|
|
..style = PaintingStyle.fill,
|
|
);
|
|
canvas.drawRRect(
|
|
rect,
|
|
Paint()
|
|
..color = lineColor
|
|
..strokeWidth = strokeWidth
|
|
..style = PaintingStyle.stroke,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _TripDiagramPainter oldDelegate) {
|
|
return oldDelegate.count != count ||
|
|
oldDelegate.rowHeight != rowHeight ||
|
|
oldDelegate.lineWidth != lineWidth ||
|
|
oldDelegate.lineColor != lineColor ||
|
|
oldDelegate.leftOffset != leftOffset;
|
|
}
|
|
}
|