From 6f2f86f936a90d23ff66c3cd623bb56debd024f1 Mon Sep 17 00:00:00 2001 From: Remy Moll Date: Sun, 16 Feb 2025 12:41:06 +0100 Subject: [PATCH] account for changed itineraries once landmarks are marked as done or deleted --- .../modules/current_trip_landmarks_list.dart | 16 ++- .../current_trip_loading_indicator.dart | 79 +++++------ frontend/lib/modules/landmark_card.dart | 127 ++++++++++-------- .../lib/modules/step_between_landmarks.dart | 8 ++ frontend/lib/structs/landmark.dart | 5 +- frontend/lib/structs/trip.dart | 10 +- 6 files changed, 135 insertions(+), 110 deletions(-) diff --git a/frontend/lib/modules/current_trip_landmarks_list.dart b/frontend/lib/modules/current_trip_landmarks_list.dart index 9ab4fbc..8a9f417 100644 --- a/frontend/lib/modules/current_trip_landmarks_list.dart +++ b/frontend/lib/modules/current_trip_landmarks_list.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:anyway/structs/landmark.dart'; @@ -24,10 +26,16 @@ List landmarksList(Trip trip, {required bool Function(Landmark) selector LandmarkCard(landmark, trip), ); - if (!landmark.visited && landmark.next != null) { - children.add( - StepBetweenLandmarks(current: landmark, next: landmark.next!) - ); + if (!landmark.visited) { + Landmark? nextLandmark = landmark.next; + while (nextLandmark != null && nextLandmark.visited) { + nextLandmark = nextLandmark.next; + } + if (nextLandmark != null) { + children.add( + StepBetweenLandmarks(current: landmark, next: nextLandmark!) + ); + } } } } diff --git a/frontend/lib/modules/current_trip_loading_indicator.dart b/frontend/lib/modules/current_trip_loading_indicator.dart index 5eaf037..83a216a 100644 --- a/frontend/lib/modules/current_trip_loading_indicator.dart +++ b/frontend/lib/modules/current_trip_loading_indicator.dart @@ -1,4 +1,5 @@ -import 'package:anyway/constants.dart'; +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:auto_size_text/auto_size_text.dart'; @@ -37,7 +38,10 @@ class _CurrentTripLoadingIndicatorState extends State FutureBuilder( Widget greeter; if (snapshot.hasData) { - greeter = AnimatedGradientText( - text: 'Creating your trip to ${snapshot.data}...', + greeter = AnimatedDotsText( + baseText: 'Creating your trip to ${snapshot.data}', style: greeterStyle, ); } else if (snapshot.hasError) { // the exact error is shown in the central part of the trip overview. No need to show it here - greeter = AnimatedGradientText( - text: 'Error while loading trip.', + greeter = Text( + 'Error while loading trip.', style: greeterStyle, ); } else { - greeter = AnimatedGradientText( - text: 'Creating your trip...', + greeter = AnimatedDotsText( + baseText: 'Creating your trip', style: greeterStyle, ); } @@ -101,61 +105,44 @@ Widget loadingText(Trip trip) => FutureBuilder( } ); -class AnimatedGradientText extends StatefulWidget { - final String text; +class AnimatedDotsText extends StatefulWidget { + final String baseText; final TextStyle style; - const AnimatedGradientText({ + const AnimatedDotsText({ Key? key, - required this.text, + required this.baseText, required this.style, }) : super(key: key); @override - _AnimatedGradientTextState createState() => _AnimatedGradientTextState(); + _AnimatedDotsTextState createState() => _AnimatedDotsTextState(); } -class _AnimatedGradientTextState extends State with SingleTickerProviderStateMixin { - late AnimationController _controller; +class _AnimatedDotsTextState extends State { + int dotCount = 0; @override void initState() { super.initState(); - _controller = AnimationController( - duration: const Duration(seconds: 1), - vsync: this, - )..repeat(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); + Timer.periodic(const Duration(seconds: 1), (timer) { + if (mounted) { + setState(() { + dotCount = (dotCount + 1) % 4; + // show up to 3 dots + }); + } else { + timer.cancel(); + } + }); } @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return ShaderMask( - shaderCallback: (bounds) { - return LinearGradient( - colors: [GRADIENT_START, GRADIENT_END, GRADIENT_START], - stops: [ - _controller.value - 1.0, - _controller.value, - _controller.value + 1.0, - ], - tileMode: TileMode.mirror, - ).createShader(bounds); - }, - child: Text( - widget.text, - style: widget.style, - ), - ); - }, + String dots = '.' * dotCount; + return Text( + '${widget.baseText}$dots', + style: widget.style, ); } } diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart index 9ef4875..8579219 100644 --- a/frontend/lib/modules/landmark_card.dart +++ b/frontend/lib/modules/landmark_card.dart @@ -47,32 +47,20 @@ class _LandmarkCardState extends State { AspectRatio( aspectRatio: 3 / 4, child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (widget.landmark.imageURL != null && widget.landmark.imageURL!.isNotEmpty) Expanded( child: CachedNetworkImage( imageUrl: widget.landmark.imageURL!, - placeholder: (context, url) => Center(child: CircularProgressIndicator()), - errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined), - fit: BoxFit.cover, + placeholder: (context, url) => const Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => imagePlaceholder(widget.landmark), + fit: BoxFit.cover ) ) else - Expanded( - child: - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [GRADIENT_START, GRADIENT_END], - ), - ), - child: Center( - child: Icon(widget.landmark.type.icon.icon, size: 50), - ), - ), - ), + imagePlaceholder(widget.landmark), + if (widget.landmark.type != typeStart && widget.landmark.type != typeFinish) Container( color: PRIMARY_COLOR, @@ -96,47 +84,54 @@ class _LandmarkCardState extends State { // Main information, useful buttons on the right Expanded( - child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.landmark.name, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - - if (widget.landmark.nameEN != null) - Text( - widget.landmark.nameEN!, - style: Theme.of(context).textTheme.bodyMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - - // fill the vspace - const Spacer(), - - SingleChildScrollView( - // allows the buttons to be scrolled - scrollDirection: Axis.horizontal, - child: Wrap( - spacing: 10, - // show the type, the website, and the wikipedia link as buttons/labels in a row - children: [ - doneToggleButton(), - if (widget.landmark.websiteURL != null) - websiteButton(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.landmark.name, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), - optionsButton() - ], - ), + if (widget.landmark.nameEN != null) + Text( + widget.landmark.nameEN!, + style: Theme.of(context).textTheme.bodyMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ] ), - ], - ), - ), + ), + + // fill the vspace + const Spacer(), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 5, right: 5, bottom: 10), + // the scroll view should be flush once the buttons are scrolled to the left + // but initially there should be some padding + child: Wrap( + spacing: 10, + // show the type, the website, and the wikipedia link as buttons/labels in a row + children: [ + doneToggleButton(), + if (widget.landmark.websiteURL != null) + websiteButton(), + + optionsButton() + ], + ), + ), + ], + ) ) ], ) @@ -195,3 +190,21 @@ class _LandmarkCardState extends State { ], ); } + + + +Widget imagePlaceholder (Landmark landmark) => Expanded( + child: + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [GRADIENT_START, GRADIENT_END], + ), + ), + child: Center( + child: Icon(landmark.type.icon.icon, size: 50), + ), + ), +); diff --git a/frontend/lib/modules/step_between_landmarks.dart b/frontend/lib/modules/step_between_landmarks.dart index 739f1b6..129763f 100644 --- a/frontend/lib/modules/step_between_landmarks.dart +++ b/frontend/lib/modules/step_between_landmarks.dart @@ -22,6 +22,14 @@ class _StepBetweenLandmarksState extends State { @override Widget build(BuildContext context) { int? time = widget.current.tripTime?.inMinutes; + + // since landmarks might have been marked as visited, the next landmark might not be the immediate next in the list + // => the precomputed trip time is not valid anymore + if (widget.current.next != widget.next) { + time = null; + } + + // round 0 travel time to 1 minute if (time != null && time < 1) { time = 1; } diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart index 8856047..5ef2cfd 100644 --- a/frontend/lib/structs/landmark.dart +++ b/frontend/lib/structs/landmark.dart @@ -29,9 +29,10 @@ final class Landmark extends LinkedListEntry{ final Duration? duration; bool visited; - // Next node + // Next node is implicitly available through the LinkedListEntry mixin // final Landmark? next; - final Duration? tripTime; + Duration? tripTime; + // the trip time depends on the next landmark, so it is not final Landmark({ diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart index e550540..820749e 100644 --- a/frontend/lib/structs/trip.dart +++ b/frontend/lib/structs/trip.dart @@ -75,8 +75,16 @@ class Trip with ChangeNotifier { notifyListeners(); } - void removeLandmark(Landmark landmark) { + void removeLandmark (Landmark landmark) async { + Landmark? previous = landmark.previous; + Landmark? next = landmark.next; landmarks.remove(landmark); + // removing the landmark means we need to recompute the time between the two adjoined landmarks + if (previous != null && next != null) { + // previous.next = next happens automatically since we are using a LinkedList + previous.tripTime = null; + // TODO + } notifyListeners(); }