Skip to content

Instantly share code, notes, and snippets.

@sajjadyousefnia
Created June 12, 2025 14:44
Show Gist options
  • Save sajjadyousefnia/4bd2fc4d69a7137f0f6bf1faa69bf0db to your computer and use it in GitHub Desktop.
Save sajjadyousefnia/4bd2fc4d69a7137f0f6bf1faa69bf0db to your computer and use it in GitHub Desktop.
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\navigation\Navigation.kt
```kt
package com.divadventure.data.navigation
import android.content.SharedPreferences
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.divadventure.App
import com.divadventure.di.SharedPreferencesModule
import com.divadventure.di.SharedPrefs
import com.divadventure.domain.models.Adventure
import com.divadventure.ui.screens.ChangeEmail
import com.divadventure.ui.screens.ForgotPasswordScreen
import com.divadventure.ui.screens.LandingScreen
import com.divadventure.ui.screens.LoginScreen
import com.divadventure.ui.screens.OnboardingScreen
import com.divadventure.ui.screens.ResetPassword
import com.divadventure.ui.screens.SignUpScreen
import com.divadventure.ui.screens.SplashScreen
import com.divadventure.ui.screens.VerificationScreen
import com.divadventure.ui.screens.main.add.AddOrEditAdventure
import com.divadventure.ui.screens.main.add.AdventureInvitationRequests
import com.divadventure.ui.screens.main.add.AdventureJoinRequests
import com.divadventure.ui.screens.main.add.AdventurePreview
import com.divadventure.ui.screens.main.add.manage.ManageAdventure
import com.divadventure.ui.screens.main.add.OwnerParticipantMenu
import com.divadventure.ui.screens.main.home.MainScreen
import com.divadventure.ui.screens.main.home.notifications.Notifications
import com.divadventure.ui.screens.main.home.notifications.search.filter.AdventureInterests
import com.divadventure.ui.screens.main.home.notifications.search.filter.Filter
import com.divadventure.ui.screens.main.home.notifications.search.filter.FilterInterests
import com.divadventure.ui.screens.main.home.notifications.search.filter.Location
import com.divadventure.ui.screens.main.home.notifications.search.filter.Status
import com.divadventure.ui.screens.main.home.notifications.search.sortby.SortBy
import com.divadventure.ui.screens.main.profile.AccountSettings
import com.divadventure.ui.screens.main.profile.ElseProfile
import com.divadventure.ui.screens.main.profile.NotificationsSettings
import com.divadventure.ui.screens.main.profile.PrivacySettings
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.viewmodel.HomeViewModel
import com.divadventure.viewmodel.MainViewModel
import com.divadventure.viewmodel.ManageAdventureViewModel
import com.divadventure.viewmodel.NotificationsViewModel
import com.divadventure.viewmodel.ProfileViewModel
import com.google.gson.Gson
import kotlinx.serialization.Serializable
import java.net.URLDecoder
import java.net.URLEncoder
/**
* Utility function to decode an Adventure object from a URL-encoded string
*/
fun decodeAdventureFromString(encodedString: String?): Adventure? {
return encodedString?.let { encoded ->
try {
// Decode the URL-encoded string first
val decodedJson = URLDecoder.decode(encoded, "UTF-8")
Gson().fromJson(decodedJson, Adventure::class.java)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
@Serializable
sealed class Screen(val route: String) {
object Splash : Screen("splash_screen")
@Serializable
object Landing : Screen("landing_screen")
object Login : Screen("login_screen")
object SignUp : Screen("signup_screen")
object Onboarding : Screen("onboarding_screen")
object Verification : Screen("verification_screen")
object VerificationChangeEmail : Screen("verification_change_email_screen")
object ResetPassword : Screen("reset_password_screen")
object ForgotPassword : Screen("forgot_password_screen")
object Main : Screen("main_screen")
object Notifications : Screen("notifications_screen")
object SortBy : Screen("sort_by_screen")
object Filter : Screen("filter_screen")
object Interests : Screen("interests_screen")
object Status : Screen("status_screen")
object AdventureInterests : Screen("adventure_interests")
object AdventurePreview : Screen("adventure_preview")
object Location : Screen("location_screen")
object ElseProfile : Screen("else_profile/{profileId}") {
fun createRoute(profileId: String): String = "else_profile/$profileId"
}
object ManageAdventure : Screen("manage_adventure/{adventure}") {
fun createRoute(adventure: String): String = "manage_adventure/$adventure"
}
object AccountSettings : Screen("account_settings")
object NotificationsSettings : Screen("notifications_settings")
object PrivacySettings : Screen("privacy_settings")
object AddOrEditAdventure : Screen("add_or_edit_adventure")
object AdventureOwnerParticipantMenu : Screen("adventure_owner_participant_menu/{adventure}") {
fun createRoute(adventure: String): String = "adventure_owner_participant_menu/$adventure"
}
object AdventureJoinRequests : Screen("adventure_join_requests/{adventure}") {
fun createRoute(adventure: String): String = "adventure_join_requests/$adventure"
}
object AdventureInvitationRequests : Screen("adventure_invitation_requests/{adventure}") {
fun createRoute(adventure: String): String = "adventure_invitation_requests/$adventure"
}
object AdventureParticipantManagement : Screen("adventure_participant_management/{adventure}") {
fun createRoute(adventure: String): String = "adventure_participant_management/$adventure"
}
}
@Composable
fun MyNavHost(
padding: PaddingValues,
navController: NavHostController = rememberNavController(),
navigationViewModel: NavigationViewModel = hiltViewModel(),
sharedPrefs: SharedPreferences = SharedPreferencesModule.provideSharedPreferences(App.Companion.getContext()!!)
) {
// Keep track of "live" routes in the back stack
val liveRoutes = remember { mutableListOf<String>() }
val customNavigationStackManager = rememberCustomNavigationStackManager(navController)
navigationViewModel.setNavigationManager(customNavigationStackManager)
val lifecycleOwner = LocalLifecycleOwner.current
val navigationEvent by navigationViewModel.navigationEvent.collectAsStateWithLifecycle(
lifecycleOwner
)
val authViewModel = hiltViewModel<AuthViewModel>()
// val loginViewModel = hiltViewModel<LoginViewModel>()
// val signupViewModel = hiltViewModel<SignupViewModel>()
val mainViewModel = hiltViewModel<MainViewModel>()
val notificationsViewModel = hiltViewModel<NotificationsViewModel>()
val homeViewModel = hiltViewModel<HomeViewModel>()
val adventureViewModel = hiltViewModel<ManageAdventureViewModel>()
LaunchedEffect(key1 = Unit) {
navigationViewModel.navigationEvent.collect { event ->
when (event) {
is NavigationEvent.NavigateTo -> {
navController.addOnDestinationChangedListener(event.onDestinationChangedListener)
navController.navigate(event.screen.route) {
launchSingleTop = true
if (event.popUpTo != null) {
popUpTo(event.popUpTo.route) {
inclusive = event.inclusive
}
}
}
if (event.removeListenerAfter) {
navController.removeOnDestinationChangedListener(event.onDestinationChangedListener)
}
}
is NavigationEvent.NavigateBack -> {
if (event.popUpToRoute != null) {
navController.popBackStack(event.popUpToRoute, event.inclusive)
} else {
navController.popBackStack()
}
}
is NavigationEvent.PopSpecific -> {
navController.popBackStack(event.route.route, event.inclusive)
}
is NavigationEvent.PopSpecificList -> {
event.routes.forEach { route ->
navController.popBackStack(route, event.inclusive)
}
}
NavigationEvent.PopBackStack -> {
navController.popBackStack()
}
NavigationEvent.RemoveAllPriorRoutes -> {
while (navController.popBackStack()) {
// Clear entire back stack
}
}
is NavigationEvent.NavigateProfile -> {
navController.addOnDestinationChangedListener(event.onDestinationChangedListener)
navController.navigate(Screen.ElseProfile.createRoute(event.profileId)) {
launchSingleTop = false
}
if (event.removeListenerAfter) {
navController.removeOnDestinationChangedListener(event.onDestinationChangedListener)
}
}
is NavigationEvent.NavigateAdventure -> {
try {
navController.addOnDestinationChangedListener(event.onDestinationChangedListener)
// Ensure the adventure object has initialized lists to prevent serialization issues
val safeAdventure = event.adventure.copy(
adventureRequest = event.adventure.adventureRequest ?: emptyList(),
adventurers = event.adventure.adventurers.ifEmpty { emptyList() }
)
// Use URI encoding to safely pass complex JSON as a navigation parameter
val adventureJson = Gson().toJson(safeAdventure)
val encodedAdventure = URLEncoder.encode(adventureJson, "UTF-8")
navController.navigate(Screen.ManageAdventure.createRoute(encodedAdventure))
if (event.removeListenerAfter) {
navController.removeOnDestinationChangedListener(event.onDestinationChangedListener)
}
} catch (e: Exception) {
// Log the error but prevent app crash
e.printStackTrace()
}
}
is NavigationEvent.NavigateAdventureOwnerParticipantMenu -> {
try {
navController.addOnDestinationChangedListener(event.onDestinationChangedListener)
// Ensure the adventure object has initialized lists to prevent serialization issues
val safeAdventure = event.adventure.copy(
adventureRequest = event.adventure.adventureRequest ?: emptyList(),
adventurers = event.adventure.adventurers.ifEmpty { emptyList() }
)
// Use URI encoding to safely pass complex JSON as a navigation parameter
val adventureJson = Gson().toJson(safeAdventure)
val encodedAdventure = URLEncoder.encode(adventureJson, "UTF-8")
navController.navigate(
Screen.AdventureOwnerParticipantMenu.createRoute(
encodedAdventure
)
) {
launchSingleTop = true
}
if (event.removeListenerAfter) {
navController.removeOnDestinationChangedListener(event.onDestinationChangedListener)
}
} catch (e: Exception) {
// Log the error but prevent app crash
e.printStackTrace()
}
}
is NavigationEvent.NavigateAdventureOptions -> {
try {
// Register destination change listener
navController.addOnDestinationChangedListener(event.onDestinationChangedListener)
// Ensure the adventure object has initialized lists to prevent serialization issues
val safeAdventure = event.adventure.copy(
adventureRequest = event.adventure.adventureRequest ?: emptyList(),
adventurers = event.adventure.adventurers.ifEmpty { emptyList() }
)
// Use URI encoding to safely pass complex JSON as a navigation parameter
val adventureJson = Gson().toJson(safeAdventure)
val encodedAdventure = URLEncoder.encode(adventureJson, "UTF-8")
// Navigate to the appropriate route based on the screen type
when (event.partName) {
is Screen.AdventureJoinRequests -> {
navController.navigate(
Screen.AdventureJoinRequests.createRoute(encodedAdventure)
) { launchSingleTop = true }
}
is Screen.AdventureInvitationRequests -> {
navController.navigate(
Screen.AdventureInvitationRequests.createRoute(encodedAdventure)
) { launchSingleTop = true }
}
is Screen.AdventureParticipantManagement -> {
navController.navigate(
Screen.AdventureParticipantManagement.createRoute(
encodedAdventure
)
) { launchSingleTop = true }
}
else -> {
throw IllegalArgumentException("Unknown screen type")
}
}
// Remove listener if needed
if (event.removeListenerAfter) {
navController.removeOnDestinationChangedListener(event.onDestinationChangedListener)
}
} catch (e: Exception) {
e.printStackTrace()
// Optional: Handle navigation error
}
}
}
}
}
NavHost(
navController = navController,
startDestination = Screen.Splash.route,
enterTransition = {
slideInHorizontally(
initialOffsetX = { it / 2 }, animationSpec = tween(
500, easing = { FastOutSlowInEasing.transform(it) })
) + fadeIn(animationSpec = tween(500))
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { -it / 2 }, animationSpec = tween(
500, easing = { FastOutSlowInEasing.transform(it) })
) + fadeOut(animationSpec = tween(500))
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { -it / 2 }, animationSpec = tween(
500, easing = { FastOutSlowInEasing.transform(it) })
) + fadeIn(animationSpec = tween(500))
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { it / 2 }, animationSpec = tween(
500, easing = { FastOutSlowInEasing.transform(it) })
) + fadeOut(animationSpec = tween(500))
},
) {
composable(Screen.Splash.route) {
SplashScreen(
navigationViewModel = navigationViewModel, viewModel = authViewModel
)
}
composable(Screen.Landing.route) {
LandingScreen(
navigationViewModel = navigationViewModel,
viewModel = authViewModel,
padding = padding
)
}
composable(Screen.Main.route) {
MainScreen(
homeViewModel,
mainViewModel,
adventureViewModel,
navigationViewModel = navigationViewModel,
padding = padding,
)
}
composable(Screen.Login.route) {
LoginScreen(
navigationViewModel = navigationViewModel,
viewModel = authViewModel,
)
}
composable(Screen.SignUp.route) {
SignUpScreen(
navigationViewModel = navigationViewModel,
viewModel = authViewModel,
padding = padding
)
}
composable(Screen.Onboarding.route) {
OnboardingScreen(
navigationViewModel = navigationViewModel, viewModel = authViewModel, padding
)
}
composable(Screen.Verification.route) {
VerificationScreen(
navigationViewModel = navigationViewModel,
viewModel = authViewModel,
padding = padding,
sharedPrefs = SharedPrefs(sharedPrefs)
)
}
composable(Screen.VerificationChangeEmail.route) {
ChangeEmail(
navigationViewModel = navigationViewModel,
viewModel = authViewModel,
padding = padding
)
}
composable(Screen.ResetPassword.route) {
ResetPassword(
navigationViewModel = navigationViewModel,
viewModel = authViewModel,
padding = padding
)
}
composable(Screen.ForgotPassword.route) {
ForgotPasswordScreen(
navigationViewModel = navigationViewModel, viewModel = authViewModel
)
}
composable(Screen.Notifications.route) {
Notifications(
padding = padding,
notificationsViewModel = notificationsViewModel,
navigationViewModel = navigationViewModel,
)
}
composable(Screen.SortBy.route) {
SortBy(
mainViewModel,
navigationViewModel,
homeViewModel,
padding = padding,
)
}
composable(Screen.Filter.route) {
Filter(
mainViewModel = mainViewModel,
navigationViewModel = navigationViewModel,
homeViewModel = homeViewModel,
paddingValues = padding
)
}
composable(Screen.Interests.route) {
FilterInterests(
navigationViewModel = navigationViewModel, homeViewModel,
paddingValues = padding
)
}
composable(Screen.AdventureInterests.route) {
AdventureInterests(
navigationViewModel = navigationViewModel,
adventureViewModel = adventureViewModel,
paddingValues = padding
)
}
composable(Screen.AdventurePreview.route) {
AdventurePreview(
adventureViewModel,
mainViewModel,
navigationViewModel,
padding
)
}
composable(Screen.Status.route) {
Status(
mainViewModel = mainViewModel,
navigationViewModel = navigationViewModel,
homeViewModel = homeViewModel,
paddingValues = padding
)
}
composable(Screen.Location.route) {
Location(
mainViewModel = mainViewModel,
navigationViewModel = navigationViewModel,
homeViewModel = homeViewModel,
paddingValues = padding
)
}
composable(
route = Screen.ElseProfile.route,
arguments = listOf(navArgument("profileId") { type = NavType.StringType })
) { backStackEntry ->
val profileId = backStackEntry.arguments?.getString("profileId") // Extract the argument
ElseProfile(
profileId = profileId ?: "", // Pass the argument to the composable
paddings = padding,
mainViewModel = mainViewModel,
profileViewModel = hiltViewModel<ProfileViewModel>(),
)
}
composable(Screen.AccountSettings.route) {
AccountSettings(padding)
}
composable(Screen.NotificationsSettings.route) {
NotificationsSettings(padding)
}
composable(Screen.PrivacySettings.route) {
PrivacySettings(padding)
}
composable(
Screen.ManageAdventure.route,
arguments = listOf(navArgument("adventure") { type = NavType.StringType })
) {
val encodedAdventureString = it.arguments?.getString("adventure")
val adventure = decodeAdventureFromString(encodedAdventureString)
ManageAdventure(
padding,
navigationViewModel,
mainViewModel,
adventureViewModel,
adventure!!
)
}
composable(
Screen.AddOrEditAdventure.route
) {
AddOrEditAdventure(
padding, mainViewModel, adventureViewModel, navigationViewModel
)
}
composable(
Screen.AdventureOwnerParticipantMenu.route,
arguments = listOf(navArgument("adventure") { type = NavType.StringType })
) {
val encodedAdventureString = it.arguments?.getString("adventure")
val adventure = decodeAdventureFromString(encodedAdventureString)
OwnerParticipantMenu(
paddingValues = padding,
adventure = adventure!!,
mainViewModel = mainViewModel,
navigationViewModel = navigationViewModel,
manageAdventureViewModel = adventureViewModel
)
}
composable(
Screen.AdventureInvitationRequests.route
) {
val encodedAdventureString = it.arguments?.getString("adventure")
val adventure = decodeAdventureFromString(encodedAdventureString)
AdventureInvitationRequests(padding, adventure!!)
}
composable(
Screen.AdventureJoinRequests.route,
arguments = listOf(navArgument("adventure") { type = NavType.StringType }) // Ensure arguments are correctly defined
) { backStackEntry -> // It's good practice to name this 'backStackEntry'
val encodedAdventureString = backStackEntry.arguments?.getString("adventure")
val adventure = decodeAdventureFromString(encodedAdventureString)
// Pass the shared adventureViewModel instance
AdventureJoinRequests(
padding = padding, // Corrected: use 'padding' to match the signature
adventure = adventure!!,
manageAdventureViewModel = adventureViewModel // Pass the shared instance
)
}
/** todo : add participant management
composable(
Screen.AdventureParticipantManagement.route
) {
val encodedAdventureString = it.arguments?.getString("adventure")
val adventure = decodeAdventureFromString(encodedAdventureString)
AdventureParticipantManagement(padding, adventure!!)
}
*/
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\navigation\NavigationEvent.kt
```kt
package com.divadventure.data.navigation
import androidx.navigation.NavController
import com.divadventure.domain.models.Adventure
sealed class NavigationEvent {
data class NavigateTo(
val screen: Screen,
val popUpTo: Screen? = null,
val inclusive: Boolean = false,
val onDestinationChangedListener: NavController.OnDestinationChangedListener,
val singleTop: Boolean = true,
val removeListenerAfter: Boolean = true
) : NavigationEvent()
object RemoveAllPriorRoutes : NavigationEvent()
data class NavigateBack(
val popUpToRoute: String? = null,
val inclusive: Boolean = false
) : NavigationEvent()
data class PopSpecific(
val route: Screen,
val inclusive: Boolean,
) : NavigationEvent()
data class NavigateProfile(
val profileId: String,
val onDestinationChangedListener: NavController.OnDestinationChangedListener,
val removeListenerAfter: Boolean = true
) : NavigationEvent()
object PopBackStack : NavigationEvent()
data class NavigateAdventure(
val adventure: Adventure,
val onDestinationChangedListener: NavController.OnDestinationChangedListener,
val removeListenerAfter: Boolean = true
) : NavigationEvent()
data class NavigateAdventureOwnerParticipantMenu(
val adventure: Adventure,
val onDestinationChangedListener: NavController.OnDestinationChangedListener,
val removeListenerAfter: Boolean = true
) : NavigationEvent()
data class NavigateAdventureOptions(
val adventure: Adventure,
val partName: Screen,
val onDestinationChangedListener: NavController.OnDestinationChangedListener,
val removeListenerAfter: Boolean = true
): NavigationEvent()
data class PopSpecificList(val routes: List<String>, val inclusive: Boolean) : NavigationEvent()
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\navigation\NavigationViewModel.kt
```kt
package com.divadventure.data.navigation
import androidx.lifecycle.viewModelScope
import com.divadventure.viewmodel.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class NavigationViewModel @Inject constructor(
) :
BaseViewModel<NavigationIntent, NavigationState>(NavigationState()) {
private lateinit var _customNavigationStackManager: CustomNavigationStackManager
// Navigation event flow for handling navigation actions
private val _navigationEvent = MutableSharedFlow<NavigationEvent>(replay = 0)
//val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent
fun setNavigationManager(customNavigationStackManager: CustomNavigationStackManager) {
_customNavigationStackManager = customNavigationStackManager
}
/**
* Navigate to a destination using the navigation event flow (suspend function)
*/
suspend fun navigateSuspend(event: NavigationEvent) {
_navigationEvent.emit(event)
}
/**
* Navigate to a destination using the navigation event flow (from non-coroutine context)
* This is a convenience method for viewModelScope.launch { navigate(event) }
*/
fun navigateFrom(event: NavigationEvent) {
viewModelScope.launch {
navigateSuspend(event)
}
}
override suspend fun handleIntent(intent: NavigationIntent) {
/*
when (intent) {
*/
/*NavigationIntent.navigateLandingToLogin -> {
navigate(NavigationEvent.NavigateTo(Screen.LoginScreen))
}*//*
*/
/*
NavigationIntent.navigateLandingToSignup -> {
navigate(NavigationEvent.NavigateTo(Screen.SignUpScreen))
}
*//*
*/
/* NavigationIntent.navigateLoginToLanding -> {
navigate(
NavigationEvent.NavigateTo(
Screen.LandingScreen,
Screen.LoginScreen,
inclusive = true
)
)
}*//*
*/
/*
NavigationIntent.navigateSignupToLanding -> {
navigate(
NavigationEvent.NavigateTo(
Screen.LandingScreen,
Screen.SignUpScreen,
inclusive = true
)
)
}
*//*
*/
/*
NavigationIntent.navaigteSignupToHome -> {
navigate(
NavigationEvent.NavigateTo(
Screen.HomeScreen, Screen.LandingScreen, inclusive = true
)
)
}
*//*
*/
/*
NavigationIntent.navigateSplashToLanding -> {
navigate(
NavigationEvent.NavigateTo(
Screen.LandingScreen,
Screen.SplashScreen,
inclusive = true
)
)
}
*//*
*/
/*
is NavigationIntent.navigateLoginToSignUp -> {
navigate(NavigationEvent.NavigateTo(Screen.SignUpScreen))
}
*//*
*/
/*
NavigationIntent.navigationToOnboard -> {
navigate(
NavigationEvent.NavigateTo(
Screen.OnboardingScreen, Screen.LandingScreen, inclusive = true
)
)
}
*//*
*/
/*
NavigationIntent.navigateOnboadringToVerification -> {
navigate(
NavigationEvent.NavigateTo(
Screen.VerificationScreen, Screen.OnboardingScreen, inclusive = true
)
)
}
*//*
*/
/*
NavigationIntent.navigateVerificationToLogin -> navigate(
NavigationEvent.NavigateTo(
Screen.LoginScreen, Screen.VerificationScreen, inclusive = true
)
)
*//*
}
*/
}
}
sealed class NavigationIntent {
// object navigateLandingToLogin : NavigationIntent()
// object navigateLandingToSignup : NavigationIntent()
// object navigateLoginToLanding : NavigationIntent()
// object navigateSignupToLanding : NavigationIntent()
// object navaigteSignupToHome : NavigationIntent()
// object navigationToOnboard : NavigationIntent()
// object navigateSplashToLanding : NavigationIntent()
// data class navigateLoginToSignUp(val noHistory: Boolean) : NavigationIntent()
// object navigateOnboadringToVerification : NavigationIntent()
// object navigateVerificationToLogin : NavigationIntent()
}
data class NavigationState(val value: String = "")
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\Repository\AdventureRepository.kt
```kt
package com.divadventure.data.Repository
import com.divadventure.data.AdventureApi
import com.divadventure.di.SharedPrefs
import com.divadventure.di.UserPrefs.KEY_ID
import com.divadventure.di.UserPrefs.KEY_TOKEN
import com.divadventure.domain.models.AdventuresResponse
import com.divadventure.domain.models.CreateAdventureRequest
import com.divadventure.domain.models.CreateAdventureResponse
import com.divadventure.domain.models.Filters
import javax.inject.Inject
class AdventureRepository @Inject constructor(
private val adventureApi: AdventureApi, private val sharedPrefs: SharedPrefs
) {
suspend fun searchAdventures(
searchQuery: String?, page: Int, filters: Filters
): AdventuresResponse {
val options = mutableMapOf<String, String?>()
// Add parameters only if they're not null
searchQuery?.let { options["query"] = it }
// filters.interests?.map { it.id }?.let { options["interests[]"] = it.joinToString(",") }
filters.interests?.map { it.id }?.let {
it.forEach { options.put("interests[]", it) }
}
filters.locationLAt?.let { options["location[lat]"] = it.toString() }
filters.locationLng?.let { options["location[lng]"] = it.toString() }
filters.startDate?.let { options["start_date"] = it }
filters.endDate?.let { options["end_date"] = it }
filters.state?.let { options["state"] = it.lowercase() }
filters.perPage?.let { options["per_page"] = it.toString() }
filters.orderBy?.let {
when {
it.contentEquals("Popular", true) -> {
options["order_by"] = "popularity"
}
it.contentEquals("Recent", true) -> {
options["order_by"] = "recently_created"
}
it.contentEquals("Near me", true) -> {
options["order_by"] = "near_me"
}
}
}
options["page"] = page.toString()
return adventureApi.searchAdventures(
bearer = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}", options = options
)
}
suspend fun getGroupedAdventures(group: String, page: Int): AdventuresResponse {
val options = mutableMapOf<String, String?>()
when {
group.contentEquals("ALL", ignoreCase = true) == false -> {
options[group.lowercase()] = sharedPrefs.getString(KEY_ID) ?: ""
}
}
options["page"] = page.toString()
options["per_page"] = "10"
return adventureApi.getAdventures(
bearer = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}", options = options
)
}
suspend fun getAllAdventures(page: Int): AdventuresResponse {
val options = mutableMapOf<String, String?>()
options["page"] = page.toString()
return adventureApi.getAdventures(
bearer = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}", options = options
)
}
suspend fun getMyAdventures(): AdventuresResponse {
return adventureApi.getMyAdventures(
bearer = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}",
)
}
suspend fun getCalendarAdventures(
group: String, startDate: String, endDate: String
): AdventuresResponse {
val options = mutableMapOf<String, String?>()
options["start_date"] = startDate
options["end_date"] = endDate
options["per_page"] = "10000"
when {
!group.contentEquals("ALL", ignoreCase = true) -> {
options[group.lowercase()] = sharedPrefs.getString(KEY_ID) ?: ""
}
}
return adventureApi.getAdventures(
bearer = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}", options = options
)
}
suspend fun getElseAdventures(profileId: String): AdventuresResponse {
return adventureApi.getUserAdventures(
bearer = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}",
id = profileId
)
}
suspend fun createNewAdventure(createAdventureRequest: CreateAdventureRequest): CreateAdventureResponse {
return adventureApi.createAdventure(
bearer = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}",
createAdventureRequest = createAdventureRequest
)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\Repository\CalendarRepository.kt
```kt
package com.divadventure.data.Repository
class CalendarRepository {
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\Repository\InterestsRepository.kt
```kt
package com.divadventure.data.Repository
import com.divadventure.data.InterestsApi
import com.divadventure.di.SharedPrefs
import com.divadventure.di.UserPrefs.KEY_TOKEN
import com.divadventure.domain.models.InterestsResponse
import javax.inject.Inject
class InterestsRepository @Inject constructor(
private val interestsApi: InterestsApi, private val sharedPrefs: SharedPrefs
) {
suspend fun getInterests(): InterestsResponse {
// Fetch the list of interests from the API
return interestsApi.getInterests(
bearer = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}"
)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\Repository\ProfileRepository.kt
```kt
package com.divadventure.data.Repository
import javax.inject.Inject
class ProfileRepository @Inject constructor(
) {
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\Repository\RequestsRepository.kt
```kt
package com.divadventure.data.Repository
import com.divadventure.data.RequestsApi
import com.divadventure.di.SharedPrefs
import com.divadventure.di.UserPrefs.KEY_TOKEN
import com.divadventure.domain.models.AdventureRequestsResponse
import com.divadventure.domain.models.Participant
import com.divadventure.domain.models.Request
import com.divadventure.domain.models.UsersAdventureRequestResponse
import jakarta.inject.Inject
class RequestsRepository @Inject constructor(
private val sharedPrefs: SharedPrefs,
private val requestsApi: RequestsApi
) {
/**
* Fetches all adventure requests for a specific adventure.
*
* @param adventureId The ID of the adventure for which to fetch requests.
*/
suspend fun fetchAllAdventureRequests(adventureId: String): AdventureRequestsResponse {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch bearer token
return requestsApi.getAdventureRequests(
id = adventureId, bearer = token
) // Fetch adventure requests
}
suspend fun fetchUserAdventureRequests(
adventureId: String, userId: String
): UsersAdventureRequestResponse {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch bearer token
return requestsApi.getAllUserRequests(
id = userId,
bearer = token, // Use the token variable
query = adventureId // Include the necessary query parameter
) // Fetch user requests
}
suspend fun createAdventureInviteRequest(
adventureId: String, userId: String
): Request {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch bearer token
return requestsApi.createAdventureInviteRequest(
bearer = token, id = adventureId, requestBody = mapOf("userId" to userId)
) // Send an invite request
}
suspend fun destroyAdventureInviteRequest(
adventureId: String, requestId: String
) {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch bearer token
requestsApi.destroyAdventureInviteRequest(
id = adventureId, bearer = token, requestId = requestId
)
}
suspend fun acceptJoinRequest(
adventureId: String, requestId: String
): com.divadventure.domain.models.AdventureRequestResponse {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch bearer token
return requestsApi.acceptJoinRequest(
id = adventureId, bearer = token, requestId = requestId
) // Accept the join request
}
suspend fun declineJoinRequest(
adventureId: String, requestId: String
): com.divadventure.domain.models.AdventureRequestResponse {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch bearer token
return requestsApi.declineJoinRequest(
id = adventureId, bearer = token, requestId = requestId
)
}
suspend fun createJoinRequest(
adventureId: String
): List<Participant> {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch bearer token
return requestsApi.createJoinRequest(
id = adventureId, bearer = token
) // Create a join request
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\Repository\UploadImageManager.kt
```kt
package com.divadventure.data.Repository
import com.divadventure.data.UploadApi
import com.divadventure.di.SharedPrefs
import jakarta.inject.Inject
class UploadImageManager @Inject constructor(
private val uploadApi: UploadApi, // API service for file uploads
private val sharedPrefs: SharedPrefs // Shared preferences for storing token
)
/*
suspend fun uploadImage(file: File): UploadResponse {
return try {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Retrieve bearer token
val requestBody =
file.asRequestBody("image/*".toMediaTypeOrNull()) // Create a request body for the file
val multipartBody = MultipartBody.Part.createFormData(
"file", file.name, requestBody
) // Create a multipart body
uploadApi.uploadImage(bearer = token, file = multipartBody) // Call the API
} catch (e: Exception) {
throw e // Rethrow exception to maintain expected behavior
}
}
*/
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\Repository\UsersRepository.kt
```kt
package com.divadventure.data.Repository
import com.divadventure.data.UsersApi
import com.divadventure.di.SharedPrefs
import com.divadventure.di.UserPrefs.KEY_TOKEN
import com.divadventure.domain.models.FriendsResponse
import com.divadventure.domain.models.UsersData
import javax.inject.Inject
class UsersRepository @Inject constructor(
private val usersApi: UsersApi,
private val sharedPrefs: SharedPrefs
) {
/**
* Retrieves the list of friends for the current user.
* This method uses the `FriendsApi` to fetch the data.
*/
suspend fun getFriends(): FriendsResponse {
return try {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch the bearer token
usersApi.getFriends(bearer = token) // Call the API with the token
} catch (e: Exception) {
throw e // Rethrow exception to preserve expected behavior
}
}
suspend fun getElseFriends(profileId: String): FriendsResponse {
return try {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch the bearer token
usersApi.getElseFriends(
bearer = token, id = profileId
)
} catch (e: Exception) {
throw e
}
}
suspend fun getUserData(userId: String): UsersData {
return try {
val token = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}" // Fetch the bearer token
usersApi.getUserData(bearer = token, id = userId) // Call the API to fetch user data
} catch (e: Exception) {
throw e // Rethrow exception to preserve expected behavior
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\data\SharedService.kt
```kt
package com.divadventure.data
import com.divadventure.domain.models.AdventureRequestResponse
import com.divadventure.domain.models.AdventureRequestsResponse
import com.divadventure.domain.models.AdventuresResponse
import com.divadventure.domain.models.CreateAdventureRequest
import com.divadventure.domain.models.CreateAdventureResponse
import com.divadventure.domain.models.FriendsResponse
import com.divadventure.domain.models.InterestsResponse
import com.divadventure.domain.models.Participant
import com.divadventure.domain.models.ReqLogin
import com.divadventure.domain.models.ReqOnboard
import com.divadventure.domain.models.ReqVerifyEmail
import com.divadventure.domain.models.ReqVerifyResetPasswordToken
import com.divadventure.domain.models.Request
import com.divadventure.domain.models.ResVerifyEmail
import com.divadventure.domain.models.ResVerifyResetPasswordToken
import com.divadventure.domain.models.SignUpResponse
import com.divadventure.domain.models.SignupRequest
import com.divadventure.domain.models.UsersAdventureRequestResponse
import com.divadventure.domain.models.UsersData
import com.divadventure.viewmodel.AuthViewModel.ResetPasswordRequest
import com.divadventure.viewmodel.AuthViewModel.ResetPasswordResponse
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.QueryMap
interface SharedService {
@POST("auth/sign-up")
fun signup(@Body request: SignupRequest): Call<SignUpResponse>
@PUT("auth/onboard")
fun onboard(
@Header("Authorization") bearer: String, @Body request: ReqOnboard
): Call<SignUpResponse>
@PUT("auth/verify-email")
fun verifyEmail(
@Header("Authorization") bearer: String, @Body request: ReqVerifyEmail
): Call<ResVerifyEmail>
@POST("auth/resend-email-verification")
fun resendVerificationEmail(
@Header("Authorization") bearer: String,
): Call<ResVerifyEmail>
@PUT("auth/update-email")
fun updateEmail(
@Header("Authorization") bearer: String, @Body request: ResVerifyEmail
): Call<ResVerifyEmail>
// Account
@GET("account/me")
fun getCurrentUser(
@Header("Authorization") bearer: String,
): Call<SignUpResponse>
@POST("auth/login")
fun login(@Body request: ReqLogin): Call<SignUpResponse>
@POST("auth/forgot-password")
fun forgotPassword(@Body request: ResVerifyEmail): Call<Unit>
@Headers("Content-Type: application/json")
@POST("auth/verify-reset-password-token")
fun verifyResetPasswordToken(
@Body reqVerifyResetPasswordToken: ReqVerifyResetPasswordToken
): Call<ResVerifyResetPasswordToken>
@POST("auth/reset-password")
fun resetPassword(
@Body request: ResetPasswordRequest
): Call<ResetPasswordResponse>
}
interface AdventureApi {
// Search adventures with query and filters
@GET("adventures/search")
suspend fun searchAdventures(
@Header("Authorization") bearer: String,
@QueryMap options: Map<String, String?>
): AdventuresResponse
@GET("account/calendar_view")
suspend fun getMyAdventures(
@Header("Authorization") bearer: String,
): AdventuresResponse
@GET("adventures")
suspend fun getAdventures(
@Header("Authorization") bearer: String,
@QueryMap options: Map<String, String?>
): AdventuresResponse
@GET("users/{id}/calendar_view")
suspend fun getUserAdventures(
@Header("Authorization") bearer: String,
@Path("id") id: String
): AdventuresResponse
@POST("adventures")
suspend fun createAdventure(
@Header("Authorization") bearer: String,
@Body createAdventureRequest: CreateAdventureRequest
): CreateAdventureResponse
/*// Get all adventures (with no ID query)
@GET("adventures")
suspend fun getAdventures(
@Header("Authorization") bearer: String,
): AdventuresResponse
// Get adventures owned by a specific user
@GET("adventures")
suspend fun getOwnedAdventures(
@Header("Authorization") bearer: String, @Query("owned") id: String
): AdventuresResponse
// Get invited adventures
@GET("adventures")
suspend fun getInvitedAdventures(
@Header("Authorization") bearer: String, @Query("invited") id: String
): AdventuresResponse
// Get joined adventures
@GET("adventures")
suspend fun getJoinedAdventures(
@Header("Authorization") bearer: String, @Query("joined") id: String
): AdventuresResponse
// Get friends' adventures
@GET("adventures")
suspend fun getFriendsAdventures(
@Header("Authorization") bearer: String, @Query("friends") id: String
): AdventuresResponse*/
}
interface UsersApi {
@GET("users/{id}")
suspend fun getUserData(
@Header("Authorization") bearer: String,
@Path("id") id: String
): UsersData
@GET("account/friends")
suspend fun getFriends(
@Header("Authorization") bearer: String
): FriendsResponse
@GET("users/{id}/friends")
suspend fun getElseFriends(
@Header("Authorization") bearer: String,
@Path("id") id: String
): FriendsResponse
}
interface InterestsApi {
@GET("interests")
suspend fun getInterests(
@Header("Authorization") bearer: String
): InterestsResponse
}
interface UploadApi {
@POST("uploader")
suspend fun uploadImage(
@Header("Authorization") bearer: String,
@Body file: RequestBody
)
}
interface CalendarApi {
@GET("account/calendar_view")
suspend fun getMyCalendar(
@Header("Authorization") bearer: String
): FriendsResponse
}
interface RequestsApi {
@GET("adventures/{id}/adventure_requests")
suspend fun getAdventureRequests(
@Header("Authorization") bearer: String,
@Path("id") id: String
): AdventureRequestsResponse
@GET("adventures/{id}/adventure_user_requests")
suspend fun getAllUserRequests(
@Header("Authorization") bearer: String,
@Path("id") id: String,
@Query("query") query: String
): UsersAdventureRequestResponse
@POST("adventures/{id}/invite_requests")
suspend fun createAdventureInviteRequest(
@Header("Authorization") bearer: String,
@Path("id") id: String,
@Body requestBody: Map<String, String>
): Request
@DELETE("adventures/{id}/invite_requests/{request_id}")
suspend fun destroyAdventureInviteRequest(
@Header("Authorization") bearer: String,
@Path("id") id: String,
@Path("request_id") requestId: String
): AdventureRequestResponse
@PUT("adventures/{id}/join_requests/{join_request_id}/accept")
suspend fun acceptJoinRequest(
@Header("Authorization") bearer: String,
@Path("id") id: String,
@Path("join_request_id") requestId: String
): AdventureRequestResponse
@PUT("adventures/{id}/join_requests/{request_id}/decline")
suspend fun declineJoinRequest(
@Header("Authorization") bearer: String,
@Path("id") id: String,
@Path("request_id") requestId: String
): AdventureRequestResponse
@POST("adventures/{id}/join_requests")
suspend fun createJoinRequest(
@Header("Authorization") bearer: String,
@Path("id") id: String
): List<Participant>
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\di\AppModule.kt
```kt
package com.divadventure.di
import android.content.Context
import com.divadventure.R
import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.net.PlacesClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideBaseUrl(): String {
val baseUrl = "https://adv-backend-staging-iec9d.ondigitalocean.app/api/v1/"
Timber.Forest.d("Providing Base URL: $baseUrl")
return baseUrl
}
@Provides
@Singleton
fun provideRetrofit(baseUrl: String): Retrofit {
Timber.Forest.d("Building Retrofit instance with Base URL: $baseUrl")
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun providePlacesClient(@ApplicationContext context: Context): PlacesClient {
// Initialize Places with the context
Places.initialize(context, context.getString(R.string.map_id))
return Places.createClient(context)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\di\FeaturesModules\AdventuresModule.kt
```kt
package com.divadventure.di.FeaturesModules
import com.divadventure.data.Repository.AdventureRepository
import com.divadventure.di.SharedPrefs
import com.divadventure.domain.usecase.AdventuresUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AdventuresModule {
@Provides
@Singleton
fun provideAdventuresUseCase(
adventureRepository: AdventureRepository,
sharedPrefs: SharedPrefs
): AdventuresUseCase {
return AdventuresUseCase(adventureRepository, sharedPrefs)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\di\FeaturesModules\DateModule.kt
```kt
package com.divadventure.di.FeaturesModules
import com.divadventure.data.Repository.AdventureRepository
import com.divadventure.di.SharedPrefs
import com.divadventure.domain.usecase.CalendarUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class) // Correct scope for application-wide dependencies
object DateModule {
@Provides
@Singleton
fun provideCalendarUseCase(
adventureRepository: AdventureRepository,
sharedPrefs: SharedPrefs
): CalendarUseCase {
return CalendarUseCase(adventureRepository, sharedPrefs)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\di\FeaturesModules\LocationModule.kt
```kt
package com.divadventure.di.FeaturesModules
import com.divadventure.data.Repository.AdventureRepository
import com.divadventure.di.SharedPrefs
import com.divadventure.domain.usecase.AdventuresUseCase
import com.divadventure.domain.usecase.LocationsUseCase
import com.google.android.libraries.places.api.net.PlacesClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object LocationModule {
@Provides
@Singleton
fun provideLocationsUseCase(placesClient: PlacesClient): LocationsUseCase {
return LocationsUseCase(placesClient)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\di\FeaturesModules\ProfileModule.kt
```kt
package com.divadventure.di.FeaturesModules
import com.divadventure.data.Repository.UsersRepository
import com.divadventure.domain.usecase.UsersUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object ProfileModule {
@Provides
@Singleton
fun provideFriendsUseCase(
usersRepository: UsersRepository
): UsersUseCase {
return UsersUseCase(
usersRepository
)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\di\FeaturesModules\RequestsModule.kt
```kt
package com.divadventure.di.FeaturesModules
import com.divadventure.data.Repository.RequestsRepository
import com.divadventure.domain.usecase.RequestsUseCase
import com.divadventure.domain.usecase.UsersUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object RequestsModule {
@Provides
@Singleton
fun provideRequestsUseCase(
requestsRepository: RequestsRepository
): RequestsUseCase {
return RequestsUseCase(
requestsRepository = requestsRepository
)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\di\HomeModule.kt
```kt
package com.divadventure.di
import com.divadventure.data.InterestsApi
import com.divadventure.data.Repository.AdventureRepository
import com.divadventure.data.Repository.InterestsRepository
import com.divadventure.domain.usecase.AdventuresUseCase
import com.divadventure.domain.usecase.CalendarUseCase
import com.divadventure.domain.usecase.InterestsUseCase
import com.divadventure.domain.usecase.LocationsUseCase
import com.google.android.libraries.places.api.net.PlacesClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(ViewModelComponent::class)
object HomeModule {
@Provides
fun provideInterestsRepository(
interestsApi: InterestsApi,
sharedPrefs: SharedPrefs
): InterestsRepository {
return InterestsRepository(interestsApi, sharedPrefs)
}
@Provides
fun provideInterestsUseCase(
interestsRepository: InterestsRepository
): InterestsUseCase {
return InterestsUseCase(interestsRepository)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\di\NetworkModule.kt
```kt
package com.divadventure.di
import com.divadventure.data.AdventureApi
import com.divadventure.data.InterestsApi
import com.divadventure.data.Repository.AdventureRepository
import com.divadventure.data.Repository.RequestsRepository
import com.divadventure.data.Repository.UsersRepository
import com.divadventure.data.RequestsApi
import com.divadventure.data.SharedService
import com.divadventure.data.UsersApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import timber.log.Timber
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class) // Fix scope to Application-wide
object NetworkModule {
@Provides
@Singleton
fun provideAuthService(retrofit: Retrofit): SharedService {
Timber.Forest.d("Creating SharedService instance from Retrofit")
return retrofit.create(SharedService::class.java)
}
@Provides
@Singleton
fun provideAdventureApi(retrofit: Retrofit): AdventureApi {
Timber.Forest.d("Creating AdventureApi instance from Retrofit")
return retrofit.create(AdventureApi::class.java)
}
@Provides
@Singleton
fun provideAdventureRepository(
adventureApi: AdventureApi,
sharedPrefs: SharedPrefs
): AdventureRepository {
Timber.Forest.d("Providing AdventureRepository instance")
return AdventureRepository(adventureApi, sharedPrefs)
}
// New additions for FriendsApi and FriendsRepository
@Provides
@Singleton
fun provideFriendsApi(retrofit: Retrofit): UsersApi {
Timber.Forest.d("Creating FriendsApi instance from Retrofit")
return retrofit.create(UsersApi::class.java)
}
@Provides
@Singleton
fun provideInterestsApi(retrofit: Retrofit): InterestsApi {
return retrofit.create(InterestsApi::class.java)
}
@Provides
@Singleton
fun provideFriendsRepository(
usersApi: UsersApi,
sharedPrefs: SharedPrefs
): UsersRepository {
Timber.Forest.d("Providing FriendsRepository instance")
return UsersRepository(usersApi, sharedPrefs)
}
@Provides
@Singleton
fun provideRequestsRepository(
requestsApi: RequestsApi, sharedPrefs: SharedPrefs
): RequestsRepository {
Timber.Forest.d("Providing RequestsRepository instance")
return RequestsRepository(sharedPrefs, requestsApi)
}
@Provides
@Singleton
fun provideRequestsApi(retrofit: Retrofit): RequestsApi {
Timber.Forest.d("Creating RequestsApi instance from Retrofit")
return retrofit.create(RequestsApi::class.java)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\di\SharedPreferencesModule.kt
```kt
package com.divadventure.di
import android.content.Context
import android.content.SharedPreferences
import timber.log.Timber
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object SharedPreferencesModule {
@Provides
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
Timber.d("Providing SharedPreferences instance")
return context.getSharedPreferences("my_app_prefs", Context.MODE_PRIVATE)
}
@Provides
@Singleton
fun provideSharedPrefs(sharedPreferences: SharedPreferences): SharedPrefs {
Timber.d("Providing SharedPrefs instance")
return SharedPrefs(sharedPreferences)
}
}
class SharedPrefs @Inject constructor(private val sharedPreferences: SharedPreferences) {
fun setString(key: String, value: String?) {
Timber.d("Setting String value for key: $key, value: $value")
sharedPreferences.edit().putString(key, value).apply()
}
fun setLong(key: String, value: Long) {
Timber.d("Setting Long value for key: $key, value: $value")
sharedPreferences.edit().putLong(key, value).apply()
}
fun getLong(key: String, defaultValue: Long): Long {
val value = sharedPreferences.getLong(key, defaultValue)
Timber.d("Getting Long value for key: $key, value: $value")
return value
}
fun getString(key: String): String? {
val value = sharedPreferences.getString(key, null)
Timber.d("Getting String value for key: $key, value: $value")
return value
}
fun setInt(key: String, value: Int) {
Timber.d("Setting Int value for key: $key, value: $value")
sharedPreferences.edit().putInt(key, value).apply()
}
fun getInt(key: String, defaultValue: Int): Int {
val value = sharedPreferences.getInt(key, defaultValue)
Timber.d("Getting Int value for key: $key, value: $value")
return value
}
fun setBoolean(key: String, value: Boolean) {
Timber.d("Setting Boolean value for key: $key, value: $value")
sharedPreferences.edit().putBoolean(key, value).apply()
}
fun getBoolean(key: String, defaultValue: Boolean): Boolean {
val value = sharedPreferences.getBoolean(key, defaultValue)
Timber.d("Getting Boolean value for key: $key, value: $value")
return value
}
}
public object AuthPrefs {
const val VERIFICATION_PASSED_BOOLEAN = "verification_passed"
// const val SEND_OTP_TIME_MILLIS = "send_otp_time_millis"
// const val ONBOARD_PASSED_BOOLEAN = "onboard_passed"
// const val FIRST_TIME_BOOLEAN = "first_time"
}
public object UserPrefs {
const val KEY_ID = "id"
const val KEY_AVATAR = "avatar"
const val KEY_FIRST_NAME = "first_name"
const val KEY_LAST_NAME = "last_name"
const val KEY_USERNAME = "username"
const val KEY_EMAIL = "email"
const val KEY_BIRTH_DATE = "birth_date"
const val KEY_BIO = "bio"
const val KEY_REFRESH_TOKEN = "refresh_token"
const val KEY_TOKEN = "token"
const val KEY_PLATFORM = "platform_token"
const val KEY_TOKEN_EXPIRES_AT = "token_expires_at"
const val KEY_REFRESH_TOKEN_EXPIRES_AT = "refresh_token_expires_at"
const val KEY_LOCATION = "location"
const val KEY_BIO_PRIVACY = "bio_privacy"
const val KEY_LOCATION_PRIVACY = "location_privacy"
const val KEY_BIRTH_DATE_PRIVACY = "birth_date_privacy"
const val KEY_FRIENDS_PRIVACY = "friends_privacy"
const val KEY_ADVENTURES_PRIVACY = "adventures_privacy"
}
/**
// Example of injection in a class
import javax.inject.Inject
class MyDataRepository @Inject constructor(private val sharedPrefs: SharedPrefs) {
fun saveUserName(username: String) {
sharedPrefs.setString("username", username)
}
fun getUserName(): String {
return sharedPrefs.getString("username", "")
}
}*/
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\models\AdventureModels.kt
```kt
package com.divadventure.domain.models
import com.google.gson.annotations.SerializedName
data class CreateAdventureResponse(
val data: Adventure
)
data class CreateAdventureRequest(
val title: String,
val description: String,
val banner: String,
@SerializedName("privacy_type")
val privacyType: Int,
@SerializedName("starts_at")
val startsAt: String,
@SerializedName("ends_at")
val endsAt: String,
val deadline: String,
@SerializedName("join_request_needed")
val joinRequestNeeded: Boolean,
@SerializedName("location_attributes")
val locationAttributes: LocationAttributes,
val interests: List<String>
)
data class LocationAttributes(
val lng: String,
val lat: String
)
data class AdventuresResponse(
@SerializedName("adventures") val adventures: List<Adventure>,
@SerializedName("meta") val meta: Meta
)
data class Adventure(
@SerializedName("id") val id: String,
@SerializedName("title") val title: String,
@SerializedName("description") val description: String,
@SerializedName("banner") val banner: String,
@SerializedName("starts_at") val startsAt: String,
@SerializedName("ends_at") val endsAt: String,
@SerializedName("privacy_type") val privacyType: String,
@SerializedName("deadline") val deadline: String,
@SerializedName("join_request_needed") val joinRequestNeeded: Boolean,
@SerializedName("owner_id") val ownerId: String,
@SerializedName("state") val state: String,
@SerializedName("current_user_adventurer_id") val currentUserAdventurerId: String?,
@SerializedName("adventure_request") val adventureRequest: List<AdventureRequest>?,
@SerializedName("adventurers_count") val adventurersCount: Int,
@SerializedName("adventurers") val adventurers: List<Adventurer>,
@SerializedName("interests") val interests: List<Interest>,
@SerializedName("location") val location: Location,
@Transient var adventureType: AdventureType? = null
)
enum class AdventureType {
Manage, Join, Going, Pending, Leave
}
data class Interest(
@SerializedName("id") val id: String,
@SerializedName("name") val name: String,
@SerializedName("created_at") val createdAt: String?,
@SerializedName("updated_at") val updatedAt: String?
)
// data class AdventureRequest(
// @SerializedName("adventure_request") val adventureRequest: List<Request>
// ) {
data class AdventureRequest(
@SerializedName("id") val id: String,
@SerializedName("user") val user: User,
@SerializedName("adventurer") val adventurer: String?, // Nullable since the value is "null"
@SerializedName("adventure") val adventure: Adventure,
@SerializedName("accepted_at") val acceptedAt: String?, // Nullable since it can be null
@SerializedName("declined_at") val declinedAt: String?, // Nullable since it can be null
@SerializedName("type") val type: String,
@SerializedName("status") val status: String,
@SerializedName("created_at") val createdAt: String
) {
data class User(
@SerializedName("id") val id: String,
@SerializedName("avatar") val avatar: String,
@SerializedName("username") val username: String,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String
)
data class Adventure(
@SerializedName("id") val id: String,
@SerializedName("banner") val banner: String,
@SerializedName("title") val title: String
)
}
// }
data class Adventurer(
@SerializedName("id") val id: String,
@SerializedName("role_name") val roleName: String,
@SerializedName("role_id") val roleId: String,
@SerializedName("user_id") val userId: String,
@SerializedName("avatar") val avatar: String,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("username") val username: String,
@SerializedName("status_with_user") val statusWithUser: String
)
data class Location(
@SerializedName("id") val id: String,
@SerializedName("locationable_type") val locationableType: String,
@SerializedName("locationable_id") val locationableId: String,
@SerializedName("lng") val lng: Double,
@SerializedName("lat") val lat: Double
)
data class Meta(
@SerializedName("current_page") val currentPage: Int,
@SerializedName("next_page") val nextPage: Int?,
@SerializedName("prev_page") val prevPage: Int?,
@SerializedName("total_pages") val totalPages: Int,
@SerializedName("total_entries") val totalEntries: Int
)
data class Filters(
var interests: List<Interest>? = null,
var locationLAt: Double? = null,
var locationLng: Double? = null,
var startDate: String? = null,
var endDate: String? = null,
var state: String? = null,
var page: Int? = null,
var perPage: Int? = null,
var orderBy: String? = null
)
data class InterestsResponse(
@SerializedName("interests") val interests: List<Interest>,
@SerializedName("meta") val meta: Meta
)
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\models\AuthModels.kt
```kt
package com.divadventure.domain.models
import com.google.gson.annotations.SerializedName
data class SignupRequest(
val email: String,
val password: String,
val password_confirmation: String,
val platform: String = "Android" // Default value for the platform
)
data class Message(
@SerializedName("message") val message: String
)
data class SignUpResponse(
@SerializedName("data") val data: UserData
)
data class UserData(
@SerializedName("id") val id: String,
@SerializedName("avatar") val avatar: String?,
@SerializedName("first_name") val firstName: String?,
@SerializedName("last_name") val lastName: String?,
@SerializedName("username") val username: String?,
@SerializedName("email") val email: String,
@SerializedName("birthdate") val birthdate: String?,
@SerializedName("bio") val bio: String?,
@SerializedName("location") val location: Location?,
@SerializedName("privacy_settings") val privacySettings: PrivacySettings,
@SerializedName("current_access_token") val currentAccessToken: CurrentAccessToken
)
data class PrivacySettings(
@SerializedName("bio") val bio: String,
@SerializedName("birthdate") val birthdate: String,
@SerializedName("adventures") val adventures: String,
@SerializedName("friends") val friends: String,
@SerializedName("location") val location: String
)
data class CurrentAccessToken(
@SerializedName("platform") val platform: String,
@SerializedName("token") val token: String,
@SerializedName("refresh_token") val refreshToken: String,
@SerializedName("expires_at") val expiresAt: String,
@SerializedName("refresh_token_expires_at") val refreshTokenExpiresAt: String
)
data class ReqOnboard(
@SerializedName("first_name")
val firstName: String,
@SerializedName("last_name")
val lastName: String,
@SerializedName("username")
val username: String
)
data class ReqVerifyEmail(
@SerializedName("token")
val token: String
)
data class ResVerifyEmail(
@SerializedName("email")
val email: String
)
data class ReqLogin(
val email: String,
val password: String,
val platform: String = "Android"
)
data class ReqVerifyResetPasswordToken(val token: String)
data class ResVerifyResetPasswordToken(val email: String)
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\models\Requests.kt
```kt
package com.divadventure.domain.models
import com.google.gson.annotations.SerializedName
data class AdventureRequestsResponse(
@SerializedName("requests") val requests: List<Request>,
@SerializedName("meta") val meta: Meta
)
data class Request(
@SerializedName("id") val id: String,
@SerializedName("user") val user: UserSpecs,
@SerializedName("adventurer") val adventurer: AdventurerSpecs?, // Make this nullable
@SerializedName("adventure") val adventure: AdventureSpecs,
@SerializedName("accepted_at") val acceptedAt: String?, // nullable
@SerializedName("declined_at") val declinedAt: String?, // nullable
@SerializedName("type") val type: String,
@SerializedName("status") val status: String,
@SerializedName("created_at") val createdAt: String
)
data class UserSpecs(
@SerializedName("id") val id: String,
@SerializedName("avatar") val avatar: String,
@SerializedName("username") val username: String,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String
)
data class AdventurerSpecs(
@SerializedName("id") val id: String,
@SerializedName("avatar") val avatar: String,
@SerializedName("username") val username: String,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String
)
data class AdventureSpecs(
@SerializedName("id") val id: String,
@SerializedName("banner") val banner: String,
@SerializedName("title") val title: String
)
data class Participant(
@SerializedName("id") val id: String,
// "participant" or "owner"
@SerializedName("role_name") val roleName: String,
@SerializedName("role_id") val roleId: String,
@SerializedName("user_id") val userId: String,
// "friends" or "not friends"
@SerializedName("avatar") val avatar: String,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("username") val username: String,
@SerializedName("status_with_user") val statusWithUser: String
)
data class UsersAdventureRequestResponse(
@SerializedName("users") val users: List<AdventureUser>,
@SerializedName("meta") val meta: Meta
)
data class AdventureUser(
@SerializedName("id") val id: String,
@SerializedName("avatar") val avatar: String,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("username") val username: String,
@SerializedName("birthdate") val birthdate: String?,
@SerializedName("bio") val bio: String?,
@SerializedName("location") val location: Location?, // (nullable)
@SerializedName("status_with_user") val statusWithUser: String,
@SerializedName("adventure_request") val adventureRequest: List<AdventureRequest>
)
data class AdventureRequestResponse(
@SerializedName("data")
val data: List<Participant>
)
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\models\UsersModels.kt
```kt
package com.divadventure.domain.models
import com.google.gson.annotations.SerializedName
data class FriendsResponse(
@SerializedName("friends") val friends: List<Friend>,
@SerializedName("meta") val meta: Meta
)
data class Friend(
@SerializedName("id") val id: String,
@SerializedName("avatar") val avatar: String,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("username") val username: String,
@SerializedName("birthdate") val birthdate: String?, // Nullable since it can be `null`
@SerializedName("status_with_user") val statusWithUser: String
)
data class UserProfile(
@SerializedName("id") val id: String,
@SerializedName("avatar") val avatar: String?,
@SerializedName("first_name") val firstName: String,
@SerializedName("last_name") val lastName: String,
@SerializedName("username") val username: String,
@SerializedName("birthdate") val birthdate: String?,
@SerializedName("bio") val bio: String?,
@SerializedName("location") val location: Location?,
@SerializedName("status_with_user") val statusWithUser: String?,
@SerializedName("current_access_token") val currentAccessToken: CurrentAccessToken?
)
data class UsersData(
val data: UserProfile
)
/*
data class Meta(
@SerializedName("current_page") val currentPage: Int,
@SerializedName("next_page") val nextPage: Int?, // Nullable since it can be `null`
@SerializedName("prev_page") val prevPage: Int?, // Nullable since it can be `null`
@SerializedName("total_pages") val totalPages: Int,
@SerializedName("total_entries") val totalEntries: Int
)
*/
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\usecase\AdventuresUseCase.kt
```kt
package com.divadventure.domain.usecase
import com.divadventure.data.Repository.AdventureRepository
import com.divadventure.di.SharedPrefs
import com.divadventure.di.UserPrefs.KEY_ID
import com.divadventure.domain.models.Adventure
import com.divadventure.domain.models.AdventureType
import com.divadventure.domain.models.AdventuresResponse
import com.divadventure.domain.models.CreateAdventureRequest
import com.divadventure.domain.models.CreateAdventureResponse
import com.divadventure.domain.models.Filters
import com.divadventure.domain.models.LocationAttributes
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
import javax.inject.Inject
class AdventuresUseCase @Inject constructor(
private val adventureRepository: AdventureRepository, private val sharedPrefs: SharedPrefs
) {
// Use case for searching adventures
// This use case takes a search query as input and returns a list of adventures that match the query.
// If no adventures are found, it returns a failure result with a NoSuchElementException.
// If an exception occurs during the search, it returns a failure result with the exception.
// If the search query is empty, it returns a failure result with an IllegalArgumentException.
// Input: searchQuery
// Output: Result<List<Adventure>>
suspend fun search(
searchQuery: String?, page: Int, filters: Filters
): Result<AdventuresResponse> {
return try {
val adventures = adventureRepository.searchAdventures(
searchQuery, page, filters
)
// if (adventures.adventures.isEmpty()) {
// Result.failure(NoSuchElementException("No adventures found for the query: $searchQuery"))
// }
// else {
Result.success(adventures)
// }
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getAllAdventures(page: Int): Result<AdventuresResponse> {
return try {
val adventures = adventureRepository.getAllAdventures(page)
return Result.success(adventures)
} catch (e: Exception) {
return Result.failure(e)
}
}
suspend fun fetchGroupedAdventures(
group: String, page: Int, startDate: String? = null, endDate: String? = null
): Result<AdventuresResponse> {
return try {
val adventuresResponse = adventureRepository.getGroupedAdventures(group, page)
Result.success(adventuresResponse)
} catch (e: Exception) {
Result.failure(e)
}
} // Close 'fetchGroupedAdventures'
suspend fun getMyAdventures(): Result<AdventuresResponse> {
return try {
val adventures = adventureRepository.getMyAdventures()
Result.success(adventures)
} catch (e: Exception) {
Result.failure(e)
}
} // Close 'getMyAdventures'
suspend fun getElseAdventures(profileId: String): Result<AdventuresResponse> {
return try {
val adventures = adventureRepository.getElseAdventures(profileId)
Result.success(adventures)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun fetchCalendarAdventures(
group: String, startDate: String, endDate: String
): Result<AdventuresResponse> {
return try {
val adventures = adventureRepository.getCalendarAdventures(group, startDate, endDate)
Result.success(adventures)
} catch (e: Exception) {
Result.failure(e)
}
// Close 'addFilters'
} // Close 'AdventuresUseCase'
suspend fun createAdventure(
title: String,
description: String,
banner: String,
privacyType: Int,
startsAt: String,
endsAt: String,
deadline: String,
joinRequestNeeded: Boolean,
locationAttributes: LocationAttributes,
interests: List<String> = mutableListOf<String>()
): Result<CreateAdventureResponse> {
return try {
val request = createAdventureRequestFromParams(
title, description, banner, privacyType,
startsAt, endsAt, deadline,
joinRequestNeeded, locationAttributes, interests
)
val response = adventureRepository.createNewAdventure(request)
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Converts adventure parameters into a CreateAdventureRequest model with proper date formatting
*/
private fun createAdventureRequestFromParams(
title: String,
description: String,
banner: String,
privacyType: Int,
startsAt: String,
endsAt: String,
deadline: String,
joinRequestNeeded: Boolean,
locationAttributes: LocationAttributes,
interests: List<String>
): CreateAdventureRequest {
// Convert startsAt and endsAt strings to Instant objects
val formatter = DateTimeFormatter.ofPattern("MMM dd yyyy - h:mm a", Locale.ENGLISH)
val startsAtInstant = LocalDateTime.parse(startsAt, formatter)
.atZone(ZoneId.systemDefault())
.toInstant()
val endsAtInstant = LocalDateTime.parse(endsAt, formatter)
.atZone(ZoneId.systemDefault())
.toInstant()
// Create formatter that can handle dates without leading zeros
val dateFormatter = DateTimeFormatter.ofPattern("yyyy-M-d", Locale.ENGLISH)
val deadlineInstant = try {
// Try standard ISO format first
LocalDate.parse(deadline).atStartOfDay(ZoneId.systemDefault()).toInstant()
} catch (e: Exception) {
// If that fails, try with custom formatter for dates without leading zeros
LocalDate.parse(deadline, dateFormatter).atStartOfDay(ZoneId.systemDefault()).toInstant()
}
return CreateAdventureRequest(
title = title,
description = description,
banner = banner,
privacyType = privacyType,
startsAt = startsAtInstant.toString(),
endsAt = endsAtInstant.toString(),
deadline = deadlineInstant.toString(),
joinRequestNeeded = joinRequestNeeded,
locationAttributes = locationAttributes,
interests = interests
)
}
/**
* Data class to hold adventure parameters with formatted date strings
*/
data class AdventureParams(
val title: String,
val description: String,
val banner: String,
val privacyType: Int,
val startsAt: String,
val endsAt: String,
val deadline: String,
val joinRequestNeeded: Boolean,
val locationAttributes: LocationAttributes,
val interests: List<String>
)
fun checkAdventureType(adventure: Adventure): AdventureType {
return when {
// adventureIsJoinable(adventure) -> AdventureType.Join
adventureIsGoing(adventure) -> AdventureType.Going
adventureIsLeaveAble(adventure) -> AdventureType.Leave
adventureIsManageable(adventure) -> AdventureType.Manage
adventureIsPendingable(adventure) -> AdventureType.Pending
else -> {
AdventureType.Join
// Timber.e(adventure.toString())
// throw IllegalStateException("Unknown Adventure state")
}
}
}
private fun adventureIsPendingable(adventure: Adventure): Boolean {
return adventure.adventureRequest != null && adventure.adventureRequest.isNotEmpty() && adventure.adventureRequest.any {
it.status.equals(
"PENDING",
true
)
} && adventure.adventureRequest.any {
it.type.equals(
"JoinRequest",
true
)
}
}
private fun adventureIsManageable(adventure: Adventure): Boolean {
return adventure.ownerId.equals(sharedPrefs.getString(KEY_ID) ?: "", true) || (
adventure.adventurersCount > 1 && adventure.adventurers.any {
it.roleName.equals("participant", true) && it.userId == sharedPrefs.getString(
KEY_ID
)
})
}
private fun adventureIsLeaveAble(adventure: Adventure): Boolean {
return adventure.adventureRequest != null &&
adventure.adventureRequest.isNotEmpty() &&
adventure.adventureRequest.any {
it.status.equals(
"PENDING",
true
)
} && adventure.adventureRequest.any {
it.type.equals(
"accepted",
true
)
}
}
private fun adventureIsGoing(adventure: Adventure): Boolean {
return adventure.adventureRequest != null
&& adventure.adventureRequest.isNotEmpty()
&& adventure.adventureRequest.any {
it.status.equals(
"pending",
true
)
}
&& adventure.adventureRequest.any {
it.type.equals(
"InviteRequest",
true
)
}
}
private fun adventureIsJoinable(adventure: Adventure): Boolean {
return adventure.adventureRequest != null
&& adventure.adventureRequest.isNotEmpty()
&& adventure.adventureRequest.any {
it.status.equals(
"PENDING",
true
)
}
&& adventure.adventureRequest.any {
it.type.equals(
"accepted",
true
)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\usecase\CalendarUseCase.kt
```kt
package com.divadventure.domain.usecase
import com.divadventure.data.Repository.AdventureRepository
import com.divadventure.di.SharedPrefs
import javax.inject.Inject
class CalendarUseCase @Inject constructor(
private val adventureRepository: AdventureRepository,
private val sharedPrefs: SharedPrefs
) {
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\usecase\InterestsUseCase.kt
```kt
package com.divadventure.domain.usecase
import com.divadventure.data.Repository.InterestsRepository
import com.divadventure.domain.models.InterestsResponse
import javax.inject.Inject
class InterestsUseCase @Inject constructor(
private val interestsRepository: InterestsRepository
) {
suspend fun fetchInterests(): Result<InterestsResponse> {
return try {
val interests = interestsRepository.getInterests()
Result.success(interests)
} catch (exception: Exception) {
Result.failure(exception)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\usecase\LocationsUseCase.kt
```kt
package com.divadventure.domain.usecase
import com.google.android.gms.maps.model.LatLng
import com.google.android.libraries.places.api.model.AutocompletePrediction
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.api.model.RectangularBounds
import com.google.android.libraries.places.api.net.FetchPlaceRequest
import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest
import com.google.android.libraries.places.api.net.PlacesClient
import kotlinx.coroutines.tasks.await
import timber.log.Timber
import javax.inject.Inject
class LocationsUseCase @Inject constructor(private val placesClient: PlacesClient) {
suspend fun predictLocations(text: String): List<AutocompletePrediction> {
return try {
val request = FindAutocompletePredictionsRequest.builder()
.setQuery(text)
.build()
val response = placesClient.findAutocompletePredictions(request).await()
response.autocompletePredictions
} catch (exception: Exception) {
Timber.e("Error fetching location predictions: $exception")
emptyList() // Return empty list in case of failure
}
}
suspend fun goLocation(placeId: String): Place? {
return try {
val placeFields = listOf(Place.Field.LAT_LNG, Place.Field.NAME, Place.Field.ADDRESS)
val request = FetchPlaceRequest.builder(placeId, placeFields).build()
val response = placesClient.fetchPlace(request).await()
response.place // Return the location
} catch (exception: Exception) {
Timber.e("Error fetching place details: $exception")
null // Return null in case of failure
}
}
suspend fun goLocation(location: LatLng): Place? {
return try {
// Step 1: Use findAutocompletePredictions to get a placeId from LatLng.
// We'll use the coordinates as the query string.
// Location biasing helps to narrow down results to the specific point.
val autocompleteRequest = FindAutocompletePredictionsRequest.builder()
.setQuery("${location.latitude},${location.longitude}") // Use coordinates as query
// Bias results to a small rectangular area around the LatLng for better accuracy.
// You might need to add: import com.google.android.libraries.places.api.model.RectangularBounds
.setLocationBias(RectangularBounds.newInstance(
LatLng(location.latitude - 0.001, location.longitude - 0.001), // SW corner
LatLng(location.latitude + 0.001, location.longitude + 0.001) // NE corner
))
.build()
val predictionResponse = placesClient.findAutocompletePredictions(autocompleteRequest).await()
if (predictionResponse.autocompletePredictions.isNotEmpty()) {
// Take the first prediction, assuming it's the most relevant for the coordinates.
val placeId = predictionResponse.autocompletePredictions[0].placeId
// Step 2: Fetch place details using the obtained placeId.
val placeFields = listOf(Place.Field.LAT_LNG, Place.Field.NAME, Place.Field.ADDRESS)
val fetchPlaceRequest = FetchPlaceRequest.builder(placeId, placeFields).build()
val fetchPlaceResponse = placesClient.fetchPlace(fetchPlaceRequest).await()
fetchPlaceResponse.place
} else {
Timber.w("No place predictions found for LatLng: $location")
null
}
} catch (exception: Exception) {
Timber.e("Error in goLocation(LatLng) for $location: $exception")
null // Return null in case of failure
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\usecase\NotificationsUseCase.kt
```kt
package com.divadventure.domain.usecase
class NotificationsUseCase {
fun sendNotification(title: String, message: String): Boolean {
// Logic to send a notification
println("Notification sent: Title: $title, Message: $message")
return true
}
fun scheduleNotification(title: String, message: String, delayInMillis: Long): Boolean {
// Logic to schedule a notification
println("Notification scheduled: Title: $title, Message: $message, Delay: $delayInMillis ms")
return true
}
fun cancelNotification(notificationId: Int): Boolean {
// Logic to cancel a notification
println("Notification with ID $notificationId cancelled")
return true
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\usecase\RequestsUseCase.kt
```kt
package com.divadventure.domain.usecase
import com.divadventure.data.Repository.RequestsRepository
import com.divadventure.domain.models.AdventureRequestResponse
import com.divadventure.domain.models.AdventureRequestsResponse
import com.divadventure.domain.models.UsersAdventureRequestResponse
import jakarta.inject.Inject
class RequestsUseCase @Inject constructor(
private val requestsRepository: RequestsRepository
) {
suspend fun fetchAdventureRequests(adventureId: String): Result<AdventureRequestsResponse> {
return try {
val requests = requestsRepository.fetchAllAdventureRequests(adventureId)
Result.success(requests)
} catch (exception: Exception) {
Result.failure(exception)
}
}
suspend fun fetchUserRequests(
adventureId: String,
userId: String
): Result<UsersAdventureRequestResponse> {
return try {
val userDataResult = requestsRepository.fetchUserAdventureRequests(adventureId, userId)
Result.success(userDataResult)
} catch (exception: Exception) {
Result.failure(exception)
}
}
suspend fun acceptJoinRequest(adventureId: String, requestId: String): Result<com.divadventure.domain.models.AdventureRequestResponse> {
return try {
val response = requestsRepository.acceptJoinRequest(adventureId, requestId)
Result.success(response)
} catch (exception: Exception) {
Result.failure(exception)
}
}
suspend fun declineJoinRequest(adventureId: String, requestId: String): Result<com.divadventure.domain.models.AdventureRequestResponse> {
return try {
val response = requestsRepository.declineJoinRequest(adventureId, requestId)
Result.success(response)
} catch (exception: Exception) {
Result.failure(exception)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\usecase\TasksUseCase.kt
```kt
package com.divadventure.domain.usecase
class TasksUseCase {
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\domain\usecase\UsersUseCase.kt
```kt
package com.divadventure.domain.usecase
import com.divadventure.data.Repository.UsersRepository
import com.divadventure.domain.models.FriendsResponse
import com.divadventure.domain.models.UsersData
import javax.inject.Inject
class UsersUseCase @Inject constructor(
private val usersRepository: UsersRepository
) {
/**
* Retrieves the list of friends.
* - If successful, returns `Result.success` with the list of friends.
* - If an error occurs, returns a `Result.failure` with the exception.
*/
suspend fun getFriends(): Result<FriendsResponse> {
return try {
val friendsResponse = usersRepository.getFriends()
if (friendsResponse.friends.isNullOrEmpty()) {
Result.failure(NoSuchElementException("No friends found."))
} else {
Result.success(friendsResponse)
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getElseFriends(profileId: String): Result<FriendsResponse> {
return try {
val friendsResponse = usersRepository.getElseFriends(profileId)
if (friendsResponse.friends.isNullOrEmpty()) {
Result.failure(NoSuchElementException("No friends found."))
} else {
Result.success(friendsResponse)
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getUserData(userId: String): Result<UsersData> {
return try {
val userData = usersRepository.getUserData(userId)
Result.success(userData)
} catch (e: Exception) {
Result.failure(e)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\AddShared.kt
```kt
package com.divadventure.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.divadventure.R
import com.divadventure.ui.screens.main.home.notifications.search.filter.SelectionImage
@Composable
fun TitleCompose(
text: String, isMandatory: Boolean = false
) {
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(
top = 20.dp, bottom = 10.dp, start = 20.dp, end = 20.dp
)
) {
Text(
modifier = Modifier.padding(
), text = text, style = TextStyle(
color = Color.Black,
fontSize = with(LocalDensity.current) { 13.dp.toSp() },
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.W600
)
)
if (isMandatory) {
AsteriskCompose()
}
}
}
@Composable
fun PersonalInfoTextField(
title: String,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
minLines: Int = 1,
mandatory: Boolean = false,
hint: String = ""
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(
start = 20.dp, top = 20.dp
)
) {
Text(
modifier = Modifier, text = title, style = TextStyle(
color = Color.Black,
platformStyle = PlatformTextStyle(includeFontPadding = false),
fontSize = with(LocalDensity.current) { 9.dp.toSp() },
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.W600,
)
)
if (mandatory) AsteriskCompose()
}
SimpleTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
descLines = minLines,
hint = hint
)
}
}
@Composable
fun SimpleTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
descLines: Int = 1,
hint: String = "",
) {
BasicTextField(
value = value,
minLines = descLines,
onValueChange = onValueChange,
modifier = modifier
.fillMaxWidth()
.padding(20.dp),
textStyle = TextStyle(
fontWeight = FontWeight.W400,
fontSize = with(LocalDensity.current) { 13.dp.toSp() },
color = Color.Black
),
interactionSource = remember { MutableInteractionSource() },
decorationBox = { innerTextField ->
Box(
modifier = Modifier
.background(color = Color.Transparent)
.padding(PaddingValues(0.dp))
) {
if (value.isEmpty()) {
Text(
text = hint, style = TextStyle(
fontWeight = FontWeight.W400,
fontSize = with(LocalDensity.current) { 13.dp.toSp() },
color = Color(0xFF8A8A8E)
)
)
}
innerTextField()
}
})
}
@Composable
fun WhiteRoundedCornerFrame(
modifier: Modifier = Modifier, content: @Composable () -> Unit
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.padding(horizontal = 20.dp)
.fillMaxWidth()
.background(color = Color.White, shape = RoundedCornerShape(8.dp))
) {
content()
}
}
@Composable
fun ItemTextClickIcon(
title: String,
value: String? = null,
isMandatory: Boolean = false,
onClick: () -> Unit = {},
content: @Composable () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(
enabled = true, onClick = onClick
)
.padding(start = 20.dp, top = 20.dp, bottom = 20.dp, end = 20.dp)
) {
Row(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(end = 4.dp), // Add some padding if value is present
text = title,
style = TextStyle(
color = Color.Black,
fontSize = with(LocalDensity.current) { 13.dp.toSp() },
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.W600
)
)
if (isMandatory) {
AsteriskCompose()
}
if (!value.isNullOrEmpty()) {
Text(
text = value,
style = TextStyle(
color = Color.Gray, // Different color for the value
fontSize = with(LocalDensity.current) { 13.dp.toSp() },
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.W400
),
modifier = Modifier.padding(start = 8.dp) // Space between title and value
)
}
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.align(Alignment.CenterVertically)
) {
content()
}
}
}
@Composable
fun MandatoryInterestsComposable(
title: String,
value: String? = null,
onClick: () -> Unit = {}
) {
ItemTextClickIcon(
isMandatory = true,
title = title,
value = value, // Pass the value to ItemTextClickIcon
content = {
Row(
modifier = Modifier
.padding()
.clickable(onClick = onClick),
verticalAlignment = Alignment.CenterVertically
) {
// The static "Label" Text is removed as ItemTextClickIcon now handles the value display.
// If no value is provided to ItemTextClickIcon, nothing will be shown there,
// which is acceptable. If a placeholder is needed when value is null/empty,
// ItemTextClickIcon's logic would need to be adjusted, or a default value passed here.
// For now, keeping it clean.
Icon(
modifier = Modifier.padding(start = 10.dp), // Keep padding for the icon
painter = painterResource(id = R.drawable.right_chevron),
tint = Color(0x3C3C4399).copy(alpha = 0.6f),
contentDescription = ""
)
}
})
}
@Composable
fun SelectionList(defaultIndex: Int = 2, onSelectItem: (Int) -> Unit) {
// State to track the selected index
var selectedIndex by remember { mutableIntStateOf(defaultIndex) } // Default selected item (index 2)
val items = listOf(
SelectionItem("Invite Only", isSelected = false),
SelectionItem("Friends", isSelected = false),
SelectionItem("Public", isSelected = false),
)
LazyColumn(
userScrollEnabled = false, modifier = Modifier.height((items.size * 65).dp)
) {
items(items.size) { index ->
SelectionRow(
text = items[index].text,
isSelected = index == selectedIndex, // Only the selected index is true
selectionImage = { isSelected -> SelectionImage(isSelected) },
onClick = {
// Update selected index
selectedIndex = index
onSelectItem.invoke(index)
})
SortDivider() // Add divider between items
}
}
}
// Define the data model for the list items
data class SelectionItem(
val text: String, val isSelected: Boolean
)
@Composable
fun AsteriskCompose() {
Box(modifier = Modifier.padding(5.dp)) {
Icon(
modifier = Modifier
.size(7.5.dp)
.align(Alignment.Center),
painter = painterResource(id = R.drawable.ic_asterisk),
contentDescription = "Asterisk Icon",
tint = Color(0xFFFF2D55)
)
}
}
@Composable
fun ChangerButton(
modifier: Modifier = Modifier,
isActive: MutableState<Boolean>,
text: String,
deActiveTextColor: Color,
deActiveButtonColor: Color,
activeTextColor: Color,
activeButtonColor: Color,
onClick: () -> Unit
) {
TextButton(
shape = RoundedCornerShape(4.dp),
modifier = modifier
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = if (isActive.value) activeButtonColor else deActiveButtonColor
),
onClick = onClick) {
Box(
modifier = Modifier.padding(vertical = 10.dp),
contentAlignment = Alignment.Center
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = text,
style = TextStyle(
fontSize = with(LocalDensity.current) { 13.dp.toSp() },
color = if (isActive.value) activeTextColor else deActiveTextColor
)
)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\AuthShared.kt
```kt
package com.divadventure.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import com.divadventure.R
@Composable
fun AuthTextField(
modifier: Modifier,
hint: String,
text: MutableState<String>,
onValueChange: (String) -> Unit,
explain: String,
essential: Boolean = false,
isPassword: Boolean = false,
isEmail: Boolean = false,
isNormalText: Boolean = false,
enabled: Boolean = true
) {
Column(
modifier = modifier
) {
if (hint.isNotEmpty()) {
Row(
Modifier.padding(20.dp, 0.dp, 0.dp, 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(hint, color = Color(0xff1C1C1E))
if (essential) {
// Text("*", style = TextStyle(color = Color.Red))
Image(
modifier = Modifier.size(7.5.dp),
contentDescription = "asterisk_essential_field",
imageVector = ImageVector.vectorResource(
R.drawable.ic_asterisk
),
colorFilter = ColorFilter.tint(Color(0xffFF2D55))
)
}
}
}
val originalColor = Color(0x3C3C4399)
val darkenPercentage = 0.4f // 20% darker
val darkerColor = lerp(originalColor, Color.Black, darkenPercentage)
Box(modifier = Modifier.padding(20.dp, 0.dp, 0.dp, 0.dp)) {
if (text.value.isEmpty()) {
Text(
text = explain, color = darkerColor, style = TextStyle(
fontSize = 14.sp, fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
BasicTextField(
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 0.dp, 0.dp, 10.dp),
value = text.value,
enabled = enabled,
onValueChange = {
text.value = it
onValueChange(it)
},
textStyle = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = Color(0xff1C1C1E),
fontSize = 17.sp
),
keyboardOptions = when {
isPassword -> {
KeyboardOptions(keyboardType = KeyboardType.Password)
}
isEmail -> {
KeyboardOptions(keyboardType = KeyboardType.Email)
}
else -> {
KeyboardOptions(keyboardType = KeyboardType.Text)
}
},
visualTransformation = when {
isPassword -> {
PasswordVisualTransformation()
}
else -> {
VisualTransformation.None
}
},
)
}
Image(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp, 0.dp, 0.dp, 0.dp)
.height(1.dp)
.background(Color(0xffb9b9bb)),
contentDescription = "horizontal_divider",
imageVector = ImageVector.vectorResource(id = R.drawable.horiontal_line),
)
}
}
class StarVisualTransformation : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return TransformedText(
AnnotatedString("*".repeat(text.length)), OffsetMapping.Identity
)
}
}
@Composable
fun TopSnackBar(
paddingTop: Dp,
title: String,
message: String,
show: Boolean,
onDismiss: () -> Unit
) {
AnimatedVisibility(
modifier = Modifier.zIndex(10f),
visible = show,
enter = fadeIn(animationSpec = tween(durationMillis = 500)),
exit = fadeOut(animationSpec = tween(durationMillis = 500)),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(
16.dp,
16.dp,
16.dp,
16.dp
)
.clip(RoundedCornerShape(23.dp))
.background(Color(0xffFAFAFA))
.statusBarsPadding()
.padding(16.dp)
.zIndex(10f),
contentAlignment = Alignment.Center
) {
Column(modifier = Modifier.padding(5.dp)) {
Row {
Image(
modifier = Modifier.size(15.dp),
contentDescription = "app_logo",
imageVector = ImageVector.vectorResource(id = R.drawable.logo)
)
Text(
modifier = Modifier
.padding(10.dp, 0.dp)
.weight(1f, false)
.fillMaxWidth(),
text = "DivAdventure",
style = TextStyle(
fontSize = 14.sp,
textAlign = TextAlign.Left,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = Color.Black
)
)
Text(
text = "now", style = TextStyle(
fontSize = 13.sp,
color = Color(0xff898989),
fontFamily = FontFamily(Font(R.font.sf_pro)),
)
)
}
Text(
modifier = Modifier.padding(0.dp, 10.dp, 10.dp, 5.dp),
text = title, style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = Color.Black,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
)
Text(
// modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp),
style = TextStyle(
color = Color.Black,
fontFamily = FontFamily(
Font(R.font.sf_pro)
), fontSize = 13.sp
),
text = message,
fontFamily = FontFamily(Font(R.font.sf_pro))
)
}
}
}
}
val CARD_HEIGHT = 55.dp
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\HomeShared.kt
```kt
package com.divadventure.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.rememberAsyncImagePainter
import com.divadventure.R
import com.divadventure.domain.models.Friend
import com.divadventure.ui.screens.main.home.GuideItem
import com.divadventure.ui.screens.main.home.getScreenWidthInDp
import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.compose.HorizontalCalendar
import com.kizitonwose.calendar.compose.rememberCalendarState
import com.kizitonwose.calendar.core.CalendarDay
import com.kizitonwose.calendar.core.yearMonth
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalTime
import java.time.YearMonth
import kotlin.time.ExperimentalTime
/**
* این فایل شامل کامپوزها و UI پراستفاده در قسمت Home است
* */
@Composable
fun SlidingDualToggleButton(
padding: Dp, options: List<String>, onToggle: (Int) -> Unit
) {
var selectedIndex by remember { mutableStateOf(0) }
val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp - (padding.value * 2).dp
val itemWidth = screenWidthDp / options.size
val density = LocalDensity.current.density
val offset = animateDpAsState(
targetValue = (selectedIndex * itemWidth.value).coerceIn(
0f, screenWidthDp.value - itemWidth.value
).dp
)
Row(
modifier = Modifier
.padding(padding)
.fillMaxWidth() // Fill the parent's width
.height(48.dp)
.background(Color(0xFFe5e5ea), RoundedCornerShape(8.dp))
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(2.dp)
.background(Color(0xFFe5e5ea), RoundedCornerShape(8.dp)),
contentAlignment = Alignment.CenterStart
) {
Row {
options.forEachIndexed { index, option ->
Box(
modifier = Modifier.weight(1f, true)
) {
Text(
fontSize = with(LocalDensity.current) { 12.dp.toSp() },
modifier = Modifier.align(Alignment.Center),
text = option,
color = Color(0xff1C1C1E)
)
}
}
}
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(Color.White),
modifier = Modifier
.width(itemWidth) // Use dynamic item width
.fillMaxHeight()
.offset { IntOffset(offset.value.roundToPx(), 0) }
.padding(end = 5.dp)) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center // Centers content both vertically and horizontally
) {
Text(
modifier = Modifier.align(
Alignment.Center
),
text = options[selectedIndex],
color = Color(0xFF1C1C1E),
fontSize = with(LocalDensity.current) { 12.dp.toSp() },
textAlign = TextAlign.Center // Center text horizontally
)
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
selectedIndex = (selectedIndex + 1) % options.size
onToggle(selectedIndex)
})
}
}
}
class TooltipShape(
private val arrowDp: Dp = 20.dp,
private val cornerRadius: Dp = 8.dp,
private val offsetX: Dp = 0.dp,
private val offsetY: Dp = 0.dp
) : Shape {
override fun createOutline(
size: Size, layoutDirection: LayoutDirection, density: Density
): Outline {
val arrowPx = with(density) { arrowDp.toPx() }
val cornerRadiusPx = with(density) { cornerRadius.toPx() }
val path = Path().apply {
// Start from top-left rounded corner
moveTo(cornerRadiusPx, arrowPx)
quadraticBezierTo(
0f, arrowPx, 0f, arrowPx + cornerRadiusPx
) // Top-left corner curve
lineTo(0f, size.height - cornerRadiusPx) // Left edge
quadraticBezierTo(
0f, size.height, cornerRadiusPx, size.height
) // Bottom-left corner curve
lineTo(size.width - cornerRadiusPx, size.height) // Bottom edge
quadraticBezierTo(
size.width, size.height, size.width, size.height - cornerRadiusPx
) // Bottom-right corner curve
lineTo(size.width, arrowPx + cornerRadiusPx) // Right edge
quadraticBezierTo(
size.width, arrowPx, size.width - cornerRadiusPx, arrowPx
) // Top-right corner curve
lineTo(size.width / 2 + arrowPx, arrowPx) // Triangle base right point
lineTo(size.width / 2, 0f) // Triangle tip
lineTo(size.width / 2 - arrowPx, arrowPx) // Triangle base left point
close() // Complete the shape
}
path.translate(Offset(offsetX.value, offsetY.value))
return Outline.Generic(path)
}
}
val calendarCirclesSize = 25.dp
val eventsColors = listOf(
"PastAdventures" to Color(0xFFD7D7DF),
"ActiveAdventures" to Color(0xFFC5EACF),
"UpcomingAdventures" to Color(0xFFF5E2C6),
"CurrentDay" to Color(0xFF30D158),
"SelectedDay" to Color(0xFF5856D6)
)
@Composable
fun AdventureCalendarItem(
modifier: Modifier = Modifier,
textColor: Color,
text: String,
backgroundColor: Color,
count: Int = 0,
countColor: Color? = null,
isSelected: Boolean = false,
onSelect: () -> (Unit) = {}
) {
var textWidth by remember { mutableStateOf(0.dp) }
var textHeight by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current.density
Column(
modifier = modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onSelect()
},
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(calendarCirclesSize)
.clip(CircleShape)
.background(backgroundColor)
.onGloballyPositioned { layoutc ->
textWidth = with(density) { layoutc.size.width / density }.dp
textHeight = with(density) { layoutc.size.height.toFloat() / density }.dp
}) {
Text(
modifier = Modifier
.align(Alignment.Center)
.padding(0.dp),
text = text,
style = TextStyle(
color = textColor,
textAlign = TextAlign.Center,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
fontWeight = FontWeight.SemiBold,
background = backgroundColor
)
)
}
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 5.dp),
text = if (count == 0) "" else count.toString(),
style = TextStyle(
color = countColor ?: textColor,
textAlign = TextAlign.Center,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontSize = with(LocalDensity.current) { 10.dp.toSp() },
fontWeight = FontWeight.W200,
)
)
}
}
@Composable
fun CalendarGuideCompose(
tooltipVisible: MutableState<Boolean>, absoluteX: Dp, absoluteY: Dp
) {
if (tooltipVisible.value) {
Dialog(
properties = DialogProperties(
dismissOnBackPress = true,
usePlatformDefaultWidth = false,
dismissOnClickOutside = true,
decorFitsSystemWindows = true
),
//shape = TooltipShape(4.dp),
onDismissRequest = { tooltipVisible.value = false },
// Align tooltip to the left-most edge
) {
Box(
modifier = Modifier
.fillMaxSize()
.clickable {
tooltipVisible.value = false
}) {
Card(
modifier = Modifier
.fillMaxWidth(0.8f)
.padding(20.dp)
.offset(x = 0.dp, y = absoluteY),
shape = TooltipShape(10.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFF2F2F2)
)
) {
Box(
modifier = Modifier.padding(10.dp)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) // Add vertical offset of 8.dp between items
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
text = "Calendar Guide",
style = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = with(LocalDensity.current) { 20.dp.toSp() },
fontFamily = FontFamily(Font(R.font.sf_pro)),
textAlign = TextAlign.Center,
color = Color.Black,
)
)
GuideItem(
"7",
Color(0xFF1C1C1E),
eventsColors.first().second,
secondText = "Past Adventures "
)
GuideItem(
"12",
Color(0xFF1C1C1E),
eventsColors[1].second,
"My Active Adventures "
)
GuideItem(
"15",
Color(0xFF1C1C1E),
eventsColors[2].second,
"My Upcoming Adventures "
)
GuideItem(
"17",
eventsColors[3].second,
Color.Transparent,
"Number of Adventures each day",
3
)
GuideItem("24", Color.White, eventsColors[3].second, "Current Day ")
GuideItem("22", Color.White, eventsColors[4].second, "Selected Day ")
}
}
}
}
}
}
}
@Composable
fun BackComposeMoreButton(
backModifier: Modifier = Modifier,
onBack: () -> Unit,
moreModifier: Modifier = Modifier,
onMore: () -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
IconButton(
modifier = backModifier
.wrapContentHeight()
.padding(horizontal = 10.dp), onClick = {
onBack()
}) {
Icon(
modifier = backModifier.size(20.dp),
tint = Color(0xff1C1C1E),
imageVector = ImageVector.vectorResource(R.drawable.left_chevron),
contentDescription = "Notification Icon"
)
}
Spacer(modifier = Modifier.weight(1f, true))
IconButton(
modifier = Modifier.padding(horizontal = 10.dp), onClick = { onMore() }) {
Icon(
modifier = Modifier
.wrapContentSize()
.padding(horizontal = 0.dp),
tint = Color(0xff1C1C1E),
imageVector = ImageVector.vectorResource(R.drawable.ic_more),
contentDescription = "more Icon"
)
}
}
}
@Composable
fun BackCompose(text: String, modifier: Modifier = Modifier, onBack: () -> Unit) {
Surface(
color = Color.White
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.background(color = Color.White)
.padding(10.dp)
.fillMaxHeight(0.05f)
) {
IconButton(
modifier = modifier.wrapContentHeight(), onClick = {
onBack()
}) {
Icon(
modifier = modifier.size(20.dp),
tint = Color(0xff1C1C1E),
imageVector = ImageVector.vectorResource(R.drawable.left_chevron),
contentDescription = "Notification Icon"
)
}
Text(
text = text,
modifier = modifier
.fillMaxWidth()
.align(Alignment.CenterVertically),
style = TextStyle(
color = Color(0xff1C1C1E),
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontSize = with(LocalDensity.current) { 20.dp.toSp() })
)
}
}
}
@Composable
fun ApplyButton(text: String = "apply", onClick: () -> Unit) {
ElevatedButton(
modifier = Modifier
.padding(40.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.elevatedButtonColors(
containerColor = Color(0xFF30D158), contentColor = Color.White
),
onClick = {
onClick()
}) {
Text(
modifier = Modifier.padding(10.dp), text = text, color = Color.White, style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontSize = with(LocalDensity.current) { 20.dp.toSp() })
)
}
}
@Composable
fun SearchField(
queryText: String, onQueryChanged: (String) -> Unit
) {
var clearQuery by remember { mutableStateOf(false) }
TextField(
shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.colors(
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
errorContainerColor = Color(0xFFF2F2F7),
focusedContainerColor = Color(0xFFF2F2F7),
disabledContainerColor = Color(0xFFF2F2F7),
unfocusedContainerColor = Color(0xFFF2F2F7)
),
textStyle = TextStyle(
color = Color(0xFF1C1C1E), fontFamily = FontFamily(Font(R.font.sf_pro))
),
value = queryText,
onValueChange = onQueryChanged,
placeholder = {
Text(
text = "Search", style = TextStyle(
color = Color(0xFF848484),
fontSize = with(LocalDensity.current) { 14.dp.toSp() })
)
},
modifier = Modifier
.padding(start = 20.dp, end = 20.dp)
.fillMaxWidth(),
singleLine = true,
leadingIcon = {
Image(
painter = painterResource(id = R.drawable.ic_search_small),
contentDescription = "Search Icon"
)
},
trailingIcon = {
AnimatedVisibility(
visible = queryText.isNotEmpty(),
enter = fadeIn() + scaleIn(),
exit = fadeOut() + scaleOut()
) {
IconButton(onClick = {
onQueryChanged("")
}) {
Image(
painter = painterResource(id = R.drawable.ic_clear),
contentDescription = "Search Icon"
)
}
}
})
}
@Composable
fun SelectionRow(
text: String,
isSelected: Boolean,
selectionImage: @Composable (Boolean) -> Unit,
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick) // Handle row click
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
selectionImage(isSelected)/*SelectionImage(isSelected = isSelected)*/ // Show tick icon only if selected
SimpleOption(text = text)
}
}
@Composable
fun HeaderWithCloseButton(
modifier: Modifier = Modifier, title: String, onCloseClick: () -> Unit
) {
Box(
modifier = modifier
.background(Color.White)
.fillMaxWidth()
.padding(10.dp)
) {
// Centered Title Text
Text(
modifier = Modifier.align(Alignment.Center), text = title, style = TextStyle(
color = Color.Black, fontSize = with(LocalDensity.current) { 20.dp.toSp() })
)
// Close Button aligned to the End
Text(
modifier = Modifier
.align(Alignment.CenterEnd)
.clickable { onCloseClick() }
.padding(10.dp), text = "Close", style = TextStyle(
color = Color(0xFF007AFF), fontSize = with(LocalDensity.current) { 14.dp.toSp() })
)
}
}
@Composable
fun SortDivider() {
HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.padding(start = 60.dp),
color = Color(0x3C3C435C).copy(alpha = 0.36f)
)
}
@Composable
fun SimpleOption(text: String) {
Text(
text = text, style = TextStyle(
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
color = Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro))
), modifier = Modifier.padding(start = 8.dp) // Add spacing between icon and text
)
}
@Composable
fun SimpleSelectionImage(isSelected: Boolean) {
Box(
modifier = Modifier.size(40.dp), contentAlignment = Alignment.Center
) {
// Show AnimatedVisibility with smooth animation when selected
AnimatedVisibility(
visible = isSelected,
enter = scaleIn(animationSpec = tween(durationMillis = 300)) + fadeIn(
animationSpec = tween(
durationMillis = 300
)
), // Smooth scale and fade-in animation
exit = scaleOut(animationSpec = tween(durationMillis = 200)) + fadeOut(
animationSpec = tween(
durationMillis = 200
)
) // Smooth scale and fade-out animation
) {
Image(
painter = painterResource(id = R.drawable.ic_tick),
contentDescription = "Selected Sort",
modifier = Modifier.align(Alignment.Center)
)
}
}
}
@OptIn(ExperimentalTime::class)
@Composable
fun SelectDate(
onDismissRequest: () -> Unit,
defaultSelectedDay: CalendarDay? = null,
useClock: Boolean = false,
onSelectDate: (CalendarDay?, LocalTime?, String) -> Unit,
) {
val calendarState = rememberCalendarState(
firstVisibleMonth = YearMonth.now(),
startMonth = YearMonth.now().minusYears(50),
endMonth = YearMonth.now().plusMonths(50)
)
val currentMonth =
calendarState.layoutInfo.visibleMonthsInfo.maxByOrNull { it.size }?.month?.yearMonth
?: YearMonth.now()
var selectedDay by remember { mutableStateOf<CalendarDay?>(defaultSelectedDay) }
var selectedClock by remember { mutableStateOf(if (useClock) LocalTime.now() else null) }
var amPm by remember { mutableStateOf("PM") }
Dialog(
onDismissRequest = onDismissRequest, properties = DialogProperties(
dismissOnBackPress = true, dismissOnClickOutside = true, usePlatformDefaultWidth = false
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF7F7F8)),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Select Date", style = TextStyle(
color = Color.Black,
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily(Font(R.font.sf_pro))
), modifier = Modifier.padding(16.dp)
)
DateNavigationBar(calendarState, currentMonth)
HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color(0xFFBABABA))
)
WeekdayRow(modifier = Modifier.background(Color.White))
CalendarWithSelectableDays(
calendarState = calendarState,
currentMonth = currentMonth,
selectedDay = selectedDay,
onDaySelected = { day -> selectedDay = day })
if (selectedClock != null) {
Row(modifier = Modifier.background(Color.White)) {
SelectableTime(
selectedClock = selectedClock!!,
amPm = amPm,
onHourChange = {
selectedClock = selectedClock!!.withHour(it)
},
onMinuteChange = {
selectedClock = selectedClock!!.withMinute(it)
},
onAmPmChange = { newAmPm -> amPm = newAmPm }
)
}
}
HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color(0xFFB9B9BA))
)
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFF7F7F8))
.padding(10.dp)
) {
TextButton(
onClick = {
onSelectDate(selectedDay, selectedClock, amPm)
}, modifier = Modifier.align(Alignment.CenterEnd)
) {
Text(
text = "Done",
color = Color(0xFF007AFF),
fontSize = with(LocalDensity.current) { 14.dp.toSp() })
}
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SelectableTime(
selectedClock: LocalTime,
amPm: String,
onHourChange: (Int) -> Unit,
onMinuteChange: (Int) -> Unit,
onAmPmChange: (String) -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
Box(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(horizontal = 20.dp), text = "Time", style = TextStyle(
color = Color.Black, textAlign = TextAlign.Left, fontWeight = FontWeight.W300
)
)
Spacer(modifier = Modifier.weight(1f, true))
// Hour input (1-12)
Card(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight()
.padding(horizontal = 20.dp, vertical = 15.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xffF2F2F2), contentColor = Color(0xFF3478F6)
)
) {
Row(
modifier = Modifier
.wrapContentWidth()
.padding(horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
BasicTextField(
value = when {
selectedClock.hour == 0 && amPm == "AM" -> "12" // Midnight
selectedClock.hour == 12 && amPm == "PM" -> "12" // Noon
selectedClock.hour > 12 && amPm == "PM" -> (selectedClock.hour - 12).toString()
selectedClock.hour == 0 && amPm == "PM" -> "" // Should not happen with PM if logic is correct
else -> selectedClock.hour.toString()
},
onValueChange = { value: String ->
val filtered = value.filter { it.isDigit() }
val numericValue = filtered.toIntOrNull()
if (filtered.length <= 2 && numericValue != null && numericValue in 1..12) {
val finalHour = if (amPm == "PM" && numericValue < 12) {
numericValue + 12
} else if (amPm == "AM" && numericValue == 12) {
0 // Midnight
} else {
numericValue
}
onHourChange(finalHour)
} else if (filtered.isEmpty()) {
onHourChange(if (amPm == "AM") 0 else 12) // Default to 12 AM or 12 PM on empty
}
},
modifier = Modifier
.background(Color.Transparent)
.width(20.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(
fontWeight = FontWeight.W300,
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
textAlign = TextAlign.Center,
color = Color(0xFF3478F6)
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
) {
TextFieldDefaults.DecorationBox(
value = when {
selectedClock.hour == 0 && amPm == "AM" -> "12" // Midnight
selectedClock.hour == 12 && amPm == "PM" -> "12" // Noon
selectedClock.hour > 12 && amPm == "PM" -> (selectedClock.hour - 12).toString()
selectedClock.hour == 0 && amPm == "PM" -> "" // Should not happen with PM if logic is correct
else -> selectedClock.hour.toString()
},
innerTextField = it,
contentPadding = PaddingValues(0.dp),
enabled = true,
singleLine = true,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
colors = TextFieldDefaults.colors(
errorContainerColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedTextColor = Color(0xFF3478F6),
disabledTextColor = Color(0xFF3478F6),
unfocusedTextColor = Color(0xFF3478F6),
errorTextColor = Color(0xFF3478F6),
errorSupportingTextColor = Color(0xFF3478F6),
focusedSupportingTextColor = Color(0xFF3478F6),
unfocusedSupportingTextColor = Color(0xFF3478F6),
disabledSupportingTextColor = Color(0xFF3478F6),
disabledIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
}
// Immutable colon
Text(
text = ":", style = LocalTextStyle.current.copy(
fontSize = 32.sp,
fontWeight = FontWeight.W300,
), modifier = Modifier.padding(horizontal = 4.dp)
)
// Minute input (00-59)
BasicTextField(
value = selectedClock.minute.toString().padStart(2, '0'),
onValueChange = { value ->
val filtered = value.filter { it.isDigit() }
val numericValue = filtered.toIntOrNull()
if ((filtered.length <= 2) && (numericValue == null || (numericValue in 0..59))) {
onMinuteChange(numericValue ?: 0)
}
},
modifier = Modifier
.background(Color.Transparent)
.width(20.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(
fontWeight = FontWeight.W300,
textAlign = TextAlign.Center, color = Color(0xFF3478F6),
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
) {
TextFieldDefaults.DecorationBox(
value = selectedClock.minute.toString().padStart(2, '0'),
innerTextField = it,
contentPadding = PaddingValues(0.dp),
enabled = true,
singleLine = true,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
colors = TextFieldDefaults.colors(
errorContainerColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedTextColor = Color(0xFF3478F6),
disabledTextColor = Color(0xFF3478F6),
unfocusedTextColor = Color(0xFF3478F6),
errorTextColor = Color(0xFF3478F6),
errorSupportingTextColor = Color(0xFF3478F6),
focusedSupportingTextColor = Color(0xFF3478F6),
unfocusedSupportingTextColor = Color(0xFF3478F6),
disabledSupportingTextColor = Color(0xFF3478F6),
disabledIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
)
)
}
Text(
text = amPm,
modifier = Modifier
.padding(start = 8.dp)
.clickable {
val newAmPm = if (amPm == "AM") "PM" else "AM"
onAmPmChange(newAmPm)
// Adjust hour when AM/PM is toggled
val currentHour = selectedClock.hour
// Determine the new hour based on the *new* newAmPm
val adjustedHour = if (newAmPm == "PM") {
if (currentHour < 12) currentHour + 12 else if (currentHour == 12) 12 else currentHour // handles 12 AM to 12 PM
} else { // newAmPm == "AM"
if (currentHour == 0) 0 // Midnight already 0
else if (currentHour > 12) currentHour - 12 // PM to AM (e.g. 13:00 to 1:00)
else if (currentHour == 12) 0 // 12 PM to 12 AM
else currentHour
}
onHourChange(adjustedHour)
}
.padding(vertical = 4.dp, horizontal = 8.dp), // Add some padding for better touch target
style = LocalTextStyle.current.copy(
fontWeight = FontWeight.W300,
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
color = Color(0xFF3478F6)
)
)
}
}
}
}
}
@Composable
private fun DateNavigationBar(calendarState: CalendarState, currentMonth: YearMonth) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
TimeNavigationBar(
modifier = Modifier.weight(1f),
currentTime = currentMonth?.month?.name ?: "",
onPreviousClick = {
CoroutineScope(Dispatchers.Main).launch {
calendarState.scrollToMonth(currentMonth.minusMonths(1))
}
},
onNextClick = {
CoroutineScope(Dispatchers.Main).launch {
calendarState.scrollToMonth(currentMonth.plusMonths(1))
}
})
TimeNavigationBar(
modifier = Modifier.weight(1f),
currentTime = currentMonth?.year?.toString() ?: "",
onPreviousClick = {
CoroutineScope(Dispatchers.Main).launch {
calendarState.scrollToMonth(currentMonth.minusYears(1))
}
},
onNextClick = {
CoroutineScope(Dispatchers.Main).launch {
calendarState.scrollToMonth(currentMonth.plusYears(1))
}
})
}
}
@Composable
fun TimeNavigationBar(
modifier: Modifier,
currentTime: String,
onPreviousClick: () -> Unit,
onNextClick: () -> Unit,
) {
Row(
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.background(Color(0xFFF9F9F9))
) {
IconButton(
modifier = Modifier.size(15.dp), onClick = onPreviousClick
) {
Icon(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.left_chevron),
contentDescription = "Previous",
tint = Color(0xFF3478F6)
)
}
Text(
text = currentTime, modifier = Modifier.padding(), style = TextStyle(
color = Color(0xFF3478F6),
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
fontWeight = FontWeight.SemiBold
)
)
IconButton(
modifier = Modifier.size(15.dp), onClick = onNextClick
) {
Icon(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.right_chevron),
contentDescription = "Next",
tint = Color(0xFF3478F6)
)
}
}
}
@Composable
fun WeekdayRow(
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(0.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
listOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun").forEach { day ->
Text(
text = day, style = TextStyle(
fontSize = with(LocalDensity.current) { 12.dp.toSp() },
color = Color(0xFF7E818C),
fontFamily = FontFamily(Font(R.font.sf_pro))
), modifier = Modifier.weight(1f), textAlign = TextAlign.Center
)
}
}
}
@Composable
fun CalendarWithSelectableDays(
calendarState: CalendarState,
currentMonth: YearMonth?,
selectedDay: CalendarDay?, // Track the currently selected day
onDaySelected: (CalendarDay) -> Unit // Callback when a day is selected
) {
HorizontalCalendar(
modifier = Modifier.background(Color.White),
state = calendarState,
dayContent = { day -> // Renders each day
DaySelectionItem(
day = day, // Pass the CalendarDay
isCurrentMonth = day.date.yearMonth == currentMonth, // Check if it's part of the current month
isSelected = day == selectedDay, // Highlight the selected day
onDayClicked = { onDaySelected(it) } // Handle day selection and pass it to the parent
)
},
userScrollEnabled = true,
calendarScrollPaged = true
)
}
@Composable
fun DaySelectionItem(
day: CalendarDay, // CalendarDay from kizitonwose library
isCurrentMonth: Boolean, // Whether the day belongs to the current month
isSelected: Boolean, // Whether the day is selected
onDayClicked: (CalendarDay) -> Unit, // Callback for day selection
modifier: Modifier = Modifier // Additional customization
) {
val backgroundColor = when {
isSelected -> Color(0xFF3478F6) // Highlight selected day
isCurrentMonth -> Color.White // Background for current month days
else -> Color.Transparent // Background for non-current month days
}
val textColor = if (isCurrentMonth) {
if (isSelected) Color.Black else Color(0xFF1C1C1E) // Dynamically change color based on selection
} else {
Color(0xFFC8C8C8) // Light gray for non-current month days
}
Box(
modifier = modifier
.size(48.dp) // Standard size for a day item
.clip(CircleShape)
.background(backgroundColor)
.clickable(
onClick = { if (isCurrentMonth) onDayClicked(day) }, // Enable click for current month only
enabled = isCurrentMonth, // Non-clickable for non-current month days
interactionSource = remember { MutableInteractionSource() },
indication = null // Remove ripple effect for clean background change
), contentAlignment = Alignment.Center
) {
Text(
text = day.date.dayOfMonth.toString(), style = TextStyle(
color = textColor,
textAlign = TextAlign.Center,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontSize = with(LocalDensity.current) { 10.dp.toSp() },
fontWeight = FontWeight.W300
)
)
}
}
@Composable
fun Friends(friends: List<Friend>) {
Box(modifier = Modifier.fillMaxSize()) {
LazyVerticalGrid(
userScrollEnabled = false,
modifier = Modifier
.height(getScreenWidthInDp() / 3 * friends.size / 2)
.background(Color(0xFFEFEFF4))
.padding(bottom = 56.dp),
columns = GridCells.Fixed(3),
contentPadding = PaddingValues(10.dp)
) {
itemsIndexed(friends) { index, item ->
Column(
modifier = Modifier
.aspectRatio(0.8f)
.padding(5.dp)
.background(Color.White, shape = RoundedCornerShape(10.dp))
) {
Card(
modifier = Modifier
.padding(10.dp)
.align(Alignment.CenterHorizontally)
.size(50.dp),
shape = CircleShape,
colors = CardDefaults.cardColors(containerColor = Color(0xFFDBDCDE))
) {
Image(
contentScale = ContentScale.Inside,
painter = rememberAsyncImagePainter(
model = item.avatar,
placeholder = painterResource(id = R.drawable.img_profile_placeholder),
error = painterResource(id = R.drawable.img_profile_placeholder) // Use placeholder if loading image fails
),
contentDescription = "Friend's photo",
modifier = Modifier
.fillMaxSize()
.align(Alignment.CenterHorizontally)
.clip(CircleShape) // Make the image circular
.background(Color.LightGray) // Optional background for contrast
)
}
Text(
modifier = Modifier
.padding(horizontal = 10.dp)
.align(Alignment.CenterHorizontally),
style = TextStyle(
textAlign = TextAlign.Center,
fontWeight = FontWeight.W300,
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = Color.Black
),
text = item.firstName + " " + item.lastName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
TextButton(
modifier = Modifier
.padding(5.dp)
.wrapContentSize(),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xffF2F2F7), contentColor = Color.Black
),
onClick = {
// Provide functionality for removing a friend here
}) {
Text(
text = "Remove Friend", style = TextStyle(
fontWeight = FontWeight.W500,
fontFamily = FontFamily(Font(R.font.sf_pro))
), maxLines = 1
)
}
}
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\ManageShared.kt
```kt
package com.divadventure.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.divadventure.R
@Composable
fun GrayTitle(text: String) {
Text(
text = text,
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFEFEFF4))
.padding(start = 10.dp, end = 10.dp, top = 20.dp, bottom = 10.dp),
style = TextStyle(
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.W600,
)
)
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\ProfileShared.kt
```kt
package com.divadventure.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import com.divadventure.viewmodel.ProfileUIEvent
import com.divadventure.viewmodel.ProfileViewModel
@Composable
fun ControlButton(text: String, onClick: () -> Unit) {
TextButton(
shape = RoundedCornerShape(5.dp), colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF30D158),
), modifier = Modifier
.fillMaxWidth()
.padding(20.dp), onClick = {}) {
Text(
text = text, fontSize = with(LocalDensity.current) {
14.dp.toSp()
}, modifier = Modifier.padding(5.dp), color = Color.White
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TabbedProfileSwitcher(
adventuresContent: @Composable () -> Unit,
friendsContent: @Composable () -> Unit,
) {
// List of tab titles
val tabTitles = listOf("Adventures", "Friends")
var selectedTabIndex by remember { mutableStateOf(0) }
Column {
SecondaryTabRow(
divider = {},
indicator = {
// Correctly access the `TabIndicatorScope` passed here
TabRowDefaults.SecondaryIndicator(
Modifier.tabIndicatorOffset(selectedTabIndex),
color = Color.Black // Set the indicator color to black
)
},
selectedTabIndex = selectedTabIndex,
containerColor = Color.White,
) {
Tab(
selected = selectedTabIndex == 0,
onClick = { selectedTabIndex = 0 },
text = { Text(tabTitles[0], style = TextStyle(color = Color.Gray)) }
)
Tab(
selected = selectedTabIndex == 1,
onClick = { selectedTabIndex = 1 },
text = { Text(tabTitles[1], style = TextStyle(color = Color.Gray)) }
)
}
// Display content based on the selected tab
when (selectedTabIndex) {
0 -> adventuresContent() // Render first tab content
1 -> friendsContent() // Render second tab content
}
}
}
@Composable
fun ProfileSettings(profileViewModel: ProfileViewModel) {
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) ModalBottomSheet(dragHandle = {
Box { }
}, onDismissRequest = { }) {
val coroutineScope = rememberCoroutineScope()
// State for Modal Bottom Sheet
val modalBottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = false,
confirmValueChange = { it != SheetValue.PartiallyExpanded } // Prevents partial collapse
)
var showBottomSheet by remember { mutableStateOf(false) }
LaunchedEffect(key1 = true) {
profileViewModel.uiEvent.collect { event ->
when (event) {
ProfileUIEvent.AnimateItem -> {}
is ProfileUIEvent.NavigateToNextScreen -> {}
ProfileUIEvent.ShowDialog -> {}
is ProfileUIEvent.ShowDim -> {}
is ProfileUIEvent.ShowSnackbar -> {
}
is ProfileUIEvent.ShowBottomSheet -> {
showBottomSheet = true
}
}
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\AuthViewModel.kt
```kt
package com.divadventure.viewmodel
import android.os.Bundle
import android.util.Patterns
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import com.divadventure.data.SharedService
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationEvent.NavigateTo
import com.divadventure.data.navigation.NavigationEvent.PopSpecific
import com.divadventure.data.navigation.Screen
import com.divadventure.di.AuthPrefs.VERIFICATION_PASSED_BOOLEAN
import com.divadventure.di.SharedPrefs
import com.divadventure.di.UserPrefs.KEY_EMAIL
import com.divadventure.di.UserPrefs.KEY_FIRST_NAME
import com.divadventure.di.UserPrefs.KEY_LAST_NAME
import com.divadventure.di.UserPrefs.KEY_TOKEN
import com.divadventure.di.UserPrefs.KEY_USERNAME
import com.divadventure.domain.models.Message
import com.divadventure.domain.models.ReqLogin
import com.divadventure.domain.models.ReqOnboard
import com.divadventure.domain.models.ReqVerifyEmail
import com.divadventure.domain.models.ReqVerifyResetPasswordToken
import com.divadventure.domain.models.ResVerifyEmail
import com.divadventure.domain.models.ResVerifyResetPasswordToken
import com.divadventure.domain.models.SignUpResponse
import com.divadventure.domain.models.SignupRequest
import com.divadventure.domain.models.UserData
import com.divadventure.ui.screens.UserDataManager
import com.divadventure.util.Helper.Companion.isEmailValid
import com.divadventure.util.NetworkManager
import com.divadventure.viewmodel.AuthUiEvent.ExecuteNavigation
import com.divadventure.viewmodel.AuthUiEvent.ShowSnackbar
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@HiltViewModel
class AuthViewModel @Inject constructor(
private val sharedService: SharedService,
private val sharedPrefs: SharedPrefs,
private val networkManager: NetworkManager
) : BaseViewModel<AuthIntent, AuthState>(AuthState()) {
private val _Auth_uiEvent = MutableSharedFlow<AuthUiEvent>()
val uiEvent = _Auth_uiEvent.asSharedFlow()
// Initialize handlers
private val splashHandler = SplashHandler()
private val landingHandler = LandingHandler(this)
private val signupHandler = SignupHandler(this)
private val onboardHandler = OnboardHandler(this)
private val verificationHandler = VerificationHandler(this)
private val loginHandler = LoginHandler(this)
private val changeEmailHandler = ChangeEmailHandler(this)
// private val verificationResendEmailHandler = VerificationResendEmailHandler(this)
private val forgotPasswordHandler = ForgotPasswordHandler(this)
private val resetPasswordHandler = ResetPasswordHandler(this)
//private val mutualHandler: MutualHandler by lazy { MutualHandler(this) }
private val mutualHandler = MutualHandler(this)
override suspend fun handleIntent(intent: AuthIntent) {
when (intent) {
is AuthIntent.SplashIntent -> splashHandler.handle(intent)
is AuthIntent.LandingIntent -> landingHandler.handle(intent)
is AuthIntent.SignupIntent -> signupHandler.handle(intent)
is AuthIntent.OnboardIntent -> onboardHandler.handle(intent)
is AuthIntent.VerificationIntent -> verificationHandler.handle(intent)
is AuthIntent.LoginIntent -> loginHandler.handle(intent)
is AuthIntent.ChangeEmailIntent -> changeEmailHandler.handle(intent)
is AuthIntent.ForgotPasswordIntent -> forgotPasswordHandler.handle(intent)
is AuthIntent.ResetPasswordIntent -> resetPasswordHandler.handle(intent)
is AuthIntent.MutualIntent -> mutualHandler.handle(intent)
}
}
//************************************************************************************************************************************************
// Example handler: SplashHandler
inner class SplashHandler() {
suspend fun handle(intent: AuthIntent.SplashIntent) {
when (intent) {
AuthIntent.SplashIntent.CheckDecision -> handleCheckDecision()
}
}
private suspend fun handleCheckDecision() {
Timber.d("SplashIntent.CheckDecision triggered.")
delay(2000)
// navigateToLandingScreen()
if (isTokenEmpty()) {
navigateToLandingScreen()
} else {
Timber.d("Token present, proceeding to requestAccount.")
requestAccount()
}
}
private fun isTokenEmpty(): Boolean {
val token = sharedPrefs.getString(KEY_TOKEN)
return token.isNullOrEmpty().also {
if (it) Timber.i("Token is null or empty.")
}
}
private suspend fun navigateToLandingScreen() {
_Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.Landing,
popUpTo = Screen.Splash,
inclusive = true,
onDestinationChangedListener = createOnDestinationChangedListener()
)
)
)
}
private fun createOnDestinationChangedListener(): NavController.OnDestinationChangedListener {
return object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController, destination: NavDestination, arguments: Bundle?
) {
// Timber.d("Navigated to ${destination.route}, removing listener.")
// controller.removeOnDestinationChangedListener(this@createOnDestinationChangedListener)
}
}
}
private fun requestAccount() {
val token = sharedPrefs.getString(KEY_TOKEN)
Timber.d("Retrieving current user with token: $token")
if (!isNetworkConnected()) {
notifyNoInternetConnection()
return
}
fetchCurrentUser(token)
}
private fun isNetworkConnected(): Boolean {
return networkManager.isNetworkConnected().also {
if (!it) Timber.w("No internet connection detected.")
}
}
private fun notifyNoInternetConnection() {
viewModelScope.launch {
_Auth_uiEvent.emit(
ShowSnackbar(
title = "Internet Issue", message = "No Internet Connection"
)
)
}
}
private fun fetchCurrentUser(token: String?) {
Timber.d("Starting fetchCurrentUser at: ${System.currentTimeMillis()} with token: $token")
sharedService.getCurrentUser("Bearer $token")
.enqueue(object : Callback<SignUpResponse> {
override fun onResponse(
call: Call<SignUpResponse?>, response: Response<SignUpResponse?>
) {
Timber.d("fetchCurrentUser onResponse received at: ${System.currentTimeMillis()}")
handleGetCurrentUserResponse(response)
Timber.d("fetchCurrentUser onResponse handling completed at: ${System.currentTimeMillis()}")
}
override fun onFailure(call: Call<SignUpResponse?>, t: Throwable) {
Timber.e("fetchCurrentUser onFailure triggered at: ${System.currentTimeMillis()} with error: ${t.message}")
viewModelScope.launch {
handleGetCurrentUserFailure(t)
Timber.d("fetchCurrentUser onFailure handling completed at: ${System.currentTimeMillis()}")
}
}
})
}
private fun handleGetCurrentUserResponse(response: Response<SignUpResponse?>) {
val body = response.body()
if (response.isSuccessful && body != null) {
navigateToHomeScreen()
} else {
handleUnsuccessfulResponse()
}
}
private fun isOnboardingRequired(): Boolean {
return sharedPrefs.getString(KEY_USERNAME).isNullOrEmpty() || sharedPrefs.getString(
KEY_FIRST_NAME
).isNullOrEmpty() || sharedPrefs.getString(KEY_LAST_NAME).isNullOrEmpty()
}
private fun handleUnsuccessfulResponse() {
viewModelScope.launch {
navigateToLandingScreen()
}
}
private suspend fun handleGetCurrentUserFailure(t: Throwable) {
Timber.e("Request failed: ${t.message}")
_Auth_uiEvent.emit(
ShowSnackbar(
title = "Operation Failed", message = "An error occurred. Please try again."
)
)
navigateToLandingScreen()
}
private fun navigateToHomeScreen() {
Timber.i("Navigating to HomeScreen.")
navigateTo(Screen.Main, Screen.Splash)
}
/*
private fun navigateToVerificationScreen() {
Timber.i("Navigating to VerificationScreen.")
updateState(
state.value.copy(
verificationState = VerificationState(
email = sharedPrefs.getString(KEY_EMAIL) ?: ""
)
)
)
navigateTo(Screen.Verification, Screen.Splash)
}
*/
private fun navigateTo(screen: Screen, popUpTo: Screen) {
viewModelScope.launch {
_Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = screen,
popUpTo = popUpTo,
inclusive = true,
onDestinationChangedListener = createOnDestinationChangedListenerForNavigation()
)
)
)
}
}
private fun createOnDestinationChangedListenerForNavigation(): NavController.OnDestinationChangedListener {
return object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController, destination: NavDestination, arguments: Bundle?
) {
Timber.d("Navigated to ${destination.route}")
controller.removeOnDestinationChangedListener(this)
}
}
}
}
//************************************************************************************************************************************************
inner class SignupHandler(private val viewModel: AuthViewModel) {
suspend fun handle(intent: AuthIntent.SignupIntent) {
when (intent) {
is AuthIntent.SignupIntent.SignUp -> handleSignUp(intent)
is AuthIntent.SignupIntent.SignUpWithGoogle -> Unit // Handle Google sign-up
is AuthIntent.SignupIntent.OnEmailChanged -> updateSignupState {
it.copy(
email = intent.email,
)
}
is AuthIntent.SignupIntent.OnPasswordChanged -> updateSignupState {
it.copy(
password = intent.password, startedTyping = true
)
}
is AuthIntent.SignupIntent.OnPasswordConfirmationChanged -> updateSignupState {
it.copy(
passwordConfirmation = intent.passwordConfirmation, startedTyping = true
)
}
}
}
private fun handleSignUp(intent: AuthIntent.SignupIntent.SignUp) {
if (!handlePreSignupChecks()) return
updateLoadingState(true)
sharedService.signup(SignupRequest(intent.email, intent.password, intent.password))
.enqueue(signupCallback())
}
private fun signupCallback() = object : Callback<SignUpResponse> {
override fun onResponse(
call: Call<SignUpResponse>, response: Response<SignUpResponse>
) {
if (response.isSuccessful) {
viewModel.viewModelScope.launch { processSignUpResponse(response) }
} else {
emitSnackbar(
"Error Sign up", extractErrorMessage(response)
)
}
}
override fun onFailure(call: Call<SignUpResponse>, t: Throwable) {
viewModel.viewModelScope.launch {
showSnackbar(
"Signup failed", t.message ?: "Unknown error"
)
}
finalizeSignupProcess()
emitSnackbar("Action Required", "Something went wrong, please try again.")
}
}
private fun emitSnackbar(title: String, message: String) {
Timber.d("Emitting snackbar with title: $title, message: $message")
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(ShowSnackbar(title, message))
}
}
private suspend fun processSignUpResponse(response: Response<SignUpResponse>) {
response.body()?.let {
if (response.isSuccessful) {
saveUserData(it)
resetSignupState()
// sharedPrefs.setLong(SEND_OTP_TIME_MILLIS, System.currentTimeMillis())
navigateToVerificationScreen()
} else showSnackbar("Signup failed", extractErrorMessage(response))
}
finalizeSignupProcess()
}
private fun navigateToVerificationScreen() {
emitNavigationEvent(
NavigateTo(
screen = Screen.Verification,
popUpTo = Screen.SignUp,
inclusive = true,
onDestinationChangedListener = object :
NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
Timber.d("Navigated to ${destination.route}")
controller.removeOnDestinationChangedListener(this)
}
})
)
}
private suspend fun updateSignupState(update: (SignupState) -> SignupState) {
viewModel.updateState(state.value.copy(signupState = update(state.value.signupState!!)))
validateSignupForm()
}
private fun validateSignupForm() {
state.value.signupState?.let {
val formError = validateSignupFields(it.email, it.password, it.passwordConfirmation)
viewModel.updateState(
state.value.copy(
signupState = it.copy(
formError = formError.orEmpty(),
formDataValid = formError == null,
passwordsMatch = it.password == it.passwordConfirmation,
is8Characters = it.password.length >= 8
)
)
)
}
}
private fun validateSignupFields(
email: String, password: String, passwordConfirmation: String
): String? {
return when {
email.isBlank() -> "Email can't be empty"
!isEmailValid(email) -> "Email is not valid"
password.isBlank() -> "Password can't be empty"
password.length < 8 -> "Password must have at least 8 characters"
password != passwordConfirmation -> "Passwords do not match"
else -> null
}
}
private fun handlePreSignupChecks(): Boolean {
if (!networkManager.isNetworkConnected()) {
viewModel.viewModelScope.launch {
showSnackbar(
"Internet Issue", "No Internet Connection"
)
}
return false
}
return state.value.signupState?.formDataValid == true
}
private fun finalizeSignupProcess() {
enableSignupButton()
updateLoadingState(false)
}
private fun showSnackbar(title: String, message: String) {
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(
ShowSnackbar(
title, message
)
)
}
}
private fun updateLoadingState(isLoading: Boolean) {
viewModel.updateState(
state.value.copy(
signupState = state.value.signupState?.copy(
isLoadingSignUp = isLoading
)
)
)
}
private fun enableSignupButton() {
viewModel.updateState(
state.value.copy(
signupState = state.value.signupState?.copy(
signupClickable = true
)
)
)
}
private fun saveUserData(body: SignUpResponse) {
UserDataManager(sharedPrefs).savePrimaryData(body)
}
private fun resetSignupState() {
viewModel.updateState(
state.value.copy(
signupState = SignupState(), verificationState = VerificationState(
email = state.value.signupState!!.email
)
)
)
}
private fun emitNavigationEvent(navigationEvent: NavigateTo) {
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent
)
)
}
}
private fun extractErrorMessage(response: Response<SignUpResponse>) =
response.errorBody()?.string()
?.let { Gson().fromJson(it, Message::class.java)?.message } ?: "Unknown Error"
}
//************************************************************************************************************************************************
inner class LandingHandler(private val viewModel: AuthViewModel) {
suspend fun handle(intent: AuthIntent.LandingIntent) {
when (intent) {
is AuthIntent.LandingIntent.gotoLogin -> navigateToLogin()
is AuthIntent.LandingIntent.gotoSignup -> navigateToSignup()
}
}
private suspend fun navigateToLogin() {
Timber.d("Navigating to LoginScreen.")
viewModel.updateState(viewModel.state.value.copy(loginState = LoginState()))
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.Login,
popUpTo = Screen.Landing,
inclusive = false,
onDestinationChangedListener = createOnDestinationChangedListener()
)
)
)
}
private suspend fun navigateToSignup() {
Timber.d("Navigating to SignUpScreen.")
viewModel.updateState(viewModel.state.value.copy(signupState = SignupState()))
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.SignUp,
popUpTo = Screen.Landing,
inclusive = false,
onDestinationChangedListener = createOnDestinationChangedListener()
)
)
)
}
private fun createOnDestinationChangedListener(): NavController.OnDestinationChangedListener {
return NavController.OnDestinationChangedListener { controller, destination, _ ->
Timber.d("Navigated to ${destination.route}, removing listener.")
}
}
}
//************************************************************************************************************************************************
inner class OnboardHandler(private val viewModel: AuthViewModel) {
suspend fun handle(intent: AuthIntent.OnboardIntent) {
when (intent) {
is AuthIntent.OnboardIntent.OnFirstNameChanged -> handleFirstNameChange(intent.firstName)
is AuthIntent.OnboardIntent.OnLastNameChanged -> handleLastNameChange(intent.lastName)
is AuthIntent.OnboardIntent.OnUserNameChanged -> handleUserNameChange(intent.userName)
is AuthIntent.OnboardIntent.Onboard -> startOnboardingProcess(intent)
}
}
private fun handleFirstNameChange(firstName: String) {
Timber.d("First name changed: $firstName")
updateOnboardState { it.copy(firstName = firstName) }
validateOnboardingData()
}
private fun handleLastNameChange(lastName: String) {
Timber.d("Last name changed: $lastName")
updateOnboardState { it.copy(lastName = lastName) }
validateOnboardingData()
}
private fun handleUserNameChange(userName: String) {
Timber.d("Username changed: $userName")
updateOnboardState { it.copy(userName = userName) }
validateOnboardingData()
}
private suspend fun startOnboardingProcess(intent: AuthIntent.OnboardIntent.Onboard) {
Timber.d("Onboarding process started.")
if (!isNetworkConnectedWithSnackbar()) return
val onboardState = state.value.onboardState ?: return
if (!onboardState.formDataValid) {
showOnboardingSnackbar("Onboarding Failed", onboardState.error ?: "Invalid data.")
return
}
// viewModel.updateLoadingState(true)
sharedService.onboard(
bearer = "Bearer ${sharedPrefs.getString(KEY_TOKEN)}", request = ReqOnboard(
firstName = onboardState.firstName,
lastName = onboardState.lastName,
username = onboardState.userName
)
).enqueue(createOnboardCallback(intent))
}
private fun saveToSharedPrefs(key: String, value: String) {
sharedPrefs.setString(key, value)
}
private fun createOnboardCallback(onboardIntent: AuthIntent.OnboardIntent.Onboard) =
object : Callback<SignUpResponse> {
override fun onResponse(
call: Call<SignUpResponse>, response: Response<SignUpResponse>
) {
if (response.isSuccessful) {
Timber.d("Onboarding succeeded")
resetOnboardingState()
saveToSharedPrefs(KEY_USERNAME, onboardIntent.userName)
Timber.d("Username saved in shared preferences: ${onboardIntent.userName}")
saveToSharedPrefs(KEY_FIRST_NAME, onboardIntent.firstName)
Timber.d("First name saved in shared preferences: ${onboardIntent.firstName}")
saveToSharedPrefs(KEY_LAST_NAME, onboardIntent.lastName)
viewModelScope.launch {
_Auth_uiEvent.emit(
ExecuteNavigation(
PopSpecific(
Screen.Landing,
false
)
)
)
}
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.Main,
popUpTo = Screen.Landing,
inclusive = true,
onDestinationChangedListener = object :
NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
Timber.d("Navigated to ${destination.route}")
controller.removeOnDestinationChangedListener(this)
}
})
)
)
}
} else {
Timber.e("Onboarding failed with: ${response.code()}")
showOnboardingSnackbar("Onboarding Failed", "Server error occurred.")
}
// viewModel.updateLoadingState(false)
}
override fun onFailure(call: Call<SignUpResponse>, t: Throwable) {
Timber.e("Onboarding request failed: ${t.message}")
showOnboardingSnackbar("Onboarding Failed", "Network error occurred.")
// viewModel.updateLoadingState(false)
}
}
private fun isNetworkConnectedWithSnackbar(): Boolean {
if (!networkManager.isNetworkConnected()) {
showOnboardingSnackbar("Internet Issue", "No Internet Connection")
return false
}
return true
}
private fun showOnboardingSnackbar(title: String, message: String) {
viewModelScope.launch {
viewModel._Auth_uiEvent.emit(ShowSnackbar(title, message))
}
}
private fun resetOnboardingState() {
viewModel.updateState(
state.value.copy(onboardState = OnboardState())
)
}
private fun updateOnboardState(update: (OnboardState) -> OnboardState) {
viewModel.updateState(
state.value.copy(
onboardState = state.value.onboardState?.let(update)
)
)
}
private fun validateOnboardingData() {
val onboardState = state.value.onboardState ?: return
val error = when {
onboardState.firstName.isBlank() -> "First name can't be empty"
onboardState.lastName.isBlank() -> "Last name can't be empty"
onboardState.userName.isBlank() -> "Username can't be empty"
else -> null
}
val isValid = error == null
updateOnboardState {
it.copy(
error = error.orEmpty(), formDataValid = isValid
)
}
}
}
//************************************************************************************************************************************************
inner class VerificationHandler(private val viewModel: AuthViewModel) {/*init {
Timber.d("VerificationHandler initialized.")
startOtpCountdown()
}*/
suspend fun handle(intent: AuthIntent.VerificationIntent) {
Timber.d("Handling VerificationIntent: $intent")
when (intent) {
is AuthIntent.VerificationIntent.OnOtpVerifyPressed -> handleOtpVerification()
is AuthIntent.VerificationIntent.OnOtpChanged -> handleOtpChanged(intent.otp)
is AuthIntent.VerificationIntent.GotoChangeEmail -> navigateToChangeEmail(
intent.email
)
is AuthIntent.VerificationIntent.ResendCode -> {
Timber.d("ResendCode intent triggered, remaining time: ${state.value.verificationState?.otpRemainTime}")
if (state.value.verificationState?.otpRemainTime?.toInt() == 0) {
requestResendOtp(
"Bearer ${sharedPrefs.getString(KEY_TOKEN)}", Screen.Verification
)
} else {
emitSnackbar(
"Resend not available",
"Resend code available in ${state.value.verificationState!!.otpRemainTime}"
)
}
}
is AuthIntent.VerificationIntent.UpdateTimer -> {
updateState(
state.value.copy(
verificationState = state.value.verificationState?.copy(
otpRemainTime = intent.time
)
)
)
}
}
}
private fun requestResendOtp(token: String, fromScreen: Screen) {
Timber.d("Attempting to resend OTP for token: $token")
if (!viewModel.networkManager.isNetworkConnected()) {
Timber.e("Network not connected. Cannot resend OTP.")
emitSnackbar("Internet Issue", "No Internet Connection")
return
}
viewModel.sharedService.resendVerificationEmail(token)
.enqueue(object : Callback<ResVerifyEmail> {
override fun onResponse(
call: Call<ResVerifyEmail?>, response: Response<ResVerifyEmail?>
) {
Timber.d("Resend OTP response received: ${response.code()}")
if (response.isSuccessful && response.body() != null) {
handleSuccessfulResendOtp()
} else if (response.code() == 425) {
Timber.w("Resend failed due to 425 response code: Too many requests.")
emitSnackbar(
"Re-sent Verification Code failed",
"server is refusing to process a request due to potential replay attacks or other security concerns."
)
}
}
override fun onFailure(call: Call<ResVerifyEmail?>, t: Throwable) {
Timber.e("Resend OTP request failed: ${t.localizedMessage}")
emitSnackbar("Action Required", "Something went wrong, please try again.")
}
})
}
private fun handleSuccessfulResendOtp() {
Timber.d("Resend OTP success. Starting countdown.")
viewModel.viewModelScope.launch {
emitSnackbar(
"Re-sent Verification Link",
"A new verification link has been sent to your email."
)
}
}
private fun emitSnackbar(title: String, message: String) {
Timber.d("Emitting snackbar with title: $title, message: $message")
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(ShowSnackbar(title, message))
}
}
private suspend fun handleOtpVerification() {
Timber.d("Handling OTP Verification.")
if (!viewModel.networkManager.isNetworkConnected()) {
Timber.e("Network not connected. Cannot verify OTP.")
emitUiEvent("Internet Issue", "No Internet Connection")
return
}
val token = "Bearer ${viewModel.sharedPrefs.getString(KEY_TOKEN)}"
val otpCode = viewModel.state.value.verificationState?.otpCode.orEmpty()
Timber.d("Verifying OTP with token: $token and otpCode: $otpCode")
viewModel.sharedService.verifyEmail(token, ReqVerifyEmail(token = otpCode))
.enqueue(createOtpVerificationCallback())
updateVerificationState { it.copy(permitEmailRevision = true) }
}
private fun createOtpVerificationCallback() = object : Callback<ResVerifyEmail> {
override fun onResponse(
call: Call<ResVerifyEmail?>, response: Response<ResVerifyEmail?>
) {
Timber.d("OTP verification response received: ${response.code()}")
when {
response.isSuccessful && response.body() != null -> viewModel.viewModelScope.launch {
Timber.d("OTP verification successful.")
handleSuccessfulVerification()
}
response.code() == 410 -> {
Timber.w("OTP verification failed: Token is invalid.")
emitUiEvent("OTP Error", "The time has been finished.")
}
else -> {
Timber.e("OTP verification failed with code: ${response.code()}.")
handleVerificationError(response)
}
}
}
override fun onFailure(call: Call<ResVerifyEmail?>, t: Throwable) {
Timber.e("OTP verification request failed: ${t.localizedMessage}")
emitUiEvent("Wrong OTP", t.message.toString())
}
}
private suspend fun handleSuccessfulVerification() {
Timber.d("Handling successful OTP verification.")/* sharedPrefs.setLong(
SEND_OTP_TIME_MILLIS, System.currentTimeMillis()
)*/
viewModel.updateState(
viewModel.state.value.copy(
onboardState = OnboardState(), verificationState = VerificationState()
)
)
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(createNavigation(Screen.Onboarding, Screen.Verification))
)
viewModel.sharedPrefs.setBoolean(VERIFICATION_PASSED_BOOLEAN, true)
}
private fun handleVerificationError(response: Response<ResVerifyEmail?>) {
Timber.e("Handling OTP verification error. Response code: ${response.code()}")
val errorBody =
Gson().fromJson<Message>(response.errorBody()?.string(), Message::class.java)
Timber.e("Error body message: ${errorBody?.message}")
emitUiEvent("OTP Error", errorBody?.message.orEmpty())
}
private suspend fun handleOtpChanged(otp: String) {
Timber.d("Handling OTP change: $otp")
updateVerificationState { it.copy(otpCode = otp, isOtpCorrect = otp.length == 6) }
if (otp.length == 6) {
Timber.d("OTP is valid. Attempting verification.")
handleOtpVerification()
}
}
private suspend fun navigateToChangeEmail(email: String) {
Timber.d("Navigating to VerificationChangeEmail with email: $email")
updateVerificationChangeEmailState(email)
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
createNavigation(
Screen.VerificationChangeEmail, Screen.Verification, false
)
)
)
}
private fun createNavigation(screen: Screen, popUpTo: Screen, inclusive: Boolean = true) =
NavigateTo(screen, popUpTo, inclusive, createDestinationChangedListener())
private fun createDestinationChangedListener() =
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController, destination: NavDestination, arguments: Bundle?
) {
Timber.d("Navigation changed to destination: ${destination.route}")
controller.removeOnDestinationChangedListener(this)
}
}
private fun updateVerificationState(update: (VerificationState) -> VerificationState) {
Timber.d("Updating verification state.")
viewModel.updateState(
viewModel.state.value.copy(
verificationState = viewModel.state.value.verificationState?.let(update)
)
)
}
private fun updateVerificationChangeEmailState(email: String) {
Timber.d("Updating VerificationChangeEmailState with email: $email")
viewModel.updateState(
viewModel.state.value.copy(ChangeEmailState = ChangeEmailState(email = email))
)
}
private fun emitUiEvent(title: String, message: String) {
Timber.d("Emitting UI Event with title: $title, message: $message")
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(ShowSnackbar(title, message))
}
}
}
//************************************************************************************************************************************************
inner class LoginHandler(private val viewModel: AuthViewModel) {
suspend fun handle(intent: AuthIntent.LoginIntent) {
when (intent) {
is AuthIntent.LoginIntent.Login -> {
Timber.d("Handling LoginIntent.Login.")
handleLoginIntent(intent)
}
is AuthIntent.LoginIntent.OnForgotPasswordChanged -> {
Timber.d("Handling OnForgotPasswordChanged with email: ${intent.email}")
handleForgotPasswordChange(intent)
}
is AuthIntent.LoginIntent.ForgotPassword -> {
Timber.d("Handling ForgotPasswordIntent for email: ${intent.forgotEmail}")
handleForgotPassword(intent)
}
is AuthIntent.LoginIntent.OnEmailChanged -> {
Timber.d("Handling OnEmailChanged with email: ${intent.email}")
handleEmailChange(intent)
}
is AuthIntent.LoginIntent.OnPasswordChanged -> {
Timber.d("Handling OnPasswordChanged.")
handlePasswordChange(intent)
}
}
}
private fun handleLoginIntent(intent: AuthIntent.LoginIntent.Login) {
Timber.i("Login intent received with email/username: ${intent.emailOrUsername}.")
if (state.value.loginState!!.loginClickable == false) {
Timber.w("Login button is not clickable. Skipping login process.")
return
}
checkLogin(intent.emailOrUsername, intent.password)
}
private fun handleForgotPasswordChange(intent: AuthIntent.LoginIntent.OnForgotPasswordChanged) {
Timber.d(
if (isEmailValid(intent.email)) "Email is valid for forgot password."
else "Email is invalid for forgot password."
)
Timber.d("Updating forgetPasswordEmailCorrect to: ${isEmailValid(intent.email)}")
viewModel.updateState(
viewModel.state.value.copy(
loginState = viewModel.state.value.loginState!!.copy(
forgetPasswordEmailCorrect = isEmailValid(intent.email),
email = intent.email
)
)
)
}
private fun handleForgotPassword(intent: AuthIntent.LoginIntent.ForgotPassword) {
Timber.i("Processing forgot password for email: ${intent.forgotEmail}")
if (viewModel.state.value.loginState!!.forgetPasswordEmailCorrect == false) {
Timber.w("Forgot password attempt with invalid email format.")
return
}
if (!viewModel.networkManager.isNetworkConnected()) {
Timber.e("Network is not connected. Cannot proceed with forgot password.")
emitUiEvent("Internet Issue", "No Internet Connection")
return
}
Timber.i("Initiating forgot password request.")
viewModel.sharedService.forgotPassword(ResVerifyEmail(email = intent.forgotEmail))
.enqueue(createForgotPasswordCallback(intent.forgotEmail))
}
private fun handleEmailChange(intent: AuthIntent.LoginIntent.OnEmailChanged) {
Timber.d("Email changed to: ${intent.email}")
viewModel.updateState(
viewModel.state.value.copy(
loginState = viewModel.state.value.loginState!!.copy(email = intent.email)
)
)
Timber.d("Re-validating login fields after email change.")
validationLogin(
viewModel.state.value.loginState!!.email,
viewModel.state.value.loginState!!.password
)
}
private fun handlePasswordChange(intent: AuthIntent.LoginIntent.OnPasswordChanged) {
Timber.d("Password change detected.")
viewModel.updateState(
viewModel.state.value.copy(
loginState = viewModel.state.value.loginState!!.copy(password = intent.password)
)
)
Timber.d("Re-validating login fields after password change.")
validationLogin(
viewModel.state.value.loginState!!.email,
viewModel.state.value.loginState!!.password
)
}
private fun checkLogin(emailOrUsername: String, password: String) {
Timber.d("Checking login with email/username: $emailOrUsername.")
if (!viewModel.networkManager.isNetworkConnected()) {
Timber.e("Network is not connected. Cannot proceed with login.")
emitUiEvent("Internet Issue", "No Internet Connection")
return
}
when {
password.isEmpty() -> {
Timber.w("Login failed: Password is empty.")
emitUiEvent("Login failed", "Password is empty")
}
emailOrUsername.isEmpty() -> {
Timber.w("Login failed: Username or email is empty.")
emitUiEvent("Login failed", "Username or email is empty")
}
else -> {
Timber.i("All fields valid. Proceeding with login.")
performLogin(emailOrUsername, password)
}
}
}
private fun performLogin(emailOrUsername: String, password: String) {
Timber.d("Sending login request with email/username: $emailOrUsername.")
viewModel.sharedService.login(ReqLogin(email = emailOrUsername, password = password))
.enqueue(createLoginCallback())
}
private fun validationLogin(email: String, password: String) {
Timber.d("Validating login fields: email=$email, password length=${password.length}.")
val loginState = when {
email.isEmpty() || password.isEmpty() -> {
Timber.w("Validation failed: Either email or password is empty.")
LoginState.ERROR_EMPTY_FIELDS
}
!isEmailValid(email) -> {
Timber.w("Validation failed: Invalid email format.")
LoginState.ERROR_INVALID_EMAIL
}
else -> {
Timber.d("Validation successful: All fields are valid.")
LoginState.VALID
}
}
Timber.d("Updating login state with validation results.")
viewModel.updateState(
viewModel.state.value.copy(
loginState = viewModel.state.value.loginState!!.copy(
formError = loginState.errorMessage,
formDataValid = loginState.isValid,
loginClickable = isEmailValid(state.value.loginState!!.email) && state.value.loginState!!.password.isNotEmpty()
)
)
)
}
private fun createForgotPasswordCallback(email: String) = object : Callback<Unit> {
override fun onResponse(call: Call<Unit?>, response: Response<Unit?>) {
if (response.isSuccessful) {
Timber.i("Forgot password email sent successfully to $email.")
viewModel.viewModelScope.launch {
Timber.d("Navigating to ForgotPasswordVerification screen.")
navigateToForgotPasswordVerification()
}
} else {
Timber.e("Failed to send forgot password email to $email. Response code: ${response.code()}")
emitUiEvent("An error occurred", "Forgot Password email not sent successfully")
}
}
override fun onFailure(call: Call<Unit?>, t: Throwable) {
Timber.e("Failed to send forgot password email due to: ${t.message}")
emitUiEvent("An error occurred", "Forgot Password email not sent successfully")
}
}
private suspend fun navigateToForgotPasswordVerification() {
Timber.d("Updating ForgotPasswordState before navigation.")
updateForgotPasswordState()
Timber.i("Navigating to ForgotPassword screen.")
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.ForgotPassword,
popUpTo = Screen.Login,
inclusive = true,
onDestinationChangedListener = createDestinationChangedListener()
)
)
)
}
private fun updateForgotPasswordState() {
Timber.d("Setting ForgotPasswordState with email: ${state.value.loginState!!.email}")
viewModel.updateState(
viewModel.state.value.copy(
forgotPasswordState = ForgotPasswordState(
email = state.value.loginState!!.email
)
)
)
}
private fun createLoginCallback() = object : Callback<SignUpResponse> {
override fun onResponse(
call: Call<SignUpResponse?>, response: Response<SignUpResponse?>
) {
if (response.isSuccessful && response.body() != null) {
Timber.i("Login successful. Saving user data.")
val body = response.body()!!
UserDataManager(viewModel.sharedPrefs).savePrimaryData(body)
onLoginSuccess()
} else {
Timber.e(
"Login failed. Response code: ${response.code()}, errorBody: ${
response.errorBody()?.string()
}"
)
emitUiEvent("Login failed", "Invalid credentials")
}
}
override fun onFailure(call: Call<SignUpResponse?>, t: Throwable) {
Timber.e("Login request failed due to: ${t.message}")
val errorMessage =
t.localizedMessage ?: "An unexpected error occurred. Please try again later."
emitUiEvent("Login failed", errorMessage)
}
}
private fun onLoginSuccess() {
Timber.i("Login process completed successfully.")
viewModelScope.launch {
_Auth_uiEvent.emit(ExecuteNavigation(PopSpecific(Screen.Landing, false)))
}
viewModel.viewModelScope.launch {
emitUiEvent("You have entered your account successfully", "Login successful")
Timber.i("Navigating to Home screen.")
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.Main,
popUpTo = Screen.Landing,
inclusive = true,
onDestinationChangedListener = createDestinationChangedListener()
)
)
)
}
}
private fun emitUiEvent(title: String, message: String) {
Timber.d("Emitting UI event with title: $title, message: $message")
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(ShowSnackbar(title, message))
}
}
private fun createDestinationChangedListener() =
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController, destination: NavDestination, arguments: Bundle?
) {
Timber.d("Navigation occurred to destination: ${destination.route}")
}
}
private fun isEmailValid(email: String): Boolean {
Timber.d("Validating email: $email")
return Patterns.EMAIL_ADDRESS.matcher(email).matches()
}
}
private enum class LoginState(val errorMessage: String, val isValid: Boolean) {
ERROR_EMPTY_FIELDS(
"Email or password can't be empty", false
),
ERROR_INVALID_EMAIL("Email is not valid", false), VALID("", true)
}
//************************************************************************************************************************************************
inner class ChangeEmailHandler(private val viewModel: AuthViewModel) {
suspend fun handle(intent: AuthIntent.ChangeEmailIntent) {
when (intent) {
is AuthIntent.ChangeEmailIntent.UpdateEmail -> handleUpdateEmail(intent.email)
AuthIntent.ChangeEmailIntent.ResendCode -> handleResendCode()
AuthIntent.ChangeEmailIntent.BackToLogin -> handleBackToLogin()
}
}
private suspend fun handleUpdateEmail(email: String) {
Timber.i("Update email intent received with email: $email")
val token = viewModel.sharedPrefs.getString(KEY_TOKEN) ?: ""
Timber.d("Token retrieved from shared preferences: $token")
requestChangeEmail(token, email, Screen.VerificationChangeEmail)
}
private suspend fun handleResendCode() {
Timber.i("Resend link intent received on VerificationChangeEmail screen.")
val token = viewModel.sharedPrefs.getString(KEY_TOKEN) ?: ""
Timber.d("Token retrieved from shared preferences for resend link: $token")
requestResendOtp(token, Screen.VerificationChangeEmail)
}
private suspend fun handleBackToLogin() {
Timber.i("Back to login intent received from VerificationChangeEmail screen.")
viewModel.updateState(
viewModel.state.value.copy(
loginState = LoginState(), ChangeEmailState = ChangeEmailState()
)
)
navigateToLoginScreen()
}
private fun navigateToLoginScreen() {
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.Login,
popUpTo = Screen.VerificationChangeEmail,
inclusive = true,
onDestinationChangedListener = createDestinationChangedListener(Screen.Login)
)
)
)
}
}
private fun requestChangeEmail(token: String, email: String, fromScreen: Screen) {
Timber.i("Requesting email change to: $email with token from screen: ${fromScreen.route}")
if (!viewModel.networkManager.isNetworkConnected()) {
emitSnackbar("Internet Issue", "No Internet Connection")
return
}
viewModel.sharedService.updateEmail("Bearer $token", ResVerifyEmail(email = email))
.enqueue(object : Callback<ResVerifyEmail> {
override fun onResponse(
call: Call<ResVerifyEmail?>, response: Response<ResVerifyEmail?>
) {
handleEmailChangeResponse(response, email, fromScreen)
}
override fun onFailure(call: Call<ResVerifyEmail?>, t: Throwable) {
handleRequestFailure(
"Email change request failed due to network or server error", t
)
}
})
}
private fun handleEmailChangeResponse(
response: Response<ResVerifyEmail?>, email: String, fromScreen: Screen
) {
if (response.isSuccessful && response.body() != null) {
Timber.i("Email change successful. Updating shared preferences and state.")
viewModel.sharedPrefs.setString(KEY_EMAIL, email)
viewModel.updateState(
viewModel.state.value.copy(
ChangeEmailState = ChangeEmailState(),
verificationState = VerificationState(email = email)
)
)
emitSnackbar(
"Email Address Updated",
"Your email address has been successfully updated, and a verification email has been sent to your new address."
)
navigateToVerificationScreen(fromScreen)
} else if (response.code() == 422) {
handleInvalidRequest(response)
}
}
private fun requestResendOtp(token: String, fromScreen: Screen) {
Timber.i("Initiating request to resend OTP with token: $token from screen: ${fromScreen.route}")
if (!viewModel.networkManager.isNetworkConnected()) {
emitSnackbar("Internet Issue", "No Internet Connection")
return
}
viewModel.sharedService.resendVerificationEmail("Bearer $token")
.enqueue(object : Callback<ResVerifyEmail> {
override fun onResponse(
call: Call<ResVerifyEmail?>, response: Response<ResVerifyEmail?>
) {
handleResendOtpResponse(response, fromScreen)
}
override fun onFailure(call: Call<ResVerifyEmail?>, t: Throwable) {
handleRequestFailure(
"Resend OTP request failed due to network or server error", t
)
}
})
}
private fun handleResendOtpResponse(
response: Response<ResVerifyEmail?>, fromScreen: Screen
) {
if (response.isSuccessful && response.body() != null) {
Timber.i("Verification link successfully resent.")
emitSnackbar(
"Re-sent Verification Link",
"We've re-sent the verification link to your email. Please check your inbox."
)
navigateToVerificationScreen(fromScreen, inclusive = false)
} else if (response.code() == 425) {
emitSnackbar(
"Re-sent Verification Code failed",
"You have to wait 2 minutes before sending another request."
)
} else {
handleGenericFailure(response, "Failed to resend verification link.")
}
}
private fun handleInvalidRequest(response: Response<ResVerifyEmail?>) {
Timber.w("Invalid request error code 422.")
val errorBody = Gson().fromJson<Message>(
response.errorBody()?.string(), Message::class.java
)
emitSnackbar("Email Address didn't update", errorBody.message)
}
private fun handleRequestFailure(message: String, throwable: Throwable) {
Timber.e("$message: ${throwable.message}")
emitSnackbar("Failure", "An error occurred. Please try again later.")
}
private fun handleGenericFailure(response: Response<ResVerifyEmail?>, defaultMsg: String) {
Timber.e("Request failed with error code: ${response.code()}")
val errorMsg = response.errorBody()?.string() ?: defaultMsg
emitSnackbar("Failure", errorMsg)
}
private fun emitSnackbar(title: String, message: String) {
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(ShowSnackbar(title, message))
}
}
private fun navigateToVerificationScreen(fromScreen: Screen, inclusive: Boolean = true) {
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.Verification,
inclusive = inclusive,
popUpTo = fromScreen,
onDestinationChangedListener = createDestinationChangedListener(Screen.Verification)
)
)
)
}
}
private fun createDestinationChangedListener(destination: Screen) =
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController, destination: NavDestination, arguments: Bundle?
) {
Timber.d("Navigated to: ${destination.route}")
controller.removeOnDestinationChangedListener(this)
}
}
}
//************************************************************************************************************************************************
inner class ForgotPasswordHandler(private val viewModel: AuthViewModel) {
suspend fun handle(intent: AuthIntent.ForgotPasswordIntent) {
when (intent) {
AuthIntent.ForgotPasswordIntent.ResendCode -> handleResendCode()
is AuthIntent.ForgotPasswordIntent.OnOtpChanged -> handleOtpChanged(intent.otp)
is AuthIntent.ForgotPasswordIntent.CheckRemainTime -> handleCheckEnableButton(intent.time)
}
}
private fun handleResendCode() {
if (state.value.forgotPasswordState?.timeEnd == false) {
emitSnackbar("OTP Error", "The time has been finished.")
return
}
requestResendOtp()
}
private fun handleOtpChanged(otp: String) {
updateVerificationState(otp)
if (otp.length == 6) {
if (state.value.forgotPasswordState!!.timeEnd == false) {
checkOtpVerification(otp)
} else {
emitSnackbar("OTP Error", "The time has been finished.")
}
}
}
private fun updateVerificationState(otp: String) {
viewModel.updateState(
viewModel.state.value.copy(
verificationState = viewModel.state.value.verificationState?.copy(
otpCode = otp, isOtpCorrect = otp.length == 6
) ?: VerificationState(otpCode = otp, isOtpCorrect = otp.length == 6)
)
)
}
private fun handleCheckEnableButton(time: Int) {
state.value.forgotPasswordState?.timeEnd = time <= 0
}
private fun checkOtpVerification(otp: String) {
viewModel.sharedService.verifyResetPasswordToken(ReqVerifyResetPasswordToken(token = otp))
.enqueue(object : Callback<ResVerifyResetPasswordToken> {
override fun onResponse(
call: Call<ResVerifyResetPasswordToken?>,
response: Response<ResVerifyResetPasswordToken?>
) {
if (response.isSuccessful && response.body() != null) handleOtpSuccess(otp)
else handleOtpError(response.code() == 401)
}
override fun onFailure(call: Call<ResVerifyResetPasswordToken?>, t: Throwable) {
emitSnackbar("Error", "Failed to verify OTP. Please try again.")
}
})
}
private fun handleOtpSuccess(otp: String) {
Timber.d("OTP verification successful.")
val email = state.value.forgotPasswordState?.email.orEmpty()
updateState(
state.value.copy(
forgotPasswordState = ForgotPasswordState(),
resetPasswordState = ResetPasswordState(
otp = otp, email = email
)
)
)
navigateToNextScreen()
}
private fun handleOtpError(isUnauthorized: Boolean) {
emitSnackbar(
if (isUnauthorized) "Invalid OTP" else "Verification Failed",
if (isUnauthorized) "The code you entered is incorrect." else "code not valid."
)
}
private fun requestResendOtp() {
if (!viewModel.networkManager.isNetworkConnected()) {
emitSnackbar("Internet Issue", "No Internet Connection")
return
}
viewModel.sharedService.forgotPassword(
ResVerifyEmail(email = state.value.forgotPasswordState!!.email)
).enqueue(object : Callback<Unit> {
override fun onResponse(call: Call<Unit>, response: Response<Unit>) {
if (response.isSuccessful) handleOtpResendSuccess()
else handleOtpResendFailure(response.code(), response)
}
override fun onFailure(call: Call<Unit>, t: Throwable) {
emitSnackbar("Action Required", "Something went wrong, please try again.")
}
})
}
private fun handleOtpResendSuccess() {
state.value.forgotPasswordState?.apply {
timeEnd = false
resetTimer = true
}
emitSnackbar(
title = "Re-sent Verification Link",
message = "We've re-sent the verification link to your email. Please check your inbox."
)
}
private fun handleOtpResendFailure(code: Int, response: Response<Unit>) {
val errorMessage =
response.errorBody()?.string() ?: "Failed to resend verification link."
emitSnackbar(
title = "Re-sent Verification Code failed",
message = if (code == 425) "You have to wait 2 minutes before sending another request."
else errorMessage
)
}
private fun emitSnackbar(title: String, message: String) {
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(ShowSnackbar(title, message))
}
}
private fun navigateToNextScreen() {
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.ResetPassword,
inclusive = true,
popUpTo = Screen.ForgotPassword,
onDestinationChangedListener = createDestinationListener()
)
)
)
}
}
private fun createDestinationListener() =
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController, destination: NavDestination, arguments: Bundle?
) {
controller.removeOnDestinationChangedListener(this)
}
}
}
//************************************************************************************************************************************************
inner class ResetPasswordHandler(private val viewModel: AuthViewModel) {
suspend fun handle(intent: AuthIntent.ResetPasswordIntent) {
when (intent) {
is AuthIntent.ResetPasswordIntent.OnPasswordChanged -> updatePasswordState(intent.password)
is AuthIntent.ResetPasswordIntent.OnConfirmPasswordChanged -> updatePasswordConfirmationState(
intent.password
)
is AuthIntent.ResetPasswordIntent.UpdatePassword -> processPasswordUpdate()
}
}
private fun updatePasswordState(password: String) {
viewModel.updateState(
viewModel.state.value.copy(
resetPasswordState = viewModel.state.value.resetPasswordState!!.copy(
password = password, startedTyping = true
)
)
)
validateResetPasswordForm()
}
private fun updatePasswordConfirmationState(passwordConfirmation: String) {
viewModel.updateState(
viewModel.state.value.copy(
resetPasswordState = viewModel.state.value.resetPasswordState!!.copy(
passwordConfirmation = passwordConfirmation, startedTyping = true
)
)
)
validateResetPasswordForm()
}
private fun processPasswordUpdate() {
val currentState = viewModel.state.value.resetPasswordState!!
if (!currentState.resetClickable) return
if (!viewModel.networkManager.isNetworkConnected()) {
emitSnackbar("Internet Issue", "No Internet Connection")
return
}
if (!currentState.formDataValid) {
emitSnackbar("Reset Password Failed", currentState.formError)
return
}
val otp = state.value.resetPasswordState!!.otp
val resetRequest = ResetPasswordRequest(
password = currentState.password,
passwordConfirmation = currentState.passwordConfirmation,
otp = otp
)
updateResetPasswordStateLoading(true)
viewModel.sharedService.resetPassword(resetRequest)
.enqueue(createPasswordUpdateCallback())
}
private fun validateResetPasswordForm() {
val currentState = viewModel.state.value.resetPasswordState!!
val password = currentState.password
val passwordConfirmation = currentState.passwordConfirmation
val issue = when {
password.isEmpty() -> "Password can't be empty"
password.length < 8 -> "Password must have at least 8 characters"
password != passwordConfirmation -> "Passwords do not match"
else -> null
}
viewModel.updateState(
viewModel.state.value.copy(
resetPasswordState = currentState.copy(
formError = issue.orEmpty(),
formDataValid = issue == null,
is8Characters = password.length >= 8,
passwordsMatch = password == passwordConfirmation
)
)
)
}
private fun createPasswordUpdateCallback(): Callback<ResetPasswordResponse> {
return object : Callback<ResetPasswordResponse> {
override fun onResponse(
call: Call<ResetPasswordResponse>, response: Response<ResetPasswordResponse>
) {
handleResetPasswordResponse(response)
}
override fun onFailure(call: Call<ResetPasswordResponse>, t: Throwable) {
emitSnackbar("Reset Failed", t.message.orEmpty())
updateResetPasswordStateLoading(false)
}
}
}
private fun handleResetPasswordResponse(response: Response<ResetPasswordResponse>) {
val currentState = viewModel.state.value.resetPasswordState!!
if (response.isSuccessful && response.body() != null) {
resetStateAndNavigateToLogin()
} else {
emitSnackbar(
"Reset Failed",
response.errorBody()?.string()?.let(::parseErrorMessage).orEmpty()
)
}
updateResetPasswordStateLoading(false)
}
private fun resetStateAndNavigateToLogin() {
viewModel.updateState(
viewModel.state.value.copy(resetPasswordState = ResetPasswordState())
)
viewModel.viewModelScope.launch {
emitSnackbar("Password Reset", "Your password has been successfully updated.")
delay(2000)
emitNavigationToLogin()
}
}
private fun updateResetPasswordStateLoading(isLoading: Boolean) {
val currentState = viewModel.state.value.resetPasswordState!!
viewModel.updateState(
viewModel.state.value.copy(
resetPasswordState = currentState.copy(
isLoading = isLoading, resetClickable = !isLoading
)
)
)
}
private fun emitSnackbar(title: String, message: String) {
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(ShowSnackbar(title, message))
}
}
private fun emitNavigationToLogin() {
viewModel.viewModelScope.launch {
viewModel._Auth_uiEvent.emit(
ExecuteNavigation(
navigationEvent = NavigateTo(
screen = Screen.Login,
popUpTo = Screen.ResetPassword,
inclusive = true,
onDestinationChangedListener = createDestinationListener()
)
)
)
}
}
private fun createDestinationListener() =
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController, destination: NavDestination, arguments: Bundle?
) {
controller.removeOnDestinationChangedListener(this)
}
}
private fun parseErrorMessage(json: String): String {
return Gson().fromJson<Message>(json, Message::class.java).message
}
}
inner class MutualHandler(val viewModel: AuthViewModel) {
suspend fun handle(intent: AuthIntent.MutualIntent) {
when (intent) {
is AuthIntent.MutualIntent.ChangeInsetsVisibility -> {
updateState(
viewModel.state.value.copy(
navigationBarVisibility = !viewModel.state.value.navigationBarVisibility,
statusBarVisibility = !viewModel.state.value.statusBarVisibility
)
)
}
}
}
}
// Placeholder data classes for API calls (adjust based on actual API requirements)
data class ResetPasswordRequest(
@SerializedName("new_password") val password: String,
@SerializedName("new_password_confirmation") val passwordConfirmation: String,
@SerializedName("reset_password_token") val otp: String
)
data class ResetPasswordResponse(
val success: Boolean, val message: String
)
}
sealed class AuthUiEvent {
data class ShowSnackbar(
val title: String, val message: String
) : AuthUiEvent()
object AnimateItem : AuthUiEvent()
data class ExecuteNavigation(val navigationEvent: NavigationEvent) : AuthUiEvent()
object ShowDialog : AuthUiEvent()
}
@Singleton
data class AuthState(
var navigationBarVisibility: Boolean = true,
var statusBarVisibility: Boolean = true,
var signupState: SignupState? = null,
var onboardState: OnboardState? = null,
var loginState: LoginState? = null,
var verificationState: VerificationState? = null,
var ChangeEmailState: ChangeEmailState? = null,
// var verificationResendEmailState: VerificationResendEmailState? = null,
var forgotPasswordState: ForgotPasswordState? = null,
var resetPasswordState: ResetPasswordState? = null,
)
data class LoginState(
var password: String = "",
var loginClickable: Boolean = false,
var email: String = "",
var formDataValid: Boolean = false,
val isEmailVerified: Boolean = false,
var error: String = "",
var formError: String = "",
var forgetPasswordEmailCorrect: Boolean = false,
var loginSuccess: Boolean = false
)
data class SignupState(
var error: String = "",
var startedTyping: Boolean = false,
var passwordsMatch: Boolean = false,
var is8Characters: Boolean = false,
var fieldsEmpty: Boolean = true,
var isLoadingSignUp: Boolean = false,
var formError: String? = null,
var formDataValid: Boolean = false,
var email: String = (""),
var password: String = "",
var signupClickable: Boolean = true,
var passwordConfirmation: String = "",
)
data class OnboardState(
var userData: UserData? = null,
var email: String = "",
var password: String = "",
var firstName: String = "",
var lastName: String = "",
var userName: String = "",
var error: String = "",
var formDataValid: Boolean = false,
)
data class VerificationState(
var otpCode: String = "",
var isOtpCorrect: Boolean = false,
var otpRemainTime: Long = 0L,
var email: String = "",
var changingEmailEnabled: Boolean = false,
var permitEmailRevision: Boolean = false,
)
data class ChangeEmailState(
var email: String = "",
)
/*data class VerificationResendEmailState(
var email: String = ""
)*/
data class ForgotPasswordState(
var email: String = "", var timeEnd: Boolean = false,
// var canRequestSendOtp: Boolean = false,
var resetTimer: Boolean = true,
)
data class ResetPasswordState(
var email: String = "",
var otp: String = "",
var startedTyping: Boolean = false,
var password: String = "",
var passwordConfirmation: String = "",
var is8Characters: Boolean = false,
var passwordsMatch: Boolean = false,
var resetClickable: Boolean = true,
var formDataValid: Boolean = false,
var isLoading: Boolean = false,
var formError: String = "",
var error: String = ""
)
sealed class AuthIntent {
sealed class SplashIntent : AuthIntent() {
object CheckDecision : SplashIntent()
}
sealed class MutualIntent : AuthIntent() {
class ChangeInsetsVisibility(val statusBar: Boolean, navigationBar: Boolean) :
MutualIntent()
}
sealed class SignupIntent : AuthIntent() {
data class SignUp(
val email: String, val password: String, val passwordConfirmation: String
) : SignupIntent()
data class OnEmailChanged(var email: String) : SignupIntent()
data class OnPasswordChanged(var password: String) : SignupIntent()
object SignUpWithGoogle : SignupIntent()
data class OnPasswordConfirmationChanged(var passwordConfirmation: String) : SignupIntent()
}
sealed class LandingIntent : AuthIntent() {
object gotoSignup : LandingIntent()
object gotoLogin : LandingIntent()
}
sealed class LoginIntent : AuthIntent() {
data class Login(val emailOrUsername: String, val password: String) : LoginIntent()
data class OnEmailChanged(val email: String) : LoginIntent()
data class OnPasswordChanged(val password: String) : LoginIntent()
data class ForgotPassword(val forgotEmail: String) : LoginIntent()
data class OnForgotPasswordChanged(val email: String) : LoginIntent()
}
sealed class OnboardIntent : AuthIntent() {
data class Onboard(var firstName: String, var lastName: String, var userName: String) :
OnboardIntent()
data class OnFirstNameChanged(var firstName: String) : OnboardIntent()
data class OnUserNameChanged(var userName: String) : OnboardIntent()
data class OnLastNameChanged(var lastName: String) : OnboardIntent()
}
sealed class VerificationIntent : AuthIntent() {
data class OnOtpChanged(val otp: String) : VerificationIntent()
data class OnOtpVerifyPressed(val otp: String) : VerificationIntent()
//data class GotoVerificationResendCEmail(val email: String) : VerificationIntent()
data class GotoChangeEmail(val email: String) : VerificationIntent()
object ResendCode : VerificationIntent()
data class UpdateTimer(val time: Long) : VerificationIntent()
}
sealed class ChangeEmailIntent : AuthIntent() {
data class UpdateEmail(val email: String) : ChangeEmailIntent()
object BackToLogin : ChangeEmailIntent()
object ResendCode : ChangeEmailIntent()
}
/*
sealed class VerificationResendEmailIntent : AuthIntent() {
data class ChangeEmail(val email: String) : VerificationResendEmailIntent()
object ResendCode : VerificationResendEmailIntent()
object BackToLogin : VerificationResendEmailIntent()
}
*/
sealed class ForgotPasswordIntent : AuthIntent() {
data class OnOtpChanged(val otp: String) : ForgotPasswordIntent()
object ResendCode : ForgotPasswordIntent()
data class CheckRemainTime(val time: Int) : ForgotPasswordIntent()
}
sealed class ResetPasswordIntent : AuthIntent() {
data class OnPasswordChanged(val password: String) : ResetPasswordIntent()
data class OnConfirmPasswordChanged(val password: String) : ResetPasswordIntent()
data class UpdatePassword(val password: String, val passwordConfirmation: String) :
ResetPasswordIntent()
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\BaseViewModel.kt
```kt
package com.divadventure.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.divadventure.data.navigation.NavigationEvent
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
abstract class BaseViewModel<Intent, State>(initialState: State) : ViewModel() {
private val _state = MutableStateFlow(initialState)
val state: StateFlow<State> = _state.asStateFlow()
private val _navigationEvent = MutableSharedFlow<NavigationEvent>(replay = 0)
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()
private val intentChannel = Channel<Intent>(Channel.Factory.UNLIMITED)
init {
viewModelScope.launch {
for (intent in intentChannel) {
handleIntent(intent)
}
}
}
fun sendIntent(intent: Intent) {
intentChannel.trySend(intent).isSuccess
}
fun navigate(event: NavigationEvent) {
viewModelScope.launch {
_navigationEvent.emit(event)
}
}
fun updateState(newState: State) {
_state.value = newState
}
abstract suspend fun handleIntent(intent: Intent)
override fun onCleared() {
intentChannel.close()
super.onCleared()
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\HomeViewModel.kt
```kt
package com.divadventure.viewmodel
import android.os.Bundle
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.NavController.OnDestinationChangedListener
import androidx.navigation.NavDestination
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationEvent.NavigateAdventure
import com.divadventure.di.AuthPrefs.VERIFICATION_PASSED_BOOLEAN
import com.divadventure.di.SharedPrefs
import com.divadventure.di.UserPrefs.KEY_TOKEN
import com.divadventure.domain.models.Adventure
import com.divadventure.domain.models.AdventureType
import com.divadventure.domain.models.Filters
import com.divadventure.domain.models.Interest
import com.divadventure.domain.models.Meta
import com.divadventure.domain.usecase.AdventuresUseCase
import com.divadventure.domain.usecase.InterestsUseCase
import com.divadventure.domain.usecase.LocationsUseCase
import com.divadventure.util.NetworkManager
import com.google.android.gms.maps.model.LatLng
import com.google.android.libraries.places.api.model.AutocompletePrediction
import com.google.android.libraries.places.api.model.Place
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val networkManager: NetworkManager,
private val sharedPrefs: SharedPrefs,
private val locationsUseCase: LocationsUseCase, // Injected use case
private val adventuresUseCase: AdventuresUseCase, // Add AdventuresUseCase here
private val interestsUseCase: InterestsUseCase // Add InterestsUseCase here
) : BaseViewModel<HomeIntent, HomeState>(HomeState()) {
private val _uiEvent = MutableSharedFlow<HomeUiEvent>(replay = 0)
val uiEvent = _uiEvent.asSharedFlow()
override suspend fun handleIntent(intent: HomeIntent) {
when (intent) {
is HomeIntent.LoadAdventuresData -> {
if (isSearch(intent.query)) {
executeSearch(intent.query, true)
} else if (state.value.group.isNotEmpty() && state.value.group != "ALL") {
fetchAdventuresGroup(state.value.group, freshRequest = true)
} else {
getAllAdventures(true)
}
}
is HomeIntent.LoadMoreAdventuresData -> {
val meta = state.value.currentMetadata
// Exit early if meta or its nextPage is null
if (meta?.nextPage == null || meta.nextPage <= 0) return
when (state.value.lastLoadMoreState) {
LoadMoreState.NONE -> {
// No further action required for NONE state
}
LoadMoreState.GROUPS -> {
// Load more adventures for the selected group
fetchAdventuresGroup(state.value.group, freshRequest = false)
}
LoadMoreState.ALL -> {
// Load all adventures
getAllAdventures(freshGet = false)
}
LoadMoreState.SEARCH -> {
// Execute search query with fresh results
executeSearch(state.value.searchQuery, freshSearch = false)
}
}
}
is HomeIntent.Refresh -> {
// Handle refresh intent.
}
is HomeIntent.Logout -> {
// Handle logout intent.
}
is HomeIntent.ClearAllShared -> {
sharedPrefs.setString(KEY_TOKEN, null)
sharedPrefs.setBoolean(VERIFICATION_PASSED_BOOLEAN, false)
}
HomeIntent.SwitchShowSearchbar -> {
when (state.value.isSearchBarVisible) {
true -> {
clearFilterSort()
state.value.searchAdventuresList = mutableListOf<Adventure>()
}
false -> {
clearFilterSort()
}
}
updateState(
state.value.copy(isSearchBarVisible = !state.value.isSearchBarVisible)
)
}
HomeIntent.GotoFilter -> {
}
HomeIntent.ApplyFilter -> {
}
HomeIntent.ShowStartDateDialog -> {
_uiEvent.emit(HomeUiEvent.ShowStartDateDialog)
}
HomeIntent.ShowEndDateDialog -> {
_uiEvent.emit(HomeUiEvent.ShowEndDateDialog)
}
is HomeIntent.ApplyEndDate -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(endDate = intent.endDate)
)
)
}
is HomeIntent.ApplyInterests -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(interests = intent.interests)
)
)
}
is HomeIntent.ApplyLocation -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(
locationLAt = intent.lat, locationLng = intent.long
)
)
)
}
is HomeIntent.ApplyStartDate -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(startDate = intent.startDate)
)
)
}
is HomeIntent.ApplyStatus -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(endDate = intent.status)
)
)
}
is HomeIntent.LocationFieldChanged -> {
updateState(
state.value.copy(
locationsPredicted = locationsUseCase.predictLocations(intent.location)
.toMutableList()
)
)
}
is HomeIntent.LocationSelected -> {
clearPredictedLocations()
val location = locationsUseCase.goLocation(intent.location.placeId)
Timber.d("Selected location: $location")
updateState(
state.value.copy(
newLocation = location,
locationsPredicted = mutableListOf(),
filters = state.value.filters.copy(
locationLAt = location?.latLng?.latitude,
locationLng = location?.latLng?.longitude
)
)
)
}
is HomeIntent.SelectGroup -> {
when (state.value.isOnCalendar) {
true -> {
fetchCalendarAdventures(
getLaterDate(
state.value.startDateFilter,
state.value.startDateCalendar
) ?: "",
getSoonerDate(
state.value.endDateFilter,
state.value.endDateCalendar
) ?: "",
group = intent.group
)
}
false -> {
if (intent.group.toString() == "ALL") {
getAllAdventures(true)
} else {
fetchAdventuresGroup(intent.group, true)
}
}
}
}
is HomeIntent.SetEndDate -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(endDate = intent.endDate)
)
)
}
is HomeIntent.SetInterests -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(interests = intent.interests)
)
)
}
is HomeIntent.SetStartDate -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(startDate = intent.startDate)
)
)
}
is HomeIntent.SetStatus -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(state = intent.status)
)
)
}
HomeIntent.FetchInterests -> {
fetchInterests()
}
is HomeIntent.ApplySortBy -> {
updateState(
state.value.copy(
filters = state.value.filters.copy(orderBy = intent.sortBy)
)
)
}
is HomeIntent.LoadCalendarAdventures -> {
if (state.value.isSearchBarVisible) {
state.value.startDateFilter = intent.startDate
state.value.endDateCalendar = intent.endDate
fetchCalendarAdventures(
intent.startDate, intent.endDate, state.value.filters
)
} else {
state.value.startDateCalendar = intent.startDate
state.value.endDateCalendar = intent.endDate
fetchCalendarAdventures(
intent.startDate,
intent.endDate,
group = state.value.group,
)
}
}
is HomeIntent.SwitchCalendarColumn -> {
when (intent.status) {
0 -> {
Timber.d("Switching calendar column to non-calendar view")
state.value.isOnCalendar = false
}
1 -> {
Timber.d("Switching calendar column to calendar view")
state.value.isOnCalendar = true
}
}
}
is HomeIntent.MapClicked -> {
val location = locationsUseCase.goLocation(intent.location)
updateState(
state.value.copy(
newLocation = location, filters = state.value.filters.copy(
locationLAt = intent.location.latitude,
locationLng = intent.location.longitude
)
)
)
}
HomeIntent.ApplyAdevntureType -> {
if (state.value.searchAdventuresList.isNullOrEmpty()) {
state.value.mainAdventuresList.forEach {
it.adventureType = adventuresUseCase.checkAdventureType(it)
}
} else {
state.value.searchAdventuresList.forEach {
it.adventureType = adventuresUseCase.checkAdventureType(it)
}
}
}
is HomeIntent.HandleAdventureClick -> {
val adventure = intent.adventure
when (adventure.adventureType) {
AdventureType.Join -> {
if (adventure.joinRequestNeeded) {
updateAdventureState(adventure.id, AdventureType.Pending)
} else {
// As per issue: "if join request need is false , it should change to Leave"
updateAdventureState(adventure.id, AdventureType.Leave)
// Potentially, this could also mean remove it immediately if it becomes "Leave"
// removeAdventureFromLists(adventure.id) // Decided to keep it visible as "Leave" first
}
}
AdventureType.Going -> {
updateState(
state.value.copy(
showGoingBottomSheet = true,
selectedAdventureForBottomSheet = adventure
)
)
}
AdventureType.Pending -> {
updateAdventureState(adventure.id, AdventureType.Join)
}
AdventureType.Leave -> {
// When a "Leave" button is clicked (originally it was "Leave")
removeAdventureFromLists(adventure.id)
}
AdventureType.Manage -> {
// This case should ideally not be sent to HomeViewModel via HandleAdventureClick
// as MainViewModel handles navigation for Manage. Log if it occurs.
Timber.w("HandleAdventureClick received for Manage type: ${adventure.id}")
manageAdventure(adventure)
}
null -> {
// Handle null case if necessary, perhaps log an error or default behavior
Timber.e("AdventureType is null for adventure: ${adventure.id}")
}
}
}
is HomeIntent.HandleBottomSheetAction -> {
val adventureToActOn = state.value.selectedAdventureForBottomSheet
if (adventureToActOn == null || adventureToActOn.id != intent.adventureId) {
// Defensive check
updateState(state.value.copy(showGoingBottomSheet = false, selectedAdventureForBottomSheet = null))
return
}
when (intent.action) {
"Yes" -> { // Clicked "Yes" on "Going" bottom sheet
updateAdventureState(adventureToActOn.id, AdventureType.Leave)
}
"No" -> { // Clicked "No" on "Going" bottom sheet
removeAdventureFromLists(adventureToActOn.id)
}
"Maybe" -> {
// Just close the bottom sheet, do nothing else to the adventure item
}
}
updateState(state.value.copy(showGoingBottomSheet = false, selectedAdventureForBottomSheet = null))
}
is HomeIntent.DismissBottomSheet -> {
updateState(state.value.copy(showGoingBottomSheet = false, selectedAdventureForBottomSheet = null))
}
}
}
private fun manageAdventure(adventure: Adventure) {
// Proceed with existing navigation logic for Manage
viewModelScope.launch {
try {
val safeAdventure = adventure.copy(
adventureRequest = adventure.adventureRequest ?: emptyList(),
adventurers = adventure.adventurers.ifEmpty { emptyList() })
_uiEvent.emit(
HomeUiEvent.NavigateToNextScreen(
NavigateAdventure(
adventure = safeAdventure,
onDestinationChangedListener = object : OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
removeListenerAfter = true
)
)
)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun updateAdventureState(adventureId: String, newType: AdventureType) {
val currentMainList = state.value.mainAdventuresList.toMutableList()
val mainIndex = currentMainList.indexOfFirst { it.id == adventureId }
if (mainIndex != -1) {
currentMainList[mainIndex] = currentMainList[mainIndex].copy(adventureType = newType)
}
val currentSearchList = state.value.searchAdventuresList.toMutableList()
val searchIndex = currentSearchList.indexOfFirst { it.id == adventureId }
if (searchIndex != -1) {
currentSearchList[searchIndex] = currentSearchList[searchIndex].copy(adventureType = newType)
}
updateState(state.value.copy(mainAdventuresList = currentMainList, searchAdventuresList = currentSearchList))
}
private fun removeAdventureFromLists(adventureId: String) {
val currentMainList = state.value.mainAdventuresList.toMutableList()
currentMainList.removeAll { it.id == adventureId }
val currentSearchList = state.value.searchAdventuresList.toMutableList()
currentSearchList.removeAll { it.id == adventureId }
updateState(state.value.copy(mainAdventuresList = currentMainList, searchAdventuresList = currentSearchList))
}
private fun getLaterDate(date1String: String, date2String: String): String? {
val formatter = DateTimeFormatter.ISO_LOCAL_DATE // Corresponds to "yyyy-MM-dd"
try {
val date1 = LocalDate.parse(date1String, formatter)
val date2 = LocalDate.parse(date2String, formatter)
return if (date1.isAfter(date2)) {
date1String
} else {
date2String
}
} catch (e: DateTimeParseException) {
Timber.e(e, "Error parsing dates: $date1String, $date2String")
// Handle cases where one or both dates might be invalid
try {
LocalDate.parse(date1String, formatter)
return date1String // date1 is valid, date2 was not
} catch (e1: DateTimeParseException) {
try {
LocalDate.parse(date2String, formatter)
return date2String // date2 is valid, date1 was not
} catch (e2: DateTimeParseException) {
return null // Both are invalid
}
}
}
}
private fun getSoonerDate(date1String: String, date2String: String): String? {
val formatter = DateTimeFormatter.ISO_LOCAL_DATE // Corresponds to "yyyy-MM-dd"
try {
val date1 = LocalDate.parse(date1String, formatter)
val date2 = LocalDate.parse(date2String, formatter)
return if (date1.isBefore(date2)) {
date1String
} else {
date2String
}
} catch (e: DateTimeParseException) {
Timber.e(e, "Error parsing dates: $date1String, $date2String")
// Handle cases where one or both dates might be invalid
// This basic version returns null if any date is invalid.
// You might want to return the valid one if the other is invalid.
try {
LocalDate.parse(date1String, formatter)
return date1String // date1 is valid, date2 was not
} catch (e1: DateTimeParseException) {
try {
LocalDate.parse(date2String, formatter)
return date2String // date2 is valid, date1 was not
} catch (e2: DateTimeParseException) {
return null // Both are invalid
}
}
}
}
private fun clearFilterSort() {
state.value.filters = Filters()
}
private suspend fun fetchCalendarAdventures(
startDate: String, endDate: String, filters: Filters? = null, group: String = ""
) {
val updatedState = state.value.copy(
isLoading = state.value.isLoading.copy(calendarIsLoading = true),
// filters = state.value.filters.copy(startDate = startDate, endDate = endDate),
group = group.replaceCreated()
)
if (state.value.isSearchBarVisible) {
updateState(updatedState.copy(searchAdventuresList = mutableListOf()))
} else {
updateState(updatedState.copy(mainAdventuresList = mutableListOf()))
}
if (!networkManager.isNetworkConnected()) { // Handle network checks
_uiEvent.tryEmit(HomeUiEvent.ShowSnackbar("Error", "No network connection"))
return
}
adventuresUseCase.fetchCalendarAdventures(group, startDate, endDate)
.onSuccess { adventures ->
// state.value.adventuresList.addAll(adventures.adventures)
if (state.value.isSearchBarVisible) {
updateState(
state.value.copy(
currentMetadata = adventures.meta,
isLoading = state.value.isLoading.copy(calendarIsLoading = false),
searchAdventuresList = adventures.adventures.toMutableList()
)
)
} else {
updateState(
state.value.copy(
currentMetadata = adventures.meta,
isLoading = state.value.isLoading.copy(calendarIsLoading = false),
mainAdventuresList = adventures.adventures.toMutableList()
)
)
}
}.onFailure { exception ->
updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(calendarIsLoading = false)
)
)
_uiEvent.tryEmit(
HomeUiEvent.ShowSnackbar(
"Error", exception.localizedMessage ?: "Failed to load calendar adventures"
)
)
}
}
private fun isSearch(query: String?): Boolean {
val state = state.value
// return query != null || state.filters.state != null || state.filters.interests.isNullOrEmpty() == false || state.filters.startDate != null || state.filters.endDate != null || state.filters.locationLAt != null || state.filters.locationLng != null
return state.isSearchBarVisible
}
private suspend fun fetchInterests() {
state.value.allInterests.clear()
if (!networkManager.isNetworkConnected()) { // Handle network checks
_uiEvent.emit(HomeUiEvent.ShowSnackbar("Error", "No network connection"))
return
}
updateState(state.value.copy(isLoading = state.value.isLoading.copy(interestsIsLoading = true))) // Update loading state
interestsUseCase.fetchInterests().onSuccess { interests ->
updateState(state.value.copy(isLoading = state.value.isLoading.copy(interestsIsLoading = false)))
state.value.allInterests.clear()
updateState(state.value)
state.value.allInterests.addAll(interests.interests)
updateState(state.value.copy(allInterests = interests.interests.toMutableList()))
}.onFailure { exception ->
updateState(state.value.copy(isLoading = state.value.isLoading.copy(interestsIsLoading = false)))
}
}
private fun String.replaceCreated(): String {
return this.replace("Created", "Owned")
}
private suspend fun fetchAdventuresGroup(
group: String,
freshRequest: Boolean = false,
) {
if (!networkManager.isNetworkConnected()) { // Handle network checks
_uiEvent.emit(HomeUiEvent.ShowSnackbar("Error", "No network connection"))
return
}
if (freshRequest) updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(adventuresLoading = true),
group = group,
lastLoadMoreState = LoadMoreState.GROUPS
)
) // Update loading state
else updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(isLoadingMore = true),
group = group,
lastLoadMoreState = LoadMoreState.GROUPS
)
) // Update loading state
adventuresUseCase.fetchGroupedAdventures(
group.replaceCreated(),
if (freshRequest) 1 else state.value.currentMetadata?.nextPage ?: 1
).onSuccess { adventures ->
if (freshRequest) {
state.value.mainAdventuresList.clear()
updateState(state.value)
}
state.value.mainAdventuresList.addAll(adventures.adventures)
updateState(
state.value.copy(
currentMetadata = adventures.meta, isLoading = state.value.isLoading.copy(
adventuresLoading = false, isLoadingMore = false
)
)
)
}.onFailure { exception ->
_uiEvent.emit(
HomeUiEvent.ShowSnackbar(
"Error", exception.localizedMessage ?: "Unknown error"
)
)
updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(
adventuresLoading = false, isLoadingMore = false
)
)
)
}
}
private suspend fun getAllAdventures(
freshGet: Boolean = false,
) {
if (!networkManager.isNetworkConnected()) { // Handle network checks
_uiEvent.emit(HomeUiEvent.ShowSnackbar("Error", "No network connection"))
return
}
// Check if we're already in ALL mode or switching to it
val wasAlreadyInAllMode = state.value.group.isEmpty() || state.value.group == "ALL"
if (freshGet) {
updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(adventuresLoading = true),
lastLoadMoreState = LoadMoreState.ALL,
group = "ALL"
)
) // Update loading state
} else {
updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(isLoadingMore = true),
lastLoadMoreState = LoadMoreState.ALL,
group = "ALL"
)
) // Update loading state
}
adventuresUseCase.getAllAdventures(
if (freshGet) 1 else state.value.currentMetadata?.nextPage ?: 1
).onSuccess { adventures ->
if (freshGet) {
if (!wasAlreadyInAllMode) {
// If we switched to ALL mode, clear the list
state.value.mainAdventuresList.clear()
updateState(state.value)
state.value.mainAdventuresList.addAll(adventures.adventures)
} else {
// If we were already in ALL mode, add new items at the start
val newList = adventures.adventures.toMutableList()
newList.addAll(state.value.mainAdventuresList)
state.value.mainAdventuresList.clear()
state.value.mainAdventuresList.addAll(newList)
}
} else {
// For load more (pagination), just append to the end
state.value.mainAdventuresList.addAll(adventures.adventures)
}
updateState(
state.value.copy(
currentMetadata = adventures.meta, isLoading = state.value.isLoading.copy(
adventuresLoading = false, isLoadingMore = false
)
)
)
// updateState(state.value)
}.onFailure { exception ->
updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(
adventuresLoading = false, isLoadingMore = false
)
)
)
_uiEvent.emit(
HomeUiEvent.ShowSnackbar(
"Error", exception.localizedMessage ?: "Unknown error"
)
)
}
}
private suspend fun executeSearch(query: String?, freshSearch: Boolean = false) {
if (!networkManager.isNetworkConnected()) { // Handle network checks
_uiEvent.emit(HomeUiEvent.ShowSnackbar("Error", "No network connection"))
return
}
// Check if the search query changed
val queryChanged = state.value.searchQuery != query
if (freshSearch) updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(adventuresLoading = true),
searchQuery = query,
lastLoadMoreState = LoadMoreState.SEARCH
)
) // Update loading state
else updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(isLoadingMore = true),
searchQuery = query,
lastLoadMoreState = LoadMoreState.SEARCH
)
)
adventuresUseCase.search(
query,
if (freshSearch == false) state.value.currentMetadata?.nextPage ?: 1 else 1,
state.value.filters
).onSuccess { adventures ->
if (freshSearch) {
if (queryChanged) {
// If query changed, clear the list and update with new data
state.value.searchAdventuresList.clear()
updateState(state.value)
state.value.searchAdventuresList.addAll(adventures.adventures)
} else {
// If it's the same query, add new elements at the start of the list
val newList = adventures.adventures.toMutableList()
newList.addAll(state.value.searchAdventuresList)
state.value.searchAdventuresList.clear()
state.value.searchAdventuresList.addAll(newList)
}
} else {
// For load more (pagination), just append to the end
state.value.searchAdventuresList.addAll(adventures.adventures)
}
updateState(
state.value.copy(
currentMetadata = adventures.meta, isLoading = state.value.isLoading.copy(
adventuresLoading = false,
isLoadingMore = false
)
)
)
// Handle success, e.g., update state or navigate
_uiEvent.emit(HomeUiEvent.ShowSnackbar("Success", "Search completed"))
}.onFailure { exception ->
updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(
adventuresLoading = false, isLoadingMore = false
)
)
)
_uiEvent.emit(
HomeUiEvent.ShowSnackbar(
"Error", exception.localizedMessage ?: "Unknown error"
)
)
}
}
private fun clearPredictedLocations() {
updateState(
state.value.copy(
locationsPredicted = mutableListOf<AutocompletePrediction>()
)
)
}
}
sealed class HomeIntent {
data class LoadAdventuresData(val query: String?) : HomeIntent()
object LoadMoreAdventuresData : HomeIntent()
object Refresh : HomeIntent()
object Logout : HomeIntent()
data class SwitchCalendarColumn(val status: Int) : HomeIntent()
data class LoadCalendarAdventures(val startDate: String, val endDate: String) : HomeIntent()
object ClearAllShared : HomeIntent()
object SwitchShowSearchbar : HomeIntent()
object GotoFilter : HomeIntent()
data class ApplySortBy(val sortBy: String) : HomeIntent()
object ApplyFilter : HomeIntent()
data class LocationSelected(val location: AutocompletePrediction) : HomeIntent()
object ShowStartDateDialog : HomeIntent()
object ShowEndDateDialog : HomeIntent()
data class ApplyInterests(val interests: MutableList<Interest>) : HomeIntent()
data class ApplyStatus(val status: String) : HomeIntent()
data class ApplyStartDate(val startDate: String) : HomeIntent()
data class ApplyLocation(val lat: Double, val long: Double) : HomeIntent()
data class ApplyEndDate(val endDate: String) : HomeIntent()
data class LocationFieldChanged(val location: String) : HomeIntent()
object ApplyAdevntureType : HomeIntent()
data class MapClicked(val location: LatLng) : HomeIntent()
// data class ExecuteSearch(val query: String) : HomeIntent()
data class SelectGroup(val group: String) : HomeIntent()
object FetchInterests : HomeIntent()
// set Filters
data class SetStartDate(val startDate: String) : HomeIntent()
data class SetEndDate(val endDate: String) : HomeIntent()
data class SetInterests(val interests: MutableList<Interest>) : HomeIntent()
data class SetStatus(val status: String?) : HomeIntent()
// New Intents for adventure click and bottom sheet
data class HandleAdventureClick(val adventure: Adventure) : HomeIntent()
data class HandleBottomSheetAction(val action: String, val adventureId: String?) : HomeIntent()
object DismissBottomSheet : HomeIntent()
}
data class HomeState(
var searchQuery: String? = "",
var lastLoadMoreState: LoadMoreState = LoadMoreState.NONE,
var currentMetadata: Meta? = null,
var filters: Filters = Filters(),
var group: String = "",
var isOnCalendar: Boolean = false,
var isSearchBarVisible: Boolean = false,
var isLoading: IsLoading = IsLoading(),
var mainAdventuresList: MutableList<Adventure> = mutableListOf(),
var searchAdventuresList: MutableList<Adventure> = mutableListOf(),
val showGoingBottomSheet: Boolean = false,
val selectedAdventureForBottomSheet: Adventure? = null,
var locationsPredicted: MutableList<AutocompletePrediction> = mutableListOf(),
var newLocation: Place? = null,
var allInterests: MutableList<Interest> = mutableListOf(),
// var selectedInterests: MutableList<Interest> = mutableListOf(),
var startDateFilter: String = "",
var endDateFilter: String = "",
var startDateCalendar: String = "",
var endDateCalendar: String = ""
)
data class IsLoading(
var searchIsLoading: Boolean = false,
var locationIsLoading: Boolean = false,
var calendarIsLoading: Boolean = false,
var interestsIsLoading: Boolean = false,
var adventuresLoading: Boolean = false,
var isLoadingMore: Boolean = false
)
sealed class HomeUiEvent() {
data class ShowSnackbar(
val title: String, val message: String
) : HomeUiEvent()
object AnimateItem : HomeUiEvent()
data class NavigateToNextScreen(val navigationEvent: NavigationEvent) : HomeUiEvent()
object ShowStartDateDialog : HomeUiEvent()
object ShowEndDateDialog : HomeUiEvent()
data class ShowDim(val show: Boolean) : HomeUiEvent()
}
enum class LoadMoreState {
NONE, GROUPS, ALL, SEARCH
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\MainViewModel.kt
```kt
package com.divadventure.viewmodel
import android.os.Bundle
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.NavController.OnDestinationChangedListener
import androidx.navigation.NavDestination
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationEvent.NavigateAdventure
import com.divadventure.data.navigation.NavigationEvent.NavigateProfile
import com.divadventure.data.navigation.NavigationEvent.NavigateTo
import com.divadventure.data.navigation.NavigationEvent.PopBackStack
import com.divadventure.data.navigation.Screen
import com.divadventure.di.AuthPrefs.VERIFICATION_PASSED_BOOLEAN
import com.divadventure.di.SharedPrefs
import com.divadventure.di.UserPrefs.KEY_TOKEN
import com.divadventure.domain.models.Adventure
import com.divadventure.domain.models.AdventureType
import com.divadventure.domain.models.Friend
import com.divadventure.util.NetworkManager
import com.divadventure.viewmodel.MainUiEvent.NavigateToNextScreen
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val networkManager: NetworkManager,
private val sharedPrefs: SharedPrefs
) : BaseViewModel<MainIntent, MainState>(MainState()) {
private val _uiEvent = MutableSharedFlow<MainUiEvent>(replay = 0)
val uiEvent = _uiEvent.asSharedFlow()
override suspend fun handleIntent(intent: MainIntent) {
when (intent) {
is MainIntent.LoadData -> {
updateState(state.value.copy(isLoading = true))
}
is MainIntent.Refresh -> {
// Handle refresh intent.
}
is MainIntent.logout -> {
// Handle logout intent.
}
is MainIntent.clearAllShared -> {
sharedPrefs.setString(KEY_TOKEN, null)
sharedPrefs.setBoolean(VERIFICATION_PASSED_BOOLEAN, false)
}
is MainIntent.ShowDim -> {
}
MainIntent.GotoFilter -> {
viewModelScope.launch {
// Ensure you only navigate if explicitly needed to prevent auto-navigation issues
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
screen = Screen.Filter,
popUpTo = null,
inclusive = false,
onDestinationChangedListener = object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
singleTop = true,
removeListenerAfter = true
)
)
)
}
}
MainIntent.GotoNotifications -> {
viewModelScope.launch {
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
screen = Screen.Notifications,
popUpTo = null,
inclusive = false,
onDestinationChangedListener = object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
singleTop = true,
removeListenerAfter = true
)
)
)
}
}
MainIntent.PopBackStack -> {
viewModelScope.launch {
_uiEvent.emit(
NavigateToNextScreen(
PopBackStack
)
)
}
}
MainIntent.GotoSortBy -> {
viewModelScope.launch {
// Ensure you only navigate if explicitly needed to prevent auto-navigation issues
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
screen = Screen.SortBy,
popUpTo = null,
inclusive = false,
onDestinationChangedListener = object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
singleTop = true,
removeListenerAfter = true
)
)
)
}
}
MainIntent.GotoInterests -> {
viewModelScope.launch {
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
screen = Screen.Interests,
popUpTo = null,
inclusive = false,
onDestinationChangedListener = object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
singleTop = true,
removeListenerAfter = true
)
)
)
}
}
MainIntent.GotoStatus -> {
viewModelScope.launch {
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
screen = Screen.Status,
popUpTo = null,
inclusive = false,
onDestinationChangedListener = object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
singleTop = true,
removeListenerAfter = true
)
)
)
}
}
MainIntent.GotoLocation -> {
viewModelScope.launch {
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
screen = Screen.Location,
popUpTo = null,
inclusive = false,
onDestinationChangedListener = object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
singleTop = true,
removeListenerAfter = true
)
)
)
}
}
is MainIntent.GotoProfile -> {
viewModelScope.launch {
_uiEvent.emit(
NavigateToNextScreen(
NavigateProfile(
intent.profileId,
onDestinationChangedListener = object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
}, true
)
)
)
}
}
MainIntent.GoNotificationsSettings -> {
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
Screen.NotificationsSettings,
onDestinationChangedListener =
object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
}
},
popUpTo = null,
singleTop = true,
removeListenerAfter = true,
inclusive = true
),
)
)
}
MainIntent.GoPrivacySettings -> {
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
Screen.PrivacySettings,
onDestinationChangedListener =
object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
}
},
popUpTo = null,
singleTop = true,
removeListenerAfter = true,
inclusive = true
),
)
)
}
MainIntent.GoAccountSettings -> {
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
Screen.AccountSettings,
onDestinationChangedListener =
object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
}
},
popUpTo = null,
singleTop = true,
removeListenerAfter = true,
inclusive = true
),
)
)
}
is MainIntent.OnSelectAdventure -> {
val adventure = intent.adventure
if (adventure.adventureType == AdventureType.Manage) {
// Proceed with existing navigation logic for Manage
viewModelScope.launch {
try {
val safeAdventure = intent.adventure.copy(
adventureRequest = intent.adventure.adventureRequest ?: emptyList(),
adventurers = intent.adventure.adventurers.ifEmpty { emptyList() }
)
_uiEvent.emit(
NavigateToNextScreen(
NavigateAdventure(
adventure = safeAdventure,
onDestinationChangedListener = object : OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
removeListenerAfter = true
)
)
)
} catch (e: Exception) {
e.printStackTrace()
}
}
} else {
// For other types, emit the new event to be handled by Home screen/HomeViewModel
viewModelScope.launch {
_uiEvent.emit(MainUiEvent.AdventureAction(adventure))
}
}
}
is MainIntent.GoOwnerParticipantMenu -> {
viewModelScope.launch {
try {
// Ensure the adventure has initialized collections to prevent serialization issues
val safeAdventure = intent.adventure.copy(
adventureRequest = intent.adventure.adventureRequest
?: emptyList(),
adventurers = intent.adventure.adventurers.ifEmpty { emptyList() }
)
_uiEvent.emit(
NavigateToNextScreen(
NavigationEvent.NavigateAdventureOwnerParticipantMenu(
adventure = safeAdventure,
onDestinationChangedListener = object :
OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
removeListenerAfter = true
)
)
)
} catch (e: Exception) {
// Log the error but prevent app crash
e.printStackTrace()
}
}
}
}
}
}
sealed class MainIntent {
data class ShowDim(val boolean: Boolean) : MainIntent()
object LoadData : MainIntent()
object Refresh : MainIntent()
object logout : MainIntent()
object clearAllShared : MainIntent()
object GotoFilter : MainIntent()
object GotoSortBy : MainIntent()
object GotoNotifications : MainIntent()
object PopBackStack : MainIntent()
object GotoInterests : MainIntent()
object GotoStatus : MainIntent()
object GoNotificationsSettings : MainIntent()
object GoPrivacySettings : MainIntent()
object GotoLocation : MainIntent()
data class OnSelectAdventure(val adventure: Adventure) : MainIntent()
data class GoOwnerParticipantMenu(val adventure: Adventure) : MainIntent()
object GoAccountSettings : MainIntent()
data class GotoProfile(val profileId: String) : MainIntent()
}
data class MainState(
val isLoading: Boolean = false,
var friends: MutableList<Friend> = mutableListOf<Friend>(),
var currentAdventure: Adventure? = null,
)
sealed class MainUiEvent() {
data class ShowSnackbar(
val title: String, val message: String
) : MainUiEvent()
object AnimateItem : MainUiEvent()
data class NavigateToNextScreen(val navigationEvent: NavigationEvent) : MainUiEvent()
object ShowDialog : MainUiEvent()
data class ShowDim(val show: Boolean) : MainUiEvent()
data class AdventureAction(val adventure: Adventure) : MainUiEvent()
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\ManageAdventureViewModel.kt
```kt
package com.divadventure.viewmodel
import androidx.lifecycle.viewModelScope
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationEvent.NavigateAdventureOptions
import com.divadventure.data.navigation.NavigationEvent.NavigateTo
import com.divadventure.data.navigation.Screen
import com.divadventure.domain.models.Adventure
import com.divadventure.domain.models.AdventureType
import com.divadventure.domain.models.Interest
import com.divadventure.domain.models.LocationAttributes
import com.divadventure.domain.models.Request
import com.divadventure.domain.usecase.AdventuresUseCase
import com.divadventure.domain.usecase.InterestsUseCase
import com.divadventure.domain.usecase.LocationsUseCase
import com.divadventure.domain.usecase.RequestsUseCase
import com.divadventure.data.Repository.RequestsRepository
import com.divadventure.util.NetworkManager
import com.divadventure.viewmodel.AdventureUIEvent.NavigateToNextScreen
import com.divadventure.viewmodel.AdventureUIEvent.ShowSnackbar
import com.google.android.gms.maps.model.LatLng
import com.google.android.libraries.places.api.model.AutocompletePrediction
import com.google.android.libraries.places.api.model.Place
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.LocalTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class ManageAdventureViewModel @Inject constructor(
private val locationsUseCase: LocationsUseCase, // Injected use case
private val networkManager: NetworkManager,
private val interestsUseCase: InterestsUseCase, // Add InterestsUseCase here
private val adventuresUseCase: AdventuresUseCase,
private val requestsUseCase: RequestsUseCase,
private val requestsRepository: RequestsRepository
) : BaseViewModel<AdventuresIntent, AdventuresState>(AdventuresState()) {
// Function to determine the adventure type for an adventure
fun getAdventureType(adventure: Adventure): AdventureType {
return adventuresUseCase.checkAdventureType(adventure)
}
// Function to perform appropriate action based on adventure type
fun performActionByType(adventureType: AdventureType, adventureId: String) {
viewModelScope.launch {
when (adventureType) {
AdventureType.Manage -> {
//_uiEvent.emit(NavigateToNextScreen(
_uiEvent.emit(ShowSnackbar("Join", "Joining the adventure..."))
/*
NavigateTo(
Screen.ManageAdventure,
arguments = mapOf("adventureId" to adventureId)
)
*/
// ))
}
AdventureType.Join -> {
// Handle join action
// You might want to call an API endpoint to join the adventure
_uiEvent.emit(ShowSnackbar("Join", "Joining the adventure..."))
}
AdventureType.Going -> {
// Handle going action
_uiEvent.emit(ShowSnackbar("Going", "You're going to this adventure"))
}
AdventureType.Pending -> {
// Handle pending action
_uiEvent.emit(ShowSnackbar("Pending", "Your request is pending approval"))
}
AdventureType.Leave -> {
// Handle leave action
// You might want to call an API endpoint to leave the adventure
_uiEvent.emit(ShowSnackbar("Leave", "Leaving the adventure..."))
}
}
}
}
private val _uiEvent = MutableSharedFlow<AdventureUIEvent>(replay = 0)
val uiEvent = _uiEvent.asSharedFlow()
override suspend fun handleIntent(intent: AdventuresIntent) {
when (intent) {
is AdventuresIntent.OnDeadlineChange -> {
updateState(
state.value.copy(
deadlineDate = intent.date
)
)
}
is AdventuresIntent.OnDescriptionChange -> {
updateState(
state.value.copy(
adventureDescription = intent.description
)
)
}
is AdventuresIntent.OnLocationAddressChange -> {
updateState(
state.value.copy(
locationAddress = intent.locationAddress
)
)
}
is AdventuresIntent.OnTitleChange -> {
updateState(
state.value.copy(
adventureTitle = intent.title
)
)
}
AdventuresIntent.OnUploadBannerImage -> {
}
is AdventuresIntent.OnLocationLatLngChange -> {
updateState(
state.value.copy(
locationLat = intent.latLng.latitude,
locationLng = intent.latLng.longitude,
// Assuming newLocation should be cleared if manually clicking on map
// and it's not directly tied to a search result Place object.
newLocation = null
)
)
}
is AdventuresIntent.OnRequestConditionChange -> {
state.value.requestCondition = intent.condition
}
is AdventuresIntent.OnSetEndDate -> {
state.value.endDate = intent.endDate
state.value.endClock = intent.endClock
// Store ISO-formatted date for duration calculations
/*
try {
val localDateTime = LocalDateTime.of(intent.endClock.toLocalDate(), intent.endClock)
state.value.endDate = localDateTime.toLocalDate().toString() // Store as ISO format (yyyy-MM-dd)
} catch (e: Exception) {
// Keep the original format if parsing fails
}
*/
}
is AdventuresIntent.OnSetStartDate -> {
state.value.startDate = intent.startDate
state.value.startClock = intent.startClock
// Store ISO-formatted date for duration calculations
/*
try {
val localDateTime = LocalDateTime.of(intent.startDate.toLocalDate(), intent.startClock)
state.value.startDate = localDateTime.toLocalDate().toString() // Store as ISO format (yyyy-MM-dd)
} catch (e: Exception) {
// Keep the original format if parsing fails
}
*/
}
is AdventuresIntent.OnSetDeadlineDate -> {
state.value.deadlineDate = intent.deadlineDate
}
is AdventuresIntent.LocationFieldChanged -> {
updateState(
state.value.copy(
locationsPredicted = locationsUseCase.predictLocations(intent.query)
.toMutableList()
)
)
}
is AdventuresIntent.LocationSelected -> {
clearPredictedLocations()
val location = locationsUseCase.goLocation(intent.selectedLocation.placeId)
location?.latLng?.latitude?.let { lat ->
location.latLng?.longitude?.let { lng ->
updateState(
state.value.copy(
newLocation = location,
locationLat = lat, locationLng = lng
)
)
}
}
}
AdventuresIntent.FetchInterests -> {
fetchInterests()
}
is AdventuresIntent.ApplyInterests -> {
updateState(
state.value.copy(
adventureInterests = intent.interests,
)
)
}
AdventuresIntent.GoInterests -> {
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
Screen.AdventureInterests,
onDestinationChangedListener = { navController, destination, arguments ->
// Add action or leave it empty based on requirements
}
)
)
)
}
is AdventuresIntent.ApplyPrivacyType -> {
state.value.privacyType = intent.privacyType
}
AdventuresIntent.PreviewAdventure -> {
viewModelScope.launch {
_uiEvent.emit(
NavigateToNextScreen(
NavigateTo(
Screen.AdventurePreview,
onDestinationChangedListener = { navController, destination, arguments ->
// Add action or leave it empty based on requirements
}
)
)
)
}
}
AdventuresIntent.PublishAdventure -> {
publishAdventure()
}
is AdventuresIntent.GetAdventure -> {
extractParamsFromAdventureRequest(intent.adventure)
}
AdventuresIntent.GoEditAdventure -> {
}
/*
AdventuresIntent.GoOwnerParticipantMenu -> {
_uiEvent.emit(
NavigateToNextScreen(
NavigationEvent.NavigateAdventureOwnerParticipantMenu(
adventure = Adventure(
id = state.value.adventureId,
title = state.value.adventureTitle,
description = state.value.adventureDescription,
banner = state.value.bannerUrl,
privacyType = state.value.privacyType,
startsAt = state.value.startDate,
endsAt = state.value.endDate,
deadline = state.value.deadlineDate,
joinRequestNeeded = state.value.requestCondition,
ownerId = TODO(),
state = TODO(),
currentUserAdventurerId = TODO(),
adventureRequest = TODO(),
adventurersCount = TODO(),
adventurers = TODO(),
interests = TODO(),
location = TODO(),
adventureType = TODO(),
),
onDestinationChangedListener = { navController, destination, arguments ->
// Add action or leave it empty based on requirements
}
)
)
)
}
*/
is AdventuresIntent.GoJoinRequests -> {
viewModelScope.launch {
_uiEvent.emit(
NavigateToNextScreen(
NavigateAdventureOptions(
adventure = intent.adventure,
partName = Screen.AdventureJoinRequests,
onDestinationChangedListener = { navController, destination, arguments ->
// Add action or leave it empty based on requirements
})
)
)
}
}
is AdventuresIntent.GoInvitationRequests -> {
viewModelScope.launch {
_uiEvent.emit(
NavigateToNextScreen(
NavigateAdventureOptions(
adventure = intent.adventure,
partName = Screen.AdventureInvitationRequests,
onDestinationChangedListener = { navController, destination, arguments ->
// Add action or leave it empty based on requirements
}
)
)
)
}
}
is AdventuresIntent.FetchJoinRequests -> fetchJoinRequests(intent.adventureId)
is AdventuresIntent.AcceptJoinRequest -> acceptJoinRequest(intent.adventureId, intent.requestId)
is AdventuresIntent.DeclineJoinRequest -> declineJoinRequest(intent.adventureId, intent.requestId)
}
}
private fun fetchJoinRequests(adventureId: String) {
viewModelScope.launch {
updateState(state.value.copy(isLoadingJoinRequests = true, joinRequestsError = null))
requestsUseCase.fetchAdventureRequests(adventureId)
.onSuccess { adventureRequestsResponse -> // adventureRequestsResponse is AdventureRequestsResponse
updateState(
state.value.copy(
isLoadingJoinRequests = false,
joinRequestsList = adventureRequestsResponse.requests // Access .requests here
)
)
}
.onFailure { exception ->
updateState(
state.value.copy(
isLoadingJoinRequests = false,
joinRequestsError = exception.message ?: "Unknown error fetching requests"
)
)
_uiEvent.emit(ShowSnackbar("Error", exception.message ?: "Failed to fetch join requests"))
}
}
}
private fun acceptJoinRequest(adventureId: String, requestId: String) {
viewModelScope.launch {
if (!networkManager.isNetworkConnected()) {
_uiEvent.emit(ShowSnackbar("Network Error", "No internet connection available"))
return@launch
}
// Optionally, add a specific loading state for this action
// updateState(state.value.copy(isProcessingRequest = true))
requestsUseCase.acceptJoinRequest(adventureId, requestId)
.onSuccess {
_uiEvent.emit(ShowSnackbar("Success", "Request accepted"))
// Refresh the join requests list
fetchJoinRequests(adventureId)
}
.onFailure { exception ->
_uiEvent.emit(ShowSnackbar("Error", exception.message ?: "Failed to accept request"))
}
// updateState(state.value.copy(isProcessingRequest = false))
}
}
private fun declineJoinRequest(adventureId: String, requestId: String) {
viewModelScope.launch {
if (!networkManager.isNetworkConnected()) {
_uiEvent.emit(ShowSnackbar("Network Error", "No internet connection available"))
return@launch
}
// Optionally, add a specific loading state for this action
// updateState(state.value.copy(isProcessingRequest = true))
requestsUseCase.declineJoinRequest(adventureId, requestId)
.onSuccess {
_uiEvent.emit(ShowSnackbar("Success", "Request declined"))
// Refresh the join requests list
fetchJoinRequests(adventureId)
}
.onFailure { exception ->
_uiEvent.emit(ShowSnackbar("Error", exception.message ?: "Failed to decline request"))
}
// updateState(state.value.copy(isProcessingRequest = false))
}
}
fun extractParamsFromAdventureRequest(adventure: Adventure) {
// Parse the date strings from ISO format to formatted display strings
val dateTimeFormatter = DateTimeFormatter.ofPattern("MMM dd yyyy - h:mm a", Locale.ENGLISH)
val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
// Parse the ISO instant strings to ZonedDateTime objects
val startsAtInstant = Instant.parse(adventure.startsAt)
.atZone(ZoneId.systemDefault())
val endsAtInstant = Instant.parse(adventure.endsAt)
.atZone(ZoneId.systemDefault())
/* val deadlineInstant = java.time.Instant.parse(adventure.deadline)
.atZone(ZoneId.systemDefault()) */
// Format the dates for display
val formattedStartsAt = dateTimeFormatter.format(startsAtInstant)
val formattedEndsAt = dateTimeFormatter.format(endsAtInstant)
// val formattedDeadline = dateFormatter.format(deadlineInstant)
updateState(
state.value.copy(
startDate = formattedStartsAt,
endDate = formattedEndsAt,
deadlineDate = adventure.deadline,
adventureId = adventure.id,
bannerUrl = adventure.banner,
adventureTitle = adventure.title,
adventureDescription = adventure.description,
locationLat = adventure.location.lat,
locationLng = adventure.location.lng,
privacyType = getPrivacyTypeInt(adventure.privacyType),
requestCondition = adventure.joinRequestNeeded,
adventureInterests = adventure.interests.toMutableList()
)
)
}
private fun getPrivacyTypeInt(string: String): Int {
when (string) {
"invite_only" -> return 0
"friends_only" -> return 1
"publicly_open" -> return 2
else -> throw IllegalArgumentException("privacy type not defined!!!!!")
}
}
private suspend fun fetchInterests() {
state.value.allInterests.clear()
if (!networkManager.isNetworkConnected()) { // Handle network checks
_uiEvent.emit(ShowSnackbar("Error", "No network connection"))
return
}
updateState(state.value.copy(interestsIsLoading = true)) // Update loading state
interestsUseCase.fetchInterests().onSuccess { interests ->
updateState(state.value.copy(interestsIsLoading = false))
state.value.allInterests.clear()
updateState(state.value)
state.value.allInterests.addAll(interests.interests)
updateState(state.value.copy(allInterests = interests.interests.toMutableList()))
}.onFailure { exception ->
updateState(state.value.copy(interestsIsLoading = false))
}
}
private fun clearPredictedLocations() {
updateState(
state.value.copy(
locationsPredicted = mutableListOf<AutocompletePrediction>()
)
)
}
private fun publishAdventure() {
viewModelScope.launch {
// Update publishing state to show loading
updateState(state.value.copy(isPublishing = true))
// Check for network connection
if (!networkManager.isNetworkConnected()) {
_uiEvent.emit(ShowSnackbar("Network Error", "No internet connection available"))
updateState(state.value.copy(isPublishing = false))
return@launch
}
// Execute the create adventure request
adventuresUseCase.createAdventure(
title = state.value.adventureTitle,
description = state.value.adventureDescription,
banner = state.value.bannerUrl,
privacyType = state.value.privacyType,
startsAt = state.value.startDate,
endsAt = state.value.endDate,
deadline = state.value.deadlineDate,
interests = state.value.adventureInterests.map { it.id },
joinRequestNeeded = state.value.requestCondition,
locationAttributes = LocationAttributes(
lat = state.value.locationLat.toString(),
lng = state.value.locationLng.toString()
)
).onSuccess { response ->
// Show success message
_uiEvent.emit(ShowSnackbar("Success", "Adventure published successfully"))
// Clear the state after successful creation
resetAdventureState()
}.onFailure { exception ->
// Show error message
_uiEvent.emit(
ShowSnackbar(
"Error",
exception.message ?: "Failed to publish adventure"
)
)
}
// Reset loading state
updateState(state.value.copy(isPublishing = false))
}
}
private fun resetAdventureState() {
// Reset all the adventure creation form fields
updateState(
state.value.copy(
adventureTitle = "",
adventureDescription = "",
startDate = "",
endDate = "",
deadlineDate = "",
locationAddress = "",
locationLat = 0.0,
locationLng = 0.0,
bannerUrl = "",
requestCondition = false,
adventureInterests = mutableListOf(),
privacyType = 2
)
)
}
}
sealed class AdventuresIntent {
data class OnTitleChange(val title: String) : AdventuresIntent()
data class OnDescriptionChange(val description: String) : AdventuresIntent()
data class OnDeadlineChange(val date: String) : AdventuresIntent()
data class OnLocationAddressChange(val locationAddress: String) : AdventuresIntent()
data class OnLocationLatLngChange(val latLng: LatLng) : AdventuresIntent()
object OnUploadBannerImage : AdventuresIntent()
data class OnRequestConditionChange(val condition: Boolean) : AdventuresIntent()
data class OnSetStartDate(val startDate: String, val startClock: LocalTime) : AdventuresIntent()
data class OnSetEndDate(val endDate: String, val endClock: LocalTime) : AdventuresIntent()
data class OnSetDeadlineDate(val deadlineDate: String) : AdventuresIntent()
data class LocationFieldChanged(val query: String) : AdventuresIntent()
data class LocationSelected(val selectedLocation: AutocompletePrediction) : AdventuresIntent()
object GoEditAdventure : AdventuresIntent()
data class GoJoinRequests(val adventure: Adventure) : AdventuresIntent()
data class GoInvitationRequests(val adventure: Adventure) : AdventuresIntent()
object GoInterests : AdventuresIntent()
object FetchInterests : AdventuresIntent()
data class ApplyInterests(val interests: MutableList<Interest>) : AdventuresIntent()
data class ApplyPrivacyType(val privacyType: Int) : AdventuresIntent()
object PreviewAdventure : AdventuresIntent()
object PublishAdventure : AdventuresIntent()
data class GetAdventure(val adventure: Adventure) : AdventuresIntent()
data class FetchJoinRequests(val adventureId: String) : AdventuresIntent()
data class AcceptJoinRequest(val adventureId: String, val requestId: String) : AdventuresIntent()
data class DeclineJoinRequest(val adventureId: String, val requestId: String) : AdventuresIntent()
}
data class AdventuresState(
var adventureId: String = "",
var adventureTitle: String = "",
var adventureDescription: String = "",
var startClock: LocalTime = LocalTime.now(),
var startDate: String = "",
var allInterests: MutableList<Interest> = mutableListOf<Interest>(),
var adventureInterests: MutableList<Interest> = mutableListOf<Interest>(),
var endClock: LocalTime = LocalTime.now(),
var endDate: String = "",
var deadlineDate: String = "",
var locationAddress: String = "",
var locationLat: Double = 0.0,
var locationLng: Double = 0.0,
var interests: MutableList<String> = mutableListOf<String>(),
var privacyType: Int = 2,
var bannerUrl: String = "",
var requestCondition: Boolean = false,
var locationsPredicted: MutableList<AutocompletePrediction> = mutableListOf(),
var newLocation: Place? = null,
var locationIsLoading: Boolean = false,
var interestsIsLoading: Boolean = false,
var isPublishing: Boolean = false,
val joinRequestsList: List<Request> = emptyList(),
val isLoadingJoinRequests: Boolean = false,
val joinRequestsError: String? = null
)
sealed class AdventureUIEvent() {
data class ShowSnackbar(
val title: String, val message: String
) : AdventureUIEvent()
object AnimateItem : AdventureUIEvent()
data class NavigateToNextScreen(val navigationEvent: NavigationEvent) : AdventureUIEvent()
object ShowStartDateDialog : AdventureUIEvent()
object ShowEndDateDialog : AdventureUIEvent()
}
// Function to perform appropriate action based on adventure type
/*
fun performActionByType(adventureType: AdventureType, adventureId: String) {
viewModelScope.launch {
when(adventureType) {
AdventureType.Manage -> {
_uiEvent.emit(NavigateToNextScreen(
NavigateTo(
Screen.ManageAdventure,
arguments = mapOf("adventureId" to adventureId)
)
))
}
AdventureType.Join -> {
// Handle join action by calling UseCase methods
// adventuresUseCase.joinAdventure(adventureId)
_uiEvent.emit(ShowSnackbar("Join", "Joining the adventure..."))
}
AdventureType.Going -> {
// Handle going action
_uiEvent.emit(ShowSnackbar("Going", "You're going to this adventure"))
}
AdventureType.Pending -> {
// Handle pending action
_uiEvent.emit(ShowSnackbar("Pending", "Your request is pending approval"))
}
AdventureType.Leave -> {
// Handle leave action by calling UseCase methods
// adventuresUseCase.leaveAdventure(adventureId)
_uiEvent.emit(ShowSnackbar("Leave", "Leaving the adventure..."))
}
}
}
}*/
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\NotificationsViewModel.kt
```kt
package com.divadventure.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.divadventure.data.SharedService
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.di.SharedPrefs
import com.divadventure.util.NetworkManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject
@HiltViewModel
class NotificationsViewModel @Inject constructor(
private val sharedService: SharedService,
private val sharedPrefs: SharedPrefs,
private val networkManager: NetworkManager
) : BaseViewModel<NotificationsIntent, NotificationsState>(NotificationsState()) {
private val _uiEvent = MutableSharedFlow<NotificationsUiEvent>(replay = 0)
val uiEvent = _uiEvent.asSharedFlow()
private val _notifications = MutableLiveData<List<String>>()
val notifications: LiveData<List<String>> get() = _notifications
init {
loadNotifications()
}
private fun loadNotifications() {
// Example: Load notifications from a data source
_notifications.value = listOf("Notification 1", "Notification 2", "Notification 3")
}
override suspend fun handleIntent(intent: NotificationsIntent) {
when (intent) {
NotificationsIntent.LoadNotifications -> {
}
NotificationsIntent.ShowBottomSheet -> {
_uiEvent.emit(NotificationsUiEvent.ShowBottomSheet)
}
}
}
}
sealed class NotificationsIntent {
object LoadNotifications : NotificationsIntent()
object ShowBottomSheet : NotificationsIntent()
}
data class NotificationsState(val state: String = "")
sealed class NotificationsUiEvent {
data class ShowSnackbar(
val title: String, val message: String
) : NotificationsUiEvent()
object ShowBottomSheet : NotificationsUiEvent()
object AnimateItem : NotificationsUiEvent()
data class NavigateToNextScreen(val navigationEvent: NavigationEvent) : NotificationsUiEvent()
object ShowDialog : NotificationsUiEvent()
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\ProfileViewModel.kt
```kt
package com.divadventure.viewmodel
import android.os.Bundle
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.NavController.OnDestinationChangedListener
import androidx.navigation.NavDestination
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationEvent.NavigateAdventure
import com.divadventure.di.SharedPrefs
import com.divadventure.di.UserPrefs.KEY_AVATAR
import com.divadventure.di.UserPrefs.KEY_BIO
import com.divadventure.di.UserPrefs.KEY_BIRTH_DATE
import com.divadventure.di.UserPrefs.KEY_FIRST_NAME
import com.divadventure.di.UserPrefs.KEY_ID
import com.divadventure.di.UserPrefs.KEY_LOCATION
import com.divadventure.di.UserPrefs.KEY_USERNAME
import com.divadventure.domain.models.Adventure
import com.divadventure.domain.models.AdventureType
import com.divadventure.domain.models.Friend
import com.divadventure.domain.models.UsersData
import com.divadventure.domain.usecase.AdventuresUseCase
import com.divadventure.domain.usecase.UsersUseCase
import com.divadventure.util.NetworkManager
import com.divadventure.viewmodel.ProfileUIEvent.ShowBottomSheet
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val networkManager: NetworkManager,
private val usersUseCase: UsersUseCase, private val sharedPrefs: SharedPrefs,
private val adventureUseCase: AdventuresUseCase
) : BaseViewModel<ProfileIntent, ProfileState>(ProfileState()) {
private val _uiEvent = MutableSharedFlow<ProfileUIEvent>(replay = 0)
val uiEvent = _uiEvent.asSharedFlow()
override suspend fun handleIntent(intent: ProfileIntent) {
when (intent) {
ProfileIntent.ClearAllShared -> {
}
is ProfileIntent.LoadElseProfileData -> {
loadUserData(intent.profileId)
}
ProfileIntent.Logout -> {
}
ProfileIntent.Refresh -> {
}
ProfileIntent.GetFriends -> {
}
ProfileIntent.LoadMyFriends -> {
fetchMyFriends()
}
is ProfileIntent.LoadElseFriends -> {
state.value.isMe = false
fetchElseFriends(intent.profileId)
}
ProfileIntent.LoadMyAdventures -> {
fetchMyAdventures()
}
ProfileIntent.LoadElseAdventures -> {
fetchElseAdventures()
}
ProfileIntent.OpenProfileBottomSheet -> {
_uiEvent.emit(ShowBottomSheet(true))
}
is ProfileIntent.CheckId -> {
updateState(
state.value.copy(
isMe = intent.id == sharedPrefs.getString(KEY_ID)
)
)
}
ProfileIntent.LoadMyUserData -> {
updateState(
state.value.copy(
birthdate = sharedPrefs.getString(KEY_BIRTH_DATE) ?: "",
bio = sharedPrefs.getString(KEY_BIO) ?: "",
firstName = sharedPrefs.getString(KEY_FIRST_NAME) ?: "",
lastName = sharedPrefs.getString(KEY_FIRST_NAME) ?: "",
userId = sharedPrefs.getString(KEY_ID) ?: "",
avatar = sharedPrefs.getString(KEY_AVATAR) ?: "",
username = sharedPrefs.getString(KEY_USERNAME) ?: "",
location = sharedPrefs.getString(KEY_LOCATION) ?: "",
)
)
}
ProfileIntent.ApplyAdevntureType -> {
state.value.adventuresList.forEach {
it.adventureType = adventureUseCase.checkAdventureType(it)
}
}
is ProfileIntent.HandleAdventureClick -> {
val adventure = intent.adventure
when (adventure.adventureType) {
AdventureType.Join -> {
if (adventure.joinRequestNeeded) {
updateAdventureState(adventure.id, AdventureType.Pending)
} else {
// As per issue: "if join request need is false , it should change to Leave"
updateAdventureState(adventure.id, AdventureType.Leave)
// Potentially, this could also mean remove it immediately if it becomes "Leave"
// removeAdventureFromLists(adventure.id) // Decided to keep it visible as "Leave" first
}
}
AdventureType.Going -> {
updateState(
state.value.copy(
showGoingBottomSheet = true,
selectedAdventureForBottomSheet = adventure
)
)
}
AdventureType.Pending -> {
updateAdventureState(adventure.id, AdventureType.Join)
}
AdventureType.Leave -> {
// When a "Leave" button is clicked (originally it was "Leave")
removeAdventureFromLists(adventure.id)
}
AdventureType.Manage -> {
// This case should ideally not be sent to HomeViewModel via HandleAdventureClick
// as MainViewModel handles navigation for Manage. Log if it occurs.
Timber.w("HandleAdventureClick received for Manage type: ${adventure.id}")
manageAdventure(adventure)
}
null -> {
// Handle null case if necessary, perhaps log an error or default behavior
Timber.e("AdventureType is null for adventure: ${adventure.id}")
}
}
}
}
}
private fun manageAdventure(adventure: Adventure) {
// Proceed with existing navigation logic for Manage
viewModelScope.launch {
try {
val safeAdventure = adventure.copy(
adventureRequest = adventure.adventureRequest ?: emptyList(),
adventurers = adventure.adventurers.ifEmpty { emptyList() })
_uiEvent.emit(
ProfileUIEvent.NavigateToNextScreen(
NavigateAdventure(
adventure = safeAdventure,
onDestinationChangedListener = object : OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
// Implement the required behavior here if needed
}
},
removeListenerAfter = true
)
)
)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun removeAdventureFromLists(adventureId: String) {
val currentMainList = state.value.adventuresList.toMutableList()
currentMainList.removeAll { it.id == adventureId }
updateState(
state.value.copy(
adventuresList = currentMainList,
)
)
}
private fun updateAdventureState(adventureId: String, newType: AdventureType) {
val currentMainList = state.value.adventuresList.toMutableList()
val mainIndex = currentMainList.indexOfFirst { it.id == adventureId }
if (mainIndex != -1) {
currentMainList[mainIndex] = currentMainList[mainIndex].copy(adventureType = newType)
}
updateState(
state.value.copy(
adventuresList = currentMainList
)
)
}
private suspend fun fetchElseAdventures() {
if (!networkManager.isNetworkConnected()) {
_uiEvent.emit(ProfileUIEvent.ShowSnackbar("Error", "No network connection"))
return
}
// Update loading state to show adventures are being fetched
updateState(state.value.copy(isLoading = state.value.isLoading.copy(adventuresLoading = true)))
try {
// Call the use case to fetch the else adventures
val result = adventureUseCase.getElseAdventures(
profileId = state.value.userId
)
result.onSuccess { adventuresResponse ->
state.value.adventuresList.clear()
updateState(state.value)
// Update the state with the fetched adventures
state.value.adventuresList.addAll(adventuresResponse.adventures)
updateState(
state.value.copy(
adventuresList = adventuresResponse.adventures.toMutableList(),
isLoading = state.value.isLoading.copy(adventuresLoading = false)
)
)
}.onFailure { exception ->
// Emit an error snackbar with a descriptive message
_uiEvent.emit(
ProfileUIEvent.ShowSnackbar(
"Error", exception.message ?: "Failed to fetch adventures"
)
)
// Make sure loading state is reset
updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(
adventuresLoading = false
)
)
)
}
} catch (e: Exception) {
// Emit a snackbar for unexpected errors
_uiEvent.emit(
ProfileUIEvent.ShowSnackbar(
"Error", "An unexpected error occurred: ${e.message}"
)
)
// Reset loading state
updateState(state.value.copy(isLoading = state.value.isLoading.copy(adventuresLoading = false)))
}
}
private suspend fun loadUserData(profileId: String) {
if (!networkManager.isNetworkConnected()) { // Handle network checks
Timber.e("No network connection while attempting to load user data for profileId: $profileId")
_uiEvent.emit(ProfileUIEvent.ShowSnackbar("Error", "No network connection"))
return
}
Timber.d("Fetching user data for profileId: $profileId")
val result = usersUseCase.getUserData(profileId)
result.onSuccess { profile ->
Timber.d("Successfully fetched user data for profileId: $profileId")
updateState(
state.value.copy(
userId = profile.data.id,
avatar = profile.data.avatar ?: "",
username = profile.data.username,
firstName = profile.data.firstName,
lastName = profile.data.lastName,
birthdate = profile.data.birthdate ?: "",
bio = profile.data.bio ?: "",
statusWithUser = profile.data.statusWithUser ?: "",
elseUserData = profile
)
)
}.onFailure { t ->
Timber.e(t, "Failed to load user data for profileId: $profileId")
_uiEvent.emit(ProfileUIEvent.ShowSnackbar("Error", "Failed to load user data"))
}
}
private suspend fun fetchMyAdventures() {
// Check for network connection
if (!networkManager.isNetworkConnected()) {
_uiEvent.emit(ProfileUIEvent.ShowSnackbar("Error", "No network connection"))
return
}
// Update loading state to show adventures are being fetched
updateState(state.value.copy(isLoading = state.value.isLoading.copy(adventuresLoading = true)))
try {
// Call the use case to fetch the user's adventures
val result = adventureUseCase.getMyAdventures()
// Handle the result
result.onSuccess { adventuresResponse ->
state.value.adventuresList.clear()
updateState(state.value)
// Update the state with the fetched adventures
state.value.adventuresList.addAll(adventuresResponse.adventures)
updateState(
state.value.copy(
adventuresList = adventuresResponse.adventures.toMutableList(),
isLoading = state.value.isLoading.copy(adventuresLoading = false)
)
)
}.onFailure { exception ->
// Emit an error snackbar with a descriptive message
_uiEvent.emit(
ProfileUIEvent.ShowSnackbar(
"Error", exception.message ?: "Failed to fetch adventures"
)
)
// Make sure loading state is reset
updateState(
state.value.copy(
isLoading = state.value.isLoading.copy(
adventuresLoading = false
)
)
)
}
} catch (e: Exception) {
// Emit a snackbar for unexpected errors
_uiEvent.emit(
ProfileUIEvent.ShowSnackbar(
"Error", "An unexpected error occurred: ${e.message}"
)
)
// Reset loading state
updateState(state.value.copy(isLoading = state.value.isLoading.copy(adventuresLoading = false)))
}
}
private suspend fun fetchElseFriends(profileId: String) {
Timber.d("Starting fetchElseFriends for profileId: $profileId")
if (!networkManager.isNetworkConnected()) { // Handle network checks
Timber.e("No network connection while attempting to fetch friends for profileId: $profileId")
_uiEvent.emit(ProfileUIEvent.ShowSnackbar("Error", "No network connection"))
return
}
Timber.d("Network connected. Updating loading state for fetchElseFriends.")
updateState(state.value.copy(isLoading = state.value.isLoading.copy(friendsIsLoading = true))) // Update loading state
usersUseCase.getElseFriends(profileId).onSuccess {
Timber.d("Successfully fetched friends for profileId: $profileId")
state.value.friends.clear()
updateState(state.value)
state.value.friends.addAll(it.friends)
updateState(state.value)
_uiEvent.emit(ProfileUIEvent.ShowSnackbar("Success", "Search completed"))
Timber.d("Friend list updated successfully for profileId: $profileId")
}.onFailure { exception ->
Timber.e(exception, "Failed to fetch friends for profileId: $profileId")
updateState(state.value.copy(isLoading = state.value.isLoading.copy(friendsIsLoading = false)))
_uiEvent.emit(
ProfileUIEvent.ShowSnackbar(
"Error", exception.localizedMessage ?: "Unknown error"
)
)
}
Timber.d("fetchElseFriends method completed for profileId: $profileId")
}
private suspend fun fetchMyFriends() {
if (!networkManager.isNetworkConnected()) { // Handle network checks
_uiEvent.emit(ProfileUIEvent.ShowSnackbar("Error", "No network connection"))
return
}
updateState(state.value.copy(isLoading = state.value.isLoading.copy(friendsIsLoading = true))) // Update loading state
usersUseCase.getFriends().onSuccess { friends ->
state.value.friends.clear()
updateState(state.value)
state.value.friends.addAll(friends.friends)
updateState(state.value)
// Handle success, e.g., update state or navigate
_uiEvent.emit(ProfileUIEvent.ShowSnackbar("Success", "Search completed"))
}.onFailure { exception ->
updateState(state.value.copy(isLoading = state.value.isLoading.copy(friendsIsLoading = false)))
_uiEvent.emit(
ProfileUIEvent.ShowSnackbar(
"Error", exception.localizedMessage ?: "Unknown error"
)
)
}
}
}
sealed class ProfileIntent {
data class LoadElseProfileData(val profileId: String) : ProfileIntent()
object OpenProfileBottomSheet : ProfileIntent()
data class CheckId(val id: String) : ProfileIntent()
object GetFriends : ProfileIntent()
object Refresh : ProfileIntent()
object Logout : ProfileIntent()
object LoadMyAdventures : ProfileIntent()
object LoadElseAdventures : ProfileIntent()
object ClearAllShared : ProfileIntent()
object LoadMyFriends : ProfileIntent()
object LoadMyUserData : ProfileIntent()
data class LoadElseFriends(val profileId: String) : ProfileIntent()
object ApplyAdevntureType : ProfileIntent()
data class HandleAdventureClick(val adventure: Adventure) : ProfileIntent()
}
data class ProfileState(
var userId: String = "",
var avatar: String? = "",
var username: String = "",
var firstName: String = "",
var birthdate: String = "",
var lastName: String = "",
var statusWithUser: String = "",
var location: String = "",
var bio: String = "",
var isMe: Boolean = true,
var friends: MutableList<Friend> = mutableListOf(),
var currentFriendsPage: Int = 0,
var adventuresList: MutableList<Adventure> = mutableListOf(),
var isLoading: LoadingProfileState = LoadingProfileState(),
var buttonTitle: String = "Edit Profile",
var elseUserData: UsersData? = null,
var selectedAdventureForBottomSheet: Adventure? = null,
var showGoingBottomSheet: Boolean = false
)
data class LoadingProfileState(
var profileIsLoading: Boolean = false,
var friendsIsLoading: Boolean = false,
var isLoadingMore: Boolean = false,
var adventuresLoading: Boolean = false
)
sealed class ProfileUIEvent() {
data class ShowSnackbar(
val title: String, val message: String
) : ProfileUIEvent()
data class ShowBottomSheet(val show: Boolean) : ProfileUIEvent()
object AnimateItem : ProfileUIEvent()
data class NavigateToNextScreen(val navigationEvent: NavigationEvent) : ProfileUIEvent()
object ShowDialog : ProfileUIEvent()
data class ShowDim(val show: Boolean) : ProfileUIEvent()
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\readme.md
```md
# **Div Adventures**
![Div Adventures Logo](app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png)
## **Project Description**
**Div Adventures** is an innovative social media application designed with a specific focus on sharing users' adventurous experiences and trips. It allows users to connect with others who share similar interests and build friendships around their love for exploration and adventure. The app incorporates various advanced Android development features and adheres to modern software architecture principles, making it a robust and high-performing platform for adventure enthusiasts.
---
### **Core Features**
1. **Adventure Sharing**:
- Users can upload **photos** and write **textual descriptions** to share their adventures.
- The shared content is visible to friends and strangers, depending on the user’s privacy settings.
- Start and end times for adventures are now displayed in a clear 'MMM dd yyyy - h:mm a' format (e.g., 'Jun 12 2023 - 9:00 PM'), improving readability.
- Selected interests for an adventure are presented neatly as a comma-separated list (e.g., 'Hiking, Camping, Photography').
- Each adventure has two types of roles: **owner** and **participants**. There is always only one owner who has full control over the adventure, including the ability to update it.
- Adventures can have three types of privacy settings: **public** (visible to everyone), **friends only** (visible only to friends), and **by invitation** (visible only to invited users).
- The adventure owner can designate certain participants as **moderators** who have the ability to invite other users to the adventure.
- **Adventure Card Interactions**: The adventure cards feature dynamic action buttons that change based on the user's relationship and status with the adventure:
- **Manage**: If the user is the owner of the adventure, a "Manage" button is displayed, allowing them to edit or manage their adventure details.
- **Join**: If the adventure is available to join:
- If a join request is needed, clicking "Join" changes the button to "Pending", indicating the request has been sent.
- If no join request is needed, clicking "Join" changes the button to "Leave", signifying the user has joined and can now choose to leave.
- **Pending**: If the user has a pending request to join an adventure, a "Pending" button is shown. Clicking this button again will revert it to "Join", effectively canceling the join request.
- **Going**: If the user is marked as "Going" to an adventure, a "Going" button is displayed. Clicking this button opens a bottom sheet with three options:
- **"Yes"**: Confirms continued participation, but changes the button to "Leave", allowing the user to leave later.
- **"No"**: The user chooses not to go, and the adventure card is removed from their list.
- **"Maybe"**: The bottom sheet is dismissed with no change in status. The user remains "Going".
- **Leave**: If the user has joined an adventure (either directly or after being "Going"), a "Leave" button is shown. Clicking this will remove the user from the adventure, and the card will be removed from their list.
2. **Search and Discover Adventures**:
- Adventures can be searched based on:
- **Interests** (e.g., hiking, diving, camping).
- **Time** (e.g., specific dates or duration of the trip).
- **Geolocations** (e.g., destinations or nearby adventures).
- Users can find content tailored to their preferences or explore diverse categories of adventure-based activities.
3. **Adventure Categories**:
- The app supports a **diverse range of adventure-related categories**, allowing users to classify and explore adventures more meaningfully.
4. **Friendship and Privacy**:
- Users can **become friends** by sending and accepting friend requests.
- Enforced **privacy settings** offer the choice between **private accounts** (where only approved friends can view content) or **public accounts** (open for everyone to explore).
5. **Navigation and UI Framework**:
- A **three-tab navigation-based UI** makes the application intuitive and easy to use.
- The **main screen layout** provides seamless access to important features like the user’s feed, search functionalities, and profile management.
6. **In-App Animations and Visuals**:
- The app integrates **Lottie animations** to provide an engaging visual experience.
- Smooth transition effects and visually rich components enhance the user experience.
---
### **Backend and Storage**
- Currently, the app relies heavily on **SharedPreferences** for minimal local storage.
- **Room Database** may be integrated later to provide structured offline data storage for caching or improved local performance.
- API integration is implemented using **Retrofit** for network communication, ensuring efficient and responsive data handling.
---
### **Design and UI**
- The design follows a **Figma-based wireframe**, translating ideas into sleek, modern UI components.
- Built entirely using **Jetpack Compose**, ensuring reactive and declarative UI design principles.
- **Accompanist** is used to handle additional UI utilities like permissions, making the implementation of user flows smoother.
- Enhanced time selection with an AM/PM toggle for more precise time input within date/time dialogs.
---
### **State Management and Architecture**
- **MVI Architecture**:
- The app is structured around the **Model-View-Intent (MVI)** architecture, ensuring clean separation of concerns and predictable state flows.
- **Jetpack ViewModels**, combined with **Flows** and **Composable functions**, ensure that app state and UI updates are handled efficiently.
- **Dependency Injection**:
- The project leverages **HILT** for dependency injection, making the codebase modular and easy to maintain.
### **ViewModels Implementation**
The application implements a robust ViewModel architecture to manage state and user interactions:
1. **BaseViewModel**:
- Abstract foundation class that implements the core MVI pattern
- Manages state through Kotlin Flow (StateFlow and SharedFlow)
- Provides a unified approach to handling intents via a Channel
- Encapsulates navigation events for consistent screen transitions
2. **Specialized ViewModels**:
- **AuthViewModel**: Manages authentication flows including login, signup, verification, and password management
- **MainViewModel**: Controls the main application flow and navigation between key screens
- **ManageAdventureViewModel**: Handles adventure creation, editing, and publishing workflow
- **NotificationsViewModel**: Manages user notifications and related UI states
3. **State Management**:
- Each ViewModel maintains its own state class (e.g., AdventuresState, AuthState)
- States are immutable and updated through controlled state transitions
- UI components observe these states via StateFlow for reactive updates
4. **Intent Processing**:
- User actions are translated into typed intents (sealed classes)
- Intents are processed asynchronously through coroutine channels
- Each intent triggers specific business logic and potential state updates
5. **UI Events**:
- One-time events (navigation, snackbars, dialogs) are handled through SharedFlow
- Prevents event duplication during configuration changes
- Maintains clear separation between persistent state and transient events
### **ViewModel Implementation**
- **BaseViewModel**:
- The app uses a custom `BaseViewModel` abstract class that serves as the foundation for all ViewModels in the application.
- Implements a standardized approach for handling intents through a Channel, state management via StateFlow, and navigation events with SharedFlow.
- Provides a clean, consistent API for all derived ViewModels to process user actions and update UI state.
- **Key ViewModels**:
- **AuthViewModel**: Manages all authentication flows including signup, login, verification, password reset, and onboarding. Implements specialized handlers for each authentication stage.
- **MainViewModel**: Controls the main application navigation and global actions, serving as a central coordination point between different screens and features.
- **ManageAdventureViewModel**: Handles the creation, editing, and preview of adventures, managing complex form state, location selection, and interest tagging.
- **NotificationsViewModel**: Manages the display and interaction with user notifications, supporting read status tracking and action responses.
- **State Management Approach**:
- Each ViewModel maintains its own state class that encapsulates all UI-relevant data.
- The states are immutable, with updates handled through copy operations to ensure state integrity.
- ViewModels expose a single StateFlow for Composables to observe, simplifying the reactive data flow.
- **Intent Processing**:
- User actions are translated into strongly-typed Intent sealed classes.
- Intents are processed asynchronously through a Channel to avoid state conflicts.
- Each ViewModel implements specialized intent handlers that encapsulate the business logic for specific user actions.
---
### **Integrated Libraries and APIs**
1. **Image Loading & Caching**:
- **Coil**: For efficient image loading and caching, ensuring smooth performance during image-heavy interactions.
2. **Calendar and Date Handling**:
- **Kizitonwose** library is used for implementing calendar functionalities, enabling users to view adventures based on specific dates.
3. **Maps and Location-Based Features**:
- **Google Maps API** is integrated for:
- Geolocation tagging.
- Viewing adventure locations on maps.
- Discovering adventures nearby.
4. **Authentication**:
- **Ohteepee** (or "OkHttp") is incorporated for secure and efficient user authentication and API communication.
5. **Permissions**:
- **Accompanist** simplifies the handling of runtime permissions, streamlining flows like accessing the camera or retrieving location data.
---
### **Current Progress and Roadmap**
1. **Completed Features**:
- User authentication and secure access.
- Adventure sharing with images, text, geolocation tagging, and filtering by interests.
- Core navigation with a three-tab UI design.
- Integration of key third-party libraries like Coil, Retrofit, and Kizitonwose.
2. **Upcoming Enhancements**:
- Implementation of **messaging** and direct communication between friends.
- Addition of a **Room Database** for offline capabilities and better caching.
- Improved recommendations for exploring adventures based on past preferences, interests, and geolocation.
- Enhanced sorting and filtering mechanisms for a personalized feed experience.
---
### **Future Potential**
- **Business Goals**:
- Incorporating **monetization features** like in-app purchases for premium categories or advertisements for adventure-related services.
- Expanding into a global audience by integrating multi-language support and region-specific recommendations.
- **Analytics and Insights**:
- Using tools like **Firebase Analytics** to track user behavior and refine the app's focus based on data-driven decisions.
- Building personalized recommendations based on user interactions, search history, and location preferences.
---
### **System Requirements**
- **Android Studio**: Arctic Fox (2023.2.1) or higher
- **Gradle Version**: 8.4+
- **JDK Version**: 11 or higher
- **Target Android SDK**: 35
- **Minimum Android SDK**: 24 (Android 7.0 Nougat)
- **Kotlin Version**: 1.9.22+
- **Google Maps API Key**: Required for map functionality
- **Device Requirements**: Android device or emulator running Android 7.0 (API 24) or higher
---
### **Technical Overview**
**Key Technologies**:
- **Language**: Kotlin (with Jetpack Compose for UI)
- **Architecture**: MVI
- **State Management**: ViewModel with `StateFlow` and `SharedFlow`
- **Dependency Injection**: HILT
- **Networking**: Retrofit, OkHttp
- **Storage**: SharedPreferences, potentially Room
**External Libraries Used**:
- **Coil**: Image loading and caching.
- **Kizitonwose**: Calendar handling.
- **Google Maps API**: Geolocation and route assistance.
- **Lottie**: For visually appealing animations.
- **Accompanist**: Permissions handling.
---
### **Development Setup**
1. **Google Maps API Setup**:
- Create a project in the [Google Cloud Console](https://console.cloud.google.com/)
- Enable the Maps SDK for Android and Places API
- Create an API key with appropriate restrictions
- Add your API key to the designated location in strings.xml
2. **Build and Run**:
- Open the project in Android Studio
- Sync Gradle files
- Build and run the app on an emulator or physical device
3. **Testing**:
- Unit tests can be run via Gradle or Android Studio
- UI tests require an emulator or connected device
4. **Security Notes**:
- Keep API keys confidential and use proper restrictions
- Use version control .gitignore to prevent sensitive data from being committed
- Follow secure coding practices when handling user data
---
### **Project Architecture**
The application follows a modular architecture based on MVI pattern:
- **Presentation Layer**:
- UI components built with Jetpack Compose
- ViewModels handling UI state and user intents
- Navigation using the Navigation Compose library
- **Domain Layer**:
- Business logic and use cases
- Model definitions representing core entities
- Repository interfaces defining data operations
- Role-based permission system that enforces access controls based on user roles (owner vs. participant) and designated moderator status
- **Data Layer**:
- API services and network communication
- Local storage and caching mechanisms
- Repository implementations
- **DI Layer**:
- HILT modules for dependency provision
- Scoped components for proper lifecycle management
---
### **Security and Privacy**
- **User Data**: The application collects and processes user information with strict privacy measures
- **Location Data**: User locations are processed only with explicit permission and for intended functionality
- **Media Content**: User-uploaded content is stored securely with appropriate access controls
- **Authentication**: Industry-standard authentication and session management practices are implemented
- **Role-Based Access Control**: The application implements a fine-grained permission system where adventure owners have full control, moderators can invite others, and privacy settings (public, friends only, by invitation) further restrict content visibility
---
### **Contacts and Support**
For questions, support, or business inquiries about Div Adventures, please contact the development team at:
- **Email**: [email protected]
- **Website**: www.divadventures.com
---
### **Device Capabilities**
Div Adventures takes advantage of modern Android device features:
- **Camera**: For capturing and sharing adventure photos
- **GPS and Location Services**: For adventure geolocation tagging and discovery
- **Network Connectivity**: For social features and content sharing
- **Sensors**: Optional integration with device sensors for enhanced adventure tracking
- **Storage**: For caching adventure content and user data
---
### **Performance Considerations**
The app is optimized for performance and resource efficiency:
- **Image Compression**: Optimizes uploaded photos for bandwidth and storage
- **Lazy Loading**: Implements lazy loading of adventure content for smooth scrolling
- **Background Processing**: Handles intensive tasks in background threads
- **Efficient API Communication**: Uses caching and batched requests to minimize network usage
- **Memory Management**: Implements proper lifecycle management to prevent memory leaks
---
### **Navigation Architecture**
The app implements a sophisticated navigation system that ensures seamless user experience across different screens and features:
#### **Core Navigation Components**
1. **Screen Routes**:
- Defined as sealed classes with static route patterns
- Support for parameterized routes with dynamic segments (e.g., profile IDs, adventure data)
- Clean separation between route definition and navigation logic
2. **Navigation Events**:
- Event-based navigation model with typed navigation actions
- Support for complex navigation patterns including:
- Standard navigation with backstack management
- Deep linking to specific destinations
- Passing complex data objects between screens
- Pop operations with granular control
3. **Transition Animations**:
- Custom animations for screen transitions
- Slide and fade effects with configurable timing and easing
- Distinct animations for forward navigation vs. back navigation
#### **Navigation Implementation**
1. **MyNavHost**:
- Central navigation component that defines the app's navigation graph
- Handles route registration and screen composition
- Manages transition animations between destinations
2. **Main Navigation Flow**:
- Three-tab bottom navigation structure (Home, Add, Profile)
- Tab-specific sub-navigation with independent backstacks
- Smooth transitions between main sections
3. **Custom Navigation Stack Manager**:
- Provides enhanced control over the navigation backstack
- Supports complex navigation patterns not available in standard Navigation components
- Maintains history state for advanced navigation scenarios
4. **Navigation Event Handling**:
- Navigation events are processed through a central event channel
- Supports navigation with destination change listeners
- Handles navigation with complex data objects using URI encoding
5. **Deep Linking**:
- Support for navigating directly to specific content (profiles, adventures)
- Secure parameter passing for complex data objects
- Error handling to prevent navigation failures
---
For additional details about specific parts of the project, please contact the development team.
```
├── app
│ └── src
│ └── main
│ └── java
│ └── com
│ └── divadventure
│ ├── data
│ │ ├── navigation
│ │ │ ├── Navigation.kt
│ │ │ ├── NavigationEvent.kt
│ │ │ └── NavigationViewModel.kt
│ │ ├── Repository
│ │ │ ├── AdventureRepository.kt
│ │ │ ├── CalendarRepository.kt
│ │ │ ├── InterestsRepository.kt
│ │ │ ├── ProfileRepository.kt
│ │ │ ├── RequestsRepository.kt
│ │ │ ├── UploadImageManager.kt
│ │ │ └── UsersRepository.kt
│ │ └── SharedService.kt
│ ├── di
│ │ ├── AppModule.kt
│ │ ├── FeaturesModules
│ │ │ ├── AdventuresModule.kt
│ │ │ ├── DateModule.kt
│ │ │ ├── LocationModule.kt
│ │ │ ├── ProfileModule.kt
│ │ │ └── RequestsModule.kt
│ │ ├── HomeModule.kt
│ │ ├── NetworkModule.kt
│ │ ├── SharedPreferencesModule.kt
│ │ └── ViewModelModules
│ ├── domain
│ │ ├── models
│ │ │ ├── AdventureModels.kt
│ │ │ ├── AuthModels.kt
│ │ │ ├── Requests.kt
│ │ │ └── UsersModels.kt
│ │ └── usecase
│ │ ├── AdventuresUseCase.kt
│ │ ├── CalendarUseCase.kt
│ │ ├── InterestsUseCase.kt
│ │ ├── LocationsUseCase.kt
│ │ ├── NotificationsUseCase.kt
│ │ ├── RequestsUseCase.kt
│ │ ├── TasksUseCase.kt
│ │ └── UsersUseCase.kt
│ ├── ui
│ │ ├── AddShared.kt
│ │ ├── AuthShared.kt
│ │ ├── HomeShared.kt
│ │ ├── ManageShared.kt
│ │ └── ProfileShared.kt
│ └── viewmodel
│ ├── AuthViewModel.kt
│ ├── BaseViewModel.kt
│ ├── HomeViewModel.kt
│ ├── MainViewModel.kt
│ ├── ManageAdventureViewModel.kt
│ ├── NotificationsViewModel.kt
│ └── ProfileViewModel.kt
└── readme.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment