diff --git a/.env.example b/.env.example index b4df0d91..e69de29b 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +0,0 @@ -ANDROID_MAP_API_KEY= -IOS_MAP_API_KEY= -HTTP_ENDPOINT= -WEBSOCKET_ENDPOINT= \ No newline at end of file diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index b3d2b925..b979389e 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -12,6 +12,7 @@ jobs: matrix: platform: [windows-latest, ubuntu-latest, macos-latest] runs-on: ${{matrix.platform}} + steps: - uses: actions/checkout@v1 - uses: actions/setup-java@v1 diff --git a/android/app/build.gradle b/android/app/build.gradle index b795e207..35c7e65e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,6 @@ apply plugin: 'com.google.gms.google-services' // END: FlutterFire Configuration apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" -apply from: project(':flutter_config').projectDir.getPath() + "/dotenv.gradle" def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') @@ -36,13 +35,28 @@ if (keystorePropertiesFile.exists()) { android { + namespace "com.ccextractor.beaconmobile" compileSdkVersion 34 buildToolsVersion '29.0.0' + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' // Match the Java version + } + sourceSets { main.java.srcDirs += 'src/main/kotlin' } + buildFeatures { + buildConfig true + } + defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.ccextractor.beaconmobile" @@ -79,7 +93,18 @@ flutter { source '../..' } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +kotlin { + jvmToolchain(17) +} + dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.android.support:multidex:1.0.3" + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..068d7d2b --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.ccextractor.beaconmobile.BuildConfig { *; } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ccextractor/beaconmobile/MainActivity.kt b/android/app/src/main/kotlin/com/ccextractor/beaconmobile/MainActivity.kt index b1daf955..b51d3f21 100644 --- a/android/app/src/main/kotlin/com/ccextractor/beaconmobile/MainActivity.kt +++ b/android/app/src/main/kotlin/com/ccextractor/beaconmobile/MainActivity.kt @@ -2,20 +2,12 @@ package com.ccextractor.beaconmobile import android.content.res.Configuration import androidx.annotation.NonNull -import cl.puntito.simple_pip_mode.PipCallbackHelper import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine class MainActivity : FlutterActivity() { - private var callbackHelper = PipCallbackHelper() override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - callbackHelper.configureFlutterEngine(flutterEngine) - } - - override fun onPictureInPictureModeChanged(active: Boolean, newConfig: Configuration?) { - super.onPictureInPictureModeChanged(active, newConfig) - callbackHelper.onPictureInPictureModeChanged(active) } } diff --git a/android/build.gradle b/android/build.gradle index 8a87ad19..f5804e64 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.8.20' + ext.kotlin_version = '2.2.0' repositories { google() jcenter() @@ -9,7 +9,7 @@ buildscript { // START: FlutterFire Configuration classpath 'com.google.gms:google-services:4.3.15' // END: FlutterFire Configuration - classpath 'com.android.tools.build:gradle:7.1.0' + classpath 'com.android.tools.build:gradle:8.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -35,6 +35,15 @@ subprojects { buildToolsVersion '29.0.0' } } + + // Fix namespace for geolocator_android + if (project.name == 'geolocator_android') { + project.android { + namespace 'com.baseflow.geolocator' + } + } + + } } @@ -42,10 +51,11 @@ rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } + subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index 4bc5c4b4..faa40955 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=false kotlin.version=1.8.20 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 302da314..a3638774 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip \ No newline at end of file diff --git a/images/icons/camp.png b/images/icons/camp.png new file mode 100644 index 00000000..780a798f Binary files /dev/null and b/images/icons/camp.png differ diff --git a/images/icons/destination.png b/images/icons/destination.png new file mode 100644 index 00000000..9732483d Binary files /dev/null and b/images/icons/destination.png differ diff --git a/images/icons/forest.png b/images/icons/forest.png new file mode 100644 index 00000000..a9d59d06 Binary files /dev/null and b/images/icons/forest.png differ diff --git a/images/icons/location-marker.png b/images/icons/location-marker.png new file mode 100644 index 00000000..7c7b6204 Binary files /dev/null and b/images/icons/location-marker.png differ diff --git a/images/icons/rain.png b/images/icons/rain.png new file mode 100644 index 00000000..44ba7d1f Binary files /dev/null and b/images/icons/rain.png differ diff --git a/images/icons/wind.png b/images/icons/wind.png new file mode 100644 index 00000000..6ea5ec28 Binary files /dev/null and b/images/icons/wind.png differ diff --git a/lib/config/enviornment_config.dart b/lib/config/enviornment_config.dart index 0b8b8a40..71059e59 100644 --- a/lib/config/enviornment_config.dart +++ b/lib/config/enviornment_config.dart @@ -1,21 +1,25 @@ import 'dart:io'; -import 'package:flutter_config/flutter_config.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; class EnvironmentConfig { - static String? get httpEndpoint => FlutterConfig.get('HTTP_ENDPOINT'); + static String? get httpEndpoint => dotenv.env['HTTP_ENDPOINT']; - static String? get websocketEndpoint => - FlutterConfig.get('WEBSOCKET_ENDPOINT'); + static String? get websocketEndpoint => dotenv.env['WEBSOCKET_ENDPOINT']; static String? get googleMapApi { if (Platform.isAndroid) { - return FlutterConfig.get('ANDROID_MAP_API_KEY'); + return dotenv.env['ANDROID_MAP_API_KEY']; } - return FlutterConfig.get('IOS_MAP_API_KEY'); + return dotenv.env['IOS_MAP_API_KEY']; } + static String? get geoApifyApiKey => dotenv.env['GEOAPIFY_API_KEY']; + + static String? get openWeatherMapApiKey => + dotenv.env['OPEN_WEATHER_MAP_API_KEY']; + static Future loadEnvVariables() async { - await FlutterConfig.loadEnvVariables(); + await dotenv.load(fileName: '.env'); } } diff --git a/lib/config/graphql_config.dart b/lib/config/graphql_config.dart index 44fde83b..0f7ee65e 100644 --- a/lib/config/graphql_config.dart +++ b/lib/config/graphql_config.dart @@ -12,7 +12,7 @@ class GraphQLConfig { WebSocketLink? _webSocketLink; static final HttpLink httpLink = HttpLink( EnvironmentConfig.httpEndpoint ?? - 'https://beacon-backend-25.onrender.com/graphql', + 'https://beacon-backend-0kpr.onrender.com/graphql', ); Future _loadAuthLink() async { @@ -24,7 +24,7 @@ class GraphQLConfig { await _getToken(); _webSocketLink = WebSocketLink( EnvironmentConfig.websocketEndpoint ?? - 'ws://beacon-backend-25.onrender.com/graphql', + 'wss://beacon-backend-0kpr.onrender.com/graphql', config: SocketClientConfig( autoReconnect: true, initialPayload: {"Authorization": token}, diff --git a/lib/config/local_notification.dart b/lib/config/local_notification.dart index 99bf4cb3..e0c09c19 100644 --- a/lib/config/local_notification.dart +++ b/lib/config/local_notification.dart @@ -18,10 +18,7 @@ class LocalNotification { const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('app_icon'); final DarwinInitializationSettings initializationSettingsIOS = - DarwinInitializationSettings( - onDidReceiveLocalNotification: (_, __, ___, ____) {} - // as Future Function(int, String?, String?, String?)? - ); + DarwinInitializationSettings(); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, diff --git a/lib/config/router/router.dart b/lib/config/router/router.dart index 7ff17774..a159533d 100644 --- a/lib/config/router/router.dart +++ b/lib/config/router/router.dart @@ -1,11 +1,13 @@ import 'package:auto_route/auto_route.dart'; import 'package:beacon/domain/entities/group/group_entity.dart'; import 'package:beacon/presentation/auth/verfication_screen.dart'; +import 'package:beacon/presentation/home/profile_screen.dart'; import 'package:beacon/presentation/splash/splash_screen.dart'; import 'package:beacon/presentation/home/home_screen.dart'; import 'package:flutter/material.dart'; import 'package:beacon/presentation/auth/auth_screen.dart'; import 'package:beacon/presentation/group/group_screen.dart'; +import 'package:beacon/presentation/group/advance_options_screen.dart'; import 'package:beacon/presentation/hike/hike_screen.dart'; import 'package:beacon/domain/entities/beacon/beacon_entity.dart'; part 'router.gr.dart'; @@ -25,5 +27,10 @@ class AppRouter extends _$AppRouter { AutoRoute( page: VerificationScreenRoute.page, ), + AutoRoute( + page: ProfileScreenRoute.page, + ), + AutoRoute( + page: AdvancedOptionsScreenRoute.page, path: '/advanced-options'), ]; } diff --git a/lib/config/router/router.gr.dart b/lib/config/router/router.gr.dart index 994646b0..139717bc 100644 --- a/lib/config/router/router.gr.dart +++ b/lib/config/router/router.gr.dart @@ -15,6 +15,21 @@ abstract class _$AppRouter extends RootStackRouter { @override final Map pagesMap = { + AdvancedOptionsScreenRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: AdvancedOptionsScreen( + key: args.key, + durationController: args.durationController, + title: args.title, + isScheduled: args.isScheduled, + startDate: args.startDate, + startTime: args.startTime, + groupId: args.groupId, + ), + ); + }, AuthScreenRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, @@ -25,7 +40,10 @@ abstract class _$AppRouter extends RootStackRouter { final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: GroupScreen(args.group), + child: GroupScreen( + args.group, + key: args.key, + ), ); }, HikeScreenRoute.name: (routeData) { @@ -45,6 +63,12 @@ abstract class _$AppRouter extends RootStackRouter { child: const HomeScreen(), ); }, + ProfileScreenRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ProfileScreen(), + ); + }, SplashScreenRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, @@ -60,6 +84,70 @@ abstract class _$AppRouter extends RootStackRouter { }; } +/// generated route for +/// [AdvancedOptionsScreen] +class AdvancedOptionsScreenRoute + extends PageRouteInfo { + AdvancedOptionsScreenRoute({ + Key? key, + required TextEditingController durationController, + required String title, + required bool isScheduled, + DateTime? startDate, + TimeOfDay? startTime, + required String groupId, + List? children, + }) : super( + AdvancedOptionsScreenRoute.name, + args: AdvancedOptionsScreenRouteArgs( + key: key, + durationController: durationController, + title: title, + isScheduled: isScheduled, + startDate: startDate, + startTime: startTime, + groupId: groupId, + ), + initialChildren: children, + ); + + static const String name = 'AdvancedOptionsScreenRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class AdvancedOptionsScreenRouteArgs { + const AdvancedOptionsScreenRouteArgs({ + this.key, + required this.durationController, + required this.title, + required this.isScheduled, + this.startDate, + this.startTime, + required this.groupId, + }); + + final Key? key; + + final TextEditingController durationController; + + final String title; + + final bool isScheduled; + + final DateTime? startDate; + + final TimeOfDay? startTime; + + final String groupId; + + @override + String toString() { + return 'AdvancedOptionsScreenRouteArgs{key: $key, durationController: $durationController, title: $title, isScheduled: $isScheduled, startDate: $startDate, startTime: $startTime, groupId: $groupId}'; + } +} + /// generated route for /// [AuthScreen] class AuthScreenRoute extends PageRouteInfo { @@ -79,10 +167,14 @@ class AuthScreenRoute extends PageRouteInfo { class GroupScreenRoute extends PageRouteInfo { GroupScreenRoute({ required GroupEntity group, + Key? key, List? children, }) : super( GroupScreenRoute.name, - args: GroupScreenRouteArgs(group: group), + args: GroupScreenRouteArgs( + group: group, + key: key, + ), initialChildren: children, ); @@ -93,13 +185,18 @@ class GroupScreenRoute extends PageRouteInfo { } class GroupScreenRouteArgs { - const GroupScreenRouteArgs({required this.group}); + const GroupScreenRouteArgs({ + required this.group, + this.key, + }); final GroupEntity group; + final Key? key; + @override String toString() { - return 'GroupScreenRouteArgs{group: $group}'; + return 'GroupScreenRouteArgs{group: $group, key: $key}'; } } @@ -160,6 +257,20 @@ class HomeScreenRoute extends PageRouteInfo { static const PageInfo page = PageInfo(name); } +/// generated route for +/// [ProfileScreen] +class ProfileScreenRoute extends PageRouteInfo { + const ProfileScreenRoute({List? children}) + : super( + ProfileScreenRoute.name, + initialChildren: children, + ); + + static const String name = 'ProfileScreenRoute'; + + static const PageInfo page = PageInfo(name); +} + /// generated route for /// [SplashScreen] class SplashScreenRoute extends PageRouteInfo { diff --git a/lib/core/queries/auth.dart b/lib/core/queries/auth.dart index b665027d..b6c96704 100644 --- a/lib/core/queries/auth.dart +++ b/lib/core/queries/auth.dart @@ -7,15 +7,16 @@ class AuthQueries { _id name email + imageUrl } } '''; } - String gAuth(String? name, String email) { + String gAuth(String? name, String email, String? imageUrl) { return ''' mutation{ - oAuth(userInput: {email: "$email", name: "$name"}) + oAuth(userInput: {email: "$email", name: "$name", imageUrl: "$imageUrl"}) } '''; } @@ -85,6 +86,7 @@ class AuthQueries { email name isVerified + imageUrl groups{ _id } diff --git a/lib/core/queries/beacon.dart b/lib/core/queries/beacon.dart index ea2735f5..c7e93e0f 100644 --- a/lib/core/queries/beacon.dart +++ b/lib/core/queries/beacon.dart @@ -10,6 +10,7 @@ class BeaconQueries { leader{ _id name + imageUrl } startsAt expiresAt @@ -27,6 +28,7 @@ class BeaconQueries { leader { _id name + imageUrl } group{ _id @@ -39,6 +41,7 @@ class BeaconQueries { followers { _id name + imageUrl } startsAt expiresAt @@ -65,6 +68,7 @@ deleteBeacon(id: "$id") leader { _id name + imageUrl } group{ _id @@ -77,6 +81,7 @@ deleteBeacon(id: "$id") followers { _id name + imageUrl } startsAt expiresAt @@ -114,6 +119,7 @@ deleteBeacon(id: "$id") _id name email + imageUrl beacons{ _id } @@ -129,6 +135,7 @@ deleteBeacon(id: "$id") followers { _id name + imageUrl } startsAt expiresAt @@ -206,6 +213,7 @@ deleteBeacon(id: "$id") leader { _id name + imageUrl } location { lat @@ -214,6 +222,7 @@ deleteBeacon(id: "$id") followers { _id name + imageUrl } startsAt expiresAt @@ -231,6 +240,7 @@ deleteBeacon(id: "$id") leader{ _id name + imageUrl location{ lat lon @@ -247,10 +257,12 @@ deleteBeacon(id: "$id") lat lon } + imageUrl } landmarks{ _id title + icon location{ lat lon @@ -258,6 +270,7 @@ deleteBeacon(id: "$id") createdBy{ _id name + imageUrl } } location{ @@ -310,6 +323,7 @@ deleteBeacon(id: "$id") leader { _id name + imageUrl location { lat lon @@ -322,6 +336,7 @@ deleteBeacon(id: "$id") followers { _id name + imageUrl } startsAt expiresAt @@ -359,11 +374,13 @@ deleteBeacon(id: "$id") _id name email + imageUrl } inactiveuser{ _id name email + imageUrl } } } @@ -376,6 +393,7 @@ deleteBeacon(id: "$id") _id name email + imageUrl location{ lat lon @@ -385,28 +403,33 @@ deleteBeacon(id: "$id") landmark{ _id title + icon location{ lat lon } + } } } '''); - String createLandmark(String? id, String lat, String lon, String? title) { + String createLandmark( + String? id, String lat, String lon, String? title, String icon) { return ''' mutation{ createLandmark( landmark:{ location:{lat:"$lat", lon:"$lon"}, - title:"$title" + title:"$title", + icon: "$icon" }, beaconID:"$id") { _id title + icon location{ lat lon @@ -414,6 +437,7 @@ deleteBeacon(id: "$id") createdBy{ _id name + imageUrl } } } @@ -427,6 +451,7 @@ deleteBeacon(id: "$id") _id name email + imageUrl location{ lat lon @@ -440,6 +465,16 @@ deleteBeacon(id: "$id") subscription StreamLocationUpdate($id: ID!){ beaconLocations(id: $id){ + userSOS { + _id + name + email + location{ + lat + lon + } + } + route{ lat lon @@ -448,6 +483,7 @@ deleteBeacon(id: "$id") updatedUser{ _id name + imageUrl location{ lat lon @@ -457,6 +493,7 @@ deleteBeacon(id: "$id") landmark{ _id title + icon location{ lat lon @@ -465,10 +502,24 @@ deleteBeacon(id: "$id") _id name email + imageUrl } } } } '''); + + String updateUserImage(String? imageUrl) { + return ''' + mutation{ + updateUserImage(imageUrl: "$imageUrl"){ + _id + name + email + imageUrl + } + } + '''; + } } diff --git a/lib/core/queries/group.dart b/lib/core/queries/group.dart index 06cea688..9b127ccc 100644 --- a/lib/core/queries/group.dart +++ b/lib/core/queries/group.dart @@ -13,10 +13,12 @@ class GroupQueries { leader{ _id name + imageUrl } members{ _id name + imageUrl } shortcode __typename @@ -39,10 +41,12 @@ class GroupQueries { leader { _id name + imageUrl } members { _id name + imageUrl } beacons { @@ -52,6 +56,7 @@ class GroupQueries { leader { _id name + imageUrl } location{ lat @@ -60,6 +65,7 @@ class GroupQueries { followers { _id name + imageUrl } startsAt expiresAt @@ -82,10 +88,12 @@ class GroupQueries { leader { _id name + imageUrl } members { _id name + imageUrl } beacons{ _id @@ -94,6 +102,7 @@ class GroupQueries { leader { _id name + imageUrl } location{ lat @@ -102,6 +111,7 @@ class GroupQueries { followers { _id name + imageUrl } startsAt expiresAt @@ -122,10 +132,12 @@ class GroupQueries { leader { _id name + imageUrl } members { _id name + imageUrl } beacons { @@ -146,6 +158,7 @@ query{ leader { _id name + imageUrl } location{ lat @@ -154,6 +167,7 @@ query{ followers { _id name + imageUrl } group{ _id @@ -178,10 +192,12 @@ query{ leader{ _id name + imageUrl } members{ _id name + imageUrl } shortcode __typename @@ -200,6 +216,7 @@ query{ _id name email + imageUrl } newBeacon{ @@ -209,10 +226,12 @@ query{ _id name email + imageUrl } followers { _id name + imageUrl } group{ _id @@ -233,10 +252,12 @@ query{ _id name email + imageUrl } followers { _id name + imageUrl } group{ _id @@ -257,10 +278,12 @@ query{ _id name email + imageUrl } followers { _id name + imageUrl } group{ _id @@ -284,6 +307,7 @@ query{ _id name email + imageUrl } } '''; @@ -293,6 +317,7 @@ query{ subscription StreamNewlyJoinedGroups($id: ID!){ groupJoined(id: $id){ name + imageUrl location{ lat lon @@ -300,4 +325,18 @@ query{ } } '''); + + String updateUserImage(String userId, String? imageUrl) { + return ''' + mutation{ + updateUserImage(userId: "${userId}", imageUrl: "$imageUrl") + { + _id + name + email + imageUrl + } + } + '''; + } } diff --git a/lib/data/datasource/remote/remote_auth_api.dart b/lib/data/datasource/remote/remote_auth_api.dart index 94786a3d..41233fa6 100644 --- a/lib/data/datasource/remote/remote_auth_api.dart +++ b/lib/data/datasource/remote/remote_auth_api.dart @@ -86,18 +86,16 @@ class RemoteAuthApi { } } - Future> gAuth(String name, String email) async { - log('name: $name'); - log('email: $email'); - + Future> gAuth( + String name, String email, String? imageUrl) async { final isConnected = await utils.checkInternetConnectivity(); if (!isConnected) { return DataFailed('Beacon is trying to connect with internet...'); } - final QueryResult result = await clientNonAuth.mutate( - MutationOptions(document: gql(_authQueries.gAuth(name, email)))); + final QueryResult result = await clientNonAuth.mutate(MutationOptions( + document: gql(_authQueries.gAuth(name, email, imageUrl)))); log(result.toString()); diff --git a/lib/data/datasource/remote/remote_hike_api.dart b/lib/data/datasource/remote/remote_hike_api.dart index 1317f018..ce9820a1 100644 --- a/lib/data/datasource/remote/remote_hike_api.dart +++ b/lib/data/datasource/remote/remote_hike_api.dart @@ -33,6 +33,8 @@ class RemoteHikeApi { final result = await _authClient.mutate(MutationOptions( document: gql(beaconQueries.fetchBeaconDetail(beaconId)))); + print("Result fetch beacon queries: ${result.data}"); + if (result.isConcrete && result.data != null) { final beaconJson = result.data!['beacon']; @@ -82,7 +84,7 @@ class RemoteHikeApi { } Future> createLandMark( - String id, String lat, String lon, String title) async { + String id, String lat, String lon, String title, String icon) async { bool isConnected = await utils.checkInternetConnectivity(); if (!isConnected) { @@ -90,13 +92,17 @@ class RemoteHikeApi { } final result = await _authClient.mutate(MutationOptions( - document: gql(beaconQueries.createLandmark(id, lat, lon, title)))); + document: + gql(beaconQueries.createLandmark(id, lat, lon, title, icon)))); + + print("Result: ${result.data}"); if (result.isConcrete && result.data != null && result.data!['createLandmark'] != null) { final newLandMark = LandMarkModel.fromJson(result.data!['createLandmark']); + print("result data: ${result.data!['createLandmark']}"); return DataSuccess(newLandMark); } else { return DataFailed(encounteredExceptionOrError(result.exception!)); @@ -122,8 +128,14 @@ class RemoteHikeApi { if (stream.hasException) { yield DataFailed('Something went wrong'); } else { + print('Stream data: ${stream.data}'); var locations = BeaconLocationsModel.fromJson(stream.data!['beaconLocations']); + print('Locations: ${locations.user}'); + print('Locations: ${locations.route}'); + print('Locations: ${locations.userSOS}'); + //print('Locations: ${locations.landmarks}'); + yield DataSuccess(locations); } } @@ -168,6 +180,8 @@ class RemoteHikeApi { final result = await _authClient .mutate(MutationOptions(document: gql(beaconQueries.sos(id)))); + print("result sos: ${result.data}"); + if (result.isConcrete && result.data != null && result.data!['sos'] != null) { diff --git a/lib/data/datasource/remote/remote_home_api.dart b/lib/data/datasource/remote/remote_home_api.dart index e183eced..3d3969b0 100644 --- a/lib/data/datasource/remote/remote_home_api.dart +++ b/lib/data/datasource/remote/remote_home_api.dart @@ -204,4 +204,20 @@ class RemoteHomeApi { return exception.graphqlErrors[0].message.toString(); } } + + Future> updateUserImage( + String userId, String? imageUrl) async { + bool isConnected = await utils.checkInternetConnectivity(); + if (!isConnected) { + return DataFailed('No internet connection'); + } + + final result = await _authClient.mutate(MutationOptions( + document: gql(_groupQueries.updateUserImage(userId, imageUrl)))); + + if (result.isConcrete && result.data != null) { + return DataSuccess(true); + } + return DataFailed(encounteredExceptionOrError(result.exception!)); + } } diff --git a/lib/data/models/landmark/landmark_model.dart b/lib/data/models/landmark/landmark_model.dart index 1a16319f..e6a09ef3 100644 --- a/lib/data/models/landmark/landmark_model.dart +++ b/lib/data/models/landmark/landmark_model.dart @@ -20,7 +20,11 @@ class LandMarkModel implements LandMarkEntity { @HiveField(3) UserModel? createdBy; - LandMarkModel({this.title, this.location, this.id, this.createdBy}); + @HiveField(4) + String? icon; + + LandMarkModel( + {this.title, this.location, this.id, this.createdBy, this.icon}); @override $LandMarkEntityCopyWith get copyWith => @@ -34,12 +38,14 @@ class LandMarkModel implements LandMarkEntity { LandMarkModel copyWithModel( {String? id, String? title, + String? icon, LocationModel? location, UserModel? createdBy}) { return LandMarkModel( id: id, title: title ?? this.title, location: location ?? this.location, + icon: icon ?? this.icon, createdBy: createdBy ?? this.createdBy); } } diff --git a/lib/data/models/landmark/landmark_model.g.dart b/lib/data/models/landmark/landmark_model.g.dart index 96a59958..f0348140 100644 --- a/lib/data/models/landmark/landmark_model.g.dart +++ b/lib/data/models/landmark/landmark_model.g.dart @@ -21,13 +21,14 @@ class LandMarkModelAdapter extends TypeAdapter { location: fields[1] as LocationModel?, id: fields[2] as String?, createdBy: fields[3] as UserModel?, + icon: fields[4] as String?, ); } @override void write(BinaryWriter writer, LandMarkModel obj) { writer - ..writeByte(4) + ..writeByte(5) ..writeByte(0) ..write(obj.title) ..writeByte(1) @@ -35,7 +36,9 @@ class LandMarkModelAdapter extends TypeAdapter { ..writeByte(2) ..write(obj.id) ..writeByte(3) - ..write(obj.createdBy); + ..write(obj.createdBy) + ..writeByte(4) + ..write(obj.icon); } @override @@ -63,6 +66,7 @@ LandMarkModel _$LandMarkModelFromJson(Map json) => createdBy: json['createdBy'] == null ? null : UserModel.fromJson(json['createdBy'] as Map), + icon: json['icon'] as String?, ); Map _$LandMarkModelToJson(LandMarkModel instance) => @@ -71,4 +75,5 @@ Map _$LandMarkModelToJson(LandMarkModel instance) => 'location': instance.location, '_id': instance.id, 'createdBy': instance.createdBy, + 'icon': instance.icon, }; diff --git a/lib/data/models/landmark/location_suggestion.dart b/lib/data/models/landmark/location_suggestion.dart new file mode 100644 index 00000000..b431db2e --- /dev/null +++ b/lib/data/models/landmark/location_suggestion.dart @@ -0,0 +1,27 @@ +class LocationSuggestion { + final String name; + final double latitude; + final double longitude; + final String fullAddress; + + LocationSuggestion({ + required this.name, + required this.latitude, + required this.longitude, + required this.fullAddress, + }); + + factory LocationSuggestion.fromJson(Map json) { + return LocationSuggestion( + name: json['formatted'] ?? json['address_line1'] ?? 'Unknown Location', + latitude: (json['lat'] ?? 0.0).toDouble(), + longitude: (json['lon'] ?? 0.0).toDouble(), + fullAddress: json['formatted'] ?? 'Unknown Address', + ); + } + + @override + String toString() { + return 'LocationSuggestion(name: $name, lat: $latitude, lon: $longitude)'; + } +} diff --git a/lib/data/models/user/user_model.dart b/lib/data/models/user/user_model.dart index 1c2b61d6..3f8ebe89 100644 --- a/lib/data/models/user/user_model.dart +++ b/lib/data/models/user/user_model.dart @@ -38,16 +38,21 @@ class UserModel implements UserEntity { @HiveField(8) bool? isVerified; - UserModel( - {this.authToken, - this.beacons, - this.email, - this.groups, - this.id, - this.isGuest, - this.location, - this.name, - this.isVerified}); + @HiveField(9) // New field number (must be unique) + String? imageUrl; // Add this line + + UserModel({ + this.authToken, + this.beacons, + this.email, + this.groups, + this.id, + this.isGuest, + this.location, + this.name, + this.isVerified, + this.imageUrl, // Add this line + }); @override $UserEntityCopyWith get copyWith => throw UnimplementedError(); @@ -67,16 +72,19 @@ class UserModel implements UserEntity { List? groups, List? beacons, LocationModel? location, + String? imageUrl, // Add this line }) { return UserModel( - id: id ?? this.id, - name: name ?? this.name, - authToken: authToken ?? this.authToken, - email: email ?? this.email, - isGuest: isGuest ?? this.isGuest, - groups: groups ?? this.groups, - beacons: beacons ?? this.beacons, - location: location ?? this.location, - isVerified: isVerified ?? this.isVerified); + id: id ?? this.id, + name: name ?? this.name, + authToken: authToken ?? this.authToken, + email: email ?? this.email, + isGuest: isGuest ?? this.isGuest, + groups: groups ?? this.groups, + beacons: beacons ?? this.beacons, + location: location ?? this.location, + isVerified: isVerified ?? this.isVerified, + imageUrl: imageUrl ?? this.imageUrl, // Add this line + ); } } diff --git a/lib/data/models/user/user_model.g.dart b/lib/data/models/user/user_model.g.dart index 45bb58a0..3102166c 100644 --- a/lib/data/models/user/user_model.g.dart +++ b/lib/data/models/user/user_model.g.dart @@ -26,13 +26,14 @@ class UserModelAdapter extends TypeAdapter { location: fields[7] as LocationModel?, name: fields[1] as String?, isVerified: fields[8] as bool?, + imageUrl: fields[9] as String?, ); } @override void write(BinaryWriter writer, UserModel obj) { writer - ..writeByte(9) + ..writeByte(10) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -50,7 +51,9 @@ class UserModelAdapter extends TypeAdapter { ..writeByte(7) ..write(obj.location) ..writeByte(8) - ..write(obj.isVerified); + ..write(obj.isVerified) + ..writeByte(9) + ..write(obj.imageUrl); } @override @@ -87,6 +90,7 @@ UserModel _$UserModelFromJson(Map json) => UserModel( : LocationModel.fromJson(json['location'] as Map), name: json['name'] as String?, isVerified: json['isVerified'] as bool?, + imageUrl: json['imageUrl'] as String?, ); Map _$UserModelToJson(UserModel instance) => { @@ -99,4 +103,5 @@ Map _$UserModelToJson(UserModel instance) => { 'beacons': instance.beacons, 'location': instance.location, 'isVerified': instance.isVerified, + 'imageUrl': instance.imageUrl, }; diff --git a/lib/data/repositories/auth_repository_implementation.dart b/lib/data/repositories/auth_repository_implementation.dart index a6a63111..533452dd 100644 --- a/lib/data/repositories/auth_repository_implementation.dart +++ b/lib/data/repositories/auth_repository_implementation.dart @@ -20,8 +20,9 @@ class AuthRepositoryImplementation implements AuthRepository { } @override - Future> oAuth(String name, String email) { - return remoteAuthApi.gAuth(name, email); + Future> oAuth( + String name, String email, String? imageUrl) { + return remoteAuthApi.gAuth(name, email, imageUrl); } @override diff --git a/lib/data/repositories/hike_repository_implementation.dart b/lib/data/repositories/hike_repository_implementation.dart index 93c0d562..14077629 100644 --- a/lib/data/repositories/hike_repository_implementation.dart +++ b/lib/data/repositories/hike_repository_implementation.dart @@ -28,8 +28,8 @@ class HikeRepositoryImplementatioin implements HikeRepository { @override Future> createLandMark( - String id, String title, String lat, String lon) { - return remoteHikeApi.createLandMark(id, lat, lon, title); + String id, String title, String lat, String lon, String icon) { + return remoteHikeApi.createLandMark(id, lat, lon, title, icon); } @override diff --git a/lib/data/repositories/home_repository_implementation.dart b/lib/data/repositories/home_repository_implementation.dart index 3804d64e..164962e6 100644 --- a/lib/data/repositories/home_repository_implementation.dart +++ b/lib/data/repositories/home_repository_implementation.dart @@ -40,4 +40,9 @@ class HomeRepostitoryImplementation implements HomeRepository { Future> changeShortcode(String groupId) { return remoteHomeApi.changeShortCode(groupId); } + + @override + Future> updateUserImage(String userId, String? imageUrl) { + return remoteHomeApi.updateUserImage(userId, imageUrl); + } } diff --git a/lib/domain/entities/beacon/beacon_entity.freezed.dart b/lib/domain/entities/beacon/beacon_entity.freezed.dart index a623a54b..ce37706c 100644 --- a/lib/domain/entities/beacon/beacon_entity.freezed.dart +++ b/lib/domain/entities/beacon/beacon_entity.freezed.dart @@ -28,7 +28,9 @@ mixin _$BeaconEntity { LocationEntity? get location => throw _privateConstructorUsedError; GroupEntity? get group => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of BeaconEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $BeaconEntityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -67,6 +69,8 @@ class _$BeaconEntityCopyWithImpl<$Res, $Val extends BeaconEntity> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of BeaconEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -130,6 +134,8 @@ class _$BeaconEntityCopyWithImpl<$Res, $Val extends BeaconEntity> ) as $Val); } + /// Create a copy of BeaconEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserEntityCopyWith<$Res>? get leader { @@ -142,6 +148,8 @@ class _$BeaconEntityCopyWithImpl<$Res, $Val extends BeaconEntity> }); } + /// Create a copy of BeaconEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $LocationEntityCopyWith<$Res>? get location { @@ -154,6 +162,8 @@ class _$BeaconEntityCopyWithImpl<$Res, $Val extends BeaconEntity> }); } + /// Create a copy of BeaconEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $GroupEntityCopyWith<$Res>? get group { @@ -204,6 +214,8 @@ class __$$BeaconEntityImplCopyWithImpl<$Res> _$BeaconEntityImpl _value, $Res Function(_$BeaconEntityImpl) _then) : super(_value, _then); + /// Create a copy of BeaconEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -378,7 +390,9 @@ class _$BeaconEntityImpl implements _BeaconEntity { location, group); - @JsonKey(ignore: true) + /// Create a copy of BeaconEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$BeaconEntityImplCopyWith<_$BeaconEntityImpl> get copyWith => @@ -421,8 +435,11 @@ abstract class _BeaconEntity implements BeaconEntity { LocationEntity? get location; @override GroupEntity? get group; + + /// Create a copy of BeaconEntity + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$BeaconEntityImplCopyWith<_$BeaconEntityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/domain/entities/group/group_entity.freezed.dart b/lib/domain/entities/group/group_entity.freezed.dart index 74507d02..fbd22908 100644 --- a/lib/domain/entities/group/group_entity.freezed.dart +++ b/lib/domain/entities/group/group_entity.freezed.dart @@ -25,7 +25,9 @@ mixin _$GroupEntity { bool get hasBeaconActivity => throw _privateConstructorUsedError; bool get hasMemberActivity => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of GroupEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $GroupEntityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -59,6 +61,8 @@ class _$GroupEntityCopyWithImpl<$Res, $Val extends GroupEntity> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of GroupEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -107,6 +111,8 @@ class _$GroupEntityCopyWithImpl<$Res, $Val extends GroupEntity> ) as $Val); } + /// Create a copy of GroupEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserEntityCopyWith<$Res>? get leader { @@ -150,6 +156,8 @@ class __$$GroupEntityImplCopyWithImpl<$Res> _$GroupEntityImpl _value, $Res Function(_$GroupEntityImpl) _then) : super(_value, _then); + /// Create a copy of GroupEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -284,7 +292,9 @@ class _$GroupEntityImpl implements _GroupEntity { hasBeaconActivity, hasMemberActivity); - @JsonKey(ignore: true) + /// Create a copy of GroupEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$GroupEntityImplCopyWith<_$GroupEntityImpl> get copyWith => @@ -318,8 +328,11 @@ abstract class _GroupEntity implements GroupEntity { bool get hasBeaconActivity; @override bool get hasMemberActivity; + + /// Create a copy of GroupEntity + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$GroupEntityImplCopyWith<_$GroupEntityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/domain/entities/landmark/landmark_entity.dart b/lib/domain/entities/landmark/landmark_entity.dart index e6d6a130..f29960f1 100644 --- a/lib/domain/entities/landmark/landmark_entity.dart +++ b/lib/domain/entities/landmark/landmark_entity.dart @@ -5,23 +5,29 @@ part 'landmark_entity.freezed.dart'; @freezed class LandMarkEntity with _$LandMarkEntity { - const factory LandMarkEntity( - {String? id, - String? title, - LocationEntity? location, - UserEntity? createdBy}) = _LandMarkEntity; + const factory LandMarkEntity({ + String? id, + String? title, + String? icon, + LocationEntity? location, + UserEntity? createdBy, + }) = _LandMarkEntity; } extension LandMarkEntityCopyWithExtension on LandMarkEntity { - LandMarkEntity copywith( - {String? id, - String? title, - LocationEntity? location, - UserEntity? createdBy}) { + LandMarkEntity copywith({ + String? id, + String? title, + String? icon, + LocationEntity? location, + UserEntity? createdBy, + }) { return LandMarkEntity( - id: id ?? this.id, - title: title ?? this.title, - location: location ?? this.location, - createdBy: createdBy ?? this.createdBy); + id: id ?? this.id, + title: title ?? this.title, + icon: icon ?? this.icon, + location: location ?? this.location, + createdBy: createdBy ?? this.createdBy, + ); } } diff --git a/lib/domain/entities/landmark/landmark_entity.freezed.dart b/lib/domain/entities/landmark/landmark_entity.freezed.dart index e0aa6955..4afc9a0c 100644 --- a/lib/domain/entities/landmark/landmark_entity.freezed.dart +++ b/lib/domain/entities/landmark/landmark_entity.freezed.dart @@ -18,10 +18,13 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$LandMarkEntity { String? get id => throw _privateConstructorUsedError; String? get title => throw _privateConstructorUsedError; + String? get icon => throw _privateConstructorUsedError; LocationEntity? get location => throw _privateConstructorUsedError; UserEntity? get createdBy => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of LandMarkEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $LandMarkEntityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -35,6 +38,7 @@ abstract class $LandMarkEntityCopyWith<$Res> { $Res call( {String? id, String? title, + String? icon, LocationEntity? location, UserEntity? createdBy}); @@ -52,11 +56,14 @@ class _$LandMarkEntityCopyWithImpl<$Res, $Val extends LandMarkEntity> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of LandMarkEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? title = freezed, + Object? icon = freezed, Object? location = freezed, Object? createdBy = freezed, }) { @@ -69,6 +76,10 @@ class _$LandMarkEntityCopyWithImpl<$Res, $Val extends LandMarkEntity> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, + icon: freezed == icon + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as String?, location: freezed == location ? _value.location : location // ignore: cast_nullable_to_non_nullable @@ -80,6 +91,8 @@ class _$LandMarkEntityCopyWithImpl<$Res, $Val extends LandMarkEntity> ) as $Val); } + /// Create a copy of LandMarkEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $LocationEntityCopyWith<$Res>? get location { @@ -92,6 +105,8 @@ class _$LandMarkEntityCopyWithImpl<$Res, $Val extends LandMarkEntity> }); } + /// Create a copy of LandMarkEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserEntityCopyWith<$Res>? get createdBy { @@ -116,6 +131,7 @@ abstract class _$$LandMarkEntityImplCopyWith<$Res> $Res call( {String? id, String? title, + String? icon, LocationEntity? location, UserEntity? createdBy}); @@ -133,11 +149,14 @@ class __$$LandMarkEntityImplCopyWithImpl<$Res> _$LandMarkEntityImpl _value, $Res Function(_$LandMarkEntityImpl) _then) : super(_value, _then); + /// Create a copy of LandMarkEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = freezed, Object? title = freezed, + Object? icon = freezed, Object? location = freezed, Object? createdBy = freezed, }) { @@ -150,6 +169,10 @@ class __$$LandMarkEntityImplCopyWithImpl<$Res> ? _value.title : title // ignore: cast_nullable_to_non_nullable as String?, + icon: freezed == icon + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as String?, location: freezed == location ? _value.location : location // ignore: cast_nullable_to_non_nullable @@ -166,20 +189,22 @@ class __$$LandMarkEntityImplCopyWithImpl<$Res> class _$LandMarkEntityImpl implements _LandMarkEntity { const _$LandMarkEntityImpl( - {this.id, this.title, this.location, this.createdBy}); + {this.id, this.title, this.icon, this.location, this.createdBy}); @override final String? id; @override final String? title; @override + final String? icon; + @override final LocationEntity? location; @override final UserEntity? createdBy; @override String toString() { - return 'LandMarkEntity(id: $id, title: $title, location: $location, createdBy: $createdBy)'; + return 'LandMarkEntity(id: $id, title: $title, icon: $icon, location: $location, createdBy: $createdBy)'; } @override @@ -189,6 +214,7 @@ class _$LandMarkEntityImpl implements _LandMarkEntity { other is _$LandMarkEntityImpl && (identical(other.id, id) || other.id == id) && (identical(other.title, title) || other.title == title) && + (identical(other.icon, icon) || other.icon == icon) && (identical(other.location, location) || other.location == location) && (identical(other.createdBy, createdBy) || @@ -196,9 +222,12 @@ class _$LandMarkEntityImpl implements _LandMarkEntity { } @override - int get hashCode => Object.hash(runtimeType, id, title, location, createdBy); + int get hashCode => + Object.hash(runtimeType, id, title, icon, location, createdBy); - @JsonKey(ignore: true) + /// Create a copy of LandMarkEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LandMarkEntityImplCopyWith<_$LandMarkEntityImpl> get copyWith => @@ -210,6 +239,7 @@ abstract class _LandMarkEntity implements LandMarkEntity { const factory _LandMarkEntity( {final String? id, final String? title, + final String? icon, final LocationEntity? location, final UserEntity? createdBy}) = _$LandMarkEntityImpl; @@ -218,11 +248,16 @@ abstract class _LandMarkEntity implements LandMarkEntity { @override String? get title; @override + String? get icon; + @override LocationEntity? get location; @override UserEntity? get createdBy; + + /// Create a copy of LandMarkEntity + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$LandMarkEntityImplCopyWith<_$LandMarkEntityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/domain/entities/location/location_entity.freezed.dart b/lib/domain/entities/location/location_entity.freezed.dart index 833de3e0..52427de8 100644 --- a/lib/domain/entities/location/location_entity.freezed.dart +++ b/lib/domain/entities/location/location_entity.freezed.dart @@ -20,7 +20,9 @@ mixin _$LocationEntity { String? get lat => throw _privateConstructorUsedError; String? get lon => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of LocationEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $LocationEntityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -44,6 +46,8 @@ class _$LocationEntityCopyWithImpl<$Res, $Val extends LocationEntity> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of LocationEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -87,6 +91,8 @@ class __$$LocationEntityImplCopyWithImpl<$Res> _$LocationEntityImpl _value, $Res Function(_$LocationEntityImpl) _then) : super(_value, _then); + /// Create a copy of LocationEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -141,7 +147,9 @@ class _$LocationEntityImpl implements _LocationEntity { @override int get hashCode => Object.hash(runtimeType, id, lat, lon); - @JsonKey(ignore: true) + /// Create a copy of LocationEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LocationEntityImplCopyWith<_$LocationEntityImpl> get copyWith => @@ -161,8 +169,11 @@ abstract class _LocationEntity implements LocationEntity { String? get lat; @override String? get lon; + + /// Create a copy of LocationEntity + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$LocationEntityImplCopyWith<_$LocationEntityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/domain/entities/subscriptions/beacon_locations_entity/beacon_locations_entity.freezed.dart b/lib/domain/entities/subscriptions/beacon_locations_entity/beacon_locations_entity.freezed.dart index 592eece5..bffe4a5b 100644 --- a/lib/domain/entities/subscriptions/beacon_locations_entity/beacon_locations_entity.freezed.dart +++ b/lib/domain/entities/subscriptions/beacon_locations_entity/beacon_locations_entity.freezed.dart @@ -21,7 +21,9 @@ mixin _$BeaconLocationsEntity { LandMarkEntity? get landmark => throw _privateConstructorUsedError; UserEntity? get user => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of BeaconLocationsEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $BeaconLocationsEntityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -54,6 +56,8 @@ class _$BeaconLocationsEntityCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of BeaconLocationsEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -82,6 +86,8 @@ class _$BeaconLocationsEntityCopyWithImpl<$Res, ) as $Val); } + /// Create a copy of BeaconLocationsEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserEntityCopyWith<$Res>? get userSOS { @@ -94,6 +100,8 @@ class _$BeaconLocationsEntityCopyWithImpl<$Res, }); } + /// Create a copy of BeaconLocationsEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $LandMarkEntityCopyWith<$Res>? get landmark { @@ -106,6 +114,8 @@ class _$BeaconLocationsEntityCopyWithImpl<$Res, }); } + /// Create a copy of BeaconLocationsEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserEntityCopyWith<$Res>? get user { @@ -151,6 +161,8 @@ class __$$BeaconLocationsEntityImplCopyWithImpl<$Res> $Res Function(_$BeaconLocationsEntityImpl) _then) : super(_value, _then); + /// Create a copy of BeaconLocationsEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -228,7 +240,9 @@ class _$BeaconLocationsEntityImpl implements _BeaconLocationsEntity { int get hashCode => Object.hash(runtimeType, userSOS, const DeepCollectionEquality().hash(_route), landmark, user); - @JsonKey(ignore: true) + /// Create a copy of BeaconLocationsEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$BeaconLocationsEntityImplCopyWith<_$BeaconLocationsEntityImpl> @@ -251,8 +265,11 @@ abstract class _BeaconLocationsEntity implements BeaconLocationsEntity { LandMarkEntity? get landmark; @override UserEntity? get user; + + /// Create a copy of BeaconLocationsEntity + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$BeaconLocationsEntityImplCopyWith<_$BeaconLocationsEntityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/domain/entities/subscriptions/join_leave_beacon_entity/join_leave_beacon_entity.freezed.dart b/lib/domain/entities/subscriptions/join_leave_beacon_entity/join_leave_beacon_entity.freezed.dart index 51bd776c..78a01a74 100644 --- a/lib/domain/entities/subscriptions/join_leave_beacon_entity/join_leave_beacon_entity.freezed.dart +++ b/lib/domain/entities/subscriptions/join_leave_beacon_entity/join_leave_beacon_entity.freezed.dart @@ -19,7 +19,9 @@ mixin _$JoinLeaveBeaconEntity { UserEntity? get newfollower => throw _privateConstructorUsedError; UserEntity? get inactiveuser => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of JoinLeaveBeaconEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $JoinLeaveBeaconEntityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -47,6 +49,8 @@ class _$JoinLeaveBeaconEntityCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of JoinLeaveBeaconEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -65,6 +69,8 @@ class _$JoinLeaveBeaconEntityCopyWithImpl<$Res, ) as $Val); } + /// Create a copy of JoinLeaveBeaconEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserEntityCopyWith<$Res>? get newfollower { @@ -77,6 +83,8 @@ class _$JoinLeaveBeaconEntityCopyWithImpl<$Res, }); } + /// Create a copy of JoinLeaveBeaconEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserEntityCopyWith<$Res>? get inactiveuser { @@ -116,6 +124,8 @@ class __$$JoinLeaveBeaconEntityImplCopyWithImpl<$Res> $Res Function(_$JoinLeaveBeaconEntityImpl) _then) : super(_value, _then); + /// Create a copy of JoinLeaveBeaconEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -164,7 +174,9 @@ class _$JoinLeaveBeaconEntityImpl implements _JoinLeaveBeaconEntity { @override int get hashCode => Object.hash(runtimeType, newfollower, inactiveuser); - @JsonKey(ignore: true) + /// Create a copy of JoinLeaveBeaconEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$JoinLeaveBeaconEntityImplCopyWith<_$JoinLeaveBeaconEntityImpl> @@ -181,8 +193,11 @@ abstract class _JoinLeaveBeaconEntity implements JoinLeaveBeaconEntity { UserEntity? get newfollower; @override UserEntity? get inactiveuser; + + /// Create a copy of JoinLeaveBeaconEntity + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$JoinLeaveBeaconEntityImplCopyWith<_$JoinLeaveBeaconEntityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/domain/entities/subscriptions/updated_group_entity/updated_group_entity.freezed.dart b/lib/domain/entities/subscriptions/updated_group_entity/updated_group_entity.freezed.dart index 2e18afd3..0774f290 100644 --- a/lib/domain/entities/subscriptions/updated_group_entity/updated_group_entity.freezed.dart +++ b/lib/domain/entities/subscriptions/updated_group_entity/updated_group_entity.freezed.dart @@ -22,7 +22,9 @@ mixin _$UpdatedGroupEntity { BeaconEntity? get newBeacon => throw _privateConstructorUsedError; UserEntity? get newUser => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of UpdatedGroupEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $UpdatedGroupEntityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -56,6 +58,8 @@ class _$UpdatedGroupEntityCopyWithImpl<$Res, $Val extends UpdatedGroupEntity> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of UpdatedGroupEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -89,6 +93,8 @@ class _$UpdatedGroupEntityCopyWithImpl<$Res, $Val extends UpdatedGroupEntity> ) as $Val); } + /// Create a copy of UpdatedGroupEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $BeaconEntityCopyWith<$Res>? get deletedBeacon { @@ -101,6 +107,8 @@ class _$UpdatedGroupEntityCopyWithImpl<$Res, $Val extends UpdatedGroupEntity> }); } + /// Create a copy of UpdatedGroupEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $BeaconEntityCopyWith<$Res>? get updatedBeacon { @@ -113,6 +121,8 @@ class _$UpdatedGroupEntityCopyWithImpl<$Res, $Val extends UpdatedGroupEntity> }); } + /// Create a copy of UpdatedGroupEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $BeaconEntityCopyWith<$Res>? get newBeacon { @@ -125,6 +135,8 @@ class _$UpdatedGroupEntityCopyWithImpl<$Res, $Val extends UpdatedGroupEntity> }); } + /// Create a copy of UpdatedGroupEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserEntityCopyWith<$Res>? get newUser { @@ -171,6 +183,8 @@ class __$$UpdatedGroupEntityImplCopyWithImpl<$Res> $Res Function(_$UpdatedGroupEntityImpl) _then) : super(_value, _then); + /// Create a copy of UpdatedGroupEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -250,7 +264,9 @@ class _$UpdatedGroupEntityImpl implements _UpdatedGroupEntity { int get hashCode => Object.hash( runtimeType, id, deletedBeacon, updatedBeacon, newBeacon, newUser); - @JsonKey(ignore: true) + /// Create a copy of UpdatedGroupEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$UpdatedGroupEntityImplCopyWith<_$UpdatedGroupEntityImpl> get copyWith => @@ -276,8 +292,11 @@ abstract class _UpdatedGroupEntity implements UpdatedGroupEntity { BeaconEntity? get newBeacon; @override UserEntity? get newUser; + + /// Create a copy of UpdatedGroupEntity + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$UpdatedGroupEntityImplCopyWith<_$UpdatedGroupEntityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/domain/entities/user/user_entity.dart b/lib/domain/entities/user/user_entity.dart index 572c9031..f3ee0fcc 100644 --- a/lib/domain/entities/user/user_entity.dart +++ b/lib/domain/entities/user/user_entity.dart @@ -7,16 +7,18 @@ part 'user_entity.freezed.dart'; @freezed class UserEntity with _$UserEntity { - const factory UserEntity( - {String? id, - List? groups, - List? beacons, - String? authToken, - String? email, - bool? isGuest, - String? name, - bool? isVerified, - LocationEntity? location}) = _UserEntity; + const factory UserEntity({ + String? id, + List? groups, + List? beacons, + String? authToken, + String? email, + bool? isGuest, + String? name, + bool? isVerified, + LocationEntity? location, + String? imageUrl, // Add this line + }) = _UserEntity; } extension UserEntityCopyWithExtension on UserEntity { @@ -30,16 +32,19 @@ extension UserEntityCopyWithExtension on UserEntity { String? name, bool? isVerified, LocationEntity? location, + String? imageUrl, // Add this line }) { return UserEntity( - id: id ?? this.id, - groups: groups ?? this.groups, - beacons: beacons ?? this.beacons, - authToken: authToken ?? this.authToken, - email: email ?? this.email, - isGuest: isGuest ?? this.isGuest, - name: name ?? this.name, - location: location ?? this.location, - isVerified: isVerified ?? this.isVerified); + id: id ?? this.id, + groups: groups ?? this.groups, + beacons: beacons ?? this.beacons, + authToken: authToken ?? this.authToken, + email: email ?? this.email, + isGuest: isGuest ?? this.isGuest, + name: name ?? this.name, + location: location ?? this.location, + isVerified: isVerified ?? this.isVerified, + imageUrl: imageUrl ?? this.imageUrl, // Add this line + ); } } diff --git a/lib/domain/entities/user/user_entity.freezed.dart b/lib/domain/entities/user/user_entity.freezed.dart index ee0b0074..bc7271ed 100644 --- a/lib/domain/entities/user/user_entity.freezed.dart +++ b/lib/domain/entities/user/user_entity.freezed.dart @@ -25,8 +25,11 @@ mixin _$UserEntity { String? get name => throw _privateConstructorUsedError; bool? get isVerified => throw _privateConstructorUsedError; LocationEntity? get location => throw _privateConstructorUsedError; + String? get imageUrl => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of UserEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $UserEntityCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -46,7 +49,8 @@ abstract class $UserEntityCopyWith<$Res> { bool? isGuest, String? name, bool? isVerified, - LocationEntity? location}); + LocationEntity? location, + String? imageUrl}); $LocationEntityCopyWith<$Res>? get location; } @@ -61,6 +65,8 @@ class _$UserEntityCopyWithImpl<$Res, $Val extends UserEntity> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of UserEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -73,6 +79,7 @@ class _$UserEntityCopyWithImpl<$Res, $Val extends UserEntity> Object? name = freezed, Object? isVerified = freezed, Object? location = freezed, + Object? imageUrl = freezed, }) { return _then(_value.copyWith( id: freezed == id @@ -111,9 +118,15 @@ class _$UserEntityCopyWithImpl<$Res, $Val extends UserEntity> ? _value.location : location // ignore: cast_nullable_to_non_nullable as LocationEntity?, + imageUrl: freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, ) as $Val); } + /// Create a copy of UserEntity + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $LocationEntityCopyWith<$Res>? get location { @@ -144,7 +157,8 @@ abstract class _$$UserEntityImplCopyWith<$Res> bool? isGuest, String? name, bool? isVerified, - LocationEntity? location}); + LocationEntity? location, + String? imageUrl}); @override $LocationEntityCopyWith<$Res>? get location; @@ -158,6 +172,8 @@ class __$$UserEntityImplCopyWithImpl<$Res> _$UserEntityImpl _value, $Res Function(_$UserEntityImpl) _then) : super(_value, _then); + /// Create a copy of UserEntity + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -170,6 +186,7 @@ class __$$UserEntityImplCopyWithImpl<$Res> Object? name = freezed, Object? isVerified = freezed, Object? location = freezed, + Object? imageUrl = freezed, }) { return _then(_$UserEntityImpl( id: freezed == id @@ -208,6 +225,10 @@ class __$$UserEntityImplCopyWithImpl<$Res> ? _value.location : location // ignore: cast_nullable_to_non_nullable as LocationEntity?, + imageUrl: freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -224,7 +245,8 @@ class _$UserEntityImpl implements _UserEntity { this.isGuest, this.name, this.isVerified, - this.location}) + this.location, + this.imageUrl}) : _groups = groups, _beacons = beacons; @@ -262,10 +284,12 @@ class _$UserEntityImpl implements _UserEntity { final bool? isVerified; @override final LocationEntity? location; + @override + final String? imageUrl; @override String toString() { - return 'UserEntity(id: $id, groups: $groups, beacons: $beacons, authToken: $authToken, email: $email, isGuest: $isGuest, name: $name, isVerified: $isVerified, location: $location)'; + return 'UserEntity(id: $id, groups: $groups, beacons: $beacons, authToken: $authToken, email: $email, isGuest: $isGuest, name: $name, isVerified: $isVerified, location: $location, imageUrl: $imageUrl)'; } @override @@ -284,7 +308,9 @@ class _$UserEntityImpl implements _UserEntity { (identical(other.isVerified, isVerified) || other.isVerified == isVerified) && (identical(other.location, location) || - other.location == location)); + other.location == location) && + (identical(other.imageUrl, imageUrl) || + other.imageUrl == imageUrl)); } @override @@ -298,9 +324,12 @@ class _$UserEntityImpl implements _UserEntity { isGuest, name, isVerified, - location); + location, + imageUrl); - @JsonKey(ignore: true) + /// Create a copy of UserEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$UserEntityImplCopyWith<_$UserEntityImpl> get copyWith => @@ -317,7 +346,8 @@ abstract class _UserEntity implements UserEntity { final bool? isGuest, final String? name, final bool? isVerified, - final LocationEntity? location}) = _$UserEntityImpl; + final LocationEntity? location, + final String? imageUrl}) = _$UserEntityImpl; @override String? get id; @@ -338,7 +368,12 @@ abstract class _UserEntity implements UserEntity { @override LocationEntity? get location; @override - @JsonKey(ignore: true) + String? get imageUrl; + + /// Create a copy of UserEntity + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) _$$UserEntityImplCopyWith<_$UserEntityImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/domain/repositories/auth_repository.dart b/lib/domain/repositories/auth_repository.dart index ff88d7d1..b53350c8 100644 --- a/lib/domain/repositories/auth_repository.dart +++ b/lib/domain/repositories/auth_repository.dart @@ -12,7 +12,8 @@ abstract class AuthRepository { // Login function Future> login(String email, String password); - Future> oAuth(String name, String email); + Future> oAuth( + String name, String email, String? imageUrl); Future> sendVerificationCode(); diff --git a/lib/domain/repositories/hike_repository.dart b/lib/domain/repositories/hike_repository.dart index 8b8a31db..38485d75 100644 --- a/lib/domain/repositories/hike_repository.dart +++ b/lib/domain/repositories/hike_repository.dart @@ -12,7 +12,7 @@ abstract class HikeRepository { String beaconId, LatLng position); Future> fetchBeaconDetails(String beaconId); Future> createLandMark( - String id, String title, String lat, String lon); + String id, String title, String lat, String lon, String icon); Future> changeUserLocation(String id, LatLng latLng); Future> sos(String beaconId); Stream> beaconLocationsSubscription( diff --git a/lib/domain/repositories/home_repository.dart b/lib/domain/repositories/home_repository.dart index 237ba5c3..f7513d45 100644 --- a/lib/domain/repositories/home_repository.dart +++ b/lib/domain/repositories/home_repository.dart @@ -10,4 +10,5 @@ abstract class HomeRepository { Stream> groupUpdateSubscription( List groupIds); Future> changeShortcode(String groupId); + Future> updateUserImage(String userId, String? imageUrl); } diff --git a/lib/domain/usecase/auth_usecase.dart b/lib/domain/usecase/auth_usecase.dart index 7a4a875f..8081e915 100644 --- a/lib/domain/usecase/auth_usecase.dart +++ b/lib/domain/usecase/auth_usecase.dart @@ -17,8 +17,9 @@ class AuthUseCase { return authRepository.login(email, password); } - Future> oAuthUseCase(String name, String email) async { - return authRepository.oAuth(name, email); + Future> oAuthUseCase( + String name, String email, String? imageUrl) async { + return authRepository.oAuth(name, email, imageUrl); } Future> getUserInfoUseCase() async { diff --git a/lib/domain/usecase/hike_usecase.dart b/lib/domain/usecase/hike_usecase.dart index 49dfb46c..bb39b011 100644 --- a/lib/domain/usecase/hike_usecase.dart +++ b/lib/domain/usecase/hike_usecase.dart @@ -23,8 +23,8 @@ class HikeUseCase { } Future> createLandMark( - String id, String title, String lat, String lon) { - return hikeRepository.createLandMark(id, title, lat, lon); + String id, String title, String lat, String lon, String icon) { + return hikeRepository.createLandMark(id, title, lat, lon, icon); } Future> changeUserLocation(String id, LatLng latlng) { diff --git a/lib/domain/usecase/home_usecase.dart b/lib/domain/usecase/home_usecase.dart index 359b9728..b60db596 100644 --- a/lib/domain/usecase/home_usecase.dart +++ b/lib/domain/usecase/home_usecase.dart @@ -32,4 +32,8 @@ class HomeUseCase { Future> changeShortcode(String groupId) { return homeRepository.changeShortcode(groupId); } + + Future> updateUserImage(String userId, String? imageUrl) { + return homeRepository.updateUserImage(userId, imageUrl); + } } diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index af84ee9b..11205422 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -50,21 +50,23 @@ class DefaultFirebaseOptions { } static const FirebaseOptions android = FirebaseOptions( - apiKey: '', - appId: '', - messagingSenderId: '', - projectId: '', - storageBucket: '', + apiKey: 'AIzaSyDhR4FOBzIkmS0Pz0gfqQISkcdFzomsxF4', + appId: '1:724090909650:android:33a605e98b134abe8f8e0b', + messagingSenderId: '724090909650', + projectId: 'beacon-b75bf', + storageBucket: 'beacon-b75bf.firebasestorage.app', ); static const FirebaseOptions ios = FirebaseOptions( - apiKey: '', - appId: '', - messagingSenderId: '', - projectId: '', - storageBucket: '', - androidClientId: '', - iosClientId: '', - iosBundleId: '', + apiKey: 'AIzaSyCRbwD5ECjgrwYHok_naB2OKGAy3whA6SY', + appId: '1:724090909650:ios:7dfc01703caf03d48f8e0b', + messagingSenderId: '724090909650', + projectId: 'beacon-b75bf', + storageBucket: 'beacon-b75bf.firebasestorage.app', + androidClientId: + '724090909650-24e79l5pait1nhksbsh9260duvqps6ri.apps.googleusercontent.com', + iosClientId: + '724090909650-441i6sm3rleavnrcdibdgjn3n1j9lqot.apps.googleusercontent.com', + iosBundleId: 'com.ccextractor.beaconmobile', ); } diff --git a/lib/presentation/auth/auth_cubit/auth_cubit.dart b/lib/presentation/auth/auth_cubit/auth_cubit.dart index 04fb0d7a..550893f4 100644 --- a/lib/presentation/auth/auth_cubit/auth_cubit.dart +++ b/lib/presentation/auth/auth_cubit/auth_cubit.dart @@ -46,7 +46,7 @@ class AuthCubit extends Cubit { Future login(String email, String password) async { emit(AuthLoadingState()); final dataState = await authUseCase.loginUserCase(email, password); - + print("Data State from login: ${dataState.data?.imageUrl}"); if (dataState is DataSuccess && dataState.data != null) { if (dataState.data!.isVerified == false) { // show verification screen @@ -79,10 +79,12 @@ class AuthCubit extends Cubit { ); final gAuth = await _googleSignIn.signIn(); + print("Google Auth: ${gAuth}"); if (gAuth != null && gAuth.displayName != null) { - var dataState = - await authUseCase.oAuthUseCase(gAuth.displayName!, gAuth.email); + // pass imageurl + var dataState = await authUseCase.oAuthUseCase( + gAuth.displayName!, gAuth.email, gAuth.photoUrl); if (dataState is DataSuccess && dataState.data != null) { emit(SuccessState()); diff --git a/lib/presentation/auth/auth_cubit/auth_state.freezed.dart b/lib/presentation/auth/auth_cubit/auth_state.freezed.dart index 8bda4037..5431a825 100644 --- a/lib/presentation/auth/auth_cubit/auth_state.freezed.dart +++ b/lib/presentation/auth/auth_cubit/auth_state.freezed.dart @@ -89,6 +89,9 @@ class _$AuthStateCopyWithImpl<$Res, $Val extends AuthState> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -105,6 +108,9 @@ class __$$InitialAuthStateImplCopyWithImpl<$Res> __$$InitialAuthStateImplCopyWithImpl(_$InitialAuthStateImpl _value, $Res Function(_$InitialAuthStateImpl) _then) : super(_value, _then); + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -225,6 +231,9 @@ class __$$AuthLoadingStateImplCopyWithImpl<$Res> __$$AuthLoadingStateImplCopyWithImpl(_$AuthLoadingStateImpl _value, $Res Function(_$AuthLoadingStateImpl) _then) : super(_value, _then); + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -348,6 +357,8 @@ class __$$AuthErrorStateImplCopyWithImpl<$Res> _$AuthErrorStateImpl _value, $Res Function(_$AuthErrorStateImpl) _then) : super(_value, _then); + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -386,7 +397,9 @@ class _$AuthErrorStateImpl implements AuthErrorState { @override int get hashCode => Object.hash(runtimeType, error); - @JsonKey(ignore: true) + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$AuthErrorStateImplCopyWith<_$AuthErrorStateImpl> get copyWith => @@ -478,7 +491,10 @@ abstract class AuthErrorState implements AuthState { const factory AuthErrorState({final String? error}) = _$AuthErrorStateImpl; String? get error; - @JsonKey(ignore: true) + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$AuthErrorStateImplCopyWith<_$AuthErrorStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -500,6 +516,8 @@ class __$$SuccessStateImplCopyWithImpl<$Res> _$SuccessStateImpl _value, $Res Function(_$SuccessStateImpl) _then) : super(_value, _then); + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -538,7 +556,9 @@ class _$SuccessStateImpl implements SuccessState { @override int get hashCode => Object.hash(runtimeType, message); - @JsonKey(ignore: true) + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SuccessStateImplCopyWith<_$SuccessStateImpl> get copyWith => @@ -629,7 +649,10 @@ abstract class SuccessState implements AuthState { const factory SuccessState({final String? message}) = _$SuccessStateImpl; String? get message; - @JsonKey(ignore: true) + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$SuccessStateImplCopyWith<_$SuccessStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -649,6 +672,9 @@ class __$$AuthVerificationStateImplCopyWithImpl<$Res> __$$AuthVerificationStateImplCopyWithImpl(_$AuthVerificationStateImpl _value, $Res Function(_$AuthVerificationStateImpl) _then) : super(_value, _then); + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc diff --git a/lib/presentation/auth/auth_screen.dart b/lib/presentation/auth/auth_screen.dart index 06ac2cb3..928fddeb 100644 --- a/lib/presentation/auth/auth_screen.dart +++ b/lib/presentation/auth/auth_screen.dart @@ -65,78 +65,160 @@ class _AuthScreenState extends State @override Widget build(BuildContext context) { - print( - "_currentPage: $_currentPage", - ); - Size screensize = MediaQuery.of(context).size; + final screenSize = MediaQuery.of(context).size; + final screenWidth = screenSize.width; + final screenHeight = screenSize.height; + + // Adaptive padding + double getHorizontalPadding() { + if (screenWidth < 360) return screenWidth * 0.05; // 5% for small phones + if (screenWidth < 400) return screenWidth * 0.06; // 6% for medium phones + if (screenWidth < 600) return screenWidth * 0.08; // 8% for large phones + return screenWidth * 0.12; // 12% for tablets + } + + // Adaptive top spacing + double getTopSpacing() { + if (screenHeight < 600) return screenHeight * 0.08; // Small screens + if (screenHeight < 700) return screenHeight * 0.10; // Medium screens + if (screenHeight < 800) return screenHeight * 0.12; // Large screens + return screenHeight * 0.15; // Very large screens + } + + // Adaptive logo size + double getLogoWidth() { + if (screenWidth < 360) + return screenWidth * 0.65; // Smaller logo for small phones + if (screenWidth < 400) return screenWidth * 0.68; // Medium phones + if (screenWidth < 600) return screenWidth * 0.70; // Large phones + return screenWidth * 0.60; // Tablets (smaller relative size) + } + + // Adaptive spacing after logo + double getLogoBottomSpacing() { + if (screenHeight < 600) return screenHeight * 0.04; // Small screens + if (screenHeight < 700) return screenHeight * 0.06; // Medium screens + if (screenHeight < 800) return screenHeight * 0.07; // Large screens + return screenHeight * 0.08; // Very large screens + } + + // Adaptive text size for welcome message + TextStyle getWelcomeTextStyle() { + final baseStyle = Theme.of(context).textTheme.headlineMedium; + if (screenWidth < 360) { + return baseStyle?.copyWith(fontSize: 20) ?? TextStyle(fontSize: 20); + } + if (screenWidth < 400) { + return baseStyle?.copyWith(fontSize: 22) ?? TextStyle(fontSize: 22); + } + return baseStyle ?? TextStyle(fontSize: 24); + } + + final horizontalPadding = getHorizontalPadding(); + final topSpacing = getTopSpacing(); + final logoWidth = getLogoWidth(); + final logoBottomSpacing = getLogoBottomSpacing(); + final welcomeTextStyle = getWelcomeTextStyle(); return PopScope( - canPop: false, - onPopInvokedWithResult: (bool didPop, Object? result) async { - if (didPop) { - return; - } + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) async { + if (didPop) { + return; + } - bool? popped = await onPopHome(); - if (popped == true) { - await SystemNavigator.pop(); + bool? popped = await onPopHome(); + if (popped == true) { + await SystemNavigator.pop(); + } + return; + }, + child: BlocConsumer( + listener: (context, state) { + if (state is SuccessState) { + appRouter.replaceNamed('/home'); + state.message != null + ? utils.showSnackBar(state.message!, context) + : null; + } else if (state is AuthVerificationState) { + context.read().navigate(); + } else if (state is AuthErrorState) { + utils.showSnackBar( + state.error!, + context, + duration: Duration(seconds: 2), + ); } - return; }, - child: BlocConsumer( - listener: (context, state) { - if (state is SuccessState) { - appRouter.replaceNamed('/home'); - state.message != null - ? utils.showSnackBar(state.message!, context) - : null; - } else if (state is AuthVerificationState) { - context.read().navigate(); - } else if (state is AuthErrorState) { - utils.showSnackBar(state.error!, context, - duration: Duration(seconds: 2)); - } - }, - builder: (context, state) { - return state is AuthLoadingState - ? LoadingScreen() - : Scaffold( - resizeToAvoidBottomInset: true, - body: SafeArea( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: screensize.width * 0.08), - child: Column( - children: [ - SizedBox(height: screensize.height * 0.15), - Text( - 'welcome to', - style: Theme.of(context).textTheme.headlineMedium, - ), - SizedBox(height: screensize.height * 0.01), - Image.asset( - 'images/beacon_logo.png', - width: screensize.width * 0.7, - fit: BoxFit.contain, - ), - SizedBox(height: screensize.height * 0.08), - Expanded( - child: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: _pageController, - children: [ - _buildSignIn(context), - _buildSignUp(context), + builder: (context, state) { + return state is AuthLoadingState + ? LoadingScreen() + : Scaffold( + resizeToAvoidBottomInset: true, + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + ), + child: Column( + children: [ + SizedBox(height: topSpacing), + Text( + 'welcome to', + style: welcomeTextStyle, + textAlign: TextAlign.center, + ), + SizedBox(height: screenHeight * 0.01), + Container( + constraints: BoxConstraints( + maxWidth: 300, // Maximum logo width + minWidth: 200, // Minimum logo width + ), + child: Image.asset( + 'images/beacon_logo.png', + width: logoWidth, + fit: BoxFit.contain, + ), + ), + SizedBox(height: logoBottomSpacing), + Expanded( + child: Container( + constraints: BoxConstraints( + maxWidth: + 500, // Maximum form width for tablets + ), + child: PageView( + physics: + const NeverScrollableScrollPhysics(), + controller: _pageController, + children: [ + _buildSignIn(context), + _buildSignUp(context), + ], + ), + ), + ), + // Add some bottom padding for very small screens + if (screenHeight < 600) SizedBox(height: 20), ], ), ), - ], - ), - ), + ), + ); + }, ), - ); - }, - )); + ), + ); + }, + ), + ); } GlobalKey _signInFormKey = GlobalKey(); @@ -234,17 +316,61 @@ class _AuthScreenState extends State Widget _buildSignUp(BuildContext context) { final authCubit = BlocProvider.of(context); - Size screensize = MediaQuery.of(context).size; + final screenSize = MediaQuery.of(context).size; + final screenWidth = screenSize.width; + final screenHeight = screenSize.height; + + // Adaptive padding based on screen size + double getHorizontalPadding() { + if (screenWidth < 360) return 20; // Small phones + if (screenWidth < 400) return 35; // Medium phones + if (screenWidth < 600) return 50; // Large phones + return 70; // Tablets + } + + // Adaptive spacing + double getVerticalSpacing() { + if (screenHeight < 600) return 0.8.h; // Small screens + if (screenHeight < 800) return 1.2.h; // Medium screens + return 1.5.h; // Large screens + } + + // Adaptive button height + double getButtonHeight() { + if (screenHeight < 600) return 40; // Small screens + if (screenHeight < 800) return 45; // Medium screens + return 50; // Large screens + } + + // Adaptive font size + double getButtonFontSize() { + if (screenWidth < 360) return 14; + if (screenWidth < 400) return 15; + return 16; + } + + final horizontalPadding = getHorizontalPadding(); + final verticalSpacing = getVerticalSpacing(); + final buttonHeight = getButtonHeight(); + final buttonFontSize = getButtonFontSize(); + return Container( - width: screensize.width, + width: screenWidth, + padding: + EdgeInsets.symmetric(horizontal: 16), // Base padding for container child: SingleChildScrollView( child: Column( children: [ Form( key: _registerFormKey, child: Container( - width: screensize.width - 70, - child: Column(children: [ + width: screenWidth - (horizontalPadding * 2), + constraints: BoxConstraints( + maxWidth: 400, // Maximum width for tablets + minWidth: 280, // Minimum width for small phones + ), + child: Column( + children: [ CustomTextField( iconData: Icons.person_2_sharp, hintText: 'Name', @@ -253,9 +379,7 @@ class _AuthScreenState extends State nextFocusNode: signUpEmailFocus, validator: Validator.validateName, ), - SizedBox( - height: 1.2.h, - ), + SizedBox(height: verticalSpacing), CustomTextField( iconData: Icons.mail, hintText: 'Email Address', @@ -264,55 +388,67 @@ class _AuthScreenState extends State nextFocusNode: signUpPasswordFocus, validator: Validator.validateEmail, ), - SizedBox( - height: 1.2.h, - ), + SizedBox(height: verticalSpacing), CustomTextField( - iconData: Icons.lock, - hintText: 'Password', - controller: signUpPasswordController, - focusNode: signUpPasswordFocus, - showTrailing: true, - validator: Validator.validatePassword), - ])), - ), - SizedBox( - height: 1.2.h, + iconData: Icons.lock, + hintText: 'Password', + controller: signUpPasswordController, + focusNode: signUpPasswordFocus, + showTrailing: true, + validator: Validator.validatePassword, + ), + ], + ), + ), ), + SizedBox(height: verticalSpacing), Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(5.0)), - ), - child: BlocBuilder( - builder: (context, state) { - return ElevatedButton( - onPressed: () { - if (_registerFormKey.currentState!.validate()) { - authCubit.register( - signUpNameController.text.trim(), - signUpEmailController.text.trim(), - signUpPasswordController.text.trim()); - } else { - utils.showSnackBar( - 'Please complete all the fields', context); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.teal, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - minimumSize: Size(screensize.width - 70, 45)), - child: const Text( - 'Continue with Email', - style: TextStyle( - color: Colors.black, - fontSize: 16, - ), + width: screenWidth - (horizontalPadding * 2), + constraints: BoxConstraints( + maxWidth: 400, // Maximum width for tablets + minWidth: 280, // Minimum width for small phones + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + ), + child: BlocBuilder( + builder: (context, state) { + return ElevatedButton( + onPressed: () { + if (_registerFormKey.currentState!.validate()) { + authCubit.register( + signUpNameController.text.trim(), + signUpEmailController.text.trim(), + signUpPasswordController.text.trim(), + ); + } else { + utils.showSnackBar( + 'Please complete all the fields', + context, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.teal, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), ), - ); - }, - )), + minimumSize: Size(double.infinity, buttonHeight), + padding: EdgeInsets.symmetric(vertical: 12), + ), + child: Text( + 'Continue with Email', + style: TextStyle( + color: Colors.black, + fontSize: buttonFontSize, + fontWeight: FontWeight.w500, + ), + ), + ); + }, + ), + ), + SizedBox(height: verticalSpacing), _switchPageHelper(), ], ), diff --git a/lib/presentation/auth/verfication_screen.dart b/lib/presentation/auth/verfication_screen.dart index 9b1753db..2758ec38 100644 --- a/lib/presentation/auth/verfication_screen.dart +++ b/lib/presentation/auth/verfication_screen.dart @@ -95,6 +95,16 @@ class _VerificationScreenState extends State { color: Colors.black45, ), ), + // check spam folder + SizedBox(height: 2.h), + Text( + 'Check your spam folder if you can\'t \nfind the email', + style: TextStyle( + fontSize: 14, + color: Colors.redAccent, + ), + ), + SizedBox(height: 5.h), // OTP input fields Row( diff --git a/lib/presentation/auth/verification_cubit/verification_state.freezed.dart b/lib/presentation/auth/verification_cubit/verification_state.freezed.dart index 688d823a..7b543783 100644 --- a/lib/presentation/auth/verification_cubit/verification_state.freezed.dart +++ b/lib/presentation/auth/verification_cubit/verification_state.freezed.dart @@ -97,6 +97,9 @@ class _$OTPVerificationStateCopyWithImpl<$Res, final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of OTPVerificationState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -113,6 +116,9 @@ class __$$InitialOTPStateImplCopyWithImpl<$Res> __$$InitialOTPStateImplCopyWithImpl( _$InitialOTPStateImpl _value, $Res Function(_$InitialOTPStateImpl) _then) : super(_value, _then); + + /// Create a copy of OTPVerificationState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -239,6 +245,9 @@ class __$$OTPSendingStateImplCopyWithImpl<$Res> __$$OTPSendingStateImplCopyWithImpl( _$OTPSendingStateImpl _value, $Res Function(_$OTPSendingStateImpl) _then) : super(_value, _then); + + /// Create a copy of OTPVerificationState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -368,6 +377,8 @@ class __$$OTPSentStateImplCopyWithImpl<$Res> _$OTPSentStateImpl _value, $Res Function(_$OTPSentStateImpl) _then) : super(_value, _then); + /// Create a copy of OTPVerificationState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -406,7 +417,9 @@ class _$OTPSentStateImpl implements OTPSentState { @override int get hashCode => Object.hash(runtimeType, otp); - @JsonKey(ignore: true) + /// Create a copy of OTPVerificationState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$OTPSentStateImplCopyWith<_$OTPSentStateImpl> get copyWith => @@ -503,7 +516,10 @@ abstract class OTPSentState implements OTPVerificationState { factory OTPSentState({final String? otp}) = _$OTPSentStateImpl; String? get otp; - @JsonKey(ignore: true) + + /// Create a copy of OTPVerificationState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$OTPSentStateImplCopyWith<_$OTPSentStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -522,6 +538,9 @@ class __$$OTPVerifyingStateImplCopyWithImpl<$Res> __$$OTPVerifyingStateImplCopyWithImpl(_$OTPVerifyingStateImpl _value, $Res Function(_$OTPVerifyingStateImpl) _then) : super(_value, _then); + + /// Create a copy of OTPVerificationState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -648,6 +667,9 @@ class __$$OTPVerifiedStateImplCopyWithImpl<$Res> __$$OTPVerifiedStateImplCopyWithImpl(_$OTPVerifiedStateImpl _value, $Res Function(_$OTPVerifiedStateImpl) _then) : super(_value, _then); + + /// Create a copy of OTPVerificationState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -774,6 +796,9 @@ class __$$OTPFailureStateImplCopyWithImpl<$Res> __$$OTPFailureStateImplCopyWithImpl( _$OTPFailureStateImpl _value, $Res Function(_$OTPFailureStateImpl) _then) : super(_value, _then); + + /// Create a copy of OTPVerificationState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc diff --git a/lib/presentation/group/advance_options_screen.dart b/lib/presentation/group/advance_options_screen.dart new file mode 100644 index 00000000..ef09c35d --- /dev/null +++ b/lib/presentation/group/advance_options_screen.dart @@ -0,0 +1,816 @@ +import 'package:auto_route/annotations.dart'; +import 'package:beacon/config/enviornment_config.dart'; +import 'package:beacon/locator.dart'; +import 'package:beacon/presentation/group/cubit/group_cubit/group_cubit.dart'; +import 'package:beacon/presentation/hike/services/geoapify_service.dart'; +import 'package:duration_picker/duration_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:responsive_sizer/responsive_sizer.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:beacon/data/models/landmark/location_suggestion.dart'; + +import '../../core/utils/constants.dart'; +import '../widgets/hike_button.dart'; +import '../widgets/screen_template.dart'; + +@RoutePage() +class AdvancedOptionsScreen extends StatefulWidget { + final TextEditingController durationController; + final String title; + final bool isScheduled; + final DateTime? startDate; + final TimeOfDay? startTime; + final String groupId; + + const AdvancedOptionsScreen({ + super.key, + required this.durationController, + required this.title, + required this.isScheduled, + this.startDate, + this.startTime, + required this.groupId, + }); + + @override + State createState() => _AdvancedOptionsScreenState(); +} + +class _AdvancedOptionsScreenState extends State { + Duration? duration = Duration(minutes: 5); + Map? weatherData; + bool isLoadingWeather = false; + String? weatherError; + final GeoapifyService _geoapifyService = GeoapifyService(); + final TextEditingController _locationController = TextEditingController(); + List _locationSuggestions = []; + bool _showLocationSuggestions = false; + LocationSuggestion? _selectedLocation; + DateTime? startDate = DateTime.now(); + TimeOfDay? startTime = + TimeOfDay(hour: TimeOfDay.now().hour, minute: TimeOfDay.now().minute + 1); + + @override + void initState() { + super.initState(); + _fetchWeatherForCurrentLocation(); + } + + Future getCurrentLocation() async { + bool serviceEnabled; + LocationPermission permission; + + // Check if location services are enabled + serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + // Location services are not enabled + return Future.error('Location services are disabled.'); + } + + // Check for permission + permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + // Permissions are denied + return Future.error('Location permissions are denied.'); + } + } + + if (permission == LocationPermission.deniedForever) { + // Permissions are permanently denied + return Future.error( + 'Location permissions are permanently denied. Cannot request permissions.'); + } + + // Get current location + return await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + } + + Future _fetchWeatherForCurrentLocation() async { + Position? position = await getCurrentLocation(); + + await _fetchWeatherData( + position?.latitude ?? 0.0, // Default to 0.0 if position is null + position?.longitude ?? 0.0, // Default to 0.0 if position is null + ); // Default to 0.0 if parsing fails + } + + Future _fetchWeatherData(double lat, double lon) async { + setState(() { + isLoadingWeather = true; + weatherError = null; + }); + + try { + final String apiKey = EnvironmentConfig.openWeatherMapApiKey!; + + String apiUrl; + if (widget.isScheduled) { + // Use forecast API for scheduled hikes + apiUrl = + 'https://api.openweathermap.org/data/2.5/forecast?lat=$lat&lon=$lon&appid=$apiKey&units=metric'; + } else { + // Use current weather API for immediate hikes + apiUrl = + 'https://api.openweathermap.org/data/2.5/weather?lat=$lat&lon=$lon&appid=$apiKey&units=metric'; + } + + final response = await http.get(Uri.parse(apiUrl)); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (widget.isScheduled) { + // Process forecast data to find weather for the scheduled date + final forecastWeather = _extractWeatherForScheduledDate(data); + setState(() { + weatherData = forecastWeather; + isLoadingWeather = false; + }); + } else { + // Use current weather data + setState(() { + weatherData = data; + isLoadingWeather = false; + }); + } + } else { + throw Exception('Failed to load weather data: ${response.statusCode}'); + } + } catch (e) { + print('Weather fetch error: $e'); + // Fallback to mock data if API fails + //await _loadMockWeatherData(); + } + } + + Map _extractWeatherForScheduledDate( + Map forecastData) { + final scheduledDate = widget.startDate!; + final scheduledTime = widget.startTime; + + // Convert scheduled date to timestamp + DateTime targetDateTime; + if (scheduledTime != null) { + targetDateTime = DateTime( + scheduledDate.year, + scheduledDate.month, + scheduledDate.day, + scheduledTime.hour, + scheduledTime.minute, + ); + } else { + targetDateTime = DateTime( + scheduledDate.year, + scheduledDate.month, + scheduledDate.day, + 12, // Default to noon if no time specified + 0, + ); + } + + final List forecasts = forecastData['list']; + Map? closestForecast; + Duration smallestDifference = Duration(days: 365); + + // Find the forecast closest to the scheduled time + for (var forecast in forecasts) { + final forecastTime = DateTime.fromMillisecondsSinceEpoch( + forecast['dt'] * 1000, + isUtc: true, + ).toLocal(); + + final difference = (forecastTime.difference(targetDateTime)).abs(); + if (difference < smallestDifference) { + smallestDifference = difference; + closestForecast = forecast; + } + } + + if (closestForecast != null) { + // Transform forecast data to match current weather API format + return { + 'main': closestForecast['main'], + 'weather': closestForecast['weather'], + 'wind': closestForecast['wind'], + 'visibility': closestForecast['visibility'] ?? 10000, + 'name': _selectedLocation?.name ?? + forecastData['city']['name'] ?? + 'Selected Location', + 'dt': closestForecast['dt'], + 'isScheduled': true, + 'scheduledDate': targetDateTime.toIso8601String(), + }; + } + + // Fallback to first forecast if no suitable match found + return { + 'main': forecasts[0]['main'], + 'weather': forecasts[0]['weather'], + 'wind': forecasts[0]['wind'], + 'visibility': forecasts[0]['visibility'] ?? 10000, + 'name': _selectedLocation?.name ?? + forecastData['city']['name'] ?? + 'Selected Location', + 'dt': forecasts[0]['dt'], + 'isScheduled': true, + 'scheduledDate': targetDateTime.toIso8601String(), + }; + } + + Future _searchLocations(String query) async { + if (query.isEmpty) { + setState(() { + _locationSuggestions = []; + _showLocationSuggestions = false; + }); + return; + } + + try { + final suggestions = await _geoapifyService.getLocationSuggestions(query); + setState(() { + _locationSuggestions = suggestions; + _showLocationSuggestions = true; + }); + } catch (e) { + print('Location search error: $e'); + setState(() { + _locationSuggestions = []; + _showLocationSuggestions = false; + }); + } + } + + void _selectLocation(LocationSuggestion location) { + setState(() { + _selectedLocation = location; + _locationController.text = location.name; + _showLocationSuggestions = false; + }); + + // Fetch weather for selected location + _fetchWeatherData(location.latitude, location.longitude); + } + + String _getWeatherTitle() { + if (weatherData == null) return 'Weather'; + + if (widget.isScheduled) { + final scheduledDate = widget.startDate!; + final now = DateTime.now(); + final difference = scheduledDate.difference(now).inDays; + + if (difference == 0) { + return 'Today\'s Weather'; + } else if (difference == 1) { + return 'Tomorrow\'s Weather'; + } else { + return 'Weather Forecast \n (${difference} days from now)'; + } + } + + return 'Current Weather'; + } + + Color _getWeatherRecommendationColor() { + // Example logic: change color based on weather condition + if (weatherData != null && weatherData!['weather'] != null) { + String main = weatherData!['weather'][0]['main'].toString().toLowerCase(); + if (main.contains('rain') || main.contains('storm')) { + return Colors.red; + } else if (main.contains('cloud')) { + return Colors.orange; + } else if (main.contains('clear')) { + return Colors.green; + } + } + return Colors.blue; + } + + IconData _getWeatherRecommendationIcon() { + if (weatherData != null && weatherData!['weather'] != null) { + String main = weatherData!['weather'][0]['main'].toString().toLowerCase(); + if (main.contains('rain') || main.contains('storm')) { + return Icons.warning; + } else if (main.contains('cloud')) { + return Icons.cloud; + } else if (main.contains('clear')) { + return Icons.wb_sunny; + } + } + return Icons.info; + } + + String _getWeatherRecommendation() { + if (weatherData != null && weatherData!['weather'] != null) { + String main = weatherData!['weather'][0]['main'].toString().toLowerCase(); + if (main.contains('rain') || main.contains('storm')) { + return 'It may be unsafe to hike due to rain or storm. Please check local advisories.'; + } else if (main.contains('cloud')) { + return 'Cloudy weather. Stay alert and check for rain updates.'; + } else if (main.contains('clear')) { + return 'Great weather for hiking! Enjoy your hike.'; + } + } + return 'Check weather conditions before starting your hike.'; + } + + Widget _buildWeatherCard() { + if (isLoadingWeather) { + return Card( + elevation: 2, + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + CircularProgressIndicator(color: kYellow), + SizedBox(width: 4.w), + Text( + widget.isScheduled + ? 'Loading weather forecast...' + : 'Loading weather conditions...', + style: TextStyle(fontSize: 14.sp), + ), + ], + ), + ), + ); + } + + if (weatherError != null) { + return Card( + elevation: 2, + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.error, color: Colors.red, size: 20.sp), + SizedBox(width: 4.w), + Expanded( + child: Text( + weatherError!, + style: TextStyle(fontSize: 14.sp), + ), + ), + IconButton( + icon: Icon(Icons.refresh, size: 20.sp), + onPressed: () { + if (_selectedLocation != null) { + _fetchWeatherData( + _selectedLocation!.latitude, + _selectedLocation!.longitude, + ); + } else { + _fetchWeatherForCurrentLocation(); + } + }, + ), + ], + ), + ), + ); + } + + if (weatherData == null) return SizedBox.shrink(); + + final temp = weatherData!['main']['temp']; + final feelsLike = weatherData!['main']['feels_like']; + final humidity = weatherData!['main']['humidity']; + final windSpeed = weatherData!['wind']['speed']; + final description = weatherData!['weather'][0]['description']; + final location = weatherData!['name']; + + return Card( + elevation: 2, + margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _getWeatherTitle(), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: kBlack, + ), + ), + IconButton( + icon: Icon(Icons.refresh, size: 20.sp), + onPressed: () { + if (_selectedLocation != null) { + _fetchWeatherData( + _selectedLocation!.latitude, + _selectedLocation!.longitude, + ); + } else { + _fetchWeatherForCurrentLocation(); + } + }, + ), + ], + ), + SizedBox(height: 1.h), + Text( + location, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + ), + ), + if (widget.isScheduled) ...[ + SizedBox(height: 1.h), + Text( + 'Scheduled for: ${widget.startDate!.day}/${widget.startDate!.month}/${widget.startDate!.year}' + + (widget.startTime != null + ? ' at ${widget.startTime!.format(context)}' + : ''), + style: TextStyle( + fontSize: 12.sp, + color: Colors.blue[600], + fontWeight: FontWeight.w500, + ), + ), + ], + SizedBox(height: 2.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${temp.toStringAsFixed(1)}°C', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: Color(0xFF673AB7), + ), + ), + Text( + 'Feels like ${feelsLike.toStringAsFixed(1)}°C', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + description.toUpperCase(), + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: kBlack, + ), + ), + ], + ), + ], + ), + SizedBox(height: 2.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildWeatherInfo(Icons.water_drop, 'Humidity', '$humidity%'), + _buildWeatherInfo( + Icons.air, 'Wind', '${windSpeed.toStringAsFixed(1)} m/s'), + _buildWeatherInfo(Icons.visibility, 'Visibility', + '${(weatherData!['visibility'] / 1000).toStringAsFixed(1)} km'), + ], + ), + SizedBox(height: 2.h), + Container( + padding: EdgeInsets.all(2.w), + decoration: BoxDecoration( + color: _getWeatherRecommendationColor().withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + _getWeatherRecommendationIcon(), + color: _getWeatherRecommendationColor(), + size: 16.sp, + ), + SizedBox(width: 2.w), + Expanded( + child: Text( + _getWeatherRecommendation(), + style: TextStyle( + fontSize: 12.sp, + color: _getWeatherRecommendationColor(), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildWeatherInfo(IconData icon, String label, String value) { + return Column( + children: [ + Icon(icon, size: 20.sp, color: Colors.grey[600]), + SizedBox(height: 1.h), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), + ), + Text( + value, + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: kBlack, + ), + ), + ], + ); + } + + Widget _buildLocationField() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _locationController, + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey[200], + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + labelText: 'Starting Location', + labelStyle: TextStyle( + fontSize: 16.sp, + color: kBlack, + ), + hintText: 'Search for a location to get weather', + hintStyle: TextStyle( + fontSize: 14.sp, + color: Colors.grey, + ), + prefixIcon: Icon( + Icons.location_on, + color: kBlack, + ), + suffixIcon: _locationController.text.isNotEmpty + ? IconButton( + icon: Icon(Icons.clear), + onPressed: () { + _locationController.clear(); + setState(() { + _showLocationSuggestions = false; + _selectedLocation = null; + }); + }, + ) + : null, + ), + onChanged: (value) { + _searchLocations(value); + }, + ), + if (_showLocationSuggestions && _locationSuggestions.isNotEmpty) + Container( + margin: EdgeInsets.only(top: 1.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: _locationSuggestions.length, + itemBuilder: (context, index) { + final suggestion = _locationSuggestions[index]; + return ListTile( + leading: Icon(Icons.location_on, color: kBlack), + title: Text( + suggestion.name, + style: TextStyle(fontSize: 14.sp), + ), + onTap: () => _selectLocation(suggestion), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildTitleField() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: TextFormField( + initialValue: widget.title, + readOnly: true, // Make it read-only since title is now final + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey[200], + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + labelText: 'Title', + labelStyle: TextStyle( + fontSize: 16.sp, + color: kBlack, + ), + hintText: 'Enter your hike title', + hintStyle: TextStyle( + fontSize: 14.sp, + color: Colors.grey, + ), + prefixIcon: Icon( + Icons.title, + color: kBlack, + ), + ), + ), + ); + } + + Widget _buildDurationField() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: InkWell( + onTap: () async { + final selectedDuration = await showDurationPicker( + context: context, + initialTime: duration ?? Duration(minutes: 5), + ); + if (selectedDuration == null) return; + + setState(() { + duration = selectedDuration; + }); + + // Format duration text + if (duration!.inHours != 0 && duration!.inMinutes != 0) { + widget.durationController.text = + '${duration!.inHours} hour ${(duration!.inMinutes % 60)} minutes'; + } else if (duration!.inMinutes != 0) { + widget.durationController.text = '${duration!.inMinutes} minutes'; + } + }, + child: TextFormField( + enabled: false, + controller: widget.durationController, + decoration: InputDecoration( + filled: true, + fillColor: Colors.grey[200], + contentPadding: EdgeInsets.symmetric( + horizontal: 4.w, + vertical: 2.h, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + labelText: 'Duration', + labelStyle: TextStyle( + fontSize: 16.sp, + color: kBlack, + ), + hintText: 'Tap to select duration', + hintStyle: TextStyle( + fontSize: 14.sp, + color: Colors.grey, + ), + prefixIcon: Icon( + Icons.timer, + color: kBlack, + ), + suffixIcon: Icon( + Icons.arrow_drop_down, + color: kBlack, + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return BeaconScreenTemplate( + body: Column( + children: [ + SizedBox(height: 2.h), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + _buildLocationField(), + _buildWeatherCard(), + _buildTitleField(), + _buildDurationField(), + SizedBox(height: 2.h), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + child: HikeButton( + buttonHeight: 6.h, + buttonWidth: double.infinity, + text: !widget.isScheduled ? 'Start Hike' : 'Schedule Hike', + textSize: 16.sp, + onTap: () async { + var groupCubit = locator(); + if (widget.isScheduled) { + DateTime start = DateTime( + startDate!.year, + startDate!.month, + startDate!.day, + startTime!.hour, + startTime!.minute, + ); + + final startsAt = start.millisecondsSinceEpoch; + final expiresAt = start.add(duration!).millisecondsSinceEpoch; + + groupCubit.createHike( + widget.title, + startsAt, + expiresAt, + widget.groupId, + false, + ); + + widget.durationController.clear(); + appRouter.maybePop(); + } else { + int startsAt = DateTime.now().millisecondsSinceEpoch; + int expiresAt = + DateTime.now().add(duration!).millisecondsSinceEpoch; + + groupCubit.createHike( + widget.title, + startsAt, + expiresAt, + widget.groupId, + true, + ); + + widget.durationController.clear(); + appRouter.maybePop(); + } + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/group/cubit/group_cubit/group_state.freezed.dart b/lib/presentation/group/cubit/group_cubit/group_state.freezed.dart index 0ee4c3f4..c743b6c0 100644 --- a/lib/presentation/group/cubit/group_cubit/group_state.freezed.dart +++ b/lib/presentation/group/cubit/group_cubit/group_state.freezed.dart @@ -151,6 +151,9 @@ class _$GroupStateCopyWithImpl<$Res, $Val extends GroupState> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -167,6 +170,9 @@ class __$$InitialGroupStateImplCopyWithImpl<$Res> __$$InitialGroupStateImplCopyWithImpl(_$InitialGroupStateImpl _value, $Res Function(_$InitialGroupStateImpl) _then) : super(_value, _then); + + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -348,6 +354,9 @@ class __$$LoadingGroupStateImplCopyWithImpl<$Res> __$$LoadingGroupStateImplCopyWithImpl(_$LoadingGroupStateImpl _value, $Res Function(_$LoadingGroupStateImpl) _then) : super(_value, _then); + + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -529,6 +538,9 @@ class __$$ShrimmerGroupStateImplCopyWithImpl<$Res> __$$ShrimmerGroupStateImplCopyWithImpl(_$ShrimmerGroupStateImpl _value, $Res Function(_$ShrimmerGroupStateImpl) _then) : super(_value, _then); + + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -719,6 +731,8 @@ class __$$AllBeaconGroupStateImplCopyWithImpl<$Res> $Res Function(_$AllBeaconGroupStateImpl) _then) : super(_value, _then); + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -823,7 +837,9 @@ class _$AllBeaconGroupStateImpl implements AllBeaconGroupState { const DeepCollectionEquality().hash(_beacons), version); - @JsonKey(ignore: true) + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$AllBeaconGroupStateImplCopyWith<_$AllBeaconGroupStateImpl> get copyWith => @@ -990,7 +1006,10 @@ abstract class AllBeaconGroupState implements GroupState { filters get type; List get beacons; int get version; - @JsonKey(ignore: true) + + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$AllBeaconGroupStateImplCopyWith<_$AllBeaconGroupStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1019,6 +1038,8 @@ class __$$NearbyBeaconGroupStateImplCopyWithImpl<$Res> $Res Function(_$NearbyBeaconGroupStateImpl) _then) : super(_value, _then); + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1105,7 +1126,9 @@ class _$NearbyBeaconGroupStateImpl implements NearbyBeaconGroupState { int get hashCode => Object.hash(runtimeType, message, type, const DeepCollectionEquality().hash(_beacons), radius, version); - @JsonKey(ignore: true) + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$NearbyBeaconGroupStateImplCopyWith<_$NearbyBeaconGroupStateImpl> @@ -1267,7 +1290,10 @@ abstract class NearbyBeaconGroupState implements GroupState { List get beacons; double get radius; int get version; - @JsonKey(ignore: true) + + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$NearbyBeaconGroupStateImplCopyWith<_$NearbyBeaconGroupStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -1297,6 +1323,8 @@ class __$$StatusFilterBeaconGroupStateImplCopyWithImpl<$Res> $Res Function(_$StatusFilterBeaconGroupStateImpl) _then) : super(_value, _then); + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1401,7 +1429,9 @@ class _$StatusFilterBeaconGroupStateImpl const DeepCollectionEquality().hash(_beacons), version); - @JsonKey(ignore: true) + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$StatusFilterBeaconGroupStateImplCopyWith< @@ -1569,7 +1599,10 @@ abstract class StatusFilterBeaconGroupState implements GroupState { filters? get type; List get beacons; int get version; - @JsonKey(ignore: true) + + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$StatusFilterBeaconGroupStateImplCopyWith< _$StatusFilterBeaconGroupStateImpl> get copyWith => throw _privateConstructorUsedError; @@ -1592,6 +1625,8 @@ class __$$ErrorGroupStateImplCopyWithImpl<$Res> _$ErrorGroupStateImpl _value, $Res Function(_$ErrorGroupStateImpl) _then) : super(_value, _then); + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -1630,7 +1665,9 @@ class _$ErrorGroupStateImpl implements ErrorGroupState { @override int get hashCode => Object.hash(runtimeType, message); - @JsonKey(ignore: true) + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ErrorGroupStateImplCopyWith<_$ErrorGroupStateImpl> get copyWith => @@ -1784,7 +1821,10 @@ abstract class ErrorGroupState implements GroupState { _$ErrorGroupStateImpl; String get message; - @JsonKey(ignore: true) + + /// Create a copy of GroupState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$ErrorGroupStateImplCopyWith<_$ErrorGroupStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/presentation/group/cubit/members_cubit/members_state.freezed.dart b/lib/presentation/group/cubit/members_cubit/members_state.freezed.dart index 8f649350..4bdca176 100644 --- a/lib/presentation/group/cubit/members_cubit/members_state.freezed.dart +++ b/lib/presentation/group/cubit/members_cubit/members_state.freezed.dart @@ -79,6 +79,9 @@ class _$MembersStateCopyWithImpl<$Res, $Val extends MembersState> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of MembersState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -95,6 +98,9 @@ class __$$InitialMemberStateImplCopyWithImpl<$Res> __$$InitialMemberStateImplCopyWithImpl(_$InitialMemberStateImpl _value, $Res Function(_$InitialMemberStateImpl) _then) : super(_value, _then); + + /// Create a copy of MembersState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -204,6 +210,9 @@ class __$$LoadingMemberStateImplCopyWithImpl<$Res> __$$LoadingMemberStateImplCopyWithImpl(_$LoadingMemberStateImpl _value, $Res Function(_$LoadingMemberStateImpl) _then) : super(_value, _then); + + /// Create a copy of MembersState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -316,6 +325,8 @@ class __$$LoadedMemberStateImplCopyWithImpl<$Res> $Res Function(_$LoadedMemberStateImpl) _then) : super(_value, _then); + /// Create a copy of MembersState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -372,7 +383,9 @@ class _$LoadedMemberStateImpl implements LoadedMemberState { int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_members), message); - @JsonKey(ignore: true) + /// Create a copy of MembersState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LoadedMemberStateImplCopyWith<_$LoadedMemberStateImpl> get copyWith => @@ -456,7 +469,10 @@ abstract class LoadedMemberState implements MembersState { List? get members; String? get message; - @JsonKey(ignore: true) + + /// Create a copy of MembersState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$LoadedMemberStateImplCopyWith<_$LoadedMemberStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/presentation/group/group_screen.dart b/lib/presentation/group/group_screen.dart index 61a21b07..e20fbb76 100644 --- a/lib/presentation/group/group_screen.dart +++ b/lib/presentation/group/group_screen.dart @@ -1,15 +1,13 @@ -import 'dart:developer'; - import 'package:auto_route/auto_route.dart'; import 'package:beacon/domain/entities/beacon/beacon_entity.dart'; import 'package:beacon/domain/entities/group/group_entity.dart'; -import 'package:beacon/presentation/auth/auth_cubit/auth_cubit.dart'; import 'package:beacon/presentation/group/cubit/group_cubit/group_cubit.dart'; import 'package:beacon/presentation/group/cubit/group_cubit/group_state.dart'; import 'package:beacon/presentation/group/cubit/members_cubit/members_cubit.dart'; import 'package:beacon/presentation/group/widgets/create_join_dialog.dart'; import 'package:beacon/presentation/group/widgets/beacon_card.dart'; import 'package:beacon/presentation/group/widgets/group_widgets.dart'; +import 'package:beacon/presentation/widgets/screen_template.dart'; import 'package:beacon/presentation/widgets/shimmer.dart'; import 'package:beacon/presentation/widgets/hike_button.dart'; import 'package:beacon/presentation/widgets/loading_screen.dart'; @@ -17,24 +15,20 @@ import 'package:beacon/locator.dart'; import 'package:beacon/core/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gap/gap.dart'; import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart'; import 'package:responsive_sizer/responsive_sizer.dart'; +import 'package:lottie/lottie.dart'; @RoutePage() class GroupScreen extends StatefulWidget { final GroupEntity group; - - GroupScreen(this.group); + const GroupScreen(this.group, {super.key}); @override - _GroupScreenState createState() => _GroupScreenState(); + State createState() => _GroupScreenState(); } -class _GroupScreenState extends State - with TickerProviderStateMixin { - late List fetchingUserBeacons; - late List fetchingNearbyBeacons; +class _GroupScreenState extends State { late GroupCubit _groupCubit; late MembersCubit _membersCubit; late ScrollController _scrollController; @@ -42,16 +36,19 @@ class _GroupScreenState extends State @override void initState() { super.initState(); - _scrollController = ScrollController(); - _scrollController.addListener(_listener); - _groupCubit = BlocProvider.of(context); - _membersCubit = BlocProvider.of(context); + _scrollController = ScrollController()..addListener(_onScroll); + _groupCubit = context.read(); + _membersCubit = context.read(); + _initializeData(); + } + + void _initializeData() { _groupCubit.init(widget.group); _groupCubit.allHikes(widget.group.id!); _membersCubit.init(widget.group); } - void _listener() { + void _onScroll() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { final state = _groupCubit.state; @@ -63,6 +60,7 @@ class _GroupScreenState extends State @override void dispose() { + _scrollController.dispose(); _groupCubit.clear(); _membersCubit.clear(); super.dispose(); @@ -70,530 +68,304 @@ class _GroupScreenState extends State @override Widget build(BuildContext context) { - final screensize = MediaQuery.of(context).size; + return BeaconScreenTemplate( + body: BlocConsumer( + listener: (context, state) { + if (state is AllBeaconGroupState && state.message != null) { + utils.showSnackBar(state.message!, context); + } + }, + builder: (context, state) { + return ModalProgressHUD( + inAsyncCall: state is LoadingGroupState, + progressIndicator: const LoadingScreen(), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: Column( + children: [ + _buildGroupHeader(), + SizedBox(height: 2.h), + _buildMembersSection(), + SizedBox(height: 3.h), + _buildActionButtons(), + SizedBox(height: 3.h), + _buildBeaconsHeader(), + SizedBox(height: 1.h), + Expanded(child: _buildBeaconsList(state)), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildGroupHeader() { + return Row( + children: [ + Text( + 'Welcome to Group ', + style: TextStyle(fontSize: 18.sp), + ), + SizedBox(width: 2.w), + Text( + widget.group.title ?? '', + style: TextStyle( + fontSize: 20.sp, + color: Colors.teal, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } - return Scaffold( - resizeToAvoidBottomInset: false, - body: SafeArea( - child: BlocConsumer( - listener: (context, state) { - if (state is AllBeaconGroupState && state.message != null) { - utils.showSnackBar(state.message!, context); - } - }, - builder: (context, state) { - return ModalProgressHUD( - progressIndicator: const LoadingScreen(), - inAsyncCall: state is LoadingGroupState, - child: Padding( - padding: EdgeInsets.only( - left: screensize.width * 0.04, - right: screensize.width * 0.04, - top: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - visualDensity: VisualDensity.compact, - padding: EdgeInsets.all(0), - icon: const Icon(Icons.arrow_back_outlined, - color: Colors.grey), - onPressed: () => AutoRouter.of(context).maybePop(), - ), - Image.asset( - 'images/beacon_logo.png', - height: 28, - ), - IconButton( - icon: const Icon(Icons.power_settings_new, - color: Colors.grey), - onPressed: () => showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Color(0xffFAFAFA), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12.0), - ), - title: - Text('Logout', style: Style.heading), - content: Text( - 'Are you sure you want to logout?', - style: TextStyle( - fontSize: 16, color: kBlack), - ), - actions: [ - HikeButton( - buttonWidth: 80, - buttonHeight: 40, - isDotted: true, - onTap: () => AutoRouter.of(context) - .maybePop(false), - text: 'No', - textSize: 18.0, - ), - SizedBox( - height: 5, - ), - HikeButton( - buttonWidth: 80, - buttonHeight: 40, - onTap: () async { - appRouter.replaceNamed('/auth'); - localApi.deleteUser(); - context - .read() - .googleSignOut(); - }, - text: 'Yes', - textSize: 18.0, - ), - ], - ))), - ], - ), - const SizedBox(height: 10), - _buildGroupName(), - const SizedBox(height: 10), - Row( - children: [ - widget.group.members != null && - widget.group.members!.isNotEmpty - ? SizedBox( - width: 40 * - widget.group.members!.length.toDouble(), - // 40 is the width of each profile circle - height: 40, - child: Stack( - children: (widget.group.members != null && - widget.group.members!.length > 3 - ? widget.group.members!.sublist(0, 3) - : widget.group.members ?? []) - .map((member) { - if (member != null) { - return Positioned( - left: widget.group.members! - .indexOf(member) * - 20.0, - child: _buildProfileCircle( - member.id == localApi.userModel.id - ? Colors.teal - : shimmerSkeletonColor, - ), - ); - } else { - return const SizedBox.shrink(); - } - }).toList(), - ), - ) - : Container(), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Group has ${widget.group.members!.length.toString()} ${widget.group.members!.length == 1 ? 'member' : 'members'}', - style: TextStyle( - color: Colors.grey, - fontSize: 14, - ), - ), - const SizedBox(height: 2), - // view all members button - GestureDetector( - onTap: () { - GroupWidgetUtils.showMembers(context); - }, - child: Text( - "View all members", - style: TextStyle( - color: kBlack, - fontSize: 14, - decoration: TextDecoration.underline, - ), - textAlign: TextAlign.start, - ), - ) - ], - ), - ], - ), - const SizedBox(height: 30), - _buildJoinCreateButton(), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'All Beacons', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.purple, - ), - ), - const SizedBox(width: 10), - IconButton( - padding: EdgeInsets.all(0), - visualDensity: VisualDensity.compact, - onPressed: () { - GroupWidgetUtils.showFilterBeaconAlertBox( - context, widget.group.id!, _groupCubit); - }, - icon: Icon( - Icons.filter_alt_outlined, - color: Colors.purple, - ), - ) - ], - ), - const SizedBox(height: 10), - Expanded(child: _groupBeacons(state)), - ], + Widget _buildMembersSection() { + final memberCount = (widget.group.members?.length ?? 0) + 1; + return Row( + children: [ + if (widget.group.members?.isNotEmpty ?? false) + SizedBox( + width: 10.w * + (widget.group.members!.length > 3 + ? 3 + : widget.group.members!.length), + height: 5.h, + child: Stack( + children: (widget.group.members!.length > 3 + ? widget.group.members!.sublist(0, 3) + : widget.group.members!) + .map((member) => Positioned( + left: widget.group.members!.indexOf(member) * 8.w, + child: _buildProfileCircle(member?.imageUrl), + )) + .toList(), + ), + ), + SizedBox(width: 3.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Group has $memberCount ${memberCount == 1 ? 'member' : 'members'}', + style: TextStyle(fontSize: 14.sp, color: Colors.grey), + ), + SizedBox(height: 0.5.h), + GestureDetector( + onTap: () => GroupWidgetUtils.showMembers(context), + child: Text( + "View all members", + style: TextStyle( + fontSize: 14.sp, + decoration: TextDecoration.underline, ), ), - ); - }, + ), + ], ), - ), + ], ); } - Widget _buildJoinCreateButton() { - Size screensize = MediaQuery.of(context).size; + Widget _buildActionButtons() { return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 45.w, + children: [ + Expanded( child: HikeButton( text: 'Create Hike', widget: Icon( Icons.add, - color: Colors.black, - size: 18, + size: 18.sp, + color: Colors.white, ), - textColor: Colors.white, - borderColor: Colors.white, - buttonWidth: screensize.width * 0.44, - buttonHeight: 45, - onTap: () { - CreateJoinBeaconDialog.createHikeDialog( - context, widget.group.id!); - }, + buttonWidth: double.infinity, + buttonHeight: 6.h, + onTap: () => CreateJoinBeaconDialog.createHikeDialog( + context, widget.group.id!), ), ), - SizedBox(width: 1.w), - Container( - width: 45.w, + SizedBox(width: 3.w), + Expanded( child: HikeButton( text: 'Join a Hike', - widget: Icon( - Icons.add, - color: Colors.black, - size: 18, - ), - buttonColor: Colors.white, + widget: Icon(Icons.add, size: 18.sp), + buttonWidth: double.infinity, + buttonHeight: 6.h, isDotted: true, - buttonWidth: screensize.width * 0.44, - buttonHeight: 45, - onTap: () async { - CreateJoinBeaconDialog.joinBeaconDialog(context); - }, + onTap: () => CreateJoinBeaconDialog.joinBeaconDialog(context), ), ), ], ); } - Widget _groupBeacons(GroupState state) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 2.0), - child: Builder( - builder: (context) { - if (state is ShrimmerGroupState) { - return Center(child: ShimmerWidget.getPlaceholder()); - } else if (state is AllBeaconGroupState) { - final beacons = state.beacons; - String message = 'You haven\'t joined or created any beacon yet'; - return _buildBeaconsList(beacons, state.isLoadingMore, - state.isCompletelyFetched, message); - } else if (state is NearbyBeaconGroupState) { - final beacons = state.beacons; - String message = - 'No beacons found under ${state.radius.toStringAsFixed(2)} m... radius'; - return _buildBeaconsList(beacons, false, false, message); - } else if (state is StatusFilterBeaconGroupState) { - final beacons = state.beacons; - var type = state.type!.name; - String message = - 'No ${type[0].toUpperCase() + type.substring(1).toLowerCase()} beacons found'; - return _buildBeaconsList(beacons, false, false, message); - } else if (state is ErrorGroupState) { - return _buildErrorWidget(state.message); - } - return _buildErrorWidget('Something went wrong!'); - }, - ), - ); - } - - Widget _buildGroupName() { + Widget _buildBeaconsHeader() { return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Welcome to Group ', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 20, color: Colors.black), - ), - SizedBox( - width: 2.w, - ), - Text( - widget.group.title!, - textAlign: TextAlign.center, + 'All Beacons', style: TextStyle( - fontSize: 24, - color: Colors.tealAccent, - fontWeight: FontWeight.bold), + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.purple, + ), + ), + IconButton( + icon: Icon(Icons.filter_alt_outlined, color: Colors.purple), + onPressed: () => GroupWidgetUtils.showFilterBeaconAlertBox( + context, widget.group.id!, _groupCubit), ), ], ); } - Widget _buildBeaconsList(List beacons, bool isLoadingMore, - bool isCompletelyFetched, String message) { - return Container( - alignment: Alignment.center, - child: beacons.isEmpty - ? SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: _noBeaconsWidget(message), - ) - : ListView.builder( - controller: _scrollController, - physics: AlwaysScrollableScrollPhysics(), - scrollDirection: Axis.vertical, - itemCount: beacons.length + - (isLoadingMore && !isCompletelyFetched ? 1 : 0), - padding: EdgeInsets.all(8), - itemBuilder: (context, index) { - if (index == beacons.length) { - return LinearProgressIndicator(); - } - return _buildBeaconCard(beacons[index]); - }, - ), - ); + Widget _buildBeaconsList(GroupState state) { + if (state is ShrimmerGroupState) { + return Center(child: ShimmerWidget.getPlaceholder()); + } else if (state is AllBeaconGroupState) { + return _buildBeaconListView( + state.beacons, + state.isLoadingMore, + state.isCompletelyFetched, + 'You haven\'t joined or created any beacon yet', + ); + } else if (state is NearbyBeaconGroupState) { + return _buildBeaconListView( + state.beacons, + false, + false, + 'No beacons found under ${state.radius.toStringAsFixed(2)} m radius', + ); + } else if (state is StatusFilterBeaconGroupState) { + final type = state.type!.name; + return _buildBeaconListView( + state.beacons, + false, + false, + 'No ${type[0].toUpperCase() + type.substring(1).toLowerCase()} beacons found', + ); + } else if (state is ErrorGroupState) { + return _buildErrorWidget(state.message); + } + return _buildErrorWidget('Something went wrong!'); } - Widget _buildBeaconCard(BeaconEntity beacon) { - return BeaconCard( - beacon: beacon, - onDelete: () async { - bool? value = await GroupWidgetUtils.deleteDialog(context); - if (value == null || !value) { - return; - } - await _groupCubit.deleteBeacon(beacon); - _groupCubit.reloadState(message: 'Beacon deleted'); - return; - }, - onReschedule: () { - GroupWidgetUtils.reScheduleHikeDialog(context, beacon); - }, - ); + Widget _buildBeaconListView( + List beacons, + bool isLoadingMore, + bool isCompletelyFetched, + String emptyMessage, + ) { + return beacons.isEmpty + ? _buildEmptyState(emptyMessage) + : ListView.builder( + controller: _scrollController, + itemCount: beacons.length + + (isLoadingMore && !isCompletelyFetched ? 1 : 0), + padding: EdgeInsets.only(top: 1.h), + itemBuilder: (context, index) { + if (index == beacons.length) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: const Center(child: LinearProgressIndicator()), + ); + } + return BeaconCard( + beacon: beacons[index], + onDelete: () => _handleDeleteBeacon(beacons[index]), + onReschedule: () => GroupWidgetUtils.reScheduleHikeDialog( + context, beacons[index]), + ); + }, + ); } - Widget _buildErrorWidget(String message) { - return Center( - child: SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: Column( - children: [ - Text( - message, - textAlign: TextAlign.center, - style: TextStyle(color: kBlack, fontSize: 20), - ), - Gap(5), - FloatingActionButton( - onPressed: () async { - try { - await locationService.openSettings(); - } catch (e) { - log('error: $e'); - } - }, - child: Icon( - Icons.settings, - color: kBlack, - ), - backgroundColor: kYellow, - ), - Gap(15), - RichText( - text: TextSpan( - style: TextStyle(color: kBlack, fontSize: 20), - children: [ - TextSpan( - text: 'Join', - style: TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: ' a Hike or '), - TextSpan( - text: 'Create', - style: TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: ' a new one! '), - ], - ), - ), - SizedBox( - height: 2.h, - ), - ], - ), - ), - ); + Future _handleDeleteBeacon(BeaconEntity beacon) async { + final shouldDelete = await GroupWidgetUtils.deleteDialog(context); + if (shouldDelete == true) { + await _groupCubit.deleteBeacon(beacon); + _groupCubit.reloadState(message: 'Beacon deleted'); + } } - Widget _noBeaconsWidget(String message) { + Widget _buildEmptyState(String message) { return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - message, - textAlign: TextAlign.center, - style: TextStyle(color: kBlack, fontSize: 20), - ), - SizedBox( - height: 2.h, - ), + Lottie.asset('animations/empty.json', width: 50.w, height: 25.h), + SizedBox(height: 3.h), + Text(message, style: TextStyle(fontSize: 16.sp)), + SizedBox(height: 3.h), RichText( text: TextSpan( - style: TextStyle(color: kBlack, fontSize: 20), + style: TextStyle(fontSize: 16.sp), children: [ - TextSpan( + const TextSpan( text: 'Join', style: TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: ' a Hike or '), - TextSpan( + const TextSpan(text: ' a Hike or '), + const TextSpan( text: 'Create', style: TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: ' a new one! '), + const TextSpan(text: ' a new one!'), ], ), ), - SizedBox( - height: 2.h, - ), ], ), ); } -} - -Widget _buildProfileCircle(Color color) { - return Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - border: Border.all(color: Colors.white, width: 2), - ), - ); -} - -class HikeCard extends StatelessWidget { - final bool isActive; - final String startTime; - final String endTime; - final String passkey; - const HikeCard({ - super.key, - required this.isActive, - required this.startTime, - required this.endTime, - required this.passkey, - }); - - @override - Widget build(BuildContext context) { - return Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16), + Widget _buildErrorWidget(String message) { + return Center( + child: SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: const [ - Icon(Icons.hiking, size: 28), - SizedBox(width: 8), - Text('Hike 1', - style: - TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - ], + Text(message, style: TextStyle(fontSize: 16.sp)), + SizedBox(height: 2.h), + FloatingActionButton( + onPressed: () => locationService.openSettings(), + child: const Icon(Icons.settings), + backgroundColor: kYellow, ), - const SizedBox(height: 8), - Text( - 'Hike is ${isActive ? "Active" : "inactive"}', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: isActive ? Colors.teal : Colors.black87, + SizedBox(height: 2.h), + RichText( + text: TextSpan( + style: TextStyle(fontSize: 16.sp), + children: [ + const TextSpan( + text: 'Join', + style: TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: ' a Hike or '), + const TextSpan( + text: 'Create', + style: TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: ' a new one!'), + ], ), ), - const SizedBox(height: 8), - Text('Started at: $startTime'), - Text('Expires at: $endTime'), - const SizedBox(height: 8), - Row( - children: [ - Text('Passkey: $passkey', - style: const TextStyle(fontWeight: FontWeight.w500)), - const Spacer(), - IconButton( - icon: const Icon(Icons.copy), - onPressed: () { - // TODO: Copy to clipboard - }, - ), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - onPressed: () { - // TODO: Delete action - }, - child: const Text("Delete"), - ), - const SizedBox(width: 8), - OutlinedButton( - onPressed: () { - // TODO: Reschedule action - }, - child: const Text("Reschedule"), - ), - ], - ), ], ), ), ); } + + Widget _buildProfileCircle(String? imageUrl) { + return Container( + width: 10.w, + height: 10.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey[300], + border: Border.all(color: Colors.white, width: 1.w), + image: imageUrl != null + ? DecorationImage(image: NetworkImage(imageUrl), fit: BoxFit.cover) + : null, + ), + ); + } } diff --git a/lib/presentation/group/widgets/beacon_card.dart b/lib/presentation/group/widgets/beacon_card.dart index 6f9cc35a..a50ab4f8 100644 --- a/lib/presentation/group/widgets/beacon_card.dart +++ b/lib/presentation/group/widgets/beacon_card.dart @@ -1,11 +1,9 @@ import 'dart:async'; -import 'package:auto_route/auto_route.dart'; import 'package:beacon/core/utils/constants.dart'; import 'package:beacon/domain/entities/beacon/beacon_entity.dart'; import 'package:beacon/locator.dart'; import 'package:beacon/presentation/group/cubit/group_cubit/group_cubit.dart'; import 'package:beacon/presentation/hike/widgets/active_beacon.dart'; -import 'package:beacon/presentation/widgets/hike_button.dart'; import 'package:beacon/presentation/group/widgets/timer.dart'; import 'package:beacon/config/router/router.dart'; import 'package:flutter/material.dart'; @@ -18,12 +16,13 @@ class BeaconCard extends StatefulWidget { final BeaconEntity beacon; final void Function()? onDelete; final void Function()? onReschedule; - BeaconCard( - {required this.beacon, - required this.onDelete, - required this.onReschedule, - Key? key}) - : super(key: key); + + const BeaconCard({ + required this.beacon, + required this.onDelete, + required this.onReschedule, + Key? key, + }) : super(key: key); @override State createState() => _BeaconCardState(); @@ -33,13 +32,14 @@ class _BeaconCardState extends State { late bool hasStarted; late bool hasEnded; late bool willStart; - DateTime now = DateTime.now(); + Timer? _rebuildTimer; + late DateTime startAt; late DateTime expiresAt; - Timer? _rebuildTimer; @override void initState() { + final now = DateTime.now(); startAt = DateTime.fromMillisecondsSinceEpoch(widget.beacon.startsAt!); expiresAt = DateTime.fromMillisecondsSinceEpoch(widget.beacon.expiresAt!); hasStarted = now.isAfter(startAt); @@ -52,47 +52,47 @@ class _BeaconCardState extends State { void scheduleRebuild() { if (hasEnded) return; + final now = DateTime.now(); late int seconds; if (willStart) { - Duration difference = startAt.difference(now); - seconds = difference.inSeconds; - } else if (hasStarted && !hasEnded) { - Duration difference = expiresAt.difference(now); - seconds = difference.inSeconds; + seconds = startAt.difference(now).inSeconds; + } else { + seconds = expiresAt.difference(now).inSeconds; } - _rebuildTimer?.cancel(); - _rebuildTimer = Timer(Duration(milliseconds: seconds * 1000 + 1000), () { - var now = DateTime.now(); - hasStarted = now.isAfter(startAt); - hasEnded = now.isAfter(expiresAt); - willStart = now.isBefore(startAt); - setState(() {}); + _rebuildTimer?.cancel(); + _rebuildTimer = Timer(Duration(seconds: seconds + 1), () { + setState(() { + final now = DateTime.now(); + hasStarted = now.isAfter(startAt); + hasEnded = now.isAfter(expiresAt); + willStart = now.isBefore(startAt); + }); + // Show notification if the beacon becomes active Future.delayed(Duration(seconds: 1), () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - duration: Duration(seconds: 5), + duration: const Duration(seconds: 5), content: Text( - '${widget.beacon.title} is now active! \nYou can join the hike', - style: TextStyle(color: Colors.black), + '${widget.beacon.title} is now active!\nYou can join the hike', + style: const TextStyle(color: Colors.black), ), - backgroundColor: kLightBlue.withValues(alpha: 0.8), + backgroundColor: kLightBlue.withAlpha(200), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), + borderRadius: BorderRadius.circular(10), ), behavior: SnackBarBehavior.floating, elevation: 5, action: SnackBarAction( - textColor: kBlue, label: 'Click to Join', - onPressed: () async { + textColor: kBlue, + onPressed: () { appRouter.push(HikeScreenRoute( - beacon: widget.beacon, - isLeader: widget.beacon.id! == localApi.userModel.id!)); + beacon: widget.beacon, + isLeader: widget.beacon.id == localApi.userModel.id, + )); }, ), ), @@ -107,253 +107,205 @@ class _BeaconCardState extends State { super.dispose(); } + String formatDate(int timestamp) { + return DateFormat("hh:mm a, d/M/y") + .format(DateTime.fromMillisecondsSinceEpoch(timestamp)); + } + @override Widget build(BuildContext context) { - BeaconEntity beacon = widget.beacon; + final beacon = widget.beacon; return InkWell( - onTap: () async { + onTap: () { locator().joinBeacon(beacon, hasEnded, hasStarted); }, child: Container( - margin: const EdgeInsets.only(bottom: 10.0), - padding: EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8, top: 8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(8.0), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + offset: Offset(0, 1), + blurRadius: 6.0, + ) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Header Row + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.hiking, color: Colors.black), + Gap(8), + Expanded( + child: Text( + '${beacon.title?.toUpperCase()} by ${beacon.leader?.name}', + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + ), + if (hasStarted && !hasEnded) + BlinkIcon() + else if (willStart) + Icon(Icons.circle, color: kYellow, size: 10), + ], + ), + Gap(8), + + /// Status Row + if (hasStarted && !hasEnded) + RichText( + text: TextSpan( + style: Style.commonTextStyle, + children: [ + const TextSpan(text: 'Hike is '), + TextSpan( + text: 'Active', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ], + ), + ) + else if (willStart) + Row( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 70.w, - child: Row( - children: [ - Icon( - Icons.hiking, - color: Colors.black, - ), - SizedBox(width: 8.0), - Text( - '${beacon.title!.toUpperCase()} by ${beacon.leader!.name} ', - style: TextStyle( - fontSize: 18.0, - color: Colors.black, - fontWeight: FontWeight.w700, - ), - ), - ], + RichText( + text: TextSpan( + style: Style.commonTextStyle, + children: [ + const TextSpan(text: 'Hike '), + TextSpan( + text: 'Starts ', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), ), - ), - Align( - alignment: Alignment.topRight, - child: (hasStarted && !hasEnded) - ? BlinkIcon() - : willStart - ? Align( - alignment: Alignment.topRight, - child: Icon( - Icons.circle, - color: kYellow, - size: 10, - ), - ) - : null, - ), - ], - ), - SizedBox(height: 8.0), - (hasStarted && !hasEnded) - ? RichText( - text: TextSpan( - text: 'Hike is ', - style: Style.commonTextStyle, - children: const [ - TextSpan( - text: 'Active', - style: TextStyle( - fontSize: 16.0, - color: Colors.black, - fontWeight: FontWeight.bold, - letterSpacing: 1.0), - ), - ], + const TextSpan( + text: 'in ', + style: TextStyle( + color: Color(0xffb6b2df), + fontSize: 14, + fontWeight: FontWeight.w400, ), - ) - : willStart - ? Row( - children: [ - RichText( - text: TextSpan( - text: 'Hike ', - style: Style.commonTextStyle, - children: const [ - TextSpan( - text: 'Starts ', - style: TextStyle( - fontSize: 16.0, - color: Colors.black, - fontWeight: FontWeight.bold, - letterSpacing: 1.0), - ), - TextSpan( - text: 'in ', - style: TextStyle( - color: const Color(0xffb6b2df), - fontSize: 14.0, - fontWeight: FontWeight.w400), - ), - ], - ), - ), - SizedBox( - width: 3.0, - ), - CountdownTimerPage( - dateTime: DateTime.fromMillisecondsSinceEpoch( - beacon.startsAt!), - name: beacon.title, - beacon: beacon, - ) - ], - ) - : Row( - children: [ - RichText( - text: TextSpan( - text: 'Hike ', - style: Style.commonTextStyle, - children: const [ - TextSpan( - text: 'is Ended', - style: TextStyle( - fontSize: 16.0, - color: Colors.black, - fontWeight: FontWeight.bold, - letterSpacing: 1.0), - ), - ], - ), - ), - ], - ), - SizedBox(height: 4.0), - Row( - children: [ - Text('Passkey: ${beacon.shortcode}', - style: Style.commonTextStyle), - Gap(10), - InkWell( - onTap: () { - Clipboard.setData(ClipboardData( - text: beacon.shortcode.toString())); - utils.showSnackBar('Shortcode copied!', context); - }, - child: Icon( - Icons.copy, - color: Colors.black, - size: 15, - )) - ], + ), + ], + ), ), - SizedBox(height: 4.0), - (beacon.startsAt != null) - ? Text( - willStart - ? 'Starting At: ${DateFormat("hh:mm a, d/M/y").format(DateTime.fromMillisecondsSinceEpoch(beacon.startsAt!)).toString()}' - : 'Started At: ${DateFormat("hh:mm a, d/M/y").format(DateTime.fromMillisecondsSinceEpoch(beacon.startsAt!)).toString()}', - style: Style.commonTextStyle) - : Container(), - SizedBox(height: 4.0), - (beacon.expiresAt != null) - ? willStart - ? Text( - 'Expiring At: ${DateFormat("hh:mm a, d/M/y").format(DateTime.fromMillisecondsSinceEpoch(beacon.expiresAt!)).toString()}', - style: Style.commonTextStyle) - : Text( - 'Expires At: ${DateFormat("hh:mm a, d/M/y").format(DateTime.fromMillisecondsSinceEpoch(beacon.expiresAt!)).toString()}', - style: Style.commonTextStyle) - : Container(), - SizedBox(height: 4.0), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 2), - visualDensity: const VisualDensity( - horizontal: -2.0, vertical: -2.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0)), - side: const BorderSide(color: Colors.black)), - onPressed: widget.onDelete, - child: const Text("Delete"), - ), - const SizedBox(width: 10), - OutlinedButton( - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 4), - visualDensity: const VisualDensity( - horizontal: .0, vertical: -2.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0)), - foregroundColor: Colors.black, - side: const BorderSide(color: Colors.black)), - onPressed: widget.onReschedule, - child: const Text("Reschedule"), - ), - ], + Gap(4), + CountdownTimerPage( + dateTime: startAt, + name: beacon.title ?? '', + beacon: beacon, ) ], ) - ], - ), - decoration: BoxDecoration( - color: Colors.grey[50], - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(8.0), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - offset: Offset(0.0, 1.0), - blurRadius: 6.0, + else + RichText( + text: TextSpan( + style: Style.commonTextStyle, + children: [ + const TextSpan(text: 'Hike '), + TextSpan( + text: 'is Ended', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ], + ), ), - ], - )), - ); - } + Gap(8), - Future deleteDialog(BuildContext context) async { - return showDialog( - context: context, - builder: (context) => AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - // actionsAlignment: MainAxisAlignment.spaceEvenly, - contentPadding: EdgeInsets.all(25.0), - content: Text( - 'Are you sure you want to delete this beacon?', - style: TextStyle(fontSize: 18, color: kBlack), + /// Passkey Row + Row( + children: [ + Text( + 'Passkey: ${beacon.shortcode}', + style: Style.commonTextStyle, + ), + Gap(10), + InkWell( + onTap: () { + Clipboard.setData( + ClipboardData(text: beacon.shortcode ?? ''), + ); + utils.showSnackBar('Shortcode copied!', context); + }, + child: Icon(Icons.copy, color: Colors.black, size: 16.sp), + ), + ], + ), + Gap(8), + + /// Start and expiry + if (beacon.startsAt != null) + Text( + willStart + ? 'Starting At: ${formatDate(beacon.startsAt!)}' + : 'Started At: ${formatDate(beacon.startsAt!)}', + style: Style.commonTextStyle, + ), + if (beacon.expiresAt != null) + Text( + willStart + ? 'Expiring At: ${formatDate(beacon.expiresAt!)}' + : 'Expires At: ${formatDate(beacon.expiresAt!)}', + style: Style.commonTextStyle, + ), + Gap(12), + + /// Action buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.black, + side: const BorderSide(color: Colors.black), + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + visualDensity: VisualDensity(horizontal: -1, vertical: -2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: widget.onDelete, + child: const Text("Delete"), + ), + Gap(8), + OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.black, + side: const BorderSide(color: Colors.black), + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + visualDensity: VisualDensity(horizontal: -1, vertical: -2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: widget.onReschedule, + child: const Text("Reschedule"), + ), + ], + ) + ], ), - actions: [ - HikeButton( - buttonHeight: 2.5.h, - buttonWidth: 8.w, - onTap: () => AutoRouter.of(context).maybePop(false), - text: 'No', - ), - HikeButton( - buttonHeight: 2.5.h, - buttonWidth: 8.w, - onTap: () => AutoRouter.of(context).maybePop(true), - text: 'Yes', - ), - ], ), ); } diff --git a/lib/presentation/group/widgets/create_join_dialog.dart b/lib/presentation/group/widgets/create_join_dialog.dart index a1fd5ff9..4cf32ee7 100644 --- a/lib/presentation/group/widgets/create_join_dialog.dart +++ b/lib/presentation/group/widgets/create_join_dialog.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:responsive_sizer/responsive_sizer.dart'; +import 'package:beacon/config/router/router.dart'; class CreateJoinGroupDialog { static GlobalKey _groupKey = GlobalKey(); @@ -539,6 +540,30 @@ class CreateJoinBeaconDialog { ), ), ), + + GestureDetector( + onTap: () => { + context.router.push(AdvancedOptionsScreenRoute( + title: title, + durationController: _durationController, + isScheduled: !isInstant, + startDate: startDate, + startTime: startTime, + groupId: groupID!)), + _durationController.clear(), + _startTimeController.clear(), + _durationController.clear(), + appRouter.maybePop(), + }, + child: Text( + "advanced options", + style: TextStyle( + fontSize: 15.sp, + color: Theme.of(context).primaryColor, + decoration: TextDecoration.underline, + ), + ), + ), SizedBox(height: 2.h), Flexible( flex: 2, diff --git a/lib/presentation/group/widgets/group_widgets.dart b/lib/presentation/group/widgets/group_widgets.dart index 6c367424..72ef2f1e 100644 --- a/lib/presentation/group/widgets/group_widgets.dart +++ b/lib/presentation/group/widgets/group_widgets.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:beacon/core/utils/validators.dart'; import 'package:beacon/domain/entities/beacon/beacon_entity.dart'; +import 'package:beacon/domain/entities/user/user_entity.dart'; import 'package:beacon/presentation/group/cubit/members_cubit/members_cubit.dart'; import 'package:beacon/presentation/group/cubit/members_cubit/members_state.dart'; import 'package:beacon/locator.dart'; @@ -30,97 +31,91 @@ class GroupWidgetUtils { } static void showMembers(BuildContext context) { - // Dialog for filtering beacons locator().loadMembers(); + showDialog( context: context, + barrierDismissible: true, builder: (context) { - bool isSmallSized = 100.h < 800; + final bool isSmallSized = 100.h < 800; + final double dialogHeight = isSmallSized ? 30.h : 25.h; + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + backgroundColor: Colors.white, title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.group), - Gap(5), + Icon(Icons.group, color: Colors.black, size: 30), + Gap(8), Text( 'Members', - textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black, + ), ) ], ), - content: Container( - height: isSmallSized ? 30.h : 25.h, - width: isSmallSized ? 200 : 300, - child: BlocConsumer( - listener: (context, state) { - if (state is LoadedMemberState && state.message != null) { - utils.showSnackBar(state.message!, context); - } - }, - builder: (context, state) { - if (state is LoadingMemberState) { - return ShimmerWidget.getPlaceholder(); - } else if (state is LoadedMemberState) { - var members = state.members; - return members!.isEmpty - ? Container( - child: - Text('Please check your internet connection'), + content: SizedBox( + height: dialogHeight, + width: isSmallSized ? 280 : 350, + child: BlocConsumer( + listener: (context, state) { + if (state is LoadedMemberState && state.message != null) { + utils.showSnackBar(state.message!, context); + } + }, + builder: (context, state) { + if (state is LoadingMemberState) { + return ShimmerWidget.getPlaceholder(); + } else if (state is LoadedMemberState) { + var members = state.members; + + if (members == null || members.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.wifi_off, + size: 40, color: Colors.grey.shade500), + const SizedBox(height: 8), + Text( + 'No members found.\nCheck your internet connection.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), ) - : ListView.builder( - shrinkWrap: true, - itemCount: members.length, - itemBuilder: (context, index) { - bool isLeader = - localApi.userModel.id! == members[0].id!; - return Container( - margin: EdgeInsets.symmetric(vertical: 10), - decoration: BoxDecoration( - color: kLightBlue, - borderRadius: - BorderRadius.all(Radius.circular(10))), - child: ListTile( - leading: index == 0 - ? Icon( - Icons.star, - color: kYellow, - ) - : Icon(Icons.person), - trailing: index == 0 - ? Text('Leader') - : isLeader - ? IconButton( - onPressed: () { - context - .read() - .removeMember( - members[index].id ?? - ''); - }, - icon: Icon( - Icons.person_remove_alt_1, - weight: 20, - color: const Color.fromARGB( - 255, 215, 103, 95), - )) - : null, - subtitle: localApi.userModel.id! == - members[index].id! - ? Text( - '(YOU)', - style: TextStyle(fontSize: 12), - ) - : null, - title: - Text(members[index].name ?? 'Anonymous'), - ), - ); - }, - ); + ], + ), + ); } - return Container(); - }, - )), + + return ListView.separated( + itemCount: members.length, + separatorBuilder: (_, __) => Gap(8), + itemBuilder: (context, index) { + final member = members[index]; + final isCurrentUser = localApi.userModel.id == member.id; + final isLeader = + localApi.userModel.id == members.first.id; + return _MemberTile( + member: member, + isLeader: index == 0, + canRemove: isLeader && index != 0, + isCurrentUser: isCurrentUser, + ); + }, + ); + } + return SizedBox.shrink(); + }, + ), + ), ); }, ); @@ -141,14 +136,15 @@ class GroupWidgetUtils { ), actions: [ HikeButton( - buttonHeight: 2.5.h, - buttonWidth: 8.w, + buttonHeight: 5.h, + buttonWidth: 20.w, onTap: () => appRouter.maybePop(false), text: 'No', + isDotted: true, ), HikeButton( - buttonHeight: 2.5.h, - buttonWidth: 8.w, + buttonHeight: 5.h, + buttonWidth: 20.w, onTap: () => appRouter.maybePop(true), text: 'Yes', ), @@ -174,130 +170,136 @@ class GroupWidgetUtils { } static Future reScheduleHikeDialog( - BuildContext context, BeaconEntity beacon) { - var startsAt = beacon.startsAt!; - var expiresAt = beacon.expiresAt!; - var previousStartDate = DateTime.fromMillisecondsSinceEpoch(startsAt); - var previousExpireDate = DateTime.fromMillisecondsSinceEpoch(expiresAt); - + BuildContext context, + BeaconEntity beacon, + ) { + var previousStartDate = + DateTime.fromMillisecondsSinceEpoch(beacon.startsAt!); + var previousExpireDate = + DateTime.fromMillisecondsSinceEpoch(beacon.expiresAt!); var previousDuration = previousExpireDate.difference(previousStartDate); - DateTime? newstartDate = previousStartDate; - TextEditingController _dateController = TextEditingController( - text: DateFormat('yyyy-MM-dd').format(previousStartDate)); + DateTime? newStartDate = previousStartDate; + var _dateController = TextEditingController( + text: DateFormat('yyyy-MM-dd').format(previousStartDate), + ); TimeOfDay? startTime = TimeOfDay( - hour: previousStartDate.hour, minute: previousStartDate.minute); - TextEditingController _startTimeController = TextEditingController( - text: DateFormat('HH:mm').format(previousStartDate)); + hour: previousStartDate.hour, + minute: previousStartDate.minute, + ); + var _startTimeController = TextEditingController( + text: DateFormat('hh:mm a').format(previousStartDate), + ); Duration? duration = previousDuration; - TextEditingController _durationController = TextEditingController( - text: previousDuration.inMinutes < 60 - ? '${previousDuration.inMinutes} minutes' - : '${previousDuration.inHours} hours'); + var _durationController = TextEditingController( + text: previousDuration.inHours > 0 + ? '${previousDuration.inHours} hour${previousDuration.inHours > 1 ? 's' : ''} ${previousDuration.inMinutes % 60} minutes' + : '${previousDuration.inMinutes} minutes', + ); - GlobalKey _createFormKey = GlobalKey(); + final _formKey = GlobalKey(); bool isSmallSized = 100.h < 800; - bool isExpired = DateTime.now() - .isAfter(DateTime.fromMillisecondsSinceEpoch(beacon.expiresAt!)); + bool isExpired = DateTime.now().isAfter(previousExpireDate); + return showDialog( context: context, builder: (context) => GestureDetector( - onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + onTap: () => FocusScope.of(context).unfocus(), child: Dialog( + backgroundColor: Colors.grey[100], shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), + borderRadius: BorderRadius.circular(16.0), ), child: SingleChildScrollView( child: Form( - key: _createFormKey, + key: _formKey, child: Container( - height: isSmallSized ? 68.h : 62.h, + height: isSmallSized ? 55.h : 50.h, child: Padding( padding: - const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Column( children: [ Text( isExpired ? 'Activate Hike' : 'Reschedule Hike', - style: TextStyle(fontSize: 30), + style: TextStyle( + fontSize: 24, + color: Colors.black, + fontWeight: FontWeight.bold, + ), ), SizedBox(height: 2.h), - // start date field + + /// Start Date Container( - height: isSmallSized ? 14.h : 12.h, + height: isSmallSized ? 11.h : 9.h, child: Padding( padding: const EdgeInsets.all(4.0), child: InkWell( onTap: () async { - newstartDate = await showDatePicker( + newStartDate = await showDatePicker( context: context, - initialDate: newstartDate ?? DateTime.now(), - firstDate: newstartDate ?? DateTime.now(), + initialDate: newStartDate ?? DateTime.now(), + firstDate: DateTime.now(), lastDate: DateTime(2100), - // builder: (context, child) => Theme( - // data: ThemeData().copyWith( - // textTheme: Theme.of(context).textTheme, - // colorScheme: ColorScheme.light( - // primary: kLightBlue, - // onPrimary: Colors.grey, - // surface: kBlue, - // ), - // ), - // child: child!), ); - if (newstartDate == null) return; - _dateController.text = DateFormat('yyyy-MM-dd') - .format(newstartDate!); + if (newStartDate != null) { + _dateController.text = DateFormat('yyyy-MM-dd') + .format(newStartDate!); + } }, child: TextFormField( validator: (value) => Validator.validateDate(value), controller: _dateController, enabled: false, - onEditingComplete: () {}, decoration: InputDecoration( - border: InputBorder.none, - hintText: 'Choose Start Date', - labelStyle: TextStyle( - fontSize: labelsize, color: kYellow), - hintStyle: TextStyle( - fontSize: hintsize, color: hintColor), - labelText: 'Start Date', - alignLabelWithHint: true, - floatingLabelBehavior: - FloatingLabelBehavior.always, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none), + fillColor: Colors.grey[200], + filled: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + border: InputBorder.none, + hintText: 'Choose Start Date', + labelStyle: TextStyle( + fontSize: 16, + color: Theme.of(context).primaryColor, + ), + hintStyle: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + labelText: 'Start Date', + floatingLabelBehavior: + FloatingLabelBehavior.always, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), ), ), ), - color: kLightBlue, ), - SizedBox(height: 2.h), - // Start Time Field + + /// Start Time Container( - height: isSmallSized ? 14.h : 12.h, + height: isSmallSized ? 11.h : 9.h, child: Padding( padding: const EdgeInsets.all(4.0), child: InkWell( onTap: () async { startTime = await showTimePicker( - context: context, - initialTime: startTime ?? - TimeOfDay( - hour: DateTime.now().hour, - minute: DateTime.now().minute + 1)); + context: context, + initialTime: startTime ?? + TimeOfDay( + hour: DateTime.now().hour, + minute: DateTime.now().minute + 1, + ), + ); if (startTime != null) { - if (startTime!.minute < 10) { - _startTimeController.text = - '${startTime!.hour}:0${startTime!.minute} ${startTime!.period == DayPeriod.am ? 'AM' : 'PM'}'; - } else { - _startTimeController.text = - '${startTime!.hour}:${startTime!.minute} ${startTime!.period == DayPeriod.am ? 'AM' : 'PM'}'; - } + _startTimeController.text = + startTime!.format(context); } }, child: TextFormField( @@ -305,31 +307,35 @@ class GroupWidgetUtils { value, _dateController.text), controller: _startTimeController, enabled: false, - onEditingComplete: () {}, decoration: InputDecoration( + fillColor: Colors.grey[200], + filled: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 16, vertical: 8), border: InputBorder.none, - alignLabelWithHint: true, - errorStyle: TextStyle(color: Colors.red[800]), - floatingLabelBehavior: - FloatingLabelBehavior.always, - labelText: 'Start Time', + hintText: 'Choose Start Time', labelStyle: TextStyle( - fontSize: labelsize, color: kYellow), + fontSize: 16, + color: Theme.of(context).primaryColor, + ), hintStyle: TextStyle( - fontSize: hintsize, color: hintColor), - hintText: 'Choose start time', + fontSize: 14, + color: Colors.grey[600], + ), + labelText: 'Start Time', + floatingLabelBehavior: + FloatingLabelBehavior.always, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, ), ), ), ), - color: kLightBlue, ), - SizedBox(height: 2.h), - // // Duration Field + + /// Duration Container( - height: isSmallSized ? 14.h : 12.h, + height: isSmallSized ? 11.h : 9.h, child: Padding( padding: const EdgeInsets.all(4.0), child: InkWell( @@ -338,77 +344,97 @@ class GroupWidgetUtils { context: context, initialTime: duration ?? Duration(minutes: 5), ); - if (duration == null) return; - if (duration!.inHours != 0 && - duration!.inMinutes != 0) { - _durationController.text = - '${duration!.inHours.toString()} hour ${(duration!.inMinutes % 60)} minutes'; - } else if (duration!.inMinutes != 0) { - _durationController.text = - '${duration!.inMinutes.toString()} minutes'; + if (duration != null) { + if (duration!.inHours > 0) { + _durationController.text = + '${duration!.inHours} hour${duration!.inHours > 1 ? 's' : ''} ${duration!.inMinutes % 60} minutes'; + } else { + _durationController.text = + '${duration!.inMinutes} minutes'; + } } }, child: TextFormField( enabled: false, controller: _durationController, validator: (value) => - Validator.validateDuration(value), + Validator.validateDuration(value.toString()), decoration: InputDecoration( - border: InputBorder.none, - alignLabelWithHint: true, - errorStyle: TextStyle(color: Colors.red[800]), - floatingLabelBehavior: - FloatingLabelBehavior.always, - labelText: 'Duration', - labelStyle: TextStyle( - fontSize: labelsize, color: kYellow), - hintStyle: TextStyle( - fontSize: hintsize, color: hintColor), - hintText: 'Enter duration of hike', - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none), + fillColor: Colors.grey[200], + filled: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + border: InputBorder.none, + hintText: 'Enter duration of hike', + labelStyle: TextStyle( + fontSize: 16, + color: Theme.of(context).primaryColor, + ), + hintStyle: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + labelText: 'Duration', + floatingLabelBehavior: + FloatingLabelBehavior.always, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), ), ), ), - color: kLightBlue, ), - SizedBox(height: 2.h), + SizedBox(height: 1.h), + + /// Button Flexible( flex: 2, - child: HikeButton( - text: 'Update', - textSize: 18.0, - textColor: Colors.white, - buttonColor: kYellow, - onTap: () async { - if (!_createFormKey.currentState!.validate()) - return; + child: ElevatedButton( + onPressed: () async { + if (_formKey.currentState!.validate()) { DateTime startsAt = DateTime( - newstartDate!.year, - newstartDate!.month, - newstartDate!.day, - startTime!.hour, - startTime!.minute); + newStartDate!.year, + newStartDate!.month, + newStartDate!.day, + startTime!.hour, + startTime!.minute, + ); final newStartsAt = startsAt.millisecondsSinceEpoch; - - final newExpiresAT = startsAt - .copyWith( - hour: startsAt.hour + duration!.inHours, - minute: - startsAt.minute + duration!.inMinutes) + final newExpiresAt = startsAt + .add(duration!) .millisecondsSinceEpoch; context.read().rescheduleHike( - newExpiresAT, newStartsAt, beacon.id!); + newExpiresAt, newStartsAt, beacon.id!); + _dateController.clear(); _startTimeController.clear(); _durationController.clear(); + appRouter.maybePop(); - // } - }), - ), + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + minimumSize: Size(160, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + padding: EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + ), + child: Text( + 'Update', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ) ], ), ), @@ -552,3 +578,85 @@ class GroupWidgetUtils { ); } } + +Widget _buildProfileCircle(String? url) { + return Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey[300], + border: Border.all(color: Colors.white, width: 2), + image: DecorationImage( + image: NetworkImage(url!), + fit: BoxFit.cover, + ), + ), + ); +} + +class _MemberTile extends StatelessWidget { + final UserEntity member; + final bool isLeader; + final bool canRemove; + final bool isCurrentUser; + + const _MemberTile({ + required this.member, + required this.isLeader, + required this.canRemove, + required this.isCurrentUser, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: kLightBlue, + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + leading: _buildProfileCircle(member.imageUrl), + title: Text( + member.name ?? 'Anonymous', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + subtitle: isCurrentUser + ? Text( + '(YOU)', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + ), + ) + : null, + trailing: isLeader + ? Text( + 'Leader', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ) + : canRemove + ? IconButton( + onPressed: () { + context + .read() + .removeMember(member.id ?? ''); + }, + icon: Icon( + Icons.person_remove_alt_1, + color: Colors.red.shade400, + ), + ) + : null, + ), + ); + } +} diff --git a/lib/presentation/hike/cubit/hike_cubit/hike_state.freezed.dart b/lib/presentation/hike/cubit/hike_cubit/hike_state.freezed.dart index 94b72b12..1aa159b2 100644 --- a/lib/presentation/hike/cubit/hike_cubit/hike_state.freezed.dart +++ b/lib/presentation/hike/cubit/hike_cubit/hike_state.freezed.dart @@ -77,6 +77,9 @@ class _$HikeStateCopyWithImpl<$Res, $Val extends HikeState> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of HikeState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -93,6 +96,9 @@ class __$$InitialHikeStateImplCopyWithImpl<$Res> __$$InitialHikeStateImplCopyWithImpl(_$InitialHikeStateImpl _value, $Res Function(_$InitialHikeStateImpl) _then) : super(_value, _then); + + /// Create a copy of HikeState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -206,6 +212,8 @@ class __$$LoadedHikeStateImplCopyWithImpl<$Res> _$LoadedHikeStateImpl _value, $Res Function(_$LoadedHikeStateImpl) _then) : super(_value, _then); + /// Create a copy of HikeState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -224,6 +232,8 @@ class __$$LoadedHikeStateImplCopyWithImpl<$Res> )); } + /// Create a copy of HikeState + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $BeaconEntityCopyWith<$Res>? get beacon { @@ -264,7 +274,9 @@ class _$LoadedHikeStateImpl implements LoadedHikeState { @override int get hashCode => Object.hash(runtimeType, beacon, message); - @JsonKey(ignore: true) + /// Create a copy of HikeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LoadedHikeStateImplCopyWith<_$LoadedHikeStateImpl> get copyWith => @@ -346,7 +358,10 @@ abstract class LoadedHikeState implements HikeState { BeaconEntity? get beacon; String? get message; - @JsonKey(ignore: true) + + /// Create a copy of HikeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$LoadedHikeStateImplCopyWith<_$LoadedHikeStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -368,6 +383,8 @@ class __$$ErrorHikeStateImplCopyWithImpl<$Res> _$ErrorHikeStateImpl _value, $Res Function(_$ErrorHikeStateImpl) _then) : super(_value, _then); + /// Create a copy of HikeState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -407,7 +424,9 @@ class _$ErrorHikeStateImpl implements ErrorHikeState { @override int get hashCode => Object.hash(runtimeType, errmessage); - @JsonKey(ignore: true) + /// Create a copy of HikeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ErrorHikeStateImplCopyWith<_$ErrorHikeStateImpl> get copyWith => @@ -487,7 +506,10 @@ abstract class ErrorHikeState implements HikeState { factory ErrorHikeState({final String? errmessage}) = _$ErrorHikeStateImpl; String? get errmessage; - @JsonKey(ignore: true) + + /// Create a copy of HikeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$ErrorHikeStateImplCopyWith<_$ErrorHikeStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/presentation/hike/cubit/location_cubit/location_cubit.dart b/lib/presentation/hike/cubit/location_cubit/location_cubit.dart index 82dbc6c7..6d9c4933 100644 --- a/lib/presentation/hike/cubit/location_cubit/location_cubit.dart +++ b/lib/presentation/hike/cubit/location_cubit/location_cubit.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; -import 'dart:ui'; +import 'dart:ui' as ui; +import 'package:http/http.dart' as http; +import 'package:flutter/services.dart'; import 'package:beacon/core/resources/data_state.dart'; import 'package:beacon/core/utils/constants.dart'; import 'package:beacon/domain/entities/beacon/beacon_entity.dart'; @@ -13,7 +15,6 @@ import 'package:beacon/domain/usecase/hike_usecase.dart'; import 'package:beacon/locator.dart'; import 'package:beacon/presentation/hike/cubit/location_cubit/location_state.dart'; import 'package:beacon/presentation/hike/cubit/panel_cubit/panel_cubit.dart'; -import 'package:beacon/presentation/widgets/custom_label_marker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animarker/core/ripple_marker.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -21,7 +22,6 @@ import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'package:gap/gap.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:location/location.dart'; -import 'package:http/http.dart' as http; import 'package:responsive_sizer/responsive_sizer.dart'; import 'package:vibration/vibration.dart'; @@ -49,6 +49,13 @@ class LocationCubit extends Cubit { LocationData? _lastLocation; Set _geofence = {}; MapType _mapType = MapType.normal; + List infoMarkers = [ + "location-marker", + "wind", + "rain", + "camp", + ]; + String selectedInfoMarker = "location-marker"; StreamSubscription>? _beaconlocationsSubscription; @@ -59,10 +66,38 @@ class LocationCubit extends Cubit { BuildContext? context; TickerProvider? vsync; + void setLandmarkIcon(String icon) { + selectedInfoMarker = icon; + // emit(InitialLocationState()); + } + void onMapCreated(GoogleMapController controller) { mapController = controller; } + void centerMap() { + Location location = new Location(); + location.changeSettings( + interval: 5000, accuracy: LocationAccuracy.high, distanceFilter: 0); + + _streamLocaitonData = + location.onLocationChanged.listen((LocationData newPosition) async { + var latLng = locationDataToLatLng(newPosition); + + mapController?.animateCamera(CameraUpdate.newCameraPosition( + CameraPosition(target: latLng, zoom: 15), + )); + }); + } + + void zoomIn() { + mapController?.animateCamera(CameraUpdate.zoomIn()); + } + + void zoomOut() { + mapController?.animateCamera(CameraUpdate.zoomOut()); + } + Future loadBeaconData( BeaconEntity beacon, TickerProvider vsync, BuildContext context) async { this.vsync = vsync; @@ -90,8 +125,16 @@ class LocationCubit extends Cubit { position: locationToLatLng(_leader!.location!), ripple: false, infoWindow: InfoWindow( - title: '${_beacon!.leader?.name ?? 'Anonymous'}}', + title: 'this is ${_beacon!.leader?.name ?? 'Anonymous'}}', ), + icon: BitmapDescriptor.bytes( + await getCircularImageWithBorderAndPointer( + _leader!.imageUrl ?? + 'https://cdn.jsdelivr.net/gh/alohe/avatars/png/toon_5.png', + size: 80, + borderColor: Colors.red, + borderWidth: 4, + )), onTap: () { log('${beacon.leader?.name}'); })); @@ -111,13 +154,13 @@ class LocationCubit extends Cubit { } if (beacon.route != null) { - var marker = Marker( - icon: - BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure), - markerId: MarkerId('leader initial position'), - position: locationToLatLng(beacon.route!.first!)); + // var marker = Marker( + // markerId: MarkerId('leader initial position'), + // icon: + // BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueYellow), + // position: locationToLatLng(beacon.route!.first!)); - _hikeMarkers.add(marker); + // _hikeMarkers.add(marker); // handling polyline here for (var point in beacon.route!) { @@ -202,7 +245,7 @@ class LocationCubit extends Cubit { emit(LoadedLocationState( geofence: _geofence, - locationMarkers: _hikeMarkers, + locationMarkers: Set.from(_hikeMarkers), polyline: _polyline, version: DateTime.now().millisecondsSinceEpoch, mapType: _mapType, @@ -215,7 +258,7 @@ class LocationCubit extends Cubit { PolylinePoints polylinePoints = PolylinePoints(); try { PolylineResult result = await polylinePoints.getRouteBetweenCoordinates( - 'AIzaSyBdIpiEfBE5DohHgBvwPTljZQAcNWcKwCs', + 'AIzaSyC72TkXzQTsnbGdZy5ldeX64y0mofn_iUs', PointLatLng(_points.first.latitude, _points.first.longitude), PointLatLng(_points.last.longitude, _points.last.longitude)); @@ -317,19 +360,19 @@ class LocationCubit extends Cubit { .listen((dataState) async { if (dataState is DataSuccess && dataState.data != null) { BeaconLocationsEntity beaconLocationsEntity = dataState.data!; - // when new landmark is created if (beaconLocationsEntity.landmark != null) { LandMarkEntity newLandMark = beaconLocationsEntity.landmark!; - await _createLandMarkMarker(newLandMark); emit(LoadedLocationState( polyline: _polyline, - locationMarkers: _hikeMarkers, + locationMarkers: + Set.from(_hikeMarkers), // Create new Set instance mapType: _mapType, geofence: _geofence, - version: DateTime.now().millisecond, + version: DateTime.now() + .millisecondsSinceEpoch, // Use millisecondsSinceEpoch message: 'A landmark is created by ${beaconLocationsEntity.landmark!.createdBy!.name ?? 'Anonymous'}')); } @@ -343,9 +386,9 @@ class LocationCubit extends Cubit { emit(LoadedLocationState( polyline: _polyline, geofence: _geofence, - locationMarkers: _hikeMarkers, + locationMarkers: Set.from(_hikeMarkers), mapType: _mapType, - version: DateTime.now().microsecond)); + version: DateTime.now().millisecondsSinceEpoch)); // add marker for user } @@ -377,6 +420,14 @@ class LocationCubit extends Cubit { if (markers.isEmpty) { _hikeMarkers.add(Marker( markerId: MarkerId(_beacon!.leader!.id.toString()), + icon: BitmapDescriptor.bytes( + await getCircularImageWithBorderAndPointer( + _leader!.imageUrl ?? + 'https://cdn.jsdelivr.net/gh/alohe/avatars/png/toon_5.png', + size: 80, + borderColor: Colors.red, + borderWidth: 4, + )), position: _points.last)); } var leaderRipplingMarker = markers.first; @@ -396,6 +447,8 @@ class LocationCubit extends Cubit { if (initialMarkers.isEmpty) { _hikeMarkers.add(RippleMarker( markerId: MarkerId('leader initial position'), + icon: BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueYellow), ripple: false, infoWindow: InfoWindow(title: 'Leader initial position'), position: _points.first)); @@ -405,10 +458,10 @@ class LocationCubit extends Cubit { calculateMapBoundsFromListOfLatLng(_points), 50)); emit(LoadedLocationState( + polyline: _polyline, geofence: _geofence, - locationMarkers: _hikeMarkers, + locationMarkers: Set.from(_hikeMarkers), mapType: _mapType, - polyline: _polyline, version: DateTime.now().millisecondsSinceEpoch)); } else if (beaconLocationsEntity.userSOS != null) { var user = beaconLocationsEntity.userSOS!; @@ -615,26 +668,30 @@ class LocationCubit extends Cubit { } Future createLandmark( - String beaconId, String title, LatLng latlng) async { + String beaconId, String title, LatLng latlng, String icon) async { var dataState = await _hikeUseCase.createLandMark(beaconId, title, - latlng.latitude.toString(), latlng.longitude.toString()); + latlng.latitude.toString(), latlng.longitude.toString(), icon); if (dataState is DataSuccess && dataState.data != null) { await _createLandMarkMarker(dataState.data!); + + await locationUpdateSubscription(beaconId); + emit(LoadedLocationState( polyline: _polyline, geofence: _geofence, mapType: _mapType, - locationMarkers: Set.from(_hikeMarkers), + locationMarkers: + Set.from(_hikeMarkers), // Create new Set instance + version: + DateTime.now().millisecondsSinceEpoch, // Consistent versioning message: 'New marker created by ${dataState.data!.createdBy!.name}')); } } Future sendSOS(String id, BuildContext context) async { final dataState = await _hikeUseCase.sos(id); - if (dataState is DataSuccess) { - log('data coming from sos: ${dataState.data.toString()}'); // // Ensure _hikeMarkers is a Set of marker objects var userId = localApi.userModel.id; @@ -675,7 +732,9 @@ class LocationCubit extends Cubit { _hikeMarkers.where((element) => element.markerId == markerId); if (existingMarkers.isEmpty) { - var newMarker = await createMarker(landMark); + var newMarker = await createMarkerWithLocalAsset( + landMark, + ); _hikeMarkers.add(newMarker); } else { // If the marker exists, update its position @@ -688,11 +747,17 @@ class LocationCubit extends Cubit { } } - void _createUserMarker(UserEntity user, {bool isLeader = false}) async { + void _createUserMarker(UserEntity user) async { final markerId = MarkerId(user.id!); final markerPosition = locationToLatLng(user.location!); - // final bitmap = await _createCustomMarkerBitmap(); + final Uint8List markerIcon = await getCircularImageWithBorderAndPointer( + user.imageUrl ?? + 'https://cdn.jsdelivr.net/gh/alohe/avatars/png/toon_5.png', + size: 80, + borderColor: Colors.deepPurple, + borderWidth: 4, + ); final existingMarkers = _hikeMarkers.where((element) => element.markerId == markerId); @@ -703,8 +768,7 @@ class LocationCubit extends Cubit { markerId: markerId, position: markerPosition, infoWindow: InfoWindow(title: user.name ?? 'Anonymous'), - icon: BitmapDescriptor.defaultMarkerWithHue( - isLeader ? BitmapDescriptor.hueRed : BitmapDescriptor.hueOrange)); + icon: BitmapDescriptor.bytes(markerIcon)); _hikeMarkers.add(newMarker); } else { // If the marker exists, update its position @@ -717,25 +781,139 @@ class LocationCubit extends Cubit { } } - Future createMarker(LandMarkEntity landmark) async { - final pictureRecorder = PictureRecorder(); - final canvas = Canvas(pictureRecorder); - final customMarker = CustomMarker(text: landmark.title!); - customMarker.paint(canvas, Size(100, 100)); - final picture = pictureRecorder.endRecording(); - final image = await picture.toImage(100, 100); - final bytes = await image.toByteData(format: ImageByteFormat.png); + Future createMarkerWithLocalAsset(LandMarkEntity landmark) async { + Future getResizedMarkerIcon( + String assetPath, int width) async { + final ByteData data = await rootBundle.load(assetPath); + final codec = await ui.instantiateImageCodec( + data.buffer.asUint8List(), + targetWidth: width, + ); + final frame = await codec.getNextFrame(); + final ui.Image image = frame.image; + + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + final Uint8List resizedBytes = byteData!.buffer.asUint8List(); + + return BitmapDescriptor.bytes(resizedBytes); + } + + BitmapDescriptor customIcon = await getResizedMarkerIcon( + landmark.icon ?? 'images/icons/location-marker.png', + 80, // desired width in pixels + ); return Marker( markerId: MarkerId(landmark.id!.toString()), position: locationToLatLng(landmark.location!), - icon: BitmapDescriptor.bytes(bytes!.buffer.asUint8List()), + icon: customIcon, infoWindow: InfoWindow( - title: 'Created by: ${landmark.createdBy?.name ?? 'Anonymous'}', + title: + '${landmark.title} by ${landmark.createdBy?.name ?? 'Anonymous'}', ), ); } + Future createMarkerWithCircularNetworkImage( + LandMarkEntity landmark) async { + final Uint8List markerIcon = await getCircularImageWithBorderAndPointer( + landmark.icon ?? 'images/icons/location-marker.png', + size: 80, + borderColor: Colors.teal, + borderWidth: 4, + isUrl: false, + ); + + return Marker( + markerId: MarkerId(landmark.id!.toString()), + position: locationToLatLng(landmark.location!), + icon: BitmapDescriptor.bytes(markerIcon), + infoWindow: InfoWindow( + title: + '${landmark.title} by ${landmark.createdBy?.name ?? 'Anonymous'}', + ), + ); + } + + Future getCircularImageWithBorderAndPointer( + String imagePath, { + int size = 150, + Color borderColor = Colors.red, + double borderWidth = 6, + bool isUrl = true, + }) async { + Uint8List bytes; + + if (isUrl) { + // Handle URL images + final http.Response response = await http.get(Uri.parse(imagePath)); + bytes = response.bodyBytes; + } else { + // Handle asset images + final ByteData byteData = await rootBundle.load(imagePath); + bytes = byteData.buffer.asUint8List(); + } + + final ui.Codec codec = await ui.instantiateImageCodec( + bytes, + targetWidth: size, + targetHeight: size, + ); + final ui.FrameInfo frame = await codec.getNextFrame(); + final ui.Image image = frame.image; + + final double radius = size / 2; + final double triangleHeight = size * 0.35; // Increase triangle height here + final double totalHeight = size + triangleHeight; + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final Paint paint = Paint()..isAntiAlias = true; + + final Offset circleCenter = Offset(radius, radius); + + // 1. Draw triangle FIRST (so it goes behind the circle) + paint + ..style = PaintingStyle.fill + ..color = borderColor; + + final Path trianglePath = Path() + ..moveTo(radius - 25, size.toDouble() - 10) // narrower base + ..lineTo(radius + 25, size.toDouble() - 10) + ..lineTo(radius, size + triangleHeight - 10) + ..close(); + + canvas.drawPath(trianglePath, paint); + + // 2. Draw white background circle + paint.color = Colors.white; + canvas.drawCircle(circleCenter, radius, paint); + + // 3. Clip and draw circular image + Path clipPath = Path() + ..addOval( + Rect.fromCircle(center: circleCenter, radius: radius - borderWidth)); + canvas.save(); + canvas.clipPath(clipPath); + canvas.drawImage(image, Offset.zero, Paint()); + canvas.restore(); + + // 4. Draw circular border + paint + ..color = borderColor + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth; + canvas.drawCircle(circleCenter, radius - borderWidth / 2, paint); + + // 5. Convert to PNG bytes + final ui.Image finalImage = + await recorder.endRecording().toImage(size, totalHeight.toInt()); + final ByteData? byteData = + await finalImage.toByteData(format: ui.ImageByteFormat.png); + + return byteData!.buffer.asUint8List(); + } + void changeGeofenceRadius(double radius, LatLng center) { var index = _geofence .toList() diff --git a/lib/presentation/hike/cubit/location_cubit/location_state.freezed.dart b/lib/presentation/hike/cubit/location_cubit/location_state.freezed.dart index f9cdabef..9c749f54 100644 --- a/lib/presentation/hike/cubit/location_cubit/location_state.freezed.dart +++ b/lib/presentation/hike/cubit/location_cubit/location_state.freezed.dart @@ -99,6 +99,9 @@ class _$LocationStateCopyWithImpl<$Res, $Val extends LocationState> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of LocationState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -115,6 +118,9 @@ class __$$InitialLocationStateImplCopyWithImpl<$Res> __$$InitialLocationStateImplCopyWithImpl(_$InitialLocationStateImpl _value, $Res Function(_$InitialLocationStateImpl) _then) : super(_value, _then); + + /// Create a copy of LocationState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -254,6 +260,8 @@ class __$$LoadedLocationStateImplCopyWithImpl<$Res> $Res Function(_$LoadedLocationStateImpl) _then) : super(_value, _then); + /// Create a copy of LocationState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -372,7 +380,9 @@ class _$LoadedLocationStateImpl implements LoadedLocationState { message, version); - @JsonKey(ignore: true) + /// Create a copy of LocationState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LoadedLocationStateImplCopyWith<_$LoadedLocationStateImpl> get copyWith => @@ -487,7 +497,10 @@ abstract class LoadedLocationState implements LocationState { Set get polyline; String? get message; int get version; - @JsonKey(ignore: true) + + /// Create a copy of LocationState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$LoadedLocationStateImplCopyWith<_$LoadedLocationStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -509,6 +522,8 @@ class __$$LocationErrorStateImplCopyWithImpl<$Res> $Res Function(_$LocationErrorStateImpl) _then) : super(_value, _then); + /// Create a copy of LocationState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -547,7 +562,9 @@ class _$LocationErrorStateImpl implements LocationErrorState { @override int get hashCode => Object.hash(runtimeType, message); - @JsonKey(ignore: true) + /// Create a copy of LocationState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LocationErrorStateImplCopyWith<_$LocationErrorStateImpl> get copyWith => @@ -649,7 +666,10 @@ abstract class LocationErrorState implements LocationState { _$LocationErrorStateImpl; String? get message; - @JsonKey(ignore: true) + + /// Create a copy of LocationState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$LocationErrorStateImplCopyWith<_$LocationErrorStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/presentation/hike/cubit/panel_cubit/panel_state.freezed.dart b/lib/presentation/hike/cubit/panel_cubit/panel_state.freezed.dart index ff5fb374..89decf7a 100644 --- a/lib/presentation/hike/cubit/panel_cubit/panel_state.freezed.dart +++ b/lib/presentation/hike/cubit/panel_cubit/panel_state.freezed.dart @@ -99,6 +99,9 @@ class _$SlidingPanelStateCopyWithImpl<$Res, $Val extends SlidingPanelState> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of SlidingPanelState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -115,6 +118,9 @@ class __$$InitialPanelStateImplCopyWithImpl<$Res> __$$InitialPanelStateImplCopyWithImpl(_$InitialPanelStateImpl _value, $Res Function(_$InitialPanelStateImpl) _then) : super(_value, _then); + + /// Create a copy of SlidingPanelState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -255,6 +261,8 @@ class __$$LoadedPanelStateImplCopyWithImpl<$Res> $Res Function(_$LoadedPanelStateImpl) _then) : super(_value, _then); + /// Create a copy of SlidingPanelState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -293,6 +301,8 @@ class __$$LoadedPanelStateImplCopyWithImpl<$Res> )); } + /// Create a copy of SlidingPanelState + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $UserEntityCopyWith<$Res>? get leader { @@ -371,7 +381,9 @@ class _$LoadedPanelStateImpl implements LoadedPanelState { const DeepCollectionEquality().hash(_followers), message); - @JsonKey(ignore: true) + /// Create a copy of SlidingPanelState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LoadedPanelStateImplCopyWith<_$LoadedPanelStateImpl> get copyWith => @@ -486,7 +498,10 @@ abstract class LoadedPanelState implements SlidingPanelState { UserEntity? get leader; List? get followers; String? get message; - @JsonKey(ignore: true) + + /// Create a copy of SlidingPanelState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$LoadedPanelStateImplCopyWith<_$LoadedPanelStateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -508,6 +523,8 @@ class __$$ErrorPanelStateImplCopyWithImpl<$Res> _$ErrorPanelStateImpl _value, $Res Function(_$ErrorPanelStateImpl) _then) : super(_value, _then); + /// Create a copy of SlidingPanelState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -546,7 +563,9 @@ class _$ErrorPanelStateImpl implements ErrorPanelState { @override int get hashCode => Object.hash(runtimeType, message); - @JsonKey(ignore: true) + /// Create a copy of SlidingPanelState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ErrorPanelStateImplCopyWith<_$ErrorPanelStateImpl> get copyWith => @@ -647,7 +666,10 @@ abstract class ErrorPanelState implements SlidingPanelState { factory ErrorPanelState({final String? message}) = _$ErrorPanelStateImpl; String? get message; - @JsonKey(ignore: true) + + /// Create a copy of SlidingPanelState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$ErrorPanelStateImplCopyWith<_$ErrorPanelStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/presentation/hike/hike_screen.dart b/lib/presentation/hike/hike_screen.dart index fd5ebe6d..f02ef50f 100644 --- a/lib/presentation/hike/hike_screen.dart +++ b/lib/presentation/hike/hike_screen.dart @@ -1,26 +1,20 @@ import 'package:auto_route/auto_route.dart'; -import 'package:beacon/config/pip_manager.dart'; import 'package:beacon/core/utils/constants.dart'; import 'package:beacon/domain/entities/beacon/beacon_entity.dart'; -import 'package:beacon/domain/entities/user/user_entity.dart'; import 'package:beacon/locator.dart'; import 'package:beacon/presentation/hike/cubit/hike_cubit/hike_cubit.dart'; import 'package:beacon/presentation/hike/cubit/hike_cubit/hike_state.dart'; import 'package:beacon/presentation/hike/cubit/location_cubit/location_cubit.dart'; import 'package:beacon/presentation/hike/cubit/location_cubit/location_state.dart'; -import 'package:beacon/presentation/hike/cubit/panel_cubit/panel_cubit.dart'; -import 'package:beacon/presentation/hike/cubit/panel_cubit/panel_state.dart'; import 'package:beacon/presentation/hike/widgets/hike_screen_widget.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:beacon/presentation/hike/widgets/search_places.dart'; +import 'package:beacon/presentation/widgets/hike_button.dart'; +import 'package:beacon/presentation/widgets/screen_template.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; -import 'package:gap/gap.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:responsive_sizer/responsive_sizer.dart'; -import 'package:simple_pip_mode/pip_widget.dart'; -import 'package:simple_pip_mode/simple_pip.dart'; -import 'package:sliding_up_panel/sliding_up_panel.dart'; @RoutePage() class HikeScreen extends StatefulWidget { @@ -42,14 +36,12 @@ class _HikeScreenState extends State void initState() { WidgetsBinding.instance.addObserver(this); _hikeCubit.startHike(widget.beacon.id!, this, context); - SimplePip().setAutoPipMode(aspectRatio: (2, 3)); super.initState(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); - PIPMode.disablePIPMode(); _hikeCubit.clear(); _locationCubit.clear(); super.dispose(); @@ -58,81 +50,116 @@ class _HikeScreenState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - if (state == AppLifecycleState.paused) {} } bool isSmallsized = 100.h < 800; - PanelController _panelController = PanelController(); @override Widget build(BuildContext context) { return Scaffold( - body: PipWidget( - onPipExited: () { - _panelController.open(); - }, - builder: (context) { - return BlocBuilder( - builder: (context, state) { - if (state is InitialHikeState) { - return Center( - child: SpinKitWave( - color: kYellow, - )); - } else if (state is ErrorHikeState) { - return Container( - child: Center(child: Text('Restart beacon')), - ); - } else { - return Scaffold( - body: Stack( + body: BlocBuilder( + builder: (context, state) { + if (state is InitialHikeState) { + return Center( + child: SpinKitWave( + color: kYellow, + )); + } else if (state is ErrorHikeState) { + return Container( + child: Center(child: Text('Restart beacon')), + ); + } else { + return BeaconScreenTemplate( + body: Stack( + children: [ + _mapScreen(), + LocationSearchWidget(widget.beacon.id!), + Positioned( + bottom: 200, + right: 10, + child: Column( children: [ - SlidingUpPanel( - onPanelOpened: () { - setState(() {}); - }, - borderRadius: BorderRadius.only( - topRight: Radius.circular(15), - topLeft: Radius.circular(15), - ), - controller: _panelController, - maxHeight: 60.h, - minHeight: isSmallsized ? 22.h : 20.h, - panel: _SlidingPanelWidget(), - collapsed: _collapsedWidget(), - body: _mapScreen()), - Align( - alignment: Alignment(-0.9, -0.9), - child: FloatingActionButton( - heroTag: 'BackButton', - backgroundColor: kYellow, - onPressed: () { - SimplePip().enterPipMode(); - }, - child: Icon( - CupertinoIcons.back, - color: kBlue, - ), - ), + FloatingActionButton( + backgroundColor: Colors.white, + mini: true, + onPressed: () => _locationCubit.zoomIn(), + child: Icon(Icons.add), + ), + FloatingActionButton( + backgroundColor: Colors.white, + mini: true, + onPressed: () => _locationCubit.zoomOut(), + child: Icon(Icons.remove), + ), + SizedBox(height: 2), + FloatingActionButton( + backgroundColor: Colors.white, + mini: true, + onPressed: () => _locationCubit.centerMap(), + child: Icon(Icons.map), ), - Align( - alignment: Alignment(0.85, -0.9), - child: HikeScreenWidget.shareButton( - context, widget.beacon.shortcode, widget.beacon)), - Align( - alignment: Alignment(1, -0.7), - child: HikeScreenWidget.showMapViewSelector(context)), - Align( - alignment: Alignment(0.85, -0.5), - child: HikeScreenWidget.sosButton( - widget.beacon.id!, context)), ], - )); - } - }, - ); - }, - pipChild: _mapScreen()), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + height: 100, + child: Container( + height: 100, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Left Circular Icon Button + Container( + width: 50, + height: 50, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 4, + ), + ], + image: DecorationImage( + image: NetworkImage( + "https://media.istockphoto.com/id/1253926432/vector/flashlight-warning-alarm-light-and-siren-light-flat-design-vector-design.jpg?s=612x612&w=0&k=20&c=yOj6Jpu7XDrPJCTfUIpQm-LWI9q9RWQB91s-N7CgQDQ="))), + ), + + const SizedBox(width: 10), + + // Right Red SOS Button + HikeButton( + buttonWidth: 70.w, + buttonHeight: 50, + text: 'Send SOS', + onTap: () { + locator() + .sendSOS(widget.beacon.id!, context); + }, + textSize: 18.0, + buttonColor: Colors.red, + textColor: Colors.white, + ) + ], + ), + )), + ], + )); + } + }, + ), ); } @@ -152,20 +179,24 @@ class _HikeScreenState extends State ); } else if (state is LoadedLocationState) { return GoogleMap( - circles: state.geofence, - polylines: state.polyline, - mapType: state.mapType, - compassEnabled: true, - onTap: (latlng) { - _panelController.close(); - HikeScreenWidget.showCreateLandMarkDialogueDialog( - context, widget.beacon.id!, latlng); - }, - zoomControlsEnabled: true, - onMapCreated: _locationCubit.onMapCreated, - markers: state.locationMarkers, - initialCameraPosition: CameraPosition( - zoom: 15, target: state.locationMarkers.first.position)); + circles: state.geofence, + polylines: state.polyline, + trafficEnabled: true, + mapType: MapType.normal, + onLongPress: (latlng) { + HikeScreenWidget.showCreateLandMarkDialogueDialog( + context, + widget.beacon.id!, + latlng, + ); + }, + onMapCreated: _locationCubit.onMapCreated, + markers: state.locationMarkers, + initialCameraPosition: CameraPosition( + zoom: 15, + target: state.locationMarkers.first.position, + ), + ); } return Center( child: GestureDetector( @@ -178,166 +209,4 @@ class _HikeScreenState extends State }, ); } - - Widget _collapsedWidget() { - var beacon = widget.beacon; - return Container( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: kBlue, - borderRadius: BorderRadius.only( - topRight: Radius.circular(10), - topLeft: Radius.circular(10), - ), - ), - child: BlocBuilder( - builder: (context, state) { - return state.when( - initial: () { - return SpinKitCircle(color: kYellow); - }, - loaded: ( - isActive, - expiringTime, - leaderAddress, - leader, - followers, - message, - ) { - followers = followers ?? []; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - alignment: Alignment.center, - child: Container( - alignment: Alignment.center, - height: 0.8.h, - width: 18.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.all(Radius.circular(10))), - ), - ), - Gap(10), - Text( - isActive == true - ? 'Beacon expiring at ${expiringTime ?? '<>'}' - : 'Beacon is expired', - style: TextStyle( - fontSize: 18, - color: Colors.white, - fontFamily: '', - fontWeight: FontWeight.w700)), - Gap(2), - Text('Beacon leader at: ${leaderAddress ?? '<>'}', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 16, - color: Colors.white, - fontFamily: '', - fontWeight: FontWeight.w600)), - Gap(1.5), - Text('Total followers: ${followers.length} ', - style: TextStyle( - fontSize: 16, - color: Colors.white, - fontFamily: '', - fontWeight: FontWeight.w500)), - Gap(1), - Text('Share the pass key to join user: ${beacon.shortcode}', - style: TextStyle( - fontSize: 15, - color: Colors.white, - fontFamily: '', - fontWeight: FontWeight.w500)) - ], - ); - }, - error: (message) { - return Text(message.toString()); - }, - ); - }, - )); - } - - Widget _SlidingPanelWidget() { - // return Container(); - return BlocBuilder( - builder: (context, state) { - return state.when( - initial: () { - return CircularProgressIndicator(); - }, - loaded: ( - isActive, - expiringTime, - leaderAddress, - leader, - followers, - message, - ) { - List members = []; - members.add(leader!); - if (followers != null) { - followers.forEach((element) { - members.add(element!); - }); - } - - return ListView.builder( - physics: NeverScrollableScrollPhysics(), - itemCount: members.length, - itemBuilder: (context, index) { - var member = members[index]; - return Container( - padding: EdgeInsets.symmetric(vertical: 5), - child: Row( - children: [ - Gap(10), - CircleAvatar( - radius: 25, - backgroundColor: kYellow, - child: Icon( - Icons.person_2_rounded, - color: Colors.white, - size: 40, - ), - ), - Gap(10), - Text( - member.name ?? 'Anonymous', - style: TextStyle(fontSize: 19), - ), - Spacer(), - Container( - height: 40, - width: 40, - child: FloatingActionButton( - backgroundColor: kYellow, - onPressed: () async { - _locationCubit - .changeCameraPosition(member.id ?? ''); - _panelController.close(); - }, - child: Icon(Icons.location_city), - ), - ), - Gap(10), - ], - ), - ); - }, - ); - }, - error: (message) { - return Text(message.toString()); - }, - ); - }, - ); - } } diff --git a/lib/presentation/hike/services/geoapify_service.dart b/lib/presentation/hike/services/geoapify_service.dart new file mode 100644 index 00000000..31ff9d8f --- /dev/null +++ b/lib/presentation/hike/services/geoapify_service.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; +import 'package:beacon/config/enviornment_config.dart'; +import 'package:beacon/data/models/landmark/location_suggestion.dart'; +import 'package:http/http.dart' as http; + +class GeoapifyService { + static String _apiKey = EnvironmentConfig.geoApifyApiKey!; + + static const String _baseUrl = + 'https://api.geoapify.com/v1/geocode/autocomplete'; + + Future> getLocationSuggestions(String query) async { + if (query.isEmpty) { + return []; + } + + final url = Uri.parse( + "$_baseUrl?text=${Uri.encodeComponent(query)}&format=json&type=city&apiKey=$_apiKey"); + + try { + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + + List results = data['results'] ?? []; + + return results.map((item) { + return LocationSuggestion.fromJson(item); + }).toList(); + } else { + throw Exception( + "Failed to fetch location suggestions: ${response.statusCode}"); + } + } catch (e) { + throw Exception("Failed to fetch location suggestions: $e"); + } + } +} diff --git a/lib/presentation/hike/stack.dart b/lib/presentation/hike/stack.dart new file mode 100644 index 00000000..acb398a1 --- /dev/null +++ b/lib/presentation/hike/stack.dart @@ -0,0 +1,32 @@ +class Stack { + final List _stack = []; + + void push(T element) { + _stack.add(element); + } + + T pop() { + if (_stack.isEmpty) { + throw StateError("No elements in the Stack"); + } else { + T lastElement = _stack.last; + _stack.removeLast(); + return lastElement; + } + } + + T top() { + if (_stack.isEmpty) { + throw StateError("No elements in the Stack"); + } else { + return _stack.last; + } + } + + bool isEmpty() { + return _stack.isEmpty; + } + + @override + String toString() => _stack.toString(); +} diff --git a/lib/presentation/hike/widgets/hike_screen_widget.dart b/lib/presentation/hike/widgets/hike_screen_widget.dart index 5afd9e7d..e41a4e25 100644 --- a/lib/presentation/hike/widgets/hike_screen_widget.dart +++ b/lib/presentation/hike/widgets/hike_screen_widget.dart @@ -4,7 +4,6 @@ import 'package:beacon/locator.dart'; import 'package:beacon/presentation/hike/cubit/location_cubit/location_cubit.dart'; import 'package:beacon/presentation/widgets/hike_button.dart'; import 'package:beacon/core/utils/constants.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_geocoder_alternative/flutter_geocoder_alternative.dart'; @@ -28,17 +27,6 @@ class HikeScreenWidget { Share.share('To join beacon follow this link: $url'); } - static Widget sosButton(String id, BuildContext context) { - return FloatingActionButton( - heroTag: 'sos', - backgroundColor: kYellow, - onPressed: () { - locator().sendSOS(id, context); - }, - child: Icon(Icons.sos), - ); - } - static Widget shareButton( BuildContext context, String? passkey, BeaconEntity beacon) { return FloatingActionButton( @@ -163,44 +151,6 @@ class HikeScreenWidget { ); } - static final Map mapTypeNames = { - MapType.hybrid: 'Hybrid', - MapType.normal: 'Normal', - MapType.satellite: 'Satellite', - MapType.terrain: 'Terrain', - }; - - static MapType _selectedMapType = MapType.normal; - - static Widget showMapViewSelector(BuildContext context) { - return DropdownButton( - icon: null, - value: _selectedMapType, - items: mapTypeNames.entries.map((entry) { - return DropdownMenuItem( - onTap: () { - locator().changeMap(entry.key); - }, - value: entry.key, - child: Text(entry.value), - ); - }).toList(), - onChanged: (newValue) {}, - selectedItemBuilder: (BuildContext context) { - return mapTypeNames.entries.map((entry) { - return FloatingActionButton( - backgroundColor: kYellow, - onPressed: null, - child: Icon(CupertinoIcons.map), - ); - }).toList(); - }, - ); - } - - static TextEditingController _landMarkeController = TextEditingController(); - static GlobalKey _landmarkFormKey = GlobalKey(); - static void selectionButton( BuildContext context, String beaconId, LatLng loc) { showDialog( @@ -233,112 +183,7 @@ class HikeScreenWidget { ); } - // static GlobalKey _geofenceKey = GlobalKey(); - // static double _value = 0.1; - - // static void showCreateGeofenceDialogueDialog( - // BuildContext context, - // String beaconId, - // LatLng loc, - // ) { - // bool isSmallSized = 100.h < 800; - // bool isGeofenceCreated = false; - - // showModalBottomSheet( - // context: context, - // isDismissible: false, - // builder: (context) { - // return Stack( - // children: [ - // Container( - // height: isSmallSized ? 30.h : 25.h, - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: BorderRadius.only( - // topLeft: Radius.circular(20), - // topRight: Radius.circular(20), - // ), - // ), - // child: Padding( - // padding: - // const EdgeInsets.symmetric(horizontal: 32, vertical: 16), - // child: Form( - // key: _geofenceKey, - // child: Column( - // mainAxisSize: MainAxisSize.min, - // children: [ - // Container( - // alignment: Alignment.topRight, - // child: IconButton( - // onPressed: () { - // appRouter.maybePop().then((value) { - // locator() - // .removeUncreatedGeofence(); - // }); - // }, - // icon: Icon( - // Icons.cancel, - // color: kBlue, - // )), - // ), - // StatefulBuilder( - // builder: (context, setState) { - // return Stack( - // children: [ - // Slider( - // activeColor: kBlue, - // inactiveColor: kYellow, - // value: _value, - // onChanged: (value) { - // setState(() { - // _value = value; - // }); - // locator() - // .changeGeofenceRadius(_value * 1000, loc); - // }, - // ), - // Align( - // alignment: Alignment(1, -1), - // child: Text( - // '${(_value * 1000).toStringAsFixed(0)} meters', - // ), - // ), - // ], - // ); - // }, - // ), - // Gap(10), - // Flexible( - // child: HikeButton( - // buttonHeight: 15, - // onTap: () async { - // if (!_geofenceKey.currentState!.validate()) return; - // locator() - // .createGeofence(beaconId, loc, _value) - // .then((onvalue) { - // isGeofenceCreated = true; - // }); - // appRouter.maybePop().then((onValue) { - // if (isGeofenceCreated) { - // locator() - // .removeUncreatedGeofence(); - // } - // }); - // }, - // text: 'Create Geofence', - // ), - // ), - // ], - // ), - // ), - // ), - // ), - // ], - // ); - // }, - // ); - // } - +// Updated static method to show the dialog static void showCreateLandMarkDialogueDialog( BuildContext context, String beaconId, @@ -346,67 +191,180 @@ class HikeScreenWidget { ) { showDialog( context: context, - builder: (context) => Dialog( - child: Container( - height: MediaQuery.of(context).size.height < 800 ? 33.h : 25.h, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), - child: Form( - key: _landmarkFormKey, - child: Column( - children: [ - Container( - height: - MediaQuery.of(context).size.height < 800 ? 14.h : 12.h, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: TextFormField( - controller: _landMarkeController, - style: TextStyle(fontSize: 20.0), - onChanged: (key) {}, - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter title for landmark"; - } else { - return null; - } - }, - decoration: InputDecoration( - border: InputBorder.none, - alignLabelWithHint: true, - floatingLabelBehavior: FloatingLabelBehavior.always, - hintText: 'Add title for the landmark', - hintStyle: - TextStyle(fontSize: hintsize, color: hintColor), - labelText: 'Title', - labelStyle: - TextStyle(fontSize: labelsize, color: kYellow), + builder: (context) => CreateLandmarkDialog( + beaconId: beaconId, + loc: loc, + ), + ); + } +} + +class CreateLandmarkDialog extends StatefulWidget { + final String beaconId; + final LatLng loc; + + const CreateLandmarkDialog({ + Key? key, + required this.beaconId, + required this.loc, + }) : super(key: key); + + @override + State createState() => _CreateLandmarkDialogState(); +} + +class _CreateLandmarkDialogState extends State { + final _landmarkFormKey = GlobalKey(); + final _landMarkeController = TextEditingController(); + String? _selectedIcon; + + // List of available icons + final List _iconOptions = [ + 'images/icons/camp.png', + 'images/icons/wind.png', + 'images/icons/location-marker.png', + 'images/icons/rain.png', + 'images/icons/forest.png', + 'images/icons/destination.png', + ]; + + @override + void dispose() { + _landMarkeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + final isSmallScreen = size.height < 800; + + return Dialog( + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + child: Container( + width: size.width * 0.85, + height: isSmallScreen ? size.height * 0.45 : size.height * 0.4, + padding: const EdgeInsets.all(24), + child: Form( + key: _landmarkFormKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Select an icon', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.grey[600], + ), + ), + + // Icon selection row + Wrap( + spacing: 8, + runSpacing: 8, + children: _iconOptions.map((iconPath) { + final isSelected = _selectedIcon == iconPath; + return GestureDetector( + onTap: () { + setState(() { + _selectedIcon = iconPath; + }); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isSelected + ? Theme.of(context).primaryColor + : Colors.transparent, + width: 2, ), + color: isSelected + ? Theme.of(context) + .primaryColor + .withValues(alpha: 0.1) + : Colors.transparent, + ), + child: Image.asset( + iconPath, + width: 40, + height: 40, ), ), - color: kLightBlue, - ), - SizedBox( - height: 2.h, - ), - Flexible( - child: HikeButton( - text: 'Create Landmark', - textSize: 14.0, - textColor: Colors.white, - buttonColor: kYellow, - onTap: () { - if (!_landmarkFormKey.currentState!.validate()) return; - appRouter.maybePop(); - locator().createLandmark( - beaconId, _landMarkeController.text, loc); - _landMarkeController.clear(); - }, + ); + }).toList(), + ), + + // Text input field + Container( + height: 60, + margin: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: kLightBlue, + borderRadius: BorderRadius.circular(8), + ), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: TextFormField( + controller: _landMarkeController, + style: const TextStyle(fontSize: 18.0), + validator: (value) { + if (value == null || value.isEmpty) { + return "Please enter title for landmark"; + } + if (_selectedIcon == null) { + return "Please select an icon"; + } + return null; + }, + decoration: InputDecoration( + hintText: 'Enter Landmark Title', + labelText: 'Title', + labelStyle: TextStyle( + fontSize: 14, + color: Theme.of(context).primaryColor, + ), + hintStyle: TextStyle( + fontSize: 16, + color: Colors.grey[400], ), + alignLabelWithHint: true, + floatingLabelBehavior: FloatingLabelBehavior.always, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, ), - ], + ), ), - ), + + const SizedBox(height: 16), + + // Create button + HikeButton( + buttonHeight: 5.5.h, + buttonWidth: 50.w, + textSize: 14.0, + textColor: Colors.white, + onTap: () { + if (!_landmarkFormKey.currentState!.validate()) return; + appRouter.maybePop(); + locator().createLandmark( + widget.beaconId, + _landMarkeController.text.trim(), + widget.loc, + _selectedIcon!); + _landMarkeController.clear(); + }, + text: 'Create Landmark', + ) + ], ), ), ), diff --git a/lib/presentation/hike/widgets/search_places.dart b/lib/presentation/hike/widgets/search_places.dart new file mode 100644 index 00000000..2348534c --- /dev/null +++ b/lib/presentation/hike/widgets/search_places.dart @@ -0,0 +1,127 @@ +import 'package:beacon/data/models/landmark/location_suggestion.dart'; +import 'package:beacon/locator.dart'; +import 'package:beacon/presentation/hike/cubit/location_cubit/location_cubit.dart'; +import 'package:beacon/presentation/hike/services/geoapify_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +class LocationSearchWidget extends StatelessWidget { + final String beaconId; + final Function(LocationSuggestion)? onLocationSelected; // Add callback + + LocationSearchWidget( + this.beaconId, { + Key? key, + this.onLocationSelected, + }) : super(key: key); + + final GeoapifyService geoapifyService = GeoapifyService(); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: TypeAheadField( + // Changed from String to LocationSuggestion + suggestionsCallback: (pattern) async { + List res = + await geoapifyService.getLocationSuggestions(pattern); + return res; // Return the LocationSuggestion objects directly + }, + builder: (context, controller, focusNode) { + return Container( + decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(50)), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: TextFormField( + controller: controller, + focusNode: focusNode, + cursorColor: Colors.deepPurpleAccent.withAlpha(120), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 12), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: Colors.red), + ), + hintText: 'Search for a place', + hintStyle: TextStyle(fontSize: 15, color: Colors.grey[600]), + prefixIcon: Icon( + Icons.search, + size: 20, + color: Colors.deepPurpleAccent.withAlpha(120), + ), + suffixIcon: IconButton( + icon: Icon(Icons.clear, color: Colors.grey), + onPressed: () { + controller.clear(); + focusNode.unfocus(); + }, + )), + style: TextStyle( + color: Colors.black, + fontSize: 14, + ), + ), + ); + }, + itemBuilder: (context, suggestion) { + return ListTile( + title: Text( + suggestion.name), // Use suggestion.name instead of suggestion + subtitle: Text( + 'Lat: ${suggestion.latitude.toStringAsFixed(4)}, Lon: ${suggestion.longitude.toStringAsFixed(4)} \n ${suggestion.fullAddress}', + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + leading: Icon(Icons.location_on, color: Colors.blue), + ); + }, + onSelected: (suggestion) { + print("Location selected: ${suggestion.name}"); + print("Coordinates: ${suggestion.latitude}, ${suggestion.longitude}"); + + // Call the callback if provided + if (onLocationSelected != null) { + onLocationSelected!(suggestion); + } + + locator().createLandmark( + beaconId, + suggestion.name, + LatLng(suggestion.latitude, suggestion.longitude), + "images/icons/location-marker.png"); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Selected: ${suggestion.name}\nLat: ${suggestion.latitude}, Lon: ${suggestion.longitude}', + ), + ), + ); + }, + hideOnEmpty: true, + decorationBuilder: (context, child) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.2), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: child, + ); + }, + ), + ); + } +} diff --git a/lib/presentation/home/home_cubit/home_cubit.dart b/lib/presentation/home/home_cubit/home_cubit.dart index 9ec802aa..987652c0 100644 --- a/lib/presentation/home/home_cubit/home_cubit.dart +++ b/lib/presentation/home/home_cubit/home_cubit.dart @@ -368,6 +368,18 @@ class HomeCubit extends Cubit { _currentGroupId = groupId; } + void updateUserImage(String userId, String? imageUrl) async { + if (imageUrl != null && imageUrl.isNotEmpty) { + var dataState = await homeUseCase.updateUserImage(userId, imageUrl); + + if (dataState is DataSuccess && dataState.data == true) { + emit(_loadedhomeState.copyWith(message: 'Profile image updated!')); + } else { + emit(_loadedhomeState.copyWith(message: dataState.error)); + } + } + } + void init() async { var groups = localApi.userModel.groups ?? []; _groupIds = List.generate(groups.length, (index) => groups[index]!.id!); diff --git a/lib/presentation/home/home_cubit/home_state.freezed.dart b/lib/presentation/home/home_cubit/home_state.freezed.dart index 506b8d75..04750ff0 100644 --- a/lib/presentation/home/home_cubit/home_state.freezed.dart +++ b/lib/presentation/home/home_cubit/home_state.freezed.dart @@ -89,6 +89,9 @@ class _$HomeStateCopyWithImpl<$Res, $Val extends HomeState> final $Val _value; // ignore: unused_field final $Res Function($Val) _then; + + /// Create a copy of HomeState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -105,6 +108,9 @@ class __$$InitialHomeStateImplCopyWithImpl<$Res> __$$InitialHomeStateImplCopyWithImpl(_$InitialHomeStateImpl _value, $Res Function(_$InitialHomeStateImpl) _then) : super(_value, _then); + + /// Create a copy of HomeState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -225,6 +231,9 @@ class __$$ShimmerHomeStateImplCopyWithImpl<$Res> __$$ShimmerHomeStateImplCopyWithImpl(_$ShimmerHomeStateImpl _value, $Res Function(_$ShimmerHomeStateImpl) _then) : super(_value, _then); + + /// Create a copy of HomeState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -345,6 +354,9 @@ class __$$LoadingHomeStateImplCopyWithImpl<$Res> __$$LoadingHomeStateImplCopyWithImpl(_$LoadingHomeStateImpl _value, $Res Function(_$LoadingHomeStateImpl) _then) : super(_value, _then); + + /// Create a copy of HomeState + /// with the given fields replaced by the non-null parameter values. } /// @nodoc @@ -472,6 +484,8 @@ class __$$LoadedHomeStateImplCopyWithImpl<$Res> _$LoadedHomeStateImpl _value, $Res Function(_$LoadedHomeStateImpl) _then) : super(_value, _then); + /// Create a copy of HomeState + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -554,7 +568,9 @@ class _$LoadedHomeStateImpl implements LoadedHomeState { isLoadingmore, hasReachedEnd); - @JsonKey(ignore: true) + /// Create a copy of HomeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$LoadedHomeStateImplCopyWith<_$LoadedHomeStateImpl> get copyWith => @@ -653,7 +669,10 @@ abstract class LoadedHomeState implements HomeState { String? get message; bool get isLoadingmore; bool get hasReachedEnd; - @JsonKey(ignore: true) + + /// Create a copy of HomeState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) _$$LoadedHomeStateImplCopyWith<_$LoadedHomeStateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/presentation/home/home_screen.dart b/lib/presentation/home/home_screen.dart index 96e609b9..dff86931 100644 --- a/lib/presentation/home/home_screen.dart +++ b/lib/presentation/home/home_screen.dart @@ -1,9 +1,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:beacon/domain/entities/group/group_entity.dart'; -import 'package:beacon/presentation/auth/auth_cubit/auth_cubit.dart'; import 'package:beacon/presentation/home/home_cubit/home_cubit.dart'; import 'package:beacon/presentation/home/home_cubit/home_state.dart'; import 'package:beacon/presentation/group/widgets/create_join_dialog.dart'; +import 'package:beacon/presentation/home/profile_screen.dart'; +import 'package:beacon/presentation/widgets/screen_template.dart'; import 'package:beacon/presentation/widgets/shimmer.dart'; import 'package:beacon/presentation/home/widgets/group_card.dart'; import 'package:beacon/presentation/widgets/hike_button.dart'; @@ -26,58 +27,19 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - Future _onPopHome(BuildContext context) async { - return showDialog( - context: context, - builder: (context) => AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - // actionsAlignment: MainAxisAlignment.spaceEvenly, - contentPadding: EdgeInsets.all(25.0), - title: Text( - 'Confirm Exit', - style: TextStyle(fontSize: 25, color: kYellow), - ), - content: Text( - 'Do you really want to exit?', - style: TextStyle(fontSize: 18, color: kBlack), - ), - actions: [ - HikeButton( - buttonHeight: 2.5.h, - buttonWidth: 8.w, - onTap: () => AutoRouter.of(context).maybePop(false), - text: 'No', - ), - SizedBox( - height: 5, - ), - HikeButton( - buttonHeight: 2.5.h, - buttonWidth: 8.w, - onTap: () => AutoRouter.of(context).maybePop(true), - text: 'Yes', - ), - ], - ), - ); - } - + int _currentIndex = 0; late ScrollController _scrollController; late HomeCubit _homeCubit; @override void initState() { - _scrollController = ScrollController(); - if (localApi.userModel.isGuest == false) { - locationService.getCurrentLocation(); - _homeCubit = BlocProvider.of(context); - _homeCubit.init(); - _homeCubit.fetchUserGroups(); - _scrollController.addListener(_onScroll); - } super.initState(); + _scrollController = ScrollController(); + locationService.getCurrentLocation(); + _homeCubit = BlocProvider.of(context); + _homeCubit.init(); + _homeCubit.fetchUserGroups(); + _scrollController.addListener(_onScroll); } void _onScroll() { @@ -90,292 +52,280 @@ class _HomeScreenState extends State { @override void dispose() { _scrollController.dispose(); - if (localApi.userModel.isGuest == false) { - _homeCubit.clear(); - } + _homeCubit.clear(); super.dispose(); } + Future _onPopHome(BuildContext context) async { + return showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 5.w, + vertical: 2.h, + ), + title: Text( + 'Confirm Exit', + style: TextStyle( + fontSize: 18.sp, + color: kYellow, + fontWeight: FontWeight.w600, + ), + ), + content: Text( + 'Do you really want to exit?', + style: TextStyle( + fontSize: 16.sp, + color: kBlack, + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + HikeButton( + buttonHeight: 5.h, + buttonWidth: 25.w, + onTap: () => AutoRouter.of(context).maybePop(false), + text: 'No', + isDotted: true, + ), + HikeButton( + buttonHeight: 5.h, + buttonWidth: 25.w, + onTap: () => AutoRouter.of(context).maybePop(true), + text: 'Yes', + ), + ], + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { - final screensize = MediaQuery.of(context).size; - return PopScope( - canPop: false, - onPopInvokedWithResult: (bool didPop, Object? result) async { - if (didPop) { - return; - } - - bool? popped = await _onPopHome(context); - if (popped == true) { - await SystemNavigator.pop(); + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) async { + if (didPop) return; + bool? popped = await _onPopHome(context); + if (popped == true) await SystemNavigator.pop(); + }, + child: BlocConsumer( + listener: (context, state) { + if (state is LoadedHomeState && state.message != null) { + utils.showSnackBar(state.message!, context); } }, - child: BlocConsumer( - listener: (context, state) { - if (state is LoadedHomeState) { - state.message != null - ? utils.showSnackBar(state.message!, context) - : null; - } - }, - builder: (context, state) { - return Scaffold( - resizeToAvoidBottomInset: false, - body: SafeArea( - child: ModalProgressHUD( - inAsyncCall: state is LoadingHomeState ? true : false, - progressIndicator: LoadingScreen(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only( - left: screensize.width * 0.04, - right: screensize.width * 0.04, - top: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Image.asset( - 'images/beacon_logo.png', - height: 28, - ), - IconButton( - icon: const Icon(Icons.power_settings_new, - color: Colors.grey), - onPressed: () => showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: Color(0xffFAFAFA), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12.0), - ), - title: Text('Logout', - style: Style.heading), - content: Text( - 'Are you sure you want to logout?', - style: TextStyle( - fontSize: 16, color: kBlack), - ), - actions: [ - HikeButton( - buttonWidth: 80, - buttonHeight: 40, - isDotted: true, - onTap: () => - AutoRouter.of(context) - .maybePop(false), - text: 'No', - textSize: 18.0, - ), - SizedBox( - height: 5, - ), - HikeButton( - buttonWidth: 80, - buttonHeight: 40, - onTap: () async { - appRouter - .replaceNamed('/auth'); - localApi.deleteUser(); - context - .read() - .googleSignOut(); - }, - text: 'Yes', - textSize: 18.0, - ), - ], - ))), - ], - ), - - // welcome message - const SizedBox(height: 20), - - // Welcome message - Row( - children: [ - Text( - 'Welcome back, ', - style: Style.subHeading - .copyWith(fontWeight: FontWeight.w600), - ), - Text( - localApi.userModel.name - .toString() - .toUpperCase()[0] + - localApi.userModel.name - .toString() - .substring(1), - style: Style.heading - .copyWith(color: Colors.teal)), - ], - ), - - // Ready to explore - Text( - 'Ready to explore?', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Color(0xFF673AB7), - ), - textAlign: TextAlign.start, - ), - - SizedBox(height: 2.h), - - // Create and Join Group - - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - HikeButton( - widget: Icon( - Icons.add, - color: Colors.black, - size: 18, - ), - buttonWidth: screensize.width * 0.44, - buttonHeight: 45, - text: 'Create Group', - onTap: () async { - CreateJoinGroupDialog.createGroupDialog( - context); - }, - ), - SizedBox( - width: 1.w, - ), - HikeButton( - widget: Icon( - Icons.add, - color: Colors.teal, - size: 18, - ), - isDotted: true, - buttonWidth: screensize.width * 0.44, - buttonHeight: 45, - text: 'Join a Group', - onTap: () async { - CreateJoinGroupDialog.joinGroupDialog( - context); - }, - ), - ], - ), - SizedBox(height: 4.h), - Text( - 'Your Groups', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.start, - ), - ], - ), - ), - - // Your Groups - _buildList() - ], - ), - )), - ); - }, - )); - } - - Widget _buildList() { - return Expanded( - child: BlocBuilder( - buildWhen: (previous, current) { - return true; - }, builder: (context, state) { - if (state is ShimmerHomeState) { - return Center( - child: ShimmerWidget.getPlaceholder(), - ); - } else if (state is LoadedHomeState) { - List groups = state.groups; - if (groups.isEmpty) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), + return Scaffold( + resizeToAvoidBottomInset: false, + body: BeaconScreenTemplate( + body: ModalProgressHUD( + inAsyncCall: state is LoadingHomeState, + progressIndicator: const LoadingScreen(), child: Padding( - padding: const EdgeInsets.only(top: 30), + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Lottie.asset('animations/empty.json', - width: 200, height: 200), - const SizedBox(height: 20), - Text( - 'You haven\'t joined or created any group yet', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.black, fontSize: 14), - ), - SizedBox( - height: 20, - ), - RichText( - text: TextSpan( - style: TextStyle(color: Colors.black, fontSize: 20), - children: [ - TextSpan( - text: 'Join', - style: TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: ' a Group or '), - TextSpan( - text: 'Create', - style: TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: ' a new one!'), - ], - ), + SizedBox(height: 2.h), + Expanded( + child: _currentIndex == 0 + ? _buildHomePage() + : _currentIndex == 1 + ? ProfileScreen() + : _buildSettingsPage(), ), ], ), ), - ); - } else { - return ListView.builder( - shrinkWrap: true, - controller: _scrollController, - physics: AlwaysScrollableScrollPhysics(), - scrollDirection: Axis.vertical, - itemCount: groups.length + - (state.isLoadingmore && !state.hasReachedEnd ? 1 : 0), - padding: EdgeInsets.all(8), - itemBuilder: (context, index) { - if (index == groups.length) { - return Center(child: LinearProgressIndicator()); - } else { - return GroupCard( - group: groups[index], - ); - // return GroupCustomWidgets.getGroupCard( - // context, groups[index]); - } - }, - ); - } + ), + ), + ); + }, + ), + ); + } + + Widget _buildHomePage() { + return Column( + children: [ + _buildHeader(), + SizedBox(height: 2.h), + Expanded(child: _buildGroupList()), + ], + ); + } + + Widget _buildHeader() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Welcome back, ', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + Flexible( + child: Text( + _getCapitalizedName(), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.teal, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + SizedBox(height: 1.h), + Text( + 'Ready to explore?', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: Color(0xFF673AB7), + ), + ), + SizedBox(height: 3.h), + _buildActionButtons(), + SizedBox(height: 3.h), + Text( + 'Your Groups', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } + + Widget _buildActionButtons() { + return Row( + children: [ + Expanded( + child: HikeButton( + widget: Icon(Icons.add, color: Colors.white, size: 20.sp), + buttonHeight: 6.h, + buttonWidth: double.infinity, + text: 'Create Group', + textSize: 14.sp, + onTap: () => CreateJoinGroupDialog.createGroupDialog(context), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: HikeButton( + widget: Icon(Icons.add, color: Colors.teal, size: 20.sp), + isDotted: true, + buttonHeight: 6.h, + buttonWidth: double.infinity, + text: 'Join a Group', + textSize: 14.sp, + onTap: () => CreateJoinGroupDialog.joinGroupDialog(context), + ), + ), + ], + ); + } + + Widget _buildGroupList() { + return BlocBuilder( + builder: (context, state) { + if (state is ShimmerHomeState) { + return Center(child: ShimmerWidget.getPlaceholder()); + } else if (state is LoadedHomeState) { + if (state.groups.isEmpty) { + return _buildEmptyState(); } + return _buildGroupsList(state.groups, state); + } + return const SizedBox(); + }, + ); + } + + Widget _buildEmptyState() { + return SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 4.h), + child: Column( + children: [ + Lottie.asset('animations/empty.json', width: 50.w, height: 25.h), + SizedBox(height: 3.h), + Text( + 'You haven\'t joined or created any group yet', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14.sp), + ), + SizedBox(height: 3.h), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle(fontSize: 16.sp, color: Colors.black), + children: [ + TextSpan( + text: 'Join', + style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: ' a Group or '), + TextSpan( + text: 'Create', + style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: ' a new one!'), + ], + ), + ), + SizedBox(height: 3.h), + IconButton( + icon: Icon(Icons.refresh, size: 20.sp, color: Colors.teal), + onPressed: () => _homeCubit.fetchUserGroups(), + ) + ], + ), + ), + ); + } - return Center( - child: Text(''), + Widget _buildGroupsList(List groups, LoadedHomeState state) { + return ListView.builder( + controller: _scrollController, + padding: EdgeInsets.only(top: 1.h), + itemCount: + groups.length + (state.isLoadingmore && !state.hasReachedEnd ? 1 : 0), + itemBuilder: (context, index) { + if (index == groups.length) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 2.h), + child: const Center(child: LinearProgressIndicator()), ); - }, + } + return GroupCard(group: groups[index]); + }, + ); + } + + Widget _buildSettingsPage() { + return Center( + child: Text( + 'Settings Page', + style: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.bold), ), ); } + + String _getCapitalizedName() { + final name = localApi.userModel.name.toString(); + if (name.isEmpty) return ''; + return name[0].toUpperCase() + name.substring(1); + } } diff --git a/lib/presentation/home/profile_screen.dart b/lib/presentation/home/profile_screen.dart new file mode 100644 index 00000000..a2a47559 --- /dev/null +++ b/lib/presentation/home/profile_screen.dart @@ -0,0 +1,446 @@ +import 'package:auto_route/annotations.dart'; +import 'package:beacon/locator.dart'; +import 'package:beacon/presentation/home/home_cubit/home_cubit.dart'; +import 'package:beacon/presentation/widgets/hike_button.dart'; +import 'package:beacon/presentation/widgets/screen_template.dart'; +import 'package:flutter/material.dart'; +import 'package:lottie/lottie.dart'; +import 'package:responsive_sizer/responsive_sizer.dart'; + +@RoutePage() +class ProfileScreen extends StatefulWidget { + const ProfileScreen({super.key}); + + @override + State createState() => _ProfileScreenState(); +} + +class _ProfileScreenState extends State { + bool showSelectImage = false; + int selectedImageIndex = -1; + int selectedBadgeCategory = 0; + + final List badgeCategories = [ + 'All', + 'Exploration', + 'Social', + 'Achievements', + 'Milestones' + ]; + + final List imageOptions = [ + "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_2.png", + "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_3.png", + "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_5.png", + "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_35.png", + "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_34.png", + "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_8.png", + "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_10.png", + "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_16.png", + "https://cdn.jsdelivr.net/gh/alohe/avatars/png/memo_24.png", + ]; + + @override + void initState() { + super.initState(); + selectedImageIndex = + imageOptions.indexOf(localApi.userModel.imageUrl ?? ''); + } + + @override + Widget build(BuildContext context) { + return BeaconScreenTemplate( + showLogout: true, + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h), + child: Column( + children: [ + if (showSelectImage) _buildImageSelectionGrid(), + _buildProfilePicture(), + SizedBox(height: 3.h), + if (!showSelectImage) ...[ + _buildProfileInfoCard(), + SizedBox(height: 3.h), + _buildGamificationSection(), + ], + if (showSelectImage) _buildActionButtons(), + ], + ), + ), + ), + ); + } + + Widget _buildImageSelectionGrid() { + return Container( + height: 45.h, + margin: EdgeInsets.only(bottom: 2.h), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12), + ), + child: LayoutBuilder( + builder: (context, constraints) { + int columns = (constraints.maxWidth / 100).floor(); + return GridView.builder( + padding: EdgeInsets.all(2.w), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + crossAxisSpacing: 2.w, + mainAxisSpacing: 2.w, + childAspectRatio: 1, + ), + itemCount: imageOptions.length, + itemBuilder: (context, index) { + return GestureDetector( + onTap: () => setState(() => selectedImageIndex = index), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all( + color: selectedImageIndex == index + ? Colors.deepPurple + : Colors.transparent, + width: 2, + ), + color: Colors.grey[200], + image: DecorationImage( + image: NetworkImage(imageOptions[index]), + fit: BoxFit.cover, + ), + ), + ), + ); + }, + ); + }, + ), + ); + } + + Widget _buildProfilePicture() { + return Stack( + alignment: Alignment.center, + children: [ + Container( + width: 30.w, + height: 30.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.deepPurple, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + child: ClipOval( + child: Image.network( + selectedImageIndex != -1 + ? imageOptions[selectedImageIndex] + : localApi.userModel.imageUrl!, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Icon( + Icons.person, + size: 20.w, + color: Colors.grey[400], + ), + ), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: GestureDetector( + onTap: () => setState(() => showSelectImage = !showSelectImage), + child: Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.deepPurple, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 6, + spreadRadius: 1, + ), + ], + ), + child: Icon( + showSelectImage ? Icons.close : Icons.camera_alt, + color: Colors.white, + size: 4.w, + ), + ), + ), + ), + ], + ); + } + + Widget _buildProfileInfoCard() { + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: EdgeInsets.all(4.w), + child: Column( + children: [ + ListTile( + leading: Icon(Icons.person, color: Colors.deepPurple), + title: Text( + 'Name', + style: TextStyle(fontSize: 12.sp, color: Colors.grey), + ), + subtitle: Text( + localApi.userModel.name ?? 'Not provided', + style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500), + ), + ), + Divider(height: 1, color: Colors.grey[200]), + ListTile( + leading: Icon(Icons.email, color: Colors.deepPurple), + title: Text( + 'Email', + style: TextStyle(fontSize: 12.sp, color: Colors.grey), + ), + subtitle: Text( + localApi.userModel.email ?? 'Not provided', + style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500), + ), + ), + ], + ), + ), + ); + } + + Widget _buildGamificationSection() { + return Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Your Adventure Progress', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Color(0xFF673AB7), + ), + ), + SizedBox(height: 2.h), + _buildProgressBar(), + SizedBox(height: 3.h), + Text( + 'Earned Badges', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 2.h), + _buildBadgesGrid(), + ], + ), + ); + } + + Widget _buildProgressBar() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Explorer Level 2', + style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600), + ), + Text( + '65%', + style: TextStyle(fontSize: 12.sp, color: Colors.grey), + ), + ], + ), + SizedBox(height: 1.h), + LinearProgressIndicator( + value: 0.65, + backgroundColor: Colors.grey[200], + valueColor: AlwaysStoppedAnimation(Colors.deepPurple), + minHeight: 1.5.h, + borderRadius: BorderRadius.circular(10), + ), + SizedBox(height: 1.h), + Text( + 'Complete 3 more hikes to reach next level', + style: TextStyle(fontSize: 12.sp, color: Colors.grey), + ), + ], + ); + } + + Widget _buildBadgesGrid() { + final achievements = [ + {'title': 'Trailblazer', 'earned': true, 'icon': Icons.flag}, + {'title': 'Nature Lover', 'earned': true, 'icon': Icons.forest}, + {'title': 'Pathfinder', 'earned': false, 'icon': Icons.directions}, + {'title': 'Marathoner', 'earned': false, 'icon': Icons.timer}, + {'title': 'Social Hiker', 'earned': true, 'icon': Icons.group}, + {'title': 'Peak Conqueror', 'earned': false, 'icon': Icons.star}, + ]; + + if (achievements.isEmpty) { + return _buildEmptyBadgesState(); + } + + return GridView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 4.w, + mainAxisSpacing: 4.w, + childAspectRatio: 0.9, + ), + itemCount: achievements.length, + itemBuilder: (context, index) { + final achievement = achievements[index]; + return Column( + children: [ + Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: achievement['earned'] as bool + ? Colors.deepPurple + : Colors.grey[300], + boxShadow: [ + BoxShadow( + color: (achievement['earned'] as bool + ? Colors.deepPurple + : Colors.grey[300]!) + .withValues(alpha: 0.3), + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + child: Center( + child: Icon( + achievement['icon'] as IconData, + size: 10.w, + color: Colors.white, + ), + ), + ), + SizedBox(height: 1.h), + Text( + achievement['title'] as String, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12.sp), + maxLines: 2, + ), + ], + ); + }, + ); + } + + Widget _buildEmptyBadgesState() { + return Column( + children: [ + Lottie.asset( + 'animations/empty_badges.json', + width: 50.w, + height: 20.h, + ), + SizedBox(height: 2.h), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle(fontSize: 16.sp, color: Colors.black), + children: [ + TextSpan(text: 'No achievements yet. '), + TextSpan( + text: 'Start exploring and engaging to earn badges!', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Color(0xFF673AB7), + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildActionButtons() { + return Column( + children: [ + SizedBox(height: 3.h), + HikeButton( + buttonWidth: double.infinity, + buttonHeight: 6.h, + text: 'Update Profile Image', + textSize: 14.sp, + onTap: _updateProfileImage, + ), + SizedBox(height: 1.h), + HikeButton( + buttonWidth: double.infinity, + buttonHeight: 6.h, + text: 'Cancel', + textSize: 14.sp, + isDotted: true, + onTap: () => setState(() => showSelectImage = false), + ), + ], + ); + } + + void _updateProfileImage() { + if (selectedImageIndex == -1) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please select an image'), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + return; + } + + HomeCubit homeCubit = locator(); + localApi.userModel.copyWithModel( + imageUrl: imageOptions[selectedImageIndex], + ); + homeCubit.updateUserImage( + localApi.userModel.id!, + imageOptions[selectedImageIndex], + ); + setState(() => showSelectImage = false); + } +} diff --git a/lib/presentation/home/widgets/group_card.dart b/lib/presentation/home/widgets/group_card.dart index 244e0c46..295ec832 100644 --- a/lib/presentation/home/widgets/group_card.dart +++ b/lib/presentation/home/widgets/group_card.dart @@ -8,62 +8,37 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:responsive_sizer/responsive_sizer.dart'; class GroupCard extends StatelessWidget { final GroupEntity group; - GroupCard({super.key, required this.group}); + const GroupCard({super.key, required this.group}); @override Widget build(BuildContext context) { - String noMembers = group.members!.length.toString(); - String noBeacons = group.beacons!.length.toString(); + final memberCount = group.members?.length ?? 0; + final beaconCount = group.beacons?.length ?? 0; + final isMember = + group.members?.any((m) => m?.id == localApi.userModel.id) ?? false; + final isLeader = group.leader?.id == localApi.userModel.id; return GestureDetector( - onTap: () async { - bool isMember = false; - for (var member in group.members!) { - if (member!.id == localApi.userModel.id) { - isMember = true; - } - } - if (group.leader!.id == localApi.userModel.id || isMember) { - var homeCubit = locator(); - homeCubit.updateCurrentGroupId(group.id!); - appRouter.push(GroupScreenRoute(group: group)).then((value) { - homeCubit.resetGroupActivity(groupId: group.id!); - homeCubit.updateCurrentGroupId(null); - }); - } else { - HomeUseCase _homeUseCase = locator(); - DataState state = - await _homeUseCase.joinGroup(group.shortcode!); - if (state is DataSuccess && state.data != null) { - var homeCubit = locator(); - homeCubit.updateCurrentGroupId(group.id!); - appRouter.push(GroupScreenRoute(group: state.data!)).then((value) { - homeCubit.resetGroupActivity(groupId: group.id); - homeCubit.updateCurrentGroupId(null); - }); - } - } - }, + onTap: () => _handleGroupTap(context, isLeader || isMember), child: Container( - margin: const EdgeInsets.only(left: 10, right: 10, bottom: 14), - //padding: const EdgeInsets.all(12), + margin: EdgeInsets.symmetric(horizontal: 2.w, vertical: 1.h), decoration: BoxDecoration( - color: Colors.grey[100], + color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(12), ), child: Slidable( - key: ValueKey(group.id!.toString()), + key: ValueKey(group.id), endActionPane: ActionPane( - motion: ScrollMotion(), + motion: const ScrollMotion(), children: [ SlidableAction( - padding: EdgeInsets.symmetric(horizontal: 0), - onPressed: (context) { - context.read().changeShortCode(group); - }, + padding: EdgeInsets.zero, + onPressed: (context) => + context.read().changeShortCode(group), backgroundColor: Colors.teal, foregroundColor: Colors.white, icon: Icons.code, @@ -73,102 +48,17 @@ class GroupCard extends StatelessWidget { ], ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 2.h), child: Column( - mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - const Icon(Icons.groups_2_rounded, color: Colors.grey), - const SizedBox(width: 8), - Text( - '${group.title.toString().toUpperCase()} by ${group.leader!.name} ', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - // Stack of profile circles - // noMembers != "0" - // ? SizedBox( - // width: 30 * group.members!.length.toDouble(), - // // 30 is the width of each profile circle - // height: 30, - // child: Stack( - // children: - // (group.members != null && group.members!.length > 3 - // ? group.members!.sublist(0, 3) - // : group.members ?? []) - // .map((member) { - // if (member != null) { - // return Positioned( - // left: group.members!.indexOf(member) * 20.0, - // child: _buildProfileCircle( - // member.id == localApi.userModel.id - // ? Colors.teal - // : Colors.grey, - // ), - // ); - // } else { - // return const SizedBox.shrink(); - // } - // }).toList(), - // ), - // ) - // : Container(), - Text( - 'Group has $noMembers members ', - style: TextStyle( - color: Colors.grey, - fontSize: 14, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Group has $noBeacons ${noBeacons == '1' ? 'beacon' : 'beacons'} ', - style: TextStyle( - color: Colors.black87, - fontSize: 14, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - 'Passkey: ${group.shortcode}', - style: TextStyle( - color: Colors.black87, - fontSize: 14, - ), - ), - const SizedBox(width: 8), - InkWell( - onTap: () { - Clipboard.setData( - ClipboardData(text: group.shortcode!)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Shortcode copied!'), - duration: Duration(seconds: 2), - ), - ); - }, - child: Icon( - Icons.copy, - size: 17, - color: Colors.grey, - ), - ) - ], - ), + _buildGroupHeader(context), + SizedBox(height: 1.h), + _buildMemberInfo(context, memberCount), + SizedBox(height: 0.5.h), + _buildBeaconInfo(context, beaconCount), + SizedBox(height: 0.5.h), + _buildShortcodeRow(context), ], ), ), @@ -176,4 +66,106 @@ class GroupCard extends StatelessWidget { ), ); } + + Widget _buildGroupHeader(BuildContext context) { + return Row( + children: [ + Icon(Icons.groups_rounded, color: Colors.grey, size: 20.sp), + SizedBox(width: 2.w), + Expanded( + child: Text( + '${group.title?.toUpperCase() ?? ''} by ${group.leader?.name ?? ''}', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16.sp, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } + + Widget _buildMemberInfo(BuildContext context, int count) { + return Row( + children: [ + Text( + 'Group has $count ${count == 1 ? 'member' : 'members'}', + style: TextStyle( + color: Colors.grey, + fontSize: 14.sp, + ), + ), + ], + ); + } + + Widget _buildBeaconInfo(BuildContext context, int count) { + return Text( + 'Group has $count ${count == 1 ? 'beacon' : 'beacons'}', + style: TextStyle( + color: Colors.black87, + fontSize: 14.sp, + ), + ); + } + + Widget _buildShortcodeRow(BuildContext context) { + return Row( + children: [ + Text( + 'Passkey: ${group.shortcode ?? ''}', + style: TextStyle( + color: Colors.black87, + fontSize: 14.sp, + ), + ), + SizedBox(width: 2.w), + GestureDetector( + onTap: () => _copyShortcode(context), + child: Icon( + Icons.copy, + size: 16.sp, + color: Colors.grey, + ), + ), + ], + ); + } + + Future _handleGroupTap(BuildContext context, bool hasAccess) async { + if (hasAccess) { + await _navigateToGroupScreen(context, group); + } else { + await _joinAndNavigateToGroup(context); + } + } + + Future _navigateToGroupScreen( + BuildContext context, GroupEntity group) async { + final homeCubit = locator(); + homeCubit.updateCurrentGroupId(group.id!); + await appRouter.push(GroupScreenRoute(group: group)); + homeCubit.resetGroupActivity(groupId: group.id!); + homeCubit.updateCurrentGroupId(null); + } + + Future _joinAndNavigateToGroup(BuildContext context) async { + final homeUseCase = locator(); + final state = await homeUseCase.joinGroup(group.shortcode!); + + if (state is DataSuccess && state.data != null) { + await _navigateToGroupScreen(context, state.data!); + } + } + + void _copyShortcode(BuildContext context) { + Clipboard.setData(ClipboardData(text: group.shortcode!)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Shortcode copied!'), + duration: Duration(seconds: 2), + ), + ); + } } diff --git a/lib/presentation/splash/splash_screen.dart b/lib/presentation/splash/splash_screen.dart index ad8aa205..87f4b209 100644 --- a/lib/presentation/splash/splash_screen.dart +++ b/lib/presentation/splash/splash_screen.dart @@ -1,16 +1,10 @@ -import 'dart:async'; - import 'package:auto_route/auto_route.dart'; import 'package:beacon/config/router/router.dart'; -import 'package:beacon/core/resources/data_state.dart'; import 'package:beacon/domain/entities/user/user_entity.dart'; import 'package:beacon/domain/usecase/auth_usecase.dart'; -import 'package:beacon/domain/usecase/group_usecase.dart'; import 'package:beacon/locator.dart'; import 'package:beacon/presentation/auth/verification_cubit/verification_cubit.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:uni_links/uni_links.dart'; import '../widgets/loading_screen.dart'; @@ -23,83 +17,40 @@ class SplashScreen extends StatefulWidget { } class _SplashScreenState extends State { - bool isCheckingUrl = false; - @override void initState() { - handleLinks(); super.initState(); + _initApp(); } - StreamSubscription? _sub; - Uri? _latestUri; - Uri? _initialUri; - - handleLinks() async { - _sub = uriLinkStream.listen((Uri? uri) { - if (!mounted) return; - setState(() { - _latestUri = uri; - }); - }, onError: (Object err) { - if (!mounted) return; - setState(() { - _latestUri = null; - }); - }); - - try { - final uri = await getInitialUri(); - if (!mounted) return; - setState(() => _initialUri = uri); - } on PlatformException { - if (!mounted) return; - setState(() => _initialUri = null); - } on FormatException catch (err) { - debugPrint(err.toString()); - if (!mounted) return; - setState(() => _initialUri = null); - } - + Future _initApp() async { await sp.init(); await localApi.init(); final authUseCase = locator(); - await localApi.userloggedIn().then((value) async { - if (_latestUri == null && _initialUri == null) { - bool isConnected = await utils.checkInternetConnectivity(); - if (isConnected) { - final userInfo = await authUseCase.getUserInfoUseCase(); - if (userInfo.data != null) { - await func(userInfo.data!); - } else { - appRouter.replaceNamed('/auth'); - } + await localApi.userloggedIn().then((loggedIn) async { + if (!loggedIn!) { + appRouter.replaceNamed('/auth'); + return; + } + + bool isConnected = await utils.checkInternetConnectivity(); + if (isConnected) { + final userInfo = await authUseCase.getUserInfoUseCase(); + if (userInfo.data != null) { + await _handleUser(userInfo.data!); } else { - appRouter.replaceNamed('/home'); - utils.showSnackBar( - 'Please connect to your internet connection!', context); + appRouter.replaceNamed('/auth'); } } else { - if (_initialUri != null) { - var shortcode = _initialUri!.queryParameters['shortcode']; - if (value == true && shortcode != null) { - await locator().joinHike(shortcode).then((dataState) { - if (dataState is DataSuccess) { - appRouter.push(HikeScreenRoute( - beacon: dataState.data!, - isLeader: dataState.data!.id == localApi.userModel.id)); - } else { - appRouter.push(HomeScreenRoute()); - } - }); - } - } + appRouter.replaceNamed('/home'); + utils.showSnackBar( + 'Please connect to your internet connection!', context); } }); } - Future func(UserEntity user) async { + Future _handleUser(UserEntity user) async { var time = await sp.loadData('time'); var otp = await sp.loadData('otp'); if (user.isVerified == true) { @@ -125,12 +76,6 @@ class _SplashScreenState extends State { } } - @override - void dispose() { - _sub?.cancel(); - super.dispose(); - } - @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/presentation/widgets/hike_button.dart b/lib/presentation/widgets/hike_button.dart index a9787656..65ab8dfe 100644 --- a/lib/presentation/widgets/hike_button.dart +++ b/lib/presentation/widgets/hike_button.dart @@ -1,4 +1,3 @@ -import 'package:beacon/core/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:dotted_border/dotted_border.dart'; @@ -17,7 +16,7 @@ class HikeButton extends StatelessWidget { HikeButton( {this.onTap, this.borderColor = Colors.white, - this.buttonColor = kYellow, + this.buttonColor = Colors.teal, this.text, this.textColor = Colors.white, this.buttonWidth = 100, @@ -50,7 +49,7 @@ class HikeButton extends StatelessWidget { ? Colors.transparent : isDisabled! ? Colors.grey - : Colors.teal, + : buttonColor, borderRadius: BorderRadius.circular(12), ), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -65,7 +64,7 @@ class HikeButton extends StatelessWidget { ? Colors.teal : isDisabled! ? Colors.white - : Colors.black, + : textColor, ), ), ]), diff --git a/lib/presentation/widgets/label_marker.dart b/lib/presentation/widgets/label_marker.dart index 8d471e0f..476df74f 100644 --- a/lib/presentation/widgets/label_marker.dart +++ b/lib/presentation/widgets/label_marker.dart @@ -42,7 +42,7 @@ extension AddExtension on Set { infoWindow: labelMarker.infoWindow, rotation: labelMarker.rotation, visible: labelMarker.visible, - zIndex: labelMarker.zIndex, + zIndexInt: labelMarker.zIndex.toInt(), onTap: labelMarker.onTap, onDragStart: labelMarker.onDragStart, onDrag: labelMarker.onDrag, diff --git a/lib/presentation/widgets/screen_template.dart b/lib/presentation/widgets/screen_template.dart new file mode 100644 index 00000000..e6f78f15 --- /dev/null +++ b/lib/presentation/widgets/screen_template.dart @@ -0,0 +1,138 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:beacon/config/router/router.dart'; +import 'package:beacon/locator.dart'; +import 'package:beacon/presentation/auth/auth_cubit/auth_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:responsive_sizer/responsive_sizer.dart'; + +import 'hike_button.dart'; + +class BeaconScreenTemplate extends StatelessWidget { + final Widget body; + final bool showAppBar; + final bool showLogout; + final bool showBottomNav; + + const BeaconScreenTemplate({ + super.key, + required this.body, + this.showAppBar = true, + this.showBottomNav = false, + this.showLogout = false, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: SafeArea( + child: Column( + children: [ + if (showAppBar) _buildAppBar(context), + Expanded(child: body), + ], + ), + ), + bottomNavigationBar: showBottomNav ? _buildBottomNavigationBar() : null, + ); + } + + Widget _buildAppBar(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon(Icons.arrow_back, color: Colors.grey, size: 20.sp), + onPressed: () => AutoRouter.of(context).maybePop(), + ), + Image.asset('images/beacon_logo.png', height: 4.h), + showLogout + ? IconButton( + icon: Icon(Icons.power_settings_new, + color: Colors.grey, size: 20.sp), + onPressed: () => _showLogoutDialog(context), + ) + : // profile icon + IconButton( + icon: SizedBox( + width: 34, + height: 34, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + border: Border.all(color: Colors.purple, width: 1), + color: Colors.grey.shade200, + image: DecorationImage( + image: NetworkImage(localApi.userModel.imageUrl!), + fit: BoxFit.cover, + ), + )), + ), + onPressed: () { + AutoRouter.of(context).push(ProfileScreenRoute()); + }, + ), + ], + ), + ); + } + + void _showLogoutDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.h), + title: Text( + 'Logout', + style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.w600), + ), + content: Text( + 'Are you sure you want to logout?', + style: TextStyle(fontSize: 14.sp), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + HikeButton( + buttonWidth: 25.w, + buttonHeight: 5.h, + isDotted: true, + onTap: () => AutoRouter.of(context).maybePop(false), + text: 'No', + textSize: 14.sp, + ), + HikeButton( + buttonWidth: 25.w, + buttonHeight: 5.h, + onTap: () { + appRouter.replaceNamed('/auth'); + localApi.deleteUser(); + context.read().googleSignOut(); + }, + text: 'Yes', + textSize: 14.sp, + ), + ], + ), + ], + ), + ); + } + + Widget _buildBottomNavigationBar() { + return BottomNavigationBar( + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), + BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), + BottomNavigationBarItem(icon: Icon(Icons.hiking), label: 'Hike'), + ], + ); + } +} diff --git a/lib/presentation/widgets/shimmer.dart b/lib/presentation/widgets/shimmer.dart index f50c7172..f3c7d962 100644 --- a/lib/presentation/widgets/shimmer.dart +++ b/lib/presentation/widgets/shimmer.dart @@ -1,95 +1,71 @@ -import 'package:beacon/core/utils/constants.dart'; import 'package:flutter/material.dart'; -import 'package:skeleton_text/skeleton_text.dart'; +import 'package:shimmer/shimmer.dart'; class ShimmerWidget { static ListView getPlaceholder() { - final BorderRadius borderRadius = BorderRadius.circular(10.0); return ListView.builder( - scrollDirection: Axis.vertical, - physics: BouncingScrollPhysics(), - itemCount: 3, - padding: const EdgeInsets.all(8.0), - itemBuilder: (BuildContext context, int index) { - return Container( - margin: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 10.0, - ), - height: 110, - decoration: BoxDecoration( - color: kBlue, - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(8.0), - boxShadow: [ - BoxShadow( - color: Colors.black26, - blurRadius: 10.0, - offset: Offset(0.0, 10.0), - ), - ], - ), - padding: - EdgeInsets.only(left: 16.0, right: 16.0, bottom: 10, top: 10), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 15.0, bottom: 10.0, right: 15.0), - child: ClipRRect( - borderRadius: borderRadius, - child: SkeletonAnimation( - child: Container( - height: 15.0, - decoration: BoxDecoration(color: shimmerSkeletonColor), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 15.0, right: 30.0, bottom: 10.0), - child: ClipRRect( - borderRadius: borderRadius, - child: SkeletonAnimation( - child: Container( - height: 10.0, - decoration: BoxDecoration(color: shimmerSkeletonColor), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 15.0, right: 45.0, bottom: 10.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: SkeletonAnimation( - child: Container( - height: 10.0, - decoration: BoxDecoration(color: shimmerSkeletonColor), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 15.0, right: 60.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(10.0), - child: SkeletonAnimation( + physics: const BouncingScrollPhysics(), + itemCount: 3, + padding: const EdgeInsets.all(8.0), + itemBuilder: (context, index) { + return _ShimmerCard(); + }, + ); + } +} + +class _ShimmerCard extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), + elevation: 5, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), + child: Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + height: 110, + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + 4, + (index) => Padding( + padding: EdgeInsets.only(bottom: index == 3 ? 0 : 8.0), child: Container( - height: 10.0, - decoration: BoxDecoration(color: shimmerSkeletonColor), + height: 10, + width: _getLineWidth(index, context), + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(4.0), + ), ), ), ), ), - ], - ), - ); - }); + ) + ], + ), + ), + ), + ); + } + + double _getLineWidth(int index, BuildContext context) { + switch (index) { + case 0: + return MediaQuery.of(context).size.width * 0.5; + case 1: + return MediaQuery.of(context).size.width * 0.4; + case 2: + return MediaQuery.of(context).size.width * 0.3; + default: + return MediaQuery.of(context).size.width * 0.2; + } } } diff --git a/pubspec.lock b/pubspec.lock index 8c17cedb..f6c011e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: @@ -54,6 +54,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.3.0" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: a21d7a936c917488653c972f62d884d8adcf8c5d37acc7cd24da33cf784546c0 + url: "https://pub.dev" + source: hosted + version: "8.1.0" bloc: dependency: "direct main" description: @@ -90,10 +98,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.4" build_resolvers: dependency: transitive description: @@ -130,10 +138,10 @@ packages: dependency: transitive description: name: built_value - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62" url: "https://pub.dev" source: hosted - version: "8.9.3" + version: "8.11.0" change_app_package_name: dependency: "direct main" description: @@ -158,6 +166,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" cli_util: dependency: transitive description: @@ -194,10 +210,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: b74247fad72c171381dbe700ca17da24deac637ab6d43c343b42867acb95c991 + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "5.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -218,10 +234,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.15.0" cross_file: dependency: transitive description: @@ -354,10 +370,10 @@ packages: dependency: transitive description: name: firebase_core_platform_interface - sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.2" firebase_core_web: dependency: transitive description: @@ -395,14 +411,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" - flutter_config: - dependency: "direct main" - description: - name: flutter_config - sha256: a07e6156bb6e776e29c6357be433155acda87d1dab1a3f787a72091a1b71ffbf - url: "https://pub.dev" - source: hosted - version: "2.0.2" flutter_countdown_timer: dependency: "direct main" description: @@ -411,6 +419,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + url: "https://pub.dev" + source: hosted + version: "5.2.1" flutter_geocoder_alternative: dependency: "direct main" description: @@ -423,10 +439,58 @@ packages: dependency: transitive description: name: flutter_hooks - sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 url: "https://pub.dev" source: hosted - version: "0.18.6" + version: "0.20.5" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -439,34 +503,34 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 url: "https://pub.dev" source: hosted - version: "17.2.4" + version: "18.0.1" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "5.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" url: "https://pub.dev" source: hosted - version: "7.2.0" + version: "8.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.28" flutter_polyline_points: dependency: "direct main" description: @@ -497,6 +561,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: "direct main" + description: + name: flutter_typeahead + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d + url: "https://pub.dev" + source: hosted + version: "5.2.0" flutter_web_plugins: dependency: transitive description: flutter @@ -562,18 +634,18 @@ packages: dependency: transitive description: name: geolocator_apple - sha256: c4ecead17985ede9634f21500072edfcb3dba0ef7b97f8d7bc556d2d722b3ba3 + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.3.13" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "4.2.6" geolocator_web: dependency: transitive description: @@ -586,10 +658,10 @@ packages: dependency: transitive description: name: geolocator_windows - sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.5" get_it: dependency: "direct main" description: @@ -610,10 +682,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.3+1" google_maps: dependency: transitive description: @@ -626,154 +698,154 @@ packages: dependency: "direct main" description: name: google_maps_flutter - sha256: "209856c8e5571626afba7182cf634b2910069dc567954e76ec3e3fb37f5e9db3" + sha256: e1805e5a5885bd14a1c407c59229f478af169bf4d04388586b19f53145a5db3a url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.12.3" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android - sha256: "1b69fbb3ab76e7a7dfcf25e60f32f81ae5d9b88285343eecb5479116d54be869" + sha256: "67745f7850655faa8a596606b627fece63f3011078eaa0c151a4774568c23ac4" url: "https://pub.dev" source: hosted - version: "2.14.12" + version: "2.17.0" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - sha256: "6f798adb0aa1db5adf551f2e39e24bd06c8c0fbe4de912fb2d9b5b3f48147b02" + sha256: d03678415da9de8ce7208c674b264fc75946f326e696b4b7f84c80920fc58df6 url: "https://pub.dev" source: hosted - version: "2.13.2" + version: "2.15.4" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface - sha256: "74bf554a3697765654d3b9140e47e6bff2fdc1e91b8a4f8eafe37de44932423c" + sha256: f8293f072ed8b068b092920a72da6693aa8b3d62dc6b5c5f0bc44c969a8a776c url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.1" google_maps_flutter_web: dependency: transitive description: name: google_maps_flutter_web - sha256: ff39211bd25d7fad125d19f757eba85bd154460907cd4d135e07e3d0f98a4130 + sha256: ce2cac714e5462bf761ff2fdfc3564c7e5d7ed0578268dccb0a54dbdb1e6214e url: "https://pub.dev" source: hosted - version: "0.5.10" + version: "0.5.12+2" google_sign_in: dependency: "direct main" description: name: google_sign_in - sha256: fad6ddc80c427b0bba705f2116204ce1173e09cf299f85e053d57a55e5b2dd56 + sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" google_sign_in_android: dependency: transitive description: name: google_sign_in_android - sha256: "7af72e5502c313865c729223b60e8ae7bce0a1011b250c24edcf30d3d7032748" + sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805 url: "https://pub.dev" source: hosted - version: "6.1.35" + version: "6.2.1" google_sign_in_ios: dependency: transitive description: name: google_sign_in_ios - sha256: "8468465516a6fdc283ffbbb06ec03a860ee34e9ff84b0454074978705b42379b" + sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96" url: "https://pub.dev" source: hosted - version: "5.8.0" + version: "5.9.0" google_sign_in_platform_interface: dependency: transitive description: name: google_sign_in_platform_interface - sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" + sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.5.0" google_sign_in_web: dependency: transitive description: name: google_sign_in_web - sha256: ada595df6c30cead48e66b1f3a050edf0c5cf2ba60c185d69690e08adcc6281b + sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded" url: "https://pub.dev" source: hosted - version: "0.12.4+3" + version: "0.12.4+4" gql: dependency: transitive description: name: gql - sha256: "998304fbb88a3956cfea10cd27a56f8e5d4b3bc110f03c952c18a9310774e8bb" + sha256: "650e79ed60c21579ca3bd17ebae8a8c8d22cde267b03a19bf3b35996baaa843a" url: "https://pub.dev" source: hosted - version: "0.14.0" + version: "1.0.1-alpha+1730759315362" gql_dedupe_link: dependency: transitive description: name: gql_dedupe_link - sha256: "89681048cf956348e865da872a40081499b8c087fc84dd4d4b9c134bd70d27b3" + sha256: "10bee0564d67c24e0c8bd08bd56e0682b64a135e58afabbeed30d85d5e9fea96" url: "https://pub.dev" source: hosted - version: "2.0.3+1" + version: "2.0.4-alpha+1715521079596" gql_error_link: dependency: transitive description: name: gql_error_link - sha256: e7bfdd2b6232f3e15861cd96c2ad6b7c9c94693843b3dea18295136a5fb5b534 + sha256: "93901458f3c050e33386dedb0ca7173e08cebd7078e4e0deca4bf23ab7a71f63" url: "https://pub.dev" source: hosted - version: "0.2.3+1" + version: "1.0.0+1" gql_exec: dependency: transitive description: name: gql_exec - sha256: "0d1fdb2e4154efbfc1dcf3f35ec36d19c8428ff0d560eb4c45b354f8f871dc50" + sha256: "394944626fae900f1d34343ecf2d62e44eb984826189c8979d305f0ae5846e38" url: "https://pub.dev" source: hosted - version: "0.4.3" + version: "1.1.1-alpha+1699813812660" gql_http_link: dependency: transitive description: name: gql_http_link - sha256: "89ef87b32947acf4189f564c095f1148b0ab9bb9996fe518716dbad66708b834" + sha256: ef6ad24d31beb5a30113e9b919eec20876903cc4b0ee0d31550047aaaba7d5dd url: "https://pub.dev" source: hosted - version: "0.4.5" + version: "1.1.0" gql_link: dependency: transitive description: name: gql_link - sha256: f7973279126bc922d465c4f4da6ed93d187085e597b3480f5e14e74d28fe14bd + sha256: c2b0adb2f6a60c2599b9128fb095316db5feb99ce444c86fb141a6964acedfa4 url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.0.1-alpha+1730759315378" gql_transform_link: dependency: transitive description: name: gql_transform_link - sha256: b1735a9a92d25a92960002a8b40dfaede95ec1e5ed848906125d69efd878661f + sha256: "0645fdd874ca1be695fd327271fdfb24c0cd6fa40774a64b946062f321a59709" url: "https://pub.dev" source: hosted - version: "0.2.2+1" + version: "1.0.0" graphql: dependency: transitive description: name: graphql - sha256: b061201579040e9548cec2bae17bbdea0ab30666cb4e7ba48b9675f14d982199 + sha256: ad11e6d12de4d73971ae1dd80885b09f3cbc0bf143b1cbc5622a6dc6d85735e7 url: "https://pub.dev" source: hosted - version: "5.1.3" + version: "5.2.0-beta.4" graphql_flutter: dependency: "direct main" description: name: graphql_flutter - sha256: "06059ac9e8417c71582f05e28a59b1416d43959d34a6a0d9565341e3a362e117" + sha256: "39b5e830bc654ab02c5b776c31675841d1a8c95840fdd284efba713b1d47e65d" url: "https://pub.dev" source: hosted - version: "5.1.2" + version: "5.2.0-beta.6" graphs: dependency: transitive description: @@ -802,10 +874,10 @@ packages: dependency: transitive description: name: html - sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.5" + version: "0.15.6" http: dependency: "direct main" description: @@ -1026,10 +1098,10 @@ packages: dependency: transitive description: name: normalize - sha256: baf8caf2d8b745af5737cca6c24f7fe3cf3158897fdbcde9a909b9c8d3e2e5af + sha256: "8a60e37de5b608eeaf9b839273370c71ebba445e9f73b08eee7725e0d92dbc43" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.8.2+1" overlay_support: dependency: "direct main" description: @@ -1042,10 +1114,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -1082,10 +1154,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -1150,6 +1222,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" + url: "https://pub.dev" + source: hosted + version: "0.10.1+2" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + pointer_interceptor_platform_interface: + dependency: transitive + description: + name: pointer_interceptor_platform_interface + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" + url: "https://pub.dev" + source: hosted + version: "0.10.0+1" + pointer_interceptor_web: + dependency: transitive + description: + name: pointer_interceptor_web + sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a" + url: "https://pub.dev" + source: hosted + version: "0.10.3" pool: dependency: transitive description: @@ -1162,18 +1266,18 @@ packages: dependency: "direct main" description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: @@ -1226,18 +1330,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -1318,14 +1422,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" - simple_pip_mode: - dependency: "direct main" - description: - name: simple_pip_mode - sha256: "89f8137fa5a8d113f39c61007d4b658048a9359362447b8cfb8bce93631882ad" - url: "https://pub.dev" - source: hosted - version: "0.8.0" skeleton_text: dependency: "direct main" description: @@ -1475,30 +1571,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - uni_links: - dependency: "direct main" - description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - uni_links_platform_interface: - dependency: transitive - description: - name: uni_links_platform_interface - sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - uni_links_web: - dependency: transitive - description: - name: uni_links_web - sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" - url: "https://pub.dev" - source: hosted - version: "0.1.0" universal_platform: dependency: transitive description: @@ -1527,10 +1599,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1583,10 +1655,10 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: transitive description: @@ -1599,10 +1671,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.5" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7aa88129..1ddc4821 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,21 +9,22 @@ environment: sdk: ">=3.3.0 <4.0.0" dependencies: - connectivity_plus: any + flutter_typeahead: ^5.2.0 + connectivity_plus: ^5.0.2 cupertino_icons: ^1.0.5 date_time_picker: ^2.1.0 duration_picker: ^1.1.1 flutter: sdk: flutter flutter_animarker: ^3.2.0 - flutter_config: ^2.0.2 + flutter_dotenv: ^5.1.0 flutter_countdown_timer: ^4.1.0 - flutter_local_notifications: ^17.2.1+1 + flutter_local_notifications: ^18.0.0 flutter_polyline_points: ^1.0.0 flutter_spinkit: ^5.2.0 fluttertoast: ^8.2.4 geolocator: any - geolocator_android: 4.1.4 + geolocator_android: ^4.1.4 get_it: ^7.6.4 google_maps_flutter: ^2.5.3 hive: ^2.2.3 @@ -40,8 +41,6 @@ dependencies: responsive_sizer: ^3.3.1 skeleton_text: ^3.0.1 sliding_up_panel: ^2.0.0+1 - uni_links: ^0.5.1 - simple_pip_mode: ^0.8.0 data_connection_checker_nulls: ^0.0.2 flutter_geocoder_alternative: any gap: ^3.0.1 @@ -71,6 +70,7 @@ dependencies: dotted_border: ^2.1.0 shimmer: ^3.0.0 dev_dependencies: + auto_route_generator: build_runner: ^2.1.2 flutter_launcher_icons: ^0.13.1 flutter_test: @@ -92,6 +92,8 @@ flutter: assets: - images/ - animations/ + - images/icons/ + # - .env fonts: - family: Inter