diff --git a/src/climbingAPI/app/routers/spot.py b/src/climbingAPI/app/routers/spot.py index 78dd99e..9b83ebf 100644 --- a/src/climbingAPI/app/routers/spot.py +++ b/src/climbingAPI/app/routers/spot.py @@ -150,6 +150,6 @@ async def delete_spot(spot_id: str, user: Auth0User = Security(auth.get_user, sc trips = await db["trip"].find({"user_id": user.id}).to_list(None) if trips: for trip in trips: - update_result = await db["trip"].update_one({"_id": ObjectId(trip['_id'])}, {"$pull": {"spot_ids": ObjectId(spot_id)}}) + update_result = await db["trip"].update_one({"_id": ObjectId(trip['_id']), "user_id": user.id}, {"$pull": {"spot_ids": spot_id}}) # spot_id was removed from trips return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/src/climbingAPI/app/routers/trip.py b/src/climbingAPI/app/routers/trip.py index 9e49744..91940f9 100644 --- a/src/climbingAPI/app/routers/trip.py +++ b/src/climbingAPI/app/routers/trip.py @@ -38,7 +38,7 @@ async def retrieve_trips(user: Auth0User = Security(auth.get_user, scopes=["read @router.get('/{trip_id}', description="Get a trip", response_model=TripModel, dependencies=[Depends(auth.implicit_scheme)]) async def retrieve_trip(trip_id: str, user: Auth0User = Security(auth.get_user, scopes=["read:diary"])): db = await get_db() - if (trip := await db["trips"].find_one({"_id": ObjectId(trip_id), "user_id": user.id})) is not None: + if (trip := await db["trip"].find_one({"_id": ObjectId(trip_id), "user_id": user.id})) is not None: return trip raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Trip {trip_id} not found") diff --git a/src/flutter_app/lib/components/add/add_ascent.dart b/src/flutter_app/lib/components/add/add_ascent.dart index cc2e461..6ebcd71 100644 --- a/src/flutter_app/lib/components/add/add_ascent.dart +++ b/src/flutter_app/lib/components/add/add_ascent.dart @@ -9,11 +9,13 @@ import '../../interfaces/pitch/pitch.dart'; import '../../services/ascent_service.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; +import '../MyTextStyles.dart'; + class AddAscent extends StatefulWidget { - const AddAscent({super.key, required this.pitches, this.onAdd}); + const AddAscent({super.key, required this.pitch, this.onAdd}); final ValueSetter? onAdd; - final List pitches; + final Pitch pitch; @override State createState() => _AddAscentState(); @@ -26,13 +28,11 @@ class _AddAscentState extends State{ final TextEditingController controllerComment = TextEditingController(); final TextEditingController controllerDate = TextEditingController(); - Pitch? pitchValue; AscentStyle? ascentStyleValue; AscentType? ascentTypeValue; @override void initState(){ - pitchValue = widget.pitches[0]; ascentStyleValue = AscentStyle.lead; ascentTypeValue = AscentType.redPoint; controllerDate.text = DateFormat('yyyy-MM-dd').format(DateTime.now()); @@ -52,19 +52,9 @@ class _AddAscentState extends State{ child: Column( mainAxisSize: MainAxisSize.min, children: [ - DropdownButton( - value: pitchValue, - items: widget.pitches.map>((Pitch pitch) { - return DropdownMenuItem( - value: pitch, - child: Text(pitch.name), - ); - }).toList(), - onChanged: (Pitch? pitch) { - setState(() { - pitchValue = pitch!; - }); - } + Text( + widget.pitch.name, + style: MyTextStyles.title, ), TextFormField( controller: controllerComment, @@ -141,11 +131,8 @@ class _AddAscentState extends State{ type: ascentTypeIndex, ); Navigator.popUntil(context, ModalRoute.withName('/')); - final pitchValue = this.pitchValue; - if (pitchValue != null) { - Ascent? createdAscent = await ascentService.createAscent(pitchValue.id, ascent, result); - widget.onAdd?.call(createdAscent!); - } + Ascent? createdAscent = await ascentService.createAscent(widget.pitch.id, ascent, result); + widget.onAdd?.call(createdAscent!); } } }, diff --git a/src/flutter_app/lib/components/add/add_pitch.dart b/src/flutter_app/lib/components/add/add_pitch.dart index 755b092..076ae5c 100644 --- a/src/flutter_app/lib/components/add/add_pitch.dart +++ b/src/flutter_app/lib/components/add/add_pitch.dart @@ -8,10 +8,12 @@ import '../../interfaces/route/route.dart'; import '../../services/pitch_service.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; +import '../MyTextStyles.dart'; + class AddPitch extends StatefulWidget { - const AddPitch({super.key, required this.routes}); + const AddPitch({super.key, required this.route}); - final List routes; + final ClimbingRoute route; @override State createState() => _AddPitchState(); @@ -28,7 +30,6 @@ class _AddPitchState extends State{ final TextEditingController controllerRating = TextEditingController(); double currentSliderValue = 0; - ClimbingRoute? dropdownValue; GradingSystem? gradingSystem; @override @@ -49,19 +50,9 @@ class _AddPitchState extends State{ child: Column( mainAxisSize: MainAxisSize.min, children: [ - DropdownButton( - value: dropdownValue, - items: widget.routes.map>((ClimbingRoute route) { - return DropdownMenuItem( - value: route, - child: Text(route.name), - ); - }).toList(), - onChanged: (ClimbingRoute? route) { - setState(() { - dropdownValue = route!; - }); - } + Text( + widget.route.name, + style: MyTextStyles.title, ), TextFormField( validator: (value) { @@ -148,10 +139,7 @@ class _AddPitchState extends State{ num: int.parse(controllerNum.text), rating: currentSliderValue.toInt(), ); - final dropdownValue = this.dropdownValue; - if (dropdownValue != null) { - Pitch? createdPitch = await pitchService.createPitch(pitch, dropdownValue.id, result); - } + Pitch? createdPitch = await pitchService.createPitch(pitch, widget.route.id, result); Navigator.popUntil(context, ModalRoute.withName('/')); } }, diff --git a/src/flutter_app/lib/components/add/add_route.dart b/src/flutter_app/lib/components/add/add_route.dart index 062f0d0..957318c 100644 --- a/src/flutter_app/lib/components/add/add_route.dart +++ b/src/flutter_app/lib/components/add/add_route.dart @@ -4,17 +4,22 @@ import 'package:flutter/services.dart'; import '../../interfaces/grade.dart'; import '../../interfaces/grading_system.dart'; import '../../interfaces/multi_pitch_route/create_multi_pitch_route.dart'; +import '../../interfaces/multi_pitch_route/multi_pitch_route.dart'; import '../../interfaces/route/route.dart'; import '../../interfaces/single_pitch_route/create_single_pitch_route.dart'; +import '../../interfaces/single_pitch_route/single_pitch_route.dart'; import '../../interfaces/spot/spot.dart'; import '../../services/route_service.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; +import '../MyTextStyles.dart'; + class AddRoute extends StatefulWidget { - const AddRoute({super.key, required this.spots, this.onAdd}); + const AddRoute({super.key, required this.spot, this.onAddMultiPitchRoute, this.onAddSinglePitchRoute}); - final List spots; - final ValueSetter? onAdd; + final Spot spot; + final ValueSetter? onAddMultiPitchRoute; + final ValueSetter? onAddSinglePitchRoute; @override State createState() => _AddRouteState(); @@ -23,7 +28,6 @@ class AddRoute extends StatefulWidget { class _AddRouteState extends State{ final GlobalKey _formKey = GlobalKey(); final RouteService routeService = RouteService(); - final TextEditingController controllerSpotId = TextEditingController(); final TextEditingController controllerComment = TextEditingController(); final TextEditingController controllerLocation = TextEditingController(); final TextEditingController controllerName = TextEditingController(); @@ -32,18 +36,17 @@ class _AddRouteState extends State{ final TextEditingController controllerLength = TextEditingController(); double currentSliderValue = 0; - Spot? dropdownValue; GradingSystem? gradingSystem; bool isMultiPitch = false; @override void initState(){ - dropdownValue = widget.spots[0]; super.initState(); } @override Widget build(BuildContext context) { + Spot spot = widget.spot; return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), @@ -55,6 +58,20 @@ class _AddRouteState extends State{ child: Column( mainAxisSize: MainAxisSize.min, children: [ + Text( + spot.name, + style: MyTextStyles.title, + ), + TextFormField( + validator: (value) { + return value!.isNotEmpty + ? null + : "please add a name"; + }, + controller: controllerName, + decoration: const InputDecoration( + hintText: "name of the route", labelText: "name"), + ), Row(children: [ const Text("Multi-pitch"), Switch( @@ -103,30 +120,6 @@ class _AddRouteState extends State{ hintText: "length of the route", labelText: "length"), ), ), - DropdownButton( - value: dropdownValue, - items: widget.spots.map>((Spot spot) { - return DropdownMenuItem( - value: spot, - child: Text(spot.name), - ); - }).toList(), - onChanged: (Spot? spot) { - setState(() { - dropdownValue = spot!; - }); - } - ), - TextFormField( - validator: (value) { - return value!.isNotEmpty - ? null - : "please add a name"; - }, - controller: controllerName, - decoration: const InputDecoration( - hintText: "name of the route", labelText: "name"), - ), TextFormField( controller: controllerLocation, decoration: const InputDecoration( @@ -171,31 +164,28 @@ class _AddRouteState extends State{ bool result = await InternetConnectionChecker().hasConnection; if (_formKey.currentState!.validate()) { Navigator.popUntil(context, ModalRoute.withName('/')); - final dropdownValue = this.dropdownValue; - if (dropdownValue != null){ - ClimbingRoute? createdRoute; - if (isMultiPitch){ - CreateMultiPitchRoute route = CreateMultiPitchRoute( - name: controllerName.text, - location: controllerLocation.text, - rating: currentSliderValue.toInt(), - comment: controllerComment.text, - ); - createdRoute = await routeService.createMultiPitchRoute(route, dropdownValue.id, result); - } else { - CreateSinglePitchRoute route = CreateSinglePitchRoute( - name: controllerName.text, - location: controllerLocation.text, - rating: currentSliderValue.toInt(), - comment: controllerComment.text, - grade: Grade(grade: controllerGrade.text, system: gradingSystem!), - length: int.parse(controllerLength.text) - ); - createdRoute = await routeService.createSinglePitchRoute(route, dropdownValue.id, result); - } - widget.onAdd?.call(createdRoute!); - setState(() {}); + if (isMultiPitch){ + CreateMultiPitchRoute route = CreateMultiPitchRoute( + name: controllerName.text, + location: controllerLocation.text, + rating: currentSliderValue.toInt(), + comment: controllerComment.text, + ); + MultiPitchRoute? createdRoute = await routeService.createMultiPitchRoute(route, spot.id, result); + widget.onAddMultiPitchRoute?.call(createdRoute!); + } else { + CreateSinglePitchRoute route = CreateSinglePitchRoute( + name: controllerName.text, + location: controllerLocation.text, + rating: currentSliderValue.toInt(), + comment: controllerComment.text, + grade: Grade(grade: controllerGrade.text, system: gradingSystem!), + length: int.parse(controllerLength.text) + ); + SinglePitchRoute? createdRoute = await routeService.createSinglePitchRoute(route, spot.id, result); + widget.onAddSinglePitchRoute?.call(createdRoute!); } + setState(() {}); } }, child: const Text("Save")) diff --git a/src/flutter_app/lib/components/add/add_spot.dart b/src/flutter_app/lib/components/add/add_spot.dart index 0e84488..9ed5a0e 100644 --- a/src/flutter_app/lib/components/add/add_spot.dart +++ b/src/flutter_app/lib/components/add/add_spot.dart @@ -1,3 +1,4 @@ +import 'package:climbing_diary/components/MyTextStyles.dart'; import 'package:climbing_diary/interfaces/trip/update_trip.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -6,18 +7,17 @@ import 'package:latlong2/latlong.dart'; import '../../interfaces/spot/create_spot.dart'; import '../../interfaces/spot/spot.dart'; import '../../interfaces/trip/trip.dart'; +import '../../pages/map_page/navigation_screen_page.dart'; import '../../services/spot_service.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; import '../../services/trip_service.dart'; class AddSpot extends StatefulWidget { - const AddSpot({super.key, required this.trips, required this.coordinates, required this.address, required this.onAdd}); + const AddSpot({super.key, this.trip, required this.onAdd}); - final LatLng coordinates; - final String address; final ValueSetter onAdd; - final List trips; + final Trip? trip; @override State createState() => _AddSpotState(); @@ -37,7 +37,6 @@ class _AddSpotState extends State{ final TextEditingController controllerCar = TextEditingController(); double currentSliderValue = 0; - Trip? dropdownValue; @override void initState(){ @@ -47,12 +46,17 @@ class _AddSpotState extends State{ @override Widget build(BuildContext context) { - controllerAddress.text = widget.address; - controllerLat.text = widget.coordinates.latitude.toString(); - controllerLong.text = widget.coordinates.longitude.toString(); + // controllerLat.text = widget.coordinates.latitude.toString(); + // controllerLong.text = widget.coordinates.longitude.toString(); List elements = []; + if (widget.trip != null){ + elements.add(Text( + widget.trip!.name, + style: MyTextStyles.title, + )); + } elements.add( TextFormField( validator: (value) { @@ -65,20 +69,24 @@ class _AddSpotState extends State{ hintText: "name", labelText: "name"), ) ); - elements.add(DropdownButton( - isExpanded: true, - value: dropdownValue, - items: widget.trips.map>((Trip trip) { - return DropdownMenuItem( - value: trip, - child: Text(trip.name), - ); - }).toList(), - onChanged: (Trip? trip) { - setState(() { - dropdownValue = trip!; - }); - } + elements.add(TextButton.icon( + icon: const Icon(Icons.place, size: 30.0), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NavigationScreenPage( + onAdd: (List results) { + controllerAddress.text = results[0]; + controllerLat.text = results[1]; + controllerLong.text = results[2]; + setState(() {}); + } + ), + ) + ); + }, + label: const Text("select location"), )); elements.add(TextFormField( controller: controllerAddress, @@ -132,6 +140,7 @@ class _AddSpotState extends State{ } return null; }, + keyboardType: TextInputType.number, controller: controllerBus, decoration: const InputDecoration( hintText: "in minutes", @@ -149,6 +158,7 @@ class _AddSpotState extends State{ return null; }, controller: controllerCar, + keyboardType: TextInputType.number, decoration: const InputDecoration( hintText: "in minutes", labelText: "Distance to parking" @@ -161,43 +171,44 @@ class _AddSpotState extends State{ ), title: const Text('Add a new spot'), content: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: elements, - ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: elements, ), + ), ), actions: [ TextButton( - onPressed: () async { - bool result = await InternetConnectionChecker().hasConnection; - if (_formKey.currentState!.validate()) { - var valDistanceParking = int.tryParse(controllerCar.text); - var valDistancePublicTransport = int.tryParse(controllerBus.text); - CreateSpot spot = CreateSpot( - comment: controllerComment.text, - coordinates: [double.parse(controllerLat.text), double.parse(controllerLong.text)], - distanceParking: (valDistanceParking != null) ? valDistanceParking : 0, - distancePublicTransport: (valDistancePublicTransport != null) ? valDistancePublicTransport : 0, - location: controllerAddress.text, - name: controllerTitle.text, - rating: currentSliderValue.toInt(), - ); - Navigator.popUntil(context, ModalRoute.withName('/')); - Spot? createdSpot = await spotService.createSpot(spot, result); - if (createdSpot != null) { - if (dropdownValue != null) { - UpdateTrip editTrip = dropdownValue!.toUpdateTrip(); - editTrip.spotIds?.add(createdSpot.id); - Trip? editedTrip = await tripService.editTrip(editTrip); + onPressed: () async { + bool result = await InternetConnectionChecker().hasConnection; + if (_formKey.currentState!.validate()) { + var valDistanceParking = int.tryParse(controllerCar.text); + var valDistancePublicTransport = int.tryParse(controllerBus.text); + CreateSpot spot = CreateSpot( + comment: controllerComment.text, + coordinates: [double.parse(controllerLat.text), double.parse(controllerLong.text)], + distanceParking: (valDistanceParking != null) ? valDistanceParking : 0, + distancePublicTransport: (valDistancePublicTransport != null) ? valDistancePublicTransport : 0, + location: controllerAddress.text, + name: controllerTitle.text, + rating: currentSliderValue.toInt(), + ); + Navigator.popUntil(context, ModalRoute.withName('/')); + Spot? createdSpot = await spotService.createSpot(spot, result); + if (createdSpot != null) { + if (widget.trip != null) { + Trip trip = widget.trip!; + UpdateTrip editTrip = trip.toUpdateTrip(); + editTrip.spotIds?.add(createdSpot.id); + Trip? editedTrip = await tripService.editTrip(editTrip); + } + widget.onAdd.call(createdSpot); } - widget.onAdd.call(createdSpot); } - } - }, - child: const Icon(Icons.save)) + }, + child: const Icon(Icons.save)) ], ); } diff --git a/src/flutter_app/lib/components/add/add_trip.dart b/src/flutter_app/lib/components/add/add_trip.dart index 8ac379a..d8e7282 100644 --- a/src/flutter_app/lib/components/add/add_trip.dart +++ b/src/flutter_app/lib/components/add/add_trip.dart @@ -7,7 +7,8 @@ import '../../services/trip_service.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; class AddTrip extends StatefulWidget { - const AddTrip({super.key}); + const AddTrip({super.key, required this.onAdd}); + final ValueSetter onAdd; @override State createState() => _AddTripState(); @@ -127,6 +128,9 @@ class _AddTripState extends State{ ); Navigator.popUntil(context, ModalRoute.withName('/')); Trip? createdTrip = await tripService.createTrip(trip, result); + if (createdTrip != null) { + widget.onAdd.call(createdTrip); + } } }, child: const Text("Save")) diff --git a/src/flutter_app/lib/components/detail/multi_pitch_route_details.dart b/src/flutter_app/lib/components/detail/multi_pitch_route_details.dart index 247026c..8be172c 100644 --- a/src/flutter_app/lib/components/detail/multi_pitch_route_details.dart +++ b/src/flutter_app/lib/components/detail/multi_pitch_route_details.dart @@ -3,19 +3,16 @@ import 'package:image_picker/image_picker.dart'; import 'package:skeletons/skeletons.dart'; import '../../interfaces/multi_pitch_route/multi_pitch_route.dart'; -import '../../interfaces/route/route.dart'; import '../../interfaces/spot/spot.dart'; import '../../interfaces/trip/trip.dart'; +import '../../pages/diary_page/timeline/pitch_timeline.dart'; import '../../services/media_service.dart'; import '../../services/pitch_service.dart'; import '../../services/route_service.dart'; import '../MyButtonStyles.dart'; import '../add/add_pitch.dart'; -import '../diary_page/timeline/pitch_timeline.dart'; import '../edit/edit_multi_pitch_route.dart'; -import '../edit/edit_route.dart'; import '../info/multi_pitch_route_info.dart'; -import '../select/select_pitch.dart'; class MultiPitchRouteDetails extends StatefulWidget { const MultiPitchRouteDetails({super.key, this.trip, required this.spot, required this.route, required this.onDelete, required this.onUpdate, required this.spotId }); @@ -53,7 +50,7 @@ class _MultiPitchRouteDetailsState extends State{ var mediaId = await mediaService.uploadMedia(img); MultiPitchRoute route = widget.route; route.mediaIds.add(mediaId); - routeService.editRoute(route.toUpdateMultiPitchRoute()); + routeService.editMultiPitchRoute(route.toUpdateMultiPitchRoute()); } setState(() { @@ -276,22 +273,7 @@ class _MultiPitchRouteDetailsState extends State{ Navigator.push( context, MaterialPageRoute( - builder: (context) => AddPitch(routes: [widget.route],), - ) - ); - }, - style: MyButtonStyles.rounded - ), - ); - elements.add( - ElevatedButton.icon( - icon: const Icon(Icons.add, size: 30.0, color: Colors.pink), - label: const Text('Add existing pitch'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SelectPitch(route: widget.route), + builder: (context) => AddPitch(route: widget.route,), ) ); }, @@ -306,7 +288,7 @@ class _MultiPitchRouteDetailsState extends State{ IconButton( onPressed: () { Navigator.pop(context); - routeService.deleteRoute(route, widget.spotId); + routeService.deleteMultiPitchRoute(route, widget.spotId); widget.onDelete.call(route); }, icon: const Icon(Icons.delete), diff --git a/src/flutter_app/lib/components/detail/pitch_details.dart b/src/flutter_app/lib/components/detail/pitch_details.dart index e41c2e5..2f7aed4 100644 --- a/src/flutter_app/lib/components/detail/pitch_details.dart +++ b/src/flutter_app/lib/components/detail/pitch_details.dart @@ -6,14 +6,13 @@ import 'package:skeletons/skeletons.dart'; import '../../interfaces/pitch/pitch.dart'; import '../../interfaces/spot/spot.dart'; import '../../interfaces/trip/trip.dart'; +import '../../pages/diary_page/timeline/ascent_timeline.dart'; import '../../services/media_service.dart'; import '../../services/pitch_service.dart'; import '../MyButtonStyles.dart'; import '../add/add_ascent.dart'; -import '../diary_page/timeline/ascent_timeline.dart'; import '../edit/edit_pitch.dart'; import '../info/pitch_info.dart'; -import '../select/select_ascent.dart'; class PitchDetails extends StatefulWidget { const PitchDetails({super.key, this.trip, required this.spot, required this.route, required this.pitch, required this.onDelete, required this.onUpdate }); @@ -255,7 +254,7 @@ class _PitchDetailsState extends State{ context, MaterialPageRoute( builder: (context) => AddAscent( - pitches: [widget.pitch], + pitch: widget.pitch, onAdd: (ascent) { widget.pitch.ascentIds.add(ascent.id); setState(() {}); @@ -267,21 +266,6 @@ class _PitchDetailsState extends State{ style: MyButtonStyles.rounded ), ); - elements.add( - ElevatedButton.icon( - icon: const Icon(Icons.add, size: 30.0, color: Colors.pink), - label: const Text('Add existing ascent'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SelectAscent(pitch: widget.pitch), - ) - ); - }, - style: MyButtonStyles.rounded - ), - ); // delete, edit, close elements.add( Row( diff --git a/src/flutter_app/lib/components/detail/single_pitch_route_details.dart b/src/flutter_app/lib/components/detail/single_pitch_route_details.dart index f160550..56afb16 100644 --- a/src/flutter_app/lib/components/detail/single_pitch_route_details.dart +++ b/src/flutter_app/lib/components/detail/single_pitch_route_details.dart @@ -1,5 +1,4 @@ import 'package:climbing_diary/components/add/add_ascent_to_single_pitch_route.dart'; -import 'package:climbing_diary/components/select/select_ascent_of_single_pitch_route.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:skeletons/skeletons.dart'; @@ -7,17 +6,13 @@ import 'package:skeletons/skeletons.dart'; import '../../interfaces/single_pitch_route/single_pitch_route.dart'; import '../../interfaces/spot/spot.dart'; import '../../interfaces/trip/trip.dart'; +import '../../pages/diary_page/timeline/ascent_timeline.dart'; import '../../services/media_service.dart'; import '../../services/pitch_service.dart'; import '../../services/route_service.dart'; import '../MyButtonStyles.dart'; -import '../add/add_ascent.dart'; -import '../add/add_pitch.dart'; -import '../diary_page/timeline/ascent_timeline.dart'; import '../edit/edit_single_pitch_route.dart'; import '../info/single_pitch_route_info.dart'; -import '../select/select_ascent.dart'; -import '../select/select_pitch.dart'; class SinglePitchRouteDetails extends StatefulWidget { const SinglePitchRouteDetails({super.key, this.trip, required this.spot, required this.route, required this.onDelete, required this.onUpdate, required this.spotId }); @@ -55,7 +50,7 @@ class _SinglePitchRouteDetailsState extends State{ var mediaId = await mediaService.uploadMedia(img); SinglePitchRoute route = widget.route; route.mediaIds.add(mediaId); - routeService.editRoute(route.toUpdateSinglePitchRoute()); + routeService.editSinglePitchRoute(route.toUpdateSinglePitchRoute()); } setState(() { @@ -131,7 +126,6 @@ class _SinglePitchRouteDetailsState extends State{ // general info elements.addAll([ SinglePitchRouteInfo( - spot: widget.spot, route: route ), Text( @@ -285,21 +279,6 @@ class _SinglePitchRouteDetailsState extends State{ style: MyButtonStyles.rounded ), ); - elements.add( - ElevatedButton.icon( - icon: const Icon(Icons.add, size: 30.0, color: Colors.pink), - label: const Text('Add existing ascent'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SelectAscentOfSinglePitchRoute(singlePitchRoute: widget.route), - ) - ); - }, - style: MyButtonStyles.rounded - ), - ); elements.add( Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -308,7 +287,7 @@ class _SinglePitchRouteDetailsState extends State{ IconButton( onPressed: () { Navigator.pop(context); - routeService.deleteRoute(route, widget.spotId); + routeService.deleteSinglePitchRoute(route, widget.spotId); widget.onDelete.call(route); }, icon: const Icon(Icons.delete), diff --git a/src/flutter_app/lib/components/detail/spot_details.dart b/src/flutter_app/lib/components/detail/spot_details.dart index 8de43e7..c10c4fe 100644 --- a/src/flutter_app/lib/components/detail/spot_details.dart +++ b/src/flutter_app/lib/components/detail/spot_details.dart @@ -5,13 +5,12 @@ import 'package:skeletons/skeletons.dart'; import '../../interfaces/spot/spot.dart'; import '../../interfaces/trip/trip.dart'; +import '../../pages/diary_page/timeline/route_timeline.dart'; import '../../services/media_service.dart'; import '../../services/spot_service.dart'; import '../MyButtonStyles.dart'; import '../add/add_route.dart'; -import '../diary_page/timeline/route_timeline.dart'; import '../edit/edit_spot.dart'; -import '../select/select_route.dart'; class SpotDetails extends StatefulWidget { const SpotDetails({super.key, this.trip, required this.spot, required this.onDelete, required this.onUpdate }); @@ -296,11 +295,15 @@ class _SpotDetailsState extends State{ context, MaterialPageRoute( builder: (context) => AddRoute( - spots: [widget.spot], - onAdd: (route) { + spot: widget.spot, + onAddMultiPitchRoute: (route) { widget.spot.multiPitchRouteIds.add(route.id); setState(() {}); }, + onAddSinglePitchRoute: (route) { + widget.spot.singlePitchRouteIds.add(route.id); + setState(() {}); + }, ), ) ); @@ -308,21 +311,6 @@ class _SpotDetailsState extends State{ style: MyButtonStyles.rounded ), ); - elements.add( - ElevatedButton.icon( - icon: const Icon(Icons.add, size: 30.0, color: Colors.pink), - label: const Text('Add existing route'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SelectRoute(spot: widget.spot), - ) - ); - }, - style: MyButtonStyles.rounded - ), - ); // delete, edit, close elements.add( Row( diff --git a/src/flutter_app/lib/components/detail/trip_details.dart b/src/flutter_app/lib/components/detail/trip_details.dart index 6953e04..30e177a 100644 --- a/src/flutter_app/lib/components/detail/trip_details.dart +++ b/src/flutter_app/lib/components/detail/trip_details.dart @@ -4,19 +4,20 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:skeletons/skeletons.dart'; +import '../../interfaces/spot/spot.dart'; import '../../interfaces/trip/trip.dart'; -import '../../pages/navigation_screen_page.dart'; +import '../../pages/diary_page/timeline/spot_timeline.dart'; import '../../services/media_service.dart'; import '../../services/trip_service.dart'; -import '../diary_page/timeline/spot_timeline.dart'; +import '../add/add_spot.dart'; import '../edit/edit_trip.dart'; class TripDetails extends StatefulWidget { - const TripDetails({super.key, required this.trip, required this.onDelete, required this.onUpdate }); + const TripDetails({super.key, required this.trip, required this.onTripDelete, required this.onTripUpdate, required this.onSpotAdd }); final Trip trip; - final ValueSetter onDelete; - final ValueSetter onUpdate; + final ValueSetter onTripDelete, onTripUpdate; + final ValueSetter onSpotAdd; @override State createState() => _TripDetailsState(); @@ -100,7 +101,7 @@ class _TripDetailsState extends State{ showDialog( context: context, builder: (BuildContext context) { - return EditTrip(trip: widget.trip, onUpdate: widget.onUpdate); + return EditTrip(trip: widget.trip, onUpdate: widget.onTripUpdate); }); } @@ -273,10 +274,12 @@ class _TripDetailsState extends State{ Navigator.push( context, MaterialPageRoute( - builder: (context) => NavigationScreenPage( + builder: (context) => AddSpot( onAdd: (spot) { - trip.spotIds.add(spot.id); + widget.onSpotAdd.call(spot); + setState(() {}); }, + trip: trip, ), ) ); @@ -292,7 +295,10 @@ class _TripDetailsState extends State{ Navigator.push( context, MaterialPageRoute( - builder: (context) => SelectSpot(trip: widget.trip), + builder: (context) => SelectSpot( + trip: widget.trip, + onAdd: widget.onSpotAdd + ), ) ); }, @@ -309,7 +315,7 @@ class _TripDetailsState extends State{ onPressed: () { Navigator.pop(context); tripService.deleteTrip(trip); - widget.onDelete.call(trip); + widget.onTripDelete.call(trip); }, icon: const Icon(Icons.delete), ), @@ -326,9 +332,12 @@ class _TripDetailsState extends State{ ); // spots if (trip.spotIds.isNotEmpty){ - elements.add( - SpotTimeline(trip: trip, spotIds: trip.spotIds, startDate: DateTime.parse(trip.startDate), endDate: DateTime.parse(trip.endDate)) - ); + elements.add(SpotTimeline( + trip: trip, + spotIds: trip.spotIds, + startDate: DateTime.parse(trip.startDate), + endDate: DateTime.parse(trip.endDate) + )); } return Stack( diff --git a/src/flutter_app/lib/components/edit/edit_multi_pitch_route.dart b/src/flutter_app/lib/components/edit/edit_multi_pitch_route.dart index 04a5237..585cc5b 100644 --- a/src/flutter_app/lib/components/edit/edit_multi_pitch_route.dart +++ b/src/flutter_app/lib/components/edit/edit_multi_pitch_route.dart @@ -1,8 +1,8 @@ +import 'package:climbing_diary/interfaces/multi_pitch_route/update_multi_pitch_route.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../../interfaces/multi_pitch_route/multi_pitch_route.dart'; -import '../../interfaces/multi_pitch_route/update_multi_pitch_route.dart'; import '../../interfaces/route/route.dart'; import '../../interfaces/route/update_route.dart'; import '../../services/route_service.dart'; @@ -110,8 +110,7 @@ class _EditMultiPitchRouteState extends State{ name: controllerName.text, rating: currentSliderValue.toInt(), ); - Navigator.of(context).pop(); - Navigator.of(context).pop(); + Navigator.popUntil(context, ModalRoute.withName('/')); MultiPitchRoute? updatedRoute = await routeService.editMultiPitchRoute(route); if (updatedRoute != null) { widget.onUpdate.call(updatedRoute); diff --git a/src/flutter_app/lib/components/edit/edit_route.dart b/src/flutter_app/lib/components/edit/edit_route.dart deleted file mode 100644 index a362350..0000000 --- a/src/flutter_app/lib/components/edit/edit_route.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -import '../../interfaces/route/route.dart'; -import '../../interfaces/route/update_route.dart'; -import '../../services/route_service.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; - -class EditRoute extends StatefulWidget { - const EditRoute({super.key, required this.route, required this.onUpdate}); - - final ClimbingRoute route; - final ValueSetter onUpdate; - - @override - State createState() => _EditRouteState(); -} - -class _EditRouteState extends State{ - final GlobalKey _formKey = GlobalKey(); - final RouteService routeService = RouteService(); - final TextEditingController controllerName = TextEditingController(); - final TextEditingController controllerDate = TextEditingController(); - final TextEditingController controllerLocation = TextEditingController(); - final TextEditingController controllerComment = TextEditingController(); - - int currentSliderValue = 0; - - @override - void initState(){ - controllerName.text = widget.route.name; - controllerDate.text = DateFormat('yyyy-MM-dd').format(DateTime.now()); - controllerLocation.text = widget.route.location; - currentSliderValue = widget.route.rating; - controllerComment.text = widget.route.comment; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - title: const Text('Edit this route'), - content: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - validator: (value) { - return value!.isNotEmpty - ? null - : "Please add a title"; - }, - controller: controllerName, - decoration: const InputDecoration( - hintText: "name", labelText: "name"), - ), - TextFormField( - controller: controllerLocation, - decoration: const InputDecoration(labelText: "location"), - ), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - "Rating", - style: TextStyle( - color: Colors.black.withOpacity(0.6), - fontSize: 16 - ) - ), - ), - ), - Slider( - value: currentSliderValue.toDouble(), - max: 5, - divisions: 5, - label: currentSliderValue.round().toString(), - onChanged: (value) { - setState(() { - currentSliderValue = value.toInt(); - }); - }, - ), - TextFormField( - controller: controllerComment, - decoration: const InputDecoration( - hintText: "Description", labelText: "Description"), - ), - ] - ), - ), - ), - actions: [ - IconButton( - onPressed: () async { - bool result = await InternetConnectionChecker().hasConnection; - if (_formKey.currentState!.validate()) { - UpdateClimbingRoute route = UpdateClimbingRoute( - id: widget.route.id, - comment: controllerComment.text, - location: controllerLocation.text, - name: controllerName.text, - rating: currentSliderValue.toInt(), - ); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - ClimbingRoute? updatedRoute = await routeService.editRoute(route); - if (updatedRoute != null) { - widget.onUpdate.call(updatedRoute); - } - } - }, - icon: const Icon(Icons.save) - ), - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close), - ), - ], - ); - } -} \ No newline at end of file diff --git a/src/flutter_app/lib/components/edit/edit_single_pitch_route.dart b/src/flutter_app/lib/components/edit/edit_single_pitch_route.dart index 43e915c..1c5a044 100644 --- a/src/flutter_app/lib/components/edit/edit_single_pitch_route.dart +++ b/src/flutter_app/lib/components/edit/edit_single_pitch_route.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; -import '../../interfaces/route/route.dart'; -import '../../interfaces/route/update_route.dart'; +import '../../interfaces/grading_system.dart'; import '../../interfaces/single_pitch_route/single_pitch_route.dart'; import '../../interfaces/single_pitch_route/update_single_pitch_route.dart'; import '../../services/route_service.dart'; @@ -21,20 +21,26 @@ class EditSinglePitchRoute extends StatefulWidget { class _EditSinglePitchRouteState extends State{ final GlobalKey _formKey = GlobalKey(); final RouteService routeService = RouteService(); - final TextEditingController controllerName = TextEditingController(); - final TextEditingController controllerDate = TextEditingController(); - final TextEditingController controllerLocation = TextEditingController(); final TextEditingController controllerComment = TextEditingController(); + final TextEditingController controllerGrade = TextEditingController(); + final TextEditingController controllerLength = TextEditingController(); + final TextEditingController controllerLocation = TextEditingController(); + final TextEditingController controllerName = TextEditingController(); + final TextEditingController controllerRating = TextEditingController(); int currentSliderValue = 0; + GradingSystem? gradingSystem; @override void initState(){ - controllerName.text = widget.route.name; - controllerDate.text = DateFormat('yyyy-MM-dd').format(DateTime.now()); + controllerComment.text = widget.route.comment; + controllerGrade.text = widget.route.grade.grade; + gradingSystem = widget.route.grade.system; + controllerLength.text = widget.route.length.toString(); controllerLocation.text = widget.route.location; + controllerName.text = widget.route.name; currentSliderValue = widget.route.rating; - controllerComment.text = widget.route.comment; + super.initState(); } @@ -55,26 +61,59 @@ class _EditSinglePitchRouteState extends State{ validator: (value) { return value!.isNotEmpty ? null - : "Please add a title"; + : "please add a name"; }, controller: controllerName, decoration: const InputDecoration( - hintText: "name", labelText: "name"), + hintText: "name of the route", labelText: "name"), + ), + TextFormField( + controller: controllerGrade, + decoration: const InputDecoration( + hintText: "grade of the route", labelText: "grade"), + ), + DropdownButton( + value: gradingSystem, + items: GradingSystem.values.map>((GradingSystem value) { + return DropdownMenuItem( + value: value, + child: Text(value.toShortString()) + ); + }).toList(), + onChanged: (GradingSystem? value) { + setState(() { + gradingSystem = value!; + }); + }, + ), + TextFormField( + controller: controllerLength, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ], + decoration: const InputDecoration( + hintText: "length of the route", labelText: "length"), ), TextFormField( controller: controllerLocation, decoration: const InputDecoration(labelText: "location"), ), + TextFormField( + controller: controllerComment, + decoration: const InputDecoration( + hintText: "comment", labelText: "comment"), + ), Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( - "Rating", - style: TextStyle( - color: Colors.black.withOpacity(0.6), - fontSize: 16 - ) + "Rating", + style: TextStyle( + color: Colors.black.withOpacity(0.6), + fontSize: 16 + ) ), ), ), @@ -89,11 +128,6 @@ class _EditSinglePitchRouteState extends State{ }); }, ), - TextFormField( - controller: controllerComment, - decoration: const InputDecoration( - hintText: "Description", labelText: "Description"), - ), ] ), ), @@ -110,8 +144,7 @@ class _EditSinglePitchRouteState extends State{ name: controllerName.text, rating: currentSliderValue.toInt(), ); - Navigator.of(context).pop(); - Navigator.of(context).pop(); + Navigator.popUntil(context, ModalRoute.withName('/')); SinglePitchRoute? updatedRoute = await routeService.editSinglePitchRoute(route); if (updatedRoute != null) { widget.onUpdate.call(updatedRoute); diff --git a/src/flutter_app/lib/components/edit/edit_spot.dart b/src/flutter_app/lib/components/edit/edit_spot.dart index 0bfdb95..2158dfe 100644 --- a/src/flutter_app/lib/components/edit/edit_spot.dart +++ b/src/flutter_app/lib/components/edit/edit_spot.dart @@ -47,7 +47,7 @@ class _EditSpotState extends State{ shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), - title: const Text('Edit this spot'), + title: const Text('edit this spot'), content: SingleChildScrollView( child: Form( key: _formKey, @@ -58,30 +58,30 @@ class _EditSpotState extends State{ validator: (value) { return value!.isNotEmpty ? null - : "Please add a title"; + : "please add a title"; }, controller: controllerTitle, decoration: const InputDecoration( - hintText: "Name of the spot", labelText: "Title"), + hintText: "name of the spot", labelText: "name"), ), TextFormField( controller: controllerAddress, - decoration: const InputDecoration(labelText: "Address"), + decoration: const InputDecoration(labelText: "address"), ), TextFormField( controller: controllerLat, - decoration: const InputDecoration(labelText: "Latitude"), + decoration: const InputDecoration(labelText: "latitude"), ), TextFormField( controller: controllerLong, - decoration: const InputDecoration(labelText: "Longitude"), + decoration: const InputDecoration(labelText: "longitude"), ), Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( - "Rating", + "rating", style: TextStyle( color: Colors.black.withOpacity(0.6), fontSize: 16 @@ -103,14 +103,14 @@ class _EditSpotState extends State{ TextFormField( controller: controllerDescription, decoration: const InputDecoration( - hintText: "Description", labelText: "Description"), + hintText: "comment", labelText: "comment"), ), TextFormField( validator: (value) { if (value != null && value != ""){ var i = int.tryParse(value); if (i == null) { - return "Must be a number"; + return "must be a number"; } } return null; @@ -118,14 +118,14 @@ class _EditSpotState extends State{ controller: controllerBus, decoration: const InputDecoration( hintText: "in minutes", - labelText: "Distance to public transport station"), + labelText: "distance to public transport station"), ), TextFormField( validator: (value) { if (value != null && value != ""){ var i = int.tryParse(value); if (i == null) { - return "Must be a number"; + return "must be a number"; } } return null; @@ -133,7 +133,7 @@ class _EditSpotState extends State{ controller: controllerCar, decoration: const InputDecoration( hintText: "in minutes", - labelText: "Distance to parking"), + labelText: "distance to parking"), ) ], ), @@ -156,8 +156,7 @@ class _EditSpotState extends State{ distancePublicTransport: (valDistancePublicTransport != null) ? valDistancePublicTransport : 0, comment: controllerDescription.text, ); - Navigator.of(context).pop(); - Navigator.of(context).pop(); + Navigator.popUntil(context, ModalRoute.withName('/')); Spot? updatedSpot = await spotService.editSpot(spot); if (updatedSpot != null) { widget.onUpdate.call(updatedSpot); diff --git a/src/flutter_app/lib/components/edit/edit_trip.dart b/src/flutter_app/lib/components/edit/edit_trip.dart index 372af3a..d95c655 100644 --- a/src/flutter_app/lib/components/edit/edit_trip.dart +++ b/src/flutter_app/lib/components/edit/edit_trip.dart @@ -129,8 +129,7 @@ class _EditTripState extends State{ rating: currentSliderValue.toInt(), comment: controllerComment.text, ); - Navigator.of(context).pop(); - Navigator.of(context).pop(); + Navigator.popUntil(context, ModalRoute.withName('/')); Trip? updatedTrip = await tripService.editTrip(trip); if (updatedTrip != null) { widget.onUpdate.call(updatedTrip); diff --git a/src/flutter_app/lib/components/info/ascent_info.dart b/src/flutter_app/lib/components/info/ascent_info.dart index c0e5338..28ad6c3 100644 --- a/src/flutter_app/lib/components/info/ascent_info.dart +++ b/src/flutter_app/lib/components/info/ascent_info.dart @@ -18,7 +18,7 @@ class AscentInfo extends StatelessWidget { // date, style and type listInfo.add(Text( - "${ascent.date} ${AscentStyle.values[ascent.style].toEmoji()} ${AscentType.values[ascent.type].toEmoji()}", + "${ascent.date} ${AscentStyle.values[ascent.style].toEmoji()}${AscentType.values[ascent.type].toEmoji()}", style: MyTextStyles.title, )); diff --git a/src/flutter_app/lib/components/info/pitch_info.dart b/src/flutter_app/lib/components/info/pitch_info.dart index 7fa7c81..6a65151 100644 --- a/src/flutter_app/lib/components/info/pitch_info.dart +++ b/src/flutter_app/lib/components/info/pitch_info.dart @@ -51,7 +51,7 @@ class _PitchInfoState extends State{ List listInfo = []; String title = widget.pitch.name; if (displayedAscent != null) { - title += " ${AscentStyle.values[displayedAscent.style].toEmoji()} ${AscentType.values[displayedAscent.type].toEmoji()}"; + title += " ${AscentStyle.values[displayedAscent.style].toEmoji()}${AscentType.values[displayedAscent.type].toEmoji()}"; } // name diff --git a/src/flutter_app/lib/components/info/route_info.dart b/src/flutter_app/lib/components/info/route_info.dart index b7d53d1..f870827 100644 --- a/src/flutter_app/lib/components/info/route_info.dart +++ b/src/flutter_app/lib/components/info/route_info.dart @@ -37,7 +37,7 @@ class _RouteInfoState extends State{ List listInfo = []; String title = widget.route.name; if (ascent != null) { - title += " ${AscentStyle.values[ascent.style].toEmoji()} ${AscentType.values[ascent.type].toEmoji()}"; + title += " ${AscentStyle.values[ascent.style].toEmoji()}${AscentType.values[ascent.type].toEmoji()}"; } listInfo.add(Text( @@ -45,12 +45,21 @@ class _RouteInfoState extends State{ style: MyTextStyles.title, )); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: listInfo, + ); + } else { + List listInfo = []; + listInfo.add(Text( + widget.route.name, + style: MyTextStyles.title, + )); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: listInfo, ); } - return const CircularProgressIndicator(); } ); } diff --git a/src/flutter_app/lib/components/info/single_pitch_route_info.dart b/src/flutter_app/lib/components/info/single_pitch_route_info.dart index dfbc984..ba55613 100644 --- a/src/flutter_app/lib/components/info/single_pitch_route_info.dart +++ b/src/flutter_app/lib/components/info/single_pitch_route_info.dart @@ -1,4 +1,3 @@ -import 'package:climbing_diary/components/detail/pitch_details.dart'; import 'package:climbing_diary/interfaces/grading_system.dart'; import 'package:climbing_diary/services/ascent_service.dart'; import 'package:flutter/material.dart'; @@ -7,16 +6,14 @@ import '../../interfaces/ascent/ascent.dart'; import '../../interfaces/ascent/ascent_style.dart'; import '../../interfaces/ascent/ascent_type.dart'; import '../../interfaces/single_pitch_route/single_pitch_route.dart'; -import '../../interfaces/spot/spot.dart'; import '../../interfaces/trip/trip.dart'; import '../MyTextStyles.dart'; class SinglePitchRouteInfo extends StatefulWidget { const SinglePitchRouteInfo( - {super.key, this.trip, required this.spot, required this.route}); + {super.key, this.trip, required this.route}); final Trip? trip; - final Spot spot; final SinglePitchRoute route; @override @@ -57,7 +54,7 @@ class _SinglePitchRouteInfoState extends State{ List listInfo = []; String title = widget.route.name; if (displayedAscent != null) { - title += " ${AscentStyle.values[displayedAscent.style].toEmoji()} ${AscentType.values[displayedAscent.type].toEmoji()}"; + title += " ${AscentStyle.values[displayedAscent.style].toEmoji()}${AscentType.values[displayedAscent.type].toEmoji()}"; } listInfo.add(Text( diff --git a/src/flutter_app/lib/components/info/trip_info.dart b/src/flutter_app/lib/components/info/trip_info.dart index ec8803c..bc46cb3 100644 --- a/src/flutter_app/lib/components/info/trip_info.dart +++ b/src/flutter_app/lib/components/info/trip_info.dart @@ -25,10 +25,12 @@ class TripInfo extends StatelessWidget { style: MyTextStyles.description, )); - listInfo.add(Text( - trip.comment, - style: MyTextStyles.description, - )); + if (trip.comment != ""){ + listInfo.add(Text( + trip.comment, + style: MyTextStyles.description, + )); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/src/flutter_app/lib/components/list_page/spot_list.dart b/src/flutter_app/lib/components/list_page/spot_list.dart deleted file mode 100644 index 3ec05e2..0000000 --- a/src/flutter_app/lib/components/list_page/spot_list.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; - -class SpotList extends StatefulWidget { - const SpotList({super.key}); - - @override - State createState() => SpotListState(); -} - -class SpotListState extends State { - - @override - void initState(){ - super.initState(); - } - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Text("spots"), - ); - } -} \ No newline at end of file diff --git a/src/flutter_app/lib/components/map_page/navigation_screen_page.dart b/src/flutter_app/lib/components/map_page/navigation_screen_page.dart deleted file mode 100644 index a4ffe4c..0000000 --- a/src/flutter_app/lib/components/map_page/navigation_screen_page.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:latlong2/latlong.dart'; -import 'package:open_street_map_search_and_pick/open_street_map_search_and_pick.dart'; - -import '../../interfaces/spot/spot.dart'; -import '../../interfaces/trip/trip.dart'; -import '../../services/location_service.dart'; -import '../../services/trip_service.dart'; -import 'add_spot.dart'; - - -class NavigationScreenPage extends StatefulWidget { - const NavigationScreenPage({super.key, required this.onAdd}); - - final ValueSetter onAdd; - - @override - State createState() => _NavigationScreenPage(); -} - -class _NavigationScreenPage extends State { - final TripService tripService = TripService(); - final LocationService locationService = LocationService(); - String address = ""; - List values = [1, 2, 3, 4, 5]; - double currentSliderValue = 1; - - @override - initState(){ - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: Future.wait([locationService.getPosition(), tripService.getTrips()]), - builder: (context, snapshot) { - if (snapshot.hasData) { - Position position = snapshot.data![0]; - List trips = snapshot.data![1]; - return Scaffold( - body: OpenStreetMapSearchAndPick( - center: LatLong(position.latitude, position.longitude), - buttonColor: Theme.of(context).colorScheme.primary, - buttonText: 'Set location', - onPicked: (pickedData) { - setState(() { - address = pickedData.address; - }); - showDialog( - context: context, - builder: (BuildContext context) => - AddSpot( - trips: trips, - coordinates: LatLng( - pickedData.latLong.latitude, - pickedData.latLong.longitude - ), - address: address, - onAdd: widget.onAdd, - ) - ); - } - ) - ); - } else if (snapshot.hasError) { - return Text('${snapshot.error}'); - } - return Scaffold( - body: Center ( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: const [ - Padding( - padding: EdgeInsets.all(50), - child: SizedBox( - height: 100.0, - width: 100.0, - child: CircularProgressIndicator(), - ), - ) - ], - ) - ) - ); - } - ); - } -} diff --git a/src/flutter_app/lib/components/select/select_ascent.dart b/src/flutter_app/lib/components/select/select_ascent.dart deleted file mode 100644 index 5b6d3d0..0000000 --- a/src/flutter_app/lib/components/select/select_ascent.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../interfaces/ascent/ascent.dart'; -import '../../interfaces/pitch/pitch.dart'; -import '../../services/ascent_service.dart'; -import '../../services/pitch_service.dart'; -import '../MyButtonStyles.dart'; - -class SelectAscent extends StatefulWidget { - const SelectAscent({super.key, required this.pitch}); - - final Pitch pitch; - - @override - State createState() => _SelectAscentState(); -} - -class _SelectAscentState extends State{ - final GlobalKey _formKey = GlobalKey(); - final AscentService ascentService = AscentService(); - final PitchService pitchService = PitchService(); - - @override - void initState(){ - super.initState(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - title: const Text('Please choose a ascent'), - content: FutureBuilder>( - future: ascentService.getAscents(), - builder: (context, snapshot) { - if (snapshot.hasData) { - List ascents = snapshot.data!; - List elements = []; - - for (int i = 0; i < ascents.length; i++){ - elements.add(ElevatedButton.icon( - icon: const Icon(Icons.arrow_forward, size: 30.0, color: Colors.pink), - label: Text(ascents[i].date), - onPressed: () { - if (!widget.pitch.ascentIds.contains(ascents[i].id)){ - widget.pitch.ascentIds.add(ascents[i].id); - pitchService.editPitch(widget.pitch.toUpdatePitch()); - } - Navigator.of(context).pop(); - }, - style: MyButtonStyles.rounded - )); - } - return Column( - children: elements, - ); - } else { - return const CircularProgressIndicator(); - } - }, - ), - ); - } -} \ No newline at end of file diff --git a/src/flutter_app/lib/components/select/select_ascent_of_single_pitch_route.dart b/src/flutter_app/lib/components/select/select_ascent_of_single_pitch_route.dart deleted file mode 100644 index 7b970ce..0000000 --- a/src/flutter_app/lib/components/select/select_ascent_of_single_pitch_route.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:climbing_diary/interfaces/single_pitch_route/single_pitch_route.dart'; -import 'package:flutter/material.dart'; - -import '../../interfaces/ascent/ascent.dart'; -import '../../interfaces/pitch/pitch.dart'; -import '../../services/ascent_service.dart'; -import '../../services/pitch_service.dart'; -import '../../services/route_service.dart'; -import '../MyButtonStyles.dart'; - -class SelectAscentOfSinglePitchRoute extends StatefulWidget { - const SelectAscentOfSinglePitchRoute({super.key, required this.singlePitchRoute}); - - final SinglePitchRoute singlePitchRoute; - - @override - State createState() => _SelectAscentOfSinglePitchRouteState(); -} - -class _SelectAscentOfSinglePitchRouteState extends State{ - final GlobalKey _formKey = GlobalKey(); - final AscentService ascentService = AscentService(); - final RouteService routeService = RouteService(); - - @override - void initState(){ - super.initState(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - title: const Text('Please choose a ascent'), - content: FutureBuilder>( - future: ascentService.getAscents(), - builder: (context, snapshot) { - if (snapshot.hasData) { - List ascents = snapshot.data!; - List elements = []; - - for (int i = 0; i < ascents.length; i++){ - elements.add(ElevatedButton.icon( - icon: const Icon(Icons.arrow_forward, size: 30.0, color: Colors.pink), - label: Text(ascents[i].date), - onPressed: () { - if (!widget.singlePitchRoute.ascentIds.contains(ascents[i].id)){ - widget.singlePitchRoute.ascentIds.add(ascents[i].id); - routeService.editSinglePitchRoute(widget.singlePitchRoute.toUpdateSinglePitchRoute()); - } - Navigator.of(context).pop(); - }, - style: MyButtonStyles.rounded - )); - } - return Column( - children: elements, - ); - } else { - return const CircularProgressIndicator(); - } - }, - ), - ); - } -} \ No newline at end of file diff --git a/src/flutter_app/lib/components/select/select_pitch.dart b/src/flutter_app/lib/components/select/select_pitch.dart deleted file mode 100644 index cdc5947..0000000 --- a/src/flutter_app/lib/components/select/select_pitch.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../interfaces/pitch/pitch.dart'; -import '../../interfaces/route/route.dart'; -import '../../services/pitch_service.dart'; -import '../../services/route_service.dart'; - -import '../MyButtonStyles.dart'; - -class SelectPitch extends StatefulWidget { - const SelectPitch({super.key, required this.route}); - - final ClimbingRoute route; - @override - State createState() => _SelectPitchState(); -} - -class _SelectPitchState extends State{ - final GlobalKey _formKey = GlobalKey(); - final PitchService pitchService = PitchService(); - final RouteService routeService = RouteService(); - - @override - void initState(){ - super.initState(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - title: const Text('Please choose a pitch'), - content: FutureBuilder>( - future: pitchService.getPitches(), - builder: (context, snapshot) { - if (snapshot.hasData) { - List pitches = snapshot.data!; - List elements = []; - - for (int i = 0; i < pitches.length; i++){ - elements.add(ElevatedButton.icon( - icon: const Icon(Icons.arrow_forward, size: 30.0, color: Colors.pink), - label: Text(pitches[i].name), - onPressed: () { - /* - if (!widget.route.pitchIds.contains(pitches[i].id)){ - widget.route.pitchIds.add(pitches[i].id); - routeService.editRoute(widget.route.toUpdateClimbingRoute()); - } - */ - Navigator.of(context).pop(); - }, - style: MyButtonStyles.rounded - )); - } - return Column( - children: elements, - ); - } else { - return const CircularProgressIndicator(); - } - }, - ), - ); - } -} \ No newline at end of file diff --git a/src/flutter_app/lib/components/select/select_route.dart b/src/flutter_app/lib/components/select/select_route.dart deleted file mode 100644 index bc5bcf5..0000000 --- a/src/flutter_app/lib/components/select/select_route.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../interfaces/route/route.dart'; -import '../../interfaces/spot/spot.dart'; -import '../../services/route_service.dart'; -import '../../services/spot_service.dart'; - -import '../MyButtonStyles.dart'; - -class SelectRoute extends StatefulWidget { - const SelectRoute({super.key, required this.spot}); - - final Spot spot; - @override - State createState() => _SelectRouteState(); -} - -class _SelectRouteState extends State{ - final GlobalKey _formKey = GlobalKey(); - final RouteService routeService = RouteService(); - final SpotService tripService = SpotService(); - - @override - void initState(){ - super.initState(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - title: const Text('Please choose a route'), - content: FutureBuilder>( - future: routeService.getRoutes(), - builder: (context, snapshot) { - if (snapshot.hasData) { - List routes = snapshot.data!; - List elements = []; - - for (int i = 0; i < routes.length; i++){ - elements.add(ElevatedButton.icon( - icon: const Icon(Icons.arrow_forward, size: 30.0, color: Colors.pink), - label: Text(routes[i].name), - onPressed: () { - if (!widget.spot.multiPitchRouteIds.contains(routes[i].id)){ - widget.spot.multiPitchRouteIds.add(routes[i].id); - tripService.editSpot(widget.spot.toUpdateSpot()); - } - Navigator.of(context).pop(); - }, - style: MyButtonStyles.rounded - )); - } - return Column( - children: elements, - ); - } else { - return const CircularProgressIndicator(); - } - }, - ), - ); - } -} \ No newline at end of file diff --git a/src/flutter_app/lib/components/select/select_spot.dart b/src/flutter_app/lib/components/select/select_spot.dart index 01f8b10..ec0a8f6 100644 --- a/src/flutter_app/lib/components/select/select_spot.dart +++ b/src/flutter_app/lib/components/select/select_spot.dart @@ -8,9 +8,11 @@ import '../../services/trip_service.dart'; import '../MyButtonStyles.dart'; class SelectSpot extends StatefulWidget { - const SelectSpot({super.key, required this.trip}); + const SelectSpot({super.key, required this.trip, required this.onAdd}); final Trip trip; + final ValueSetter onAdd; + @override State createState() => _SelectSpotState(); } @@ -42,12 +44,13 @@ class _SelectSpotState extends State{ elements.add(ElevatedButton.icon( icon: const Icon(Icons.arrow_forward, size: 30.0, color: Colors.pink), label: Text(spots[i].name), - onPressed: () { + onPressed: () async { if (!widget.trip.spotIds.contains(spots[i].id)){ widget.trip.spotIds.add(spots[i].id); - tripService.editTrip(widget.trip.toUpdateTrip()); + Trip? updatedTrip = await tripService.editTrip(widget.trip.toUpdateTrip()); + widget.onAdd.call(spots[i]); } - Navigator.of(context).pop(); + Navigator.popUntil(context, ModalRoute.withName('/')); }, style: MyButtonStyles.rounded )); diff --git a/src/flutter_app/lib/interfaces/grade.dart b/src/flutter_app/lib/interfaces/grade.dart index ee74f80..3d0c98c 100644 --- a/src/flutter_app/lib/interfaces/grade.dart +++ b/src/flutter_app/lib/interfaces/grade.dart @@ -14,13 +14,13 @@ class Grade{ ['5.1', '5.1', '5.1', '5.1', '5.2', '5.3', '5.4', '5.6', '5.7', '5.7', '5.8', '5.9', '5.10a', '5.10b', '5.10c', '5.10d', '5.11a', '5.11b', '5.11c', '5.11d', '5.12a', '5.12b', '5.12c', '5.12d', '5.13a', '5.13b', '5.13c', '5.13c', '5.13d', '5.14a', '5.14b', '5.14c', '5.14d', '5.14d', '5.15a', '5.15a', '5.15b', '5.15c', '5.15d'], // USA-YDS ['3', '3', '3', '3', '4a', '4a', '4a', '4a', '4b', '4c', '4c', '5a', '5a', '5b', '5b', '5c', '5c', '5c', '6a', '6a', '6a', '6b', '6b', '6c', '6c', '6c', '7a', '7a', '7a', '7a', '7b', '7b', '7c', '7c', '7c', '7c', '7c', '7c', '7c'], // UK-Tech ['VD', 'VD', 'VD', 'VD', 'VD', 'S', 'S', 'S', 'HS', 'HS', 'VS', 'HVS', 'E1', 'E1', 'E2', 'E2', 'E3', 'E3', 'E4', 'E4', 'E5', 'E5', 'E6', 'E6', 'E7', 'E7', 'E8', 'E8', 'E9', 'E9', 'E10', 'E11', 'E12', 'E12', 'E12', 'E12', 'E12', 'E12', 'E12'], // UK-Adj - ['1', '2', '3', '3', '4a', '4b', '4c', '5a', '5a', '5b', '5b', '5c', '6a', '6a+', '6b''6b+', '6c', '6c+', '7a', '7a+', '7b', '7b+', '7c', '7c+', '8a', '8a', '8a+', '8a+''8b', '8b+', '8c', '8c+', '9a', '9a', '9a+', '9a+', '9b', '9b+', '9c'], // French - ['I', 'II', 'III', 'III+', 'IV-', 'IV', 'IV+', 'V−', 'V', 'V+', 'VI-', 'VI', 'VI+''VII-', 'VII', 'VII+', 'VII+', 'VIII-', 'VIII', 'VIII+', 'VIII+', 'IX-', 'IX''IX+', 'IX+', 'X-', 'X-', 'X-', 'X', 'X+', 'XI-', 'XI-', 'XI', 'XI', 'XI+', 'XI+''XII-', 'XII-', 'XII'], // UIAA - ['11', '11', '12', '12', '12', '12', '12', '13', '14', '15', '16', '17', '18', '19''20', '21', '22', '23', '24', '25', '26', '26', '27', '28', '29', '29', '30', '30''31', '32', '33', '34', '35', '35', '35', '35', '35', '35', '35'], // Australien - ['I', 'II', 'III', '', 'IV', 'IV', 'V', 'V', 'VI', 'VIIa', 'VIIa', 'VIIb', 'VIIc''VIIIa', 'VIIIb', 'VIIIc', 'VIIIc', 'IXa', 'IXb', 'IXc', 'IXc', 'Xa', 'Xb', 'Xc''Xc', 'Xc', 'XIa', 'XIa', 'XIb', 'XIc', 'XIIa', 'XIIa', 'XIIb', 'XIIb', 'XIIb''XIIb', 'XIIb', 'XIIb', 'XIIb'], // Sachsen - ['1', '2', '3', '3+', '4-', '4', '4+', '5−', '5', '5', '5+', '5+', '6-', '6-', '6', '6''6+', '6+', '7-', '7', '7+', '8-', '8', '8+', '9-', '9', '9+', '9+', '10-', '10''10+', '11-', '11', '11', '11+', '11+', '11+', '11+', '11+'], // Skandinavien - ['Isup', 'II', 'IIsup', '', 'III', 'IIIsup', 'III', 'IIIsup', 'IV', 'IV', 'IVsup''V', 'Vsup', 'VI', 'VI', 'VIsup', 'VIIa', 'VIIa', 'VIIb', 'VIIc', 'VIIIa''VIIIb', 'VIIIc', 'IXa', 'IXb', 'IXc', 'Xa', 'Xa', 'Xb', 'Xc', 'XIa', 'XIa', 'XIa''XIa', 'XIa', 'XIa', 'XIa', 'XIa', 'XIa'], // Brasilien - ['1', '1', '2', '2', '3', '3', '3', '3', '4a', '4a', '4a', '4b', '4b', '4b', '4b', '4c''5a', '5a', '5b', '5c', '6a', '6b', '6c', '7a', '7a+', '7b', '7b+', '7c', '7c+''8a', '8a+', '8b', '8b+', '8c', '8c+', '8c+', '8c+', '9a', '9a'], //Fb + ['1', '2', '3', '3', '4a', '4b', '4c', '5a', '5a', '5b', '5b', '5c', '6a', '6a+', '6b', '6b+', '6c', '6c+', '7a', '7a+', '7b', '7b+', '7c', '7c+', '8a', '8a', '8a+', '8a+''8b', '8b+', '8c', '8c+', '9a', '9a', '9a+', '9a+', '9b', '9b+', '9c'], // French + ['I', 'II', 'III', 'III+', 'IV-', 'IV', 'IV+', 'V−', 'V', 'V+', 'VI-', 'VI', 'VI+', 'VII-', 'VII', 'VII+', 'VII+', 'VIII-', 'VIII', 'VIII+', 'VIII+', 'IX-', 'IX''IX+', 'IX+', 'X-', 'X-', 'X-', 'X', 'X+', 'XI-', 'XI-', 'XI', 'XI', 'XI+', 'XI+''XII-', 'XII-', 'XII'], // UIAA + ['11', '11', '12', '12', '12', '12', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '26', '27', '28', '29', '29', '30', '30''31', '32', '33', '34', '35', '35', '35', '35', '35', '35', '35'], // Australien + ['I', 'II', 'III', '', 'IV', 'IV', 'V', 'V', 'VI', 'VIIa', 'VIIa', 'VIIb', 'VIIc', 'VIIIa', 'VIIIb', 'VIIIc', 'VIIIc', 'IXa', 'IXb', 'IXc', 'IXc', 'Xa', 'Xb', 'Xc''Xc', 'Xc', 'XIa', 'XIa', 'XIb', 'XIc', 'XIIa', 'XIIa', 'XIIb', 'XIIb', 'XIIb''XIIb', 'XIIb', 'XIIb', 'XIIb'], // Sachsen + ['1', '2', '3', '3+', '4-', '4', '4+', '5−', '5', '5', '5+', '5+', '6-', '6-', '6', '6', '6+', '6+', '7-', '7', '7+', '8-', '8', '8+', '9-', '9', '9+', '9+', '10-', '10''10+', '11-', '11', '11', '11+', '11+', '11+', '11+', '11+'], // Skandinavien + ['Isup', 'II', 'IIsup', '', 'III', 'IIIsup', 'III', 'IIIsup', 'IV', 'IV', 'IVsup', 'V', 'Vsup', 'VI', 'VI', 'VIsup', 'VIIa', 'VIIa', 'VIIb', 'VIIc', 'VIIIa''VIIIb', 'VIIIc', 'IXa', 'IXb', 'IXc', 'Xa', 'Xa', 'Xb', 'Xc', 'XIa', 'XIa', 'XIa''XIa', 'XIa', 'XIa', 'XIa', 'XIa', 'XIa'], // Brasilien + ['1', '1', '2', '2', '3', '3', '3', '3', '4a', '4a', '4a', '4b', '4b', '4b', '4b', '4c', '5a', '5a', '5b', '5c', '6a', '6b', '6c', '7a', '7a+', '7b', '7b+', '7c', '7c+''8a', '8a+', '8b', '8b+', '8c', '8c+', '8c+', '8c+', '9a', '9a'], //Fb ]; Grade translate(GradingSystem gs){ diff --git a/src/flutter_app/lib/main.dart b/src/flutter_app/lib/main.dart index 39237d6..b7ca9e7 100644 --- a/src/flutter_app/lib/main.dart +++ b/src/flutter_app/lib/main.dart @@ -1,11 +1,11 @@ import 'package:auth0_flutter/auth0_flutter.dart'; -import 'package:climbing_diary/pages/list_page.dart'; +import 'package:climbing_diary/pages/list_page/list_page.dart'; +import 'package:climbing_diary/pages/map_page/map_page.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; import 'package:overlay_support/overlay_support.dart'; import 'config/environment.dart'; -import 'pages/diary_page.dart'; -import 'components/map_page/map_page.dart'; -import 'pages/statistic_page.dart'; +import 'pages/diary_page/diary_page.dart'; +import 'pages/statistic_page/statistic_page.dart'; import 'services/locator.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; diff --git a/src/flutter_app/lib/pages/diary_page.dart b/src/flutter_app/lib/pages/diary_page/diary_page.dart similarity index 83% rename from src/flutter_app/lib/pages/diary_page.dart rename to src/flutter_app/lib/pages/diary_page/diary_page.dart index ac2a87e..9124da6 100644 --- a/src/flutter_app/lib/pages/diary_page.dart +++ b/src/flutter_app/lib/pages/diary_page/diary_page.dart @@ -1,5 +1,5 @@ +import 'package:climbing_diary/pages/diary_page/timeline/trip_timeline.dart'; import 'package:flutter/material.dart'; -import '../components/diary_page/timeline/trip_timeline.dart'; class DiaryPage extends StatefulWidget { const DiaryPage({super.key}); diff --git a/src/flutter_app/lib/components/diary_page/image_list_view.dart b/src/flutter_app/lib/pages/diary_page/image_list_view.dart similarity index 100% rename from src/flutter_app/lib/components/diary_page/image_list_view.dart rename to src/flutter_app/lib/pages/diary_page/image_list_view.dart diff --git a/src/flutter_app/lib/components/diary_page/rating_row.dart b/src/flutter_app/lib/pages/diary_page/rating_row.dart similarity index 100% rename from src/flutter_app/lib/components/diary_page/rating_row.dart rename to src/flutter_app/lib/pages/diary_page/rating_row.dart diff --git a/src/flutter_app/lib/components/detail/route_details.dart b/src/flutter_app/lib/pages/diary_page/spot_details.dart similarity index 71% rename from src/flutter_app/lib/components/detail/route_details.dart rename to src/flutter_app/lib/pages/diary_page/spot_details.dart index 3a4c76f..e70c4a3 100644 --- a/src/flutter_app/lib/components/detail/route_details.dart +++ b/src/flutter_app/lib/pages/diary_page/spot_details.dart @@ -1,41 +1,34 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:latlong2/latlong.dart'; import 'package:skeletons/skeletons.dart'; -import '../../interfaces/route/route.dart'; +import '../../components/MyButtonStyles.dart'; +import '../../components/edit/edit_spot.dart'; import '../../interfaces/spot/spot.dart'; import '../../interfaces/trip/trip.dart'; import '../../services/media_service.dart'; -import '../../services/pitch_service.dart'; -import '../../services/route_service.dart'; -import '../MyButtonStyles.dart'; -import '../add/add_pitch.dart'; -import '../diary_page/timeline/pitch_timeline.dart'; -import '../edit/edit_route.dart'; -import '../select/select_pitch.dart'; +import '../../services/spot_service.dart'; +import 'timeline/route_timeline.dart'; -class RouteDetails extends StatefulWidget { - const RouteDetails({super.key, this.trip, required this.spot, required this.route, required this.onDelete, required this.onUpdate, required this.spotId }); +class SpotDetails extends StatefulWidget { + const SpotDetails({super.key, this.trip, required this.spot, required this.onDelete, required this.onUpdate }); final Trip? trip; final Spot spot; - final ClimbingRoute route; - final ValueSetter onDelete; - final ValueSetter onUpdate; - final String spotId; + final ValueSetter onDelete, onUpdate; @override - State createState() => _RouteDetailsState(); + State createState() => _SpotDetailsState(); } -class _RouteDetailsState extends State{ +class _SpotDetailsState extends State{ final MediaService mediaService = MediaService(); - final RouteService routeService = RouteService(); - final PitchService pitchService = PitchService(); + final SpotService spotService = SpotService(); Future> fetchURLs() { List> futures = []; - for (var mediaId in widget.route.mediaIds) { + for (var mediaId in widget.spot.mediaIds) { futures.add(mediaService.getMediumUrl(mediaId)); } return Future.wait(futures); @@ -48,9 +41,9 @@ class _RouteDetailsState extends State{ var img = await picker.pickImage(source: media); if (img != null){ var mediaId = await mediaService.uploadMedia(img); - ClimbingRoute route = widget.route; - route.mediaIds.add(mediaId); - routeService.editRoute(route.toUpdateClimbingRoute()); + Spot spot = widget.spot; + spot.mediaIds.add(mediaId); + spotService.editSpot(spot.toUpdateSpot()); } setState(() { @@ -103,11 +96,11 @@ class _RouteDetailsState extends State{ }); } - void editRouteDialog() { + void editSpotDialog() { showDialog( context: context, builder: (BuildContext context) { - return EditRoute(route: widget.route, onUpdate: widget.onUpdate); + return EditSpot(spot: widget.spot, onUpdate: widget.onUpdate); }); } @@ -119,19 +112,25 @@ class _RouteDetailsState extends State{ @override Widget build(BuildContext context) { List elements = []; - ClimbingRoute route = widget.route; // general info elements.addAll([ Text( - route.name, + widget.spot.name, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w600 ), ), Text( - route.location, + '${round(widget.spot.coordinates[0], decimals: 8)}, ${round(widget.spot.coordinates[1], decimals: 8)}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400 + ), + ), + Text( + widget.spot.location, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w400 @@ -141,7 +140,7 @@ class _RouteDetailsState extends State{ List ratingRowElements = []; for (var i = 0; i < 5; i++){ - if (route.rating > i) { + if (widget.spot.rating > i) { ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.pink)); } else { ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.grey)); @@ -149,14 +148,40 @@ class _RouteDetailsState extends State{ } elements.add(Center(child: Padding( - padding: const EdgeInsets.only(top: 10), + padding: const EdgeInsets.all(10), child:Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: ratingRowElements, ) ))); - if (route.comment.isNotEmpty) { + // time to walk transport + elements.add(Center(child: Padding( + padding: const EdgeInsets.all(5), + child:Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Icon(Icons.train, size: 30.0, color: Colors.green), + Text( + '${widget.spot.distancePublicTransport} min', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400 + ), + ), + const Icon(Icons.directions_car, size: 30.0, color: Colors.red), + Text( + '${widget.spot.distanceParking} min', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400 + ), + ) + ], + ) + ))); + + if (widget.spot.comment.isNotEmpty) { elements.add(Container( margin: const EdgeInsets.all(15.0), padding: const EdgeInsets.all(5.0), @@ -165,12 +190,12 @@ class _RouteDetailsState extends State{ borderRadius: BorderRadius.circular(10), ), child: Text( - route.comment, + widget.spot.comment, ) )); } // images - if (route.mediaIds.isNotEmpty) { + if (widget.spot.mediaIds.isNotEmpty) { List imageWidgets = []; Future> futureMediaUrls = fetchURLs(); @@ -219,7 +244,7 @@ class _RouteDetailsState extends State{ ); } List skeletons = []; - for (var i = 0; i < route.mediaIds.length; i++){ + for (var i = 0; i < widget.spot.mediaIds.length; i++){ skeletons.add(skeleton); } return Container( @@ -259,52 +284,22 @@ class _RouteDetailsState extends State{ ), ); } - // add pitch - elements.add( - ElevatedButton.icon( - icon: const Icon(Icons.add, size: 30.0, color: Colors.pink), - label: const Text('Add new pitch'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AddPitch(routes: [widget.route],), - ) - ); - }, - style: MyButtonStyles.rounded - ), - ); - elements.add( - ElevatedButton.icon( - icon: const Icon(Icons.add, size: 30.0, color: Colors.pink), - label: const Text('Add existing pitch'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SelectPitch(route: widget.route), - ) - ); - }, - style: MyButtonStyles.rounded - ), - ); + // delete, edit, close elements.add( Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // delete route button + // delete spot button IconButton( onPressed: () { Navigator.pop(context); - routeService.deleteRoute(route, widget.spotId); - widget.onDelete.call(route); + spotService.deleteSpot(widget.spot); + widget.onDelete.call(widget.spot); }, icon: const Icon(Icons.delete), ), IconButton( - onPressed: () => editRouteDialog(), + onPressed: () => editSpotDialog(), icon: const Icon(Icons.edit), ), IconButton( @@ -314,9 +309,22 @@ class _RouteDetailsState extends State{ ], ) ); + // routes + if (widget.spot.multiPitchRouteIds.isNotEmpty){ + elements.add( + RouteTimeline( + trip: widget.trip, + spot: widget.spot, + singlePitchRouteIds: widget.spot.singlePitchRouteIds, + multiPitchRouteIds: widget.spot.multiPitchRouteIds, + startDate: DateTime(1923), + endDate: DateTime(2123) + ) + ); + } return Stack( children: [ - Padding( + Container( padding: const EdgeInsets.all(20), child: ListView( children: elements diff --git a/src/flutter_app/lib/components/diary_page/timeline/ascent_timeline.dart b/src/flutter_app/lib/pages/diary_page/timeline/ascent_timeline.dart similarity index 97% rename from src/flutter_app/lib/components/diary_page/timeline/ascent_timeline.dart rename to src/flutter_app/lib/pages/diary_page/timeline/ascent_timeline.dart index 15dc10c..9554372 100644 --- a/src/flutter_app/lib/components/diary_page/timeline/ascent_timeline.dart +++ b/src/flutter_app/lib/pages/diary_page/timeline/ascent_timeline.dart @@ -1,16 +1,15 @@ -import 'package:climbing_diary/components/diary_page/image_list_view.dart'; import 'package:climbing_diary/interfaces/route/route.dart'; import 'package:flutter/material.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; import 'package:timelines/timelines.dart'; +import '../../../components/detail/ascent_details.dart'; +import '../../../components/info/ascent_info.dart'; import '../../../interfaces/ascent/ascent.dart'; -import '../../../interfaces/pitch/pitch.dart'; import '../../../interfaces/trip/trip.dart'; import '../../../interfaces/spot/spot.dart'; import '../../../services/ascent_service.dart'; -import '../../detail/ascent_details.dart'; -import '../../info/ascent_info.dart'; +import '../image_list_view.dart'; class AscentTimeline extends StatefulWidget { const AscentTimeline({super.key, this.trip, required this.spot, required this.route, required this.pitchId, required this.ascentIds, required this.onDelete, required this.onUpdate, required this.startDate, required this.endDate, required this.ofMultiPitch}); diff --git a/src/flutter_app/lib/components/diary_page/timeline/pitch_timeline.dart b/src/flutter_app/lib/pages/diary_page/timeline/pitch_timeline.dart similarity index 94% rename from src/flutter_app/lib/components/diary_page/timeline/pitch_timeline.dart rename to src/flutter_app/lib/pages/diary_page/timeline/pitch_timeline.dart index 1753ae8..a461b70 100644 --- a/src/flutter_app/lib/components/diary_page/timeline/pitch_timeline.dart +++ b/src/flutter_app/lib/pages/diary_page/timeline/pitch_timeline.dart @@ -1,16 +1,16 @@ -import 'package:climbing_diary/components/diary_page/image_list_view.dart'; -import 'package:climbing_diary/components/diary_page/rating_row.dart'; import 'package:flutter/material.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; import 'package:timelines/timelines.dart'; +import '../../../components/detail/pitch_details.dart'; +import '../../../components/info/pitch_info.dart'; import '../../../interfaces/pitch/pitch.dart'; import '../../../interfaces/route/route.dart'; import '../../../interfaces/spot/spot.dart'; import '../../../interfaces/trip/trip.dart'; import '../../../services/pitch_service.dart'; -import '../../detail/pitch_details.dart'; -import '../../info/pitch_info.dart'; +import '../image_list_view.dart'; +import '../rating_row.dart'; class PitchTimeline extends StatefulWidget { const PitchTimeline({super.key, this.trip, required this.spot, required this.route, required this.pitchIds}); @@ -72,7 +72,9 @@ class PitchTimelineState extends State { setState(() {}); } - return Column( + return Column(children: [ExpansionTile( + leading: const Icon(Icons.commit), + title: const Text("pitches"), children: [ FixedTimeline.tileBuilder( theme: TimelineThemeData( @@ -137,7 +139,7 @@ class PitchTimelineState extends State { const SolidLineConnector(color: Color(0xff66c97f)), ), ) - ], + ])], ); } else { return const CircularProgressIndicator(); diff --git a/src/flutter_app/lib/components/diary_page/timeline/route_timeline.dart b/src/flutter_app/lib/pages/diary_page/timeline/route_timeline.dart similarity index 94% rename from src/flutter_app/lib/components/diary_page/timeline/route_timeline.dart rename to src/flutter_app/lib/pages/diary_page/timeline/route_timeline.dart index 1f77597..1d77e6d 100644 --- a/src/flutter_app/lib/components/diary_page/timeline/route_timeline.dart +++ b/src/flutter_app/lib/pages/diary_page/timeline/route_timeline.dart @@ -1,21 +1,21 @@ -import 'package:climbing_diary/components/diary_page/image_list_view.dart'; -import 'package:climbing_diary/components/diary_page/timeline/pitch_timeline.dart'; -import 'package:climbing_diary/components/diary_page/rating_row.dart'; import 'package:climbing_diary/interfaces/multi_pitch_route/multi_pitch_route.dart'; import 'package:climbing_diary/interfaces/single_pitch_route/single_pitch_route.dart'; +import 'package:climbing_diary/pages/diary_page/timeline/pitch_timeline.dart'; import 'package:flutter/material.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; import 'package:timelines/timelines.dart'; +import '../../../components/detail/multi_pitch_route_details.dart'; +import '../../../components/detail/single_pitch_route_details.dart'; +import '../../../components/info/multi_pitch_route_info.dart'; +import '../../../components/info/route_info.dart'; +import '../../../components/info/single_pitch_route_info.dart'; import '../../../interfaces/spot/spot.dart'; import '../../../interfaces/trip/trip.dart'; import '../../../services/pitch_service.dart'; import '../../../services/route_service.dart'; -import '../../detail/multi_pitch_route_details.dart'; -import '../../detail/single_pitch_route_details.dart'; -import '../../info/multi_pitch_route_info.dart'; -import '../../info/route_info.dart'; -import '../../info/single_pitch_route_info.dart'; +import '../image_list_view.dart'; +import '../rating_row.dart'; class RouteTimeline extends StatefulWidget { const RouteTimeline({super.key, this.trip, required this.spot, required this.singlePitchRouteIds, required this.multiPitchRouteIds, required this.startDate, required this.endDate}); @@ -190,7 +190,6 @@ class RouteTimelineState extends State { // route info SinglePitchRoute singlePitchRoute = singlePitchRoutes[index]; elements.add(SinglePitchRouteInfo( - spot: widget.spot, route: singlePitchRoute )); // rating as hearts in a row @@ -243,7 +242,11 @@ class RouteTimelineState extends State { ); } return Column( - children: elements, + children: [ExpansionTile( + leading: const Icon(Icons.route), + title: const Text("routes"), + children: elements, + )], ); } else { return const CircularProgressIndicator(); diff --git a/src/flutter_app/lib/components/diary_page/timeline/spot_timeline.dart b/src/flutter_app/lib/pages/diary_page/timeline/spot_timeline.dart similarity index 92% rename from src/flutter_app/lib/components/diary_page/timeline/spot_timeline.dart rename to src/flutter_app/lib/pages/diary_page/timeline/spot_timeline.dart index 85529de..94be511 100644 --- a/src/flutter_app/lib/components/diary_page/timeline/spot_timeline.dart +++ b/src/flutter_app/lib/pages/diary_page/timeline/spot_timeline.dart @@ -1,17 +1,16 @@ -import 'package:climbing_diary/components/diary_page/image_list_view.dart'; -import 'package:climbing_diary/components/diary_page/rating_row.dart'; -import 'package:climbing_diary/components/diary_page/timeline/route_timeline.dart'; +import 'package:climbing_diary/pages/diary_page/timeline/route_timeline.dart'; import 'package:flutter/material.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; import 'package:timelines/timelines.dart'; +import '../../../components/info/spot_info.dart'; import '../../../interfaces/spot/spot.dart'; import '../../../interfaces/trip/trip.dart'; -import '../../../interfaces/trip/update_trip.dart'; import '../../../services/spot_service.dart'; import '../../../services/trip_service.dart'; -import '../../detail/spot_details.dart'; -import '../../info/spot_info.dart'; +import '../image_list_view.dart'; +import '../rating_row.dart'; +import '../spot_details.dart'; class SpotTimeline extends StatefulWidget { const SpotTimeline({super.key, required this.spotIds, required this.trip, required this.startDate, required this.endDate}); @@ -43,11 +42,13 @@ class SpotTimelineState extends State { if (snapshot.hasData) { var online = snapshot.data!; if (online) { + return FutureBuilder>( future: Future.wait(spotIds.map((spotId) => spotService.getSpot(spotId))), builder: (context, snapshot) { if (snapshot.hasData) { List spots = snapshot.data!.whereType().toList(); + spots.sort((a, b) => a.name.compareTo(b.name)); updateSpotCallback(Spot spot) { var index = -1; @@ -64,12 +65,12 @@ class SpotTimelineState extends State { deleteSpotCallback(Spot spot) { spots.remove(spot); widget.trip.spotIds.remove(spot.id); - UpdateTrip editTrip = widget.trip.toUpdateTrip(); - tripService.editTrip(editTrip); setState(() {}); } - return Column( + return Column(children: [ExpansionTile( + leading: const Icon(Icons.place), + title: const Text("spots"), children: [ FixedTimeline.tileBuilder( theme: TimelineThemeData( @@ -148,8 +149,8 @@ class SpotTimelineState extends State { connectorBuilder: (_, index, ___) => const SolidLineConnector(color: Color(0xff66c97f)), ), - ) - ], + )] + )], ); } else { return const CircularProgressIndicator(); diff --git a/src/flutter_app/lib/components/diary_page/timeline/trip_timeline.dart b/src/flutter_app/lib/pages/diary_page/timeline/trip_timeline.dart similarity index 85% rename from src/flutter_app/lib/components/diary_page/timeline/trip_timeline.dart rename to src/flutter_app/lib/pages/diary_page/timeline/trip_timeline.dart index e105699..6ce0039 100644 --- a/src/flutter_app/lib/components/diary_page/timeline/trip_timeline.dart +++ b/src/flutter_app/lib/pages/diary_page/timeline/trip_timeline.dart @@ -1,15 +1,15 @@ -import 'package:climbing_diary/components/diary_page/image_list_view.dart'; -import 'package:climbing_diary/components/diary_page/rating_row.dart'; -import 'package:climbing_diary/components/diary_page/timeline/spot_timeline.dart'; +import 'package:climbing_diary/pages/diary_page/timeline/spot_timeline.dart'; import 'package:flutter/material.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; import 'package:timelines/timelines.dart'; +import '../../../components/add/add_trip.dart'; +import '../../../components/detail/trip_details.dart'; +import '../../../components/info/trip_info.dart'; import '../../../interfaces/trip/trip.dart'; import '../../../services/trip_service.dart'; -import '../../add/add_trip.dart'; -import '../../detail/trip_details.dart'; -import '../../info/trip_info.dart'; +import '../image_list_view.dart'; +import '../rating_row.dart'; class TripTimeline extends StatefulWidget { const TripTimeline({super.key}); @@ -68,7 +68,12 @@ class TripTimelineState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => const AddTrip(), + builder: (context) => AddTrip( + onAdd: (trip) { + trips.add(trip); + setState(() {}); + } + ), ) ); }, @@ -108,7 +113,7 @@ class TripTimelineState extends State { trip: trips[index], spotIds: trips[index].spotIds, startDate: DateTime.parse(trips[index].startDate), - endDate: DateTime.parse(trips[index].endDate) + endDate: DateTime.parse(trips[index].endDate), ) ); } @@ -122,8 +127,13 @@ class TripTimelineState extends State { borderRadius: BorderRadius.circular(20), ), child: TripDetails(trip: trips[index], - onDelete: deleteTripCallback, - onUpdate: updateTripCallback) + onTripDelete: deleteTripCallback, + onTripUpdate: updateTripCallback, + onSpotAdd: (spot) { + trips[index].spotIds.add(spot.id); + setState(() {}); + }, + ) ), ), child: Ink( diff --git a/src/flutter_app/lib/pages/list_page.dart b/src/flutter_app/lib/pages/list_page.dart deleted file mode 100644 index 7bd5521..0000000 --- a/src/flutter_app/lib/pages/list_page.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:latlong2/latlong.dart'; -import '../interfaces/spot/spot.dart'; -import '../services/spot_service.dart'; - -class ListPage extends StatefulWidget { - const ListPage({super.key}); - - @override - State createState() => ListPageState(); -} - -class ListPageState extends State { - final GlobalKey _formKey = GlobalKey(); - final TextEditingController controllerSearch = TextEditingController(); - - final SpotService spotService = SpotService(); - - @override - void initState(){ - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: FutureBuilder>( - future: spotService.getSpotsByName(controllerSearch.text), - builder: (context, snapshot) { - if (snapshot.hasData) { - List spots = snapshot.data!; - List elements = []; - elements.add(Form( - key: _formKey, - child: - Padding( - padding: const EdgeInsets.all(20), - child: TextFormField( - controller: controllerSearch, - decoration: const InputDecoration( - icon: Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), - borderSide: BorderSide(color: Colors.blue), - ), - hintText: "spot, route or pitch name", - labelText: "spot, route or pitch name" - ), - onChanged: (String s) { - setState(() {}); - } - ), - ), - )); - for (Spot spot in spots){ - elements.add( - Padding( - padding: const EdgeInsets.all(5), - child: Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - spot.name, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w600, - ), - ), - Text( - '${round(spot.coordinates[0], decimals: 8)}, ${round(spot.coordinates[1], decimals: 8)}', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400 - ), - ), - ] - ), - ) - ) - ) - ); - } - return ListView( - children: elements, - ); - } else { - return const CircularProgressIndicator(); - } - } - ) - ); - } -} \ No newline at end of file diff --git a/src/flutter_app/lib/pages/list_page/list_page.dart b/src/flutter_app/lib/pages/list_page/list_page.dart new file mode 100644 index 0000000..726c9b9 --- /dev/null +++ b/src/flutter_app/lib/pages/list_page/list_page.dart @@ -0,0 +1,167 @@ +import 'package:climbing_diary/interfaces/multi_pitch_route/multi_pitch_route.dart'; +import 'package:climbing_diary/pages/list_page/pitch_list.dart'; +import 'package:climbing_diary/pages/list_page/spot_list.dart'; +import 'package:climbing_diary/pages/list_page/route_list.dart'; +import 'package:climbing_diary/services/pitch_service.dart'; +import 'package:flutter/material.dart'; +import '../../interfaces/pitch/pitch.dart'; +import '../../interfaces/single_pitch_route/single_pitch_route.dart'; +import '../../interfaces/spot/spot.dart'; +import '../../services/route_service.dart'; +import '../../services/spot_service.dart'; + +class ListPage extends StatefulWidget { + const ListPage({super.key}); + + @override + State createState() => ListPageState(); +} + +class ListPageState extends State { + final GlobalKey _formKey = GlobalKey(); + final TextEditingController controllerSearch = TextEditingController(); + + final SpotService spotService = SpotService(); + final RouteService routeService = RouteService(); + final PitchService pitchService = PitchService(); + + bool searchSpots = true; + bool searchRoutes = false; + bool searchPitches = false; + + @override + void initState(){ + super.initState(); + searchSpots = true; + searchRoutes = false; + searchPitches = false; + } + + @override + Widget build(BuildContext context) { + Widget search = Form( + key: _formKey, + child: + Padding( + padding: const EdgeInsets.all(20), + child: TextFormField( + controller: controllerSearch, + decoration: const InputDecoration( + icon: Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(25.0)), + borderSide: BorderSide(color: Colors.blue), + ), + hintText: "name", + labelText: "name" + ), + onChanged: (String s) { + setState(() {}); + } + ), + ), + ); + + Widget switches = Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Text("spots"), + Switch( + value: searchSpots, + onChanged: (bool value) { + setState(() { + searchSpots = value; + }); + } + ), + const Text("routes"), + Switch( + value: searchRoutes, + onChanged: (bool value) { + setState(() { + searchRoutes = value; + }); + } + ), + const Text("pitches"), + Switch( + value: searchPitches, + onChanged: (bool value) { + setState(() { + searchPitches = value; + }); + } + ), + ] + ); + + Widget spotList = FutureBuilder>( + future: spotService.getSpotsByName(controllerSearch.text), + builder: (context, snapshot) { + if (snapshot.hasData) { + List spots = snapshot.data!; + return SpotList( + spots: spots + ); + } else { + return const CircularProgressIndicator(); + } + } + ); + + Widget routeList = FutureBuilder>( + future: routeService.getMultiPitchRoutesByName(controllerSearch.text), + builder: (context, snapshot) { + if (snapshot.hasData) { + List multiPitchRoutes = snapshot.data!; + return FutureBuilder>( + future: routeService.getSinglePitchRoutesByName(controllerSearch.text), + builder: (context, snapshot) { + if (snapshot.hasData) { + List singlePitchRoutes = snapshot.data!; + return RouteList( + singlePitchRoutes: singlePitchRoutes, + multiPitchRoutes: multiPitchRoutes, + ); + } else { + return const CircularProgressIndicator(); + } + } + ); + } else { + return const CircularProgressIndicator(); + } + } + ); + + Widget pitchList = FutureBuilder>( + future: pitchService.getPitchesByName(controllerSearch.text), + builder: (context, snapshot) { + if (snapshot.hasData) { + List pitches = snapshot.data!; + return PitchList( + pitches: pitches + ); + } else { + return const CircularProgressIndicator(); + } + } + ); + + List elements = []; + + if (searchSpots){ + elements.add(spotList); + } + if(searchRoutes){ + elements.add(routeList); + } + if(searchPitches){ + elements.add(pitchList); + } + + return Scaffold( + body: Column(children: [search, switches, Expanded(child: ListView(children: elements))]) + ); + } +} \ No newline at end of file diff --git a/src/flutter_app/lib/pages/list_page/multi_pitch_route_details.dart b/src/flutter_app/lib/pages/list_page/multi_pitch_route_details.dart new file mode 100644 index 0000000..774309b --- /dev/null +++ b/src/flutter_app/lib/pages/list_page/multi_pitch_route_details.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:skeletons/skeletons.dart'; + +import '../../components/info/multi_pitch_route_info.dart'; +import '../../interfaces/multi_pitch_route/multi_pitch_route.dart'; +import '../../services/media_service.dart'; +import '../../services/pitch_service.dart'; +import '../../services/route_service.dart'; + +class MultiPitchRouteDetails extends StatefulWidget { + const MultiPitchRouteDetails({super.key, required this.route}); + + final MultiPitchRoute route; + + @override + State createState() => _MultiPitchRouteDetailsState(); +} + +class _MultiPitchRouteDetailsState extends State{ + final MediaService mediaService = MediaService(); + final RouteService routeService = RouteService(); + final PitchService pitchService = PitchService(); + + Future> fetchURLs() { + List> futures = []; + for (var mediaId in widget.route.mediaIds) { + futures.add(mediaService.getMediumUrl(mediaId)); + } + return Future.wait(futures); + } + + XFile? image; + final ImagePicker picker = ImagePicker(); + + Future getImage(ImageSource media) async { + var img = await picker.pickImage(source: media); + if (img != null){ + var mediaId = await mediaService.uploadMedia(img); + MultiPitchRoute route = widget.route; + route.mediaIds.add(mediaId); + routeService.editMultiPitchRoute(route.toUpdateMultiPitchRoute()); + } + + setState(() { + image = img; + }); + } + + void addImageDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + title: const Text('Please choose media to select'), + content: SizedBox( + height: MediaQuery.of(context).size.height / 6, + child: Column( + children: [ + ElevatedButton( + //if user click this button, user can upload image from gallery + onPressed: () { + Navigator.pop(context); + getImage(ImageSource.gallery); + }, + child: Row( + children: const [ + Icon(Icons.image), + Text('From Gallery'), + ], + ), + ), + ElevatedButton( + //if user click this button. user can upload image from camera + onPressed: () { + Navigator.pop(context); + getImage(ImageSource.camera); + }, + child: Row( + children: const [ + Icon(Icons.camera), + Text('From Camera'), + ], + ), + ), + ], + ), + ), + ); + }); + } + + @override + void initState(){ + super.initState(); + } + + @override + Widget build(BuildContext context) { + List elements = []; + MultiPitchRoute route = widget.route; + + // general info + elements.addAll([ + Text( + route.name, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600 + ), + ), + MultiPitchInfo( + pitchIds: route.pitchIds + ), + Text( + route.location, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400 + ), + )]); + // rating + List ratingRowElements = []; + + for (var i = 0; i < 5; i++){ + if (route.rating > i) { + ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.pink)); + } else { + ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.grey)); + } + } + + elements.add(Center(child: Padding( + padding: const EdgeInsets.only(top: 10), + child:Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: ratingRowElements, + ) + ))); + + if (route.comment.isNotEmpty) { + elements.add(Container( + margin: const EdgeInsets.all(15.0), + padding: const EdgeInsets.all(5.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + route.comment, + ) + )); + } + // images + if (route.mediaIds.isNotEmpty) { + List imageWidgets = []; + Future> futureMediaUrls = fetchURLs(); + + imageWidgets.add( + FutureBuilder>( + future: futureMediaUrls, + builder: (context, snapshot) { + Widget skeleton = const Padding( + padding: EdgeInsets.all(5), + child: SkeletonAvatar( + style: SkeletonAvatarStyle( + shape: BoxShape.rectangle, width: 150, height: 250 + ), + ) + ); + + if (snapshot.data != null){ + List urls = snapshot.data!; + List images = []; + for (var url in urls){ + images.add( + Padding( + padding: const EdgeInsets.all(5.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.network( + url, + fit: BoxFit.fitHeight, + loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return skeleton; + }, + ) + ), + ) + ); + } + return Container( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: images + ) + ); + } + List skeletons = []; + for (var i = 0; i < route.mediaIds.length; i++){ + skeletons.add(skeleton); + } + return Container( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: skeletons + ) + ); + } + ) + ); + elements.add( + SizedBox( + height: 250, + child: ListView( + scrollDirection: Axis.horizontal, + children: imageWidgets + ) + ), + ); + } + return Column( + children: elements + ); + } +} \ No newline at end of file diff --git a/src/flutter_app/lib/pages/list_page/pitch_details.dart b/src/flutter_app/lib/pages/list_page/pitch_details.dart new file mode 100644 index 0000000..0cb1162 --- /dev/null +++ b/src/flutter_app/lib/pages/list_page/pitch_details.dart @@ -0,0 +1,217 @@ +import 'package:climbing_diary/interfaces/route/route.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:skeletons/skeletons.dart'; + +import '../../components/info/pitch_info.dart'; +import '../../interfaces/pitch/pitch.dart'; +import '../../pages/diary_page/timeline/ascent_timeline.dart'; +import '../../services/media_service.dart'; +import '../../services/pitch_service.dart'; + +class PitchDetails extends StatefulWidget { + const PitchDetails({super.key, required this.pitch}); + + final Pitch pitch; + + @override + State createState() => _PitchDetailsState(); +} + +class _PitchDetailsState extends State{ + final MediaService mediaService = MediaService(); + final PitchService pitchService = PitchService(); + + Future> fetchURLs() { + List> futures = []; + for (var mediaId in widget.pitch.mediaIds) { + futures.add(mediaService.getMediumUrl(mediaId)); + } + return Future.wait(futures); + } + + XFile? image; + final ImagePicker picker = ImagePicker(); + + Future getImage(ImageSource media) async { + var img = await picker.pickImage(source: media); + if (img != null){ + var mediaId = await mediaService.uploadMedia(img); + Pitch pitch = widget.pitch; + pitch.mediaIds.add(mediaId); + pitchService.editPitch(pitch.toUpdatePitch()); + } + + setState(() { + image = img; + }); + } + + void addImageDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + title: const Text('Please choose media to select'), + content: SizedBox( + height: MediaQuery.of(context).size.height / 6, + child: Column( + children: [ + ElevatedButton( + //if user click this button, user can upload image from gallery + onPressed: () { + Navigator.pop(context); + getImage(ImageSource.gallery); + }, + child: Row( + children: const [ + Icon(Icons.image), + Text('From Gallery'), + ], + ), + ), + ElevatedButton( + //if user click this button. user can upload image from camera + onPressed: () { + Navigator.pop(context); + getImage(ImageSource.camera); + }, + child: Row( + children: const [ + Icon(Icons.camera), + Text('From Camera'), + ], + ), + ), + ], + ), + ), + ); + }); + } + + @override + void initState(){ + super.initState(); + } + + @override + Widget build(BuildContext context) { + Pitch pitch = widget.pitch; + List elements = []; + + // general info + elements.add(PitchInfo(pitch: pitch)); + // rating + List ratingRowElements = []; + + for (var i = 0; i < 5; i++){ + if (pitch.rating > i) { + ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.pink)); + } else { + ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.grey)); + } + } + + elements.add(Center(child: Padding( + padding: const EdgeInsets.all(10), + child:Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: ratingRowElements, + ) + ))); + + if (pitch.comment.isNotEmpty) { + elements.add(Container( + margin: const EdgeInsets.all(15.0), + padding: const EdgeInsets.all(5.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + pitch.comment, + ) + )); + } + // images + if (pitch.mediaIds.isNotEmpty) { + List imageWidgets = []; + Future> futureMediaUrls = fetchURLs(); + + imageWidgets.add( + FutureBuilder>( + future: futureMediaUrls, + builder: (context, snapshot) { + Widget skeleton = const Padding( + padding: EdgeInsets.all(5), + child: SkeletonAvatar( + style: SkeletonAvatarStyle( + shape: BoxShape.rectangle, width: 150, height: 250 + ), + ) + ); + + if (snapshot.data != null){ + List urls = snapshot.data!; + List images = []; + for (var url in urls){ + images.add( + Padding( + padding: const EdgeInsets.all(5.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.network( + url, + fit: BoxFit.fitHeight, + loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return skeleton; + }, + ) + ), + ) + ); + } + return Container( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: images + ) + ); + } + List skeletons = []; + for (var i = 0; i < pitch.mediaIds.length; i++){ + skeletons.add(skeleton); + } + return Container( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: skeletons + ) + ); + } + ) + ); + elements.add( + SizedBox( + height: 250, + child: ListView( + scrollDirection: Axis.horizontal, + children: imageWidgets + ) + ), + ); + } + + return Column( + children: elements + ); + } +} \ No newline at end of file diff --git a/src/flutter_app/lib/pages/list_page/pitch_list.dart b/src/flutter_app/lib/pages/list_page/pitch_list.dart new file mode 100644 index 0000000..09f8a3d --- /dev/null +++ b/src/flutter_app/lib/pages/list_page/pitch_list.dart @@ -0,0 +1,48 @@ +import 'package:climbing_diary/interfaces/multi_pitch_route/multi_pitch_route.dart'; +import 'package:climbing_diary/interfaces/single_pitch_route/single_pitch_route.dart'; +import 'package:flutter/material.dart'; +import '../../../services/pitch_service.dart'; +import '../../../services/route_service.dart'; +import '../../interfaces/pitch/pitch.dart'; +import 'pitch_details.dart'; + +class PitchList extends StatefulWidget { + const PitchList({super.key, required this.pitches}); + + final List pitches; + + @override + State createState() => PitchListState(); +} + +class PitchListState extends State { + final RouteService routeService = RouteService(); + final PitchService pitchService = PitchService(); + + @override + void initState(){ + super.initState(); + } + + @override + Widget build(BuildContext context) { + List pitches = widget.pitches; + pitches.sort((a, b) => a.name.compareTo(b.name)); + return Column( + children: pitches.map((pitch) => buildList(pitch)).toList(), + ); + } + + Widget buildList(Pitch pitch){ + return ExpansionTile( + title: Text( + "${pitch.name} (${pitch.num})", + ), + children: [Padding( + padding: const EdgeInsets.only(left: 20, right: 20), + child: PitchDetails( + pitch: pitch, + ))] + ); + } +} \ No newline at end of file diff --git a/src/flutter_app/lib/pages/list_page/route_list.dart b/src/flutter_app/lib/pages/list_page/route_list.dart new file mode 100644 index 0000000..33e2bdb --- /dev/null +++ b/src/flutter_app/lib/pages/list_page/route_list.dart @@ -0,0 +1,71 @@ +import 'package:climbing_diary/interfaces/multi_pitch_route/multi_pitch_route.dart'; +import 'package:climbing_diary/interfaces/single_pitch_route/single_pitch_route.dart'; +import 'package:flutter/material.dart'; +import '../../../services/pitch_service.dart'; +import '../../../services/route_service.dart'; +import 'multi_pitch_route_details.dart'; +import 'single_pitch_route_details.dart'; + +class RouteList extends StatefulWidget { + const RouteList({super.key, required this.singlePitchRoutes, required this.multiPitchRoutes}); + + final List singlePitchRoutes; + final List multiPitchRoutes; + + @override + State createState() => RouteListState(); +} + +class RouteListState extends State { + final RouteService routeService = RouteService(); + final PitchService pitchService = PitchService(); + + @override + void initState(){ + super.initState(); + } + + @override + Widget build(BuildContext context) { + List singlePitchRoutes = widget.singlePitchRoutes; + List multiPitchRoutes = widget.multiPitchRoutes; + + singlePitchRoutes.sort((a, b) => a.name.compareTo(b.name)); + multiPitchRoutes.sort((a, b) => a.name.compareTo(b.name)); + + List elements = singlePitchRoutes.map((route) => buildSinglePitchRouteList(route)).toList(); + elements += multiPitchRoutes.map((route) => buildMultiPitchRouteList(route)).toList(); + + return Column( + children: elements, + ); + } + + Widget buildSinglePitchRouteList(SinglePitchRoute route){ + return ExpansionTile( + title: Text( + route.name, + ), + children: [Padding( + padding: const EdgeInsets.only(left: 20, right: 20), + child: SinglePitchRouteDetails( + route: route, + ) + )] + ); + } + + Widget buildMultiPitchRouteList(MultiPitchRoute route){ + return ExpansionTile( + title: Text( + route.name, + ), + children: [Padding( + padding: const EdgeInsets.only(left: 20, right: 20), + child: MultiPitchRouteDetails( + route: route, + ) + )] + ); + } +} \ No newline at end of file diff --git a/src/flutter_app/lib/pages/list_page/single_pitch_route_details.dart b/src/flutter_app/lib/pages/list_page/single_pitch_route_details.dart new file mode 100644 index 0000000..bdd5a6e --- /dev/null +++ b/src/flutter_app/lib/pages/list_page/single_pitch_route_details.dart @@ -0,0 +1,220 @@ +import 'package:climbing_diary/components/add/add_ascent_to_single_pitch_route.dart'; +import 'package:climbing_diary/components/info/single_pitch_route_info.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:skeletons/skeletons.dart'; + +import '../../components/MyButtonStyles.dart'; +import '../../interfaces/single_pitch_route/single_pitch_route.dart'; +import '../../services/media_service.dart'; +import '../../services/pitch_service.dart'; +import '../../services/route_service.dart'; + +class SinglePitchRouteDetails extends StatefulWidget { + const SinglePitchRouteDetails({super.key, required this.route}); + + final SinglePitchRoute route; + @override + State createState() => _SinglePitchRouteDetailsState(); +} + +class _SinglePitchRouteDetailsState extends State{ + final MediaService mediaService = MediaService(); + final RouteService routeService = RouteService(); + final PitchService pitchService = PitchService(); + + Future> fetchURLs() { + List> futures = []; + for (var mediaId in widget.route.mediaIds) { + futures.add(mediaService.getMediumUrl(mediaId)); + } + return Future.wait(futures); + } + + XFile? image; + final ImagePicker picker = ImagePicker(); + + Future getImage(ImageSource media) async { + var img = await picker.pickImage(source: media); + if (img != null){ + var mediaId = await mediaService.uploadMedia(img); + SinglePitchRoute route = widget.route; + route.mediaIds.add(mediaId); + routeService.editSinglePitchRoute(route.toUpdateSinglePitchRoute()); + } + + setState(() { + image = img; + }); + } + + void addImageDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + title: const Text('Please choose media to select'), + content: SizedBox( + height: MediaQuery.of(context).size.height / 6, + child: Column( + children: [ + ElevatedButton( + //if user click this button, user can upload image from gallery + onPressed: () { + Navigator.pop(context); + getImage(ImageSource.gallery); + }, + child: Row( + children: const [ + Icon(Icons.image), + Text('From Gallery'), + ], + ), + ), + ElevatedButton( + //if user click this button. user can upload image from camera + onPressed: () { + Navigator.pop(context); + getImage(ImageSource.camera); + }, + child: Row( + children: const [ + Icon(Icons.camera), + Text('From Camera'), + ], + ), + ), + ], + ), + ), + ); + }); + } + + @override + void initState(){ + super.initState(); + } + + @override + Widget build(BuildContext context) { + List elements = []; + SinglePitchRoute route = widget.route; + + // general info + elements.add(SinglePitchRouteInfo( + route: route, + )); + // rating + List ratingRowElements = []; + + for (var i = 0; i < 5; i++){ + if (route.rating > i) { + ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.pink)); + } else { + ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.grey)); + } + } + + elements.add(Center(child: Padding( + padding: const EdgeInsets.only(top: 10), + child:Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: ratingRowElements, + ) + ))); + + if (route.comment.isNotEmpty) { + elements.add(Container( + margin: const EdgeInsets.all(15.0), + padding: const EdgeInsets.all(5.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + route.comment, + ) + )); + } + // images + if (route.mediaIds.isNotEmpty) { + List imageWidgets = []; + Future> futureMediaUrls = fetchURLs(); + + imageWidgets.add( + FutureBuilder>( + future: futureMediaUrls, + builder: (context, snapshot) { + Widget skeleton = const Padding( + padding: EdgeInsets.all(5), + child: SkeletonAvatar( + style: SkeletonAvatarStyle( + shape: BoxShape.rectangle, width: 150, height: 250 + ), + ) + ); + + if (snapshot.data != null){ + List urls = snapshot.data!; + List images = []; + for (var url in urls){ + images.add( + Padding( + padding: const EdgeInsets.all(5.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.network( + url, + fit: BoxFit.fitHeight, + loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return skeleton; + }, + ) + ), + ) + ); + } + return Container( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: images + ) + ); + } + List skeletons = []; + for (var i = 0; i < route.mediaIds.length; i++){ + skeletons.add(skeleton); + } + return Container( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: skeletons + ) + ); + } + ) + ); + elements.add( + SizedBox( + height: 250, + child: ListView( + scrollDirection: Axis.horizontal, + children: imageWidgets + ) + ), + ); + } + + return Column( + children: elements + ); + } +} \ No newline at end of file diff --git a/src/flutter_app/lib/pages/list_page/spot_details.dart b/src/flutter_app/lib/pages/list_page/spot_details.dart new file mode 100644 index 0000000..5c7f3b2 --- /dev/null +++ b/src/flutter_app/lib/pages/list_page/spot_details.dart @@ -0,0 +1,261 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:skeletons/skeletons.dart'; + +import '../../components/MyButtonStyles.dart'; +import '../../components/edit/edit_spot.dart'; +import '../../interfaces/spot/spot.dart'; +import '../../interfaces/trip/trip.dart'; +import '../../services/media_service.dart'; +import '../../services/spot_service.dart'; + +class SpotDetails extends StatefulWidget { + const SpotDetails({super.key, required this.spot}); + + final Spot spot; + + @override + State createState() => _SpotDetailsState(); +} + +class _SpotDetailsState extends State{ + final MediaService mediaService = MediaService(); + final SpotService spotService = SpotService(); + + Future> fetchURLs() { + List> futures = []; + for (var mediaId in widget.spot.mediaIds) { + futures.add(mediaService.getMediumUrl(mediaId)); + } + return Future.wait(futures); + } + + XFile? image; + final ImagePicker picker = ImagePicker(); + + Future getImage(ImageSource media) async { + var img = await picker.pickImage(source: media); + if (img != null){ + var mediaId = await mediaService.uploadMedia(img); + Spot spot = widget.spot; + spot.mediaIds.add(mediaId); + spotService.editSpot(spot.toUpdateSpot()); + } + + setState(() { + image = img; + }); + } + + void addImageDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + title: const Text('Please choose media to select'), + content: SizedBox( + height: MediaQuery.of(context).size.height / 6, + child: Column( + children: [ + ElevatedButton( + //if user click this button, user can upload image from gallery + onPressed: () { + Navigator.pop(context); + getImage(ImageSource.gallery); + }, + child: Row( + children: const [ + Icon(Icons.image), + Text('From Gallery'), + ], + ), + ), + ElevatedButton( + //if user click this button. user can upload image from camera + onPressed: () { + Navigator.pop(context); + getImage(ImageSource.camera); + }, + child: Row( + children: const [ + Icon(Icons.camera), + Text('From Camera'), + ], + ), + ), + ], + ), + ), + ); + }); + } + + @override + void initState(){ + super.initState(); + } + + @override + Widget build(BuildContext context) { + List elements = []; + + // general info + elements.addAll([ + Text( + widget.spot.name, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600 + ), + ), + Text( + '${round(widget.spot.coordinates[0], decimals: 8)}, ${round(widget.spot.coordinates[1], decimals: 8)}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400 + ), + ), + Text( + widget.spot.location, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400 + ), + )]); + // rating + List ratingRowElements = []; + + for (var i = 0; i < 5; i++){ + if (widget.spot.rating > i) { + ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.pink)); + } else { + ratingRowElements.add(const Icon(Icons.favorite, size: 30.0, color: Colors.grey)); + } + } + + elements.add(Center(child: Padding( + padding: const EdgeInsets.all(10), + child:Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: ratingRowElements, + ) + ))); + + // time to walk transport + elements.add(Center(child: Padding( + padding: const EdgeInsets.all(5), + child:Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Icon(Icons.train, size: 30.0, color: Colors.green), + Text( + '${widget.spot.distancePublicTransport} min', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400 + ), + ), + const Icon(Icons.directions_car, size: 30.0, color: Colors.red), + Text( + '${widget.spot.distanceParking} min', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400 + ), + ) + ], + ) + ))); + + if (widget.spot.comment.isNotEmpty) { + elements.add(Container( + margin: const EdgeInsets.all(15.0), + padding: const EdgeInsets.all(5.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + widget.spot.comment, + ) + )); + } + // images + if (widget.spot.mediaIds.isNotEmpty) { + List imageWidgets = []; + Future> futureMediaUrls = fetchURLs(); + + imageWidgets.add( + FutureBuilder>( + future: futureMediaUrls, + builder: (context, snapshot) { + Widget skeleton = const Padding( + padding: EdgeInsets.all(5), + child: SkeletonAvatar( + style: SkeletonAvatarStyle( + shape: BoxShape.rectangle, width: 150, height: 250 + ), + ) + ); + + if (snapshot.data != null){ + List urls = snapshot.data!; + List images = []; + for (var url in urls){ + images.add( + Padding( + padding: const EdgeInsets.all(5.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.network( + url, + fit: BoxFit.fitHeight, + loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return skeleton; + }, + ) + ), + ) + ); + } + return Container( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: images + ) + ); + } + List skeletons = []; + for (var i = 0; i < widget.spot.mediaIds.length; i++){ + skeletons.add(skeleton); + } + return Container( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: skeletons + ) + ); + } + ) + ); + elements.add( + SizedBox( + height: 250, + child: ListView( + scrollDirection: Axis.horizontal, + children: imageWidgets + ) + ), + ); + } + return Column(children: elements); + } +} \ No newline at end of file diff --git a/src/flutter_app/lib/pages/list_page/spot_list.dart b/src/flutter_app/lib/pages/list_page/spot_list.dart new file mode 100644 index 0000000..212cabf --- /dev/null +++ b/src/flutter_app/lib/pages/list_page/spot_list.dart @@ -0,0 +1,46 @@ +import 'spot_details.dart'; +import 'package:flutter/material.dart'; +import 'package:internet_connection_checker/internet_connection_checker.dart'; + +import '../../../interfaces/spot/spot.dart'; +import '../../../services/spot_service.dart'; + +class SpotList extends StatefulWidget { + const SpotList({super.key, required this.spots}); + + final List spots; + + @override + State createState() => SpotListState(); +} + +class SpotListState extends State { + final SpotService spotService = SpotService(); + + @override + void initState(){ + super.initState(); + } + + @override + Widget build(BuildContext context) { + List spots = widget.spots; + spots.sort((a, b) => a.name.compareTo(b.name)); + return Column( + children: spots.map((spot) => buildList(spot)).toList(), + ); + } + + Widget buildList(Spot spot){ + return ExpansionTile( + title: Text( + spot.name, + ), + children: [Padding( + padding: const EdgeInsets.only(left: 20, right: 20), + child: SpotDetails( + spot: spot, + ))] + ); + } +} \ No newline at end of file diff --git a/src/flutter_app/lib/components/map_page/add_spot.dart b/src/flutter_app/lib/pages/map_page/add_spot.dart similarity index 60% rename from src/flutter_app/lib/components/map_page/add_spot.dart rename to src/flutter_app/lib/pages/map_page/add_spot.dart index f07ef95..58c5d73 100644 --- a/src/flutter_app/lib/components/map_page/add_spot.dart +++ b/src/flutter_app/lib/pages/map_page/add_spot.dart @@ -1,7 +1,7 @@ +import 'package:climbing_diary/components/MyTextStyles.dart'; import 'package:climbing_diary/interfaces/trip/update_trip.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:latlong2/latlong.dart'; import '../../interfaces/spot/create_spot.dart'; import '../../interfaces/spot/spot.dart'; @@ -10,14 +10,13 @@ import '../../services/spot_service.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; import '../../services/trip_service.dart'; +import 'navigation_screen_page.dart'; class AddSpot extends StatefulWidget { - const AddSpot({super.key, required this.trips, required this.coordinates, required this.address, required this.onAdd}); + const AddSpot({super.key, this.trip, required this.onAdd}); - final LatLng coordinates; - final String address; final ValueSetter onAdd; - final List trips; + final Trip? trip; @override State createState() => _AddSpotState(); @@ -37,7 +36,6 @@ class _AddSpotState extends State{ final TextEditingController controllerCar = TextEditingController(); double currentSliderValue = 0; - Trip? dropdownValue; @override void initState(){ @@ -47,12 +45,17 @@ class _AddSpotState extends State{ @override Widget build(BuildContext context) { - controllerAddress.text = widget.address; - controllerLat.text = widget.coordinates.latitude.toString(); - controllerLong.text = widget.coordinates.longitude.toString(); + // controllerLat.text = widget.coordinates.latitude.toString(); + // controllerLong.text = widget.coordinates.longitude.toString(); List elements = []; + if (widget.trip != null){ + elements.add(Text( + widget.trip!.name, + style: MyTextStyles.title, + )); + } elements.add( TextFormField( validator: (value) { @@ -65,20 +68,24 @@ class _AddSpotState extends State{ hintText: "name", labelText: "name"), ) ); - elements.add(DropdownButton( - isExpanded: true, - value: dropdownValue, - items: widget.trips.map>((Trip trip) { - return DropdownMenuItem( - value: trip, - child: Text(trip.name), - ); - }).toList(), - onChanged: (Trip? trip) { - setState(() { - dropdownValue = trip!; - }); - } + elements.add(TextButton.icon( + icon: const Icon(Icons.place, size: 30.0), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NavigationScreenPage( + onAdd: (List results) { + controllerAddress.text = results[0]; + controllerLat.text = results[1]; + controllerLong.text = results[2]; + setState(() {}); + } + ), + ) + ); + }, + label: const Text("select location"), )); elements.add(TextFormField( controller: controllerAddress, @@ -132,6 +139,7 @@ class _AddSpotState extends State{ } return null; }, + keyboardType: TextInputType.number, controller: controllerBus, decoration: const InputDecoration( hintText: "in minutes", @@ -148,6 +156,7 @@ class _AddSpotState extends State{ } return null; }, + keyboardType: TextInputType.number, controller: controllerCar, decoration: const InputDecoration( hintText: "in minutes", @@ -161,41 +170,44 @@ class _AddSpotState extends State{ ), title: const Text('Add a new spot'), content: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: elements, - ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: elements, ), + ), ), actions: [ TextButton( - onPressed: () async { - bool result = await InternetConnectionChecker().hasConnection; - if (_formKey.currentState!.validate()) { - var valDistanceParking = int.tryParse(controllerCar.text); - var valDistancePublicTransport = int.tryParse(controllerBus.text); - CreateSpot spot = CreateSpot( - comment: controllerComment.text, - coordinates: [double.parse(controllerLat.text), double.parse(controllerLong.text)], - distanceParking: (valDistanceParking != null) ? valDistanceParking : 0, - distancePublicTransport: (valDistancePublicTransport != null) ? valDistancePublicTransport : 0, - location: controllerAddress.text, - name: controllerTitle.text, - rating: currentSliderValue.toInt(), - ); - Navigator.popUntil(context, ModalRoute.withName('/')); - Spot? createdSpot = await spotService.createSpot(spot, result); - UpdateTrip editTrip = dropdownValue!.toUpdateTrip(); - if (createdSpot != null) { - editTrip.spotIds?.add(createdSpot.id); - Trip? editedTrip = await tripService.editTrip(editTrip); - widget.onAdd.call(createdSpot); + onPressed: () async { + bool result = await InternetConnectionChecker().hasConnection; + if (_formKey.currentState!.validate()) { + var valDistanceParking = int.tryParse(controllerCar.text); + var valDistancePublicTransport = int.tryParse(controllerBus.text); + CreateSpot spot = CreateSpot( + comment: controllerComment.text, + coordinates: [double.parse(controllerLat.text), double.parse(controllerLong.text)], + distanceParking: (valDistanceParking != null) ? valDistanceParking : 0, + distancePublicTransport: (valDistancePublicTransport != null) ? valDistancePublicTransport : 0, + location: controllerAddress.text, + name: controllerTitle.text, + rating: currentSliderValue.toInt(), + ); + Navigator.popUntil(context, ModalRoute.withName('/')); + Spot? createdSpot = await spotService.createSpot(spot, result); + if (createdSpot != null) { + if (widget.trip != null) { + Trip trip = widget.trip!; + UpdateTrip editTrip = trip.toUpdateTrip(); + editTrip.spotIds?.add(createdSpot.id); + Trip? editedTrip = await tripService.editTrip(editTrip); + } + widget.onAdd.call(createdSpot); + } } - } - }, - child: const Icon(Icons.save)) + }, + child: const Icon(Icons.save)) ], ); } diff --git a/src/flutter_app/lib/components/map_page/map_page.dart b/src/flutter_app/lib/pages/map_page/map_page.dart similarity index 80% rename from src/flutter_app/lib/components/map_page/map_page.dart rename to src/flutter_app/lib/pages/map_page/map_page.dart index d0e2a4d..bab1eeb 100644 --- a/src/flutter_app/lib/components/map_page/map_page.dart +++ b/src/flutter_app/lib/pages/map_page/map_page.dart @@ -1,9 +1,9 @@ import 'package:climbing_diary/pages/save_location_no_connection.dart'; import 'package:flutter/material.dart'; -import 'details/spot_details.dart'; +import 'add_spot.dart'; +import 'spot_details.dart'; import '../../services/cache.dart'; -import 'navigation_screen_page.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; @@ -94,25 +94,17 @@ class _MapPageState extends State { ], ) ), - floatingActionButton: IconButton( - icon: const Icon(Icons.add, size: 50.0), + floatingActionButton: FloatingActionButton( onPressed: () { - if (online) { - Navigator.push( - context, - MaterialPageRoute( - builder: ( - context) => NavigationScreenPage(onAdd: addSpotCallback)), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: ( - context) => SaveLocationNoConnectionPage(onAdd: addSpotCallback)), - ); - } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddSpot(onAdd: (spot) => addSpotCallback(spot))), + ); }, + backgroundColor: Colors.green, + elevation: 5, + child: const Icon(Icons.add, size: 50.0), ) ); } @@ -141,25 +133,18 @@ class _MapPageState extends State { ], ) ), - floatingActionButton: IconButton( - icon: const Icon(Icons.add, size: 50.0), + floatingActionButton: FloatingActionButton( onPressed: () { - if (online) { - Navigator.push( - context, - MaterialPageRoute( - builder: ( - context) => NavigationScreenPage(onAdd: addSpotCallback)), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: ( - context) => SaveLocationNoConnectionPage(onAdd: addSpotCallback)), - ); - } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddSpot(onAdd: (spot) => addSpotCallback(spot)), + ) + ); }, + backgroundColor: Colors.green, + elevation: 5, + child: const Icon(Icons.add, size: 50.0, color: Colors.white,), ) ); } else if (snapshot.hasError) { @@ -182,6 +167,8 @@ class _MapPageState extends State { builder: (context) => SaveLocationNoConnectionPage(onAdd: (Spot value) {})), ); }, + backgroundColor: Colors.green, + elevation: 5, child: const Icon(Icons.add), ), ); diff --git a/src/flutter_app/lib/pages/map_page/navigation_screen_page.dart b/src/flutter_app/lib/pages/map_page/navigation_screen_page.dart new file mode 100644 index 0000000..3461ca0 --- /dev/null +++ b/src/flutter_app/lib/pages/map_page/navigation_screen_page.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:open_street_map_search_and_pick/open_street_map_search_and_pick.dart'; + +import '../../interfaces/trip/trip.dart'; +import '../../services/location_service.dart'; +import '../../services/trip_service.dart'; + +class NavigationScreenPage extends StatefulWidget { + const NavigationScreenPage({super.key, required this.onAdd}); + + final ValueSetter> onAdd; + + @override + State createState() => _NavigationScreenPage(); +} + +class _NavigationScreenPage extends State { + final TripService tripService = TripService(); + final LocationService locationService = LocationService(); + String address = ""; + List values = [1, 2, 3, 4, 5]; + double currentSliderValue = 1; + + @override + initState(){ + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: Future.wait([locationService.getPosition(), tripService.getTrips()]), + builder: (context, snapshot) { + if (snapshot.hasData) { + Position position = snapshot.data![0]; + List trips = snapshot.data![1]; + return Scaffold( + body: OpenStreetMapSearchAndPick( + center: LatLong(position.latitude, position.longitude), + buttonColor: Theme.of(context).colorScheme.primary, + buttonText: 'Set location', + onPicked: (pickedData) { + setState(() { + address = pickedData.address; + widget.onAdd.call([address.toString(), pickedData.latLong.latitude.toString(), pickedData.latLong.longitude.toString()]); + Navigator.pop(context); + }); + } + ) + ); + } else if (snapshot.hasError) { + return Text('${snapshot.error}'); + } + return Scaffold( + body: Center ( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + Padding( + padding: EdgeInsets.all(50), + child: SizedBox( + height: 100.0, + width: 100.0, + child: CircularProgressIndicator(), + ), + ) + ], + ) + ) + ); + } + ); + } +} diff --git a/src/flutter_app/lib/pages/map_page/route_list.dart b/src/flutter_app/lib/pages/map_page/route_list.dart new file mode 100644 index 0000000..a10b934 --- /dev/null +++ b/src/flutter_app/lib/pages/map_page/route_list.dart @@ -0,0 +1,269 @@ +import 'package:climbing_diary/interfaces/multi_pitch_route/multi_pitch_route.dart'; +import 'package:climbing_diary/interfaces/single_pitch_route/single_pitch_route.dart'; +import 'package:flutter/material.dart'; +import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:timelines/timelines.dart'; + +import '../../../interfaces/spot/spot.dart'; +import '../../../interfaces/trip/trip.dart'; +import '../../../services/pitch_service.dart'; +import '../../../services/route_service.dart'; +import '../../components/detail/multi_pitch_route_details.dart'; +import '../../components/detail/single_pitch_route_details.dart'; +import '../../components/info/multi_pitch_route_info.dart'; +import '../../components/info/route_info.dart'; +import '../../components/info/single_pitch_route_info.dart'; +import '../diary_page/image_list_view.dart'; +import '../diary_page/rating_row.dart'; +import '../diary_page/timeline/pitch_timeline.dart'; + +class RouteList extends StatefulWidget { + const RouteList({super.key, this.trip, required this.spot, required this.singlePitchRouteIds, required this.multiPitchRouteIds}); + + final Trip? trip; + final Spot spot; + final List singlePitchRouteIds, multiPitchRouteIds; + + @override + State createState() => RouteListState(); +} + +class RouteListState extends State { + final RouteService routeService = RouteService(); + final PitchService pitchService = PitchService(); + + @override + void initState(){ + super.initState(); + checkConnection(); + } + + @override + Widget build(BuildContext context) { + List singlePitchRouteIds = widget.singlePitchRouteIds; + List multiPitchRouteIds = widget.multiPitchRouteIds; + return FutureBuilder( + future: checkConnection(), + builder: (context, snapshot) { + if (snapshot.hasData) { + var online = snapshot.data!; + if (online) { + List elements = []; + return FutureBuilder>( + future: Future.wait(multiPitchRouteIds.map((routeId) => routeService.getMultiPitchRoute(routeId))), + builder: (context, snapshot) { + if (snapshot.hasData) { + List multiPitchRoutes = snapshot.data!.whereType().toList(); + multiPitchRoutes.sort((a, b) => a.name.compareTo(b.name)); + + updateMultiPitchRouteCallback(MultiPitchRoute route) { + var index = -1; + for (int i = 0; i < multiPitchRoutes.length; i++) { + if (multiPitchRoutes[i].id == route.id) { + index = i; + } + } + multiPitchRoutes.removeAt(index); + multiPitchRoutes.add(route); + setState(() {}); + } + + deleteMultiPitchRouteCallback(MultiPitchRoute route) { + multiPitchRoutes.remove(route); + setState(() {}); + } + if (multiPitchRoutes.isNotEmpty){ + elements.add( + FixedTimeline.tileBuilder( + theme: TimelineThemeData( + nodePosition: 0, + color: const Color(0xff989898), + indicatorTheme: const IndicatorThemeData( + position: 0, + size: 20.0, + ), + connectorTheme: const ConnectorThemeData( + thickness: 2.5, + ), + ), + builder: TimelineTileBuilder.connected( + connectionDirection: ConnectionDirection.before, + itemCount: multiPitchRoutes.length, + contentsBuilder: (_, index) { + List timeLineElements = []; + // route info + MultiPitchRoute multiPitchRoute = multiPitchRoutes[index]; + timeLineElements.add(RouteInfo(route: multiPitchRoute)); + // rating as hearts in a row + timeLineElements.add(RatingRow(rating: multiPitchRoute.rating)); + // images list view + if (multiPitchRoute.mediaIds.isNotEmpty) { + timeLineElements.add( + ImageListView(mediaIds: multiPitchRoute.mediaIds) + ); + } + // pitches + if (multiPitchRoute.pitchIds.isNotEmpty) { + // multi pitch + timeLineElements.add( + MultiPitchInfo( + pitchIds: multiPitchRoute.pitchIds + ) + ); + timeLineElements.add( + PitchTimeline( + trip: widget.trip, + spot: widget.spot, + route: multiPitchRoute, + pitchIds: multiPitchRoute.pitchIds + ) + ); + } + return InkWell( + onTap: () => showDialog( + context: context, + builder: (BuildContext context) => + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: MultiPitchRouteDetails( + spot: widget.spot, + route: multiPitchRoute, + onDelete: deleteMultiPitchRouteCallback, + onUpdate: updateMultiPitchRouteCallback, + spotId: widget.spot.id + ) + ) + ), + child: Ink( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: timeLineElements, + ), + ) + ), + ); + }, + indicatorBuilder: (_, index) { + return const OutlinedDotIndicator( + borderWidth: 2.5, + color: Color(0xff66c97f), + ); + }, + connectorBuilder: (_, index, ___) => + const SolidLineConnector(color: Color(0xff66c97f)), + ), + ), + ); + } + return FutureBuilder>( + future: Future.wait(singlePitchRouteIds.map((routeId) => routeService.getSinglePitchRoute(routeId))), + builder: (context, snapshot) { + if (snapshot.hasData) { + List singlePitchRoutes = snapshot.data!.whereType().toList(); + singlePitchRoutes.sort((a, b) => a.name.compareTo(b.name)); + + if (singlePitchRoutes.isNotEmpty){ + elements.add( + FixedTimeline.tileBuilder( + theme: TimelineThemeData( + nodePosition: 0, + color: const Color(0xff989898), + indicatorTheme: const IndicatorThemeData( + position: 0, + size: 20.0, + ), + connectorTheme: const ConnectorThemeData( + thickness: 2.5, + ), + ), + builder: TimelineTileBuilder.connected( + connectionDirection: ConnectionDirection.before, + itemCount: singlePitchRoutes.length, + contentsBuilder: (_, index) { + List elements = []; + // route info + SinglePitchRoute singlePitchRoute = singlePitchRoutes[index]; + elements.add(SinglePitchRouteInfo( + route: singlePitchRoute + )); + // rating as hearts in a row + elements.add(RatingRow(rating: singlePitchRoute.rating)); + // images list view + if (singlePitchRoute.mediaIds.isNotEmpty) { + elements.add( + ImageListView(mediaIds: singlePitchRoute.mediaIds) + ); + } + + return InkWell( + onTap: () => showDialog( + context: context, + builder: (BuildContext context) => + Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SinglePitchRouteDetails( + spot: widget.spot, + route: singlePitchRoute, + onDelete: (SinglePitchRoute sPR) => {}, + onUpdate: (SinglePitchRoute sPR) => {}, + spotId: widget.spot.id) + ) + ), + child: Ink( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: elements, + ), + ) + ), + ); + }, + indicatorBuilder: (_, index) { + return const OutlinedDotIndicator( + borderWidth: 2.5, + color: Color(0xff66c97f), + ); + }, + connectorBuilder: (_, index, ___) => + const SolidLineConnector(color: Color(0xff66c97f)), + ), + ) + ); + } + return Column( + children: elements, + ); + } else { + return const CircularProgressIndicator(); + } + } + ); + } else { + return const CircularProgressIndicator(); + } + } + ); + } else { + return const CircularProgressIndicator(); + } + } else { + return const CircularProgressIndicator(); + } + } + ); + } + + Future checkConnection() async { + return await InternetConnectionChecker().hasConnection; + } +} \ No newline at end of file diff --git a/src/flutter_app/lib/components/map_page/save_location_no_connection.dart b/src/flutter_app/lib/pages/map_page/save_location_no_connection.dart similarity index 93% rename from src/flutter_app/lib/components/map_page/save_location_no_connection.dart rename to src/flutter_app/lib/pages/map_page/save_location_no_connection.dart index ff75e25..61ecdaa 100644 --- a/src/flutter_app/lib/components/map_page/save_location_no_connection.dart +++ b/src/flutter_app/lib/pages/map_page/save_location_no_connection.dart @@ -35,9 +35,6 @@ class _SaveLocationNoConnectionPage extends State Position position = snapshot.data!; return Scaffold( body: AddSpot( - trips: [], - coordinates: LatLng(position.latitude, position.longitude), - address: " ", onAdd: widget.onAdd ) ); diff --git a/src/flutter_app/lib/components/map_page/details/spot_details.dart b/src/flutter_app/lib/pages/map_page/spot_details.dart similarity index 94% rename from src/flutter_app/lib/components/map_page/details/spot_details.dart rename to src/flutter_app/lib/pages/map_page/spot_details.dart index 58b0fbc..0f95e8c 100644 --- a/src/flutter_app/lib/components/map_page/details/spot_details.dart +++ b/src/flutter_app/lib/pages/map_page/spot_details.dart @@ -3,15 +3,14 @@ import 'package:image_picker/image_picker.dart'; import 'package:latlong2/latlong.dart'; import 'package:skeletons/skeletons.dart'; -import '../../../interfaces/spot/spot.dart'; -import '../../../interfaces/trip/trip.dart'; -import '../../../services/media_service.dart'; -import '../../../services/spot_service.dart'; -import '../../MyButtonStyles.dart'; -import '../../add/add_route.dart'; -import '../../diary_page/timeline/route_timeline.dart'; -import '../../edit/edit_spot.dart'; -import '../../select/select_route.dart'; +import '../../components/MyButtonStyles.dart'; +import '../../components/add/add_route.dart'; +import '../../components/edit/edit_spot.dart'; +import '../../interfaces/spot/spot.dart'; +import '../../interfaces/trip/trip.dart'; +import '../../services/media_service.dart'; +import '../../services/spot_service.dart'; +import 'route_list.dart'; class SpotDetails extends StatefulWidget { const SpotDetails({super.key, this.trip, required this.spot, required this.onDelete, required this.onUpdate }); @@ -296,11 +295,15 @@ class _SpotDetailsState extends State{ context, MaterialPageRoute( builder: (context) => AddRoute( - spots: [widget.spot], - onAdd: (route) { + spot: widget.spot, + onAddMultiPitchRoute: (route) { widget.spot.multiPitchRouteIds.add(route.id); setState(() {}); }, + onAddSinglePitchRoute: (route) { + widget.spot.singlePitchRouteIds.add(route.id); + setState(() {}); + }, ), ) ); @@ -334,16 +337,13 @@ class _SpotDetailsState extends State{ ) ); // routes - // routes if (widget.spot.multiPitchRouteIds.isNotEmpty || widget.spot.singlePitchRouteIds.isNotEmpty){ elements.add( - RouteTimeline( + RouteList( trip: widget.trip, spot: widget.spot, singlePitchRouteIds: widget.spot.singlePitchRouteIds, multiPitchRouteIds: widget.spot.multiPitchRouteIds, - startDate: DateTime(1923), - endDate: DateTime(2123), ) ); } diff --git a/src/flutter_app/lib/pages/navigation_screen_page.dart b/src/flutter_app/lib/pages/navigation_screen_page.dart index 14cc686..62c8bf2 100644 --- a/src/flutter_app/lib/pages/navigation_screen_page.dart +++ b/src/flutter_app/lib/pages/navigation_screen_page.dart @@ -12,7 +12,7 @@ import '../services/trip_service.dart'; class NavigationScreenPage extends StatefulWidget { const NavigationScreenPage({super.key, required this.onAdd}); - final ValueSetter onAdd; + final ValueSetter> onAdd; @override State createState() => _NavigationScreenPage(); @@ -47,19 +47,6 @@ class _NavigationScreenPage extends State { setState(() { address = pickedData.address; }); - showDialog( - context: context, - builder: (BuildContext context) => - AddSpot( - trips: trips, - coordinates: LatLng( - pickedData.latLong.latitude, - pickedData.latLong.longitude - ), - address: address, - onAdd: widget.onAdd, - ) - ); } ) ); diff --git a/src/flutter_app/lib/pages/save_location_no_connection.dart b/src/flutter_app/lib/pages/save_location_no_connection.dart index 6a0ecea..529520d 100644 --- a/src/flutter_app/lib/pages/save_location_no_connection.dart +++ b/src/flutter_app/lib/pages/save_location_no_connection.dart @@ -34,9 +34,6 @@ class _SaveLocationNoConnectionPage extends State Position position = snapshot.data!; return Scaffold( body: AddSpot( - trips: [], - coordinates: LatLng(position.latitude, position.longitude), - address: " ", onAdd: widget.onAdd ) ); diff --git a/src/flutter_app/lib/pages/statistic_page.dart b/src/flutter_app/lib/pages/statistic_page/statistic_page.dart similarity index 95% rename from src/flutter_app/lib/pages/statistic_page.dart rename to src/flutter_app/lib/pages/statistic_page/statistic_page.dart index a6e6c8a..1308eba 100644 --- a/src/flutter_app/lib/pages/statistic_page.dart +++ b/src/flutter_app/lib/pages/statistic_page/statistic_page.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:overlay_support/overlay_support.dart'; import 'package:flutter_heatmap_calendar/flutter_heatmap_calendar.dart'; -import '../interfaces/ascent/ascent.dart'; -import '../services/ascent_service.dart'; +import '../../interfaces/ascent/ascent.dart'; +import '../../services/ascent_service.dart'; class StatisticPage extends StatefulWidget { const StatisticPage({super.key}); diff --git a/src/flutter_app/lib/services/pitch_service.dart b/src/flutter_app/lib/services/pitch_service.dart index 5727dca..3557380 100644 --- a/src/flutter_app/lib/services/pitch_service.dart +++ b/src/flutter_app/lib/services/pitch_service.dart @@ -60,6 +60,37 @@ class PitchService { } } + Future> getPitchesByName(String name) async { + if (name == ""){ + return []; + } + try { + final Response response = await netWorkLocator.dio.get('$climbingApiHost/pitch'); + + if (response.statusCode == 200) { + // If the server did return a 200 OK response, then parse the JSON. + List pitches = []; + response.data.forEach((s) { + Pitch pitch = Pitch.fromJson(s); + if (pitch.name.contains(name)) { + pitches.add(pitch); + } + }); + return pitches; + } + } catch (e) { + if (e is DioError) { + if (e.error.toString().contains("OS Error: Connection refused, errno = 111")){ + showSimpleNotification( + const Text('Couldn\'t connect to API'), + background: Colors.red, + ); + } + } + } + return []; + } + Future createPitch(CreatePitch createPitch, String routeId, bool hasConnection) async { CreatePitch pitch = CreatePitch( comment: (createPitch.comment != null) ? createPitch.comment! : "", diff --git a/src/flutter_app/lib/services/route_service.dart b/src/flutter_app/lib/services/route_service.dart index 3ed7dd3..9050ef7 100644 --- a/src/flutter_app/lib/services/route_service.dart +++ b/src/flutter_app/lib/services/route_service.dart @@ -27,153 +27,6 @@ class RouteService { final String climbingApiHost = Environment().config.climbingApiHost; final String mediaApiHost = Environment().config.mediaApiHost; - Future> getRoutes() async { - try { - final Response response = await netWorkLocator.dio.get('$climbingApiHost/route'); - - if (response.statusCode == 200) { - // If the server did return a 200 OK response, then parse the JSON. - List routes = []; - // save to cache - Box box = Hive.box('routes'); - response.data.forEach((s) { - ClimbingRoute route = ClimbingRoute.fromJson(s); - if (!box.containsKey(route.id)) { - box.put(route.id, route.toJson()); - } - routes.add(route); - }); - return routes; - } - } catch (e) { - if (e is DioError) { - if (e.error.toString().contains("OS Error: Connection refused, errno = 111")){ - showSimpleNotification( - const Text('Couldn\'t connect to API'), - background: Colors.red, - ); - } - } else { - print(e); - } - } - return []; - } - - Future getRoute(String routeId) async { - try{ - return getMultiPitchRoute(routeId); - } catch(e) { - return getSinglePitchRoute(routeId); - } - } - - Future createRoute(CreateClimbingRoute createRoute, String spotId, bool hasConnection) async { - CreateClimbingRoute route = CreateClimbingRoute( - comment: (createRoute.comment != null) ? createRoute.comment! : "", - location: createRoute.location, - name: createRoute.name, - rating: createRoute.rating, - ); - if (hasConnection) { - var data = route.toJson(); - return uploadRoute(spotId, data); - } else { - // save to cache - Box box = Hive.box('upload_later_routes'); - Map routeJson = route.toJson(); - box.put(routeJson.hashCode, routeJson); - } - return null; - } - - Future editRoute(UpdateClimbingRoute route) async { - try { - final Response response = await netWorkLocator.dio - .put('$climbingApiHost/route/${route.id}', data: route.toJson()); - if (response.statusCode == 200) { - // TODO deleteRouteFromEditQueue(route.hashCode); - return ClimbingRoute.fromJson(response.data); - } else { - throw Exception('Failed to edit route'); - } - } catch (e) { - if (e is DioError) { - if (e.error.toString().contains('OS Error: No address associated with hostname, errno = 7')){ - // this means we are offline so queue this route and edit later - Box box = Hive.box('edit_later_routes'); - Map routeJson = route.toJson(); - box.put(routeJson.hashCode, routeJson); - } - } - } finally { - // TODO editRouteFromCache(route); - } - return null; - } - - Future deleteRoute(ClimbingRoute route, String spotId) async { - try { - for (var id in route.mediaIds) { - final Response mediaResponse = - await netWorkLocator.dio.delete('$mediaApiHost/media/$id'); - if (mediaResponse.statusCode != 204) { - throw Exception('Failed to delete medium'); - } - } - - final Response routeResponse = - await netWorkLocator.dio.delete('$climbingApiHost/route/${route.id}/spot/$spotId'); - if (routeResponse.statusCode != 204) { - throw Exception('Failed to delete route'); - } - // TODO deleteRouteFromDeleteQueue(route.toJson().hashCode); - return routeResponse.data; - } catch (e) { - if (e is DioError) { - if (e.error.toString().contains('OS Error: No address associated with hostname, errno = 7')){ - // this means we are offline so queue this route and delete later - Box box = Hive.box('delete_later_routes'); - Map routeJson = route.toJson(); - box.put(routeJson.hashCode, routeJson); - } - } - } finally { - // TODO deleteRouteFromCache(route.id); - } - } - - Future uploadRoute(String spotId, Map data) async { - try { - final Response response = await netWorkLocator.dio - .post('$climbingApiHost/route/spot/$spotId', data: data); - if (response.statusCode == 201) { - return ClimbingRoute.fromJson(response.data); - } else { - throw Exception('Failed to create route'); - } - } catch (e) { - if (e is DioError) { - final response = e.response; - if (response != null) { - switch (response.statusCode) { - case 409: - showSimpleNotification( - const Text('This route already exists!'), - background: Colors.red, - ); - break; - default: - throw Exception('Failed to create route'); - } - } - } - } finally { - // TODO deleteRouteFromUploadQueue(data.hashCode); - } - return null; - } - Future> getMultiPitchRoutes() async { try { final Response response = await netWorkLocator.dio.get('$climbingApiHost/multi_pitch_route'); @@ -217,6 +70,25 @@ class RouteService { } } + Future> getMultiPitchRoutesByName(String name) async { + if (name == ""){ + return []; + } + final Response response = await netWorkLocator.dio.get('$climbingApiHost/multi_pitch_route'); + if (response.statusCode == 200) { + List routes = []; + response.data.forEach((r) { + MultiPitchRoute route = MultiPitchRoute.fromJson(r); + if (route.name.contains(name)) { + routes.add(route); + } + }); + return routes; + } else { + throw Exception('Failed to load route'); + } + } + Future getMultiPitchRouteIfWithinDateRange(String routeId, DateTime startDate, DateTime endDate) async { final Response response = await netWorkLocator.dio.get('$climbingApiHost/multi_pitch_route/$routeId'); if (response.statusCode == 200) { @@ -438,6 +310,25 @@ class RouteService { } } + Future> getSinglePitchRoutesByName(String name) async { + if (name == ""){ + return []; + } + final Response response = await netWorkLocator.dio.get('$climbingApiHost/single_pitch_route'); + if (response.statusCode == 200) { + List routes = []; + response.data.forEach((r) { + SinglePitchRoute route = SinglePitchRoute.fromJson(r); + if (route.name.contains(name)) { + routes.add(route); + } + }); + return routes; + } else { + throw Exception('Failed to load route'); + } + } + Future getSinglePitchRouteIfWithinDateRange(String routeId, DateTime startDate, DateTime endDate) async { final Response response = await netWorkLocator.dio.get('$climbingApiHost/single_pitch_route/$routeId'); if (response.statusCode == 200) { diff --git a/src/flutter_app/lib/services/spot_service.dart b/src/flutter_app/lib/services/spot_service.dart index fae5dda..683ea35 100644 --- a/src/flutter_app/lib/services/spot_service.dart +++ b/src/flutter_app/lib/services/spot_service.dart @@ -3,6 +3,10 @@ import 'package:hive/hive.dart'; import 'package:overlay_support/overlay_support.dart'; import '../config/environment.dart'; +import '../interfaces/ascent/ascent.dart'; +import '../interfaces/multi_pitch_route/multi_pitch_route.dart'; +import '../interfaces/pitch/pitch.dart'; +import '../interfaces/single_pitch_route/single_pitch_route.dart'; import '../interfaces/spot/create_spot.dart'; import 'package:dio/dio.dart'; @@ -51,6 +55,9 @@ class SpotService { } Future> getSpotsByName(String name) async { + if (name == ""){ + return []; + } try { final Response response = await netWorkLocator.dio.get('$climbingApiHost/spot'); @@ -88,6 +95,47 @@ class SpotService { } } + Future getSpotIfWithinDateRange(String spotId, DateTime startDate, DateTime endDate) async { + final Response spotResponse = + await netWorkLocator.dio.get('$climbingApiHost/spot/$spotId'); + if (spotResponse.statusCode == 200) { + Spot spot = Spot.fromJson(spotResponse.data); + for (String routeId in spot.multiPitchRouteIds){ + final Response multiPitchResponse = await netWorkLocator.dio.get('$climbingApiHost/multi_pitch_route/$routeId'); + if (multiPitchResponse.statusCode == 200) { + MultiPitchRoute multiPitchRoute = MultiPitchRoute.fromJson(multiPitchResponse.data); + for (String pitchId in multiPitchRoute.pitchIds){ + final Response pitchResponse = await netWorkLocator.dio.get('$climbingApiHost/pitch/$pitchId'); + Pitch pitch = Pitch.fromJson(pitchResponse.data); + for (String ascentId in pitch.ascentIds){ + final Response ascentResponse = await netWorkLocator.dio.get('$climbingApiHost/ascent/$ascentId'); + Ascent ascent = Ascent.fromJson(ascentResponse.data); + DateTime dateOfAscent = DateTime.parse(ascent.date); + if ((dateOfAscent.isAfter(startDate) && dateOfAscent.isBefore(endDate)) || dateOfAscent.isAtSameMomentAs(startDate) || dateOfAscent.isAtSameMomentAs(endDate)){ + return spot; + } + } + } + } + } + for (String routeId in spot.singlePitchRouteIds){ + final Response singlePitchResponse = await netWorkLocator.dio.get('$climbingApiHost/single_pitch_route/$routeId'); + if (singlePitchResponse.statusCode == 200) { + SinglePitchRoute singlePitchRoute = SinglePitchRoute.fromJson(singlePitchResponse.data); + for (String ascentId in singlePitchRoute.ascentIds){ + final Response ascentResponse = await netWorkLocator.dio.get('$climbingApiHost/ascent/$ascentId'); + Ascent ascent = Ascent.fromJson(ascentResponse.data); + DateTime dateOfAscent = DateTime.parse(ascent.date); + if ((dateOfAscent.isAfter(startDate) && dateOfAscent.isBefore(endDate)) || dateOfAscent.isAtSameMomentAs(startDate) || dateOfAscent.isAtSameMomentAs(endDate)){ + return spot; + } + } + } + } + } + return null; + } + Future createSpot(CreateSpot createSpot, bool hasConnection) async { CreateSpot spot = CreateSpot( name: createSpot.name,