neoagent 3.0.1-beta.31 → 3.0.1-beta.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
part of 'main.dart';
|
|
2
2
|
|
|
3
|
-
const double _timelineAxisHeaderHeight = 56;
|
|
4
|
-
const double _timelineLaneLabelWidth = 132;
|
|
5
|
-
const double _timelinePlotRightPadding = 84;
|
|
6
|
-
const double _timelineLanePadding = 18;
|
|
7
|
-
const double _timelineLaneGap = 18;
|
|
8
|
-
const double _timelineNodeWidth = 208;
|
|
9
|
-
const double _timelineNodeHeight = 88;
|
|
10
|
-
const double _timelineNodeGap = 18;
|
|
11
|
-
const double _timelineCanvasBottomPadding = 28;
|
|
12
|
-
const double _timelinePlotInset = _timelineNodeWidth / 2;
|
|
13
|
-
|
|
14
3
|
class TimelinePanel extends StatefulWidget {
|
|
15
4
|
const TimelinePanel({super.key, required this.controller});
|
|
16
5
|
|
|
@@ -21,21 +10,7 @@ class TimelinePanel extends StatefulWidget {
|
|
|
21
10
|
}
|
|
22
11
|
|
|
23
12
|
class _TimelinePanelState extends State<TimelinePanel> {
|
|
24
|
-
final TransformationController _viewportController =
|
|
25
|
-
TransformationController();
|
|
26
|
-
|
|
27
13
|
int? _selectedEventId;
|
|
28
|
-
int? _hoveredEventId;
|
|
29
|
-
|
|
30
|
-
@override
|
|
31
|
-
void dispose() {
|
|
32
|
-
_viewportController.dispose();
|
|
33
|
-
super.dispose();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
void _resetViewport() {
|
|
37
|
-
_viewportController.value = Matrix4.identity();
|
|
38
|
-
}
|
|
39
14
|
|
|
40
15
|
void _selectEvent(TimelineEventItem item) {
|
|
41
16
|
if (_selectedEventId == item.id) {
|
|
@@ -46,15 +21,6 @@ class _TimelinePanelState extends State<TimelinePanel> {
|
|
|
46
21
|
});
|
|
47
22
|
}
|
|
48
23
|
|
|
49
|
-
void _setHoveredEvent(int? eventId) {
|
|
50
|
-
if (_hoveredEventId == eventId) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
setState(() {
|
|
54
|
-
_hoveredEventId = eventId;
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
24
|
void _moveSelection(List<TimelineEventItem> items, int offset) {
|
|
59
25
|
if (items.isEmpty) {
|
|
60
26
|
return;
|
|
@@ -81,13 +47,14 @@ class _TimelinePanelState extends State<TimelinePanel> {
|
|
|
81
47
|
return item;
|
|
82
48
|
}
|
|
83
49
|
}
|
|
84
|
-
_selectedEventId = items.
|
|
85
|
-
return items.
|
|
50
|
+
_selectedEventId = items.first.id;
|
|
51
|
+
return items.first;
|
|
86
52
|
}
|
|
87
53
|
|
|
88
54
|
@override
|
|
89
55
|
Widget build(BuildContext context) {
|
|
90
56
|
final items = _sortedTimelineEvents(widget.controller.timelineItems);
|
|
57
|
+
final groups = _groupTimelineEvents(items);
|
|
91
58
|
final selectedEvent = _resolveSelectedEvent(items);
|
|
92
59
|
final selectedIndex = selectedEvent == null
|
|
93
60
|
? -1
|
|
@@ -98,71 +65,12 @@ class _TimelinePanelState extends State<TimelinePanel> {
|
|
|
98
65
|
child: Column(
|
|
99
66
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
100
67
|
children: <Widget>[
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
),
|
|
106
|
-
const SizedBox(height: 12),
|
|
107
|
-
Wrap(
|
|
108
|
-
spacing: 10,
|
|
109
|
-
runSpacing: 10,
|
|
110
|
-
children: <Widget>[
|
|
111
|
-
for (final filter in const <({String id, String label})>[
|
|
112
|
-
(id: 'screen', label: 'Screen'),
|
|
113
|
-
(id: 'tasks', label: 'Tasks'),
|
|
114
|
-
(id: 'runs', label: 'Runs'),
|
|
115
|
-
])
|
|
116
|
-
FilterChip(
|
|
117
|
-
selected: widget.controller.selectedTimelineSources.contains(
|
|
118
|
-
filter.id,
|
|
119
|
-
),
|
|
120
|
-
label: Text(filter.label),
|
|
121
|
-
onSelected: (_) =>
|
|
122
|
-
widget.controller.toggleTimelineSource(filter.id),
|
|
123
|
-
),
|
|
124
|
-
OutlinedButton.icon(
|
|
125
|
-
onPressed: widget.controller.isRefreshingTimeline
|
|
126
|
-
? null
|
|
127
|
-
: widget.controller.refreshTimeline,
|
|
128
|
-
icon: widget.controller.isRefreshingTimeline
|
|
129
|
-
? const SizedBox.square(
|
|
130
|
-
dimension: 14,
|
|
131
|
-
child: CircularProgressIndicator(strokeWidth: 2),
|
|
132
|
-
)
|
|
133
|
-
: const Icon(Icons.sync_outlined),
|
|
134
|
-
label: const Text('Refresh'),
|
|
135
|
-
),
|
|
136
|
-
OutlinedButton.icon(
|
|
137
|
-
onPressed: items.isEmpty ? null : _resetViewport,
|
|
138
|
-
icon: const Icon(Icons.center_focus_strong_rounded),
|
|
139
|
-
label: const Text('Reset view'),
|
|
140
|
-
),
|
|
141
|
-
],
|
|
68
|
+
_TimelineHeroHeader(
|
|
69
|
+
items: items,
|
|
70
|
+
selectedEvent: selectedEvent,
|
|
71
|
+
controller: widget.controller,
|
|
142
72
|
),
|
|
143
|
-
|
|
144
|
-
const SizedBox(height: 12),
|
|
145
|
-
Wrap(
|
|
146
|
-
spacing: 10,
|
|
147
|
-
runSpacing: 10,
|
|
148
|
-
children: <Widget>[
|
|
149
|
-
_MetaPill(
|
|
150
|
-
label: '${items.length} events',
|
|
151
|
-
icon: Icons.bubble_chart_outlined,
|
|
152
|
-
),
|
|
153
|
-
_MetaPill(
|
|
154
|
-
label: _formatTimelineRange(items.first, items.last),
|
|
155
|
-
icon: Icons.schedule_outlined,
|
|
156
|
-
),
|
|
157
|
-
_MetaPill(
|
|
158
|
-
label: 'Drag to pan, scroll or pinch to zoom',
|
|
159
|
-
icon: Icons.pan_tool_alt_outlined,
|
|
160
|
-
color: _info,
|
|
161
|
-
),
|
|
162
|
-
],
|
|
163
|
-
),
|
|
164
|
-
],
|
|
165
|
-
const SizedBox(height: 16),
|
|
73
|
+
const SizedBox(height: 18),
|
|
166
74
|
Expanded(
|
|
167
75
|
child: items.isEmpty
|
|
168
76
|
? Card(
|
|
@@ -178,16 +86,13 @@ class _TimelinePanelState extends State<TimelinePanel> {
|
|
|
178
86
|
)
|
|
179
87
|
: LayoutBuilder(
|
|
180
88
|
builder: (context, constraints) {
|
|
181
|
-
final isWide = constraints.maxWidth >=
|
|
182
|
-
final
|
|
183
|
-
|
|
89
|
+
final isWide = constraints.maxWidth >= 1180;
|
|
90
|
+
final feedPane = _TimelineFeedPane(
|
|
91
|
+
groups: groups,
|
|
184
92
|
selectedEventId: selectedEvent?.id,
|
|
185
|
-
hoveredEventId: _hoveredEventId,
|
|
186
|
-
viewportController: _viewportController,
|
|
187
93
|
onSelectEvent: _selectEvent,
|
|
188
|
-
onHoverEvent: _setHoveredEvent,
|
|
189
94
|
);
|
|
190
|
-
final detailPane =
|
|
95
|
+
final detailPane = _TimelineDetailPane(
|
|
191
96
|
items: items,
|
|
192
97
|
selectedEvent: selectedEvent,
|
|
193
98
|
selectedIndex: selectedIndex,
|
|
@@ -214,18 +119,19 @@ class _TimelinePanelState extends State<TimelinePanel> {
|
|
|
214
119
|
return Row(
|
|
215
120
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
216
121
|
children: <Widget>[
|
|
217
|
-
Expanded(flex:
|
|
122
|
+
Expanded(flex: 10, child: feedPane),
|
|
218
123
|
const SizedBox(width: 16),
|
|
219
|
-
SizedBox(width:
|
|
124
|
+
SizedBox(width: 420, child: detailPane),
|
|
220
125
|
],
|
|
221
126
|
);
|
|
222
127
|
}
|
|
128
|
+
|
|
223
129
|
return Column(
|
|
224
130
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
225
131
|
children: <Widget>[
|
|
226
|
-
Expanded(flex:
|
|
132
|
+
Expanded(flex: 11, child: feedPane),
|
|
227
133
|
const SizedBox(height: 16),
|
|
228
|
-
Expanded(flex:
|
|
134
|
+
Expanded(flex: 8, child: detailPane),
|
|
229
135
|
],
|
|
230
136
|
);
|
|
231
137
|
},
|
|
@@ -237,868 +143,1113 @@ class _TimelinePanelState extends State<TimelinePanel> {
|
|
|
237
143
|
}
|
|
238
144
|
}
|
|
239
145
|
|
|
240
|
-
class
|
|
241
|
-
const
|
|
146
|
+
class _TimelineHeroHeader extends StatelessWidget {
|
|
147
|
+
const _TimelineHeroHeader({
|
|
242
148
|
required this.items,
|
|
149
|
+
required this.selectedEvent,
|
|
150
|
+
required this.controller,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
final List<TimelineEventItem> items;
|
|
154
|
+
final TimelineEventItem? selectedEvent;
|
|
155
|
+
final NeoAgentController controller;
|
|
156
|
+
|
|
157
|
+
@override
|
|
158
|
+
Widget build(BuildContext context) {
|
|
159
|
+
final focusedDay =
|
|
160
|
+
selectedEvent?.occurredAt ??
|
|
161
|
+
(items.isEmpty ? null : items.first.occurredAt);
|
|
162
|
+
|
|
163
|
+
return Container(
|
|
164
|
+
width: double.infinity,
|
|
165
|
+
padding: const EdgeInsets.all(22),
|
|
166
|
+
decoration: BoxDecoration(
|
|
167
|
+
gradient: LinearGradient(
|
|
168
|
+
colors: <Color>[
|
|
169
|
+
_bgSecondary.withValues(alpha: 0.96),
|
|
170
|
+
_bgPrimary.withValues(alpha: 0.9),
|
|
171
|
+
],
|
|
172
|
+
begin: Alignment.topLeft,
|
|
173
|
+
end: Alignment.bottomRight,
|
|
174
|
+
),
|
|
175
|
+
borderRadius: BorderRadius.circular(28),
|
|
176
|
+
border: Border.all(color: _borderLight),
|
|
177
|
+
boxShadow: <BoxShadow>[
|
|
178
|
+
BoxShadow(
|
|
179
|
+
color: Colors.black.withValues(alpha: 0.12),
|
|
180
|
+
blurRadius: 26,
|
|
181
|
+
offset: const Offset(0, 16),
|
|
182
|
+
),
|
|
183
|
+
],
|
|
184
|
+
),
|
|
185
|
+
child: Column(
|
|
186
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
187
|
+
children: <Widget>[
|
|
188
|
+
LayoutBuilder(
|
|
189
|
+
builder: (context, constraints) {
|
|
190
|
+
final isWide = constraints.maxWidth >= 940;
|
|
191
|
+
final heading = Column(
|
|
192
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
193
|
+
children: <Widget>[
|
|
194
|
+
Text(
|
|
195
|
+
'ACTIVITY FEED',
|
|
196
|
+
style: TextStyle(
|
|
197
|
+
color: _accentHover,
|
|
198
|
+
fontSize: 13,
|
|
199
|
+
fontWeight: FontWeight.w700,
|
|
200
|
+
letterSpacing: 4.2,
|
|
201
|
+
),
|
|
202
|
+
),
|
|
203
|
+
const SizedBox(height: 14),
|
|
204
|
+
const Text(
|
|
205
|
+
'Timeline',
|
|
206
|
+
style: TextStyle(
|
|
207
|
+
fontSize: 40,
|
|
208
|
+
fontWeight: FontWeight.w800,
|
|
209
|
+
height: 1,
|
|
210
|
+
),
|
|
211
|
+
),
|
|
212
|
+
const SizedBox(height: 14),
|
|
213
|
+
Text(
|
|
214
|
+
'Emails, AI actions, recordings, tasks and run activity in one chronological feed.',
|
|
215
|
+
style: TextStyle(
|
|
216
|
+
color: _textSecondary,
|
|
217
|
+
fontSize: 16.5,
|
|
218
|
+
height: 1.35,
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
],
|
|
222
|
+
);
|
|
223
|
+
final summary = Wrap(
|
|
224
|
+
spacing: 10,
|
|
225
|
+
runSpacing: 10,
|
|
226
|
+
alignment: isWide ? WrapAlignment.end : WrapAlignment.start,
|
|
227
|
+
children: <Widget>[
|
|
228
|
+
if (items.isNotEmpty)
|
|
229
|
+
_TimelineStatPill(
|
|
230
|
+
icon: Icons.event_note_rounded,
|
|
231
|
+
label: '${items.length} events',
|
|
232
|
+
),
|
|
233
|
+
if (items.length > 1)
|
|
234
|
+
_TimelineStatPill(
|
|
235
|
+
icon: Icons.schedule_outlined,
|
|
236
|
+
label: _formatTimelineRange(items.first, items.last),
|
|
237
|
+
),
|
|
238
|
+
if (focusedDay != null)
|
|
239
|
+
_TimelineStatPill(
|
|
240
|
+
icon: Icons.calendar_today_outlined,
|
|
241
|
+
label: _formatTimelineDate(focusedDay.toLocal()),
|
|
242
|
+
),
|
|
243
|
+
],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (isWide) {
|
|
247
|
+
return Row(
|
|
248
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
249
|
+
children: <Widget>[
|
|
250
|
+
Expanded(child: heading),
|
|
251
|
+
const SizedBox(width: 20),
|
|
252
|
+
ConstrainedBox(
|
|
253
|
+
constraints: const BoxConstraints(maxWidth: 440),
|
|
254
|
+
child: Align(
|
|
255
|
+
alignment: Alignment.topRight,
|
|
256
|
+
child: summary,
|
|
257
|
+
),
|
|
258
|
+
),
|
|
259
|
+
],
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return Column(
|
|
264
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
265
|
+
children: <Widget>[
|
|
266
|
+
heading,
|
|
267
|
+
const SizedBox(height: 18),
|
|
268
|
+
summary,
|
|
269
|
+
],
|
|
270
|
+
);
|
|
271
|
+
},
|
|
272
|
+
),
|
|
273
|
+
const SizedBox(height: 18),
|
|
274
|
+
Wrap(
|
|
275
|
+
spacing: 10,
|
|
276
|
+
runSpacing: 10,
|
|
277
|
+
children: <Widget>[
|
|
278
|
+
for (final filter in const <({String id, String label})>[
|
|
279
|
+
(id: 'screen', label: 'Screen'),
|
|
280
|
+
(id: 'tasks', label: 'Tasks'),
|
|
281
|
+
(id: 'runs', label: 'Runs'),
|
|
282
|
+
])
|
|
283
|
+
FilterChip(
|
|
284
|
+
selected: controller.selectedTimelineSources.contains(
|
|
285
|
+
filter.id,
|
|
286
|
+
),
|
|
287
|
+
label: Text(filter.label),
|
|
288
|
+
onSelected: (_) => controller.toggleTimelineSource(filter.id),
|
|
289
|
+
avatar: Icon(
|
|
290
|
+
_timelineLaneIcon(filter.id),
|
|
291
|
+
size: 16,
|
|
292
|
+
color:
|
|
293
|
+
controller.selectedTimelineSources.contains(filter.id)
|
|
294
|
+
? _bgSecondary
|
|
295
|
+
: _sourceColorForKind(filter.id),
|
|
296
|
+
),
|
|
297
|
+
),
|
|
298
|
+
OutlinedButton.icon(
|
|
299
|
+
onPressed: controller.isRefreshingTimeline
|
|
300
|
+
? null
|
|
301
|
+
: controller.refreshTimeline,
|
|
302
|
+
icon: controller.isRefreshingTimeline
|
|
303
|
+
? const SizedBox.square(
|
|
304
|
+
dimension: 14,
|
|
305
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
306
|
+
)
|
|
307
|
+
: const Icon(Icons.sync_outlined),
|
|
308
|
+
label: const Text('Refresh'),
|
|
309
|
+
),
|
|
310
|
+
],
|
|
311
|
+
),
|
|
312
|
+
],
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
class _TimelineFeedPane extends StatelessWidget {
|
|
319
|
+
const _TimelineFeedPane({
|
|
320
|
+
required this.groups,
|
|
243
321
|
required this.selectedEventId,
|
|
244
|
-
required this.hoveredEventId,
|
|
245
|
-
required this.viewportController,
|
|
246
322
|
required this.onSelectEvent,
|
|
247
|
-
required this.onHoverEvent,
|
|
248
323
|
});
|
|
249
324
|
|
|
250
|
-
final List<
|
|
325
|
+
final List<_TimelineDayGroup> groups;
|
|
251
326
|
final int? selectedEventId;
|
|
252
|
-
final int? hoveredEventId;
|
|
253
|
-
final TransformationController viewportController;
|
|
254
327
|
final ValueChanged<TimelineEventItem> onSelectEvent;
|
|
255
|
-
final ValueChanged<int?> onHoverEvent;
|
|
256
328
|
|
|
257
329
|
@override
|
|
258
330
|
Widget build(BuildContext context) {
|
|
259
331
|
return Card(
|
|
260
332
|
clipBehavior: Clip.antiAlias,
|
|
261
|
-
child:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
333
|
+
child: Column(
|
|
334
|
+
children: <Widget>[
|
|
335
|
+
Container(
|
|
336
|
+
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
|
337
|
+
decoration: BoxDecoration(
|
|
338
|
+
color: _bgSecondary.withValues(alpha: 0.78),
|
|
339
|
+
border: Border(bottom: BorderSide(color: _border)),
|
|
340
|
+
),
|
|
341
|
+
child: Row(
|
|
267
342
|
children: <Widget>[
|
|
268
343
|
Expanded(
|
|
269
|
-
child:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const Text(
|
|
273
|
-
'Timeline map',
|
|
274
|
-
style: TextStyle(
|
|
275
|
-
fontSize: 16,
|
|
276
|
-
fontWeight: FontWeight.w800,
|
|
277
|
-
),
|
|
278
|
-
),
|
|
279
|
-
const SizedBox(height: 4),
|
|
280
|
-
Text(
|
|
281
|
-
'Events are placed by real timestamp and grouped into source lanes.',
|
|
282
|
-
style: TextStyle(color: _textSecondary, height: 1.35),
|
|
283
|
-
),
|
|
284
|
-
],
|
|
344
|
+
child: Text(
|
|
345
|
+
'Timeline feed',
|
|
346
|
+
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800),
|
|
285
347
|
),
|
|
286
348
|
),
|
|
349
|
+
Text(
|
|
350
|
+
'${groups.fold<int>(0, (sum, group) => sum + group.items.length)} entries',
|
|
351
|
+
style: TextStyle(color: _textMuted, fontSize: 12.5),
|
|
352
|
+
),
|
|
353
|
+
],
|
|
354
|
+
),
|
|
355
|
+
),
|
|
356
|
+
Expanded(
|
|
357
|
+
child: ListView.builder(
|
|
358
|
+
padding: const EdgeInsets.all(0),
|
|
359
|
+
itemCount: groups.length,
|
|
360
|
+
itemBuilder: (context, index) {
|
|
361
|
+
final group = groups[index];
|
|
362
|
+
return _TimelineDaySection(
|
|
363
|
+
group: group,
|
|
364
|
+
selectedEventId: selectedEventId,
|
|
365
|
+
onSelectEvent: onSelectEvent,
|
|
366
|
+
);
|
|
367
|
+
},
|
|
368
|
+
),
|
|
369
|
+
),
|
|
370
|
+
],
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
class _TimelineDaySection extends StatelessWidget {
|
|
377
|
+
const _TimelineDaySection({
|
|
378
|
+
required this.group,
|
|
379
|
+
required this.selectedEventId,
|
|
380
|
+
required this.onSelectEvent,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
final _TimelineDayGroup group;
|
|
384
|
+
final int? selectedEventId;
|
|
385
|
+
final ValueChanged<TimelineEventItem> onSelectEvent;
|
|
386
|
+
|
|
387
|
+
@override
|
|
388
|
+
Widget build(BuildContext context) {
|
|
389
|
+
return Container(
|
|
390
|
+
decoration: BoxDecoration(
|
|
391
|
+
border: Border(bottom: BorderSide(color: _border)),
|
|
392
|
+
),
|
|
393
|
+
child: Column(
|
|
394
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
395
|
+
children: <Widget>[
|
|
396
|
+
Padding(
|
|
397
|
+
padding: const EdgeInsets.fromLTRB(20, 18, 20, 14),
|
|
398
|
+
child: Row(
|
|
399
|
+
children: <Widget>[
|
|
287
400
|
Container(
|
|
288
401
|
padding: const EdgeInsets.symmetric(
|
|
289
|
-
horizontal:
|
|
402
|
+
horizontal: 14,
|
|
290
403
|
vertical: 8,
|
|
291
404
|
),
|
|
292
405
|
decoration: BoxDecoration(
|
|
293
|
-
color:
|
|
406
|
+
color: group.isToday ? _accent : _bgTertiary,
|
|
294
407
|
borderRadius: BorderRadius.circular(999),
|
|
295
|
-
border: Border.all(color: _borderLight),
|
|
296
408
|
),
|
|
297
|
-
child:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
409
|
+
child: Text(
|
|
410
|
+
group.label,
|
|
411
|
+
style: TextStyle(
|
|
412
|
+
color: group.isToday ? _bgSecondary : _textPrimary,
|
|
413
|
+
fontWeight: FontWeight.w800,
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
),
|
|
417
|
+
const SizedBox(width: 14),
|
|
418
|
+
Expanded(
|
|
419
|
+
child: Text(
|
|
420
|
+
group.dateLabel.toUpperCase(),
|
|
421
|
+
style: TextStyle(
|
|
422
|
+
color: _textMuted,
|
|
423
|
+
fontSize: 12.5,
|
|
424
|
+
fontWeight: FontWeight.w700,
|
|
425
|
+
letterSpacing: 3,
|
|
426
|
+
),
|
|
311
427
|
),
|
|
312
428
|
),
|
|
429
|
+
Text(
|
|
430
|
+
'${group.items.length} events',
|
|
431
|
+
style: TextStyle(color: _textMuted, fontSize: 12.5),
|
|
432
|
+
),
|
|
313
433
|
],
|
|
314
434
|
),
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return ClipRRect(
|
|
324
|
-
borderRadius: BorderRadius.circular(18),
|
|
325
|
-
child: DecoratedBox(
|
|
326
|
-
decoration: BoxDecoration(
|
|
327
|
-
gradient: LinearGradient(
|
|
328
|
-
colors: <Color>[
|
|
329
|
-
_bgSecondary.withValues(alpha: 0.92),
|
|
330
|
-
_bgCard.withValues(alpha: 0.78),
|
|
331
|
-
],
|
|
332
|
-
begin: Alignment.topLeft,
|
|
333
|
-
end: Alignment.bottomRight,
|
|
334
|
-
),
|
|
335
|
-
border: Border.all(color: _borderLight),
|
|
336
|
-
),
|
|
337
|
-
child: InteractiveViewer(
|
|
338
|
-
transformationController: viewportController,
|
|
339
|
-
boundaryMargin: const EdgeInsets.all(160),
|
|
340
|
-
minScale: 0.65,
|
|
341
|
-
maxScale: 2.8,
|
|
342
|
-
panEnabled: true,
|
|
343
|
-
scaleEnabled: true,
|
|
344
|
-
constrained: false,
|
|
345
|
-
trackpadScrollCausesScale: true,
|
|
346
|
-
child: SizedBox(
|
|
347
|
-
width: scene.canvasWidth,
|
|
348
|
-
height: scene.canvasHeight,
|
|
349
|
-
child: Stack(
|
|
350
|
-
children: <Widget>[
|
|
351
|
-
Positioned.fill(
|
|
352
|
-
child: CustomPaint(
|
|
353
|
-
painter: _TimelineScenePainter(
|
|
354
|
-
scene: scene,
|
|
355
|
-
selectedEventId: selectedEventId,
|
|
356
|
-
),
|
|
357
|
-
),
|
|
358
|
-
),
|
|
359
|
-
for (final lane in scene.lanes)
|
|
360
|
-
Positioned(
|
|
361
|
-
left: 16,
|
|
362
|
-
top: lane.top + 14,
|
|
363
|
-
child: _TimelineLaneBadge(
|
|
364
|
-
label: lane.label,
|
|
365
|
-
color: lane.color,
|
|
366
|
-
icon: _timelineLaneIcon(lane.sourceKind),
|
|
367
|
-
),
|
|
368
|
-
),
|
|
369
|
-
for (final entry in scene.entries)
|
|
370
|
-
Positioned(
|
|
371
|
-
left: entry.left,
|
|
372
|
-
top: entry.top,
|
|
373
|
-
width: _timelineNodeWidth,
|
|
374
|
-
height: _timelineNodeHeight,
|
|
375
|
-
child: _TimelineEventNode(
|
|
376
|
-
item: entry.item,
|
|
377
|
-
isSelected:
|
|
378
|
-
entry.item.id == selectedEventId,
|
|
379
|
-
isHovered: entry.item.id == hoveredEventId,
|
|
380
|
-
onTap: () => onSelectEvent(entry.item),
|
|
381
|
-
onHoverChanged: (hovering) => onHoverEvent(
|
|
382
|
-
hovering ? entry.item.id : null,
|
|
383
|
-
),
|
|
384
|
-
),
|
|
385
|
-
),
|
|
386
|
-
],
|
|
387
|
-
),
|
|
388
|
-
),
|
|
389
|
-
),
|
|
390
|
-
),
|
|
391
|
-
);
|
|
392
|
-
},
|
|
393
|
-
),
|
|
435
|
+
),
|
|
436
|
+
for (var index = 0; index < group.items.length; index++)
|
|
437
|
+
_TimelineFeedRow(
|
|
438
|
+
item: group.items[index],
|
|
439
|
+
isSelected: group.items[index].id == selectedEventId,
|
|
440
|
+
isFirst: index == 0,
|
|
441
|
+
isLast: index == group.items.length - 1,
|
|
442
|
+
onTap: () => onSelectEvent(group.items[index]),
|
|
394
443
|
),
|
|
395
|
-
|
|
396
|
-
),
|
|
444
|
+
],
|
|
397
445
|
),
|
|
398
446
|
);
|
|
399
447
|
}
|
|
400
448
|
}
|
|
401
449
|
|
|
402
|
-
class
|
|
403
|
-
const
|
|
404
|
-
required this.
|
|
405
|
-
required this.
|
|
406
|
-
required this.
|
|
407
|
-
this.
|
|
408
|
-
this.
|
|
409
|
-
this.onOpenRun,
|
|
450
|
+
class _TimelineFeedRow extends StatelessWidget {
|
|
451
|
+
const _TimelineFeedRow({
|
|
452
|
+
required this.item,
|
|
453
|
+
required this.isSelected,
|
|
454
|
+
required this.isFirst,
|
|
455
|
+
required this.isLast,
|
|
456
|
+
required this.onTap,
|
|
410
457
|
});
|
|
411
458
|
|
|
412
|
-
final
|
|
413
|
-
final
|
|
414
|
-
final
|
|
415
|
-
final
|
|
416
|
-
final VoidCallback
|
|
417
|
-
final VoidCallback? onOpenRun;
|
|
459
|
+
final TimelineEventItem item;
|
|
460
|
+
final bool isSelected;
|
|
461
|
+
final bool isFirst;
|
|
462
|
+
final bool isLast;
|
|
463
|
+
final VoidCallback onTap;
|
|
418
464
|
|
|
419
465
|
@override
|
|
420
466
|
Widget build(BuildContext context) {
|
|
421
|
-
final
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
467
|
+
final sourceColor = item.sourceColor;
|
|
468
|
+
final chips = _timelineEventChips(item);
|
|
469
|
+
|
|
470
|
+
return Material(
|
|
471
|
+
color: isSelected
|
|
472
|
+
? sourceColor.withValues(alpha: 0.08)
|
|
473
|
+
: Colors.transparent,
|
|
474
|
+
child: InkWell(
|
|
475
|
+
onTap: onTap,
|
|
476
|
+
child: Padding(
|
|
477
|
+
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
|
478
|
+
child: Row(
|
|
479
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
480
|
+
children: <Widget>[
|
|
481
|
+
SizedBox(
|
|
482
|
+
width: 82,
|
|
483
|
+
child: Padding(
|
|
484
|
+
padding: const EdgeInsets.only(top: 28),
|
|
431
485
|
child: Text(
|
|
432
|
-
|
|
433
|
-
style: TextStyle(
|
|
486
|
+
_formatTimelineTime(item.occurredAt),
|
|
487
|
+
style: TextStyle(
|
|
488
|
+
color: isSelected ? _textPrimary : _textMuted,
|
|
489
|
+
fontSize: 14,
|
|
490
|
+
fontWeight: FontWeight.w600,
|
|
491
|
+
),
|
|
434
492
|
),
|
|
435
493
|
),
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
494
|
+
),
|
|
495
|
+
SizedBox(
|
|
496
|
+
width: 30,
|
|
497
|
+
child: Column(
|
|
498
|
+
children: <Widget>[
|
|
499
|
+
Container(
|
|
500
|
+
width: 2,
|
|
501
|
+
height: isFirst ? 24 : 18,
|
|
502
|
+
color: isFirst ? Colors.transparent : _borderLight,
|
|
503
|
+
),
|
|
504
|
+
Container(
|
|
505
|
+
width: 16,
|
|
506
|
+
height: 16,
|
|
507
|
+
decoration: BoxDecoration(
|
|
508
|
+
shape: BoxShape.circle,
|
|
509
|
+
color: _bgCard,
|
|
510
|
+
border: Border.all(
|
|
511
|
+
color: sourceColor.withValues(alpha: 0.95),
|
|
512
|
+
width: 3,
|
|
513
|
+
),
|
|
514
|
+
boxShadow: <BoxShadow>[
|
|
515
|
+
BoxShadow(
|
|
516
|
+
color: sourceColor.withValues(alpha: 0.22),
|
|
517
|
+
blurRadius: 12,
|
|
518
|
+
),
|
|
519
|
+
],
|
|
520
|
+
),
|
|
521
|
+
),
|
|
522
|
+
Container(
|
|
523
|
+
width: 2,
|
|
524
|
+
height: isLast ? 26 : 148,
|
|
525
|
+
color: isLast ? Colors.transparent : _borderLight,
|
|
526
|
+
),
|
|
527
|
+
],
|
|
449
528
|
),
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const SizedBox(height: 12),
|
|
453
|
-
if (item == null)
|
|
529
|
+
),
|
|
530
|
+
const SizedBox(width: 18),
|
|
454
531
|
Expanded(
|
|
455
|
-
child:
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
532
|
+
child: Container(
|
|
533
|
+
margin: const EdgeInsets.only(top: 14, bottom: 14),
|
|
534
|
+
padding: const EdgeInsets.all(18),
|
|
535
|
+
decoration: BoxDecoration(
|
|
536
|
+
color: isSelected
|
|
537
|
+
? _bgCard.withValues(alpha: 0.98)
|
|
538
|
+
: _bgSecondary.withValues(alpha: 0.52),
|
|
539
|
+
borderRadius: BorderRadius.circular(20),
|
|
540
|
+
border: Border.all(
|
|
541
|
+
color: isSelected
|
|
542
|
+
? sourceColor.withValues(alpha: 0.45)
|
|
543
|
+
: _border,
|
|
544
|
+
),
|
|
545
|
+
boxShadow: isSelected
|
|
546
|
+
? <BoxShadow>[
|
|
547
|
+
BoxShadow(
|
|
548
|
+
color: sourceColor.withValues(alpha: 0.08),
|
|
549
|
+
blurRadius: 18,
|
|
550
|
+
offset: const Offset(0, 10),
|
|
551
|
+
),
|
|
552
|
+
]
|
|
553
|
+
: const <BoxShadow>[],
|
|
459
554
|
),
|
|
460
|
-
),
|
|
461
|
-
)
|
|
462
|
-
else
|
|
463
|
-
Expanded(
|
|
464
|
-
child: SingleChildScrollView(
|
|
465
555
|
child: Column(
|
|
466
556
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
467
557
|
children: <Widget>[
|
|
468
558
|
Row(
|
|
469
559
|
children: <Widget>[
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
560
|
+
_TimelineTypeChip(item: item),
|
|
561
|
+
const Spacer(),
|
|
562
|
+
if (isSelected)
|
|
563
|
+
Icon(
|
|
564
|
+
Icons.chevron_right_rounded,
|
|
565
|
+
color: sourceColor,
|
|
474
566
|
),
|
|
475
|
-
decoration: BoxDecoration(
|
|
476
|
-
color: item.sourceColor.withValues(alpha: 0.14),
|
|
477
|
-
borderRadius: BorderRadius.circular(999),
|
|
478
|
-
),
|
|
479
|
-
child: Text(
|
|
480
|
-
item.sourceLabel,
|
|
481
|
-
style: TextStyle(
|
|
482
|
-
color: item.sourceColor,
|
|
483
|
-
fontWeight: FontWeight.w700,
|
|
484
|
-
),
|
|
485
|
-
),
|
|
486
|
-
),
|
|
487
|
-
const Spacer(),
|
|
488
|
-
Text(
|
|
489
|
-
item.occurredAtLabel,
|
|
490
|
-
style: TextStyle(color: _textMuted, fontSize: 12),
|
|
491
|
-
),
|
|
492
567
|
],
|
|
493
568
|
),
|
|
494
569
|
const SizedBox(height: 14),
|
|
495
570
|
Text(
|
|
496
571
|
item.title.ifEmpty(item.taskName),
|
|
497
572
|
style: const TextStyle(
|
|
498
|
-
fontSize:
|
|
573
|
+
fontSize: 17,
|
|
499
574
|
fontWeight: FontWeight.w800,
|
|
500
|
-
height: 1.
|
|
575
|
+
height: 1.2,
|
|
501
576
|
),
|
|
502
577
|
),
|
|
503
|
-
const SizedBox(height:
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
color: item.sourceColor,
|
|
514
|
-
),
|
|
515
|
-
_MetaPill(
|
|
516
|
-
label: _timelineNodeSubtitle(item),
|
|
517
|
-
icon: _timelineLaneIcon(item.sourceKind),
|
|
518
|
-
),
|
|
519
|
-
],
|
|
578
|
+
const SizedBox(height: 10),
|
|
579
|
+
Text(
|
|
580
|
+
_timelineCardDescription(item),
|
|
581
|
+
maxLines: 2,
|
|
582
|
+
overflow: TextOverflow.ellipsis,
|
|
583
|
+
style: TextStyle(
|
|
584
|
+
color: _textSecondary,
|
|
585
|
+
fontSize: 14.5,
|
|
586
|
+
height: 1.4,
|
|
587
|
+
),
|
|
520
588
|
),
|
|
521
|
-
|
|
522
|
-
|
|
589
|
+
if (chips.isNotEmpty) ...<Widget>[
|
|
590
|
+
const SizedBox(height: 14),
|
|
591
|
+
Wrap(
|
|
592
|
+
spacing: 8,
|
|
593
|
+
runSpacing: 8,
|
|
594
|
+
children: <Widget>[
|
|
595
|
+
for (final chip in chips) _TimelineInlineChip(chip),
|
|
596
|
+
],
|
|
597
|
+
),
|
|
598
|
+
],
|
|
523
599
|
],
|
|
524
600
|
),
|
|
525
601
|
),
|
|
526
602
|
),
|
|
527
|
-
|
|
603
|
+
],
|
|
604
|
+
),
|
|
528
605
|
),
|
|
529
606
|
),
|
|
530
607
|
);
|
|
531
608
|
}
|
|
532
609
|
}
|
|
533
610
|
|
|
534
|
-
class
|
|
535
|
-
const
|
|
536
|
-
required this.label,
|
|
537
|
-
required this.color,
|
|
538
|
-
required this.icon,
|
|
539
|
-
});
|
|
611
|
+
class _TimelineTypeChip extends StatelessWidget {
|
|
612
|
+
const _TimelineTypeChip({required this.item});
|
|
540
613
|
|
|
541
|
-
final
|
|
542
|
-
final Color color;
|
|
543
|
-
final IconData icon;
|
|
614
|
+
final TimelineEventItem item;
|
|
544
615
|
|
|
545
616
|
@override
|
|
546
617
|
Widget build(BuildContext context) {
|
|
547
618
|
return Container(
|
|
548
619
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
549
620
|
decoration: BoxDecoration(
|
|
550
|
-
color:
|
|
551
|
-
borderRadius: BorderRadius.circular(
|
|
552
|
-
border: Border.all(color:
|
|
553
|
-
boxShadow: <BoxShadow>[
|
|
554
|
-
BoxShadow(
|
|
555
|
-
color: Colors.black.withValues(alpha: 0.08),
|
|
556
|
-
blurRadius: 16,
|
|
557
|
-
offset: const Offset(0, 8),
|
|
558
|
-
),
|
|
559
|
-
],
|
|
621
|
+
color: item.sourceColor.withValues(alpha: 0.12),
|
|
622
|
+
borderRadius: BorderRadius.circular(999),
|
|
623
|
+
border: Border.all(color: item.sourceColor.withValues(alpha: 0.24)),
|
|
560
624
|
),
|
|
561
625
|
child: Row(
|
|
562
626
|
mainAxisSize: MainAxisSize.min,
|
|
563
627
|
children: <Widget>[
|
|
564
|
-
Icon(
|
|
628
|
+
Icon(
|
|
629
|
+
_timelineLaneIcon(item.sourceKind),
|
|
630
|
+
size: 15,
|
|
631
|
+
color: item.sourceColor,
|
|
632
|
+
),
|
|
565
633
|
const SizedBox(width: 8),
|
|
566
|
-
Text(
|
|
634
|
+
Text(
|
|
635
|
+
item.sourceLabel.toLowerCase(),
|
|
636
|
+
style: TextStyle(
|
|
637
|
+
color: item.sourceColor,
|
|
638
|
+
fontSize: 13,
|
|
639
|
+
fontWeight: FontWeight.w700,
|
|
640
|
+
letterSpacing: 0.4,
|
|
641
|
+
),
|
|
642
|
+
),
|
|
567
643
|
],
|
|
568
644
|
),
|
|
569
645
|
);
|
|
570
646
|
}
|
|
571
647
|
}
|
|
572
648
|
|
|
573
|
-
class
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
649
|
+
class _TimelineInlineChip extends StatelessWidget {
|
|
650
|
+
const _TimelineInlineChip(this.chip);
|
|
651
|
+
|
|
652
|
+
final _TimelineChip chip;
|
|
653
|
+
|
|
654
|
+
@override
|
|
655
|
+
Widget build(BuildContext context) {
|
|
656
|
+
return Container(
|
|
657
|
+
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
|
658
|
+
decoration: BoxDecoration(
|
|
659
|
+
color: chip.color.withValues(alpha: 0.1),
|
|
660
|
+
borderRadius: BorderRadius.circular(12),
|
|
661
|
+
border: Border.all(color: chip.color.withValues(alpha: 0.2)),
|
|
662
|
+
),
|
|
663
|
+
child: Text(
|
|
664
|
+
chip.label,
|
|
665
|
+
style: TextStyle(
|
|
666
|
+
color: chip.color,
|
|
667
|
+
fontSize: 12.5,
|
|
668
|
+
fontWeight: FontWeight.w700,
|
|
669
|
+
),
|
|
670
|
+
),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
class _TimelineDetailPane extends StatelessWidget {
|
|
676
|
+
const _TimelineDetailPane({
|
|
677
|
+
required this.items,
|
|
678
|
+
required this.selectedEvent,
|
|
679
|
+
required this.selectedIndex,
|
|
680
|
+
this.onSelectPrevious,
|
|
681
|
+
this.onSelectNext,
|
|
682
|
+
this.onOpenRun,
|
|
580
683
|
});
|
|
581
684
|
|
|
582
|
-
final TimelineEventItem
|
|
583
|
-
final
|
|
584
|
-
final
|
|
585
|
-
final VoidCallback
|
|
586
|
-
final
|
|
685
|
+
final List<TimelineEventItem> items;
|
|
686
|
+
final TimelineEventItem? selectedEvent;
|
|
687
|
+
final int selectedIndex;
|
|
688
|
+
final VoidCallback? onSelectPrevious;
|
|
689
|
+
final VoidCallback? onSelectNext;
|
|
690
|
+
final VoidCallback? onOpenRun;
|
|
587
691
|
|
|
588
692
|
@override
|
|
589
693
|
Widget build(BuildContext context) {
|
|
590
|
-
final
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
:
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
694
|
+
final item = selectedEvent;
|
|
695
|
+
return Card(
|
|
696
|
+
clipBehavior: Clip.antiAlias,
|
|
697
|
+
child: Column(
|
|
698
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
699
|
+
children: <Widget>[
|
|
700
|
+
Container(
|
|
701
|
+
padding: const EdgeInsets.fromLTRB(20, 18, 20, 16),
|
|
702
|
+
decoration: BoxDecoration(
|
|
703
|
+
color: _bgSecondary.withValues(alpha: 0.8),
|
|
704
|
+
border: Border(bottom: BorderSide(color: _border)),
|
|
705
|
+
),
|
|
706
|
+
child: Row(
|
|
707
|
+
children: <Widget>[
|
|
708
|
+
Expanded(
|
|
709
|
+
child: Column(
|
|
710
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
711
|
+
children: <Widget>[
|
|
712
|
+
Text(
|
|
713
|
+
'DETAIL',
|
|
714
|
+
style: TextStyle(
|
|
715
|
+
color: _textMuted,
|
|
716
|
+
fontSize: 12,
|
|
717
|
+
fontWeight: FontWeight.w700,
|
|
718
|
+
letterSpacing: 4,
|
|
719
|
+
),
|
|
720
|
+
),
|
|
721
|
+
const SizedBox(height: 6),
|
|
722
|
+
const Text(
|
|
723
|
+
'Event detail',
|
|
724
|
+
style: TextStyle(
|
|
725
|
+
fontSize: 18,
|
|
726
|
+
fontWeight: FontWeight.w800,
|
|
727
|
+
),
|
|
728
|
+
),
|
|
624
729
|
],
|
|
625
|
-
begin: Alignment.topLeft,
|
|
626
|
-
end: Alignment.bottomRight,
|
|
627
730
|
),
|
|
628
|
-
border: Border.all(color: borderColor),
|
|
629
|
-
boxShadow: <BoxShadow>[
|
|
630
|
-
BoxShadow(
|
|
631
|
-
color: glowColor,
|
|
632
|
-
blurRadius: active ? 24 : 18,
|
|
633
|
-
offset: const Offset(0, 10),
|
|
634
|
-
),
|
|
635
|
-
],
|
|
636
731
|
),
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
732
|
+
IconButton(
|
|
733
|
+
tooltip: 'Previous event',
|
|
734
|
+
onPressed: onSelectPrevious,
|
|
735
|
+
icon: const Icon(Icons.chevron_left_rounded),
|
|
736
|
+
),
|
|
737
|
+
Text(
|
|
738
|
+
item == null ? '0/0' : '${selectedIndex + 1}/${items.length}',
|
|
739
|
+
style: TextStyle(color: _textMuted, fontSize: 12.5),
|
|
740
|
+
),
|
|
741
|
+
IconButton(
|
|
742
|
+
tooltip: 'Next event',
|
|
743
|
+
onPressed: onSelectNext,
|
|
744
|
+
icon: const Icon(Icons.chevron_right_rounded),
|
|
745
|
+
),
|
|
746
|
+
],
|
|
747
|
+
),
|
|
748
|
+
),
|
|
749
|
+
Expanded(
|
|
750
|
+
child: item == null
|
|
751
|
+
? Center(
|
|
752
|
+
child: Text(
|
|
753
|
+
'Select an event from the feed.',
|
|
754
|
+
style: TextStyle(color: _textSecondary),
|
|
755
|
+
),
|
|
756
|
+
)
|
|
757
|
+
: SingleChildScrollView(
|
|
758
|
+
padding: const EdgeInsets.all(20),
|
|
759
|
+
child: Column(
|
|
760
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
641
761
|
children: <Widget>[
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
762
|
+
Wrap(
|
|
763
|
+
spacing: 10,
|
|
764
|
+
runSpacing: 10,
|
|
765
|
+
children: <Widget>[
|
|
766
|
+
_TimelineStatPill(
|
|
767
|
+
icon: _timelineLaneIcon(item.sourceKind),
|
|
768
|
+
label: item.sourceLabel,
|
|
769
|
+
color: item.sourceColor,
|
|
770
|
+
),
|
|
771
|
+
_TimelineStatPill(
|
|
772
|
+
icon: Icons.schedule_outlined,
|
|
773
|
+
label:
|
|
774
|
+
'${_formatTimelineTime(item.occurredAt)} · ${_formatTimelineDate(item.occurredAt.toLocal())}',
|
|
775
|
+
),
|
|
776
|
+
],
|
|
777
|
+
),
|
|
778
|
+
const SizedBox(height: 18),
|
|
779
|
+
Text(
|
|
780
|
+
item.title.ifEmpty(item.taskName),
|
|
781
|
+
style: const TextStyle(
|
|
782
|
+
fontSize: 28,
|
|
783
|
+
fontWeight: FontWeight.w800,
|
|
784
|
+
height: 1.15,
|
|
654
785
|
),
|
|
655
786
|
),
|
|
656
|
-
const SizedBox(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
fontWeight: FontWeight.w700,
|
|
664
|
-
),
|
|
787
|
+
const SizedBox(height: 12),
|
|
788
|
+
Text(
|
|
789
|
+
_timelineDetailDescription(item),
|
|
790
|
+
style: TextStyle(
|
|
791
|
+
color: _textSecondary,
|
|
792
|
+
fontSize: 15,
|
|
793
|
+
height: 1.5,
|
|
665
794
|
),
|
|
666
795
|
),
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
796
|
+
const SizedBox(height: 18),
|
|
797
|
+
Wrap(
|
|
798
|
+
spacing: 8,
|
|
799
|
+
runSpacing: 8,
|
|
800
|
+
children: <Widget>[
|
|
801
|
+
_TimelineInlineChip(
|
|
802
|
+
_TimelineChip(
|
|
803
|
+
label: _titleCase(
|
|
804
|
+
item.eventKind.replaceAll('_', ' '),
|
|
805
|
+
),
|
|
806
|
+
color: item.sourceColor,
|
|
807
|
+
),
|
|
808
|
+
),
|
|
809
|
+
for (final chip in _timelineEventChips(
|
|
810
|
+
item,
|
|
811
|
+
).take(4))
|
|
812
|
+
_TimelineInlineChip(chip),
|
|
813
|
+
],
|
|
814
|
+
),
|
|
815
|
+
const SizedBox(height: 20),
|
|
816
|
+
_TimelineDetailGrid(item: item),
|
|
817
|
+
if (onOpenRun != null) ...<Widget>[
|
|
818
|
+
const SizedBox(height: 20),
|
|
819
|
+
SizedBox(
|
|
820
|
+
width: double.infinity,
|
|
821
|
+
child: FilledButton.icon(
|
|
822
|
+
onPressed: onOpenRun,
|
|
823
|
+
icon: const Icon(Icons.open_in_new_rounded),
|
|
824
|
+
label: const Text('Open linked run'),
|
|
825
|
+
),
|
|
672
826
|
),
|
|
827
|
+
],
|
|
828
|
+
const SizedBox(height: 20),
|
|
829
|
+
..._timelineDetailBody(item, onOpenRun),
|
|
673
830
|
],
|
|
674
831
|
),
|
|
675
|
-
|
|
676
|
-
Expanded(
|
|
677
|
-
child: Text(
|
|
678
|
-
item.title.ifEmpty(item.taskName),
|
|
679
|
-
maxLines: 2,
|
|
680
|
-
overflow: TextOverflow.ellipsis,
|
|
681
|
-
style: const TextStyle(
|
|
682
|
-
fontSize: 13.5,
|
|
683
|
-
fontWeight: FontWeight.w700,
|
|
684
|
-
height: 1.22,
|
|
685
|
-
),
|
|
686
|
-
),
|
|
687
|
-
),
|
|
688
|
-
const SizedBox(height: 8),
|
|
689
|
-
Text(
|
|
690
|
-
subtitle,
|
|
691
|
-
maxLines: 1,
|
|
692
|
-
overflow: TextOverflow.ellipsis,
|
|
693
|
-
style: TextStyle(color: _textSecondary, fontSize: 11.5),
|
|
694
|
-
),
|
|
695
|
-
],
|
|
696
|
-
),
|
|
697
|
-
),
|
|
698
|
-
),
|
|
832
|
+
),
|
|
699
833
|
),
|
|
700
|
-
|
|
834
|
+
],
|
|
701
835
|
),
|
|
702
836
|
);
|
|
703
837
|
}
|
|
704
838
|
}
|
|
705
839
|
|
|
706
|
-
class
|
|
707
|
-
const
|
|
708
|
-
required this.scene,
|
|
709
|
-
required this.selectedEventId,
|
|
710
|
-
});
|
|
840
|
+
class _TimelineDetailGrid extends StatelessWidget {
|
|
841
|
+
const _TimelineDetailGrid({required this.item});
|
|
711
842
|
|
|
712
|
-
final
|
|
713
|
-
final int? selectedEventId;
|
|
843
|
+
final TimelineEventItem item;
|
|
714
844
|
|
|
715
845
|
@override
|
|
716
|
-
|
|
717
|
-
final
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
final laneRect = Rect.fromLTWH(
|
|
736
|
-
8,
|
|
737
|
-
lane.top,
|
|
738
|
-
scene.canvasWidth - 16,
|
|
739
|
-
lane.height,
|
|
740
|
-
);
|
|
741
|
-
laneFillPaint.color = lane.color.withValues(
|
|
742
|
-
alpha: index.isEven ? 0.05 : 0.025,
|
|
743
|
-
);
|
|
744
|
-
canvas.drawRRect(
|
|
745
|
-
RRect.fromRectAndRadius(laneRect, const Radius.circular(22)),
|
|
746
|
-
laneFillPaint,
|
|
747
|
-
);
|
|
748
|
-
canvas.drawRRect(
|
|
749
|
-
RRect.fromRectAndRadius(laneRect, const Radius.circular(22)),
|
|
750
|
-
laneStrokePaint,
|
|
751
|
-
);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
final selectedEntry = selectedEventId == null
|
|
755
|
-
? null
|
|
756
|
-
: scene.entries.cast<_TimelineSceneEntry?>().firstWhere(
|
|
757
|
-
(entry) => entry?.item.id == selectedEventId,
|
|
758
|
-
orElse: () => null,
|
|
759
|
-
);
|
|
760
|
-
if (selectedEntry != null) {
|
|
761
|
-
final selectedLinePaint = Paint()
|
|
762
|
-
..color = selectedEntry.item.sourceColor.withValues(alpha: 0.26)
|
|
763
|
-
..strokeWidth = 2;
|
|
764
|
-
canvas.drawLine(
|
|
765
|
-
Offset(selectedEntry.centerX, _timelineAxisHeaderHeight - 4),
|
|
766
|
-
Offset(selectedEntry.centerX, scene.canvasHeight),
|
|
767
|
-
selectedLinePaint,
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
final tickPaint = Paint()
|
|
772
|
-
..color = _borderLight
|
|
773
|
-
..strokeWidth = 1;
|
|
774
|
-
for (final tick in scene.ticks) {
|
|
775
|
-
canvas.drawLine(
|
|
776
|
-
Offset(tick.x, _timelineAxisHeaderHeight - 8),
|
|
777
|
-
Offset(tick.x, scene.canvasHeight - 10),
|
|
778
|
-
tickPaint,
|
|
779
|
-
);
|
|
780
|
-
final textPainter = TextPainter(
|
|
781
|
-
text: TextSpan(
|
|
782
|
-
text: tick.label,
|
|
783
|
-
style: TextStyle(
|
|
784
|
-
color: _textMuted,
|
|
785
|
-
fontSize: 11,
|
|
786
|
-
fontWeight: FontWeight.w600,
|
|
846
|
+
Widget build(BuildContext context) {
|
|
847
|
+
final cells = _timelineDetailCells(item);
|
|
848
|
+
return GridView.builder(
|
|
849
|
+
shrinkWrap: true,
|
|
850
|
+
physics: const NeverScrollableScrollPhysics(),
|
|
851
|
+
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
852
|
+
crossAxisCount: 2,
|
|
853
|
+
mainAxisSpacing: 1,
|
|
854
|
+
crossAxisSpacing: 1,
|
|
855
|
+
childAspectRatio: 1.48,
|
|
856
|
+
),
|
|
857
|
+
itemCount: cells.length,
|
|
858
|
+
itemBuilder: (context, index) {
|
|
859
|
+
final cell = cells[index];
|
|
860
|
+
return Container(
|
|
861
|
+
padding: const EdgeInsets.all(16),
|
|
862
|
+
decoration: BoxDecoration(
|
|
863
|
+
color: _bgSecondary.withValues(alpha: 0.68),
|
|
864
|
+
border: Border.all(color: _border),
|
|
787
865
|
),
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
866
|
+
child: Column(
|
|
867
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
868
|
+
children: <Widget>[
|
|
869
|
+
Text(
|
|
870
|
+
cell.label,
|
|
871
|
+
style: TextStyle(
|
|
872
|
+
color: _textMuted,
|
|
873
|
+
fontSize: 11.5,
|
|
874
|
+
fontWeight: FontWeight.w700,
|
|
875
|
+
letterSpacing: 2.6,
|
|
876
|
+
),
|
|
877
|
+
),
|
|
878
|
+
const Spacer(),
|
|
879
|
+
Text(
|
|
880
|
+
cell.value,
|
|
881
|
+
maxLines: 3,
|
|
882
|
+
overflow: TextOverflow.ellipsis,
|
|
883
|
+
style: TextStyle(
|
|
884
|
+
color: cell.emphasized
|
|
885
|
+
? cell.color ?? _textPrimary
|
|
886
|
+
: _textPrimary,
|
|
887
|
+
fontSize: cell.emphasized ? 18 : 16,
|
|
888
|
+
fontWeight: FontWeight.w700,
|
|
889
|
+
height: 1.2,
|
|
890
|
+
),
|
|
891
|
+
),
|
|
892
|
+
],
|
|
893
|
+
),
|
|
894
|
+
);
|
|
895
|
+
},
|
|
896
|
+
);
|
|
799
897
|
}
|
|
800
898
|
}
|
|
801
899
|
|
|
802
|
-
class
|
|
803
|
-
const
|
|
804
|
-
required this.
|
|
805
|
-
required this.
|
|
806
|
-
|
|
807
|
-
required this.entries,
|
|
808
|
-
required this.ticks,
|
|
900
|
+
class _TimelineStatPill extends StatelessWidget {
|
|
901
|
+
const _TimelineStatPill({
|
|
902
|
+
required this.icon,
|
|
903
|
+
required this.label,
|
|
904
|
+
this.color,
|
|
809
905
|
});
|
|
810
906
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}) {
|
|
815
|
-
final lanes = _buildTimelineLanes(items);
|
|
816
|
-
final minimumCanvasWidth = math.max(
|
|
817
|
-
viewportWidth - 36,
|
|
818
|
-
items.length * 156 + _timelineLaneLabelWidth + _timelinePlotRightPadding,
|
|
819
|
-
);
|
|
820
|
-
final plotStart = _timelineLaneLabelWidth + _timelinePlotInset;
|
|
821
|
-
final plotWidth =
|
|
822
|
-
minimumCanvasWidth -
|
|
823
|
-
plotStart -
|
|
824
|
-
_timelinePlotRightPadding -
|
|
825
|
-
_timelinePlotInset;
|
|
826
|
-
final start = items.first.occurredAt;
|
|
827
|
-
final end = items.last.occurredAt;
|
|
828
|
-
final durationMs = math.max(end.difference(start).inMilliseconds, 1);
|
|
829
|
-
final entries = <_TimelineSceneEntry>[];
|
|
830
|
-
final laneGeometries = <_TimelineLaneGeometry>[];
|
|
831
|
-
var laneTop = _timelineAxisHeaderHeight;
|
|
832
|
-
|
|
833
|
-
for (final lane in lanes) {
|
|
834
|
-
final laneItems = items
|
|
835
|
-
.where((item) => item.sourceKind == lane.sourceKind)
|
|
836
|
-
.toList(growable: false);
|
|
837
|
-
final positioned = <({TimelineEventItem item, double x, int track})>[];
|
|
838
|
-
final trackLastEdge = <double>[];
|
|
839
|
-
for (final item in laneItems) {
|
|
840
|
-
final elapsedMs = item.occurredAt.difference(start).inMilliseconds;
|
|
841
|
-
final fraction = elapsedMs / durationMs;
|
|
842
|
-
final x =
|
|
843
|
-
plotStart + (fraction * plotWidth).clamp(0, plotWidth.toDouble());
|
|
844
|
-
var track = 0;
|
|
845
|
-
while (track < trackLastEdge.length &&
|
|
846
|
-
x - trackLastEdge[track] < _timelineNodeWidth + _timelineNodeGap) {
|
|
847
|
-
track += 1;
|
|
848
|
-
}
|
|
849
|
-
if (track == trackLastEdge.length) {
|
|
850
|
-
trackLastEdge.add(x);
|
|
851
|
-
} else {
|
|
852
|
-
trackLastEdge[track] = x;
|
|
853
|
-
}
|
|
854
|
-
positioned.add((item: item, x: x, track: track));
|
|
855
|
-
}
|
|
907
|
+
final IconData icon;
|
|
908
|
+
final String label;
|
|
909
|
+
final Color? color;
|
|
856
910
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
left: entry.x - (_timelineNodeWidth / 2),
|
|
880
|
-
top: top,
|
|
911
|
+
@override
|
|
912
|
+
Widget build(BuildContext context) {
|
|
913
|
+
final tone = color ?? _textSecondary;
|
|
914
|
+
return Container(
|
|
915
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
|
916
|
+
decoration: BoxDecoration(
|
|
917
|
+
color: _bgPrimary.withValues(alpha: 0.55),
|
|
918
|
+
borderRadius: BorderRadius.circular(16),
|
|
919
|
+
border: Border.all(color: _borderLight),
|
|
920
|
+
),
|
|
921
|
+
child: Row(
|
|
922
|
+
mainAxisSize: MainAxisSize.min,
|
|
923
|
+
children: <Widget>[
|
|
924
|
+
Icon(icon, size: 16, color: tone),
|
|
925
|
+
const SizedBox(width: 8),
|
|
926
|
+
Text(
|
|
927
|
+
label,
|
|
928
|
+
style: TextStyle(
|
|
929
|
+
color: tone,
|
|
930
|
+
fontSize: 13,
|
|
931
|
+
fontWeight: FontWeight.w700,
|
|
932
|
+
),
|
|
881
933
|
),
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
laneTop += laneHeight + _timelineLaneGap;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
return _TimelineSceneLayout(
|
|
888
|
-
canvasWidth: minimumCanvasWidth,
|
|
889
|
-
canvasHeight: laneTop + _timelineCanvasBottomPadding,
|
|
890
|
-
lanes: laneGeometries,
|
|
891
|
-
entries: entries,
|
|
892
|
-
ticks: _buildTimelineTicks(
|
|
893
|
-
start: start,
|
|
894
|
-
end: end,
|
|
895
|
-
plotStart: plotStart,
|
|
896
|
-
plotWidth: plotWidth,
|
|
934
|
+
],
|
|
897
935
|
),
|
|
898
936
|
);
|
|
899
937
|
}
|
|
900
|
-
|
|
901
|
-
final double canvasWidth;
|
|
902
|
-
final double canvasHeight;
|
|
903
|
-
final List<_TimelineLaneGeometry> lanes;
|
|
904
|
-
final List<_TimelineSceneEntry> entries;
|
|
905
|
-
final List<_TimelineTick> ticks;
|
|
906
938
|
}
|
|
907
939
|
|
|
908
|
-
class
|
|
909
|
-
const
|
|
910
|
-
required this.
|
|
940
|
+
class _TimelineDayGroup {
|
|
941
|
+
const _TimelineDayGroup({
|
|
942
|
+
required this.date,
|
|
911
943
|
required this.label,
|
|
912
|
-
required this.
|
|
944
|
+
required this.dateLabel,
|
|
945
|
+
required this.isToday,
|
|
946
|
+
required this.items,
|
|
913
947
|
});
|
|
914
948
|
|
|
915
|
-
final
|
|
949
|
+
final DateTime date;
|
|
916
950
|
final String label;
|
|
917
|
-
final
|
|
951
|
+
final String dateLabel;
|
|
952
|
+
final bool isToday;
|
|
953
|
+
final List<TimelineEventItem> items;
|
|
918
954
|
}
|
|
919
955
|
|
|
920
|
-
class
|
|
921
|
-
const
|
|
922
|
-
required this.sourceKind,
|
|
923
|
-
required this.label,
|
|
924
|
-
required this.color,
|
|
925
|
-
required this.top,
|
|
926
|
-
required this.height,
|
|
927
|
-
});
|
|
956
|
+
class _TimelineChip {
|
|
957
|
+
const _TimelineChip({required this.label, required this.color});
|
|
928
958
|
|
|
929
|
-
final String sourceKind;
|
|
930
959
|
final String label;
|
|
931
960
|
final Color color;
|
|
932
|
-
final double top;
|
|
933
|
-
final double height;
|
|
934
961
|
}
|
|
935
962
|
|
|
936
|
-
class
|
|
937
|
-
const
|
|
938
|
-
required this.
|
|
939
|
-
required this.
|
|
940
|
-
|
|
963
|
+
class _TimelineDetailCell {
|
|
964
|
+
const _TimelineDetailCell({
|
|
965
|
+
required this.label,
|
|
966
|
+
required this.value,
|
|
967
|
+
this.emphasized = false,
|
|
968
|
+
this.color,
|
|
941
969
|
});
|
|
942
970
|
|
|
943
|
-
final TimelineEventItem item;
|
|
944
|
-
final double left;
|
|
945
|
-
final double top;
|
|
946
|
-
|
|
947
|
-
double get centerX => left + (_timelineNodeWidth / 2);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
class _TimelineTick {
|
|
951
|
-
const _TimelineTick({required this.x, required this.label});
|
|
952
|
-
|
|
953
|
-
final double x;
|
|
954
971
|
final String label;
|
|
972
|
+
final String value;
|
|
973
|
+
final bool emphasized;
|
|
974
|
+
final Color? color;
|
|
955
975
|
}
|
|
956
976
|
|
|
957
977
|
List<TimelineEventItem> _sortedTimelineEvents(List<TimelineEventItem> items) {
|
|
958
978
|
final sorted = List<TimelineEventItem>.of(items);
|
|
959
979
|
sorted.sort((a, b) {
|
|
960
|
-
final timestampCompare =
|
|
980
|
+
final timestampCompare = b.occurredAt.compareTo(a.occurredAt);
|
|
961
981
|
if (timestampCompare != 0) {
|
|
962
982
|
return timestampCompare;
|
|
963
983
|
}
|
|
964
|
-
return
|
|
984
|
+
return b.id.compareTo(a.id);
|
|
965
985
|
});
|
|
966
986
|
return sorted;
|
|
967
987
|
}
|
|
968
988
|
|
|
969
|
-
List<
|
|
970
|
-
|
|
971
|
-
)
|
|
972
|
-
|
|
973
|
-
final
|
|
974
|
-
for (final item in items) {
|
|
975
|
-
firstBySource.putIfAbsent(item.sourceKind, () => item);
|
|
976
|
-
}
|
|
989
|
+
List<_TimelineDayGroup> _groupTimelineEvents(List<TimelineEventItem> items) {
|
|
990
|
+
final groups = <_TimelineDayGroup>[];
|
|
991
|
+
final now = DateTime.now();
|
|
992
|
+
DateTime? activeDate;
|
|
993
|
+
final buffer = <TimelineEventItem>[];
|
|
977
994
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
if (sample == null) {
|
|
982
|
-
continue;
|
|
995
|
+
void flush() {
|
|
996
|
+
if (activeDate == null || buffer.isEmpty) {
|
|
997
|
+
return;
|
|
983
998
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
999
|
+
final day = activeDate;
|
|
1000
|
+
groups.add(
|
|
1001
|
+
_TimelineDayGroup(
|
|
1002
|
+
date: day,
|
|
1003
|
+
label: _timelineDayLabel(day, now),
|
|
1004
|
+
dateLabel: _formatTimelineDate(day),
|
|
1005
|
+
isToday: _isSameDay(day, now),
|
|
1006
|
+
items: List<TimelineEventItem>.of(buffer),
|
|
989
1007
|
),
|
|
990
1008
|
);
|
|
1009
|
+
buffer.clear();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
for (final item in items) {
|
|
1013
|
+
final local = item.occurredAt.toLocal();
|
|
1014
|
+
final day = DateTime(local.year, local.month, local.day);
|
|
1015
|
+
if (activeDate == null || !_isSameDay(activeDate, day)) {
|
|
1016
|
+
flush();
|
|
1017
|
+
activeDate = day;
|
|
1018
|
+
}
|
|
1019
|
+
buffer.add(item);
|
|
991
1020
|
}
|
|
1021
|
+
flush();
|
|
1022
|
+
return groups;
|
|
1023
|
+
}
|
|
992
1024
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1025
|
+
List<_TimelineChip> _timelineEventChips(TimelineEventItem item) {
|
|
1026
|
+
final chips = <_TimelineChip>[];
|
|
1027
|
+
if (item.sourceKind == 'screen') {
|
|
1028
|
+
final duration = _timelineSpanDuration(item);
|
|
1029
|
+
if (duration != null) {
|
|
1030
|
+
chips.add(_TimelineChip(label: duration, color: _accent));
|
|
1031
|
+
}
|
|
1032
|
+
if (item.appName.trim().isNotEmpty) {
|
|
1033
|
+
chips.add(_TimelineChip(label: item.appName.trim(), color: _info));
|
|
1034
|
+
}
|
|
1035
|
+
if (item.windowTitle.trim().isNotEmpty) {
|
|
1036
|
+
chips.add(
|
|
1037
|
+
_TimelineChip(label: item.windowTitle.trim(), color: _textSecondary),
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
} else if (item.sourceKind == 'tasks') {
|
|
1041
|
+
chips.add(
|
|
1042
|
+
_TimelineChip(
|
|
1043
|
+
label: _titleCase(item.eventKind.replaceAll('_', ' ')),
|
|
1044
|
+
color: _warning,
|
|
1005
1045
|
),
|
|
1006
1046
|
);
|
|
1047
|
+
if (item.taskName.trim().isNotEmpty &&
|
|
1048
|
+
item.taskName.trim() != item.title.trim()) {
|
|
1049
|
+
chips.add(_TimelineChip(label: item.taskName.trim(), color: _success));
|
|
1050
|
+
}
|
|
1051
|
+
} else if (item.sourceKind == 'runs') {
|
|
1052
|
+
chips.add(
|
|
1053
|
+
_TimelineChip(
|
|
1054
|
+
label: _titleCase(item.eventKind.replaceAll('_', ' ')),
|
|
1055
|
+
color: _success,
|
|
1056
|
+
),
|
|
1057
|
+
);
|
|
1058
|
+
if (item.runId.isNotEmpty) {
|
|
1059
|
+
chips.add(_TimelineChip(label: 'Run linked', color: _accentAlt));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (item.deviceLabel.trim().isNotEmpty && item.sourceKind != 'screen') {
|
|
1063
|
+
chips.add(_TimelineChip(label: item.deviceLabel.trim(), color: _info));
|
|
1007
1064
|
}
|
|
1008
|
-
return
|
|
1065
|
+
return chips.take(4).toList(growable: false);
|
|
1009
1066
|
}
|
|
1010
1067
|
|
|
1011
|
-
List<
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
label:
|
|
1068
|
+
List<_TimelineDetailCell> _timelineDetailCells(TimelineEventItem item) {
|
|
1069
|
+
final cells = <_TimelineDetailCell>[
|
|
1070
|
+
_TimelineDetailCell(
|
|
1071
|
+
label: 'SOURCE',
|
|
1072
|
+
value: item.sourceLabel,
|
|
1073
|
+
emphasized: true,
|
|
1074
|
+
color: item.sourceColor,
|
|
1075
|
+
),
|
|
1076
|
+
_TimelineDetailCell(
|
|
1077
|
+
label: 'KIND',
|
|
1078
|
+
value: _titleCase(item.eventKind.replaceAll('_', ' ')),
|
|
1079
|
+
),
|
|
1080
|
+
_TimelineDetailCell(
|
|
1081
|
+
label: 'TIME',
|
|
1082
|
+
value:
|
|
1083
|
+
'${_formatTimelineTime(item.occurredAt)}\n${_formatTimelineDate(item.occurredAt.toLocal())}',
|
|
1084
|
+
),
|
|
1085
|
+
];
|
|
1086
|
+
|
|
1087
|
+
if (item.sourceKind == 'screen') {
|
|
1088
|
+
cells.add(
|
|
1089
|
+
_TimelineDetailCell(
|
|
1090
|
+
label: 'DURATION',
|
|
1091
|
+
value: _timelineSpanDuration(item) ?? item.screenSpanLabel,
|
|
1034
1092
|
),
|
|
1035
1093
|
);
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1094
|
+
cells.add(
|
|
1095
|
+
_TimelineDetailCell(
|
|
1096
|
+
label: 'APP',
|
|
1097
|
+
value: item.appName.ifEmpty('Unknown app'),
|
|
1098
|
+
),
|
|
1099
|
+
);
|
|
1100
|
+
cells.add(
|
|
1101
|
+
_TimelineDetailCell(
|
|
1102
|
+
label: 'DEVICE',
|
|
1103
|
+
value: item.deviceLabel.ifEmpty('Desktop'),
|
|
1104
|
+
),
|
|
1105
|
+
);
|
|
1106
|
+
} else if (item.sourceKind == 'tasks') {
|
|
1107
|
+
cells.add(
|
|
1108
|
+
_TimelineDetailCell(
|
|
1109
|
+
label: 'TASK',
|
|
1110
|
+
value: item.taskName.ifEmpty(item.title),
|
|
1111
|
+
),
|
|
1112
|
+
);
|
|
1113
|
+
cells.add(
|
|
1114
|
+
_TimelineDetailCell(
|
|
1115
|
+
label: 'RUN LINK',
|
|
1116
|
+
value: item.runId.isNotEmpty ? 'Available' : 'None',
|
|
1117
|
+
),
|
|
1118
|
+
);
|
|
1119
|
+
cells.add(
|
|
1120
|
+
_TimelineDetailCell(
|
|
1121
|
+
label: 'SUMMARY',
|
|
1122
|
+
value: item.summary.trim().ifEmpty('No summary'),
|
|
1123
|
+
),
|
|
1124
|
+
);
|
|
1125
|
+
} else if (item.sourceKind == 'runs') {
|
|
1126
|
+
cells.add(
|
|
1127
|
+
_TimelineDetailCell(
|
|
1128
|
+
label: 'RUN',
|
|
1129
|
+
value: item.runId.isNotEmpty ? item.runId : 'Unavailable',
|
|
1130
|
+
),
|
|
1131
|
+
);
|
|
1132
|
+
cells.add(
|
|
1133
|
+
_TimelineDetailCell(
|
|
1134
|
+
label: 'SUMMARY',
|
|
1135
|
+
value: item.summary.trim().ifEmpty('No summary'),
|
|
1042
1136
|
),
|
|
1043
1137
|
);
|
|
1138
|
+
cells.add(
|
|
1139
|
+
_TimelineDetailCell(
|
|
1140
|
+
label: 'TITLE',
|
|
1141
|
+
value: item.title.ifEmpty('Untitled run event'),
|
|
1142
|
+
),
|
|
1143
|
+
);
|
|
1144
|
+
} else {
|
|
1145
|
+
cells.add(
|
|
1146
|
+
_TimelineDetailCell(
|
|
1147
|
+
label: 'SUMMARY',
|
|
1148
|
+
value: item.summary.trim().ifEmpty('No summary'),
|
|
1149
|
+
),
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (cells.length.isOdd) {
|
|
1154
|
+
cells.add(const _TimelineDetailCell(label: 'STATUS', value: 'Captured'));
|
|
1044
1155
|
}
|
|
1045
|
-
return
|
|
1156
|
+
return cells.take(6).toList(growable: false);
|
|
1046
1157
|
}
|
|
1047
1158
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1159
|
+
String _timelineCardDescription(TimelineEventItem item) {
|
|
1160
|
+
switch (item.sourceKind) {
|
|
1161
|
+
case 'screen':
|
|
1162
|
+
final preview = item.previewText.trim();
|
|
1163
|
+
if (preview.isNotEmpty) {
|
|
1164
|
+
return preview;
|
|
1165
|
+
}
|
|
1166
|
+
return '${item.deviceLabel.ifEmpty('Desktop')} · ${item.appName.ifEmpty('Unknown app')}';
|
|
1167
|
+
case 'tasks':
|
|
1168
|
+
case 'runs':
|
|
1169
|
+
return item.summary.trim().ifEmpty(
|
|
1170
|
+
_titleCase(item.eventKind.replaceAll('_', ' ')),
|
|
1171
|
+
);
|
|
1172
|
+
default:
|
|
1173
|
+
return item.summary.trim().ifEmpty(item.sourceLabel);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
String _timelineDetailDescription(TimelineEventItem item) {
|
|
1178
|
+
final body = item.sourceKind == 'screen'
|
|
1179
|
+
? item.previewText.trim()
|
|
1180
|
+
: item.summary.trim();
|
|
1181
|
+
if (body.isNotEmpty) {
|
|
1182
|
+
return body;
|
|
1183
|
+
}
|
|
1184
|
+
if (item.sourceKind == 'screen') {
|
|
1185
|
+
return '${item.appName.ifEmpty('Unknown app')} on ${item.deviceLabel.ifEmpty('Desktop')}';
|
|
1186
|
+
}
|
|
1187
|
+
return _titleCase(item.eventKind.replaceAll('_', ' '));
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
String? _timelineSpanDuration(TimelineEventItem item) {
|
|
1191
|
+
final start = item.startedAt;
|
|
1192
|
+
final end = item.endedAt;
|
|
1193
|
+
if (start == null || end == null) {
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
final span = end.difference(start);
|
|
1197
|
+
if (span.inSeconds < 60) {
|
|
1198
|
+
return '${span.inSeconds}s';
|
|
1199
|
+
}
|
|
1200
|
+
if (span.inMinutes < 60) {
|
|
1201
|
+
final seconds = span.inSeconds % 60;
|
|
1202
|
+
if (seconds == 0) {
|
|
1203
|
+
return '${span.inMinutes}m';
|
|
1067
1204
|
}
|
|
1205
|
+
return '${span.inMinutes}m ${seconds}s';
|
|
1206
|
+
}
|
|
1207
|
+
final minutes = span.inMinutes % 60;
|
|
1208
|
+
if (minutes == 0) {
|
|
1209
|
+
return '${span.inHours}h';
|
|
1068
1210
|
}
|
|
1069
|
-
return
|
|
1211
|
+
return '${span.inHours}h ${minutes}m';
|
|
1070
1212
|
}
|
|
1071
1213
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1214
|
+
Color _sourceColorForKind(String kind) {
|
|
1215
|
+
switch (kind) {
|
|
1216
|
+
case 'screen':
|
|
1217
|
+
return _accent;
|
|
1218
|
+
case 'tasks':
|
|
1219
|
+
return _warning;
|
|
1220
|
+
case 'runs':
|
|
1221
|
+
return _success;
|
|
1222
|
+
default:
|
|
1223
|
+
return _textSecondary;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
String _timelineDayLabel(DateTime day, DateTime now) {
|
|
1228
|
+
if (_isSameDay(day, now)) {
|
|
1229
|
+
return 'Today';
|
|
1230
|
+
}
|
|
1231
|
+
final yesterday = now.subtract(const Duration(days: 1));
|
|
1232
|
+
if (_isSameDay(day, yesterday)) {
|
|
1233
|
+
return 'Yesterday';
|
|
1234
|
+
}
|
|
1235
|
+
return _weekdayShort(day.weekday);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
bool _isSameDay(DateTime a, DateTime b) {
|
|
1239
|
+
return a.year == b.year && a.month == b.month && a.day == b.day;
|
|
1076
1240
|
}
|
|
1077
1241
|
|
|
1078
1242
|
String _formatTimelineRange(TimelineEventItem first, TimelineEventItem last) {
|
|
1079
|
-
final start =
|
|
1080
|
-
final end =
|
|
1243
|
+
final start = last.occurredAt.toLocal();
|
|
1244
|
+
final end = first.occurredAt.toLocal();
|
|
1081
1245
|
final startDate = _formatTimelineDate(start);
|
|
1082
1246
|
final endDate = _formatTimelineDate(end);
|
|
1083
1247
|
final startTime = _formatTimelineTime(start);
|
|
1084
1248
|
final endTime = _formatTimelineTime(end);
|
|
1085
|
-
if (start
|
|
1086
|
-
start.month == end.month &&
|
|
1087
|
-
start.day == end.day) {
|
|
1249
|
+
if (_isSameDay(start, end)) {
|
|
1088
1250
|
return '$startDate · $startTime - $endTime';
|
|
1089
1251
|
}
|
|
1090
|
-
return '$startDate
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
String _formatTimelineTick(DateTime value, Duration step) {
|
|
1094
|
-
final local = value.toLocal();
|
|
1095
|
-
if (step.inHours < 24) {
|
|
1096
|
-
return _formatTimelineTime(local);
|
|
1097
|
-
}
|
|
1098
|
-
if (step.inDays < 30) {
|
|
1099
|
-
return '${_monthShort(local.month)} ${local.day}';
|
|
1100
|
-
}
|
|
1101
|
-
return '${_monthShort(local.month)} ${local.year}';
|
|
1252
|
+
return '$startDate -> $endDate';
|
|
1102
1253
|
}
|
|
1103
1254
|
|
|
1104
1255
|
String _formatTimelineDate(DateTime value) {
|
|
@@ -1130,21 +1281,9 @@ String _monthShort(int month) {
|
|
|
1130
1281
|
return labels[(month - 1).clamp(0, labels.length - 1)];
|
|
1131
1282
|
}
|
|
1132
1283
|
|
|
1133
|
-
String
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
return item.appName
|
|
1137
|
-
.ifEmpty(item.windowTitle)
|
|
1138
|
-
.ifEmpty(item.deviceLabel.ifEmpty('Passive capture'));
|
|
1139
|
-
case 'tasks':
|
|
1140
|
-
return _titleCase(item.eventKind.replaceAll('_', ' '));
|
|
1141
|
-
case 'runs':
|
|
1142
|
-
return item.summary.trim().ifEmpty(
|
|
1143
|
-
_titleCase(item.eventKind.replaceAll('_', ' ')),
|
|
1144
|
-
);
|
|
1145
|
-
default:
|
|
1146
|
-
return item.sourceLabel;
|
|
1147
|
-
}
|
|
1284
|
+
String _weekdayShort(int weekday) {
|
|
1285
|
+
const labels = <String>['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
1286
|
+
return labels[(weekday - 1).clamp(0, labels.length - 1)];
|
|
1148
1287
|
}
|
|
1149
1288
|
|
|
1150
1289
|
IconData _timelineLaneIcon(String sourceKind) {
|
|
@@ -1165,66 +1304,48 @@ List<Widget> _timelineDetailBody(
|
|
|
1165
1304
|
VoidCallback? onOpenRun,
|
|
1166
1305
|
) {
|
|
1167
1306
|
final content = <Widget>[];
|
|
1168
|
-
final body = item.sourceKind == 'screen'
|
|
1169
|
-
? item.previewText.trim()
|
|
1170
|
-
: item.summary.trim();
|
|
1171
|
-
if (body.isNotEmpty) {
|
|
1172
|
-
content.add(
|
|
1173
|
-
Text(body, style: TextStyle(color: _textSecondary, height: 1.5)),
|
|
1174
|
-
);
|
|
1175
|
-
content.add(const SizedBox(height: 16));
|
|
1176
|
-
}
|
|
1177
1307
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
if (item.windowTitle.isNotEmpty)
|
|
1187
|
-
_TimelineMetaLine(
|
|
1188
|
-
icon: Icons.web_asset_outlined,
|
|
1189
|
-
text: item.windowTitle,
|
|
1190
|
-
),
|
|
1191
|
-
_TimelineMetaLine(
|
|
1192
|
-
icon: Icons.schedule_outlined,
|
|
1193
|
-
text: item.screenSpanLabel,
|
|
1194
|
-
),
|
|
1195
|
-
]);
|
|
1196
|
-
break;
|
|
1197
|
-
case 'tasks':
|
|
1198
|
-
case 'runs':
|
|
1199
|
-
content.addAll(<Widget>[
|
|
1308
|
+
if (item.sourceKind == 'screen') {
|
|
1309
|
+
content.addAll(<Widget>[
|
|
1310
|
+
_TimelineMetaLine(
|
|
1311
|
+
icon: Icons.computer_outlined,
|
|
1312
|
+
text:
|
|
1313
|
+
'${item.deviceLabel.ifEmpty('Desktop')} · ${item.appName.ifEmpty('Unknown app')}',
|
|
1314
|
+
),
|
|
1315
|
+
if (item.windowTitle.isNotEmpty)
|
|
1200
1316
|
_TimelineMetaLine(
|
|
1201
|
-
icon:
|
|
1202
|
-
|
|
1203
|
-
: Icons.monitor_heart_outlined,
|
|
1204
|
-
text: _titleCase(item.eventKind.replaceAll('_', ' ')),
|
|
1317
|
+
icon: Icons.web_asset_outlined,
|
|
1318
|
+
text: item.windowTitle,
|
|
1205
1319
|
),
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1320
|
+
_TimelineMetaLine(
|
|
1321
|
+
icon: Icons.schedule_outlined,
|
|
1322
|
+
text: item.screenSpanLabel,
|
|
1323
|
+
),
|
|
1324
|
+
]);
|
|
1325
|
+
} else {
|
|
1326
|
+
content.add(
|
|
1327
|
+
_TimelineMetaLine(
|
|
1328
|
+
icon: item.sourceKind == 'tasks'
|
|
1329
|
+
? Icons.task_alt_outlined
|
|
1330
|
+
: Icons.monitor_heart_outlined,
|
|
1331
|
+
text: _titleCase(item.eventKind.replaceAll('_', ' ')),
|
|
1332
|
+
),
|
|
1333
|
+
);
|
|
1334
|
+
if (item.runId.isNotEmpty && onOpenRun != null) {
|
|
1335
|
+
content.add(
|
|
1336
|
+
Padding(
|
|
1337
|
+
padding: const EdgeInsets.only(top: 8),
|
|
1338
|
+
child: Align(
|
|
1339
|
+
alignment: Alignment.centerLeft,
|
|
1340
|
+
child: TextButton.icon(
|
|
1341
|
+
onPressed: onOpenRun,
|
|
1342
|
+
icon: const Icon(Icons.open_in_new_rounded, size: 16),
|
|
1343
|
+
label: const Text('Open run'),
|
|
1216
1344
|
),
|
|
1217
1345
|
),
|
|
1218
|
-
]);
|
|
1219
|
-
break;
|
|
1220
|
-
default:
|
|
1221
|
-
content.add(
|
|
1222
|
-
_TimelineMetaLine(
|
|
1223
|
-
icon: Icons.info_outline_rounded,
|
|
1224
|
-
text: _titleCase(item.eventKind.replaceAll('_', ' ')),
|
|
1225
1346
|
),
|
|
1226
1347
|
);
|
|
1227
|
-
|
|
1348
|
+
}
|
|
1228
1349
|
}
|
|
1229
1350
|
return content;
|
|
1230
1351
|
}
|