Skip to content

Commit

Permalink
Merge branch 'main' into app-diagram
Browse files Browse the repository at this point in the history
  • Loading branch information
CHOOSEIT authored May 31, 2024
2 parents 7206c22 + 1b5b5b3 commit 52318b2
Show file tree
Hide file tree
Showing 38 changed files with 582 additions and 79 deletions.
14 changes: 11 additions & 3 deletions lib/models/ui/challenge_details.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import "package:flutter/foundation.dart";
import "package:google_maps_flutter/google_maps_flutter.dart";

/// This class was hard to design. We considered multiple possible implementations,
/// and decided to use this one. If we add a parameter requiring to double the number of
/// constructors once more, it should be changed. Here are all the possibilities:
/// 1) Use one constructor per possibility, storing unused parameter as null internally (it is
/// the possibility we chose here).
/// 2) Use a single constructor, asking the developer instanciating it to set the correct
/// 2) Use a single constructor, asking the developer instantiating it to set the correct
/// parameters to null (a null distance to finish the challenge, for instance).
/// 3) Use a single constructor, but with additional boolean parameters to specify if the
/// class is a group challenge or if it is finished. This requires asserts to check that
Expand All @@ -18,6 +19,7 @@ class ChallengeDetails {
final int? distance;
final int? timeLeft;
final int reward;
final LatLng location;

/// Creates a [ChallengeDetails] with the given parameters. The [title] is the
/// post's title, the [distance] is the distance to the challenge in meters, the [timeLeft]
Expand All @@ -28,27 +30,32 @@ class ChallengeDetails {
required int this.distance,
required int this.timeLeft,
required this.reward,
required this.location,
});

const ChallengeDetails.group({
required this.title,
required int this.distance,
required this.reward,
required this.location,
}) : timeLeft = null;

const ChallengeDetails.soloFinished({
required this.title,
required int this.timeLeft,
required this.reward,
required this.location,
}) : distance = null;

const ChallengeDetails.groupFinished({
required this.title,
required this.reward,
required this.location,
}) : distance = null,
timeLeft = null;

bool get isFinished => distance == null;

bool get isGroupChallenge => timeLeft == null;

@override
Expand All @@ -57,11 +64,12 @@ class ChallengeDetails {
other.title == title &&
other.distance == distance &&
other.timeLeft == timeLeft &&
other.reward == reward;
other.reward == reward &&
other.location == location;
}

@override
int get hashCode {
return Object.hash(title, distance, timeLeft, reward);
return Object.hash(title, distance, timeLeft, reward, location);
}
}
5 changes: 5 additions & 0 deletions lib/models/ui/post_details.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import "package:flutter/foundation.dart";
import "package:geoflutterfire_plus/geoflutterfire_plus.dart";
import "package:google_maps_flutter/google_maps_flutter.dart";
import "package:proxima/models/database/post/post_firestore.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/models/database/user/user_firestore.dart";
import "package:proxima/models/database/user/user_id_firestore.dart";
import "package:proxima/utils/extensions/geopoint_extensions.dart";

@immutable

Expand All @@ -21,6 +23,7 @@ class PostDetails {
final DateTime publicationDate;
final int distance; // in meters
final bool isChallenge;
final LatLng location;

const PostDetails({
required this.postId,
Expand All @@ -34,6 +37,7 @@ class PostDetails {
required this.ownerCentauriPoints,
required this.publicationDate,
required this.distance,
required this.location,
this.isChallenge = false,
});

Expand Down Expand Up @@ -99,6 +103,7 @@ class PostDetails {
) *
1000)
.round(),
location: postFirestore.location.geoPoint.toLatLng(),
isChallenge: isChallenge,
);
}
Expand Down
32 changes: 32 additions & 0 deletions lib/models/ui/selected_page_details.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import "package:flutter/material.dart";
import "package:proxima/views/navigation/bottom_navigation_bar/navigation_bar_routes.dart";

/// A class that holds the currently open page in the home screen, with optional
/// arguments.
class SelectedPageDetails {
final NavigationBarRoutes route;
final Object? args;

const SelectedPageDetails({
required this.route,
this.args,
});

Widget page() {
return route.page(args);
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;

return other is SelectedPageDetails &&
other.route == route &&
other.args == args;
}

@override
int get hashCode {
return Object.hash(route, args);
}
}
8 changes: 6 additions & 2 deletions lib/models/ui/user_post_details.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "package:flutter/foundation.dart";
import "package:google_maps_flutter/google_maps_flutter.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";

/// A post that belongs to the current logged in user.
Expand All @@ -8,12 +9,13 @@ class UserPostDetails {
final PostIdFirestore postId;
final String title;
final String description;
// TODO add the location to be able to redirect to the map (see #184)
final LatLng location;

const UserPostDetails({
required this.postId,
required this.title,
required this.description,
required this.location,
});

@override
Expand All @@ -23,7 +25,8 @@ class UserPostDetails {
return other is UserPostDetails &&
other.postId == postId &&
other.title == title &&
other.description == description;
other.description == description &&
other.location == location;
}

@override
Expand All @@ -32,6 +35,7 @@ class UserPostDetails {
postId,
title,
description,
location,
);
}
}
8 changes: 8 additions & 0 deletions lib/utils/extensions/geopoint_extensions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import "package:cloud_firestore/cloud_firestore.dart";
import "package:google_maps_flutter/google_maps_flutter.dart";

extension ToLatLng on GeoPoint {
LatLng toLatLng() {
return LatLng(latitude, longitude);
}
}
3 changes: 3 additions & 0 deletions lib/viewmodels/challenge_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "package:proxima/models/ui/challenge_details.dart";
import "package:proxima/services/database/challenge_repository_service.dart";
import "package:proxima/services/database/post_repository_service.dart";
import "package:proxima/services/sensors/geolocation_service.dart";
import "package:proxima/utils/extensions/geopoint_extensions.dart";
import "package:proxima/viewmodels/dynamic_user_avatar_view_model.dart";
import "package:proxima/viewmodels/login_view_model.dart";
import "package:proxima/viewmodels/map/map_pin_view_model.dart";
Expand Down Expand Up @@ -49,12 +50,14 @@ class ChallengeViewModel
distance: distanceM,
timeLeft: timeLeft.inHours,
reward: ChallengeRepositoryService.soloChallengeReward,
location: post.location.geoPoint.toLatLng(),
);
} else {
return ChallengeDetails.soloFinished(
title: post.data.title,
timeLeft: timeLeft.inHours,
reward: ChallengeRepositoryService.soloChallengeReward,
location: post.location.geoPoint.toLatLng(),
);
}
});
Expand Down
22 changes: 19 additions & 3 deletions lib/viewmodels/map/map_pin_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ class MapPinViewModel extends AutoDisposeAsyncNotifier<List<MapPinDetails>> {

/// Get nearby posts
Future<List<MapPinDetails>> _getNearbyPosts() async {
//checking if the location services are enabled directly here so that
//we throw the same exception as the challenge case
final locationCheck =
await ref.read(geolocationServiceProvider).checkLocationServices();

if (locationCheck != null) {
throw locationCheck;
}

try {
await ref.watch(livePositionStreamProvider.future);
} catch (e) {
ref.invalidate(livePositionStreamProvider);
}

final position = await ref.watch(livePositionStreamProvider.future);
final postRepository = ref.watch(postRepositoryServiceProvider);
final userRepository = ref.watch(userRepositoryServiceProvider);
Expand Down Expand Up @@ -106,14 +121,15 @@ class MapPinViewModel extends AutoDisposeAsyncNotifier<List<MapPinDetails>> {

/// Get user active challenges
Future<List<MapPinDetails>> _getUserChallenges() async {
final postRepository = ref.watch(postRepositoryServiceProvider);
final challengeRepostory = ref.watch(challengeRepositoryServiceProvider);
final userId = ref.watch(validLoggedInUserIdProvider);
// Only doing a read here, to decrease the number of database reads
// (we don't want to re-read the challenges when the position changes).
final position =
await ref.read(geolocationServiceProvider).getCurrentPosition();

final postRepository = ref.watch(postRepositoryServiceProvider);
final challengeRepostory = ref.watch(challengeRepositoryServiceProvider);
final userId = ref.watch(validLoggedInUserIdProvider);

final userChallenges = await challengeRepostory.getChallenges(
userId,
position,
Expand Down
39 changes: 26 additions & 13 deletions lib/viewmodels/map/map_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@ import "package:google_maps_flutter/google_maps_flutter.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/ui/map_details.dart";
import "package:proxima/services/sensors/geolocation_service.dart";
import "package:proxima/utils/extensions/geopoint_extensions.dart";
import "package:proxima/viewmodels/posts_feed_view_model.dart";

/// This view model is responsible for managing the actual location
/// of the user on the map and the displayed circles.
class MapViewModel extends AutoDisposeAsyncNotifier<MapDetails> {
/// of the user on the map and the displayed circles. Building with a [LatLng]
/// will set the initial location of the map to the given [LatLng]. Otherwise
/// it will use the current location of the user.
class MapViewModel extends AutoDisposeFamilyAsyncNotifier<MapDetails, LatLng?> {
@override
Future<MapDetails> build() async {
final actualLocation =
await ref.read(geolocationServiceProvider).getCurrentPosition();

return MapDetails(
initialLocation:
LatLng(actualLocation.latitude, actualLocation.longitude),
);
Future<MapDetails> build([LatLng? arg]) async {
if (arg != null) {
disableFollowUser();
return MapDetails(
initialLocation: arg,
);
} else {
enableFollowUser();
final geoPoint =
await ref.read(geolocationServiceProvider).getCurrentPosition();
return MapDetails(
initialLocation: geoPoint.toLatLng(),
);
}
}

final Set<Circle> _circles = {};
Expand Down Expand Up @@ -71,8 +80,12 @@ class MapViewModel extends AutoDisposeAsyncNotifier<MapDetails> {

/// Move the camera to the target location
/// Only moves the camera if the follow user is enabled
Future<void> updateCamera(LatLng userPosition) async {
if (!_followUser) return;
Future<void> updateCamera(
LatLng userPosition, {
bool followEvent = true,
}) async {
if (followEvent && !_followUser) return;
_followUser = followEvent;
final GoogleMapController controller = await _mapController.future;
// reset zoom to initial
// center camera on target
Expand All @@ -83,6 +96,6 @@ class MapViewModel extends AutoDisposeAsyncNotifier<MapDetails> {
}

final mapViewModelProvider =
AsyncNotifierProvider.autoDispose<MapViewModel, MapDetails>(
AsyncNotifierProvider.autoDispose.family<MapViewModel, MapDetails, LatLng?>(
() => MapViewModel(),
);
33 changes: 33 additions & 0 deletions lib/viewmodels/option_selection/selected_page_view_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/ui/selected_page_details.dart";
import "package:proxima/viewmodels/option_selection/options_view_model.dart";
import "package:proxima/views/navigation/bottom_navigation_bar/navigation_bar_routes.dart";

/// A view model that holds the currently open page in the home screen. This
/// should never contain a page that does not have a bottom navigation bar.
/// Those should instead be pushed directly to the navigation stack.
class SelectedPageViewModel extends OptionsViewModel<SelectedPageDetails> {
static const defaultSelectedPage =
SelectedPageDetails(route: NavigationBarRoutes.feed);

SelectedPageViewModel() : super(defaultSelectedPage);

@override
void setOption(SelectedPageDetails option) {
if (option.route.routeDestination != null) {
throw Exception(
"This page should be pushed and not set as the selected page, push it directly from your context instead.",
);
}
super.setOption(option);
}

void selectPage(NavigationBarRoutes route, [Object? args]) {
setOption(SelectedPageDetails(route: route, args: args));
}
}

final selectedPageViewModelProvider =
NotifierProvider<SelectedPageViewModel, SelectedPageDetails>(
() => SelectedPageViewModel(),
);
2 changes: 2 additions & 0 deletions lib/viewmodels/user_posts_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:proxima/models/database/post/post_id_firestore.dart";
import "package:proxima/models/ui/user_post_details.dart";
import "package:proxima/services/database/post_repository_service.dart";
import "package:proxima/utils/extensions/geopoint_extensions.dart";
import "package:proxima/viewmodels/login_view_model.dart";
import "package:proxima/viewmodels/map/map_pin_view_model.dart";
import "package:proxima/viewmodels/posts_feed_view_model.dart";
Expand Down Expand Up @@ -33,6 +34,7 @@ class UserPostsViewModel extends AutoDisposeAsyncNotifier<UserPostsState> {
postId: post.id,
title: post.data.title,
description: post.data.description,
location: post.location.geoPoint.toLatLng(),
);

return userPost;
Expand Down
8 changes: 4 additions & 4 deletions lib/views/components/content/info_pop_up.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ class InfoPopUp extends StatelessWidget {
static const popUpTitleKey = Key("profilePopUpTitle");
static const popUpDescriptionKey = Key("profilePopUpDescription");

const InfoPopUp({super.key, this.title, this.content, this.button});
const InfoPopUp({super.key, this.title, this.content, this.actions});

final String? title;
final String? content;
final Widget? button;
final List<Widget>? actions;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -52,14 +52,14 @@ class InfoPopUp extends StatelessWidget {
left: 24.0,
top: 8.0,
right: 24.0,
bottom: button != null ? 12.0 : 0.0,
bottom: actions != null ? 12.0 : 0.0,
),
actionsPadding: const EdgeInsets.only(
right: 24.0,
bottom: 12.0,
left: 24.0,
),
actions: button != null ? [button!] : [],
actions: actions ?? [],
);
}
}
Loading

0 comments on commit 52318b2

Please sign in to comment.