better visual coherence

This commit is contained in:
Remy Moll 2024-09-25 14:47:13 +02:00
parent d323194ea7
commit 88b825ea31
16 changed files with 221 additions and 131 deletions

View File

@ -13,6 +13,11 @@ const Color GRADIENT_END = Color(0xFFE72E77);
const Color PRIMARY_COLOR = Color(0xFFF38F1A);
const double TRIP_PANEL_MAX_HEIGHT = 0.8;
const double TRIP_PANEL_MIN_HEIGHT = 0.12;
ThemeData APP_THEME = ThemeData(
primaryColor: PRIMARY_COLOR,
@ -33,20 +38,43 @@ ThemeData APP_THEME = ThemeData(
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
backgroundColor: PRIMARY_COLOR,
textStyle: TextStyle(
color: Colors.red
)
textButtonTheme: const TextButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
side: WidgetStatePropertyAll(
BorderSide(
color: PRIMARY_COLOR,
width: 1,
),
),
)
),
),
buttonTheme: ButtonThemeData(
buttonColor: PRIMARY_COLOR,
textTheme: ButtonTextTheme.primary,
elevatedButtonTheme: const ElevatedButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
)
),
outlinedButtonTheme: const OutlinedButtonThemeData(
style: ButtonStyle(
foregroundColor: WidgetStatePropertyAll(PRIMARY_COLOR),
)
),
cardTheme: const CardTheme(
shadowColor: Colors.grey,
elevation: 2,
margin: EdgeInsets.all(10),
),
sliderTheme: const SliderThemeData(
trackHeight: 15,
inactiveTrackColor: Colors.grey,
thumbColor: PRIMARY_COLOR,
activeTrackColor: GRADIENT_END
)
);

View File

@ -1,5 +1,6 @@
import 'dart:developer';
import 'package:anyway/constants.dart';
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
@ -20,8 +21,12 @@ class Greeter extends StatefulWidget {
class _GreeterState extends State<Greeter> {
Widget greeterBuilder (BuildContext context, Widget? child) {
ThemeData theme = Theme.of(context);
TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24);
final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0));
TextStyle greeterStyle = TextStyle(
foreground: Paint()..shader = textGradient,
fontWeight: FontWeight.bold,
fontSize: 26
);
Widget topGreeter;
@ -91,26 +96,10 @@ class _GreeterState extends State<Greeter> {
}
return Center(
child: Column(
children: [
// Padding(padding: EdgeInsets.only(top: 20)),
topGreeter,
Padding(
padding: EdgeInsets.all(20),
child: bottomGreeter
),
],
)
child: topGreeter,
);
}
Widget bottomGreeter = const Text(
"Busy day ahead? Here is how to make the most of it!",
style: TextStyle(color: Colors.black, fontSize: 18),
textAlign: TextAlign.center,
);
@override
Widget build(BuildContext context) {

View File

@ -16,7 +16,7 @@ List<Widget> landmarksList(Trip trip) {
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == start ) {
if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) {
children.add(
const Text("No landmarks in this trip"),
);

View File

@ -1,7 +1,7 @@
import 'dart:collection';
import 'package:anyway/constants.dart';
import 'package:anyway/modules/themed_marker.dart';
import 'package:anyway/modules/landmark_map_marker.dart';
import 'package:flutter/material.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';

View File

@ -0,0 +1,82 @@
import 'package:anyway/constants.dart';
import 'package:flutter/material.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/modules/current_trip_summary.dart';
import 'package:anyway/modules/current_trip_save_button.dart';
import 'package:anyway/modules/current_trip_landmarks_list.dart';
import 'package:anyway/modules/current_trip_greeter.dart';
class CurrentTripPanel extends StatefulWidget {
final ScrollController controller;
final Trip trip;
const CurrentTripPanel({
super.key,
required this.controller,
required this.trip,
});
@override
State<CurrentTripPanel> createState() => _CurrentTripPanelState();
}
class _CurrentTripPanelState extends State<CurrentTripPanel> {
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.trip,
builder: (context, child) {
if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') {
return ListView(
controller: widget.controller,
padding: const EdgeInsets.only(bottom: 30, left: 5, right: 5),
children: [
SizedBox(
// reuse the exact same height as the panel has when collapsed
// this way the greeter will be centered when the panel is collapsed
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
child: Greeter(trip: widget.trip),
),
const Padding(padding: EdgeInsets.only(top: 10)),
// CurrentTripSummary(trip: widget.trip),
// const Divider(),
...landmarksList(widget.trip),
const Padding(padding: EdgeInsets.only(top: 10)),
Center(child: saveButton(widget.trip)),
],
);
} else if(widget.trip.uuid == 'pending') {
return SizedBox(
// reuse the exact same height as the panel has when collapsed
// this way the greeter will be centered when the panel is collapsed
height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 20,
child: Greeter(trip: widget.trip)
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 50,
),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text('Error: ${widget.trip.errorDescription}'),
),
],
);
}
}
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:anyway/main.dart';
import 'package:anyway/structs/trip.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
@ -8,6 +9,13 @@ Widget saveButton(Trip trip) => ElevatedButton(
onPressed: () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
trip.toPrefs(prefs);
rootScaffoldMessengerKey.currentState!.showSnackBar(
SnackBar(
content: Text('Trip saved'),
duration: Duration(seconds: 2),
dismissDirection: DismissDirection.horizontal
)
);
},
child: SizedBox(
width: 100,

View File

@ -0,0 +1,31 @@
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
class CurrentTripSummary extends StatefulWidget {
final Trip trip;
const CurrentTripSummary({
super.key,
required this.trip,
});
@override
State<CurrentTripSummary> createState() => _CurrentTripSummaryState();
}
class _CurrentTripSummaryState extends State<CurrentTripSummary> {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Summary'),
// Text('Start: ${widget.trip.start}'),
// Text('End: ${widget.trip.end}'),
Text('Total duration: ${widget.trip.totalTime}'),
Text('Total distance: ${widget.trip.totalTime}'),
// Text('Fuel: ${widget.trip.fuel}'),
// Text('Cost: ${widget.trip.cost}'),
],
);
}
}

View File

@ -36,7 +36,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
width: 160,
child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL ?? '',
placeholder: (context, url) => CircularProgressIndicator(),
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
// TODO: make this a switch statement to load a placeholder if null
// cover the whole container meaning the image will be cropped
@ -84,21 +84,18 @@ class _LandmarkCardState extends State<LandmarkCard> {
// show the type, the website, and the wikipedia link as buttons/labels in a row
children: [
TextButton.icon(
style: theme.iconButtonTheme.style,
onPressed: () {},
icon: widget.landmark.type.icon,
label: Text(widget.landmark.type.name),
),
if (widget.landmark.duration != null && widget.landmark.duration!.inMinutes > 0)
TextButton.icon(
style: theme.iconButtonTheme.style,
onPressed: () {},
icon: Icon(Icons.hourglass_bottom),
label: Text('${widget.landmark.duration!.inMinutes} minutes'),
),
if (widget.landmark.websiteURL != null)
TextButton.icon(
style: theme.iconButtonTheme.style,
onPressed: () async {
// open a browser with the website link
await launchUrl(Uri.parse(widget.landmark.websiteURL!));
@ -108,7 +105,6 @@ class _LandmarkCardState extends State<LandmarkCard> {
),
if (widget.landmark.wikipediaURL != null)
TextButton.icon(
style: theme.iconButtonTheme.style,
onPressed: () async {
// open a browser with the wikipedia link
await launchUrl(Uri.parse(widget.landmark.wikipediaURL!));

View File

@ -17,21 +17,9 @@ class ThemedMarker extends StatelessWidget {
Widget build(BuildContext context) {
// This returns an outlined circle, with an icon corresponding to the landmark type
// As a small dot, the number of the landmark is displayed in the top right
Icon icon;
if (landmark.type == sightseeing) {
icon = Icon(Icons.church, color: Colors.black, size: 50);
} else if (landmark.type == nature) {
icon = Icon(Icons.park, color: Colors.black, size: 50);
} else if (landmark.type == shopping) {
icon = Icon(Icons.shopping_cart, color: Colors.black, size: 50);
} else if (landmark.type == start || landmark.type == finish) {
icon = Icon(Icons.flag, color: Colors.black, size: 50);
} else {
icon = Icon(Icons.location_on, color: Colors.black, size: 50);
}
Widget? positionIndicator;
if (landmark.type != start && landmark.type != finish) {
if (landmark.type != typeStart && landmark.type != typeFinish) {
positionIndicator = Positioned(
top: 0,
right: 0,
@ -56,8 +44,10 @@ class ThemedMarker extends StatelessWidget {
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 5),
),
padding: EdgeInsets.all(5),
child: icon
width: 70,
height: 70,
padding: const EdgeInsets.all(5),
child: Icon(landmark.type.icon.icon, size: 50),
),
if (positionIndicator != null) positionIndicator,
],

View File

@ -42,7 +42,7 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
uuid: 'pending',
name: query,
location: [location.latitude, location.longitude],
type: start
type: typeStart
)
);
}
@ -65,9 +65,8 @@ class _NewTripLocationSearchState extends State<NewTripLocationSearch> {
late Widget useCurrentLocationButton = ElevatedButton(
onPressed: () {
// setTripLocation(location);
// TODO: get current location
onPressed: () async {
},
child: Text('Use current location'),
);

View File

@ -2,7 +2,7 @@
import 'dart:developer';
import 'package:anyway/constants.dart';
import 'package:anyway/modules/themed_marker.dart';
import 'package:anyway/modules/landmark_map_marker.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
@ -37,7 +37,7 @@ class _NewTripMapState extends State<NewTripMap> {
uuid: 'pending',
name: 'start',
location: [location.latitude, location.longitude],
type: start
type: typeStart
)
);
}

View File

@ -1,11 +1,10 @@
import 'package:anyway/modules/current_trip_save_button.dart';
import 'package:anyway/constants.dart';
import 'package:flutter/material.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
import 'package:anyway/structs/trip.dart';
import 'package:anyway/modules/current_trip_landmarks_list.dart';
import 'package:anyway/modules/current_trip_greeter.dart';
import 'package:anyway/modules/current_trip_map.dart';
import 'package:anyway/modules/current_trip_panel.dart';
@ -27,15 +26,20 @@ class _TripPageState extends State<TripPage> {
@override
Widget build(BuildContext context) {
return SlidingUpPanel(
panelBuilder: (sc) => _panelFull(sc),
// collapsed: _floatingCollapsed(),
// use panelBuilder instead of panel so that we can reuse the scrollcontroller for the listview
panelBuilder: (scrollcontroller) => CurrentTripPanel(controller: scrollcontroller, trip: widget.trip),
// using collapsed and panelBuilder seems to show both at the same time, so we include the greeter in the panelBuilder
// collapsed: Greeter(trip: widget.trip),
body: CurrentTripMap(trip: widget.trip),
// renderPanelSheet: false,
// backdropEnabled: true,
maxHeight: MediaQuery.of(context).size.height * 0.8,
padding: EdgeInsets.only(left: 10, right: 10, top: 25, bottom: 10),
// panelSnapping: false,
borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
minHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT,
maxHeight: MediaQuery.of(context).size.height * TRIP_PANEL_MAX_HEIGHT,
// padding in this context is annoying: it offsets the notion of vertical alignment.
// children that want to be centered vertically need to have their size adjusted by 2x the padding
padding: const EdgeInsets.only(top: 10),
// Panel snapping should not be disabled because it significantly improves the user experience
// panelSnapping: false
borderRadius: const BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
parallaxEnabled: true,
boxShadow: const [
BoxShadow(
blurRadius: 20.0,
@ -44,41 +48,4 @@ class _TripPageState extends State<TripPage> {
],
);
}
Widget _panelFull(ScrollController sc) {
return ListenableBuilder(
listenable: widget.trip,
builder: (context, child) {
if (widget.trip.uuid != 'pending' && widget.trip.uuid != 'error') {
return ListView(
controller: sc,
padding: EdgeInsets.only(bottom: 35),
children: [
Greeter(trip: widget.trip),
...landmarksList(widget.trip),
Padding(padding: EdgeInsets.only(top: 10)),
Center(child: saveButton(widget.trip)),
],
);
} else if(widget.trip.uuid == 'pending') {
return Greeter(trip: widget.trip);
} else {
return Column(
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${widget.trip.errorDescription}'),
),
],
);
}
}
);
}
}

View File

@ -28,25 +28,26 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
// child: Icon(Icons.person, size: 100),
// )
// ),
Padding(padding: EdgeInsets.only(top: 30)),
Center(
child: FutureBuilder(
future: widget.trip.cityName,
builder: (context, snapshot) => Text(
'New trip to ${snapshot.hasData ? snapshot.data! : "..."}',
style: TextStyle(fontSize: 24)
'Your trip to ${snapshot.hasData ? snapshot.data! : "..."}',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)
)
)
),
Divider(indent: 25, endIndent: 25, height: 50),
Center(
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
padding: EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 0),
child: Text('Tell us about your ideal trip.', style: TextStyle(fontSize: 18))
),
),
Divider(indent: 25, endIndent: 25, height: 50),
durationPicker(preferences.maxTime),
preferenceSliders([preferences.sightseeing, preferences.shopping, preferences.nature]),
@ -63,7 +64,7 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
shadowColor: Colors.grey,
child: ListTile(
leading: Icon(Icons.timer),
title: Text('How long should the trip be?'),
title: Text(preferences.maxTime.description),
subtitle: CupertinoTimerPicker(
mode: CupertinoTimerPickerMode.hm,
initialTimerDuration: Duration(minutes: 90),
@ -84,8 +85,6 @@ class _NewTripPreferencesPageState extends State<NewTripPreferencesPage> {
for (SinglePreference pref in prefs) {
sliders.add(
Card(
margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
shadowColor: Colors.grey,
child: ListTile(
leading: pref.icon,
title: Text(pref.name),

View File

@ -174,8 +174,8 @@ class _SettingsPageState extends State<SettingsPage> {
Text('Our privacy policy is available under:'),
TextButton.icon(
icon: Icon(Icons.info, color: Colors.white),
label: Text(PRIVACY_URL, style: TextStyle(color: Colors.white)),
icon: Icon(Icons.info),
label: Text(PRIVACY_URL),
onPressed: () async{
await launchUrl(Uri.parse(PRIVACY_URL));
}

View File

@ -4,13 +4,13 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
LandmarkType sightseeing = LandmarkType(name: 'sightseeing');
LandmarkType nature = LandmarkType(name: 'nature');
LandmarkType shopping = LandmarkType(name: 'shopping');
LandmarkType typeSightseeing = LandmarkType(name: 'sightseeing');
LandmarkType typeNature = LandmarkType(name: 'nature');
LandmarkType typeShopping = LandmarkType(name: 'shopping');
// LandmarkType museum = LandmarkType(name: 'Museum');
// LandmarkType restaurant = LandmarkType(name: 'Restaurant');
LandmarkType start = LandmarkType(name: 'start');
LandmarkType finish = LandmarkType(name: 'finish');
LandmarkType typeStart = LandmarkType(name: 'start');
LandmarkType typeFinish = LandmarkType(name: 'finish');
final class Landmark extends LinkedListEntry<Landmark>{
@ -130,22 +130,22 @@ class LandmarkType {
LandmarkType({required this.name, this.icon = const Icon(Icons.location_on)}) {
switch (name) {
case 'sightseeing':
icon = Icon(Icons.church);
icon = const Icon(Icons.church);
break;
case 'nature':
icon = Icon(Icons.eco);
icon = const Icon(Icons.eco);
break;
case 'shopping':
icon = Icon(Icons.shopping_cart);
icon = const Icon(Icons.shopping_cart);
break;
case 'start':
icon = Icon(Icons.play_arrow);
icon = const Icon(Icons.play_arrow);
break;
case 'finish':
icon = Icon(Icons.flag);
icon = const Icon(Icons.flag);
break;
default:
icon = Icon(Icons.location_on);
icon = const Icon(Icons.location_on);
}
}
@override

View File

@ -1,3 +1,4 @@
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart';
@ -28,27 +29,27 @@ class UserPreferences {
slug: "sightseeing",
description: "How much do you like sightseeing?",
value: 0,
icon: Icon(Icons.church),
icon: typeSightseeing.icon,
);
SinglePreference shopping = SinglePreference(
name: "Shopping",
slug: "shopping",
description: "How much do you like shopping?",
value: 0,
icon: Icon(Icons.shopping_bag),
icon: typeShopping.icon,
);
SinglePreference nature = SinglePreference(
name: "Nature",
slug: "nature",
description: "How much do you like nature?",
value: 0,
icon: Icon(Icons.landscape),
icon: typeNature.icon,
);
SinglePreference maxTime = SinglePreference(
name: "Trip duration",
slug: "duration",
description: "How long do you want your trip to be?",
description: "How long should your trip be?",
value: 30,
minVal: 30,
maxVal: 720,