Files
zendegi/packages/z-timeline/lib/main.dart
2026-03-02 09:09:00 +01:00

217 lines
5.7 KiB
Dart

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<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
TimelineState? _state;
List<TimelineGroup> _groups = const [];
List<TimelineEntry> _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<TimelineGroup> _convertGroups(List<TimelineGroupData> groups) {
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) {
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,
hasEnd: item.end != null,
),
);
}
}
return entries;
}
({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)),
);
}
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 payload = <String, Object?>{
'entryId': entry.id,
'newStart': newStart.toIso8601String(),
'newGroupId': newGroupId,
'newLane': newLane,
};
if (entry.hasEnd) {
final duration = entry.end.difference(entry.start);
payload['newEnd'] = newStart.add(duration).toIso8601String();
} else {
payload['newEnd'] = null;
}
emitEvent('entry_moved', payload);
}
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,
),
),
),
),
);
}
}