diff --git a/.gitea/workflows/workflow_build-app-android.yaml b/.gitea/workflows/workflow_build-app-android.yaml index 5002752..d7bfd82 100644 --- a/.gitea/workflows/workflow_build-app-android.yaml +++ b/.gitea/workflows/workflow_build-app-android.yaml @@ -37,8 +37,8 @@ jobs: - uses: https://github.com/actions/setup-java@v4 with: - java-version: '17' - distribution: 'zulu' + java-version: '21' + distribution: 'oracle' - name: Setup Android SDK uses: https://github.com/android-actions/setup-android@v3 diff --git a/.vscode/launch.json b/.vscode/launch.json index dc2c672..d586fee 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -33,8 +33,11 @@ "request": "launch", "program": "lib/main.dart", "cwd": "${workspaceFolder}/frontend", + "args": [ + "--dart-define-from-file=secrets.json" + ], "env": { - "GOOGLE_MAPS_API_KEY": "testing" + "ANDROID_GOOGLE_MAPS_API_KEY": "AIzaSyD6RK_pzKFc8T-t1t0jiC3PNRZwNXECFG4" } }, { diff --git a/default.nix b/default.nix index b183363..a2f69a4 100644 --- a/default.nix +++ b/default.nix @@ -1,17 +1,19 @@ { pkgs ? import { config.android_sdk.accept_license = true; config.allowUnfree = true; } }: pkgs.mkShell { - buildInputs = [ - pkgs.flutter - #pkgs.android-tools # for adb - #pkgs.openjdk # required for Android builds + + buildInputs = with pkgs; [ + flutter + android-tools # for adb + openjdk # required for Android builds + # pkgs.androidenv.androidPkgs.androidsdk # The customized SDK that we've made above + # androidenv.androidPkgs.ndk-bundle ]; # Set up Android SDK paths if needed + ANDROID_HOME = "/scratch/remy/android"; + shellHook = '' - export ANDROID_SDK_ROOT=${pkgs.androidsdk}/libexec/android-sdk - export PATH=$PATH:${pkgs.androidsdk}/libexec/android-sdk/platform-tools echo "Flutter dev environment ready. 'adb' and 'flutter' are available." ''; } - diff --git a/frontend/android/settings.gradle b/frontend/android/settings.gradle index 8db313d..4618afc 100644 --- a/frontend/android/settings.gradle +++ b/frontend/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false + id "com.android.application" version "8.7.0" apply false id "org.jetbrains.kotlin.android" version "2.0.20" apply false } diff --git a/frontend/lib/constants.dart b/frontend/lib/constants.dart index 3475d02..b382c38 100644 --- a/frontend/lib/constants.dart +++ b/frontend/lib/constants.dart @@ -26,13 +26,13 @@ ThemeData APP_THEME = ThemeData( cardColor: Colors.white, useMaterial3: true, - colorScheme: ColorScheme.light( + colorScheme: const ColorScheme.light( primary: PRIMARY_COLOR, secondary: GRADIENT_END, surface: Colors.white, error: Colors.red, onPrimary: Colors.white, - onSecondary: const Color.fromARGB(255, 30, 22, 22), + onSecondary: Color.fromARGB(255, 30, 22, 22), onSurface: Colors.black, onError: Colors.white, brightness: Brightness.light, @@ -64,12 +64,6 @@ ThemeData APP_THEME = ThemeData( ), - cardTheme: const CardTheme( - shadowColor: Colors.grey, - elevation: 2, - margin: EdgeInsets.all(10), - ), - sliderTheme: const SliderThemeData( trackHeight: 15, inactiveTrackColor: Colors.grey, @@ -83,4 +77,4 @@ const Gradient APP_GRADIENT = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [GRADIENT_START, GRADIENT_END], -); \ No newline at end of file +); diff --git a/frontend/lib/modules/current_trip_error_message.dart b/frontend/lib/modules/current_trip_error_message.dart index cf48f72..9d92de0 100644 --- a/frontend/lib/modules/current_trip_error_message.dart +++ b/frontend/lib/modules/current_trip_error_message.dart @@ -20,7 +20,7 @@ class _CurrentTripErrorMessageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( + const Text( "😢", style: TextStyle( fontSize: 40, diff --git a/frontend/lib/modules/current_trip_greeter.dart b/frontend/lib/modules/current_trip_greeter.dart index 7655b73..d071c4d 100644 --- a/frontend/lib/modules/current_trip_greeter.dart +++ b/frontend/lib/modules/current_trip_greeter.dart @@ -8,7 +8,7 @@ import 'package:anyway/structs/trip.dart'; class CurrentTripGreeter extends StatefulWidget { final Trip trip; - CurrentTripGreeter({ + const CurrentTripGreeter({ super.key, required this.trip, }); @@ -47,4 +47,4 @@ class _CurrentTripGreeterState extends State { ) ); -} \ No newline at end of file +} diff --git a/frontend/lib/modules/current_trip_landmarks_list.dart b/frontend/lib/modules/current_trip_landmarks_list.dart index 8a9f417..305559c 100644 --- a/frontend/lib/modules/current_trip_landmarks_list.dart +++ b/frontend/lib/modules/current_trip_landmarks_list.dart @@ -10,7 +10,7 @@ import 'package:anyway/modules/landmark_card.dart'; // Returns a list of widgets that represent the landmarks matching the given selector List landmarksList(Trip trip, {required bool Function(Landmark) selector}) { - + List children = []; if (trip.landmarks.isEmpty || trip.landmarks.length <= 1 && trip.landmarks.first.type == typeStart ) { @@ -30,10 +30,10 @@ List landmarksList(Trip trip, {required bool Function(Landmark) selector Landmark? nextLandmark = landmark.next; while (nextLandmark != null && nextLandmark.visited) { nextLandmark = nextLandmark.next; - } + } if (nextLandmark != null) { children.add( - StepBetweenLandmarks(current: landmark, next: nextLandmark!) + 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 60cc98c..e8e9921 100644 --- a/frontend/lib/modules/current_trip_loading_indicator.dart +++ b/frontend/lib/modules/current_trip_loading_indicator.dart @@ -49,7 +49,7 @@ class _CurrentTripLoadingIndicatorState extends State _StatusTextState(); @@ -110,10 +110,10 @@ class AnimatedDotsText extends StatefulWidget { final TextStyle style; const AnimatedDotsText({ - Key? key, + super.key, required this.baseText, required this.style, - }) : super(key: key); + }); @override _AnimatedDotsTextState createState() => _AnimatedDotsTextState(); diff --git a/frontend/lib/modules/current_trip_locations.dart b/frontend/lib/modules/current_trip_locations.dart new file mode 100644 index 0000000..63984a2 --- /dev/null +++ b/frontend/lib/modules/current_trip_locations.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:auto_size_text/auto_size_text.dart'; + +import 'package:anyway/structs/trip.dart'; + + +List> locationActions = [ + {'name': 'Toilet', 'action': () {}}, + {'name': 'Food', 'action': () {}}, + {'name': 'Surrounding landmarks', 'action': () {}}, + +]; + +class CurrentTripLocations extends StatefulWidget { + final Trip? trip; + + const CurrentTripLocations({super.key, this.trip}); + + @override + State createState() => _CurrentTripLocationsState(); +} + +class _CurrentTripLocationsState extends State { + @override + Widget build(BuildContext context) { + // A horizontally scrolling list of buttons with predefined actions + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), + child: Row( + children: [ + if (widget.trip != null) + for (Map action in locationActions) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 3.0), + child: ElevatedButton( + onPressed: action['action'], + child: AutoSizeText( + action['name'], + maxLines: 1, + minFontSize: 8, + maxFontSize: 16, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/modules/current_trip_map.dart b/frontend/lib/modules/current_trip_map.dart index d1c6407..ca8991d 100644 --- a/frontend/lib/modules/current_trip_map.dart +++ b/frontend/lib/modules/current_trip_map.dart @@ -13,7 +13,7 @@ import 'package:anyway/modules/landmark_map_marker.dart'; class CurrentTripMap extends StatefulWidget { final Trip? trip; - CurrentTripMap({this.trip}); + const CurrentTripMap({super.key, this.trip}); @override State createState() => _CurrentTripMapState(); @@ -22,7 +22,7 @@ class CurrentTripMap extends StatefulWidget { class _CurrentTripMapState extends State { late GoogleMapController mapController; - CameraPosition _cameraPosition = CameraPosition( + final CameraPosition _cameraPosition = const CameraPosition( target: LatLng(48.8566, 2.3522), zoom: 11.0, ); @@ -41,7 +41,7 @@ class _CurrentTripMapState extends State { void dispose() { widget.trip?.removeListener(setMapMarkers); widget.trip?.removeListener(setMapRoute); - + super.dispose(); } diff --git a/frontend/lib/modules/current_trip_overview.dart b/frontend/lib/modules/current_trip_overview.dart new file mode 100644 index 0000000..19f7492 --- /dev/null +++ b/frontend/lib/modules/current_trip_overview.dart @@ -0,0 +1,31 @@ +import 'package:anyway/structs/trip.dart'; + +import 'package:flutter/material.dart'; + +import 'package:anyway/modules/current_trip_map.dart'; +import 'package:anyway/modules/current_trip_locations.dart'; + + +class CurrentTripOverview extends StatefulWidget { + final Trip? trip; + + const CurrentTripOverview({super.key, this.trip}); + + + @override + State createState() => _CurrentTripOverviewState(); +} + +class _CurrentTripOverviewState extends State { + @override + Widget build(BuildContext context) { + // The background map has a horizontally scrolling list of rounded buttons overlaid + return Stack( + alignment: Alignment.topLeft, + children: [ + CurrentTripMap(trip: widget.trip), + CurrentTripLocations(trip: widget.trip), + ], + ); + } +} diff --git a/frontend/lib/modules/current_trip_panel.dart b/frontend/lib/modules/current_trip_panel.dart index f793b2c..599f7d4 100644 --- a/frontend/lib/modules/current_trip_panel.dart +++ b/frontend/lib/modules/current_trip_panel.dart @@ -45,7 +45,7 @@ class _CurrentTripPanelState extends State { // this way the greeter will be centered when the panel is collapsed // note that we need to account for the padding above height: MediaQuery.of(context).size.height * TRIP_PANEL_MIN_HEIGHT - 10, - child: Center(child: + child: Center(child: AutoSizeText( maxLines: 1, 'Error', @@ -81,7 +81,7 @@ class _CurrentTripPanelState extends State { ), Padding( - padding: EdgeInsets.all(10), + padding: const EdgeInsets.all(10), child: Container( decoration: BoxDecoration( color: Colors.grey[100], @@ -94,9 +94,6 @@ class _CurrentTripPanelState extends State { ExpansionTile( leading: const Icon(Icons.location_on), title: const Text('Visited Landmarks (tap to expand)'), - children: [ - ...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited), - ], visualDensity: VisualDensity.compact, collapsedShape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), @@ -104,12 +101,15 @@ class _CurrentTripPanelState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), + children: [ + ...landmarksList(widget.trip, selector: (Landmark landmark) => landmark.visited), + ], ), ], ), ), ), - + const Padding(padding: EdgeInsets.only(top: 10)), diff --git a/frontend/lib/modules/current_trip_save_button.dart b/frontend/lib/modules/current_trip_save_button.dart index 32d7ba6..1791496 100644 --- a/frontend/lib/modules/current_trip_save_button.dart +++ b/frontend/lib/modules/current_trip_save_button.dart @@ -20,14 +20,14 @@ class _saveButtonState extends State { onPressed: () async { savedTrips.addTrip(widget.trip); rootScaffoldMessengerKey.currentState!.showSnackBar( - SnackBar( + const SnackBar( content: Text('Trip saved'), duration: Duration(seconds: 2), dismissDirection: DismissDirection.horizontal ) ); }, - child: SizedBox( + child: const SizedBox( width: 100, child: Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart index 491b6da..20ac04b 100644 --- a/frontend/lib/modules/landmark_card.dart +++ b/frontend/lib/modules/landmark_card.dart @@ -13,12 +13,12 @@ import 'package:anyway/structs/landmark.dart'; class LandmarkCard extends StatefulWidget { final Landmark landmark; final Trip parentTrip; - - LandmarkCard( + + const LandmarkCard( this.landmark, this.parentTrip, ); - + @override _LandmarkCardState createState() => _LandmarkCardState(); } @@ -26,7 +26,7 @@ class LandmarkCard extends StatefulWidget { class _LandmarkCardState extends State { @override - Widget build(BuildContext context) { + Widget build(BuildContext context) { return Container( constraints: BoxConstraints( // express the max height in terms text lines @@ -38,7 +38,7 @@ class _LandmarkCardState extends State { ), elevation: 5, clipBehavior: Clip.antiAliasWithSaveLayer, - + // if the image is available, display it on the left side of the card, otherwise only display the text child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -66,11 +66,11 @@ class _LandmarkCardState extends State { color: PRIMARY_COLOR, child: Center( child: Padding( - padding: EdgeInsets.all(5), + padding: const EdgeInsets.all(5), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.timer_outlined, size: 16), + const Icon(Icons.timer_outlined, size: 16), Text("${widget.landmark.duration?.inMinutes} minutes"), ], ) @@ -97,7 +97,7 @@ class _LandmarkCardState extends State { overflow: TextOverflow.ellipsis, maxLines: 2, ), - + if (widget.landmark.nameEN != null) Text( widget.landmark.nameEN!, @@ -114,7 +114,7 @@ class _LandmarkCardState extends State { SingleChildScrollView( scrollDirection: Axis.horizontal, - padding: EdgeInsets.only(left: 5, right: 5, bottom: 10), + padding: const 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( @@ -124,7 +124,7 @@ class _LandmarkCardState extends State { doneToggleButton(), if (widget.landmark.websiteURL != null) websiteButton(), - + optionsButton() ], ), @@ -181,7 +181,7 @@ class _LandmarkCardState extends State { title: const Text('Favorite'), onTap: () async { rootScaffoldMessengerKey.currentState!.showSnackBar( - SnackBar(content: Text("Not implemented yet")) + const SnackBar(content: Text("Not implemented yet")) ); }, ), @@ -193,7 +193,7 @@ class _LandmarkCardState extends State { Widget imagePlaceholder (Landmark landmark) => Expanded( - child: + child: Container( decoration: const BoxDecoration( gradient: LinearGradient( diff --git a/frontend/lib/modules/landmark_map_marker.dart b/frontend/lib/modules/landmark_map_marker.dart index fccf50a..1a5512e 100644 --- a/frontend/lib/modules/landmark_map_marker.dart +++ b/frontend/lib/modules/landmark_map_marker.dart @@ -7,7 +7,7 @@ class ThemedMarker extends StatelessWidget { final Landmark landmark; final int position; - ThemedMarker({ + const ThemedMarker({ super.key, required this.landmark, required this.position @@ -24,12 +24,12 @@ class ThemedMarker extends StatelessWidget { top: 0, right: 0, child: Container( - padding: EdgeInsets.all(5), + padding: const EdgeInsets.all(5), decoration: BoxDecoration( color: Colors.grey[100], shape: BoxShape.circle, ), - child: Text('$position', style: TextStyle(color: Colors.black, fontSize: 25)), + child: Text('$position', style: const TextStyle(color: Colors.black, fontSize: 25)), ), ); } @@ -40,7 +40,7 @@ class ThemedMarker extends StatelessWidget { children: [ Container( decoration: BoxDecoration( - gradient: landmark.visited ? LinearGradient(colors: [Colors.grey, Colors.white]) : APP_GRADIENT, + gradient: landmark.visited ? const LinearGradient(colors: [Colors.grey, Colors.white]) : APP_GRADIENT, shape: BoxShape.circle, border: Border.all(color: Colors.black, width: 5), ), @@ -54,4 +54,4 @@ class ThemedMarker extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/lib/modules/new_trip_button.dart b/frontend/lib/modules/new_trip_button.dart index 3eb5d3b..639ce47 100644 --- a/frontend/lib/modules/new_trip_button.dart +++ b/frontend/lib/modules/new_trip_button.dart @@ -11,7 +11,7 @@ class NewTripButton extends StatefulWidget { final Trip trip; final UserPreferences preferences; - const NewTripButton({ + const NewTripButton({super.key, required this.trip, required this.preferences, }); @@ -35,8 +35,8 @@ class _NewTripButtonState extends State { return FloatingActionButton.extended( onPressed: onPressed, icon: const Icon(Icons.directions), - label: AutoSizeText('Start planning!'), - ); + label: const AutoSizeText('Start planning!'), + ); } ); } diff --git a/frontend/lib/modules/new_trip_location_search.dart b/frontend/lib/modules/new_trip_location_search.dart index d89d4a2..dc63166 100644 --- a/frontend/lib/modules/new_trip_location_search.dart +++ b/frontend/lib/modules/new_trip_location_search.dart @@ -21,7 +21,7 @@ const Map debugLocations = { class NewTripLocationSearch extends StatefulWidget { Future prefs = SharedPreferences.getInstance(); Trip trip; - + NewTripLocationSearch( this.trip, ); @@ -71,13 +71,13 @@ class _NewTripLocationSearchState extends State { hintText: 'Enter a city name or long press on the map.', onSubmitted: setTripLocation, controller: _controller, - leading: Icon(Icons.search), + leading: const Icon(Icons.search), trailing: [ ElevatedButton( onPressed: () { setTripLocation(_controller.text); }, - child: Text('Search'), + child: const Text('Search'), ) ] ); @@ -97,7 +97,7 @@ class _NewTripLocationSearchState extends State { ) ); }, - child: Text('Use current location'), + child: const Text('Use current location'), ); @override diff --git a/frontend/lib/modules/new_trip_map.dart b/frontend/lib/modules/new_trip_map.dart index b033b16..213c464 100644 --- a/frontend/lib/modules/new_trip_map.dart +++ b/frontend/lib/modules/new_trip_map.dart @@ -106,4 +106,4 @@ class _NewTripMapState extends State { myLocationEnabled: useLocation, ); } -} \ No newline at end of file +} diff --git a/frontend/lib/modules/new_trip_options_button.dart b/frontend/lib/modules/new_trip_options_button.dart index 499c642..dea117c 100644 --- a/frontend/lib/modules/new_trip_options_button.dart +++ b/frontend/lib/modules/new_trip_options_button.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; class NewTripOptionsButton extends StatefulWidget { final Trip trip; - const NewTripOptionsButton({required this.trip}); + const NewTripOptionsButton({super.key, required this.trip}); @override State createState() => _NewTripOptionsButtonState(); @@ -33,7 +33,7 @@ class _NewTripOptionsButtonState extends State { }, icon: const Icon(Icons.add), label: const AutoSizeText('Set preferences') - ); + ); } ); } diff --git a/frontend/lib/modules/onbarding_agreement_card.dart b/frontend/lib/modules/onbarding_agreement_card.dart index bc7a293..6fbe370 100644 --- a/frontend/lib/modules/onbarding_agreement_card.dart +++ b/frontend/lib/modules/onbarding_agreement_card.dart @@ -13,8 +13,8 @@ class OnboardingAgreementCard extends StatefulWidget { final ValueChanged onAgreementChanged; - OnboardingAgreementCard({ - super.key, + const OnboardingAgreementCard({ + super.key, required this.title, required this.description, required this.imagePath, @@ -30,12 +30,12 @@ class _OnboardingAgreementCardState extends State { @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.all(20), + padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ OnboardingCard(title: widget.title, description: widget.description, imagePath: widget.imagePath), - Padding(padding: EdgeInsets.only(top: 20)), + const Padding(padding: EdgeInsets.only(top: 20)), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -65,7 +65,7 @@ class _OnboardingAgreementCardState extends State { ); }, ), - + // The text of the agreement Text( "I agree to the ", @@ -73,7 +73,7 @@ class _OnboardingAgreementCardState extends State { color: Colors.white, ), ), - + // The clickable text of the agreement that shows the agreement text GestureDetector( onTap: () { @@ -91,9 +91,9 @@ class _OnboardingAgreementCardState extends State { data: snapshot.data.toString(), ); } else { - return CircularProgressIndicator(); + return const CircularProgressIndicator(); } - + }, ) ); diff --git a/frontend/lib/modules/onboarding_card.dart b/frontend/lib/modules/onboarding_card.dart index 094d641..1202b73 100644 --- a/frontend/lib/modules/onboarding_card.dart +++ b/frontend/lib/modules/onboarding_card.dart @@ -6,7 +6,7 @@ class OnboardingCard extends StatelessWidget { final String description; final String imagePath; - const OnboardingCard({ + const OnboardingCard({super.key, required this.title, required this.description, required this.imagePath, @@ -14,9 +14,9 @@ class OnboardingCard extends StatelessWidget { @override Widget build(BuildContext context) { - + return Padding( - padding: EdgeInsets.all(20), + padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -26,12 +26,12 @@ class OnboardingCard extends StatelessWidget { color: Colors.white, ), ), - Padding(padding: EdgeInsets.only(top: 20)), + const Padding(padding: EdgeInsets.only(top: 20)), SvgPicture.asset( imagePath, height: 200, ), - Padding(padding: EdgeInsets.only(top: 20)), + const Padding(padding: EdgeInsets.only(top: 20)), Text( description, style: Theme.of(context).textTheme.bodyMedium!.copyWith( diff --git a/frontend/lib/modules/trips_saved_list.dart b/frontend/lib/modules/trips_saved_list.dart index fd53ada..d7bc1d1 100644 --- a/frontend/lib/modules/trips_saved_list.dart +++ b/frontend/lib/modules/trips_saved_list.dart @@ -17,48 +17,78 @@ class TripsOverview extends StatefulWidget { } class _TripsOverviewState extends State { - Widget listBuild (BuildContext context, SavedTrips trips) { - List children; - List items = trips.trips; - children = List.generate(items.length, (index) { - Trip trip = items[index]; - return ListTile( - title: FutureBuilder( - future: trip.cityName, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Text("Trip to ${snapshot.data}"); - } else if (snapshot.hasError) { - return Text("Error: ${snapshot.error}"); - } else { - return const Text("Trip to ..."); - } - }, - ), - leading: Icon(Icons.pin_drop), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => TripPage(trip: trip) - ) - ); + Widget tripListItemBuilder(BuildContext context, int index) { + Trip trip = widget.trips.trips[index]; + return ListTile( + title: FutureBuilder( + future: trip.cityName, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return Text("Trip to ${snapshot.data}"); + } else if (snapshot.hasError) { + return Text("Error: ${snapshot.error}"); + } else { + return const Text("Trip to ..."); + } }, - ); - }); + ), + // emoji of the country flag of the trip's country + leading: const Icon(Icons.pin_drop), - return ListView( - children: children, - padding: const EdgeInsets.only(top: 0), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => TripPage(trip: trip) + ) + ); + }, ); } + + // Widget listBuild (BuildContext context, SavedTrips trips) { + // List children; + // List items = trips.trips; + // children = List.generate(items.length, (index) { + // Trip trip = items[index]; + // return ListTile( + // title: FutureBuilder( + // future: trip.cityName, + // builder: (BuildContext context, AsyncSnapshot snapshot) { + // if (snapshot.hasData) { + // return Text("Trip to ${snapshot.data}"); + // } else if (snapshot.hasError) { + // return Text("Error: ${snapshot.error}"); + // } else { + // return const Text("Trip to ..."); + // } + // }, + // ), + // leading: const Icon(Icons.pin_drop), + // onTap: () { + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => TripPage(trip: trip) + // ) + // ); + // }, + // ); + // }); + + // return ListView( + // padding: const EdgeInsets.only(top: 0), + // children: children, + // ); + // } + @override Widget build(BuildContext context) { return ListenableBuilder( listenable: widget.trips, - builder: (BuildContext context, Widget? child) { - return listBuild(context, widget.trips); - } + builder: (BuildContext context, Widget? child) => ListView.builder( + itemCount: widget.trips.trips.length, + itemBuilder: tripListItemBuilder, + ) ); } } diff --git a/frontend/lib/pages/current_trip.dart b/frontend/lib/pages/current_trip.dart index 8f70652..4eb83a5 100644 --- a/frontend/lib/pages/current_trip.dart +++ b/frontend/lib/pages/current_trip.dart @@ -4,10 +4,10 @@ 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_map.dart'; +import 'package:anyway/modules/current_trip_overview.dart'; import 'package:anyway/modules/current_trip_panel.dart'; -final Shader textGradient = APP_GRADIENT.createShader(Rect.fromLTWH(0.0, 0.0, 200.0, 70.0)); +final Shader textGradient = APP_GRADIENT.createShader(const Rect.fromLTWH(0.0, 0.0, 200.0, 70.0)); TextStyle greeterStyle = TextStyle( foreground: Paint()..shader = textGradient, fontWeight: FontWeight.bold, @@ -18,7 +18,7 @@ TextStyle greeterStyle = TextStyle( class TripPage extends StatefulWidget { final Trip trip; - TripPage({ + const TripPage({super.key, required this.trip, }); @@ -39,7 +39,7 @@ class _TripPageState extends State with ScaffoldLayout{ 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), + body: CurrentTripOverview(trip: widget.trip), 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. diff --git a/frontend/lib/pages/new_trip_location.dart b/frontend/lib/pages/new_trip_location.dart index cca895d..476c382 100644 --- a/frontend/lib/pages/new_trip_location.dart +++ b/frontend/lib/pages/new_trip_location.dart @@ -8,7 +8,7 @@ import 'package:anyway/modules/new_trip_map.dart'; class NewTripPage extends StatefulWidget { - const NewTripPage({Key? key}) : super(key: key); + const NewTripPage({super.key}); @override _NewTripPageState createState() => _NewTripPageState(); @@ -30,14 +30,14 @@ class _NewTripPageState extends State with ScaffoldLayout { children: [ NewTripMap(trip), Padding( - padding: EdgeInsets.all(15), + padding: const EdgeInsets.all(15), child: NewTripLocationSearch(trip), ), ], ), floatingActionButton: NewTripOptionsButton(trip: trip), ), - title: Text("New Trip"), + title: const Text("New Trip"), helpTexts: [ "Setting the start location", "To set the starting point, type a city name in the search bar. You can also navigate the map like you're used to and long press anywhere to set a starting point." diff --git a/frontend/lib/pages/new_trip_preferences.dart b/frontend/lib/pages/new_trip_preferences.dart index 4d65c10..b10e361 100644 --- a/frontend/lib/pages/new_trip_preferences.dart +++ b/frontend/lib/pages/new_trip_preferences.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; class NewTripPreferencesPage extends StatefulWidget { final Trip trip; - const NewTripPreferencesPage({required this.trip}); + const NewTripPreferencesPage({super.key, required this.trip}); @override _NewTripPreferencesPageState createState() => _NewTripPreferencesPageState(); @@ -34,14 +34,14 @@ class _NewTripPreferencesPageState extends State with Sc child: Scaffold( body: ListView( children: [ - Center( + const Center( child: Padding( 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), + const Divider(indent: 25, endIndent: 25, height: 50), durationPicker(preferences.maxTime), @@ -78,7 +78,7 @@ class _NewTripPreferencesPageState extends State with Sc title: Text(preferences.maxTime.description), subtitle: CupertinoTimerPicker( mode: CupertinoTimerPickerMode.hm, - initialTimerDuration: Duration(minutes: 90), + initialTimerDuration: const Duration(minutes: 90), minuteInterval: 15, onTimerDurationChanged: (Duration newDuration) { setState(() { diff --git a/frontend/lib/pages/no_trips_page.dart b/frontend/lib/pages/no_trips_page.dart index 163eec7..c009ead 100644 --- a/frontend/lib/pages/no_trips_page.dart +++ b/frontend/lib/pages/no_trips_page.dart @@ -23,7 +23,7 @@ class _NoTripsPageState extends State with ScaffoldLayout { style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center, ), - Padding(padding: EdgeInsets.only(bottom: 10)), + const Padding(padding: EdgeInsets.only(bottom: 10)), Text( "You can start a new trip by clicking the button below", style: Theme.of(context).textTheme.bodyMedium, @@ -50,4 +50,4 @@ class _NoTripsPageState extends State with ScaffoldLayout { ) ) ); -} \ No newline at end of file +} diff --git a/frontend/lib/pages/onboarding.dart b/frontend/lib/pages/onboarding.dart index 847ee3a..c5d3446 100644 --- a/frontend/lib/pages/onboarding.dart +++ b/frontend/lib/pages/onboarding.dart @@ -116,9 +116,9 @@ class _OnboardingPageState extends State { if ((_controller.hasClients ? (_controller.page ?? _controller.initialPage) : 0) != fullCards.length - 1) { return FloatingActionButton.extended( onPressed: () { - controller.nextPage(duration: Duration(milliseconds: 500), curve: Curves.ease); + controller.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.ease); }, - label: Icon(Icons.arrow_forward) + label: const Icon(Icons.arrow_forward) ); } else { // only allow the user to proceed if they have agreed to the terms and conditions diff --git a/frontend/lib/pages/settings.dart b/frontend/lib/pages/settings.dart index 214c327..949edc3 100644 --- a/frontend/lib/pages/settings.dart +++ b/frontend/lib/pages/settings.dart @@ -11,6 +11,8 @@ import 'package:anyway/layouts/scaffold.dart'; bool debugMode = false; class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + @override _SettingsPageState createState() => _SettingsPageState(); } @@ -20,31 +22,31 @@ class _SettingsPageState extends State with ScaffoldLayout { Widget build (BuildContext context) => mainScaffold( context, child: ListView( - padding: EdgeInsets.all(15), + padding: const EdgeInsets.all(15), children: [ // First a round, centered image - Center( + const Center( child: CircleAvatar( radius: 75, child: Icon(Icons.settings, size: 100), ) ), - Center( + const Center( child: Text('Global settings', style: TextStyle(fontSize: 24)) ), - Divider(indent: 25, endIndent: 25, height: 50), + const Divider(indent: 25, endIndent: 25, height: 50), darkMode(), setLocationUsage(), setDebugMode(), - Divider(indent: 25, endIndent: 25, height: 50), + const Divider(indent: 25, endIndent: 25, height: 50), privacyInfo(), ] ), - title: Text('Settings'), + title: const Text('Settings'), helpTexts: [ 'Settings', 'Preferences set in this page are global and will affect the entire application.' @@ -54,9 +56,9 @@ class _SettingsPageState extends State with ScaffoldLayout { Widget setDebugMode() { return Row( children: [ - Text('Debugging: use a custom API URL'), + const Text('Debugging: use a custom API URL'), // white space - Spacer(), + const Spacer(), Switch( value: debugMode, onChanged: (bool? newValue) { @@ -67,7 +69,7 @@ class _SettingsPageState extends State with ScaffoldLayout { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text('Debug mode - use a custom API endpoint'), + title: const Text('Debug mode - use a custom API endpoint'), content: TextField( controller: TextEditingController(text: API_URL_DEBUG), onChanged: (value) { @@ -78,7 +80,7 @@ class _SettingsPageState extends State with ScaffoldLayout { ), actions: [ TextButton( - child: Text('OK'), + child: const Text('OK'), onPressed: () { Navigator.of(context).pop(); }, @@ -98,14 +100,14 @@ class _SettingsPageState extends State with ScaffoldLayout { Widget darkMode() { return Row( children: [ - Text('Dark mode'), - Spacer(), + const Text('Dark mode'), + const Spacer(), Switch( value: Theme.of(context).brightness == Brightness.dark, onChanged: (bool? newValue) { setState(() { rootScaffoldMessengerKey.currentState!.showSnackBar( - SnackBar(content: Text('Dark mode is not implemented yet')) + const SnackBar(content: Text('Dark mode is not implemented yet')) ); // if (newValue!) { // // Dark mode @@ -125,9 +127,9 @@ class _SettingsPageState extends State with ScaffoldLayout { Future preferences = SharedPreferences.getInstance(); return Row( children: [ - Text('Use location services'), + const Text('Use location services'), // white space - Spacer(), + const Spacer(), FutureBuilder( future: preferences, builder: (context, snapshot) { @@ -138,7 +140,7 @@ class _SettingsPageState extends State with ScaffoldLayout { onChanged: setUseLocation, ); } else { - return CircularProgressIndicator(); + return const CircularProgressIndicator(); } } ) @@ -150,12 +152,12 @@ class _SettingsPageState extends State with ScaffoldLayout { await Permission.locationWhenInUse .onDeniedCallback(() { rootScaffoldMessengerKey.currentState!.showSnackBar( - SnackBar(content: Text('Location services are required for this feature')) + const SnackBar(content: Text('Location services are required for this feature')) ); }) .onGrantedCallback(() { rootScaffoldMessengerKey.currentState!.showSnackBar( - SnackBar(content: Text('Location services are now enabled')) + const SnackBar(content: Text('Location services are now enabled')) ); SharedPreferences.getInstance().then( (SharedPreferences prefs) { @@ -167,9 +169,9 @@ class _SettingsPageState extends State with ScaffoldLayout { }) .onPermanentlyDeniedCallback(() { rootScaffoldMessengerKey.currentState!.showSnackBar( - SnackBar(content: Text('Location services are required for this feature')) + const SnackBar(content: Text('Location services are required for this feature')) ); - }) + }) .request(); } @@ -177,12 +179,12 @@ class _SettingsPageState extends State with ScaffoldLayout { return Center( child: Column( children: [ - Text('AnyWay does not collect or store any of the data that is submitted via the app. The location of your trip is not stored. The location feature is only used to show your current location on the map.', textAlign: TextAlign.center), - Padding(padding: EdgeInsets.only(top: 3)), - Text('Our full privacy policy is available under:', textAlign: TextAlign.center), - + const Text('AnyWay does not collect or store any of the data that is submitted via the app. The location of your trip is not stored. The location feature is only used to show your current location on the map.', textAlign: TextAlign.center), + const Padding(padding: EdgeInsets.only(top: 3)), + const Text('Our full privacy policy is available under:', textAlign: TextAlign.center), + TextButton.icon( - icon: Icon(Icons.info), + icon: const Icon(Icons.info), label: Text(PRIVACY_URL), onPressed: () async{ await launchUrl(Uri.parse(PRIVACY_URL)); diff --git a/frontend/lib/structs/preferences.dart b/frontend/lib/structs/preferences.dart index e36f35d..d095593 100644 --- a/frontend/lib/structs/preferences.dart +++ b/frontend/lib/structs/preferences.dart @@ -53,7 +53,7 @@ class UserPreferences { value: 30, minVal: 30, maxVal: 720, - icon: Icon(Icons.timer), + icon: const Icon(Icons.timer), ); @@ -66,4 +66,4 @@ class UserPreferences { "max_time_minute": maxTime.value }; } -} \ No newline at end of file +} diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart index ef09e4e..b025239 100644 --- a/frontend/lib/structs/trip.dart +++ b/frontend/lib/structs/trip.dart @@ -18,7 +18,7 @@ class Trip with ChangeNotifier { String? errorDescription; Future get cityName async { - List? location = landmarks.firstOrNull?.location; + List? location = landmarks.firstOrNull?.location; if (GeocodingPlatform.instance == null) { return '$location'; } else if (location == null) { @@ -48,7 +48,7 @@ class Trip with ChangeNotifier { LinkedList? landmarks // a trip can be created with no landmarks, but the list should be initialized anyway }) : landmarks = landmarks ?? LinkedList(); - + factory Trip.fromJson(Map json) { Trip trip = Trip( @@ -82,11 +82,11 @@ class Trip with ChangeNotifier { // 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 - this.totalTime -= previous.tripTime ?? Duration.zero; + totalTime -= previous.tripTime ?? Duration.zero; previous.tripTime = null; // TODO } - this.totalTime -= landmark.tripTime ?? Duration.zero; + totalTime -= landmark.tripTime ?? Duration.zero; notifyListeners(); } diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart index 5ce3209..9e02e9b 100644 --- a/frontend/lib/utils/fetch_trip.dart +++ b/frontend/lib/utils/fetch_trip.dart @@ -21,7 +21,7 @@ Dio dio = Dio( validateStatus: (status) => status! <= 500, receiveDataWhenStatusError: true, contentType: Headers.jsonContentType, - responseType: ResponseType.json, + responseType: ResponseType.json, ), ); @@ -32,7 +32,7 @@ fetchTrip( ) async { Map data = { "preferences": preferences.toJson(), - "start": trip.landmarks!.first.location, + "start": trip.landmarks.first.location, }; String dataString = jsonEncode(data); log(dataString); @@ -45,7 +45,7 @@ fetchTrip( ); } catch (e) { trip.updateUUID("error"); - + // Format the error message to be more user friendly String errorDescription; if (e is DioException) { @@ -93,7 +93,7 @@ fetchTrip( // Actualy no need to throw an exception, we can just log the error and let the user retry // throw Exception(errorDetail); } else { - + // if the response data is not json, throw an error if (response.data is! Map) { log("${response.data.runtimeType}"); @@ -142,7 +142,7 @@ patchLandmarkImage(Landmark landmark) async { // the image is a google photos link, we should get the image behind the link String? newUrl = await getImageUrlFromGooglePhotos(landmark.imageURL!); // also set the new url if it is null - landmark.imageURL = newUrl; + landmark.imageURL = newUrl; } } diff --git a/frontend/lib/utils/get_first_page.dart b/frontend/lib/utils/get_first_page.dart index bc54b79..913fb3d 100644 --- a/frontend/lib/utils/get_first_page.dart +++ b/frontend/lib/utils/get_first_page.dart @@ -23,28 +23,28 @@ Widget getFirstPage() { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasData) { List trips = snapshot.data!; - if (trips.length > 0) { + if (trips.isNotEmpty) { return TripPage(trip: trips[0]); } else { - return NoTripsPage(); + return const NoTripsPage(); } } else { - return Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator()); } } else { - return Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator()); } }, ); } else { - return OnboardingPage(); + return const OnboardingPage(); } } else { - return OnboardingPage(); + return const OnboardingPage(); } } else { - return OnboardingPage(); + return const OnboardingPage(); } }, ); -} \ No newline at end of file +} diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 7d97f7a..62d371f 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" auto_size_text: dependency: "direct main" description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -420,10 +420,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -969,10 +969,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: diff --git a/frontend/secrets.json b/frontend/secrets.json new file mode 100644 index 0000000..c632eeb --- /dev/null +++ b/frontend/secrets.json @@ -0,0 +1,3 @@ +{ + "ANDROID_GOOGLE_MAPS_API_KEY": "lfhasölkjfslöfjöksjf" +} diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart deleted file mode 100644 index f15d413..0000000 --- a/frontend/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:anyway/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}