account for changed itineraries once landmarks are marked as done or deleted

This commit is contained in:
Remy Moll 2025-02-16 12:41:06 +01:00
parent 56c55883ea
commit 6f2f86f936
6 changed files with 135 additions and 110 deletions

View File

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:anyway/structs/landmark.dart'; import 'package:anyway/structs/landmark.dart';
@ -24,10 +26,16 @@ List<Widget> landmarksList(Trip trip, {required bool Function(Landmark) selector
LandmarkCard(landmark, trip), LandmarkCard(landmark, trip),
); );
if (!landmark.visited && landmark.next != null) { if (!landmark.visited) {
children.add( Landmark? nextLandmark = landmark.next;
StepBetweenLandmarks(current: landmark, next: landmark.next!) while (nextLandmark != null && nextLandmark.visited) {
); nextLandmark = nextLandmark.next;
}
if (nextLandmark != null) {
children.add(
StepBetweenLandmarks(current: landmark, next: nextLandmark!)
);
}
} }
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:anyway/constants.dart'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
@ -37,7 +38,10 @@ class _CurrentTripLoadingIndicatorState extends State<CurrentTripLoadingIndicato
// As a gimmick, and a way to show that the app is still working, show a few loading dots // As a gimmick, and a way to show that the app is still working, show a few loading dots
const Align( const Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: StatusText(), child: Padding(
padding: EdgeInsets.only(bottom: 12),
child: StatusText(),
)
) )
], ],
); );
@ -81,19 +85,19 @@ Widget loadingText(Trip trip) => FutureBuilder(
Widget greeter; Widget greeter;
if (snapshot.hasData) { if (snapshot.hasData) {
greeter = AnimatedGradientText( greeter = AnimatedDotsText(
text: 'Creating your trip to ${snapshot.data}...', baseText: 'Creating your trip to ${snapshot.data}',
style: greeterStyle, style: greeterStyle,
); );
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
// the exact error is shown in the central part of the trip overview. No need to show it here // the exact error is shown in the central part of the trip overview. No need to show it here
greeter = AnimatedGradientText( greeter = Text(
text: 'Error while loading trip.', 'Error while loading trip.',
style: greeterStyle, style: greeterStyle,
); );
} else { } else {
greeter = AnimatedGradientText( greeter = AnimatedDotsText(
text: 'Creating your trip...', baseText: 'Creating your trip',
style: greeterStyle, style: greeterStyle,
); );
} }
@ -101,61 +105,44 @@ Widget loadingText(Trip trip) => FutureBuilder(
} }
); );
class AnimatedGradientText extends StatefulWidget { class AnimatedDotsText extends StatefulWidget {
final String text; final String baseText;
final TextStyle style; final TextStyle style;
const AnimatedGradientText({ const AnimatedDotsText({
Key? key, Key? key,
required this.text, required this.baseText,
required this.style, required this.style,
}) : super(key: key); }) : super(key: key);
@override @override
_AnimatedGradientTextState createState() => _AnimatedGradientTextState(); _AnimatedDotsTextState createState() => _AnimatedDotsTextState();
} }
class _AnimatedGradientTextState extends State<AnimatedGradientText> with SingleTickerProviderStateMixin { class _AnimatedDotsTextState extends State<AnimatedDotsText> {
late AnimationController _controller; int dotCount = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = AnimationController( Timer.periodic(const Duration(seconds: 1), (timer) {
duration: const Duration(seconds: 1), if (mounted) {
vsync: this, setState(() {
)..repeat(); dotCount = (dotCount + 1) % 4;
} // show up to 3 dots
});
@override } else {
void dispose() { timer.cancel();
_controller.dispose(); }
super.dispose(); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedBuilder( String dots = '.' * dotCount;
animation: _controller, return Text(
builder: (context, child) { '${widget.baseText}$dots',
return ShaderMask( style: widget.style,
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,
),
);
},
); );
} }
} }

View File

@ -47,32 +47,20 @@ class _LandmarkCardState extends State<LandmarkCard> {
AspectRatio( AspectRatio(
aspectRatio: 3 / 4, aspectRatio: 3 / 4,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (widget.landmark.imageURL != null && widget.landmark.imageURL!.isNotEmpty) if (widget.landmark.imageURL != null && widget.landmark.imageURL!.isNotEmpty)
Expanded( Expanded(
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL!, imageUrl: widget.landmark.imageURL!,
placeholder: (context, url) => Center(child: CircularProgressIndicator()), placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined), errorWidget: (context, url, error) => imagePlaceholder(widget.landmark),
fit: BoxFit.cover, fit: BoxFit.cover
) )
) )
else else
Expanded( imagePlaceholder(widget.landmark),
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),
),
),
),
if (widget.landmark.type != typeStart && widget.landmark.type != typeFinish) if (widget.landmark.type != typeStart && widget.landmark.type != typeFinish)
Container( Container(
color: PRIMARY_COLOR, color: PRIMARY_COLOR,
@ -96,47 +84,54 @@ class _LandmarkCardState extends State<LandmarkCard> {
// Main information, useful buttons on the right // Main information, useful buttons on the right
Expanded( Expanded(
child: Padding( child: Column(
padding: const EdgeInsets.all(10), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Padding(
children: [ padding: const EdgeInsets.all(10),
Text( child: Column(
widget.landmark.name, crossAxisAlignment: CrossAxisAlignment.start,
style: Theme.of(context).textTheme.titleMedium, children: [
overflow: TextOverflow.ellipsis, Text(
maxLines: 2, widget.landmark.name,
), style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.ellipsis,
if (widget.landmark.nameEN != null) maxLines: 2,
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(),
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<LandmarkCard> {
], ],
); );
} }
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),
),
),
);

View File

@ -22,6 +22,14 @@ class _StepBetweenLandmarksState extends State<StepBetweenLandmarks> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int? time = widget.current.tripTime?.inMinutes; 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) { if (time != null && time < 1) {
time = 1; time = 1;
} }

View File

@ -29,9 +29,10 @@ final class Landmark extends LinkedListEntry<Landmark>{
final Duration? duration; final Duration? duration;
bool visited; bool visited;
// Next node // Next node is implicitly available through the LinkedListEntry mixin
// final Landmark? next; // final Landmark? next;
final Duration? tripTime; Duration? tripTime;
// the trip time depends on the next landmark, so it is not final
Landmark({ Landmark({

View File

@ -75,8 +75,16 @@ class Trip with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void removeLandmark(Landmark landmark) { void removeLandmark (Landmark landmark) async {
Landmark? previous = landmark.previous;
Landmark? next = landmark.next;
landmarks.remove(landmark); 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(); notifyListeners();
} }