new pending visibility
Some checks failed
Release / meta (push) Successful in 8s
Release / release-dev (push) Has been cancelled
Release / release-master (push) Has been cancelled
Release / android-build (push) Has been cancelled
Release / linux-build (push) Has been cancelled
Release / web-build (push) Has been cancelled
Some checks failed
Release / meta (push) Successful in 8s
Release / release-dev (push) Has been cancelled
Release / release-master (push) Has been cancelled
Release / android-build (push) Has been cancelled
Release / linux-build (push) Has been cancelled
Release / web-build (push) Has been cancelled
This commit is contained in:
@@ -34,6 +34,7 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
bool _isDeleting = false;
|
bool _isDeleting = false;
|
||||||
final Set<int> _moderatingEventIds = {};
|
final Set<int> _moderatingEventIds = {};
|
||||||
|
final Set<String> _expandedPendingAttrs = {};
|
||||||
bool _showPending = true;
|
bool _showPending = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -613,16 +614,30 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
if (isElevated)
|
if (isElevated)
|
||||||
SwitchListTile.adaptive(
|
Row(
|
||||||
contentPadding: EdgeInsets.zero,
|
children: [
|
||||||
title: const Text('Show pending entries'),
|
Expanded(
|
||||||
value: _showPending,
|
child: SwitchListTile.adaptive(
|
||||||
onChanged: (value) {
|
contentPadding: EdgeInsets.zero,
|
||||||
setState(() {
|
title: const Text('Show pending entries'),
|
||||||
_showPending = value;
|
value: _showPending,
|
||||||
});
|
onChanged: (value) async {
|
||||||
_persistPendingVisibility(value);
|
setState(() {
|
||||||
},
|
_showPending = value;
|
||||||
|
});
|
||||||
|
await _persistPendingVisibility(value);
|
||||||
|
if (mounted) {
|
||||||
|
await _load();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Refresh timeline',
|
||||||
|
onPressed: _load,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
_TimelineGrid(
|
_TimelineGrid(
|
||||||
entries: visibleTimeline,
|
entries: visibleTimeline,
|
||||||
@@ -633,6 +648,14 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
onDeleteEntry: _deleteEntry,
|
onDeleteEntry: _deleteEntry,
|
||||||
onModeratePending: _moderatePendingEntry,
|
onModeratePending: _moderatePendingEntry,
|
||||||
pendingActionEventIds: _moderatingEventIds,
|
pendingActionEventIds: _moderatingEventIds,
|
||||||
|
expandedPendingAttrs: _expandedPendingAttrs,
|
||||||
|
onTogglePendingAttr: (attrCode) {
|
||||||
|
setState(() {
|
||||||
|
if (!_expandedPendingAttrs.add(attrCode)) {
|
||||||
|
_expandedPendingAttrs.remove(attrCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_EventEditor(
|
_EventEditor(
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class _TimelineGrid extends StatefulWidget {
|
|||||||
this.onDeleteEntry,
|
this.onDeleteEntry,
|
||||||
this.onModeratePending,
|
this.onModeratePending,
|
||||||
this.pendingActionEventIds = const {},
|
this.pendingActionEventIds = const {},
|
||||||
|
this.expandedPendingAttrs = const {},
|
||||||
|
this.onTogglePendingAttr,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<LocoAttrVersion> entries;
|
final List<LocoAttrVersion> entries;
|
||||||
@@ -21,6 +23,8 @@ class _TimelineGrid extends StatefulWidget {
|
|||||||
_PendingModerationAction action,
|
_PendingModerationAction action,
|
||||||
)? onModeratePending;
|
)? onModeratePending;
|
||||||
final Set<int> pendingActionEventIds;
|
final Set<int> pendingActionEventIds;
|
||||||
|
final Set<String> expandedPendingAttrs;
|
||||||
|
final void Function(String attrCode)? onTogglePendingAttr;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_TimelineGrid> createState() => _TimelineGridState();
|
State<_TimelineGrid> createState() => _TimelineGridState();
|
||||||
@@ -86,12 +90,15 @@ class _TimelineGridState extends State<_TimelineGrid> {
|
|||||||
'build_day',
|
'build_day',
|
||||||
}.contains(code);
|
}.contains(code);
|
||||||
}).toList();
|
}).toList();
|
||||||
final model = _TimelineModel.fromEntries(filteredEntries);
|
final model = _TimelineModel.fromEntries(
|
||||||
|
filteredEntries,
|
||||||
|
expandedAttrCodes: widget.expandedPendingAttrs,
|
||||||
|
);
|
||||||
final axisSegments = model.axisSegments;
|
final axisSegments = model.axisSegments;
|
||||||
const labelWidth = 110.0;
|
const labelWidth = 110.0;
|
||||||
const rowHeight = 52.0;
|
const rowHeight = 52.0;
|
||||||
const double axisHeight = 48;
|
const double axisHeight = 48;
|
||||||
final rows = model.attrRows.entries.toList();
|
final rows = model.rows;
|
||||||
final totalRowsHeight = rows.length * rowHeight;
|
final totalRowsHeight = rows.length * rowHeight;
|
||||||
final axisWidth = math.max(model.axisTotalWidth, 120.0);
|
final axisWidth = math.max(model.axisTotalWidth, 120.0);
|
||||||
final double viewHeight = totalRowsHeight + axisHeight + 8;
|
final double viewHeight = totalRowsHeight + axisHeight + 8;
|
||||||
@@ -131,7 +138,12 @@ class _TimelineGridState extends State<_TimelineGrid> {
|
|||||||
itemExtent: rowHeight,
|
itemExtent: rowHeight,
|
||||||
itemCount: rows.length,
|
itemCount: rows.length,
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final label = _formatAttrLabel(rows[index].key);
|
final row = rows[index];
|
||||||
|
final label = row.isPrimary
|
||||||
|
? _formatAttrLabel(row.attrCode)
|
||||||
|
: (row.pendingUser?.trim().isNotEmpty == true
|
||||||
|
? row.pendingUser!.trim()
|
||||||
|
: 'Unknown');
|
||||||
return Container(
|
return Container(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -148,12 +160,49 @@ class _TimelineGridState extends State<_TimelineGrid> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Row(
|
||||||
label,
|
children: [
|
||||||
style: Theme.of(context)
|
if (!row.isPrimary) ...[
|
||||||
.textTheme
|
Icon(
|
||||||
.labelLarge
|
Icons.subdirectory_arrow_right,
|
||||||
?.copyWith(fontWeight: FontWeight.w700),
|
size: 16,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
],
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(
|
||||||
|
fontWeight: row.isPrimary
|
||||||
|
? FontWeight.w700
|
||||||
|
: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (row.showExpandToggle)
|
||||||
|
IconButton(
|
||||||
|
onPressed: widget.onTogglePendingAttr == null
|
||||||
|
? null
|
||||||
|
: () => widget.onTogglePendingAttr?.call(
|
||||||
|
row.attrCode,
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
row.isExpanded
|
||||||
|
? Icons.expand_less
|
||||||
|
: Icons.expand_more,
|
||||||
|
),
|
||||||
|
tooltip: row.isExpanded
|
||||||
|
? 'Collapse pending rows'
|
||||||
|
: 'Expand pending rows',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -188,7 +237,7 @@ class _TimelineGridState extends State<_TimelineGrid> {
|
|||||||
itemExtent: rowHeight,
|
itemExtent: rowHeight,
|
||||||
itemCount: rows.length,
|
itemCount: rows.length,
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final blocks = rows[index].value;
|
final blocks = rows[index].blocks;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 2.0),
|
const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
@@ -657,79 +706,252 @@ DateTime _safeEnd(DateTime start, DateTime? end) {
|
|||||||
return end;
|
return end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _startKey(DateTime date) => date.year * 10000 + date.month * 100 + date.day;
|
||||||
|
|
||||||
|
bool _isOverlappingStart(LocoAttrVersion entry, Set<int> approvedStartKeys) {
|
||||||
|
final start = _effectiveStart(entry);
|
||||||
|
if (start == null) return false;
|
||||||
|
return approvedStartKeys.contains(_startKey(start));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_ValueSegment> _segmentsForEntries(
|
||||||
|
List<LocoAttrVersion> items,
|
||||||
|
DateTime now,
|
||||||
|
) {
|
||||||
|
if (items.isEmpty) return const [];
|
||||||
|
final sorted = [...items];
|
||||||
|
sorted.sort(
|
||||||
|
(a, b) => (_effectiveStart(a) ?? now)
|
||||||
|
.compareTo(_effectiveStart(b) ?? now),
|
||||||
|
);
|
||||||
|
final segments = <_ValueSegment>[];
|
||||||
|
for (int i = 0; i < sorted.length; i++) {
|
||||||
|
final entry = sorted[i];
|
||||||
|
final start = _effectiveStart(entry) ?? now;
|
||||||
|
final nextStart = i < sorted.length - 1
|
||||||
|
? _effectiveStart(sorted[i + 1])
|
||||||
|
: null;
|
||||||
|
final rawEnd = entry.validTo ?? nextStart ?? now;
|
||||||
|
final end = _safeEnd(start, rawEnd);
|
||||||
|
segments.add(
|
||||||
|
_ValueSegment(
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
value: _formatValueWithUnits(entry),
|
||||||
|
entry: entry,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DateTime> _buildBoundaries(
|
||||||
|
List<_ValueSegment> segments,
|
||||||
|
DateTime now,
|
||||||
|
) {
|
||||||
|
DateTime? minStart;
|
||||||
|
DateTime? maxEnd;
|
||||||
|
final boundaryDates = <DateTime>{};
|
||||||
|
for (final seg in segments) {
|
||||||
|
boundaryDates.add(seg.start);
|
||||||
|
boundaryDates.add(seg.end);
|
||||||
|
minStart = minStart == null || seg.start.isBefore(minStart!)
|
||||||
|
? seg.start
|
||||||
|
: minStart;
|
||||||
|
maxEnd = maxEnd == null || seg.end.isAfter(maxEnd!) ? seg.end : maxEnd;
|
||||||
|
}
|
||||||
|
minStart ??= now.subtract(const Duration(days: 1));
|
||||||
|
final effectiveMaxEnd = maxEnd ?? now;
|
||||||
|
boundaryDates.add(effectiveMaxEnd);
|
||||||
|
var boundaries = boundaryDates.toList()..sort();
|
||||||
|
if (boundaries.length < 2) {
|
||||||
|
boundaries = [minStart!, effectiveMaxEnd];
|
||||||
|
}
|
||||||
|
return boundaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineRowSpec {
|
||||||
|
final String id;
|
||||||
|
final String attrCode;
|
||||||
|
final List<_ValueSegment> segments;
|
||||||
|
final bool isPrimary;
|
||||||
|
final bool showExpandToggle;
|
||||||
|
final bool isExpanded;
|
||||||
|
final String? userLabel;
|
||||||
|
|
||||||
|
const _TimelineRowSpec._({
|
||||||
|
required this.id,
|
||||||
|
required this.attrCode,
|
||||||
|
required this.segments,
|
||||||
|
required this.isPrimary,
|
||||||
|
required this.showExpandToggle,
|
||||||
|
required this.isExpanded,
|
||||||
|
this.userLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory _TimelineRowSpec.primary({
|
||||||
|
required String attrCode,
|
||||||
|
required List<_ValueSegment> segments,
|
||||||
|
required bool showExpandToggle,
|
||||||
|
required bool isExpanded,
|
||||||
|
}) {
|
||||||
|
return _TimelineRowSpec._(
|
||||||
|
id: attrCode,
|
||||||
|
attrCode: attrCode,
|
||||||
|
segments: segments,
|
||||||
|
isPrimary: true,
|
||||||
|
showExpandToggle: showExpandToggle,
|
||||||
|
isExpanded: isExpanded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory _TimelineRowSpec.pending({
|
||||||
|
required String attrCode,
|
||||||
|
required String userLabel,
|
||||||
|
required List<_ValueSegment> segments,
|
||||||
|
}) {
|
||||||
|
return _TimelineRowSpec._(
|
||||||
|
id: '$attrCode::$userLabel',
|
||||||
|
attrCode: attrCode,
|
||||||
|
segments: segments,
|
||||||
|
isPrimary: false,
|
||||||
|
showExpandToggle: false,
|
||||||
|
isExpanded: false,
|
||||||
|
userLabel: userLabel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineRowData {
|
||||||
|
final String id;
|
||||||
|
final String attrCode;
|
||||||
|
final List<_ValueBlock> blocks;
|
||||||
|
final bool isPrimary;
|
||||||
|
final bool showExpandToggle;
|
||||||
|
final bool isExpanded;
|
||||||
|
final String? pendingUser;
|
||||||
|
|
||||||
|
const _TimelineRowData({
|
||||||
|
required this.id,
|
||||||
|
required this.attrCode,
|
||||||
|
required this.blocks,
|
||||||
|
required this.isPrimary,
|
||||||
|
required this.showExpandToggle,
|
||||||
|
required this.isExpanded,
|
||||||
|
this.pendingUser,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class _TimelineModel {
|
class _TimelineModel {
|
||||||
final List<_AxisSegment> axisSegments;
|
final List<_AxisSegment> axisSegments;
|
||||||
final Map<String, List<_ValueBlock>> attrRows;
|
final List<_TimelineRowData> rows;
|
||||||
final String endLabel;
|
final String endLabel;
|
||||||
final List<DateTime> boundaries;
|
final List<DateTime> boundaries;
|
||||||
final double axisTotalWidth;
|
final double axisTotalWidth;
|
||||||
|
|
||||||
_TimelineModel({
|
_TimelineModel({
|
||||||
required this.axisSegments,
|
required this.axisSegments,
|
||||||
required this.attrRows,
|
required this.rows,
|
||||||
required this.endLabel,
|
required this.endLabel,
|
||||||
required this.boundaries,
|
required this.boundaries,
|
||||||
required this.axisTotalWidth,
|
required this.axisTotalWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory _TimelineModel.fromEntries(List<LocoAttrVersion> entries) {
|
factory _TimelineModel.fromEntries(
|
||||||
|
List<LocoAttrVersion> entries, {
|
||||||
|
Set<String> expandedAttrCodes = const {},
|
||||||
|
}) {
|
||||||
final effectiveEntries = entries
|
final effectiveEntries = entries
|
||||||
.where((e) => _effectiveStart(e) != null)
|
.where((e) => _effectiveStart(e) != null)
|
||||||
.toList();
|
.toList();
|
||||||
final grouped = <String, List<LocoAttrVersion>>{};
|
final grouped = <String, List<LocoAttrVersion>>{};
|
||||||
|
final attrOrder = <String>[];
|
||||||
for (final entry in effectiveEntries) {
|
for (final entry in effectiveEntries) {
|
||||||
grouped.putIfAbsent(entry.attrCode, () => []).add(entry);
|
final key = entry.attrCode;
|
||||||
|
if (!grouped.containsKey(key)) {
|
||||||
|
attrOrder.add(key);
|
||||||
|
}
|
||||||
|
grouped.putIfAbsent(key, () => []).add(entry);
|
||||||
}
|
}
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
DateTime? minStart;
|
final allSegments = <_ValueSegment>[];
|
||||||
DateTime? maxEnd;
|
final rowSpecs = <_TimelineRowSpec>[];
|
||||||
final attrSegments = <String, List<_ValueSegment>>{};
|
for (final attr in attrOrder) {
|
||||||
|
final items = grouped[attr] ?? const [];
|
||||||
|
final approved = items.where((e) => !e.isPending).toList();
|
||||||
|
final pending = items.where((e) => e.isPending).toList();
|
||||||
|
final approvedSegments = _segmentsForEntries(approved, now);
|
||||||
|
|
||||||
grouped.forEach((attr, items) {
|
final approvedStartKeys = <int>{};
|
||||||
items.sort(
|
for (final entry in approved) {
|
||||||
(a, b) => (_effectiveStart(a) ?? now)
|
final start = _effectiveStart(entry);
|
||||||
.compareTo(_effectiveStart(b) ?? now),
|
if (start == null) continue;
|
||||||
|
approvedStartKeys.add(_startKey(start));
|
||||||
|
}
|
||||||
|
|
||||||
|
final pendingByUser = <String, List<LocoAttrVersion>>{};
|
||||||
|
final overlapByUser = <String, List<LocoAttrVersion>>{};
|
||||||
|
for (final entry in pending) {
|
||||||
|
final user = (entry.suggestedBy ?? '').trim().isEmpty
|
||||||
|
? 'Unknown'
|
||||||
|
: entry.suggestedBy!.trim();
|
||||||
|
pendingByUser.putIfAbsent(user, () => []).add(entry);
|
||||||
|
final start = _effectiveStart(entry);
|
||||||
|
if (start == null) continue;
|
||||||
|
if (approvedStartKeys.contains(_startKey(start))) {
|
||||||
|
overlapByUser.putIfAbsent(user, () => []).add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasOverlap = overlapByUser.isNotEmpty;
|
||||||
|
final canToggle = pending.length > 1 && !hasOverlap;
|
||||||
|
final isExpanded = expandedAttrCodes.contains(attr);
|
||||||
|
|
||||||
|
final nonOverlapPending =
|
||||||
|
pending.where((e) => !_isOverlappingStart(e, approvedStartKeys)).toList();
|
||||||
|
final baseEntries = isExpanded ? approved : [...approved, ...nonOverlapPending];
|
||||||
|
final baseSegments =
|
||||||
|
isExpanded ? approvedSegments : _segmentsForEntries(baseEntries, now);
|
||||||
|
|
||||||
|
rowSpecs.add(
|
||||||
|
_TimelineRowSpec.primary(
|
||||||
|
attrCode: attr,
|
||||||
|
segments: baseSegments,
|
||||||
|
showExpandToggle: canToggle,
|
||||||
|
isExpanded: isExpanded,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
final segments = <_ValueSegment>[];
|
allSegments.addAll(baseSegments);
|
||||||
for (int i = 0; i < items.length; i++) {
|
|
||||||
final entry = items[i];
|
|
||||||
final start = _effectiveStart(entry) ?? now;
|
|
||||||
final nextStart = i < items.length - 1
|
|
||||||
? _effectiveStart(items[i + 1])
|
|
||||||
: null;
|
|
||||||
final rawEnd = entry.validTo ?? nextStart ?? now;
|
|
||||||
final end = _safeEnd(start, rawEnd);
|
|
||||||
segments.add(
|
|
||||||
_ValueSegment(
|
|
||||||
start: start,
|
|
||||||
end: end,
|
|
||||||
value: _formatValueWithUnits(entry),
|
|
||||||
entry: entry,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
minStart = minStart == null || start.isBefore(minStart!)
|
|
||||||
? start
|
|
||||||
: minStart;
|
|
||||||
maxEnd = maxEnd == null || end.isAfter(maxEnd!) ? end : maxEnd;
|
|
||||||
}
|
|
||||||
attrSegments[attr] = segments;
|
|
||||||
});
|
|
||||||
|
|
||||||
minStart ??= now.subtract(const Duration(days: 1));
|
final shouldShowPendingRows = isExpanded || hasOverlap;
|
||||||
final effectiveMaxEnd = maxEnd ?? now;
|
if (shouldShowPendingRows) {
|
||||||
|
final users = isExpanded
|
||||||
final boundaryDates = <DateTime>{};
|
? pendingByUser.keys.toList()
|
||||||
for (final segments in attrSegments.values) {
|
: overlapByUser.keys.toList();
|
||||||
for (final seg in segments) {
|
users.sort();
|
||||||
boundaryDates.add(seg.start);
|
for (final user in users) {
|
||||||
boundaryDates.add(seg.end);
|
final pendingEntries = isExpanded
|
||||||
|
? (pendingByUser[user] ?? const [])
|
||||||
|
: (overlapByUser[user] ?? const []);
|
||||||
|
if (pendingEntries.isEmpty) continue;
|
||||||
|
final userPendingSegments = _segmentsForEntries(pendingEntries, now);
|
||||||
|
final combinedSegments = [
|
||||||
|
...approvedSegments,
|
||||||
|
...userPendingSegments,
|
||||||
|
];
|
||||||
|
rowSpecs.add(
|
||||||
|
_TimelineRowSpec.pending(
|
||||||
|
attrCode: attr,
|
||||||
|
userLabel: user,
|
||||||
|
segments: combinedSegments,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
allSegments.addAll(combinedSegments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
boundaryDates.add(effectiveMaxEnd);
|
|
||||||
var boundaries = boundaryDates.toList()..sort();
|
final boundaries = _buildBoundaries(allSegments, now);
|
||||||
if (boundaries.length < 2) {
|
|
||||||
boundaries = [minStart!, effectiveMaxEnd];
|
|
||||||
}
|
|
||||||
|
|
||||||
final axisSegments = <_AxisSegment>[];
|
final axisSegments = <_AxisSegment>[];
|
||||||
const double yearWidth = 240.0;
|
const double yearWidth = 240.0;
|
||||||
@@ -753,10 +975,10 @@ class _TimelineModel {
|
|||||||
final axisTotalWidth =
|
final axisTotalWidth =
|
||||||
axisSegments.fold<double>(0, (sum, seg) => sum + seg.width);
|
axisSegments.fold<double>(0, (sum, seg) => sum + seg.width);
|
||||||
|
|
||||||
final attrRows = <String, List<_ValueBlock>>{};
|
final rows = <_TimelineRowData>[];
|
||||||
for (final entry in attrSegments.entries) {
|
for (final spec in rowSpecs) {
|
||||||
final blocks = <_ValueBlock>[];
|
final blocks = <_ValueBlock>[];
|
||||||
for (final seg in entry.value) {
|
for (final seg in spec.segments) {
|
||||||
final left = _positionForDate(seg.start, boundaries, axisSegments);
|
final left = _positionForDate(seg.start, boundaries, axisSegments);
|
||||||
final right = _positionForDate(seg.end, boundaries, axisSegments);
|
final right = _positionForDate(seg.end, boundaries, axisSegments);
|
||||||
final span = right - left;
|
final span = right - left;
|
||||||
@@ -770,13 +992,24 @@ class _TimelineModel {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
attrRows[entry.key] = blocks;
|
rows.add(
|
||||||
|
_TimelineRowData(
|
||||||
|
id: spec.id,
|
||||||
|
attrCode: spec.attrCode,
|
||||||
|
blocks: blocks,
|
||||||
|
isPrimary: spec.isPrimary,
|
||||||
|
showExpandToggle: spec.showExpandToggle,
|
||||||
|
isExpanded: spec.isExpanded,
|
||||||
|
pendingUser: spec.userLabel,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final endLabel = _formatDate(effectiveMaxEnd) ?? 'Now';
|
final endLabel =
|
||||||
|
boundaries.isNotEmpty ? _formatDate(boundaries.last) ?? 'Now' : 'Now';
|
||||||
return _TimelineModel(
|
return _TimelineModel(
|
||||||
axisSegments: axisSegments,
|
axisSegments: axisSegments,
|
||||||
attrRows: attrRows,
|
rows: rows,
|
||||||
endLabel: endLabel,
|
endLabel: endLabel,
|
||||||
boundaries: boundaries,
|
boundaries: boundaries,
|
||||||
axisTotalWidth: axisTotalWidth,
|
axisTotalWidth: axisTotalWidth,
|
||||||
|
|||||||
Reference in New Issue
Block a user