diff --git a/frontend/lib/constants.dart b/frontend/lib/constants.dart index 7856578..d4e756c 100644 --- a/frontend/lib/constants.dart +++ b/frontend/lib/constants.dart @@ -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 + ) ); diff --git a/frontend/lib/modules/current_trip_greeter.dart b/frontend/lib/modules/current_trip_greeter.dart index b0771d5..6a2118a 100644 --- a/frontend/lib/modules/current_trip_greeter.dart +++ b/frontend/lib/modules/current_trip_greeter.dart @@ -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 { 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 { } 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) { diff --git a/frontend/lib/modules/current_trip_landmarks_list.dart b/frontend/lib/modules/current_trip_landmarks_list.dart index ada17f2..b486a97 100644 --- a/frontend/lib/modules/current_trip_landmarks_list.dart +++ b/frontend/lib/modules/current_trip_landmarks_list.dart @@ -16,7 +16,7 @@ List 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"), ); diff --git a/frontend/lib/modules/current_trip_map.dart b/frontend/lib/modules/current_trip_map.dart index 3c770f3..9efdc4c 100644 --- a/frontend/lib/modules/current_trip_map.dart +++ b/frontend/lib/modules/current_trip_map.dart @@ -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'; diff --git a/frontend/lib/modules/current_trip_panel.dart b/frontend/lib/modules/current_trip_panel.dart new file mode 100644 index 0000000..cd73f28 --- /dev/null +++ b/frontend/lib/modules/current_trip_panel.dart @@ -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 createState() => _CurrentTripPanelState(); +} + +class _CurrentTripPanelState extends State { + @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}'), + ), + ], + ); + } + } + ); + } +} diff --git a/frontend/lib/modules/current_trip_save_button.dart b/frontend/lib/modules/current_trip_save_button.dart index d95c580..f4587cf 100644 --- a/frontend/lib/modules/current_trip_save_button.dart +++ b/frontend/lib/modules/current_trip_save_button.dart @@ -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, diff --git a/frontend/lib/modules/current_trip_summary.dart b/frontend/lib/modules/current_trip_summary.dart new file mode 100644 index 0000000..77c9461 --- /dev/null +++ b/frontend/lib/modules/current_trip_summary.dart @@ -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 createState() => _CurrentTripSummaryState(); +} + +class _CurrentTripSummaryState extends State { + @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}'), + ], + ); + + } +} \ No newline at end of file diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart index 78b6a27..43d42ef 100644 --- a/frontend/lib/modules/landmark_card.dart +++ b/frontend/lib/modules/landmark_card.dart @@ -36,7 +36,7 @@ class _LandmarkCardState extends State { 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 { // 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 { ), 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!)); diff --git a/frontend/lib/modules/themed_marker.dart b/frontend/lib/modules/landmark_map_marker.dart similarity index 66% rename from frontend/lib/modules/themed_marker.dart rename to frontend/lib/modules/landmark_map_marker.dart index de52739..2e8f5d7 100644 --- a/frontend/lib/modules/themed_marker.dart +++ b/frontend/lib/modules/landmark_map_marker.dart @@ -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, ], diff --git a/frontend/lib/modules/new_trip_location_search.dart b/frontend/lib/modules/new_trip_location_search.dart index 2aae71b..c737a75 100644 --- a/frontend/lib/modules/new_trip_location_search.dart +++ b/frontend/lib/modules/new_trip_location_search.dart @@ -42,7 +42,7 @@ class _NewTripLocationSearchState extends State { uuid: 'pending', name: query, location: [location.latitude, location.longitude], - type: start + type: typeStart ) ); } @@ -65,9 +65,8 @@ class _NewTripLocationSearchState extends State { late Widget useCurrentLocationButton = ElevatedButton( - onPressed: () { - // setTripLocation(location); - // TODO: get current location + onPressed: () async { + }, child: Text('Use current location'), ); diff --git a/frontend/lib/modules/new_trip_map.dart b/frontend/lib/modules/new_trip_map.dart index 9c6c2cd..49445fc 100644 --- a/frontend/lib/modules/new_trip_map.dart +++ b/frontend/lib/modules/new_trip_map.dart @@ -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 { uuid: 'pending', name: 'start', location: [location.latitude, location.longitude], - type: start + type: typeStart ) ); } diff --git a/frontend/lib/pages/current_trip.dart b/frontend/lib/pages/current_trip.dart index 3e3c57e..834b3f6 100644 --- a/frontend/lib/pages/current_trip.dart +++ b/frontend/lib/pages/current_trip.dart @@ -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 { @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 { ], ); } - - - 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}'), - ), - ], - ); - } - } - ); - } } diff --git a/frontend/lib/pages/new_trip_preferences.dart b/frontend/lib/pages/new_trip_preferences.dart index 5fb7640..9f60055 100644 --- a/frontend/lib/pages/new_trip_preferences.dart +++ b/frontend/lib/pages/new_trip_preferences.dart @@ -28,25 +28,26 @@ class _NewTripPreferencesPageState extends State { // 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 { 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 { 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), diff --git a/frontend/lib/pages/settings.dart b/frontend/lib/pages/settings.dart index 4cb0248..8a97c90 100644 --- a/frontend/lib/pages/settings.dart +++ b/frontend/lib/pages/settings.dart @@ -174,8 +174,8 @@ class _SettingsPageState extends State { 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)); } diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart index e6d3734..6729a65 100644 --- a/frontend/lib/structs/landmark.dart +++ b/frontend/lib/structs/landmark.dart @@ -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{ @@ -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 diff --git a/frontend/lib/structs/preferences.dart b/frontend/lib/structs/preferences.dart index 315b5c0..e36f35d 100644 --- a/frontend/lib/structs/preferences.dart +++ b/frontend/lib/structs/preferences.dart @@ -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,