117 lines
3.2 KiB
Dart
117 lines
3.2 KiB
Dart
/// Renders Timeline Geometry pieces for the FocusFlow Flutter timeline.
|
|
library;
|
|
|
|
/// Converts timeline clock values into vertical pixel positions.
|
|
class TimelineGeometry {
|
|
/// Creates geometry for a visible time range.
|
|
const TimelineGeometry({
|
|
required this.startMinutes,
|
|
required this.endMinutes,
|
|
required this.pixelsPerMinute,
|
|
this.minCardHeight = 0,
|
|
}) : assert(startMinutes >= 0),
|
|
assert(endMinutes <= 24 * 60),
|
|
assert(endMinutes > startMinutes),
|
|
assert(minCardHeight >= 0);
|
|
|
|
/// First visible minute from midnight.
|
|
final int startMinutes;
|
|
|
|
/// Last visible minute from midnight.
|
|
final int endMinutes;
|
|
|
|
/// Vertical scale used to convert minutes into pixels.
|
|
final double pixelsPerMinute;
|
|
|
|
/// Minimum rendered height for a timeline card.
|
|
///
|
|
/// Defaults to zero so scheduled cards match their exact duration on the
|
|
/// timeline.
|
|
final double minCardHeight;
|
|
|
|
/// Total vertical height of the visible timeline.
|
|
double get totalHeight => (endMinutes - startMinutes) * pixelsPerMinute;
|
|
|
|
/// Returns the vertical offset for [minutes] since midnight.
|
|
double yForMinutesSinceMidnight(int minutes) {
|
|
return (minutes - startMinutes) * pixelsPerMinute;
|
|
}
|
|
|
|
/// Returns the rendered height for a duration in minutes.
|
|
double heightForDuration(int minutes) {
|
|
final height = minutes * pixelsPerMinute;
|
|
return height < minCardHeight ? minCardHeight : height;
|
|
}
|
|
|
|
/// Generates timeline ticks at [stepMinutes] intervals.
|
|
List<TimelineTick> ticks({int stepMinutes = 15}) {
|
|
return [
|
|
for (
|
|
var minute = startMinutes;
|
|
minute <= endMinutes;
|
|
minute += stepMinutes
|
|
)
|
|
TimelineTick(
|
|
minutesSinceMidnight: minute,
|
|
y: yForMinutesSinceMidnight(minute),
|
|
label: _labelFor(minute),
|
|
major: minute % 60 == 0,
|
|
),
|
|
];
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
return identical(this, other) ||
|
|
other is TimelineGeometry &&
|
|
other.startMinutes == startMinutes &&
|
|
other.endMinutes == endMinutes &&
|
|
other.pixelsPerMinute == pixelsPerMinute &&
|
|
other.minCardHeight == minCardHeight;
|
|
}
|
|
|
|
@override
|
|
int get hashCode {
|
|
return Object.hash(
|
|
startMinutes,
|
|
endMinutes,
|
|
pixelsPerMinute,
|
|
minCardHeight,
|
|
);
|
|
}
|
|
|
|
static String _labelFor(int minutes) {
|
|
final hour24 = (minutes ~/ 60) % 24;
|
|
final minute = minutes % 60;
|
|
final period = hour24 >= 12 ? 'PM' : 'AM';
|
|
final rawHour = hour24 % 12;
|
|
final hour = rawHour == 0 ? 12 : rawHour;
|
|
if (minute == 0) {
|
|
return '$hour:00 $period';
|
|
}
|
|
return '$hour:${minute.toString().padLeft(2, '0')}';
|
|
}
|
|
}
|
|
|
|
/// A labeled tick on the compact timeline axis.
|
|
class TimelineTick {
|
|
/// Creates a timeline tick.
|
|
const TimelineTick({
|
|
required this.minutesSinceMidnight,
|
|
required this.y,
|
|
required this.label,
|
|
required this.major,
|
|
});
|
|
|
|
/// Minute from midnight represented by this tick.
|
|
final int minutesSinceMidnight;
|
|
|
|
/// Vertical pixel offset for this tick.
|
|
final double y;
|
|
|
|
/// Display label for this tick.
|
|
final String label;
|
|
|
|
/// Whether this tick represents a major interval.
|
|
final bool major;
|
|
}
|