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; @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.timeline.groups); final entries = _convertEntries(state.timeline.groups); final domain = _computeDomain(entries); setState(() { _state = state; _groups = groups; _entries = entries; _viewport ??= TimelineViewportNotifier( start: domain.start, end: domain.end, ); }); _emitContentHeight(); } List _convertGroups(List groups) { return [for (final g in groups) TimelineGroup(id: g.id, title: g.title)]; } List _convertEntries(List groups) { final entries = []; for (final group in groups) { 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)); entries.add( TimelineEntry( id: item.id, groupId: group.id, start: start, end: end, lane: item.lane, ), ); } } return entries; } ({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 = newStart.add(duration); emitEvent('entry_moved', { 'entryId': entry.id, 'newStart': newStart.toIso8601String(), 'newEnd': newEnd.toIso8601String(), 'newGroupId': newGroupId, 'newLane': newLane, }); } 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}); } String _labelForEntry(TimelineEntry entry) { final state = _state; if (state == null) return entry.id; for (final group in state.timeline.groups) { for (final item in group.items) { if (item.id == entry.id) return item.title; } } return 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: ThemeData.dark(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, ), ), ), ), ); } }