import 'package:flutter/material.dart'; import 'bridge.dart'; import 'state.dart'; import 'timeline.dart'; void main() { runApp(const MainApp()); } class MainApp extends StatefulWidget { const MainApp({super.key}); @override State createState() => _MainAppState(); } class _MainAppState extends State { TimelineState? _state; List _groups = const []; List _entries = const []; TimelineViewportNotifier? _viewport; bool _darkMode = true; @override void initState() { super.initState(); _initBridge(); } @override void dispose() { _viewport?.dispose(); super.dispose(); } void _initBridge() { final initial = readInitialState(); if (initial != null) { _applyState(TimelineState.fromJson(initial)); } onStateUpdated((json) { _applyState(TimelineState.fromJson(json)); }); } void _applyState(TimelineState state) { final groups = _convertGroups(state); final entries = _convertEntries(state); final domain = _computeDomain(entries); setState(() { _state = state; _groups = groups; _entries = entries; _darkMode = state.darkMode; _viewport ??= TimelineViewportNotifier( start: domain.start, end: domain.end, ); }); _emitContentHeight(); } /// Build an ordered list of [TimelineGroup] using [groupOrder]. List _convertGroups(TimelineState state) { return [ for (final id in state.groupOrder) if (state.groups[id] case final g?) TimelineGroup(id: g.id, title: g.title), ]; } /// Build a flat list of [TimelineEntry] from the normalized items map. List _convertEntries(TimelineState state) { return [ for (final item in state.items.values) () { final start = DateTime.parse(item.start); final end = item.end != null ? DateTime.parse(item.end!) : start.add(const Duration(days: 1)); return TimelineEntry( id: item.id, groupId: item.groupId, start: start, end: end, lane: item.lane, hasEnd: item.end != null, ); }(), ]; } ({DateTime start, DateTime end}) _computeDomain(List entries) { if (entries.isEmpty) { final now = DateTime.now(); return ( start: now.subtract(const Duration(days: 30)), end: now.add(const Duration(days: 30)), ); } var earliest = entries.first.start; var latest = entries.first.end; for (final e in entries) { if (e.start.isBefore(earliest)) earliest = e.start; if (e.end.isAfter(latest)) latest = e.end; } // 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)); } void _onEntryMoved( TimelineEntry entry, DateTime newStart, String newGroupId, int newLane, ) { final duration = entry.end.difference(entry.start); final newEnd = entry.hasEnd ? newStart.add(duration) : null; // Optimistic update – apply locally before the host round-trips. if (_state case final state?) { final oldItem = state.items[entry.id]; if (oldItem != null) { final updatedItems = Map.of(state.items); updatedItems[entry.id] = TimelineItemData( id: oldItem.id, groupId: newGroupId, title: oldItem.title, description: oldItem.description, start: newStart.toIso8601String(), end: newEnd?.toIso8601String(), lane: newLane, ); final updatedState = TimelineState( timeline: state.timeline, groups: state.groups, items: updatedItems, groupOrder: state.groupOrder, selectedItemId: state.selectedItemId, ); _applyState(updatedState); } } emitEvent('entry_moved', { 'entryId': entry.id, 'newStart': newStart.toIso8601String(), 'newGroupId': newGroupId, 'newLane': newLane, 'newEnd': newEnd?.toIso8601String(), }); } void _emitContentHeight() { var totalHeight = 0.0; for (final group in _groups) { totalHeight += ZTimelineConstants.groupHeaderHeight; final groupEntries = _entries.where((e) => e.groupId == group.id); var maxLane = 0; for (final e in groupEntries) { 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; } emitEvent('content_height', {'height': totalHeight}); } /// O(1) label lookup from the normalized items map. String _labelForEntry(TimelineEntry entry) { return _state?.items[entry.id]?.title ?? entry.id; } static const _groupColors = [ Color(0xFF4285F4), // blue Color(0xFF34A853), // green Color(0xFFFBBC04), // yellow Color(0xFFEA4335), // red Color(0xFF9C27B0), // purple Color(0xFF00BCD4), // cyan Color(0xFFFF9800), // orange Color(0xFF795548), // brown ]; Color _colorForEntry(TimelineEntry entry) { final groupIndex = _groups.indexWhere((g) => g.id == entry.groupId); if (groupIndex < 0) return _groupColors[0]; return _groupColors[groupIndex % _groupColors.length]; } @override Widget build(BuildContext context) { final viewport = _viewport; return MaterialApp( debugShowCheckedModeBanner: false, theme: _darkMode ? ThemeData.dark(useMaterial3: true) : ThemeData.light(useMaterial3: true), home: Scaffold( backgroundColor: Colors.transparent, body: _state == null || viewport == null ? const Center(child: Text('Waiting for state...')) : ZTimelineScope( viewport: viewport, child: ZTimelineInteractor( child: ZTimelineView( groups: _groups, entries: _entries, viewport: viewport, labelBuilder: _labelForEntry, colorBuilder: _colorForEntry, enableDrag: true, onEntryMoved: _onEntryMoved, ), ), ), ), ); } }