always set lane
This commit is contained in:
@@ -64,35 +64,25 @@ class _MainAppState extends State<MainApp> {
|
||||
}
|
||||
|
||||
List<TimelineGroup> _convertGroups(List<TimelineGroupData> groups) {
|
||||
return [
|
||||
for (final g in groups) TimelineGroup(id: g.id, title: g.title),
|
||||
];
|
||||
return [for (final g in groups) TimelineGroup(id: g.id, title: g.title)];
|
||||
}
|
||||
|
||||
List<TimelineEntry> _convertEntries(List<TimelineGroupData> groups) {
|
||||
final entries = <TimelineEntry>[];
|
||||
for (final group in groups) {
|
||||
// Collect all items for this group to compute lanes
|
||||
final groupItems = group.items;
|
||||
final sorted = [...groupItems]..sort(
|
||||
(a, b) => a.start.compareTo(b.start),
|
||||
);
|
||||
|
||||
for (final item in sorted) {
|
||||
for (final item in group.items) {
|
||||
final start = DateTime.parse(item.start);
|
||||
final end = item.end != null
|
||||
? DateTime.parse(item.end!)
|
||||
: start.add(const Duration(days: 1));
|
||||
|
||||
final lane = _assignLane(entries, group.id, start, end);
|
||||
|
||||
entries.add(
|
||||
TimelineEntry(
|
||||
id: item.id,
|
||||
groupId: group.id,
|
||||
start: start,
|
||||
end: end,
|
||||
lane: lane,
|
||||
lane: item.lane,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -100,26 +90,13 @@ class _MainAppState extends State<MainApp> {
|
||||
return entries;
|
||||
}
|
||||
|
||||
int _assignLane(
|
||||
List<TimelineEntry> existing,
|
||||
String groupId,
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
) {
|
||||
final groupEntries = existing.where((e) => e.groupId == groupId);
|
||||
for (var lane = 1; lane <= 100; lane++) {
|
||||
final hasConflict = groupEntries.any(
|
||||
(e) => e.lane == lane && e.overlaps(start, end),
|
||||
);
|
||||
if (!hasConflict) return lane;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
({DateTime start, DateTime end}) _computeDomain(List<TimelineEntry> entries) {
|
||||
if (entries.isEmpty) {
|
||||
final now = DateTime.now();
|
||||
return (start: now.subtract(const Duration(days: 30)), end: now.add(const Duration(days: 30)));
|
||||
return (
|
||||
start: now.subtract(const Duration(days: 30)),
|
||||
end: now.add(const Duration(days: 30)),
|
||||
);
|
||||
}
|
||||
|
||||
var earliest = entries.first.start;
|
||||
@@ -132,10 +109,7 @@ class _MainAppState extends State<MainApp> {
|
||||
// Add 10% padding on each side
|
||||
final span = latest.difference(earliest);
|
||||
final padding = Duration(milliseconds: (span.inMilliseconds * 0.1).round());
|
||||
return (
|
||||
start: earliest.subtract(padding),
|
||||
end: latest.add(padding),
|
||||
);
|
||||
return (start: earliest.subtract(padding), end: latest.add(padding));
|
||||
}
|
||||
|
||||
void _onEntryMoved(
|
||||
@@ -144,34 +118,16 @@ class _MainAppState extends State<MainApp> {
|
||||
String newGroupId,
|
||||
int newLane,
|
||||
) {
|
||||
// Emit event to React via bridge
|
||||
final duration = entry.end.difference(entry.start);
|
||||
final newEnd = newStart.add(duration);
|
||||
|
||||
emitEvent('entry_moved', {
|
||||
'entryId': entry.id,
|
||||
'newStart': newStart.toIso8601String(),
|
||||
'newEnd': newEnd.toIso8601String(),
|
||||
'newGroupId': newGroupId,
|
||||
'newLane': newLane,
|
||||
});
|
||||
|
||||
// Update local state so Flutter UI reflects the move immediately
|
||||
setState(() {
|
||||
final duration = entry.end.difference(entry.start);
|
||||
final newEnd = newStart.add(duration);
|
||||
|
||||
_entries = [
|
||||
for (final e in _entries)
|
||||
if (e.id == entry.id)
|
||||
e.copyWith(
|
||||
groupId: newGroupId,
|
||||
start: newStart,
|
||||
end: newEnd,
|
||||
lane: newLane,
|
||||
)
|
||||
else
|
||||
e,
|
||||
];
|
||||
});
|
||||
|
||||
_emitContentHeight();
|
||||
}
|
||||
|
||||
void _emitContentHeight() {
|
||||
@@ -184,9 +140,12 @@ class _MainAppState extends State<MainApp> {
|
||||
if (e.lane > maxLane) maxLane = e.lane;
|
||||
}
|
||||
final lanesCount = maxLane.clamp(0, 1000);
|
||||
totalHeight += lanesCount * ZTimelineConstants.laneHeight
|
||||
+ (lanesCount > 0 ? (lanesCount - 1) * ZTimelineConstants.laneVerticalSpacing : 0)
|
||||
+ ZTimelineConstants.verticalOuterPadding * 2;
|
||||
totalHeight +=
|
||||
lanesCount * ZTimelineConstants.laneHeight +
|
||||
(lanesCount > 0
|
||||
? (lanesCount - 1) * ZTimelineConstants.laneVerticalSpacing
|
||||
: 0) +
|
||||
ZTimelineConstants.verticalOuterPadding * 2;
|
||||
}
|
||||
emitEvent('content_height', {'height': totalHeight});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import 'timeline_view.dart';
|
||||
|
||||
/// A drop target wrapper for a timeline group.
|
||||
///
|
||||
/// Wraps group lanes content and handles drag-and-drop operations.
|
||||
/// Wraps the entire group column (header + lanes) and handles drag-and-drop
|
||||
/// operations. The [verticalOffset] accounts for the header height and padding
|
||||
/// so that lane calculations are correct relative to the lanes stack.
|
||||
///
|
||||
/// The ghost overlay is rendered by the parent widget in the same Stack.
|
||||
class GroupDropTarget extends StatelessWidget {
|
||||
const GroupDropTarget({
|
||||
@@ -24,6 +27,7 @@ class GroupDropTarget extends StatelessWidget {
|
||||
required this.laneHeight,
|
||||
required this.lanesCount,
|
||||
required this.onEntryMoved,
|
||||
required this.verticalOffset,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
@@ -36,6 +40,11 @@ class GroupDropTarget extends StatelessWidget {
|
||||
final double laneHeight;
|
||||
final int lanesCount;
|
||||
final OnEntryMoved? onEntryMoved;
|
||||
|
||||
/// The vertical offset from the top of this widget to the top of the lanes
|
||||
/// stack. This accounts for the group header height and any padding.
|
||||
final double verticalOffset;
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
@@ -65,8 +74,12 @@ class GroupDropTarget extends StatelessWidget {
|
||||
viewport.end,
|
||||
);
|
||||
|
||||
// Subtract header + padding offset so Y is relative to the lanes stack.
|
||||
// When the cursor is over the header, adjustedY is negative and clamps
|
||||
// to lane 1.
|
||||
final adjustedY = local.dy - verticalOffset;
|
||||
final rawLane = LayoutCoordinateService.yToLane(
|
||||
y: local.dy,
|
||||
y: adjustedY,
|
||||
laneHeight: laneHeight,
|
||||
);
|
||||
final maxAllowedLane = (lanesCount <= 0 ? 1 : lanesCount) + 1;
|
||||
|
||||
@@ -84,7 +84,7 @@ class ZTimelineView extends StatelessWidget {
|
||||
projected[group.id] ?? const <ProjectedEntry>[];
|
||||
final lanesCount = _countLanes(groupEntries);
|
||||
|
||||
return Column(
|
||||
final column = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_GroupHeader(title: group.title, height: groupHeaderHeight),
|
||||
@@ -98,11 +98,31 @@ class ZTimelineView extends StatelessWidget {
|
||||
colorBuilder: colorBuilder,
|
||||
labelBuilder: labelBuilder,
|
||||
contentWidth: contentWidth,
|
||||
onEntryMoved: onEntryMoved,
|
||||
enableDrag: enableDrag,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Wrap the entire group (header + lanes) in a DragTarget
|
||||
// so dragging over headers still updates the ghost position.
|
||||
if (enableDrag && onEntryMoved != null) {
|
||||
return GroupDropTarget(
|
||||
group: group,
|
||||
entries: groupEntries,
|
||||
allEntries: entries,
|
||||
viewport: viewport,
|
||||
contentWidth: contentWidth,
|
||||
laneHeight: laneHeight,
|
||||
lanesCount: lanesCount,
|
||||
onEntryMoved: onEntryMoved,
|
||||
verticalOffset:
|
||||
groupHeaderHeight +
|
||||
ZTimelineConstants.verticalOuterPadding,
|
||||
child: column,
|
||||
);
|
||||
}
|
||||
|
||||
return column;
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -157,7 +177,6 @@ class _GroupLanes extends StatelessWidget {
|
||||
required this.labelBuilder,
|
||||
required this.colorBuilder,
|
||||
required this.contentWidth,
|
||||
required this.onEntryMoved,
|
||||
required this.enableDrag,
|
||||
});
|
||||
|
||||
@@ -170,7 +189,6 @@ class _GroupLanes extends StatelessWidget {
|
||||
final EntryLabelBuilder labelBuilder;
|
||||
final EntryColorBuilder colorBuilder;
|
||||
final double contentWidth;
|
||||
final OnEntryMoved? onEntryMoved;
|
||||
final bool enableDrag;
|
||||
|
||||
@override
|
||||
@@ -264,21 +282,6 @@ class _GroupLanes extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
|
||||
// Wrap with DragTarget if drag is enabled
|
||||
if (enableDrag && onEntryMoved != null) {
|
||||
innerStack = GroupDropTarget(
|
||||
group: group,
|
||||
entries: entries,
|
||||
allEntries: allEntries,
|
||||
viewport: viewport,
|
||||
contentWidth: contentWidth,
|
||||
laneHeight: laneHeight,
|
||||
lanesCount: lanesCount,
|
||||
onEntryMoved: onEntryMoved,
|
||||
child: innerStack,
|
||||
);
|
||||
}
|
||||
|
||||
return AnimatedSize(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeInOut,
|
||||
|
||||
@@ -61,6 +61,7 @@ class TimelineItemData {
|
||||
final String? description;
|
||||
final String start;
|
||||
final String? end;
|
||||
final int lane;
|
||||
|
||||
TimelineItemData({
|
||||
required this.id,
|
||||
@@ -68,6 +69,7 @@ class TimelineItemData {
|
||||
this.description,
|
||||
required this.start,
|
||||
this.end,
|
||||
required this.lane,
|
||||
});
|
||||
|
||||
factory TimelineItemData.fromJson(Map<String, dynamic> json) {
|
||||
@@ -77,6 +79,7 @@ class TimelineItemData {
|
||||
description: json['description'] as String?,
|
||||
start: json['start'] as String,
|
||||
end: json['end'] as String?,
|
||||
lane: json['lane'] as int,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user