@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
33import 'package:phoenix_socket/phoenix_socket.dart' ;
44import 'package:waydowntown/mixins/run_state_mixin.dart' ;
55import 'package:waydowntown/models/run.dart' ;
6+ import 'package:waydowntown/models/submission.dart' ;
67import 'package:waydowntown/run_header.dart' ;
8+ import 'package:waydowntown/services/user_service.dart' ;
79
810abstract class StringDetector {
911 Stream <String > get detectedStrings;
@@ -65,8 +67,8 @@ class CollectorGameState extends State<CollectorGame>
6567 List <HintItem > hints = [];
6668 bool isLoadingHint = false ;
6769 Map <String , String > itemErrors = {};
68-
69- final GlobalKey < AnimatedListState > _listKey = GlobalKey < AnimatedListState >() ;
70+ bool showIncorrectSubmissions = true ;
71+ String ? currentUserId ;
7072
7173 @override
7274 Dio get dio => widget.dio;
@@ -81,6 +83,13 @@ class CollectorGameState extends State<CollectorGame>
8183 WidgetsBinding .instance.addObserver (this );
8284 widget.detector.detectedStrings.listen (_onItemDetected);
8385 widget.detector.startDetecting ();
86+ UserService .getUserId ().then ((id) {
87+ if (mounted) {
88+ setState (() {
89+ currentUserId = id;
90+ });
91+ }
92+ });
8493 }
8594
8695 void _onItemDetected (String value) {
@@ -179,8 +188,6 @@ class CollectorGameState extends State<CollectorGame>
179188 if (mounted) {
180189 setState (() {
181190 detectedItems.insert (0 , item);
182- _listKey.currentState
183- ? .insertItem (0 , duration: const Duration (milliseconds: 300 ));
184191 });
185192 }
186193 }
@@ -189,21 +196,13 @@ class CollectorGameState extends State<CollectorGame>
189196 if (mounted) {
190197 setState (() {
191198 hints.insert (0 , hint);
192- _listKey.currentState
193- ? .insertItem (0 , duration: const Duration (milliseconds: 300 ));
194199 });
195200 }
196201 }
197202
198203 void _removeHint (HintItem hint) {
199204 final index = hints.indexOf (hint);
200205 if (index != - 1 ) {
201- final removedHint = hints[index];
202- _listKey.currentState? .removeItem (
203- index,
204- (context, animation) => _buildHintTile (removedHint, animation),
205- duration: const Duration (milliseconds: 300 ),
206- );
207206 setState (() {
208207 hints.removeAt (index);
209208 });
@@ -248,53 +247,43 @@ class CollectorGameState extends State<CollectorGame>
248247 }
249248 }
250249
251- Widget _buildItem (
252- BuildContext context, DetectedItem item, Animation <double > animation) {
253- return SlideTransition (
254- position: Tween <Offset >(
255- begin: const Offset (0 , - 1 ),
256- end: Offset .zero,
257- ).animate (CurvedAnimation (
258- parent: animation,
259- curve: Curves .easeInOutCubic,
260- )),
261- child: Column (
262- crossAxisAlignment: CrossAxisAlignment .start,
263- children: [
264- ListTile (
265- title: Text (item.value),
266- leading: _getIconForState (item.state, item.value),
267- trailing:
268- ! widget.autoSubmit && item.state == SubmissionState .unsubmitted
269- ? ElevatedButton (
270- onPressed: () => submitItem (item),
271- child: const Text ('Submit' ),
272- )
273- : null ,
274- onTap: item.state == SubmissionState .unsubmitted ||
275- item.state == SubmissionState .error
276- ? () => submitItem (item)
277- : null ,
278- ),
279- if (item.state == SubmissionState .correct && item.matchedHint != null )
280- Padding (
281- padding:
282- const EdgeInsets .only (left: 72.0 , right: 16.0 , bottom: 8.0 ),
283- child: Text (
284- item.matchedHint! ,
285- style: const TextStyle (
286- fontStyle: FontStyle .italic,
287- color: Colors .blue,
288- ),
250+ Widget _buildItem (BuildContext context, DetectedItem item) {
251+ return Column (
252+ crossAxisAlignment: CrossAxisAlignment .start,
253+ children: [
254+ ListTile (
255+ title: Text (item.value),
256+ leading: _getIconForState (item.state, item.value),
257+ trailing:
258+ ! widget.autoSubmit && item.state == SubmissionState .unsubmitted
259+ ? ElevatedButton (
260+ onPressed: () => submitItem (item),
261+ child: const Text ('Submit' ),
262+ )
263+ : null ,
264+ onTap: item.state == SubmissionState .unsubmitted ||
265+ item.state == SubmissionState .error
266+ ? () => submitItem (item)
267+ : null ,
268+ ),
269+ if (item.state == SubmissionState .correct && item.matchedHint != null )
270+ Padding (
271+ padding:
272+ const EdgeInsets .only (left: 72.0 , right: 16.0 , bottom: 8.0 ),
273+ child: Text (
274+ item.matchedHint! ,
275+ style: const TextStyle (
276+ fontStyle: FontStyle .italic,
277+ color: Colors .blue,
289278 ),
290279 ),
291- ] ,
292- ) ,
280+ ) ,
281+ ] ,
293282 );
294283 }
295284
296- Widget _buildHintTile (HintItem hint, [ Animation < double > ? animation] ) {
297- Widget tile = Card (
285+ Widget _buildHintTile (HintItem hint) {
286+ return Card (
298287 margin: const EdgeInsets .symmetric (horizontal: 8.0 , vertical: 4.0 ),
299288 color: Colors .blue.shade50,
300289 child: ListTile (
@@ -311,38 +300,73 @@ class CollectorGameState extends State<CollectorGame>
311300 ),
312301 ),
313302 );
303+ }
314304
315- if (animation != null ) {
316- return SlideTransition (
317- position: Tween <Offset >(
318- begin: const Offset (0 , - 1 ),
319- end: Offset .zero,
320- ).animate (CurvedAnimation (
321- parent: animation,
322- curve: Curves .easeInOutCubic,
323- )),
324- child: tile,
325- );
326- }
305+ Widget _buildSubmissionTile (Submission submission) {
306+ final state = submission.correct
307+ ? SubmissionState .correct
308+ : SubmissionState .incorrect;
309+ final isCurrentUser =
310+ currentUserId != null && submission.creatorId == currentUserId;
311+
312+ return ListTile (
313+ title: Text (submission.submission),
314+ leading: _getIconForState (state, submission.submission),
315+ subtitle: currentUserId == null
316+ ? null
317+ : Text (isCurrentUser ? 'You' : 'Teammate' ),
318+ );
319+ }
327320
328- return tile;
321+ List <Submission > _visibleTeamSubmissions () {
322+ return currentRun.submissions
323+ .where ((submission) => showIncorrectSubmissions || submission.correct)
324+ .toList ();
329325 }
330326
331- Widget _buildListItem (
332- BuildContext context, int index, Animation <double > animation) {
333- final allItems = [
334- ...hints.map ((hint) => MapEntry (hint.receivedAt,
335- (Animation <double > anim) => _buildHintTile (hint, anim))),
336- ...detectedItems.map ((item) => MapEntry (item.submittedAt,
337- (Animation <double > anim) => _buildItem (context, item, anim))),
338- ]..sort ((a, b) => b.key.compareTo (a.key));
327+ List <_TimelineItem > _buildTimelineItems (BuildContext context) {
328+ final items = < _TimelineItem > [];
329+ final teamSubmissions = _visibleTeamSubmissions ();
330+ final teamSubmissionValues =
331+ teamSubmissions.map ((submission) => submission.submission).toSet ();
332+
333+ for (final hint in hints) {
334+ items.add (_TimelineItem (
335+ timestamp: hint.receivedAt,
336+ widget: _buildHintTile (hint),
337+ ));
338+ }
339+
340+ for (final item in detectedItems) {
341+ if (! showIncorrectSubmissions && item.state == SubmissionState .incorrect) {
342+ continue ;
343+ }
344+ if ((item.state == SubmissionState .correct ||
345+ item.state == SubmissionState .incorrect) &&
346+ teamSubmissionValues.contains (item.value)) {
347+ continue ;
348+ }
349+
350+ items.add (_TimelineItem (
351+ timestamp: item.submittedAt,
352+ widget: _buildItem (context, item),
353+ ));
354+ }
355+
356+ for (final submission in teamSubmissions) {
357+ items.add (_TimelineItem (
358+ timestamp: submission.insertedAt,
359+ widget: _buildSubmissionTile (submission),
360+ ));
361+ }
339362
340- return allItems[index].value (animation);
363+ items.sort ((a, b) => b.timestamp.compareTo (a.timestamp));
364+ return items;
341365 }
342366
343367 @override
344368 Widget build (BuildContext context) {
345- final int totalItems = detectedItems.length + hints.length ;
369+ final timelineItems = _buildTimelineItems (context) ;
346370
347371 final unrevealedHintsExist = widget.run.specification.answers
348372 ? .any ((answer) => answer.hasHint && answer.hint == null );
@@ -352,6 +376,19 @@ class CollectorGameState extends State<CollectorGame>
352376 appBar: AppBar (
353377 title: Text (widget.runtimeType.toString ()),
354378 actions: [
379+ IconButton (
380+ icon: Icon (showIncorrectSubmissions
381+ ? Icons .visibility
382+ : Icons .visibility_off),
383+ tooltip: showIncorrectSubmissions
384+ ? 'Hide incorrect submissions'
385+ : 'Show incorrect submissions' ,
386+ onPressed: () {
387+ setState (() {
388+ showIncorrectSubmissions = ! showIncorrectSubmissions;
389+ });
390+ },
391+ ),
355392 if (showHintButton)
356393 IconButton (
357394 icon: isLoadingHint
@@ -379,13 +416,14 @@ class CollectorGameState extends State<CollectorGame>
379416 else
380417 widget.inputBuilder (context, widget.detector),
381418 Expanded (
382- child: AnimatedList (
383- key: _listKey,
384- initialItemCount: totalItems,
385- itemBuilder: (context, index, animation) {
386- return _buildListItem (context, index, animation);
387- },
388- ),
419+ child: timelineItems.isEmpty
420+ ? const Center (child: Text ('No submissions yet.' ))
421+ : ListView .builder (
422+ itemCount: timelineItems.length,
423+ itemBuilder: (context, index) {
424+ return timelineItems[index].widget;
425+ },
426+ ),
389427 ),
390428 ],
391429 ),
@@ -414,3 +452,10 @@ class CollectorGameState extends State<CollectorGame>
414452 super .dispose ();
415453 }
416454}
455+
456+ class _TimelineItem {
457+ final DateTime timestamp;
458+ final Widget widget;
459+
460+ const _TimelineItem ({required this .timestamp, required this .widget});
461+ }
0 commit comments