Skip to content

Instantly share code, notes, and snippets.

@sajjadyousefnia
Created June 13, 2025 04:02
Show Gist options
  • Save sajjadyousefnia/a5509f6ea7f41a1764c123b2f21129b3 to your computer and use it in GitHub Desktop.
Save sajjadyousefnia/a5509f6ea7f41a1764c123b2f21129b3 to your computer and use it in GitHub Desktop.
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\App.kt
```kt
package com.divadventure
import android.app.Application
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import com.divadventure.BuildConfig
import com.divadventure.App.Companion.weakContext
import com.google.android.libraries.places.api.Places
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import java.lang.ref.WeakReference
@HiltAndroidApp
class App : Application() {
override fun onCreate() {
super.onCreate()
weakContext = WeakReference(this)
Places.initialize(applicationContext, "AIzaSyCFUxI4_4TdJvF_piOdkifFRGa2SuH3W3U")
Timber.i("Application onCreate called")
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Timber.d("Timber DebugTree planted for logging")
} else {
Timber.w("Timber is not planted as the app is in Release mode")
}
}
companion object {
private var weakContext: WeakReference<Context>? = null
fun getContext(): Context? {
return weakContext?.get()
}
fun isNetworkConnected(context: Context): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork ?: return false
val activeNetwork =
connectivityManager.getNetworkCapabilities(network) ?: return false
return when {
activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
} else {
val networkInfo = connectivityManager.activeNetworkInfo ?: return false
return networkInfo.isConnected
}
}
}
}
```
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\MainActivity.kt
```kt
package com.divadventure
import android.app.Activity
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import com.divadventure.data.navigation.MyNavHost
import com.divadventure.ui.theme.DivAdventureTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
Color.White.hashCode(),
Color.White.hashCode()
),
navigationBarStyle = SystemBarStyle.auto(
Color.White.hashCode(),
Color.White.hashCode()
)
)
// Tell the system to let our app draw behind the system bars
WindowCompat.setDecorFitsSystemWindows(window, true)
setContent {
DivAdventureTheme {
SetStatusBarAndNavigationColors(
statusBarIconsColor = Color.Black,
navigationKeysColor = Color.Black
)
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Scaffold(modifier = Modifier.fillMaxSize()) { padding ->
MyNavHost(padding)
} }
}
}
}
}
@Composable
fun SetStatusBarAndNavigationColors(
statusBarIconsColor: Color,
navigationKeysColor: Color
) {
val view = LocalView.current
val window = (view.context as Activity).window
val windowInsetsController = WindowCompat.getInsetsController(window, view)
SideEffect {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Set status bar color
window.statusBarColor = Color.Transparent.toArgb()
// Set navigation bar color
window.navigationBarColor = Color.Transparent.toArgb()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
windowInsetsController.isAppearanceLightStatusBars = statusBarIconsColor == Color.Black
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
windowInsetsController.isAppearanceLightNavigationBars =
navigationKeysColor == Color.Black
}
}
}
```
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\components\AdventureActionButton.kt
```kt
package com.divadventure.ui.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.divadventure.domain.models.Adventure
import com.divadventure.domain.models.AdventureType
import com.divadventure.domain.usecase.AdventuresUseCase
import com.divadventure.viewmodel.ManageAdventureViewModel
/**
* A reusable adventure action button that changes color and behavior based on adventure type
*/
@Composable
fun AdventureActionButton(
adventure: Adventure,
adventuresUseCase: AdventuresUseCase,
viewModel: ManageAdventureViewModel,
modifier: Modifier = Modifier
) {
// Determine the adventure type
val adventureType = adventuresUseCase.checkAdventureType(adventure)
// Get the appropriate color for this adventure type
val buttonColor = when (adventureType) {
AdventureType.Manage -> Color(0xFFB175FF) // Indigo
AdventureType.Join -> Color(0xFF30D158) // Green
AdventureType.Going -> Color(0xFF0A84FF) // Orange
AdventureType.Pending -> Color(0xffFFCC00) // Deep Orange
AdventureType.Leave -> Color(0xFFF2F2F7) // Red
}
// Get the appropriate text for this adventure type
val buttonText = when (adventureType) {
AdventureType.Manage -> "Manage"
AdventureType.Join -> "Join"
AdventureType.Going -> "Going"
AdventureType.Pending -> "Pending"
AdventureType.Leave -> "Leave"
}
TextButton(
onClick = { viewModel.performActionByType(adventureType, adventure.id) },
colors = ButtonDefaults.textButtonColors(
contentColor = buttonColor
),
modifier = modifier.padding(horizontal = 8.dp)
) {
Text(
text = buttonText,
fontWeight = FontWeight.SemiBold
)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\components\AdventureItem.kt
```kt
package com.divadventure.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.divadventure.domain.models.Adventure
import com.divadventure.domain.models.AdventureType
import com.divadventure.domain.usecase.AdventuresUseCase
import com.divadventure.viewmodel.ManageAdventureViewModel
/**
* Adventure item card that includes a type-specific action button
* The type checking is performed by the view model, not inside this component
*/
@Composable
fun AdventureItem(
adventure: Adventure,
viewModel: ManageAdventureViewModel,
modifier: Modifier = Modifier
) {
// Use the ViewModel to determine the adventure type
val adventureType =
remember(adventure.id) { viewModel.getAdventureType(adventure) } as AdventureType
Card(
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
// Adventure title
Text(
text = adventure.title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
// Adventure description
Text(
text = adventure.description,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2
)
Spacer(modifier = Modifier.height(16.dp))
// Bottom row with action button
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.weight(1f))
// The AdventureTypeButton doesn't directly use UseCase or ViewModel
// It just takes the already-determined type and an onClick handler
AdventureTypeButton(
adventureType = adventureType,
onClick = {
// Handle button click through the viewModel
viewModel.performActionByType(adventureType, adventure.id)
}
)
}
}
}
}
/**
* Adventure item card with action button that changes based on adventure type
*/
@Composable
fun AdventureItem(
adventure: Adventure,
adventuresUseCase: AdventuresUseCase,
viewModel: ManageAdventureViewModel,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
// Adventure title
Text(
text = adventure.title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
// Adventure description
Text(
text = adventure.description,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2
)
Spacer(modifier = Modifier.height(16.dp))
// Bottom row with action button
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.weight(1f))
// The action button that changes color and behavior based on adventure type
AdventureActionButton(
adventure = adventure,
adventuresUseCase = adventuresUseCase,
viewModel = viewModel
)
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\components\AdventureTypeButton.kt
```kt
package com.divadventure.ui.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.divadventure.domain.models.AdventureType
/**
* A pure UI component for rendering a button styled according to adventure type
* This component doesn't depend on use cases or view models directly
*/
@Composable
fun AdventureTypeButton(
adventureType: AdventureType,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
// Get the appropriate color for this adventure type
val buttonColor = when(adventureType) {
AdventureType.Manage -> Color(0xFF3F51B5) // Indigo
AdventureType.Join -> Color(0xFF4CAF50) // Green
AdventureType.Going -> Color(0xFFFF9800) // Orange
AdventureType.Pending -> Color(0xFFFF5722) // Deep Orange
AdventureType.Leave -> Color(0xFFF44336) // Red
}
// Get the appropriate text for this adventure type
val buttonText = when(adventureType) {
AdventureType.Manage -> "Manage"
AdventureType.Join -> "Join"
AdventureType.Going -> "Going"
AdventureType.Pending -> "Pending"
AdventureType.Leave -> "Leave"
}
TextButton(
onClick = onClick,
colors = ButtonDefaults.textButtonColors(
contentColor = buttonColor
),
modifier = modifier.padding(horizontal = 8.dp)
) {
Text(
text = buttonText,
fontWeight = FontWeight.SemiBold
)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\components\StaticMap.kt
```kt
package com.divadventure.ui.components
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MapProperties
import com.google.maps.android.compose.MapUiSettings
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.MarkerState
import com.google.maps.android.compose.rememberCameraPositionState
/**
* A non-interactive Google Map composable that displays a static view of a location.
*
* @param latitude The latitude coordinate to center the map on
* @param longitude The longitude coordinate to center the map on
* @param modifier Optional modifier for customizing the map's appearance
* @param zoomLevel The zoom level for the map (default: 13f)
*/
@Composable
fun StaticMap(
latitude: Double,
longitude: Double,
modifier: Modifier = Modifier,
zoomLevel: Float = 13f
) {
val location = LatLng(latitude, longitude)
// Set up camera position to focus on the provided coordinates
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(location, zoomLevel)
}
// Disable UI controls and gestures
val uiSettings = MapUiSettings(
zoomControlsEnabled = false,
scrollGesturesEnabled = false,
zoomGesturesEnabled = false,
tiltGesturesEnabled = false,
rotationGesturesEnabled = false,
myLocationButtonEnabled = false,
compassEnabled = false,
indoorLevelPickerEnabled = false,
mapToolbarEnabled = false,
scrollGesturesEnabledDuringRotateOrZoom = false
)
val mapProperties = MapProperties(maxZoomPreference = zoomLevel, minZoomPreference = zoomLevel)
GoogleMap(
modifier = modifier
.fillMaxWidth()
.aspectRatio(1f) // 1:1 aspect ratio
.clip(RoundedCornerShape(8.dp)),
cameraPositionState = cameraPositionState,
uiSettings = uiSettings,
properties = mapProperties
) {
// Add a marker at the specified location
Marker(
state = MarkerState(position = location),
title = "Selected Location"
)
}
}
```
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.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.platform.LocalDensity
import androidx.compose.ui.res.painterResource
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 androidx.compose.ui.unit.sp
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,
)
)
}
// -- Sample Data Model --
enum class FriendStatus { REMOVED, PENDING, ADD }
data class Friend(
val name: String,
val username: String,
val imageRes: Int, // Replace with your image resource
val status: FriendStatus
)
// -- Sample List --
val friends = listOf(
Friend("Babak Basseri", "@babak-ba", R.drawable.random_image_1, FriendStatus.REMOVED),
Friend("Barbod Sharif", "@barbodddd", R.drawable.random_image_2, FriendStatus.REMOVED),
Friend("Barin Ahmadi", "@barin", R.drawable.random_image_3, FriendStatus.PENDING),
Friend("Barin Ahmadi", "@barin", R.drawable.random_image_3, FriendStatus.REMOVED),
Friend("Barbod Sharif", "@barbodddd", R.drawable.random_image_2, FriendStatus.ADD),
Friend("Barin Ahmadi", "@barin", R.drawable.random_image_3, FriendStatus.REMOVED),
Friend("Barin Ahmadi", "@barin", R.drawable.random_image_3, FriendStatus.REMOVED),
Friend("Barin Ahmadi", "@barin", R.drawable.random_image_3, FriendStatus.REMOVED),
Friend("Barin Ahmadi", "@barin", R.drawable.random_image_3, FriendStatus.REMOVED),
)
@Composable
fun FriendList(friends: List<Friend>) {
Column(modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF8F9FB))) {
friends.forEach { friend ->
FriendItem(friend)
}
}
}
@Composable
fun FriendItem(friend: Friend) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = friend.imageRes),
contentDescription = null,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(14.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = friend.name,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
Text(
text = friend.username,
color = Color.Gray,
fontSize = 13.sp
)
}
Spacer(modifier = Modifier.width(10.dp))
FriendActionButton(friend.status)
}
}
}
@Composable
fun FriendActionButton(status: FriendStatus) {
when (status) {
FriendStatus.REMOVED -> Button(
onClick = { /*TODO*/ },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFF6F7FB),
contentColor = Color.Gray
),
shape = RoundedCornerShape(6.dp)
) {
Text("Remove Friend")
}
FriendStatus.PENDING -> Button(
onClick = { /*TODO*/ },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFF9800),
contentColor = Color.White
),
shape = RoundedCornerShape(6.dp)
) {
Text("Pending")
}
FriendStatus.ADD -> Button(
onClick = { /*TODO*/ },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF19C37D),
contentColor = Color.White
),
shape = RoundedCornerShape(6.dp)
) {
Text("Add Friend")
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\otp\ModifierExt.kt
```kt
package com.divadventure.ui.otp
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.dp
/**
* Creates a Modifier that adds a bottom stroke to a Composable.
*
* @param color The color of the stroke.
* @param strokeWidth The thickness of the stroke.
* @return A Modifier that draws a bottom stroke.
*/
fun Modifier.bottomStroke(color: Color, strokeWidth: Dp = 2.dp): Modifier = this.then(
Modifier.drawBehind {
val strokePx = strokeWidth.toPx()
// Draw a line at the bottom
drawLine(
color = color,
start = Offset(x = 0f, y = size.height - strokePx / 2),
end = Offset(x = size.width, y = size.height - strokePx / 2),
strokeWidth = strokePx
)
}
)
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\otp\OtpInputField.kt
```kt
package com.divadventure.ui.otp
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
/**
* Data class representing an individual OTP field.
*
* @property text The text content of the OTP field.
* @property focusRequester A FocusRequester to manage focus on the field.
*/
private data class OtpField(
val text: String,
val index: Int,
val focusRequester: FocusRequester? = null
)
/**
* A Composable function that creates a row of OTP input fields based on the specified count.
* Each field supports custom visual modifications and can handle input in various formats
* depending on the specified keyboard type. The function manages the creation and updating
* of these fields dynamically based on the current OTP value and provides functionality
* for managing focus transitions between the fields.
*
* @param otp A mutable state holding the current OTP value. This state is observed for changes
* to update the individual fields and to reset focus as necessary.
* @param count The number of OTP input boxes to display. This defines how many individual
* fields will be generated and managed.
* @param otpBoxModifier A Modifier passed to each OTP input box for custom styling and behavior.
* It allows for adjustments such as size or specific visual effects.
* Note: Avoid adding padding directly to `otpBoxModifier` as it may interfere
* with the layout calculations for the OTP fields. If padding is necessary,
* consider applying it to surrounding elements or within the `OtpBox` composable.
* @param otpTextType The type of keyboard to display when a field is focused, typically set to
* KeyboardType.Number for OTP inputs. This can be adjusted if alphanumeric
* OTPs are required.
* @param textColor The color used for the text within each OTP box, allowing for visual customization.
*
* The function sets up each input field with its own state and focus requester, managing
* internal state updates in response to changes in the OTP value and user interactions.
* The layout is organized as a horizontal row of text fields, with each field designed to
* capture a single character of the OTP. Focus automatically advances to the next field upon
* input, and if configured, input characters can be visually obscured for security.
*
* Example usage:
* ```kotlin
* OtpInputField(
* otp = remember { mutableStateOf("12345") },
* count = 5,
* otpBoxModifier = Modifier.border(1.dp, Color.Black).background(Color.White),
* otpTextType = KeyboardType.Number,
* textColor = Color.Black // Setting the text color to black
* )
* ```
* This example sets up an OTP field with a basic black border and white background, without padding.
*/
@Composable
fun OtpInputField(
otp: MutableState<String>, // The current OTP value.
count: Int = 5, // Number of OTP boxes.
otpBoxModifier: Modifier = Modifier
.border(1.pxToDp(), Color.Gray)
.background(Color.White),
otpTextType: KeyboardType = KeyboardType.Number,
textColor: Color = Color.Black
) {
val scope = rememberCoroutineScope()
// Initialize state for each OTP box with its character and optional focus requester.
val otpFieldsValues = remember {
(0 until count).mapIndexed { index, i ->
mutableStateOf(
OtpField(
text = otp.value.getOrNull(i)?.toString() ?: "",
index = index,
focusRequester = FocusRequester()
)
)
}
}
// Update each OTP box's value when the overall OTP value changes, and manage focus.
LaunchedEffect(key1 = otp.value) {
for (i in otpFieldsValues.indices) {
otpFieldsValues[i].value =
otpFieldsValues[i].value.copy(otp.value.getOrNull(i)?.toString() ?: "")
}
// Request focus on the first box if the OTP is blank (e.g., reset).
if (otp.value.isBlank()) {
try {
otpFieldsValues[0].value.focusRequester?.requestFocus()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// Create a row of OTP boxes.
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
repeat(count) { index ->
// For each OTP box, manage its value, focus, and what happens on value change.
OtpBox(
modifier = otpBoxModifier,
otpValue = otpFieldsValues[index].value,
textType = otpTextType,
textColor = textColor,
isLastItem = index == count - 1, // Check if this box is the last in the sequence.
totalBoxCount = count,
onValueChange = { newValue ->
// Handling logic for input changes, including moving focus and updating OTP state.
scope.launch {
handleOtpInputChange(index, count, newValue, otpFieldsValues, otp)
}
},
onFocusSet = { focusRequester ->
// Save the focus requester for each box to manage focus programmatically.
otpFieldsValues[index].value =
otpFieldsValues[index].value.copy(focusRequester = focusRequester)
},
onNext = {
// Attempt to move focus to the next box when the "next" action is triggered.
focusNextBox(index, count, otpFieldsValues)
},
)
}
}
}
/**
* Handles input changes for each OTP box and manages the logic for updating the OTP state
* and managing focus transitions between OTP boxes.
*
* @param index The index of the OTP box where the input change occurred.
* @param count The total number of OTP boxes.
* @param newValue The new value inputted into the OTP box at the specified index.
* @param otpFieldsValues A list of mutable states, each representing an individual OTP box's state.
* @param otp A mutable state holding the current concatenated value of all OTP boxes.
*
* The function updates the text of the targeted OTP box based on the length and content of `newValue`.
* If `newValue` contains only one character, it replaces the existing text in the current box.
* If two characters are present, likely from rapid user input, it sets the box's text to the second character,
* assuming the first character was already accepted. If multiple characters are pasted,
* they are distributed across the subsequent boxes starting from the current index.
*
* Focus management is also handled, where focus is moved to the next box if a single character is inputted,
* and moved back to the previous box if the current box is cleared. This is especially useful for
* scenarios where users might quickly navigate between OTP fields either by typing or deleting characters.
*
* Exception handling is used to catch and log any errors that occur during focus management to avoid
* crashing the application and to provide debug information.
*/
private fun handleOtpInputChange(
index: Int,
count: Int,
newValue: String,
otpFieldsValues: List<MutableState<OtpField>>,
otp: MutableState<String>
) {
// Handle input for the current box.
if (newValue.length <= 1) {
// Directly set the new value if it's a single character.
otpFieldsValues[index].value = otpFieldsValues[index].value.copy(text = newValue)
} else if (newValue.length == 2) {
// If length of new value is 2, we can guess the user is typing focusing on current box
// In this case set the unmatched character only
otpFieldsValues[index].value =
otpFieldsValues[index].value.copy(text = newValue.lastOrNull()?.toString() ?: "")
} else if (newValue.isNotEmpty()) {
// If pasting multiple characters, distribute them across the boxes starting from the current index.
newValue.forEachIndexed { i, char ->
if (index + i < count) {
otpFieldsValues[index + i].value =
otpFieldsValues[index + i].value.copy(text = char.toString())
}
}
}
// Update the overall OTP state.
var currentOtp = ""
otpFieldsValues.forEach {
currentOtp += it.value.text
}
try {
// Logic to manage focus.
if (newValue.isEmpty() && index > 0) {
// If clearing a box and it's not the first box, move focus to the previous box.
otpFieldsValues.getOrNull(index - 1)?.value?.focusRequester?.requestFocus()
} else if (index < count - 1 && newValue.isNotEmpty()) {
// If adding a character and not on the last box, move focus to the next box.
otpFieldsValues.getOrNull(index + 1)?.value?.focusRequester?.requestFocus()
}
} catch (e: Exception) {
e.printStackTrace()
}
otp.value = currentOtp
}
private fun focusNextBox(
index: Int,
count: Int,
otpFieldsValues: List<MutableState<OtpField>>
) {
if (index + 1 < count) {
// Move focus to the next box if the current one is filled and it's not the last box.
try {
otpFieldsValues[index + 1].value.focusRequester?.requestFocus()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@Composable
private fun OtpBox(
modifier: Modifier,
otpValue: OtpField, // Current value of this OTP box.
textType: KeyboardType = KeyboardType.Number,
textColor: Color = Color.Black,
isLastItem: Boolean, // Whether this box is the last in the sequence.
totalBoxCount: Int, // Total number of OTP boxes for layout calculations.
onValueChange: (String) -> Unit, // Callback for when the value changes.
onFocusSet: (FocusRequester) -> Unit, // Callback to set focus requester.
onNext: () -> Unit, // Callback for handling "next" action, typically moving focus forward.
) {
val focusManager = LocalFocusManager.current
val focusRequest = otpValue.focusRequester ?: FocusRequester()
val keyboardController = LocalSoftwareKeyboardController.current
// Calculate the size of the box based on screen width and total count.
// If you're using this in Kotlin multiplatform mobile
// val screenWidth = LocalWindowInfo.current.containerSize.width
// If you're using this in Android
val screenWidth = LocalConfiguration.current.screenWidthDp.dp.dpToPx().toInt()
val paddingValue = 5
val totalBoxSize = (screenWidth / totalBoxCount) - paddingValue * totalBoxCount
Box(
modifier = modifier
.size(totalBoxSize.pxToDp()),
contentAlignment = Alignment.Center,
) {
BasicTextField(
value = TextFieldValue(otpValue.text, TextRange(maxOf(0, otpValue.text.length))),
onValueChange = {
// Logic to prevent re-triggering onValueChange when focusing.
if (!it.text.equals(otpValue)) {
onValueChange(it.text)
}
},
// Setup for focus and keyboard behavior.
modifier = Modifier
.testTag("otpBox${otpValue.index}")
.focusRequester(focusRequest)
.onGloballyPositioned {
onFocusSet(focusRequest)
},
textStyle = MaterialTheme.typography.titleLarge.copy(
textAlign = TextAlign.Center,
color = textColor
),
keyboardOptions = KeyboardOptions(
keyboardType = textType,
imeAction = if (isLastItem) ImeAction.Done else ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = {
onNext()
},
onDone = {
// Hide keyboard and clear focus when done.
keyboardController?.hide()
focusManager.clearFocus()
}
),
singleLine = true,
visualTransformation = getVisualTransformation(textType),
)
}
}
/**
* Provides an appropriate VisualTransformation based on the specified keyboard type.
* This method is used to determine how text should be displayed in the UI.
*
* @param textType The type of keyboard input expected, which determines if the text should be obscured.
* @return A VisualTransformation that either obscures text for password fields or displays text normally.
* Password and NumberPassword fields will have their input obscured with bullet characters.
* All other fields will display text as entered.
*/
@Composable
private fun getVisualTransformation(textType: KeyboardType) =
if (textType == KeyboardType.NumberPassword || textType == KeyboardType.Password) PasswordVisualTransformation() else VisualTransformation.None
@Composable
fun Dp.dpToPx() = with(LocalDensity.current) { [email protected]() }
@Composable
fun Int.pxToDp() = with(LocalDensity.current) { [email protected]() }
@Preview
@Composable
fun OtpView_Preivew() {
MaterialTheme {
val otpValue = remember {
mutableStateOf("124")
}
Column(
modifier = Modifier.padding(40.pxToDp()),
verticalArrangement = Arrangement.spacedBy(20.pxToDp())
) {
OtpInputField(
otp = otpValue,
count = 4,
otpBoxModifier = Modifier
.border(1.pxToDp(), Color.Black)
.background(Color.White),
otpTextType = KeyboardType.Number
)
OtpInputField(
otp = otpValue,
count = 4,
otpTextType = KeyboardType.NumberPassword,
otpBoxModifier = Modifier
.border(3.pxToDp(), Color.Gray)
.background(Color.White)
)
OtpInputField(
otp = otpValue,
count = 5,
textColor = MaterialTheme.colorScheme.onBackground,
otpBoxModifier = Modifier
.border(7.pxToDp(), Color(0xFF277F51), shape = RoundedCornerShape(12.pxToDp()))
)
OtpInputField(
otp = otpValue,
count = 5,
otpBoxModifier = Modifier
.bottomStroke(color = Color.DarkGray, strokeWidth = 6.pxToDp())
)
}
}
}
```
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\ui\screens\ChangeEmail.kt
```kt
package com.divadventure.ui.screens
import android.os.CountDownTimer
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
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.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.divadventure.R
import com.divadventure.ui.TopSnackBar
import com.divadventure.ui.theme.SystemUIManager
import com.divadventure.viewmodel.AuthIntent
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.viewmodel.AuthUiEvent
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import timber.log.Timber
@Composable
fun ChangeEmail(
viewModel: AuthViewModel, navigationViewModel: NavigationViewModel, padding: PaddingValues
) {
val state by viewModel.state.collectAsState()
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
Color.Transparent,
Color.Transparent,
hideNavigationBar = !state.navigationBarVisibility,
hideStatusBar = !state.statusBarVisibility,
onSystemBarsVisibilityChange = { statusbar, navigationBar ->
/*
viewModel.sendIntent(
AuthIntent.MutualIntent.ChangeInsetsVisibility(
statusbar, navigationBar
)
)
*/
})
val systemUiController = rememberSystemUiController()
val darkTheme = isSystemInDarkTheme()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.White, darkIcons = !darkTheme
)
systemUiController.setNavigationBarColor(
color = Color(0xffefeff4), darkIcons = !darkTheme
)
}
Box(
modifier = Modifier.background(color = Color(0xffefeff4))
) {
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is AuthUiEvent.ExecuteNavigation -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
TopSnackBar(
paddingTop = padding.calculateTopPadding(),
title = topSnackBarTitle,
message = topSnackBarMessage,
show = showTopSnackBar,
onDismiss = { showTopSnackBar = false })
// Handle timer and auto-dismiss
LaunchedEffect(showTopSnackBar) {
if (showTopSnackBar) {
timer.value = object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
coroutineScope.launch {
showTopSnackBar = false
}
}
}.start()
} else {
timer.value?.cancel()
}
}
var email by remember {
mutableStateOf(state.ChangeEmailState!!.email)
}
val focusRequester = remember {
FocusRequester()
}
var revisionEmailButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
var continueButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
revisionEmailButtonColor = if (state.verificationState!!.permitEmailRevision) {
Color(0xff30D158)
} else {
Color(0xffBFBFBF)
}
continueButtonColor = if (state.verificationState!!.isOtpCorrect) {
Color(0xff30D158)
} else {
Color(0xffBFBFBF)
}
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.padding(0.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.background(color = Color.White)
.padding(50.dp, top = 15.dp + padding.calculateTopPadding(), bottom = 15.dp)
.fillMaxWidth()
) {
Text(
"Email Verification", style = TextStyle(
textAlign = TextAlign.Left,
color = Color(0xff1C1C1E),
fontSize = 20.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Bold
)
)
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(30.dp, 20.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White,
disabledContainerColor = Color.White,
contentColor = Color(0xff1C1C1E),
disabledContentColor = Color(0xff1C1C1E)
)
) {
Box(modifier = Modifier.fillMaxSize()) {
BasicTextField(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center)
.padding(15.dp, 0.dp),
textStyle = TextStyle(fontSize = 17.sp, textAlign = TextAlign.Left),
value = email,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email
),
onValueChange = { value: String ->
email = value
})
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(30.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(15.dp, Alignment.CenterVertically),
) {
Card(
modifier = Modifier
.clickable {
Timber.d("Verification: OTP is incorrect")
viewModel.sendIntent(
AuthIntent.ChangeEmailIntent.UpdateEmail(
email
)
)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xff30D158),
disabledContainerColor = Color(0xff30D158),
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Update"
)
}
// show forms error
}/*
Card(
modifier = Modifier
.clickable {
*//*
viewModel.sendIntent(
AuthIntent.OnNavigation(
source = Screen.VerificationScreen.route,
destination = Screen.LoginScreen.route
)
)
*//*
viewModel.sendIntent(
AuthIntent.VerificationChangeEmailIntent.ResendCode
)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White,
disabledContainerColor = Color.White,
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
color = Color(0xff007AFF),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Resend Link"
)
}
// show forms error
}
*/
Card(
modifier = Modifier
.clickable {
Timber.d("Verification: OTP is incorrect")
viewModel.sendIntent(
AuthIntent.ChangeEmailIntent.BackToLogin
)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White,
disabledContainerColor = Color.White,
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
color = Color(0xff007AFF),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Back To Login"
)
}
// show forms error
}
}
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\ForgotPasswordScreen.kt
```kt
package com.divadventure.ui.screens
import android.os.CountDownTimer
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
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.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.composeuisuite.ohteepee.OhTeePeeDefaults
import com.composeuisuite.ohteepee.OhTeePeeInput
import com.divadventure.R
import com.divadventure.ui.TopSnackBar
import com.divadventure.ui.theme.SystemUIManager
import com.divadventure.viewmodel.AuthIntent
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.viewmodel.AuthUiEvent
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
@Composable
fun ForgotPasswordScreen(
viewModel: AuthViewModel,
navigationViewModel: NavigationViewModel,
) {
val state by viewModel.state.collectAsState()
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
Color.Transparent,
Color.Transparent,
hideNavigationBar = !state.navigationBarVisibility,
hideStatusBar = !state.statusBarVisibility,
onSystemBarsVisibilityChange = { statusbar, navigationBar ->
viewModel.sendIntent(
AuthIntent.MutualIntent.ChangeInsetsVisibility(
statusbar, navigationBar
)
)
})
Scaffold(
containerColor = Color(0xffefeff4)
) { paddingValues: PaddingValues ->
var otpString by remember { mutableStateOf("") }
val email = remember {
mutableStateOf("")
}
remember {
FocusRequester()
}
var continueButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
continueButtonColor = if (state.forgotPasswordState!!.timeEnd) {
Color(0xff30D158)
} else {
Color(0xffBFBFBF)
}
var resendButtonColor by remember {
mutableStateOf(Color(0xff007AFF))
}
resendButtonColor = if (state.forgotPasswordState!!.timeEnd == true) {
Color(0xff007AFF)
} else {
Color(0xffBFBFBF)
}
// this config will be used for each cell
val defaultCellConfig = OhTeePeeDefaults.cellConfiguration(
borderColor = Color.LightGray,
borderWidth = 0.dp,
shape = RoundedCornerShape(8.dp),
textStyle = TextStyle(
color = Color.Black
)
)
val filledCellConfig = OhTeePeeDefaults.cellConfiguration(
borderColor = Color(0xff007AFF),
borderWidth = 1.dp,
shape = RoundedCornerShape(8.dp),
textStyle = TextStyle(
color = Color.Black
)
)
Box(
modifier = Modifier.fillMaxSize()
) {
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is AuthUiEvent.ExecuteNavigation -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
// Handle timer and auto-dismiss
LaunchedEffect(showTopSnackBar) {
if (showTopSnackBar) {
timer.value = object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
coroutineScope.launch {
showTopSnackBar = false
}
}
}.start()
} else {
timer.value?.cancel()
}
}
TopSnackBar(
paddingTop = paddingValues.calculateTopPadding(),
title = topSnackBarTitle,
message = topSnackBarMessage,
show = showTopSnackBar,
onDismiss = { showTopSnackBar = false })
Column(
modifier = Modifier, horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.background(color = Color.White)
.padding(
50.dp,
bottom = 15.dp,
top = 15.dp + paddingValues.calculateTopPadding()
)
.fillMaxWidth()
) {
Text(
"Verification Code", style = TextStyle(
textAlign = TextAlign.Left,
color = Color(0xff1C1C1E),
fontSize = 20.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Bold
)
)
}
Box(
modifier = Modifier
.padding(30.dp)
.fillMaxWidth()
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier
.padding(0.dp, 5.dp)
.fillMaxWidth(),
text = "A verification code has been sent to",
style = TextStyle(
color = Color.Black,
textAlign = TextAlign.Center,
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
Text(
modifier = Modifier.fillMaxWidth(),
text = state.forgotPasswordState!!.email,
style = TextStyle(
color = Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontSize = 16.sp,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold
)
)
}
}
var otpTimeLeft by remember { mutableStateOf(120) } // Initialize timeLeft to 120 seconds
LaunchedEffect(key1 = state.forgotPasswordState!!.resetTimer) {
if (state.forgotPasswordState!!.resetTimer) {
otpTimeLeft = 120 // Reset the timer
while (otpTimeLeft > 0) {
delay(1000L) // Wait for 1 second
otpTimeLeft-- // Decrease timeLeft by 1
// Send the updated timeLeft to the ViewModel
viewModel.sendIntent(
AuthIntent.ForgotPasswordIntent.CheckRemainTime(
otpTimeLeft
)
)
}
viewModel.sendIntent(
AuthIntent.ForgotPasswordIntent.CheckRemainTime(
otpTimeLeft
)
)
// Handle the case when timeLeft reaches 0 (e.g., disable resend button)
Timber.d("Timer Finished")
viewModel.state.value.forgotPasswordState!!.resetTimer =
false // Set resetTimer to false
}
}
Text(
modifier = Modifier
.padding(30.dp, 0.dp)
.fillMaxWidth(),
text = buildAnnotatedString {
append(
if (state.forgotPasswordState!!.timeEnd == false) "Please check your inbox and enter the verification code below to verify your email address. The code will expire in "
else "The time has been finished"
)
withStyle(style = TextStyle(fontWeight = FontWeight.Bold).toSpanStyle()) {
append(
if (state.forgotPasswordState!!.timeEnd == false) " $otpTimeLeft seconds"
else ""
)
}
},
style = TextStyle(
color = Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
textAlign = TextAlign.Center,
)
Box(modifier = Modifier.fillMaxWidth()) {
OhTeePeeInput(
modifier = Modifier
.align(Alignment.Center)
.padding(0.dp, 20.dp),
value = otpString,
onValueChange = { newValue: String, isValid: Boolean ->
otpString = newValue
viewModel.sendIntent(
AuthIntent.ForgotPasswordIntent.OnOtpChanged(otpString.trim())
)
Timber.d("OTP: $newValue")
},
autoFocusByDefault = true,
horizontalArrangement = Arrangement.spacedBy(1.dp),
configurations = OhTeePeeDefaults.inputConfiguration(
cellsCount = 6,
cellModifier = Modifier
.width(46.dp)
.height(54.dp),
activeCellConfig = filledCellConfig,
emptyCellConfig = defaultCellConfig,
filledCellConfig = filledCellConfig,
),
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(30.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
) {
Card(
modifier = Modifier
.clickable {
/*
viewModel.sendIntent(
AuthIntent.VerificationIntent.OnOtpVerifyPressed(
otp = otpString
)
)
*/
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xff30D158),
disabledContainerColor = Color(0xff30D158),
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Verify"
)
}
// show forms error
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 10.dp)
) {
Text(
modifier =
if (otpTimeLeft == 0)
Modifier.clickable {
viewModel.sendIntent(
AuthIntent.ForgotPasswordIntent.ResendCode
)
}
else Modifier,
text = "Resend Code",
fontSize = 17.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
style = TextStyle(
color = resendButtonColor
),
textAlign = TextAlign.Left
)
Spacer(modifier = Modifier.weight(1f, true))
}
}
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\LandingScreen.kt
```kt
package com.divadventure.ui.screens
import android.os.CountDownTimer
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
import com.divadventure.R
import com.divadventure.ui.theme.SystemUIManager
import com.divadventure.viewmodel.AuthIntent
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.viewmodel.AuthUiEvent
import kotlin.random.Random
@Composable
fun LandingScreen(
viewModel: AuthViewModel,
navigationViewModel: NavigationViewModel,
padding: PaddingValues
) {
val state by viewModel.state.collectAsState()
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
Color.Transparent,
Color.Transparent,
hideNavigationBar = !state.navigationBarVisibility,
hideStatusBar = !state.statusBarVisibility,
onSystemBarsVisibilityChange = { statusbar, navigationBar ->
/*
viewModel.sendIntent(
AuthIntent.MutualIntent.ChangeInsetsVisibility(
statusbar, navigationBar
)
)
*/
})
Scaffold(
topBar = {
// Add a top bar here if required in the future
}
) { paddingValues ->
var mainTextColor = remember { mutableStateOf(Color(0xff1C1C1E)) }
val random = Random.nextInt(1, 3)
val landing = when (random) {
1 -> {
mainTextColor.value = Color(0xff1C1C1E)
R.drawable.landing1
}
else -> {
mainTextColor.value = Color.White
R.drawable.landing2
}
}
Box(modifier = Modifier.fillMaxSize()) {
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is AuthUiEvent.ExecuteNavigation -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
Image(
painter = rememberAsyncImagePainter(model = landing),
contentDescription = "Landing",
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
Column(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(0.2f))
Text(
modifier = Modifier.weight(0.4f),
text = "DivAdventure", style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
color = mainTextColor.value
)
)
}
Column(
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
.padding(0.dp, 0.dp, 0.dp, 80.dp),
verticalArrangement = Arrangement.spacedBy(
10.dp,
Alignment.Bottom
)
) {
val coroutineScope = rememberCoroutineScope()
Card(
modifier = Modifier
.clickable {
viewModel.sendIntent(
AuthIntent.LandingIntent.gotoSignup
)
}
.fillMaxWidth()
.padding(15.dp, 0.dp),
colors = CardColors(
containerColor = Color(0xFF30D158),
contentColor = Color.White,
disabledContainerColor = Color(0xFF30D158),
disabledContentColor = Color.White
), shape = RoundedCornerShape(4.dp)
) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = "Sign Up", modifier = Modifier
.padding(16.dp),
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 17.sp
),
textAlign = TextAlign.Center
)
}
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(15.dp, 0.dp),
colors = CardColors(
containerColor = Color(0xFFf2f2f7),
contentColor = Color(0xff251514),
disabledContainerColor = Color(0xFFf2f2f7),
disabledContentColor = Color(0xff251514)
),
shape = RoundedCornerShape(4.dp).copy(all = CornerSize(4.dp))
) {
Box(
modifier = Modifier
.clickable {
viewModel.sendIntent(
AuthIntent.LandingIntent.gotoLogin
)
}
.fillMaxWidth(), contentAlignment = Alignment.Center
) {
Text(
text = "Login", modifier = Modifier.padding(16.dp), style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 17.sp
),
textAlign = TextAlign.Center
)
}
}
}
}
}
} // Closing Scaffold
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\Login.kt
```kt
package com.divadventure.ui.screens
import android.os.CountDownTimer
import androidx.compose.foundation.BorderStroke
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.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
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.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.divadventure.R
import com.divadventure.ui.AuthTextField
import com.divadventure.ui.CARD_HEIGHT
import com.divadventure.ui.TopSnackBar
import com.divadventure.ui.theme.SystemUIManager
import com.divadventure.viewmodel.AuthIntent
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.viewmodel.AuthUiEvent
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
viewModel: AuthViewModel,
navigationViewModel: NavigationViewModel,
) {
val state by viewModel.state.collectAsState()
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
Color.Transparent,
Color.Transparent,
hideNavigationBar = !state.navigationBarVisibility,
hideStatusBar = !state.statusBarVisibility,
onSystemBarsVisibilityChange = { statusbar, navigationBar ->
/*
viewModel.sendIntent(
AuthIntent.MutualIntent.ChangeInsetsVisibility(
statusbar, navigationBar
)
)
*/
})
val snackBarHost = remember { SnackbarHostState() }
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is AuthUiEvent.ExecuteNavigation -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
// Handle timer and auto-dismiss
LaunchedEffect(showTopSnackBar) {
if (showTopSnackBar) {
timer.value = object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
coroutineScope.launch {
showTopSnackBar = false
}
}
}.start()
} else {
timer.value?.cancel()
}
}
Scaffold(
// modifier = Modifier.padding(padding),
containerColor = Color.White
) { padding ->
var forgetPasswordEmailButtonColor by remember { mutableStateOf(Color(0xffBFBFBF)) }
forgetPasswordEmailButtonColor = when (state.loginState!!.forgetPasswordEmailCorrect) {
true -> {
Color(0xff007AFF)
}
false -> {
Color(0xffBFBFBF)
}
}
var loginButtonColor by remember {
mutableStateOf(
Color(0xff30D158)
)
}
when (state.loginState!!.loginClickable) {
true -> {
loginButtonColor = Color(0xff30D158)
}
false -> {
loginButtonColor = Color(0xffBFBFBF)
}
}
var emailOrUsername = remember {
mutableStateOf("")
}
var password = remember {
mutableStateOf("")
}
var showDialog by remember {
mutableStateOf(false)
}
var forgotEmail by remember {
mutableStateOf("")
}
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
is AuthUiEvent.ExecuteNavigation -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
launch {
snackBarHost.showSnackbar(
message = event.message, duration = SnackbarDuration.Long
)
}
}
}
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
if (showDialog) {
Dialog(
onDismissRequest = { showDialog = false },
) {
Card(
modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(
containerColor = Color(0xFFf2f2f2),
disabledContainerColor = Color(0xFFf2f2f2),
contentColor = Color.White,
disabledContentColor = Color.White
)
) {
Column {
Column(
modifier = Modifier.padding(10.dp, 10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Forgot Password",
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 10.dp),
style = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 22.sp,
textAlign = TextAlign.Center,
color = Color.Black
)
)
Text(
modifier = Modifier
.padding(0.dp, 10.dp)
.fillMaxWidth(),
text = "To initiate the password reset process, " + "please provide your email address below:",
style = TextStyle(color = Color.Black, fontSize = 16.sp),
textAlign = TextAlign.Center
)
Card(
modifier = Modifier
.padding(10.dp, 10.dp)
.heightIn(20.dp, 50.dp),
border = BorderStroke(
1.dp, Color(0x40000000)
),
shape = RoundedCornerShape(6.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
TextField(
modifier = Modifier
.fillMaxSize(),
value = forgotEmail,
onValueChange = {
forgotEmail = it
viewModel.sendIntent(
AuthIntent.LoginIntent.OnForgotPasswordChanged(it)
)
},
textStyle = TextStyle(
color = Color.Black, fontSize = 16.sp
),
colors = OutlinedTextFieldDefaults.colors(
errorBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
cursorColor = Color.Black
),
shape = RoundedCornerShape(1.dp),
interactionSource = remember { MutableInteractionSource() },
singleLine = true
)
}
}
}
HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.background(Color.Transparent)
)
Row(
modifier = Modifier.height(50.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.clickable {
showDialog = false
}
.weight(0.5f, true)) {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Cancel",
style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xff007AFF),
textAlign = TextAlign.Center
)
)
}
VerticalDivider(
modifier = Modifier.width(1.dp)
)
Box(
modifier = Modifier
.clickable {
viewModel.sendIntent(
AuthIntent.LoginIntent.ForgotPassword(
forgotEmail
)
)
showDialog = false
}
.weight(0.5f, true)) {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Send Code",
style = TextStyle(
color = forgetPasswordEmailButtonColor,
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold
)
)
}
}
}
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(0.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(modifier = Modifier.weight(0.3f, true)) {
TopSnackBar(
paddingTop = padding.calculateTopPadding(),
title = topSnackBarTitle,
message = topSnackBarMessage,
show = showTopSnackBar,
onDismiss = { showTopSnackBar = false })
Text(
modifier = Modifier
.align(
Alignment.Center
)
.padding(0.dp, 0.dp), text = "DivAdventure", style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
color = Color(0xff30D158),
)
)
}
Column(modifier = Modifier.weight(0.333f, true)) {
AuthTextField(
modifier = Modifier, hint = "Email", text = emailOrUsername, onValueChange = {
viewModel.sendIntent(
AuthIntent.LoginIntent.OnEmailChanged(
email = emailOrUsername.value
)
)
}, explain = "Your Email Address"
)
AuthTextField(
modifier = Modifier.padding(0.dp, 40.dp, 0.dp, 0.dp),
hint = "Password",
text = password,
isPassword = true,
onValueChange = {
viewModel.sendIntent(
AuthIntent.LoginIntent.OnPasswordChanged(
password = password.value
)
)
},
explain = "Your Password"
)
Box(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Forgot Password?", style = TextStyle(
textDecoration = TextDecoration.Underline,
fontWeight = FontWeight.SemiBold,
color = Color.Black,
fontSize = 12.sp,
textAlign = TextAlign.Center
), modifier = Modifier
.padding(0.dp, 20.dp)
.align(
Alignment.Center
)
.clickable {
// viewModel.sendIntent(AuthIntent.LoginIntent.ForgotPassword)
showDialog = true
})
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(0.333f, true)
.padding(15.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(15.dp, Alignment.Top)
) {
Card(
modifier = Modifier
.clickable {
viewModel.sendIntent(
AuthIntent.LoginIntent.Login(
emailOrUsername = emailOrUsername.value,
password = password.value
)
)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(CARD_HEIGHT),
colors = CardDefaults.cardColors(
containerColor = loginButtonColor,
disabledContainerColor = loginButtonColor,
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Sign In"
)
}
// show forms error
Text(state.loginState!!.error, color = Color.Red)
}
Card(
modifier = Modifier
.clickable {
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(CARD_HEIGHT),
border = BorderStroke(1.dp, Color(0xffBFBFBF)),
colors = CardDefaults.cardColors(
containerColor = Color.White,
disabledContainerColor = Color.White,
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier.padding(0.dp, 0.dp, 10.dp, 0.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_google),
contentDescription = "google",
)
Text(
modifier = Modifier.padding(
10.dp, 0.dp, 0.dp, 0.dp
), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color.Black
), text = "Sign In with Google"
)
}
}
}
Card(
modifier = Modifier
.clickable {}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(CARD_HEIGHT),
colors = CardDefaults.cardColors(
containerColor = Color(0xff2553B4),
disabledContainerColor = Color(0xff2553B4),
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier.padding(0.dp, 0.dp, 10.dp, 0.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_facebook),
contentDescription = "facebook",
)
Text(
modifier = Modifier.padding(
10.dp, 0.dp, 0.dp, 0.dp
), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color.White
), text = "Sign In with Facebook"
)
}
}
}
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\add\Add.kt
```kt
package com.divadventure.ui.screens.main.add
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.divadventure.R
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.ui.BackCompose
import com.divadventure.ui.ChangerButton
import com.divadventure.ui.ItemTextClickIcon
import com.divadventure.ui.MandatoryInterestsComposable
import com.divadventure.ui.PersonalInfoTextField
import com.divadventure.ui.SelectDate
import com.divadventure.ui.SelectionList
import com.divadventure.ui.SimpleTextField
import com.divadventure.ui.TitleCompose
import com.divadventure.ui.WhiteRoundedCornerFrame
import com.divadventure.ui.screens.main.home.notifications.search.filter.GoogleMapWithLocationSearch
import com.divadventure.ui.screens.main.home.notifications.search.filter.calendarDayToString
import com.divadventure.viewmodel.AdventureUIEvent.AnimateItem
import com.divadventure.viewmodel.AdventureUIEvent.NavigateToNextScreen
import com.divadventure.viewmodel.AdventureUIEvent.ShowEndDateDialog
import com.divadventure.viewmodel.AdventureUIEvent.ShowSnackbar
import com.divadventure.viewmodel.AdventureUIEvent.ShowStartDateDialog
import com.divadventure.viewmodel.AdventuresIntent
import com.divadventure.viewmodel.MainUiEvent
import com.divadventure.viewmodel.MainViewModel
import com.divadventure.viewmodel.ManageAdventureViewModel
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
@Composable
fun AddOrEditAdventure(
paddingValues: PaddingValues,
mainViewModel: MainViewModel,
manageAdventureViewModel: ManageAdventureViewModel,
navigationViewModel: NavigationViewModel,
) {
val manageAdventureState = manageAdventureViewModel.state.collectAsState().value
val onSelectPrivacy: (Int) -> Unit = {
manageAdventureViewModel.sendIntent(AdventuresIntent.ApplyPrivacyType(it))
}
var adventureTitle by remember { mutableStateOf(manageAdventureState.adventureTitle) }
var aboutAdventure by remember { mutableStateOf(manageAdventureState.adventureDescription) }
var requestCondition by remember { mutableStateOf(manageAdventureState.requestCondition) }
var formIsReady = remember { mutableStateOf(false) }
var showStartDateDialog by remember { mutableStateOf(false) }
var showEndDateDialog by remember { mutableStateOf(false) }
var showDeadlineDateDialog by remember { mutableStateOf(false) }
var locationInputText by remember { mutableStateOf(manageAdventureState.locationAddress) }
val snackBarHost = remember { SnackbarHostState() }
LaunchedEffect(
adventureTitle,
aboutAdventure,
manageAdventureState.startDate,
manageAdventureState.endDate,
manageAdventureState.deadlineDate,
manageAdventureState.locationLat,
manageAdventureState.locationLng
) {
formIsReady.value = adventureTitle.isNotBlank() &&
aboutAdventure.isNotBlank() &&
manageAdventureState.startDate.isNotBlank() &&
manageAdventureState.endDate.isNotBlank() &&
manageAdventureState.deadlineDate.isNotBlank() &&
manageAdventureState.locationLat != 0.0 &&
manageAdventureState.locationLng != 0.0
}
LaunchedEffect(key1 = true) {
manageAdventureViewModel.uiEvent.collect { event ->
when (event) {
is NavigateToNextScreen -> {
navigationViewModel.navigate(event.navigationEvent)
}
AnimateItem -> {}
ShowEndDateDialog -> {
showEndDateDialog = true
}
ShowStartDateDialog -> {
showStartDateDialog = true
}
is ShowSnackbar -> {
// Show snackbar when adventure is published or on error
launch {
snackBarHost.showSnackbar(
message = event.message, duration = SnackbarDuration.Long
)
}
}
}
}
}
LaunchedEffect(key1 = true) {
mainViewModel.uiEvent.collect { event ->
when {
event == MainUiEvent.AnimateItem -> {
}
event is MainUiEvent.NavigateToNextScreen -> {
navigationViewModel.navigate(event.navigationEvent)
}
event == MainUiEvent.ShowDialog -> {
}
event is MainUiEvent.ShowDim -> {
}
event is MainUiEvent.ShowSnackbar -> {
}
}
}
}
if (showStartDateDialog) {
SelectDate(
onDismissRequest = { showStartDateDialog = false },
useClock = true,
onSelectDate = { date, clock, amPm ->
if (date != null && clock != null) {
val localDateTime = LocalDateTime.of(date.date, clock)
val formatter = DateTimeFormatter.ofPattern("MMM dd yyyy - h:mm a", Locale.ENGLISH)
val formattedDateTime = localDateTime.format(formatter)
manageAdventureViewModel.sendIntent(
AdventuresIntent.OnSetStartDate(
formattedDateTime, clock
)
)
}
showStartDateDialog = false
})
} else if (showEndDateDialog) {
SelectDate(
onDismissRequest = { showEndDateDialog = false },
useClock = true,
onSelectDate = { date, clock, amPm ->
if (date != null && clock != null) {
val localDateTime = LocalDateTime.of(date.date, clock)
val formatter = DateTimeFormatter.ofPattern("MMM dd yyyy - h:mm a", Locale.ENGLISH)
val formattedDateTime = localDateTime.format(formatter)
manageAdventureViewModel.sendIntent(
AdventuresIntent.OnSetEndDate(
formattedDateTime, clock
)
)
}
showEndDateDialog = false
})
} else if (showDeadlineDateDialog) {
SelectDate(
onDismissRequest = { showDeadlineDateDialog = false }, onSelectDate = { date, clock, amPm ->
if (date != null) {
// Deadline doesn't use time/amPm, so keep original logic for formattedDate
val formattedDate = calendarDayToString(date)
manageAdventureViewModel.sendIntent(
AdventuresIntent.OnDeadlineChange(
formattedDate
)
)
}
showDeadlineDateDialog = false
}
)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(paddingValues)
.background(Color(0xFFEFEFF4))
.padding(bottom = 56.dp),
) {
val scrollState = rememberScrollState()
// Create a nested scroll connection that prioritizes map gestures
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Let the parent Column handle vertical scrolling when not over the map
return Offset.Zero
}
}
}
fun Modifier.onPointerInteractionStartEnd(
onPointerStart: () -> Unit,
onPointerEnd: () -> Unit,
) = pointerInput(onPointerStart, onPointerEnd) {
awaitEachGesture {
awaitFirstDown(requireUnconsumed = false)
onPointerStart()
do {
val event = awaitPointerEvent()
} while (event.changes.any { it.pressed })
onPointerEnd()
}
}
val isMapMoving = remember { mutableStateOf(false) }
val parentScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Customize the behavior if needed
return Offset.Zero
}
}
}
Column(
modifier = Modifier
.verticalScroll(scrollState, enabled = !isMapMoving.value)
.nestedScroll(parentScrollConnection)
) {
BackCompose(
"Create New Adventure"
) {
}
TitleCompose("Title", true)
WhiteRoundedCornerFrame(
) {
SimpleTextField(
adventureTitle, {
adventureTitle = it
manageAdventureViewModel.sendIntent(AdventuresIntent.OnTitleChange(it))
}, descLines = 1, hint = "Adventure’s title"
)
}
WhiteRoundedCornerFrame(modifier = Modifier.padding(top = 20.dp)) {
PersonalInfoTextField(
title = "Description",
value = aboutAdventure,
onValueChange = {
aboutAdventure = it
manageAdventureViewModel.sendIntent(AdventuresIntent.OnDescriptionChange(it))
},
modifier = Modifier.padding(top = 0.dp),
minLines = 5,
mandatory = true,
hint = "About Adventure"
)
}
TitleCompose("Date & Time")
WhiteRoundedCornerFrame(modifier = Modifier.padding(top = 20.dp)) {
Column {
ItemTextClickIcon(
title = "Start Date",
value = manageAdventureState.startDate, // Assuming state name
onClick = {
showStartDateDialog = true
},
content = {
Icon(
tint = Color(0x8484846E),
painter = painterResource(id = R.drawable.ic_date_time),
contentDescription = ""
)
})
HorizontalDivider(color = Color(0xffB9B9BB), modifier = Modifier.fillMaxWidth())
ItemTextClickIcon(
title = "End Date",
value = manageAdventureState.endDate, // Assuming state name
onClick = {
showEndDateDialog = true
},
content = {
Icon(
tint = Color(0x8484846E),
painter = painterResource(id = R.drawable.ic_date_time),
contentDescription = ""
)
})
}
}
TitleCompose(
text = "Deadline \uDBC0\uDD74 ", isMandatory = false
)
WhiteRoundedCornerFrame(modifier = Modifier.padding(top = 20.dp)) {
ItemTextClickIcon(
title = "Deadline", // Changed title
value = manageAdventureState.deadlineDate.ifEmpty { null }, // Display selected date
onClick = {
showDeadlineDateDialog = true
},
content = {
Icon(
tint = Color(0x8484846E),
painter = painterResource(id = R.drawable.ic_calendar2),
contentDescription = "Set Deadline Date" // Added content description
)
})
}
TitleCompose("Location")
GoogleMapWithLocationSearch(
modifier = Modifier,
searchFieldValue = locationInputText,
onSearchFieldValueChange = { locationInputText = it },
locationsPredicted = manageAdventureState.locationsPredicted,
newLocation = manageAdventureState.newLocation,
isLocationLoading = manageAdventureState.locationIsLoading,
onLocationFieldChanged = { query ->
manageAdventureViewModel.sendIntent(AdventuresIntent.LocationFieldChanged(query))
},
onLocationSelected = { selectedLocation ->
// locationInputText is updated by GoogleMapWithLocationSearch's onLocationSelected callback
// which calls onSearchFieldValueChange internally.
manageAdventureViewModel.sendIntent(
AdventuresIntent.LocationSelected(
selectedLocation
)
)
},
onBackPressed = {
},
showHeader = false,
headerTitle = "",
isMapMoving = isMapMoving,
onMapClicked = { latLng ->
locationInputText = "${latLng.latitude}, ${latLng.longitude}"
manageAdventureViewModel.sendIntent(AdventuresIntent.OnLocationLatLngChange(latLng))
}
)
WhiteRoundedCornerFrame(modifier = Modifier.padding(top = 20.dp)) {
// Assuming manageAdventureState.adventureInterests is List<Interest> and Interest has a 'name' property
// Also assuming 'Interest' class is imported or accessible.
// If adventureInterests can be null, add a null check.
val interestsString = if (manageAdventureState.adventureInterests.isNotEmpty()) {
manageAdventureState.adventureInterests.joinToString(", ") { it.name } // Or it.toString() if it's List<String>
} else {
null // Pass null if no interests are selected, ItemTextClickIcon will handle it
}
MandatoryInterestsComposable(
title = "Interests",
value = interestsString,
onClick = {
manageAdventureViewModel.sendIntent(AdventuresIntent.GoInterests)
}
)
}
WhiteRoundedCornerFrame(modifier = Modifier.padding(top = 20.dp)) {
ItemTextClickIcon(
title = "Upload Banner Image", content = {
Image(
contentDescription = "",
painter = painterResource(id = R.drawable.ic_gallery)
)
}, isMandatory = true
)
}
TitleCompose("Privacy", true)
WhiteRoundedCornerFrame(modifier = Modifier.padding(top = 20.dp)) {
SelectionList(
defaultIndex = manageAdventureState.privacyType,
onSelectItem = onSelectPrivacy
)
}
WhiteRoundedCornerFrame(modifier = Modifier.padding(vertical = 20.dp)) {
ItemTextClickIcon(title = "Request Condition", isMandatory = true) {
Switch(
checked = requestCondition,
onCheckedChange = {
requestCondition = it
manageAdventureViewModel.sendIntent(
AdventuresIntent.OnRequestConditionChange(
it
)
)
},
modifier = Modifier,
enabled = true,
colors = SwitchDefaults.colors(
checkedTrackColor = Color(0xFF34C759),
checkedThumbColor = Color.White,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = Color(0xFFE9E9EB),
uncheckedBorderColor = Color(0xFFE9E9EB),
checkedBorderColor = Color(0xFF34C759)
),
)
}
}
ChangerButton(
modifier = Modifier
.padding(
top = 20.dp, bottom = 10.dp, start = 20.dp, end = 20.dp
)
.fillMaxWidth(),
isActive = formIsReady,
text = if (manageAdventureState.isPublishing) "Publishing..." else "Publish",
deActiveTextColor = Color.White,
deActiveButtonColor = Color(0xFFBFBFBF),
activeTextColor = Color.White,
activeButtonColor = Color(0xFF30D158), // Changed color
onClick = {
if (formIsReady.value && !manageAdventureState.isPublishing) {
manageAdventureViewModel.sendIntent(AdventuresIntent.PublishAdventure)
}
})
// Show a loading indicator when publishing
if (manageAdventureState.isPublishing) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = Color(0xFF30D158),
modifier = Modifier.padding(vertical = 8.dp)
)
}
}
ChangerButton(
modifier = Modifier
.padding(top = 0.dp, bottom = 20.dp, start = 20.dp, end = 20.dp)
.fillMaxWidth(),
isActive = formIsReady,
text = "Preview",
deActiveTextColor = Color(0xFF848484),
deActiveButtonColor = Color.White,
activeTextColor = Color(0xFF007AFF),
activeButtonColor = Color.White,
onClick = {
if (formIsReady.value && !manageAdventureState.isPublishing) {
manageAdventureViewModel.sendIntent(AdventuresIntent.PreviewAdventure)
}
})
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\add\AdventureInformation.kt
```kt
package com.divadventure.ui.screens.main.add
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.divadventure.R
import com.divadventure.domain.models.Adventure
import com.divadventure.ui.screens.main.home.ProfilesCompose
import com.divadventure.viewmodel.AdventuresState
import com.divadventure.viewmodel.MainIntent
import com.divadventure.viewmodel.MainViewModel
/**
* A composable function that displays adventure information including banner image,
* participant profiles, management button, and detailed information.
*
* @param adventure The adventure model containing data to display
* @param adventureState The state object containing adventure details
* @param durationDays The calculated duration of the adventure in days
* @param mainViewModel The MainViewModel for handling UI actions
* @param onManageParticipants Callback for when the manage participants button is clicked
*/
@Composable
fun AdventureInformation(
adventure: Adventure,
adventureState: AdventuresState,
durationDays: Int,
mainViewModel: MainViewModel? = null,
onManageParticipants: (Adventure) -> Unit = {}
) {
Column {
// Banner image
Image(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current).data(adventure.banner)
.diskCachePolicy(CachePolicy.ENABLED) // Enable disk caching
.crossfade(true).build()
),
contentScale = ContentScale.Crop,
contentDescription = "Adventure banner image",
)
// Adventure details section
// Participant profiles and management button
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
ProfilesCompose(
list = adventure.adventurers,
onClickItem = {}
)
Spacer(modifier = Modifier.weight(1f))
Button(
shape = RoundedCornerShape(4.dp),
modifier = Modifier
.height(32.dp),
onClick = {
if (mainViewModel != null) {
mainViewModel.sendIntent(MainIntent.GoOwnerParticipantMenu(adventure))
} else {
onManageParticipants(adventure)
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFB175FF)),
) {
Image(
painter = rememberAsyncImagePainter(model = R.drawable.ic_edit_manage),
contentDescription = "Manage participants",
modifier = Modifier,
contentScale = ContentScale.Fit
)
}
}
AdventureDetailsSection(
adventureState = adventureState,
durationDays = durationDays,
)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\add\AdventureOptions.kt
```kt
package com.divadventure.ui.screens.main.add
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
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.Arrangement
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
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.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.rememberAsyncImagePainter
import com.divadventure.R
import com.divadventure.domain.models.Adventure
import com.divadventure.domain.models.AdventureRequest
import com.divadventure.domain.models.Request // Assuming this is the correct Request model
import com.divadventure.ui.BackCompose
import com.divadventure.ui.SearchField
// import com.divadventure.ui.screens.main.home.notifications.RequestItem // Commented out if not used
import com.divadventure.viewmodel.AdventureUIEvent
import com.divadventure.viewmodel.AdventuresIntent
import com.divadventure.viewmodel.ManageAdventureViewModel
@Composable
fun AdventureInvitationRequests(
padding: PaddingValues, adventure: Adventure
) {
var searchField by remember { mutableStateOf("") }
// var requests = List<FriendRequestUiModel> = adventure.adventureRequest
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(padding)
) {
Scaffold(topBar = {
}, content = { innerPadding ->
// Add content here later if needed
Column {
BackCompose(
"Invitation Requests "
) { }
SearchField(
queryText = searchField
) {
searchField = it
}
FriendRequestSendCancelList(
requests = adventure.adventureRequest?.filter {
it.type.equals(
"inviteRequest", true
)
} ?: emptyList()
)
}
})
}
}
@Composable
fun FriendRequestSendCancelList(requests: List<AdventureRequest>) {
// Use remember for local UI state demo; for production, drive from ViewModel
val requestStates = remember { requests.map { mutableStateOf(it.acceptedAt != null) } }
LazyColumn {
itemsIndexed(requests) { idx, req ->
RequestSendCancelItem(
imageId = req.user.avatar,
userName = req.user.username,
date = req.createdAt,
isSent = requestStates[idx].value,
onActionClick = { currentlySent ->
// Toggle phase
requestStates[idx].value = !currentlySent
// Call your ViewModel or networking logic here
}
)
}
}
}
@Composable
fun AdventureJoinRequests(
padding: PaddingValues,
adventure: Adventure,
manageAdventureViewModel: ManageAdventureViewModel // Add this parameter
) {
val snackbarHostState = remember { SnackbarHostState() }
// Use the passed ViewModel instance
val uiState by manageAdventureViewModel.state.collectAsState()
val joinRequestsList = uiState.joinRequestsList
val isLoadingJoinRequests = uiState.isLoadingJoinRequests
// val joinRequestsError = uiState.joinRequestsError // Error is handled via Snackbar event
LaunchedEffect(adventure.id) {
manageAdventureViewModel.sendIntent(AdventuresIntent.FetchJoinRequests(adventure.id))
}
LaunchedEffect(Unit) {
manageAdventureViewModel.uiEvent.collect { event ->
when (event) {
is AdventureUIEvent.ShowSnackbar -> {
snackbarHostState.showSnackbar(
message = event.message,
actionLabel = if (event.title.equals("Error", true)) "Dismiss" else null
)
}
// Handle other UI events if necessary
else -> {}
}
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(padding)
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
BackCompose("Join Requests") {
// Handle back navigation if needed
}
if (isLoadingJoinRequests) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
if (joinRequestsList.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No join requests found.")
}
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(joinRequestsList) { request ->
JoinRequestItem(
request = request,
onAccept = {
manageAdventureViewModel.sendIntent( // Use the passed ViewModel
AdventuresIntent.AcceptJoinRequest(adventure.id, request.id)
)
},
onDecline = {
manageAdventureViewModel.sendIntent( // Use the passed ViewModel
AdventuresIntent.DeclineJoinRequest(adventure.id, request.id)
)
}
)
}
}
}
}
}
}
}
@Composable
fun JoinRequestItem(
request: Request,
onAccept: () -> Unit,
onDecline: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberAsyncImagePainter(
model = request.user.avatar ?: R.drawable.img_profile_placeholder // Corrected name
),
contentDescription = "User Avatar",
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.size(8.dp))
Column {
Text(
text = "${request.user.firstName ?: ""} ${request.user.lastName ?: ""}",
fontWeight = FontWeight.Bold
)
Text(
text = "@${request.user.username ?: "N/A"}",
fontSize = 12.sp,
color = Color.Gray
)
}
}
Row {
Button(
onClick = onAccept,
modifier = Modifier.height(36.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF30D158))
) {
Text("Accept", color = Color.White, fontSize = 12.sp)
}
Spacer(modifier = Modifier.size(8.dp))
Button(
onClick = onDecline,
modifier = Modifier.height(36.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
) {
Text("Decline", color = Color.White, fontSize = 12.sp)
}
}
}
}
/** todo
@Composable
fun AdventureParticipantManagement(padding: PaddingValues, adventure: Adventure) {
var searchField by remember { mutableStateOf("") }
Scaffold(topBar = {
}, content = { innerPadding ->
// Add content here later if needed
Column {
BackCompose(
"Participant Management "
) { }
SearchField(
queryText = searchField
) {
searchField = it
}
AdventureParticipantManagement(
padding = PaddingValues(0.dp),
participants = participants,
onRemoveParticipant = { */
/* Remove logic here *//*
}
)
}
})
}
data class ParticipantUiModel(
val id: String,
val avatar: String, // image URL
val name: String,
val username: String
)
@Composable
fun AdventureParticipantManagement(
padding: PaddingValues,
participants: List<ParticipantUiModel>,
onRemoveParticipant: (ParticipantUiModel) -> Unit
) {
var searchQuery by remember { mutableStateOf("") }
val filteredParticipants = remember(searchQuery, participants) {
if (searchQuery.isBlank()) participants
else participants.filter {
it.name.contains(searchQuery, ignoreCase = true) ||
it.username.contains(searchQuery, ignoreCase = true)
}
}
Scaffold(
topBar = { */
/* You can add your topBar here if needed *//*
}
) { innerPadding ->
Column(modifier = Modifier.padding(padding)) {
BackCompose("Participant Management") {
// Back action here
}
SearchField(
queryText = searchQuery,
onQueryChanged = { searchQuery = it }
)
LazyColumn {
items(filteredParticipants) { participant ->
ParticipantItem(
participant = participant,
onRemove = { onRemoveParticipant(participant) }
)
}
}
}
}
}
@Composable
fun ParticipantItem(
participant: ParticipantUiModel,
onRemove: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberAsyncImagePainter(model = participant.avatar),
contentDescription = "Participant Avatar",
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(50))
)
Column(modifier = Modifier.padding(start = 14.dp)) {
Text(
text = participant.name,
fontWeight = FontWeight.SemiBold,
color = Color.Black,
fontSize = 16.sp
)
Text(
text = participant.username,
color = Color(0xFF999999),
fontSize = 13.sp
)
}
Spacer(modifier = Modifier.weight(1f))
TextButton(
shape = RoundedCornerShape(6.dp),
colors = ButtonDefaults.textButtonColors(containerColor = Color(0xFF30D158)),
modifier = Modifier.height(34.dp),
onClick = onRemove
) {
Text(
text = "Remove",
color = Color.White,
fontWeight = FontWeight.SemiBold,
fontSize = 15.sp,
modifier = Modifier.padding(horizontal = 12.dp)
)
}
}
}
*/
@Composable
fun RequestSendCancelItem(
imageId: String,
userName: String,
date: String,
isSent: Boolean,
onActionClick: (Boolean) -> Unit // Pass current phase; you handle logic in the caller
) {
Row(
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 10.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberAsyncImagePainter(model = imageId),
contentDescription = "Friend Request Profile Image",
modifier = Modifier
.size(50.dp)
.clip(RoundedCornerShape(4.dp))
)
Column(modifier = Modifier.padding(start = 8.dp)) {
Text(
text = userName,
style = TextStyle(
color = Color.Black,
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
fontSize = 16.sp
)
Text(
text = buildAnnotatedString {
withStyle(
style = SpanStyle(
color = Color(0xFF565656),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Light
)
) { append(if (isSent) "Friend request sent. " else "Send a friend request. ") }
withStyle(
style = SpanStyle(
color = Color(0xFFAEAEAE),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Normal
)
) { append(date) }
}
)
}
Spacer(modifier = Modifier.weight(1f, true))
TextButton(
modifier = Modifier.height(32.5.dp),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.textButtonColors(
containerColor = if (!isSent) Color(0xFF30D158) else Color(0xFFFFDAD6)
),
onClick = { onActionClick(isSent) }
) {
Text(
modifier = Modifier
.height(16.dp)
.padding(horizontal = 16.dp),
text = if (!isSent) "Send" else "Cancel",
color = if (!isSent) Color.White else Color.Red,
style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp
)
)
}
}
}
data class FriendRequestUiModel(
val imageId: Int,
val userName: String,
val date: String,
var isSent: Boolean // true = request sent, show Cancel; false = not sent, show Send
)
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\add\AdventurePreview.kt
```kt
package com.divadventure.ui.screens.main.add
import androidx.compose.foundation.background
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
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.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.divadventure.R
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.ui.BackCompose
import com.divadventure.viewmodel.MainViewModel
import com.divadventure.viewmodel.ManageAdventureViewModel
@Composable
fun AdventurePreview(
adventureViewModel: ManageAdventureViewModel,
mainViewModel: MainViewModel,
navigationViewModel: NavigationViewModel,
padding: PaddingValues
) {
val adventureState = adventureViewModel.state.collectAsState().value
val durationDays = remember {
derivedStateOf {
calculateDurationDays(adventureState.startDate, adventureState.endDate)
}
}
Scaffold(
modifier = Modifier
.background(Color.White)
.fillMaxSize()
) { innerPadding ->
val scrollState = rememberScrollState()
// Create a nested scroll connection to handle nested scrolling properly
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource) = Offset.Zero
}
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(padding)
.nestedScroll(nestedScrollConnection)
.verticalScroll(scrollState)
) {
BackCompose(
adventureState.adventureTitle
) {
// Handle back navigation here
}
/* todo : AsyncImage(
model = adventureState.bannerUrl,
contentDescription = "Adventure banner image"
)*/
// Display adventure details using the new composable
AdventureDetailsSection(
adventureState = adventureState,
durationDays = durationDays.value
)
}
}
}
@Composable
fun InterestChip(text: String) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(
color = Color(0xFFEFEFF4),
shape = RoundedCornerShape(16.dp)
)
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Text(
text = text,
style = TextStyle(
fontSize = with(LocalDensity.current) { 9.dp.toSp() },
color = Color(0xFF848484),
fontWeight = FontWeight.Medium
)
)
}
}
/**
* A reusable row component for displaying information in the adventure preview screen.
*
* @param icon The icon to display (optional)
* @param iconDescription Content description for the icon
* @param text The text to display in the row
* @param textColor The color of the text
* @param textSize The size of the text in dp
* @param showIcon Whether to show the icon or not
* @param modifier Additional modifier for customization
*/
@Composable
fun AdventureInfoRow(
icon: ImageVector? = null,
iconDescription: String = "",
text: String,
textColor: Color = Color(0xff2B323A),
textSize: androidx.compose.ui.unit.Dp = 9.dp,
showIcon: Boolean = true,
modifier: Modifier = Modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = modifier.padding(horizontal = 20.dp, vertical = 10.dp)
) {
if (showIcon && icon != null) {
Icon(
imageVector = icon,
contentDescription = iconDescription,
tint = Color(0xFF30D158),
modifier = Modifier.padding(end = 8.dp)
)
}
Text(
text = text,
style = TextStyle(
color = textColor,
fontSize = with(LocalDensity.current) { textSize.toSp() },
fontWeight = FontWeight.W500,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
}
}
/**
* A composable that displays the details section of an adventure including:
* - Start date
* - Location information
* - Duration
* - Description
* - Interests list
* - Location map
*
* @param adventureState The state containing all adventure details
* @param durationDays The calculated duration of the adventure in days
* @param modifier Additional modifier for customization
*/
@Composable
fun AdventureDetailsSection(
adventureState: com.divadventure.viewmodel.AdventuresState,
durationDays: Int,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
// Start date information
AdventureInfoRow(
icon = ImageVector.vectorResource(R.drawable.ic_calendar),
iconDescription = "Calendar Icon",
text = adventureState.startDate
)
// Location information
AdventureInfoRow(
icon = ImageVector.vectorResource(id = R.drawable.ic_send),
iconDescription = "Location Icon",
text = if (adventureState.locationAddress.isNullOrBlank()) {
adventureState.locationLat.toString() + " , " + adventureState.locationLng.toString()
} else adventureState.locationAddress
)
// Display duration in days
AdventureInfoRow(
text = when {
durationDays > 0 -> "in ${durationDays} day${if (durationDays > 1) "s" else ""}"
durationDays == 0 -> "Same day adventure"
else -> "Invalid duration"
},
textColor = Color(0xff5856D6),
textSize = 11.dp,
showIcon = false
)
// Adventure description
Text(
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
text = adventureState.adventureDescription,
style = TextStyle(
fontSize = with(LocalDensity.current) { 9.dp.toSp() },
color = Color.Black,
fontWeight = FontWeight.W400,
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
// Interests Grid
if (adventureState.adventureInterests.isNotEmpty()) {
Text(
text = "Interests",
style = TextStyle(
fontSize = with(LocalDensity.current) { 12.dp.toSp() },
color = Color.Black,
fontWeight = FontWeight.W600
),
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp)
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
) {
androidx.compose.foundation.layout.FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
adventureState.adventureInterests.forEach { interest ->
InterestChip(interest.name)
}
}
}
}
// Location map
if (adventureState.locationLat != 0.0 && adventureState.locationLng != 0.0) {
Text(
text = "Location",
style = TextStyle(
fontSize = with(LocalDensity.current) { 12.dp.toSp() },
color = Color.Black,
fontWeight = FontWeight.W600
),
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp)
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 8.dp)
) {
com.divadventure.ui.components.StaticMap(
latitude = adventureState.locationLat,
longitude = adventureState.locationLng
)
}
}
}
}
/**
* Calculates the duration in days between two date strings.
*
* @param startDate The start date as a string.
* @param endDate The end date as a string.
* @param dateTimeFormat The date time format to parse the input dates.
* @param locale The locale for date formatting (defaults to US).
* @return The duration in days, or 0 if there's an error or empty date fields.
*/
fun calculateDurationDays(
startDate: String,
endDate: String,
dateTimeFormat: String = "MMM dd yyyy - h:mm a",
locale: java.util.Locale = java.util.Locale.US
): Int {
return try {
if (startDate.isNotEmpty() && endDate.isNotEmpty()) {
val formatter = java.time.format.DateTimeFormatter.ofPattern(dateTimeFormat, locale)
val startLocalDate = java.time.LocalDateTime.parse(startDate, formatter).toLocalDate()
val endLocalDate = java.time.LocalDateTime.parse(endDate, formatter).toLocalDate()
java.time.temporal.ChronoUnit.DAYS.between(startLocalDate, endLocalDate).toInt()
} else {
0
}
} catch (e: Exception) {
println("Date parsing error: ${e.message}")
0 // Return 0 for any errors while parsing
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\add\manage\Manage.kt
```kt
package com.divadventure.ui.screens.main.add.manage
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.divadventure.R
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.domain.models.Adventure
import com.divadventure.ui.GrayTitle
import com.divadventure.ui.screens.main.add.AdventureInformation
import com.divadventure.ui.screens.main.add.calculateDurationDays
import com.divadventure.ui.screens.main.home.AdventureItemSelection
import com.divadventure.viewmodel.AdventuresIntent
import com.divadventure.viewmodel.MainUiEvent
import com.divadventure.viewmodel.MainViewModel
import com.divadventure.viewmodel.ManageAdventureViewModel
import kotlinx.coroutines.launch
@Composable
fun ManageAdventure(
padding: PaddingValues,
navigationViewModel: NavigationViewModel, mainViewModel: MainViewModel,
adventureViewModel: ManageAdventureViewModel,
adventure: Adventure
) {
val snackBarHost = remember { SnackbarHostState() }
val navController = rememberNavController()
LaunchedEffect(key1 = true) {
mainViewModel.uiEvent.collect { event ->
when (event) {
MainUiEvent.AnimateItem -> {
}
is MainUiEvent.NavigateToNextScreen -> {
navigationViewModel.navigate(event.navigationEvent)
}
MainUiEvent.ShowDialog -> {}
is MainUiEvent.ShowDim -> {}
is MainUiEvent.ShowSnackbar -> {
launch {
snackBarHost.showSnackbar(
message = event.message, duration = SnackbarDuration.Long
)
}
}
is MainUiEvent.AdventureAction -> {}
}
}
}
var adventureTitle: String = ""
val adventureParts = listOf<String>("Information", "Task", "Gallery", "Comments", "Expense")
var selectedItem by remember { mutableStateOf("Information") }
// Update selectedItem when navigation changes
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
LaunchedEffect(currentRoute) {
currentRoute?.let {
if (adventureParts.contains(it)) {
selectedItem = it
}
}
}
adventureViewModel.sendIntent(AdventuresIntent.GetAdventure(adventure))
var adventurers = adventure!!.adventurers
val adventureState = adventureViewModel.state.collectAsState().value
val durationDays = remember {
derivedStateOf {
calculateDurationDays(adventureState.startDate, adventureState.endDate)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(padding)
) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
GrayTitle(adventure.title)
AdventureItemSelection(
items = adventureParts,
selectedItemId = selectedItem,
onItemSelected = {
selectedItem = it
navController.navigate(it) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
NavHost(
navController = navController,
startDestination = adventureParts[0]
) {
composable(adventureParts[0]) {
AdventureInformation(
adventure = adventure,
adventureState = adventureState,
durationDays = durationDays.value,
mainViewModel = mainViewModel
)
}
composable(adventureParts[1]) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
}
}
composable(adventureParts[2]) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
}
}
composable(adventureParts[3]) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
}
}
composable(adventureParts[4]) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
}
}
}
}
}
}
@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\screens\main\add\ManageAdventure.kt
```kt
package com.divadventure.ui.screens.main.add
import androidx.compose.runtime.Composable
@Composable
fun OwnerManageAdventure() {
}
@Composable
fun ParticipantManageAdventure() {
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\add\ManageAdventureOwnerModerator.kt
```kt
package com.divadventure.ui.screens.main.add
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxSize
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.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
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.data.navigation.NavigationViewModel
import com.divadventure.data.navigation.Screen
import com.divadventure.domain.models.Adventure
import com.divadventure.ui.BackCompose
import com.divadventure.viewmodel.AdventureUIEvent
import com.divadventure.viewmodel.AdventuresIntent
import com.divadventure.viewmodel.MainViewModel
import com.divadventure.viewmodel.ManageAdventureViewModel
@Composable
fun OwnerParticipantMenu(
paddingValues: PaddingValues,
adventure: Adventure,
mainViewModel: MainViewModel,
manageAdventureViewModel: ManageAdventureViewModel,
navigationViewModel: NavigationViewModel
) {
LaunchedEffect(key1 = true) {
manageAdventureViewModel.uiEvent.collect { event ->
when (event) {
is AdventureUIEvent.NavigateToNextScreen -> {
navigationViewModel.navigate(event.navigationEvent)
}
AdventureUIEvent.ShowEndDateDialog -> {
}
is AdventureUIEvent.ShowSnackbar -> {}
AdventureUIEvent.ShowStartDateDialog -> {}
AdventureUIEvent.AnimateItem -> {}
}
}
}
val manageAdventureState = manageAdventureViewModel.state.collectAsState().value
val selectedOption = if (adventure.adventureRequest.isNullOrEmpty() == false) {
1 // let's assume index 1 (Join Requests) is selected for demo
} else {
-1
}
var goMenuItem: (screen: Screen) -> Unit = { screen ->
when {
screen is Screen.AdventureJoinRequests -> {
manageAdventureViewModel.sendIntent(AdventuresIntent.GoJoinRequests(adventure))
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(paddingValues)
) {
Scaffold(
containerColor = Color(0xFFEFEFF4), // light background
content = { innerPadding ->
Column(
modifier = Modifier
) {
BackCompose(text = "Manage ${manageAdventureState.adventureTitle}") {
// Handle back press
}
Spacer(modifier = Modifier.height(20.dp))
ManageOptionList(selectedOption = selectedOption, goMenuItem)
}
}
)
}
}
@Composable
fun ManageOptionList(selectedOption: Int, goMenuItem: (Screen) -> Unit) {
val options = listOf(
ManageOptionData(
icon = ImageVector.vectorResource(R.drawable.ic_gear), // replace with your vector if needed
title = "Edit Adventure",
subtitle = "Update Adventure Details"
),
ManageOptionData(
icon = ImageVector.vectorResource(R.drawable.ic_person_svg), // replace with actual drawable name
title = "Join Requests",
subtitle = "Participant's Join Request"
),
ManageOptionData(
icon = ImageVector.vectorResource(R.drawable.ic_mail),
title = "Invitation Requests",
subtitle = "Send Invitation to Participants"
),
ManageOptionData(
icon = ImageVector.vectorResource(R.drawable.ic_moderator), // Changed from AdminPanelSettings which doesn't exist in the imports
title = "Moderator Management",
subtitle = "Assign/Revoke Moderator Role"
),
ManageOptionData(
icon = ImageVector.vectorResource(R.drawable.ic_participant_management), // Changed from GroupRemove which doesn't exist in the imports
title = "Participant Management",
subtitle = "Remove Participants"
)
)
options.forEachIndexed { index, data ->
ManageOption(
data = data,
isSelected = index == selectedOption,
onClick = {
when (index) {
0 -> {
// Navigate to Edit Adventure Screen
}
1 -> {
// Navigate to Join Requests Screen
goMenuItem(Screen.AdventureJoinRequests)
}
2 -> {
// Navigate to Invitation Requests Screen
}
3 -> {
// Navigate to Moderator Management Screen
}
4 -> {
// Navigate to Participant Management Screen
}
}
}
)
Spacer(modifier = Modifier.height(4.dp))
}
}
data class ManageOptionData(
val icon: ImageVector,
val title: String,
val subtitle: String
)
@Composable
fun ManageOption(
data: ManageOptionData,
isSelected: Boolean = false,
onClick: () -> Unit = {}
) {
val backgroundColor = if (isSelected) Color(0xFFDDEAFF) else Color.White
val iconColor = if (isSelected) Color(0xFF007AFF) else Color.Black
val titleColor = if (isSelected) Color(0xFF007AFF) else Color.Black
val subtitleColor = if (isSelected) Color(0xFF007AFF).copy(alpha = 0.8f) else Color.Gray
Card(
colors = CardDefaults.cardColors(
containerColor = Color.White
),
shape = RoundedCornerShape(10.dp),
modifier = Modifier
.padding(horizontal = 12.dp)
.fillMaxWidth()
.background(backgroundColor, RoundedCornerShape(10.dp))
.clickable { onClick() },
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 14.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = data.icon,
contentDescription = null,
tint = iconColor,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = data.title,
color = titleColor,
style = MaterialTheme.typography.bodyLarge.copy(
fontFamily = FontFamily(Font(R.font.sf_pro))
),
fontWeight = FontWeight.SemiBold
)
Text(
text = data.subtitle,
color = subtitleColor,
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\home\Home.kt
```kt
package com.divadventure.ui.screens.main.home
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
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.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.layout.positionOnScreen
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.navigation.NavHostController
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.divadventure.R
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.domain.models.Adventure
import com.divadventure.domain.models.AdventureType
import com.divadventure.domain.models.Adventurer
import com.divadventure.ui.AdventureCalendarItem
import com.divadventure.ui.CalendarGuideCompose
import com.divadventure.ui.SlidingDualToggleButton
import com.divadventure.ui.WeekdayRow
import com.divadventure.ui.calendarCirclesSize
import com.divadventure.ui.screens.Loader
import com.divadventure.ui.screens.main.home.notifications.BottomSheetContent
import com.divadventure.ui.screens.main.home.notifications.GeneralBottomSheet
import com.divadventure.ui.screens.main.home.notifications.search.Search
import com.divadventure.util.Helper.Companion.convertDateString
import com.divadventure.util.Helper.Companion.formatDateTime
import com.divadventure.viewmodel.HomeIntent
import com.divadventure.viewmodel.HomeUiEvent
import com.divadventure.viewmodel.HomeViewModel
import com.divadventure.viewmodel.MainIntent
import com.divadventure.viewmodel.MainUiEvent
import com.divadventure.viewmodel.MainViewModel
import com.divadventure.viewmodel.ProfileIntent
import com.divadventure.viewmodel.ProfileViewModel
import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.compose.ContentHeightMode
import com.kizitonwose.calendar.compose.HorizontalCalendar
import com.kizitonwose.calendar.compose.rememberCalendarState
import com.kizitonwose.calendar.core.atStartOfMonth
import com.kizitonwose.calendar.core.yearMonth
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import kotlin.math.min
// Global constants for UI elements
val groupItems = listOf("All", "Created", "Invited", "Joined", "Friends")
val showTypes = listOf("List", "Calendar")
/**
* Composable function that displays the header for the application.
* The header includes the application logo, title, notification icon, and search icon.
*
* @param mainViewModel The instance of [MainViewModel] used to handle user interactions,
* such as navigating to the notifications screen.
* @param homeViewModel The instance of [HomeViewModel] used to handle toggle logic for
* displaying the search bar in the UI.
*/
@Composable
fun Header(
mainViewModel: MainViewModel, homeViewModel: HomeViewModel
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
) {
// Logo for the application, can also have click actions for navigation
IconButton(
modifier = Modifier.padding(start = 10.dp), onClick = {
// Handle logo click if needed
}) {
Icon(
painter = painterResource(id = R.drawable.logo_green),
contentDescription = "app_logo",
tint = Color.Unspecified
)
}
// Text to display the application title
Text(
modifier = Modifier
.padding(end = 10.dp)
.align(Alignment.CenterVertically),
text = "DivAdventure",
style = TextStyle(
fontSize = with(LocalDensity.current) { 16.dp.toSp() },
color = Color.Black,
textAlign = TextAlign.Center,
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
// Spacer to push content (icons) to the end of the row
Spacer(
modifier = Modifier.weight(1f, true)
)
// Notifications icon button and click action to navigate or show notifications
IconButton(
modifier = Modifier.padding(horizontal = 0.dp), onClick = {
mainViewModel.sendIntent(MainIntent.GotoNotifications)
}) {
Icon(
tint = Color(0xFF30D158),
painter = painterResource(id = R.drawable.ic_ring),
contentDescription = "notifications"
)
}
// Search icon button and click action to toggle the search bar in the home UI
IconButton(
modifier = Modifier.padding(horizontal = 0.dp), onClick = {
homeViewModel.sendIntent(HomeIntent.SwitchShowSearchbar)
}) {
Icon(
painter = painterResource(id = R.drawable.ic_search),
contentDescription = "placeholder image",
tint = Color.Unspecified
)
}
}
}
/**
* Composable function that represents the Home screen of the application.
* This function handles UI-related events, manages navigation, and manages the structure of the UI components.
*
* @param mainViewModel The instance of [MainViewModel] responsible for handling business logic,
* state management, and UI events.
* @param homeViewModel The instance of [HomeViewModel] handling home screen specific logic.
* @param navigationViewModel The instance of [NavigationViewModel] used to manage
* navigation-related actions and logic.
* @param navController The [NavHostController] instance used to enable navigation
* between different composables.
* @param padding The [PaddingValues] providing padding for the entire Home screen layout.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Home(
mainViewModel: MainViewModel,
homeViewModel: HomeViewModel,
navigationViewModel: NavigationViewModel,
navController: NavHostController,
padding: PaddingValues
) {
// Collect the current state of the home screen
val homeState by homeViewModel.state.collectAsState()
// Handle UI events and Snackbar
LaunchedEffect(Unit) {
mainViewModel.uiEvent.collect { event ->
when (event) {
is MainUiEvent.ShowSnackbar -> { /* Handle Snackbar */ }
MainUiEvent.ShowDialog -> { /* Handle Dialog */ }
is MainUiEvent.NavigateToNextScreen -> {
navigationViewModel.navigate(event.navigationEvent)
}
// Add this new case:
is MainUiEvent.AdventureAction -> {
}
// MainUiEvent.AnimateItem, is MainUiEvent.ShowDim, is MainUiEvent.ShowSnackbar -> { /* Handle other events */ } // Original line, ensure ShowSnackbar is handled if it was separate
MainUiEvent.AnimateItem -> {
}
is MainUiEvent.ShowDim -> {
}
}
}
}
LaunchedEffect(key1 = true) {
homeViewModel.uiEvent.collect { event ->
when (event) {
is HomeUiEvent.ShowDim -> {}
is HomeUiEvent.ShowSnackbar -> {}
HomeUiEvent.AnimateItem -> {}
is HomeUiEvent.NavigateToNextScreen -> {
navigationViewModel.navigate(event.navigationEvent)
}
HomeUiEvent.ShowStartDateDialog -> {
}
HomeUiEvent.ShowEndDateDialog -> {
}
else -> {
// do nothing
}
}
}
}
Box(modifier = Modifier.padding(padding)) {
Scaffold(
containerColor = Color(0xFFefeff4), topBar = {
AnimatedVisibilityContent(
isSearchBarVisible = homeState.isSearchBarVisible,
mainViewModel = mainViewModel,
homeViewModel = homeViewModel,
padding = padding
)
}) { innerPadding ->
var selectedShowType by remember { mutableStateOf(showTypes.first()) }
LaunchedEffect(selectedShowType) {
homeViewModel.sendIntent(
HomeIntent.SwitchCalendarColumn(
showTypes.indexOf(
selectedShowType
)
)
)
}
var getCurrentDate: (YearMonth?) -> Unit = {
if (selectedShowType == showTypes[1] && it != null) {
homeViewModel.sendIntent(
HomeIntent.LoadCalendarAdventures(
startDate = it.atStartOfMonth().toString(),
endDate = it.atEndOfMonth().toString()
)
)
}
}
BinarySwitcher(
modifier = Modifier.padding(
bottom = 56.dp, top = innerPadding.calculateTopPadding()
), options = showTypes, selectedOption = selectedShowType, onSelectOption = {
when (it) {
showTypes[0] -> {
homeViewModel.sendIntent(HomeIntent.LoadAdventuresData(query = null))
}
}
selectedShowType = it
}, calendarContent = {
SelectableCalendar(
padding = PaddingValues(),
true,
getCurrentDate,
when (homeState.isSearchBarVisible) {
true -> homeState.searchAdventuresList
false -> homeState.mainAdventuresList
},
homeState.isLoading.calendarIsLoading
)
}, listContent = {
var onClickItem: ((Adventurer) -> Unit) = { adventurer ->
mainViewModel.sendIntent(MainIntent.GotoProfile(adventurer.userId))
}
AdventuresList(viewModel = homeViewModel, mainViewModel, onClickItem, true)
// Use rememberSaveable to persist the flag across recompositions
// To fetch adventures in first run
val initialLoadPerformed = rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (!initialLoadPerformed.value) {
homeViewModel.sendIntent(HomeIntent.LoadAdventuresData(query = null))
initialLoadPerformed.value = true
}
}
})
}
// It's often better to place the BottomSheet outside the Scaffold's content lambda
// if it's meant to overlay everything within this Box scope.
if (homeState.showGoingBottomSheet && homeState.selectedAdventureForBottomSheet != null) {
GeneralBottomSheet(
showBottomSheet = true, // Controlled by homeState.showGoingBottomSheet
onDismissRequest = {
homeViewModel.sendIntent(HomeIntent.DismissBottomSheet)
},
content = {
BottomSheetContent(
options = listOf("Yes", "No", "Maybe"),
onOptionClick = { option ->
homeViewModel.sendIntent(
HomeIntent.HandleBottomSheetAction(
action = option,
adventureId = homeState.selectedAdventureForBottomSheet?.id
)
)
},
onCancelClick = { // "Cancel" button in BottomSheetContent usually calls onDismissRequest
homeViewModel.sendIntent(HomeIntent.DismissBottomSheet)
}
// Optional: Provide custom styles if needed for "Yes", "No", "Maybe"
)
}
)
}
}
}
/**
* Composable function to manage the visibility and animations of two different UI states.
* It toggles between a header view with options and a search bar view based on the value of `isSearchBarVisible`.
*
* @param isSearchBarVisible A nullable Boolean indicating which UI state is visible.
* `true` shows the search bar, while `false` shows the header and options.
* @param mainViewModel The instance of [MainViewModel] managing the main app logic and state.
* @param homeViewModel The instance of [HomeViewModel] managing the home screen logic and state.
* @param padding The [PaddingValues] passed to handle padding adjustments for the content.
*/
/**
* Composable function to manage the visibility and animations of two different UI states.
* It toggles between a header view with options and a search bar view.
*
* @param isSearchBarVisible A nullable Boolean indicating which UI state is visible
* @param mainViewModel The main view model for app logic and state
* @param homeViewModel The view model for home screen logic and state
* @param padding Padding values for content layout adjustments
*/
@Composable
private fun AnimatedVisibilityContent(
isSearchBarVisible: Boolean?,
mainViewModel: MainViewModel,
homeViewModel: HomeViewModel,
padding: PaddingValues
) {
Box {
// Header and list selection view
AnimatedVisibility(
visible = isSearchBarVisible == false,
enter = slideInHorizontally(
initialOffsetX = { -it },
animationSpec = tween(durationMillis = 400)
),
exit = slideOutHorizontally(
targetOffsetX = { -it },
animationSpec = tween(durationMillis = 400)
)
) {
Column {
// App header with logo, title, and action icons
Header(mainViewModel, homeViewModel)
// Adventure filter options
var selectedItem by remember { mutableStateOf("All") }
AdventureItemSelection(
items = groupItems,
selectedItemId = selectedItem,
onItemSelected = {
selectedItem = it
homeViewModel.sendIntent(HomeIntent.SelectGroup(it))
}
)
}
}
// Search bar view
AnimatedVisibility(
visible = isSearchBarVisible == true,
enter = slideInHorizontally(
initialOffsetX = { it },
animationSpec = tween(durationMillis = 400)
),
exit = slideOutHorizontally(
targetOffsetX = { it },
animationSpec = tween(durationMillis = 400)
)
) {
Search(mainViewModel, homeViewModel, padding)
}
}
}
/**
* Composable function that provides a two-option toggle switch
* and displays corresponding content based on the selected option.
*
* @param modifier Modifier to be applied to the composable layout
* @param options A list of options to toggle between
* @param selectedOption The currently selected option
* @param onSelectOption Callback triggered when the selected option changes
* @param calendarContent A composable lambda to display calendar-related content
* @param listContent A composable lambda to display list-related content
*/
@Composable
fun BinarySwitcher(
modifier: Modifier,
options: List<String>,
selectedOption: String,
onSelectOption: (String) -> Unit,
calendarContent: @Composable () -> Unit,
listContent: @Composable () -> Unit
) {
Column(modifier = modifier) {
// Toggle button for switching between views
SlidingDualToggleButton(
padding = 20.dp,
options = options,
onToggle = { index ->
onSelectOption(options[index])
}
)
// Show content based on selected option
AnimatedVisibility(visible = selectedOption == options[0]) {
listContent()
}
AnimatedVisibility(visible = selectedOption == options[1]) {
calendarContent()
}
}
}
/**
* Composable function to display a row of profile images with overlapping arrangement.
* If the list contains more than five items, displays a "+N" counter to indicate additional profiles.
*
* @param list A list of adventurers whose profiles should be displayed
* @param onClickItem Callback when an adventurer profile is clicked
* @param overlap The horizontal overlap between consecutive profile images. Default is 10.dp
*/
@Composable
fun ProfilesCompose(
list: List<Adventurer>,
onClickItem: (Adventurer) -> Unit,
overlap: Dp = 10.dp
) {
LazyRow {
// Limit the number of displayed items to a maximum of 5
val displayedItems = min(list.size, 5)
// Display each profile item in the list up to the limit
itemsIndexed(list.take(displayedItems)) { index, item ->
ProfileItem(
item = item,
overlap = overlap,
index = index,
onClickItem = onClickItem
)
}
// Display a "+N" counter if there are more than 5 items
if (list.size > 5) {
item {
ExtraProfilesCounter(
count = list.size - 5,
overlap = overlap,
displayedItems = displayedItems
)
}
}
}
}
/**
* Composable to display a single profile image with click functionality.
*
* @param item The adventurer whose profile to display
* @param overlap The horizontal overlap to apply
* @param index The position index used to calculate the offset
* @param onClickItem Callback when the profile is clicked
*/
@Composable
fun ProfileItem(
item: Adventurer,
overlap: Dp,
index: Int,
onClickItem: (Adventurer) -> Unit
) {
Box(
modifier = Modifier
.offset(x = -(index * overlap.value).dp)
.clickable { onClickItem(item) }
) {
// Show the profile image or placeholder as appropriate
if (item.avatar.isNullOrEmpty()) {
ProfilePlaceholder()
} else {
ProfileImage(resource = item.avatar)
}
}
}
/**
* Placeholder composable for cases where the profile image is not available.
*/
@Composable
fun ProfilePlaceholder() {
Box(
modifier = Modifier
.size(40.dp)
.background(color = Color.Gray, shape = CircleShape)
)
}
/**
* Composable for rendering profile images.
*
* @param resource The URL of the profile image to display
*/
@Composable
fun ProfileImage(resource: String) {
Image(
painter = rememberAsyncImagePainter(model = resource),
contentDescription = "Profile Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(color = Color.Gray, shape = CircleShape)
)
}
/**
* Composable for the "+N" counter that shows how many extra profiles are not displayed.
*
* @param count The number of additional profiles not shown
* @param overlap The horizontal overlap to apply
* @param displayedItems The number of items already displayed
*/
@Composable
fun ExtraProfilesCounter(count: Int, overlap: Dp, displayedItems: Int) {
Box(modifier = Modifier.offset(x = -(displayedItems * overlap.value).dp)) {
Surface(
shape = CircleShape,
color = Color.Gray.copy(alpha = 0.5f),
modifier = Modifier.size(40.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = "+$count",
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}
}
/**
* Displays an adventure item within a card layout containing an image, title, event details,
* participant profiles, and a join button.
*
* @param item A data object containing the resources necessary to populate the adventure item,
* specifically including the image resource ID to display.
*/
/**
* Displays an adventure item within a card layout containing an image, title, event details,
* participant profiles, and a join button.
*
* @param index The position of this item in a list
* @param item The adventure data to display
* @param onClickAdventurerItem Callback when an adventurer profile is clicked
* @param onSelectAdventure Callback when the action button is clicked
*/
@Composable
fun AdventureCard(
index: Int,
item: Adventure,
onClickAdventurerItem: (Adventurer) -> Unit,
onSelectAdventure: (Adventure) -> Unit
) {
// Get the appropriate color and text for this adventure type
val (buttonColor, buttonText) = when (item.adventureType) {
AdventureType.Manage -> Color(0xFF3F51B5) to "Manage" // Indigo
AdventureType.Join -> Color(0xFF4CAF50) to "Join" // Green
AdventureType.Going -> Color(0xFFFF9800) to "Going" // Orange
AdventureType.Pending -> Color(0xFFFF5722) to "Pending" // Deep Orange
AdventureType.Leave -> Color(0xFFF44336) to "Leave" // Red
null -> Color.Gray to "Unknown" // Fallback
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 10.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Image(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
alignment = Alignment.Center,
contentScale = ContentScale.Crop,
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current).data(item.banner)
.diskCachePolicy(CachePolicy.ENABLED) // Enable disk caching
.crossfade(true).build()
), // Using Coil for image loading
contentDescription = "Placeholder image description"
)
Text(
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
text = item.title,
style = TextStyle(
fontSize = with(LocalDensity.current) { 16.dp.toSp() },
color = Color(0xFF000000),
fontFamily = FontFamily(
Font(R.font.sf_pro)
),
fontWeight = FontWeight.SemiBold
)
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp)
) {
// Calendar section
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_calendar),
contentDescription = "Calendar Icon",
tint = Color(0xFF30D158)
)
Text(
text = item.startsAt.formatDateTime(),
modifier = Modifier
.weight(1f)
.padding(start = 4.dp, end = 8.dp),
style = TextStyle(color = Color(0xff2B323A)),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Location section
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_send),
contentDescription = "Location Icon",
tint = Color(0xFF30D158)
)
Text(
text = item.description,
modifier = Modifier
.weight(1f)
.padding(start = 4.dp, end = 8.dp),
style = TextStyle(color = Color(0xff2B323A)),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Row(
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp)
) {
// نشان دهنده ی لیست افراد حاضر در یک رویداد است
ProfilesCompose(
item.adventurers, onClickAdventurerItem
)
Spacer(modifier = Modifier.weight(1f))
// با زدن این دکمه می توان به رویداد ملحق شد
TextButton(
shape = RoundedCornerShape(4.dp), // Example: 8.dp rounded corners
colors = ButtonDefaults.buttonColors(
containerColor = buttonColor
), onClick = {
onSelectAdventure(item)
}) {
Text(
modifier = Modifier.padding(horizontal = 10.dp),
text = buttonText,
maxLines = 1,
color = Color.White,
style = TextStyle(
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
fontWeight = FontWeight.Bold
)
)
}
}
}
}
}
/**
* A composable function that represents a single selectable item in an adventure item list.
*
* @param title The text displayed for the item.
* @param modifier Modifier to be applied to the composable.
* @param isSelected Boolean indicating whether the item is currently selected.
* @param onSelect Callback that will be triggered when the item is selected.
*/
/**
* A composable function that represents a single selectable item in an adventure item list.
*
* @param title The text displayed for the item
* @param modifier Modifier to be applied to the composable
* @param isSelected Boolean indicating whether the item is currently selected
* @param onSelect Callback that will be triggered when the item is selected
*/
@Composable
fun SingleAdventureItem(
title: String,
modifier: Modifier,
isSelected: Boolean,
onSelect: () -> Unit
) {
Box(modifier = Modifier.clickable { onSelect() }) {
Text(
text = title,
modifier = modifier
.background(if (isSelected) Color(0xFF30D158) else Color.White)
.padding(10.dp),
style = TextStyle(
fontSize = with(LocalDensity.current) { 12.dp.toSp() },
color = if (isSelected) Color.White else Color(0xFF848484),
textAlign = TextAlign.Center,
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
}
// برای سوییچ بین انواع رویدادها است، همه رویدادها، رویدادهای ساخته شده توسط ما، رویدادهایی که دعوت شده ایم، رویدادهایی که ملحق شده ایم و رویدادهایی که مربوط به دوستان ما است
/**
* A composable function that allows selection from a list of adventure-related items.
* Displays selectable items horizontally with an indicator for the selected item.
*
* @param modifier The modifier to be applied to the composable, allowing customization of its appearance and layout.
* @param items A list of strings representing the adventure items to be displayed.
* @param selectedItemId The identifier for the currently selected item.
* @param onItemSelected A callback function triggered when an item is selected, providing the selected item's identifier.
*/
/**
* A composable function that allows selection from a list of adventure-related items.
* Displays selectable items horizontally with an indicator for the selected item.
*
* @param modifier The modifier to be applied to the composable
* @param items A list of strings representing the adventure items to be displayed
* @param selectedItemId The identifier for the currently selected item
* @param onItemSelected A callback function triggered when an item is selected
*/
@Composable
fun AdventureItemSelection(
modifier: Modifier = Modifier,
items: List<String>,
selectedItemId: String,
onItemSelected: (String) -> Unit
) {
Box(
modifier = Modifier
.background(Color.White)
.fillMaxWidth()
) {
Row(
modifier = modifier
.padding(horizontal = 20.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
items.forEach { item ->
SingleAdventureItem(
title = item,
modifier = Modifier,
isSelected = item == selectedItemId,
onSelect = { onItemSelected(item) }
)
// No need for a spacer with 0.dp padding
}
}
}
}
// راهنمای محتویات نشان داده شده در تقویم است
/**
* A composable function that displays a guide item consisting of a styled circular item
* with text and an accompanying label.
*
* @param firstItemText The text to be displayed inside the first circular element.
* @param firstItemTextColor The color of the text in the first circular element.
* @param firstItemColor The background color of the first circular element.
* @param secondText The text to be displayed next to the first circular element.
*/
@Composable
fun GuideItem(
firstItemText: String,
firstItemTextColor: Color,
firstItemColor: Color,
secondText: String,
count: Int = 0
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp) // Add horizontal offset of 8.dp between items,
, verticalAlignment = Alignment.CenterVertically
) {
var yOffset by remember { mutableFloatStateOf(0f) }
val density = LocalDensity.current // Get the density object
AdventureCalendarItem(
modifier = Modifier
.align(Alignment.CenterVertically)
.offset(y = with(density) { (yOffset - (calendarCirclesSize.toPx() / 2)).toDp() }),
textColor = firstItemTextColor,
text = firstItemText,
backgroundColor = firstItemColor,
count
)
Box(
modifier = Modifier
.align(alignment = Alignment.Top)
.padding()
.onGloballyPositioned { layoutCoordinates ->
yOffset =
layoutCoordinates.positionInParent().y + (layoutCoordinates.size.height / 2)
}, contentAlignment = Alignment.TopCenter
) {
Text(
modifier = Modifier.wrapContentHeight(),
text = secondText,
maxLines = 1,
style = TextStyle(
fontSize = with(LocalDensity.current) { 16.dp.toSp() },
color = Color.Black,
fontFamily = FontFamily(
Font(R.font.sf_pro)
)
)
)
}
}
}
@Composable
fun AdventuresList(
viewModel: ViewModel,
mainViewModel: MainViewModel,
onClickItem: ((Adventurer) -> Unit),
isScrollable: Boolean
) {
when (viewModel) {
is HomeViewModel -> {
val state = viewModel.state.collectAsState().value
// adds adventures types to them
viewModel.sendIntent(HomeIntent.ApplyAdevntureType)
val loadMore = {
viewModel.sendIntent(HomeIntent.LoadMoreAdventuresData)
}
val isLoadingMore = state.isLoading.isLoadingMore
AdventuresContent(
isScrollable = isScrollable, { adventure ->
viewModel.sendIntent(HomeIntent.HandleAdventureClick(adventure))
// mainViewModel.sendIntent(MainIntent.OnSelectAdventure(adventure))
},
loadMore,
isLoadingMore,
adventures = when (viewModel.state.collectAsState().value.isSearchBarVisible) {
true -> {
viewModel.state.collectAsState().value.searchAdventuresList
}
false -> {
viewModel.state.collectAsState().value.mainAdventuresList
}
},
isLoading = state.isLoading.adventuresLoading,
onClickItem = onClickItem
)
}
is ProfileViewModel -> {
val state = viewModel.state.collectAsState().value
// adds adventures types to them
viewModel.sendIntent(ProfileIntent.ApplyAdevntureType)
val isLoadingMore = state.isLoading.isLoadingMore
val loadMore = {}
AdventuresContent(
isScrollable, { adventure ->
viewModel.sendIntent(ProfileIntent.HandleAdventureClick(adventure))
// mainViewModel.sendIntent(MainIntent.OnSelectAdventure(adventure))
},
loadMore,
isLoadingMore,
adventures = state.adventuresList,
isLoading = state.isLoading.adventuresLoading,
onClickItem = onClickItem
)
}
else -> throw IllegalArgumentException("Unknown ViewModel type")
}
}
@Composable
fun AdventuresContent(
isScrollable: Boolean,
onSelectAdventure: (Adventure) -> Unit,
onLoadMore: () -> Unit,
isLoadingMore: Boolean,
adventures: List<Adventure>,
isLoading: Boolean,
onClickItem: (Adventurer) -> Unit
) {
val listState = rememberLazyListState()
// Trigger loading more data when near the end of the list
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull() }.collect { lastVisibleItem ->
// If the last visible item is near the end of the list, load more
if (lastVisibleItem != null && adventures.isNotEmpty() && lastVisibleItem.index >= adventures.size - 2 && !isLoadingMore) {
onLoadMore()
}
}
}
LazyColumn(
userScrollEnabled = isScrollable, modifier = Modifier.height(
if (adventures.isNotEmpty()) ((getScreenWidthInDp() + 100.dp) * adventures.size)
else 500.dp
), state = listState, content = {
if (isLoading) {
item {
Box(
modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center
) {
Loader(modifier = Modifier.size(100.dp))
}
}
} else itemsIndexed(adventures) { index, item ->
AdventureCard(
index = index,
item = item,
onClickAdventurerItem = onClickItem,
onSelectAdventure
)
}
// Show loading spinner when fetching more items
if (isLoadingMore) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Loader(modifier = Modifier.size(100.dp))
}
}
}
})
}
/**
* A composable function representing a selectable calendar with event indicators and navigation
* functionality. It displays the calendar's current month, allows scrolling between months,
* and showcases event items below the calendar.
*
* @param mainViewModel the main view model to manage the state and handle navigation events or data updates.
* @param padding the padding values to adjust the layout of the composable within its parent container.
*/
// تقویمی که نشان دهنده ی زمان و تعداد برگزاری اونت ها است و نوع آن ها را نیز نشان می دهد
/**
* A composable function representing a selectable calendar with event indicators and navigation.
* It displays the calendar's current month, allows scrolling between months,
* and showcases event items below the calendar.
*
* @param padding Padding values to adjust the layout within its parent container
* @param isScrollable Whether the calendar content should be scrollable
* @param onChangeMonth Callback when the visible month changes
* @param adventuresList List of adventures to display in the calendar
* @param isAdventuresLoading Whether adventures are currently being loaded
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun SelectableCalendar(
padding: PaddingValues,
isScrollable: Boolean,
onChangeMonth: ((YearMonth?) -> Unit)? = null,
adventuresList: MutableList<Adventure>,
isAdventuresLoading: Boolean,
) {
// Setup coroutine scope for animations
val coroutineScope = rememberCoroutineScope()
// Initialize calendar state
val calendarState = rememberCalendarState(
firstVisibleMonth = YearMonth.now(),
startMonth = YearMonth.now().minusYears(50),
endMonth = YearMonth.now().plusMonths(50)
).apply {
firstDayOfWeek = DayOfWeek.MONDAY
}
// Track the current visible month
val currentMonth = calendarState.layoutInfo.visibleMonthsInfo
.maxByOrNull { it.size }?.month?.yearMonth
// Track the selected day
val selectedDay = remember { mutableIntStateOf(-1) }
// Reset selection when month changes
LaunchedEffect(currentMonth) {
onChangeMonth?.invoke(currentMonth)
selectedDay.intValue = -1
}
// Tooltip-related state
val tooltipVisible = remember { mutableStateOf(false) }
var textWidth by remember { mutableStateOf(0.dp) }
var textHeight by remember { mutableStateOf(0.dp) }
var absoluteX by remember { mutableStateOf(0.dp) }
var absoluteY by remember { mutableStateOf(0.dp) }
Card(
colors = CardDefaults.cardColors(
containerColor = Color.White
), modifier = Modifier.padding(horizontal = 20.dp)
) {
LazyColumn(
userScrollEnabled = isScrollable,
verticalArrangement = Arrangement.Top,
modifier = Modifier.height((70.dp) * (adventuresList.size) + getScreenWidthInDp())
) {
item(
key = 0,
) {
// CompositionLocalProvider {
CalendarContent(
coroutineScope = coroutineScope,
calendarState = calendarState,
adventuresList = adventuresList,
currentMonth = currentMonth,
tooltipVisible = tooltipVisible,
selectedDay = selectedDay,
onTooltipPositionUpdate = { x, y, width, height ->
textWidth = width
textHeight = height
absoluteX = x
absoluteY = y
})
// }
}
if (isAdventuresLoading) {
item {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
) {
Loader(
modifier = Modifier
.height(50.dp)
.align(Alignment.Center)
)
}
}
}
val itemsList = adventuresList
itemsIndexed(itemsList.filter {
!(selectedDay.value > 0) || it?.startsAt?.convertDateString()
?.split(" ")?.get(1)?.toInt() == selectedDay.value
}) { index, adventure ->
// else {
// Safely access the adventure item
adventure?.let {
AdventureEventItem(it) // Use the actual event data here
}
// }
}
}
}
CalendarGuideCompose(
tooltipVisible,
absoluteX - textWidth,
absoluteY - padding.calculateBottomPadding() + textHeight / 2 // + textHeight - padding.calculateBottomPadding()
)
}
@Composable
fun CalendarContent(
modifier: Modifier = Modifier,
coroutineScope: CoroutineScope,
adventuresList: List<Adventure>,
calendarState: CalendarState,
currentMonth: YearMonth?,
tooltipVisible: MutableState<Boolean>,
selectedDay: MutableState<Int>,
onTooltipPositionUpdate: (Dp, Dp, Dp, Dp) -> Unit
) {
Column(
modifier = modifier.padding(start = 20.dp, end = 20.dp, top = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Calendar Header
Row(
modifier = Modifier.padding(vertical = 0.dp, horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Display the current month and year
Text(
style = TextStyle(
fontWeight = FontWeight.SemiBold,
color = Color(0xFF1C1C1E),
fontSize = with(LocalDensity.current) { 14.dp.toSp() }),
text = currentMonth?.month?.name ?: ""
)
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 5.dp),
style = TextStyle(
fontWeight = FontWeight.SemiBold,
color = Color(0xFF1C1C1E),
fontSize = with(LocalDensity.current) { 14.dp.toSp() }),
text = currentMonth?.year.toString()
)
// Tooltip for additional information
InfoText(
modifier = Modifier,
text = stringResource(id = R.string.info_symbol),
isTooltipVisible = tooltipVisible.value,
onTooltipToggle = { tooltipVisible.value = !tooltipVisible.value },
onPositionUpdate = onTooltipPositionUpdate
)
Spacer(modifier = Modifier.weight(1f))
// Navigation Buttons (Left and Right)
CalendarNavigationButton(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 40.dp),
imageResId = R.drawable.ic_calendar_left_arrow,
contentDescription = ""
) {
coroutineScope.launch {
calendarState.animateScrollToMonth(
calendarState.firstVisibleMonth.yearMonth.minusMonths(1)
)
}
}
CalendarNavigationButton(
modifier = Modifier.align(Alignment.CenterVertically),
imageResId = R.drawable.ic_calendar_right_arrow,
contentDescription = ""
) {
coroutineScope.launch {
calendarState.animateScrollToMonth(
calendarState.firstVisibleMonth.yearMonth.plusMonths(1)
)
}
}
}
// Divider
HorizontalDivider(
color = Color(0xFFE4E5E7),
thickness = 1.dp,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp, horizontal = 10.dp)
.background(Color(0xFFE4E5E7))
)
// Display the Weekday Row and the Calendar
WeekdayRow()
AdventureCalendar(
modifier = Modifier,
adventuresList,
calendarState = calendarState,
currentMonth = currentMonth,
selectedDay
)
}
}
@Composable
fun AdventureCalendar(
modifier: Modifier = Modifier,
adventuresList: List<Adventure>,
calendarState: CalendarState,
currentMonth: YearMonth?,
selectedDay: MutableState<Int>
) {
val sortedAdventures = adventuresList.groupBy { it.startsAt.convertDateString() }
// var selectedDay by remember { mutableIntStateOf(-1) }
HorizontalCalendar(
state = calendarState,
modifier = modifier
.wrapContentSize()
.padding(top = 10.dp),
dayContent = { day ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(0.dp)
.background(Color.White)
) {
/* LaunchedEffect(
calendarState.firstVisibleMonth
) {
selectedDay.value = -1
}
LaunchedEffect(
calendarState.lastVisibleMonth
) {
selectedDay.value = -1
}
*/
val isNowDay = LocalDate.now().dayOfMonth == day.date.dayOfMonth
val isNowMonth = YearMonth.now().monthValue == day.date.monthValue
val isNowYear = LocalDate.now().year == day.date.year
val isNowDate = isNowMonth && isNowDay && isNowYear
if (day.date.yearMonth.month.value == currentMonth?.month?.value) {
val day = day.date.dayOfMonth
Timber.d("Processing day: $day")
val adventuresCoDay =
sortedAdventures.filter { it.key.split(" ")[1].toInt() == day }.values.firstOrNull()
val occur = adventuresCoDay?.size ?: 0
Timber.d("Occurrences for day $day: $occur")
var onSelect = {
selectedDay.value = day
}
val isSelected = day == selectedDay.value
val selectedBackgroundColor = Color(0xFF5856D6)
when {
isNowDate -> {
AdventureCalendarItem(
text = day.toString(),
textColor = Color.White,
backgroundColor = if (isSelected) selectedBackgroundColor else Color(
0xFF30D158
),
countColor = Color(0xFF30D158),
count = occur,
onSelect = onSelect,
isSelected = isSelected
)
}
occur > 1 -> {
Timber.d("Day $day has more than 1 event.")
AdventureCalendarItem(
text = day.toString(),
textColor = if (isSelected) Color.White
else Color(0xFF30D158),
backgroundColor = if (isSelected) selectedBackgroundColor else Color.White,
countColor = Color(0xFF30D158),
count = occur,
onSelect = onSelect,
isSelected = isSelected
)
}
occur == 1 -> {
Timber.d("Day $day has 1 event.")
when (adventuresCoDay!!.first().state) {
"upcoming" -> {
AdventureCalendarItem(
text = day.toString(),
textColor = Color.Black,
backgroundColor = if (isSelected) selectedBackgroundColor else Color(
0xFFF5E2C6
),
onSelect = onSelect,
isSelected = isSelected
)
}
"past" -> {
AdventureCalendarItem(
text = day.toString(),
textColor = Color.Black,
backgroundColor = if (isSelected) selectedBackgroundColor else Color(
0xFFD7D7DF
),
onSelect = onSelect,
isSelected = isSelected
)
}
"active" -> {
AdventureCalendarItem(
text = day.toString(),
textColor = Color.Black,
backgroundColor = if (isSelected) selectedBackgroundColor else Color(
0xFFC5EACF
),
onSelect = onSelect,
isSelected = isSelected
)
}
}
}
occur == 0 -> {
Timber.d("Day $day has no events.")
AdventureCalendarItem(
text = day.toString(),
textColor = if (isSelected) Color.White else Color(0xff1C1C1E),
backgroundColor = if (isSelected) selectedBackgroundColor else Color.White,
onSelect = onSelect,
isSelected = isSelected
)
}
}
}
// }
}
},
userScrollEnabled = true,
calendarScrollPaged = true,
contentHeightMode = ContentHeightMode.Wrap
)
}
/**
* A composable function that displays information text with tooltip functionality.
* When clicked, it toggles a tooltip and provides position information for proper placement.
*
* @param modifier The modifier to be applied to the composable
* @param text The text to be displayed
* @param isTooltipVisible Boolean indicating whether the tooltip is currently visible
* @param onTooltipToggle Callback for toggling tooltip visibility
* @param onPositionUpdate Callback that provides position data for tooltip placement
*/
@Composable
fun InfoText(
modifier: Modifier = Modifier,
text: String,
isTooltipVisible: Boolean,
onTooltipToggle: (Boolean) -> Unit,
onPositionUpdate: (Dp, Dp, Dp, Dp) -> Unit
) {
val density = LocalDensity.current
Text(
text = text,
modifier = modifier
.padding(start = 5.dp)
.clickable { onTooltipToggle(!isTooltipVisible) }
.onGloballyPositioned { layoutCoordinates ->
// Calculate position measurements for tooltip placement
val textWidth = with(density) { layoutCoordinates.size.width.toFloat().toDp() }
val textHeight = with(density) { layoutCoordinates.size.height.toFloat().toDp() }
val absoluteX = with(density) { layoutCoordinates.positionOnScreen().x.toDp() }
val absoluteY = with(density) { layoutCoordinates.positionOnScreen().y.toDp() }
// Update tooltip position
onPositionUpdate(absoluteX, absoluteY, textWidth, textHeight)
},
style = TextStyle(
fontSize = with(LocalDensity.current) { 16.dp.toSp() },
color = Color(0xFF1C1C1E),
fontWeight = FontWeight.Bold
)
)
}
/*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CalendarCard(
calendarState: CalendarState, // Passed as a parameter
modifier: Modifier = Modifier,
onPrevMonthClick: () -> Unit,
onNextMonthClick: () -> Unit,
currentMonth: YearMonth?,
tooltipVisible: MutableState<Boolean>,
onTooltipPositionUpdate: (Dp, Dp, Dp, Dp) -> Unit
) {
Card(
colors = CardDefaults.cardColors(containerColor = Color.White),
modifier = modifier.padding(horizontal = 20.dp)
) {
Column(
modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Calendar header with month name, year, and navigation controls
Row(
modifier = Modifier.padding(vertical = 0.dp, horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = currentMonth?.month?.name ?: "", style = TextStyle(
fontWeight = FontWeight.SemiBold,
color = Color(0xFF1C1C1E),
fontSize = with(LocalDensity.current) { 14.dp.toSp() })
)
Text(
text = currentMonth?.year.toString(),
style = TextStyle(
fontWeight = FontWeight.SemiBold,
color = Color(0xFF1C1C1E),
fontSize = with(LocalDensity.current) { 14.dp.toSp() }),
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 5.dp)
)
InfoText(
text = stringResource(id = R.string.info_symbol),
isTooltipVisible = tooltipVisible.value,
modifier = Modifier,
onTooltipToggle = { tooltipVisible.value = !tooltipVisible.value },
onPositionUpdate = onTooltipPositionUpdate
)
Spacer(modifier = Modifier.weight(1f))
CalendarNavigationButton(
imageResId = R.drawable.ic_calendar_left_arrow,
contentDescription = "",
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 40.dp),
onClick = onPrevMonthClick
)
CalendarNavigationButton(
imageResId = R.drawable.ic_calendar_right_arrow,
contentDescription = "",
modifier = Modifier.align(Alignment.CenterVertically),
onClick = onNextMonthClick
)
}
// Divider
HorizontalDivider(
color = Color(0xFFE4E5E7),
thickness = 1.dp,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp, horizontal = 10.dp)
)
// Weekday names and calendar layout
WeekdayRow()
AdventureCalendar(
adventuresList = emptyList(),
calendarState = calendarState,
currentMonth = currentMonth
)
}
}
}
*/
@Composable
fun CalendarNavigationButton(
modifier: Modifier = Modifier,
imageResId: Int,
contentDescription: String,
onClick: suspend () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
Image(
modifier = modifier/*
.align(Alignment.CenterVertically)
*//*
.padding(horizontal = 40.dp)
*/.clickable {
coroutineScope.launch {
onClick()
}
}, painter = painterResource(id = imageResId), contentDescription = contentDescription
)
}
/**
* Represents an item in the calendar view indicating an event and its associated date.
*
* @param imageId Resource ID of the image to display alongside the event item.
* @param eventName Name or title of the adventure event to be displayed.
* @param eventDate Date or time of the adventure event to be displayed.
*/
// نشان دهنده ی آیتم موجود در تقویم و این که زمان برگزاری آن چه موقع است
/**
* Represents an item in the calendar view indicating an event and its associated date.
*
* @param adventure The adventure data to display in the calendar item
*/
@Composable
fun AdventureEventItem(adventure: Adventure) {
Box(
modifier = Modifier
.height(70.dp)
.padding(5.dp),
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
// Adventure thumbnail image
Image(
painter = rememberAsyncImagePainter(
model = adventure.banner,
imageLoader = ImageLoader.Builder(LocalContext.current)
.diskCachePolicy(CachePolicy.ENABLED)
.build()
),
contentDescription = "Adventure thumbnail",
modifier = Modifier
.size(76.dp)
.padding(10.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
// Content section with title and date
Box(modifier = Modifier.weight(1f)) {
HorizontalDivider(
thickness = 1.dp,
modifier = Modifier.height(1.dp),
color = Color(0xFFEAEAEA)
)
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly
) {
// Title text
Text(
text = adventure.title,
modifier = Modifier.padding(horizontal = 10.dp),
style = TextStyle(
color = Color.Black,
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
// Date range text
Text(
text = "${adventure.startsAt.convertDateString()} - ${adventure.endsAt.convertDateString()}",
modifier = Modifier.padding(horizontal = 8.dp),
style = TextStyle(
fontSize = with(LocalDensity.current) { 12.dp.toSp() },
color = Color(0xFFA3A3A3),
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
// Chevron indicator
Image(
painter = painterResource(id = R.drawable.right_chevron),
contentDescription = "View details",
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 10.dp)
)
}
}
}
}
@Composable
fun getScreenWidthInDp(): Dp {
val configuration = LocalConfiguration.current
return configuration.screenWidthDp.dp
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\home\notifications\Notifications.kt
```kt
package com.divadventure.ui.screens.main.home.notifications
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
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.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
import com.divadventure.R
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.ui.BackCompose
import com.divadventure.viewmodel.NotificationsIntent
import com.divadventure.viewmodel.NotificationsUiEvent
import com.divadventure.viewmodel.NotificationsViewModel
// این کامپوز مانند صفحه ی نتویفیکشن اینستگرام است، و می توان در آن لیست درخواستهای فالو کردن و همچنین درخواست همراهی در ماجراجویی ها را مشاهده کرد همچنین یادآوری برگزاری رویدادها هم در اینجا ممکن است
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Notifications(
notificationsViewModel: NotificationsViewModel,
navigationViewModel: NavigationViewModel,
padding: PaddingValues
) {
val state by notificationsViewModel.state.collectAsState()
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) {
notificationsViewModel.uiEvent.collect { event ->
when (event) {
NotificationsUiEvent.AnimateItem -> {
}
is NotificationsUiEvent.NavigateToNextScreen -> {
}
NotificationsUiEvent.ShowBottomSheet -> {
showBottomSheet = true
}
NotificationsUiEvent.ShowDialog -> {
}
is NotificationsUiEvent.ShowSnackbar -> {
}
}
}
}
// Get the Back Press Dispatcher
val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
// Register the Back Press Callback
DisposableEffect(backPressedDispatcher) {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Trigger PopSpecific to navigate back without additional NavigateTo call
navigationViewModel.navigate(
NavigationEvent.PopBackStack
)
}
}
backPressedDispatcher?.addCallback(callback)
// Cleanup callback
onDispose {
callback.remove()
}
}
if (showBottomSheet) {
// باتم شیتی که در آن می توان درخواست پیوستن به یک رویداد از طرف کاربری دیگر یا ماجراجویی را پذیرفت یا رد کرد
GeneralBottomSheet(
showBottomSheet = showBottomSheet,
onDismissRequest = { showBottomSheet = false },
content = {
BottomSheetContent(
listOf("Yes", "Maybe", "No"),
onOptionClick = { option ->
// Handle option click (yes, maybe, no)
println("Option selected: $option")
showBottomSheet = false
},
onCancelClick = {
// Handle cancel click
println("Cancel clicked")
showBottomSheet = false
}
)
}
)
}
Scaffold(
containerColor = Color.White
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
Column {
BackCompose("Notifications") {
backPressedDispatcher?.onBackPressed()
}
LazyColumn(
) {
item {
NotificationTimeCategory("This Week")
}
item {
NotificationAdventureItem(
R.drawable.landing1, "Camping", "accepted your join request. ", "3h ago"
)
}
item {
TimeCategoryDivider()
}
item {
NotificationTimeCategory("This Month")
}
item {
RequestItem(
imageId = R.drawable.random_image_2,
userName = "John Doe",
date = "2d ago"
)
}
item {
NotificationMentionItem(
imageId = R.drawable.random_image_3,
userName = "Jane Smith",
date = "5h ago"
)
}
item {
RequestItem(
imageId = R.drawable.random_image_3,
userName = "Default User",
date = "8h ago"
)
}
item {
NotificationInvitationItem(
notificationsViewModel,
imageId = R.drawable.random_image_4,
userName = "Alice Anderson",
date = "12h ago"
)
}
}
}
}
}
}
/**
* Displays a notification invitation item, showing user information, a message, and a button
* to handle the response to the invitation.
*
* @param notificationsViewModel The [NotificationsViewModel] instance used to handle actions.
* @param imageId The resource ID of the image to display as the user's profile picture.
* @param userName The name of the user who sent the invitation.
* @param date The date associated with the invitation.
*/
@Composable
fun NotificationInvitationItem(
notificationsViewModel: NotificationsViewModel,
imageId: Int,
userName: String,
date: String
) {
Row(
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 10.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
Image(
painter = painterResource(id = imageId),
contentDescription = "Friend Request Profile Image",
modifier = Modifier
.size(50.dp)
.clip(RoundedCornerShape(4.dp))
)
Column {
Text(
text = userName, style = TextStyle(
color = Color.Black,
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily(Font(R.font.sf_pro))
), fontSize = 16.sp
)
Text(
text = buildAnnotatedString {
withStyle(
style = SpanStyle(
color = Color(0xFF565656),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Light
)
) {
append("invited you to an \n")
}
withStyle(
style = SpanStyle(
color = Color(0xFF007AFF),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Normal
)
) {
append("adventure. ")
}
withStyle(
style = SpanStyle(
color = Color(0xFFAEAEAE),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Normal
)
) {
append(date)
}
}, style = TextStyle(
)
)
}
Spacer(modifier = Modifier.weight(1f, true))
TextButton(
modifier = Modifier.height(40.dp),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.textButtonColors(
containerColor = Color(0xFF007AFF)
),
onClick = {
notificationsViewModel.sendIntent(
NotificationsIntent.ShowBottomSheet
)
}) {
Text(
modifier = Modifier
.height(16.dp)
.padding(horizontal = 15.dp),
text = "Going ?",
color = Color.White,
style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.SemiBold,
fontSize = 12.sp
)
)
}
}
}
//***************************************************************
@Composable
fun NotificationMentionItem(imageId: Int, userName: String, date: String) {
Row(
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 10.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
Image(
painter = painterResource(id = imageId),
contentDescription = "Friend Request Profile Image",
modifier = Modifier
.size(50.dp)
.clip(RoundedCornerShape(4.dp))
)
Column {
Text(
text = userName, style = TextStyle(
color = Color.Black,
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily(Font(R.font.sf_pro))
), fontSize = 16.sp
)
Text(
text = buildAnnotatedString {
withStyle(
style = SpanStyle(
color = Color(0xFF565656),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Light
)
) {
append("mentioned you in a ")
}
withStyle(
style = SpanStyle(
color = Color(0xFF007AFF),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Normal
)
) {
append("comment. ")
}
withStyle(
style = SpanStyle(
color = Color(0xFFAEAEAE),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Normal
)
) {
append(date)
}
}, style = TextStyle(
)
)
}
}
}
/**
* نوتیفیکیشنی که از طرف شخصی آمده و درخواست دوستی دارد
* */
@Composable
fun RequestItem(imageId: Int, userName: String, date: String) {
Row(
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 10.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
Image(
painter = rememberAsyncImagePainter(model = imageId),
contentDescription = "Friend Request Profile Image",
modifier = Modifier
.size(50.dp)
.clip(RoundedCornerShape(4.dp))
)
Column {
Text(
text = userName, style = TextStyle(
color = Color.Black,
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily(Font(R.font.sf_pro))
), fontSize = 16.sp
)
Text(
text = buildAnnotatedString {
withStyle(
style = SpanStyle(
color = Color(0xFF565656),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Light
)
) {
append("has sent a friend \n request. ")
}
withStyle(
style = SpanStyle(
color = Color(0xFFAEAEAE),
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Normal
)
) {
append(date)
}
}, style = TextStyle(
)
)
}
Spacer(modifier = Modifier.weight(1f, true))
TextButton(
modifier = Modifier.height(32.5.dp),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.textButtonColors(
containerColor = Color(0xFF30D158)
),
onClick = { }) {
Text(
modifier = Modifier
.height(16.dp)
.padding(horizontal = 10.dp),
text = "Accept",
color = Color.White,
style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp
)
)
}
TextButton(
modifier = Modifier.height(32.5.dp),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.textButtonColors(
containerColor = Color(0xffF2F2F7)
),
onClick = { }) {
Text(
modifier = Modifier
.height(16.dp)
.padding(horizontal = 10.dp),
text = "Decline",
color = Color.Black,
style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp
)
)
}
}
}
@Composable
fun TimeCategoryDivider() {
Box(
modifier = Modifier.padding(vertical = 5.dp)
) {
HorizontalDivider(
color = Color(0xFFF2F2F7),
modifier = Modifier
.align(Alignment.Center)
.background(Color(0xFFF2F2F7))
.fillMaxWidth()
.height(0.25.dp)
)
}
}
@Composable
fun NotificationTimeCategory(text: String) {
Text(
text = text,
modifier = Modifier.padding(start = 20.dp, top = 10.dp, end = 20.dp),
style = TextStyle(
color = Color(0xff1C1C1E),
fontWeight = FontWeight.SemiBold,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
fontSize = 20.sp
)
}
@Composable
fun NotificationAdventureItem(
imageId: Int, eventName: String, eventDescription: String, time: String
) {
Box(
modifier = Modifier
.height(70.dp)
.padding(10.dp), contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier.align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.wrapContentHeight(), contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = imageId),
contentDescription = "",
modifier = Modifier
.size(76.dp)
.align(Alignment.Center)
.aspectRatio(1f) // Ensures square aspect ratio
.padding(10.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.FillBounds
)
}
Box {
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly
) {
Text(
modifier = Modifier.padding(horizontal = 10.dp),
text = eventName,
style = TextStyle(
color = Color.Black, fontSize = 18.sp, fontWeight = FontWeight.SemiBold
),
fontFamily = FontFamily(Font(R.font.sf_pro))
)
Text(
modifier = Modifier.padding(horizontal = 10.dp),
text = buildAnnotatedString {
withStyle(SpanStyle(color = Color(0xFF565656))) {
append(eventDescription)
}
} + buildAnnotatedString {
withStyle(SpanStyle(color = Color(0xFFAEAEAE))) {
append(time)
}
},
style = TextStyle(
fontSize = 16.sp,
color = Color(0xFFA3A3A3),
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
}
}
Spacer(modifier = Modifier.weight(1f))
}
}
}
@ExperimentalMaterial3Api
@Composable
fun GeneralBottomSheet(
modifier: Modifier = Modifier,
showBottomSheet: Boolean,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit // Accept composable content
) {
if (showBottomSheet) {
ModalBottomSheet(
sheetState = rememberModalBottomSheetState(),
modifier = modifier.wrapContentHeight(),
containerColor = Color.Transparent,
dragHandle = { Box { } },
onDismissRequest = onDismissRequest,
content = {
Box(
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 80.dp)
.fillMaxSize()
.background(Color.Transparent)
) {
content() // Pass the composable content here
}
},
scrimColor = Color(0x82000000) // Optional: Customize scrim color
)
}
}
@Composable
fun BottomSheetContent(
options: List<String>,
onOptionClick: (String) -> Unit,
onCancelClick: () -> Unit,
optionStyles: List<TextStyle> = emptyList()
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
ResponseOptionsCard(
options = options,
optionStyles = optionStyles,
onOptionClick = onOptionClick
)
CancelOptionCard(
onCancelClick = onCancelClick
)
}
}
@Composable
fun ResponseOptionsCard(
options: List<String>,
optionStyles: List<TextStyle> = emptyList(),
onOptionClick: (String) -> Unit
) {
Card(
colors = CardDefaults.cardColors(containerColor = Color.White),
modifier = Modifier
.fillMaxWidth()
.height((70 * options.size).dp),
shape = RoundedCornerShape(14.dp)
) {
Column {
options.forEachIndexed { index, option ->
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.clickable { onOptionClick(option) },
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = option,
style = optionStyles.getOrNull(index)
?: TextStyle( // Check if a style is provided
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF007AFF)
)
)
// Divider except for the last option
if (index < options.size) {
BottomSheetDivider()
}
}
}
}
}
}
@Composable
fun BottomSheetDivider() {
HorizontalDivider(
color = Color(0x82000000).copy(alpha = 0.51f),
modifier = Modifier
.height(1.dp)
.fillMaxWidth()
)
}
@Composable
fun CancelOptionCard(
onCancelClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(70.dp)
.clickable { onCancelClick() },
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Cancel",
style = TextStyle(
fontSize = 20.sp,
color = Color(0xFFFF3B30)
)
)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\home\notifications\search\filter\Filter.kt
```kt
package com.divadventure.ui.screens.main.home.notifications.search.filter
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.divadventure.R
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.ui.ApplyButton
import com.divadventure.ui.BackCompose
import com.divadventure.ui.SelectDate
import com.divadventure.viewmodel.HomeIntent
import com.divadventure.viewmodel.HomeUiEvent
import com.divadventure.viewmodel.HomeViewModel
import com.divadventure.viewmodel.MainIntent
import com.divadventure.viewmodel.MainUiEvent
import com.divadventure.viewmodel.MainViewModel
import com.kizitonwose.calendar.core.CalendarDay
@Composable
fun Filter(
mainViewModel: MainViewModel,
navigationViewModel: NavigationViewModel,
homeViewModel: HomeViewModel,
paddingValues: PaddingValues
) {
var state = homeViewModel.state.collectAsState().value
var showStartDateDialog by remember { mutableStateOf(false) }
var showEndDateDialog by remember { mutableStateOf(false) }
LaunchedEffect(key1 = true) {
homeViewModel.uiEvent.collect { event ->
when (event) {
is HomeUiEvent.ShowDim -> {}
is HomeUiEvent.ShowSnackbar -> {}
HomeUiEvent.AnimateItem -> {}
is HomeUiEvent.NavigateToNextScreen -> {
}
HomeUiEvent.ShowStartDateDialog -> {
showStartDateDialog = true
}
HomeUiEvent.ShowEndDateDialog -> {
showEndDateDialog = true
}
else -> {
// do nothing
}
}
}
}
LaunchedEffect(key1 = true) {
mainViewModel.uiEvent.collect { event ->
when (event) {
MainUiEvent.AnimateItem -> {
}
is MainUiEvent.NavigateToNextScreen -> {
navigationViewModel.navigate(event.navigationEvent)
}
MainUiEvent.ShowDialog -> {
}
is MainUiEvent.ShowDim -> {
}
is MainUiEvent.ShowSnackbar -> {
}
is MainUiEvent.AdventureAction -> {
}
}
}
}
if (showStartDateDialog) {
SelectDate(
onDismissRequest = { showStartDateDialog = false },
onSelectDate = { date, cloc, amPm ->
if (date != null) {
val formattedDate = calendarDayToString(date)
// Use formattedDate if needed
homeViewModel.sendIntent(HomeIntent.SetStartDate(formattedDate))
}
showStartDateDialog = false
})
} else if (showEndDateDialog) {
SelectDate(
onDismissRequest = { showEndDateDialog = false },
onSelectDate = { date, clock, amPm ->
if (date != null) {
val formattedDate = calendarDayToString(date)
// Use formattedDate if needed
homeViewModel.sendIntent(HomeIntent.SetEndDate(formattedDate))
}
showEndDateDialog = false
})
}
Box(modifier = Modifier.background(Color.White)) {
Box(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.background(Color(0xFFF2F2F7))
) {
Column {
BackCompose("Filter", modifier = Modifier) {
navigationViewModel.navigate(NavigationEvent.PopBackStack)
}
Column(
modifier = Modifier.padding(top = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
FilterItem(
R.drawable.ic_heart,
"Interests",
state.filters.interests?.joinToString(", ") { it.name } ?: "") {
mainViewModel.sendIntent(MainIntent.GotoInterests)
}
FilterItem(
R.drawable.ic_location, "Location", state.newLocation?.name ?: ""
) {
mainViewModel.sendIntent(MainIntent.GotoLocation)
}
FilterItem(
R.drawable.ic_calendar, "Start Date", state.filters.startDate ?: ""
) {
homeViewModel.sendIntent(HomeIntent.ShowStartDateDialog)
}
FilterItem(
R.drawable.ic_calendar, "End Date", state.filters.endDate ?: ""
) {
homeViewModel.sendIntent(HomeIntent.ShowEndDateDialog)
}
FilterItem(R.drawable.ic_flag, "Status", state.filters.state ?: "", {
mainViewModel.sendIntent(MainIntent.GotoStatus)
})
ApplyButton {
homeViewModel.sendIntent(HomeIntent.ApplyFilter)
navigationViewModel.navigate(NavigationEvent.PopBackStack)
}
}
}
}
}
}
@Composable
fun FilterItem(imageId: Int, title: String, selectionText: String, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.clickable {
onClick()
}, colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Row(
modifier = Modifier.padding(vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(Color(0xFF848484)),
painter = painterResource(id = imageId),
contentDescription = title,
modifier = Modifier
.padding(10.dp)
.size(25.dp)
)
Text(
text = title,
modifier = Modifier.padding(vertical = 10.dp),
style = TextStyle(color = Color(0xFF1C1C1E))
)
Text(
modifier = Modifier
.weight(1f, true)
.align(Alignment.CenterVertically),
text = selectionText,
color = Color(0xff848484),
style = TextStyle(
textAlign = androidx.compose.ui.text.style.TextAlign.End, fontSize = 14.sp
)
)
Image(
imageVector = ImageVector.vectorResource(R.drawable.right_chevron),
contentDescription = title,
modifier = Modifier.padding(10.dp)
)
}
}
}
fun calendarDayToString(calendarDay: CalendarDay): String {
return calendarDay.date.year.toString() + "-" + calendarDay.date.monthValue.toString()
.toString() + "-" + calendarDay.date.dayOfMonth.toString()
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\home\notifications\search\filter\Interests.kt
```kt
package com.divadventure.ui.screens.main.home.notifications.search.filter
import androidx.compose.animation.AnimatedVisibility
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.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.divadventure.R
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.domain.models.Interest
import com.divadventure.ui.ApplyButton
import com.divadventure.ui.HeaderWithCloseButton
import com.divadventure.ui.SearchField
import com.divadventure.ui.SelectionRow
import com.divadventure.ui.SortDivider
import com.divadventure.ui.screens.Loader
import com.divadventure.viewmodel.AdventuresIntent
import com.divadventure.viewmodel.HomeIntent
import com.divadventure.viewmodel.HomeViewModel
import com.divadventure.viewmodel.ManageAdventureViewModel
@Composable
fun AdventureInterests(
navigationViewModel: NavigationViewModel,
adventureViewModel: ManageAdventureViewModel,
paddingValues: PaddingValues
) {
// Move all homeViewModel interactions to the top level
val state = adventureViewModel.state.collectAsState()
val allInterests = state.value.allInterests.sortedBy { it.name }
var querySearch by remember { mutableStateOf("") }
var selectedItems = remember {
state.value.adventureInterests?.toMutableStateList()
?: emptyList<Interest>().toMutableStateList()
}
if (allInterests.isEmpty()) {
adventureViewModel.sendIntent(AdventuresIntent.FetchInterests)
}
// Define callbacks that will be passed down
val onApplyInterests = {
adventureViewModel.sendIntent(AdventuresIntent.ApplyInterests(selectedItems.toMutableList()))
navigationViewModel.navigate(NavigationEvent.PopBackStack)
}
InterestsScreenLayout(
paddingValues = paddingValues,
isLoading = allInterests.isNotEmpty(),
hasInterests = allInterests.isNotEmpty(),
onCloseClick = {
navigationViewModel.navigate(NavigationEvent.PopBackStack)
},
searchContent = {
SearchField(queryText = querySearch) {
querySearch = it
}
},
listContent = {
SelectionList(
allInterests = allInterests,
selectedItems = selectedItems,
querySearch = querySearch
)
},
applyButtonContent = {
ApplyButton("Apply") {
onApplyInterests()
}
})
}
@Composable
fun FilterInterests(
navigationViewModel: NavigationViewModel,
homeViewModel: HomeViewModel,
paddingValues: PaddingValues
) {
// Move all homeViewModel interactions to the top level
val state = homeViewModel.state.collectAsState()
val allInterests = state.value.allInterests.sortedBy { it.name }
val isInterestsEmpty = state.value.allInterests.isEmpty()
var querySearch by remember { mutableStateOf("") }
var selectedItems =
remember {
state.value.filters.interests?.toMutableStateList()
?: emptyList<Interest>().toMutableStateList()
}
if (allInterests.isEmpty()) {
homeViewModel.sendIntent(HomeIntent.FetchInterests)
}
// Define callbacks that will be passed down
val onApplyInterests = {
homeViewModel.sendIntent(HomeIntent.ApplyInterests(selectedItems.toMutableList()))
navigationViewModel.navigate(NavigationEvent.PopBackStack)
}
InterestsScreenLayout(
paddingValues = paddingValues,
isLoading = allInterests.isNotEmpty(),
hasInterests = allInterests.isNotEmpty(),
onCloseClick = {
navigationViewModel.navigate(NavigationEvent.PopBackStack)
},
searchContent = {
SearchField(queryText = querySearch) {
querySearch = it
}
},
listContent = {
SelectionList(
allInterests = allInterests,
selectedItems = selectedItems,
querySearch = querySearch
)
},
applyButtonContent = {
ApplyButton("Apply") {
onApplyInterests()
}
})
}
@Composable
fun InterestsScreenLayout(
paddingValues: PaddingValues,
isLoading: Boolean,
hasInterests: Boolean,
onCloseClick: () -> Unit,
searchContent: @Composable () -> Unit,
listContent: @Composable () -> Unit,
applyButtonContent: @Composable () -> Unit
) {
Box(
modifier = Modifier
.background(Color.White)
.fillMaxSize()
.padding(paddingValues)
.background(Color(0xFFEFEFF4))
) {
AnimatedVisibility(isLoading) {
Loader(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = Color.White)
.padding(0.dp)
) {
HeaderWithCloseButton(title = "Interests") {
onCloseClick()
}
AnimatedVisibility(hasInterests) {
Column {
Box(modifier = Modifier.padding(bottom = 15.dp)) {
searchContent()
}
}
}
listContent()
AnimatedVisibility(hasInterests) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(color = Color(0xFFEFEFF4))
) {
applyButtonContent()
}
}
}
}
}
@Composable
fun SelectionImage(isSelected: Boolean) {
Box(
modifier = Modifier.size(40.dp), contentAlignment = Alignment.Center
) {
// Show AnimatedVisibility with smooth fade-in and scale-in animation when visible
AnimatedVisibility(
visible = isSelected,
enter = fadeIn(animationSpec = tween(durationMillis = 300)) + scaleIn(
initialScale = 0.8f, animationSpec = tween(durationMillis = 300)
),
exit = fadeOut(animationSpec = tween(durationMillis = 300)) + scaleOut(
targetScale = 0.8f, animationSpec = tween(durationMillis = 300)
)
) {
Image(
painter = painterResource(id = R.drawable.ic_circle_selected),
contentDescription = "Selected Sort",
modifier = Modifier.align(Alignment.Center)
)
}
// Show AnimatedVisibility for the unselected state
AnimatedVisibility(
visible = !isSelected,
enter = fadeIn(animationSpec = tween(durationMillis = 300)) + scaleIn(
initialScale = 0.8f, animationSpec = tween(durationMillis = 300)
),
exit = fadeOut(animationSpec = tween(durationMillis = 300)) + scaleOut(
targetScale = 0.8f, animationSpec = tween(durationMillis = 300)
)
) {
Image(
painter = painterResource(id = R.drawable.ic_circle_unselected),
contentDescription = "Not Selected",
modifier = Modifier.align(Alignment.Center)
)
}
}
}
@Composable
fun SelectionList(
allInterests: List<Interest>,
selectedItems: MutableList<Interest>,
querySearch: String
) {
val filteredItems = remember(allInterests, selectedItems, querySearch) {
val unselectedItems = allInterests
.filterNot { selectedItems.contains(it) }
.map { it.name }
.filter { it.startsWith(querySearch, ignoreCase = true) }
val selectedItemNames = selectedItems.map { it.name }
unselectedItems + selectedItemNames
}
AnimatedVisibility(visible = allInterests.isNotEmpty()) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.45f)
) {
itemsIndexed(filteredItems) { _, item ->
SelectionRowWithDivider(
text = item,
isSelected = selectedItems.any { it.name == item },
onItemClick = { handleSelectionClick(item, allInterests, selectedItems) }
)
}
}
}
}
@Composable
fun SelectionRowWithDivider(
text: String,
isSelected: Boolean,
onItemClick: () -> Unit
) {
SelectionRow(
text = text,
isSelected = isSelected,
selectionImage = { isSelected -> SelectionImage(isSelected) },
onClick = onItemClick
)
SortDivider()
}
private fun handleSelectionClick(
item: String,
allInterests: List<Interest>,
selectedItems: MutableList<Interest>
) {
if (selectedItems.any { it.name == item }) {
selectedItems.removeIf { it.name == item }
} else {
allInterests.firstOrNull { it.name == item }?.let { selectedItems.add(it) }
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\home\notifications\search\filter\Location.kt
```kt
package com.divadventure.ui.screens.main.home.notifications.search.filter
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
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.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
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.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.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
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.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.divadventure.R
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.ui.HeaderWithCloseButton
import com.divadventure.ui.screens.Loader
import com.divadventure.viewmodel.HomeIntent
import com.divadventure.viewmodel.HomeViewModel
import com.divadventure.viewmodel.MainViewModel
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.CameraPosition
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.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MapUiSettings
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.rememberMarkerState
import kotlinx.coroutines.launch
import java.util.Locale
@Composable
fun Location(
mainViewModel: MainViewModel,
navigationViewModel: NavigationViewModel,
homeViewModel: HomeViewModel,
paddingValues: PaddingValues
) {
var locationScreenSearchText by remember { mutableStateOf("") }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(paddingValues)
.background(Color(0xFFEFEFF4))
) {
val homeState = homeViewModel.state.collectAsState().value
GoogleMapWithLocationSearch(
searchFieldValue = locationScreenSearchText,
onSearchFieldValueChange = { locationScreenSearchText = it },
locationsPredicted = homeState.locationsPredicted,
newLocation = homeState.newLocation,
isLocationLoading = homeState.isLoading.locationIsLoading,
onLocationFieldChanged = { query ->
homeViewModel.sendIntent(HomeIntent.LocationFieldChanged(query))
},
onLocationSelected = { selectedLocation ->
// locationScreenSearchText is updated by GoogleMapWithLocationSearch's onLocationSelected
homeViewModel.sendIntent(HomeIntent.LocationSelected(selectedLocation))
},
onBackPressed = {
navigationViewModel.navigate(NavigationEvent.PopBackStack)
},
onMapClicked = { latLng ->
locationScreenSearchText = "${latLng.latitude}, ${latLng.longitude}"
homeViewModel.sendIntent(HomeIntent.MapClicked(latLng))
},
)
}
}
data class LocationSearchCallbacks(
val onLocationFieldChanged: (String) -> Unit,
val onLocationSelected: (AutocompletePrediction) -> Unit,
val onBackPressed: (() -> Unit)? = null
)
data class LocationSearchData(
val locationsPredicted: MutableList<AutocompletePrediction>,
val newLocation: Place?,
val isLocationLoading: Boolean
)
@Composable
fun GoogleMapWithLocationSearch(
modifier: Modifier = Modifier,
searchFieldValue: String,
onSearchFieldValueChange: (String) -> Unit,
locationsPredicted: MutableList<AutocompletePrediction>,
newLocation: Place?,
isLocationLoading: Boolean,
onLocationFieldChanged: (String) -> Unit,
onLocationSelected: (AutocompletePrediction) -> Unit,
onBackPressed: (() -> Unit)? = null,
showHeader: Boolean = true,
headerTitle: String = "Location",
isMapMoving: MutableState<Boolean> = mutableStateOf(false),
onMapClicked: (LatLng) -> Unit,
) {
var isDropdownVisible = remember { mutableStateOf(false) } // Initialize to false
// var isLocationFound = remember { mutableStateOf(false) } // Seems unused, consider removing
LaunchedEffect(locationsPredicted) {
isDropdownVisible.value = locationsPredicted.isNotEmpty()
}
Box(
modifier = modifier.fillMaxSize()
) {
// Optional Header
if (showHeader) {
HeaderWithCloseButton(
modifier = Modifier,
title = headerTitle
) {
onBackPressed?.invoke()
}
}
// Map Section
// Map Section - clipped to its bounds
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
awaitPointerEvent() // Consume all pointer input — disables scroll for parent
}
}
}/*.pointerInteropFilter {
// Always consume touch events so they don't propagate to the Column
true
}*/
) {
GoogleMapScreen(
modifier = Modifier
.padding(top = if (showHeader) 0.dp else 120.dp)
.fillMaxSize()
.aspectRatio(1f),
isMapMoving,
newLocation = newLocation,
// textState = textState, // No longer needed here
items = locationsPredicted, // This might also be redundant if GoogleMapScreen doesn't use it directly
onMapLatLngClicked = onMapClicked
)
}
// Location Input Section
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.fillMaxWidth()
.padding(
start = 20.dp,
end = 20.dp,
bottom = 30.dp,
top = if (showHeader) 80.dp else 0.dp
)
) {
if (showHeader) Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()
) {
Text("Location", style = TextStyle(fontSize = 14.sp, color = Color(0xFF1C1C1E)))
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_asterisk),
contentDescription = "Location Image",
colorFilter = ColorFilter.tint(Color.Red),
modifier = Modifier
.padding(start = 0.dp)
.size(7.5.dp)
)
}
Column {
TextField(
singleLine = true,
value = searchFieldValue,
onValueChange = { newValue ->
onSearchFieldValueChange(newValue)
if (newValue.isNotEmpty()) {
onLocationFieldChanged(newValue)
}
},
modifier = Modifier
.fillMaxWidth()
.clip(
RoundedCornerShape(10.dp)
)
.background(Color.White),
shape = RoundedCornerShape(10.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.White,
unfocusedContainerColor = Color.White,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
textStyle = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
color = Color(0xFF1C1C1E)
),
leadingIcon = {
Box(modifier = Modifier.padding(5.dp)) {
Text(
text = "to", style = TextStyle(
fontSize = 18.sp, color = Color(0xFF1C1C1E)
)
)
}
},
trailingIcon = {
AnimatedVisibility(isLocationLoading) {
Loader(modifier = Modifier.size(50.dp))
}
}
)
/* var dropdownOffsetY = remember { mutableStateOf(0f) }
HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color(0xFFB7B7B7))
.onGloballyPositioned { coordinates ->
dropdownOffsetY.value = coordinates.positionInWindow().y
}
)*/
isDropdownVisible.value = locationsPredicted.isNotEmpty()
DropdownAlwaysDropDown(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
isDropdownVisible = isDropdownVisible,
items = locationsPredicted,
currentSearchText = searchFieldValue,
onLocationSelected = { selectedLocation ->
isDropdownVisible.value = false // Hide dropdown first
onSearchFieldValueChange(selectedLocation.getPrimaryText(null).toString()) // Update search text field
onLocationSelected(selectedLocation) // Propagate the selection to the parent
}
)
}
}
}
}
@Composable
fun MapItem(
item: AutocompletePrediction,
startHint: String, // Changed from MutableState<String> to String
onclick: () -> Unit = {}
) {
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
onclick()
}
.padding(10.dp)) {
val annotatedText = buildAnnotatedString {
// Use startHint directly
if (item.getPrimaryText(null).toString().startsWith(startHint, true)) {
// Make the startHint part semi-bold
withStyle(
style = SpanStyle(
fontWeight = FontWeight.SemiBold
)
) {
append(startHint)
}
// Append the rest of the text in normal style
append(
item.getPrimaryText(null).toString().lowercase(Locale.getDefault())
.removePrefix(startHint.lowercase(Locale.getDefault()))
)
} else {
// No style change, just display the text as is
append(item.getPrimaryText(null).toString())
}
}
Text(
text = annotatedText, style = TextStyle(color = Color(0xFF1C1C1E), fontSize = 18.sp)
)
}
}
@Composable
fun DropdownAlwaysDropDown(
modifier: Modifier = Modifier,
isDropdownVisible: MutableState<Boolean>,
items: MutableList<AutocompletePrediction>,
currentSearchText: String, // Changed from textState
onLocationSelected: (AutocompletePrediction) -> Unit
) {
AnimatedVisibility(
visible = isDropdownVisible.value,
enter = fadeIn() + expandVertically(), // Fade-in and expand from the top
exit = fadeOut() + shrinkVertically() // Fade-out and shrink to the top
) {
// DropdownMenu positioned explicitly downward
Card(
colors = CardDefaults.cardColors(
containerColor = Color.White
), shape = RoundedCornerShape(bottomEnd = 10.dp, bottomStart = 10.dp)
) {
if (items.isNotEmpty()) {
LazyColumn(modifier = Modifier.height((50 * items.size).dp)) {
itemsIndexed(items) { index, item ->
MapItem(item = item, startHint = currentSearchText, onclick = {
// isDropdownVisible.value = false // Removed from here
onLocationSelected(item) // This now calls the lambda defined in GoogleMapWithLocationSearch
})
}
}
}
// Dropdown Items
}
}
}
@Composable
fun GoogleMapScreen(
modifier: Modifier,
isMapMoving: MutableState<Boolean>,
newLocation: Place?,
// textState: MutableState<String>, // Removed
items: MutableList<AutocompletePrediction>, // This seems for map markers, not directly for search text
onMapLatLngClicked: (LatLng) -> Unit
) {
// Define a default location (latitude and longitude)
val defaultLocation = LatLng(37.7749, -122.4194) // San Francisco, for example
val coroutineScope = rememberCoroutineScope()
// Create the camera position state and initially center it on the default location
val cameraPositionState = remember {
CameraPositionState(
position = CameraPosition.fromLatLngZoom(
defaultLocation, 10f
)
)
}
// Use rememberMarkerState for the marker position - this will update the marker on the map
val markerState = rememberMarkerState(position = defaultLocation)
// Animate camera to the new location
LaunchedEffect(newLocation) {
newLocation?.let {
items.clear()
coroutineScope.launch {
cameraPositionState.animate(
CameraUpdateFactory.newLatLngZoom(it.latLng, 12f)
)
// Update the marker state position
markerState.position = it.latLng
}
}
}
fun Modifier.onPointerInteractionStartEnd(
onPointerStart: () -> Unit,
onPointerEnd: () -> Unit,
) = pointerInput(onPointerStart, onPointerEnd) {
awaitEachGesture {
awaitFirstDown(requireUnconsumed = false)
onPointerStart()
do {
val event = awaitPointerEvent()
} while (event.changes.any { it.pressed })
onPointerEnd()
}
}
// Display Google Map
GoogleMap(
modifier = modifier.onPointerInteractionStartEnd(
onPointerStart = {
isMapMoving.value = true
},
onPointerEnd = {
isMapMoving.value = false
}
),
cameraPositionState = cameraPositionState,
uiSettings = MapUiSettings(
zoomControlsEnabled = true,
scrollGesturesEnabled = true,
scrollGesturesEnabledDuringRotateOrZoom = true
),
onMapClick = { latLng ->
// Update the marker state position when map is clicked
markerState.position = latLng
onMapLatLngClicked(latLng)
}
) {
// Use the markerState instead of creating a new Marker each time
Marker(state = markerState)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\home\notifications\search\filter\Status.kt
```kt
package com.divadventure.ui.screens.main.home.notifications.search.filter
import androidx.compose.foundation.background
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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.ui.ApplyButton
import com.divadventure.ui.HeaderWithCloseButton
import com.divadventure.ui.screens.main.home.notifications.search.sortby.SortOptions
import com.divadventure.viewmodel.HomeIntent
import com.divadventure.viewmodel.HomeViewModel
import com.divadventure.viewmodel.MainViewModel
@Composable
fun Status(
paddingValues: PaddingValues,
navigationViewModel: NavigationViewModel,
mainViewModel: MainViewModel,
homeViewModel: HomeViewModel
) {
var state = homeViewModel.state
val sortOptions = listOf("Past", "Active", "Upcoming") // List of options
var selectedSort by remember { mutableStateOf(state.value.filters.state) }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
Column(
verticalArrangement = Arrangement.spacedBy(20.dp),
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.background(Color(0xFFF2F2F7))
) {
HeaderWithCloseButton(title = "Status") {
navigationViewModel.navigate(NavigationEvent.PopBackStack)
}
// Pass the list of options to SortOptions
SortOptions(
options = sortOptions,
selectedSort = selectedSort ?: "",
onSortSelected = { selectedSort = it }
)
ApplyButton(onClick = {
homeViewModel.sendIntent(HomeIntent.SetStatus(selectedSort))
navigationViewModel.navigate(NavigationEvent.PopBackStack)
})
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\home\notifications\search\Search.kt
```kt
package com.divadventure.ui.screens.main.home.notifications.search
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import com.divadventure.R
import com.divadventure.ui.BackCompose
import com.divadventure.ui.SearchField
import com.divadventure.viewmodel.HomeIntent
import com.divadventure.viewmodel.HomeViewModel
import com.divadventure.viewmodel.MainIntent
import com.divadventure.viewmodel.MainViewModel
// یک کامپوز برای جستجو بین دویدادها و همچنین فیلتر کردن و مرتب کردن به ترتیب دلخواه است
@Composable
fun Search(mainViewModel: MainViewModel, homeViewModel: HomeViewModel, padding: PaddingValues) {
var queryText by remember { mutableStateOf("") }
Column(
modifier = Modifier
.background(color = Color.White)
.padding(top = padding.calculateTopPadding())
) {
BackCompose("Search") {
homeViewModel.sendIntent(HomeIntent.SwitchShowSearchbar)
}
SearchField(queryText = queryText, onQueryChanged = {
queryText = it
homeViewModel.sendIntent(HomeIntent.LoadAdventuresData(queryText))
})
Row(
horizontalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.padding(20.dp)
) {
Row(
modifier = Modifier.clickable {
mainViewModel.sendIntent(MainIntent.GotoFilter)
},
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
modifier = Modifier.size(25.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
getFilterColor(
homeViewModel
)
),
painter = painterResource(id = R.drawable.ic_filter),
contentDescription = "Placeholder image"
)
Text("Filter", style = TextStyle(color = Color(0xFF848484)))
}
Row(
modifier = Modifier.clickable {
mainViewModel.sendIntent(MainIntent.GotoSortBy)
},
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
modifier = Modifier.size(25.dp),
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
getSortByColor(
homeViewModel
)
),
painter = painterResource(id = R.drawable.ic_sort_by),
contentDescription = "Placeholder image"
)
Text("Sort by", style = TextStyle(color = Color(0xFF848484)))
}
}
}
}
fun getFilterColor(homeViewModel: HomeViewModel): Color {
val state = homeViewModel.state.value
if (state.filters.state.isNullOrEmpty() &&
state.filters.startDate.isNullOrEmpty() &&
state.filters.endDate.isNullOrEmpty() &&
state.filters.state.isNullOrEmpty() &&
state.filters.interests.isNullOrEmpty() &&
state.filters.locationLAt == null &&
state.filters.locationLng == null
) {
return Color(0xFF848484)
} else return Color(0xFF30D158)
}
fun getSortByColor(homeViewModel: HomeViewModel): Color {
val state = homeViewModel.state.value
if (state.filters.orderBy.isNullOrEmpty()) {
return Color(0xFF848484)
} else {
return Color(0xFF30D158)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\home\notifications\search\sortby\SortBy.kt
```kt
package com.divadventure.ui.screens.main.home.notifications.search.sortby
import androidx.compose.foundation.background
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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.ui.ApplyButton
import com.divadventure.ui.BackCompose
import com.divadventure.ui.SelectionRow
import com.divadventure.ui.SimpleSelectionImage
import com.divadventure.ui.SortDivider
import com.divadventure.viewmodel.HomeIntent
import com.divadventure.viewmodel.HomeViewModel
import com.divadventure.viewmodel.MainViewModel
@Composable
fun SortBy(
mainViewModel: MainViewModel,
navigationViewModel: NavigationViewModel,
homeViewModel: HomeViewModel,
padding: PaddingValues
) {
var state = homeViewModel.state
val sortOptions = listOf("Popular", "Recent", "Near me") // List of options
var selectedSort by remember { mutableStateOf(state.value.filters.orderBy ?: "") }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
Column(
verticalArrangement = Arrangement.spacedBy(20.dp),
modifier = Modifier
.padding(padding)
.fillMaxSize()
.background(Color(0xFFF2F2F7))
) {
BackCompose(
"Sort by", modifier = Modifier
) {
navigationViewModel.navigate(NavigationEvent.PopBackStack)
}
// Pass the list of options to SortOptions
SortOptions(
options = sortOptions,
selectedSort = selectedSort,
onSortSelected = { selectedSort = it }
)
ApplyButton(onClick = {
homeViewModel.sendIntent(HomeIntent.ApplySortBy(selectedSort))
navigationViewModel.navigate(NavigationEvent.PopBackStack)
})
}
}
}
@Composable
fun SortOptions(
options: List<String>, // List of sort options passed as a parameter
selectedSort: String,
onSortSelected: (String) -> Unit
) {
LazyColumn(
modifier = Modifier.background(color = Color.White),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(options.size) { index ->
val sortOption = options[index]
// Sort Option
SelectionRow(
text = sortOption,
isSelected = selectedSort == sortOption,
selectionImage = { isSelected -> SimpleSelectionImage(isSelected) },
onClick = { onSortSelected(sortOption) }
)
// Add a divider between items, but not after the last item
if (index < options.size - 1) {
SortDivider()
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\Main.kt
```kt
package com.divadventure.ui.screens.main.home
import android.app.Activity
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
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.fillMaxHeight
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.shape.CircleShape
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.divadventure.R
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.data.navigation.rememberCustomNavigationStackManager
import com.divadventure.ui.screens.main.add.AddOrEditAdventure
import com.divadventure.ui.screens.main.profile.MyProfile
import com.divadventure.ui.theme.SystemUIManager
import com.divadventure.viewmodel.HomeViewModel
import com.divadventure.viewmodel.MainViewModel
import com.divadventure.viewmodel.ManageAdventureViewModel
import com.divadventure.viewmodel.ProfileViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
/*
* این یک برنامه یشبکه اجتماعی است که افراد می توانند ماجراجویی ها و سفرهای خود را به اشتراک بگذراند و مثل اینستگرام می توانند تا افرادی یا ماجراجویی هایی را دنبال کنند، همچنین امکان ایجاد سفرها یا ماجراجویی های جدیدی نیز وجود دارد.
* */
// View mode options for displaying content
@Composable
fun MainScreen(
homeViewModel: HomeViewModel,
mainViewModel: MainViewModel,
adventureViewModel: ManageAdventureViewModel,
navigationViewModel: NavigationViewModel,
padding: PaddingValues,
) {
val navController = rememberNavController()
val context = LocalContext.current
val scope = rememberCoroutineScope()
var backPressedTime by remember { mutableStateOf(0L) } // Use 'by' for delegation
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
statusBarColor = Color.Transparent,
navigationBarColor = Color.Transparent,
false,
false,
onSystemBarsVisibilityChange = { isVisible1: Boolean, isVisible2: Boolean ->
})
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val currentScreenTitle = when (currentDestination?.route) {
BottomNavItem.Home.route -> BottomNavItem.Home.label
BottomNavItem.Add.route -> BottomNavItem.Add.label
BottomNavItem.Profile.route -> BottomNavItem.Profile.label
else -> "Unknown"
}
val customNavigationStackManager = rememberCustomNavigationStackManager(navController)
navigationViewModel.setNavigationManager(customNavigationStackManager)
val lifecycleOwner = LocalLifecycleOwner.current
val navigationEvent by navigationViewModel.navigationEvent.collectAsStateWithLifecycle(
lifecycleOwner
)
val mainViewModel = hiltViewModel<MainViewModel>()
BackHandler(enabled = true) { // Enable the BackHandler
if (System.currentTimeMillis() - backPressedTime < 2000) { // 2 seconds threshold
// If second back press is within 2 seconds, finish the activity
(context as? Activity)?.finish()
} else {
// Otherwise, show a toast and update the back press time
scope.launch {
Toast.makeText(context, "Press back again to exit", Toast.LENGTH_SHORT).show()
}
backPressedTime = System.currentTimeMillis()
}
}
Scaffold(bottomBar = {
BottomNavigationBar(
navController = navController,
navigationViewModel,
mainViewModel = mainViewModel,
padding
)
}, content = { innerPadding ->
NavHost(
navController = navController,
startDestination = BottomNavItem.Home.route,
) {
composable(BottomNavItem.Home.route) {
Home(
mainViewModel,
homeViewModel,
navigationViewModel,
navController,
padding
) // Your Home page content
}
composable(BottomNavItem.Profile.route) {
MyProfile(
navigationViewModel,
padding,
hiltViewModel<MainViewModel>(),
hiltViewModel<ProfileViewModel>(),
) // Your Profile page content
}
composable(BottomNavItem.Add.route) {
AddOrEditAdventure(
padding,
mainViewModel,
adventureViewModel,
navigationViewModel
) // Your Notifications page content
}
}
})
}
sealed class BottomNavItem(val route: String, val label: String, val icon: Int) {
object Add : BottomNavItem("add", "", R.drawable.ic_add)
object Profile : BottomNavItem("profile", "Profile", R.drawable.ic_person_svg)
object Home : BottomNavItem("home", "Home", R.drawable.ic_home)
}
@Composable
fun BottomNavigationBar(
navController: NavHostController,
navigationViewModel: NavigationViewModel,
mainViewModel: MainViewModel,
padding: PaddingValues
) {
val navItems = listOf(
BottomNavItem.Home, BottomNavItem.Add, BottomNavItem.Profile
)
Column(modifier = Modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFF30D158))
.height(2.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth(1f / 3f)
.height(2.dp)
.align(Alignment.TopCenter)
// .offset(x = -(1 / 3f).dp)
.background(Color.White)
)
}/* این کامپوز شامل نویگیشین اصلی برنامه است که امکان جایبجایی بین قسمت های اصلی برنامه را می دهد */
BottomAppBar(
modifier = Modifier.height(56.dp + padding.calculateBottomPadding()), // Decreased height
containerColor = Color(0xFF30D158), contentColor = Color.White,
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
navItems.forEachIndexed { index, item ->
val selected = currentDestination?.hierarchy?.any { it.route == item.route } == true
NavigationBarItem(
colors = NavigationBarItemDefaults.colors(
indicatorColor = Color.Transparent
),
icon = {
NavigationItem(
item = item, index = index
)
},
alwaysShowLabel = true,
selected = selected,
onClick = {
Timber.d("Item selected: ${item.label}")
//mainViewModel.sendIntent(MainIntent.clearAllShared)
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
)
}
}
}
}
@Composable
fun NavigationItem(
item: BottomNavItem, index: Int, modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxHeight(), contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.align(Alignment.Center),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
val iconSize = if (index == 1) 40.dp else 30.dp
val iconShape = if (index == 1) CircleShape else RectangleShape
val iconBackground =
if (index == 1) Color.White.copy(alpha = 0.28f) else Color.Transparent
val iconPadding = if (index == 1) 10.dp else 5.dp
Icon(
imageVector = ImageVector.vectorResource(item.icon),
contentDescription = item.label,
tint = Color.White,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(iconSize)
.clip(iconShape)
.background(iconBackground)
.padding(iconPadding)
)
if (index != 1) {
Text(
text = item.label,
textAlign = TextAlign.Center,
style = TextStyle(fontSize = 14.sp, color = Color.White)
)
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\profile\AccountSettings.kt
```kt
package com.divadventure.ui.screens.main.profile
import androidx.compose.foundation.background
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
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.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
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.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.divadventure.R
import com.divadventure.ui.BackCompose
import com.divadventure.ui.ItemTextClickIcon
import com.divadventure.ui.PersonalInfoTextField
import com.divadventure.ui.SimpleTextField
import com.divadventure.ui.TitleCompose
import com.google.maps.android.compose.GoogleMap
@Composable
fun AccountSettings(
paddingValues: PaddingValues
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(paddingValues)
.background(Color(0xFFEFEFF4))
) {
var userName by remember { mutableStateOf("USERNAME") }
var email by remember { mutableStateOf("EMAIL") }
var firstName by remember { mutableStateOf("Hasan") }
var lastName by remember { mutableStateOf("ALi") }
var birthDate by remember { mutableStateOf("Your Birth Date") }
var bio by remember { mutableStateOf("bio bio bio") }
var address by remember { mutableStateOf("Your Address") }
val scrollState = rememberScrollState()
Column(
modifier = Modifier.verticalScroll(scrollState) // Enable scrolling
) {
BackCompose(text = "Account Settings") {}
Card(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(100.dp),
shape = CircleShape
) {}
TitleCompose("Contact Info")
Column(
modifier = Modifier
.padding(horizontal = 20.dp)
.fillMaxWidth()
.background(Color.White, RoundedCornerShape(8.dp))
) {
SimpleTextField(
value = userName,
onValueChange = {
userName = it
},
)
HorizontalDivider(
color = Color(0x3C3C435C).copy(alpha = 0.36f),
modifier = Modifier
.height(1.dp)
.fillMaxWidth()
.background(Color(0x3C3C435C))
)
SimpleTextField(
value = email,
onValueChange = {
email = it
},
)
}
TitleCompose("Personal Info")
Column(
modifier = Modifier
.padding(horizontal = 20.dp)
.fillMaxWidth()
.background(Color.White, RoundedCornerShape(10.dp))
) {
PersonalInfoTextField(
title = "First Name", value = firstName, {
firstName = it
}, Modifier.fillMaxWidth()
)
HorizontalDivider(
color = Color(0x3C3C435C).copy(alpha = 0.36f),
modifier = Modifier
.height(1.dp)
.padding(start = 20.dp)
.fillMaxWidth()
.background(Color(0x3C3C435C))
)
PersonalInfoTextField(
title = "Last Name", value = lastName, {
lastName = it
}, Modifier.fillMaxWidth()
)
HorizontalDivider(
color = Color(0x3C3C435C).copy(alpha = 0.36f),
modifier = Modifier
.height(1.dp)
.padding(start = 20.dp)
.fillMaxWidth()
.background(Color(0x3C3C435C).copy(alpha = 0.36f))
)
PersonalInfoTextField(
title = "Birth Date",
value = birthDate,
onValueChange = { birthDate = it },
modifier = Modifier.fillMaxWidth(),
)
}
Column(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp, top = 20.dp)
.fillMaxWidth()
.background(Color.White, RoundedCornerShape(10.dp))
) {
PersonalInfoTextField(
title = "Biography", value = "Bio", onValueChange = {
bio = it
}, modifier = Modifier.fillMaxWidth(), minLines = 5
)
}
Column(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp, top = 20.dp)
.fillMaxWidth()
.background(Color.White, RoundedCornerShape(10.dp))
) {
ObligatoryInterestsComposable("Interests")
}
TitleCompose("Location")
Column(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp)
.fillMaxWidth()
.background(Color.White, RoundedCornerShape(10.dp))
) {
SimpleTextField(
value = address,
onValueChange = { address = it },
modifier = Modifier,
descLines = 1
)
}
GoogleMap(
Modifier
.padding(top = 20.dp)
.fillMaxSize()
.aspectRatio(1f),
)
Column(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 30.dp)
.fillMaxWidth()
.background(Color.White, RoundedCornerShape(8.dp))
) {
ChangePassword()
HorizontalDivider(
color = Color(0x3C3C435C).copy(alpha = 0.36f),
modifier = Modifier
.height(1.dp)
.fillMaxWidth()
.background(Color(0x3C3C435C))
)
DeleteAccount()
}
}
}
}
@Composable
fun DeleteAccount() {
TextButton(onClick = {}) {
Text(
fontSize = with(LocalDensity.current) { 13.dp.toSp() },
text = "Delete Account",
style = TextStyle(color = Color.Black, fontWeight = FontWeight.W400)
)
}
}
@Composable
fun ChangePassword() {
TextButton(onClick = {}) {
Text(
text = "Delete Account",
fontSize = with(LocalDensity.current) { 13.dp.toSp() },
style = TextStyle(color = Color(0xFFFF0000), fontWeight = FontWeight.W400)
)
}
}
@Composable
fun ObligatoryInterestsComposable(title: String) {
ItemTextClickIcon(
title = title, content = {
Row(modifier = Modifier.padding()) {
Text("Label", style = TextStyle(color = Color(0x3C3C4399).copy(alpha = 0.6f)))
Icon(
modifier = Modifier.padding(start = 10.dp),
painter = painterResource(id = R.drawable.right_chevron),
tint = Color(0x3C3C4399).copy(alpha = 0.6f),
contentDescription = ""
)
}
})
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\profile\NotificationsSettings.kt
```kt
package com.divadventure.ui.screens.main.profile
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.divadventure.ui.BackCompose
import com.divadventure.ui.screens.main.home.notifications.search.sortby.SortOptions
@Composable
fun NotificationsSettings(padding: PaddingValues) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFEFEFF4))
.padding(padding)
) {
val sortOptions = listOf("Email", "Application") // List of options
var selectedSort by remember { mutableStateOf(sortOptions[0]) }
val parentScrollState = rememberLazyListState()
val childScrollState = rememberLazyListState()
Column {
BackCompose(
"Notification", modifier = Modifier
) {
}
// Pass the list of options to SortOptions
SortOptions(
options = sortOptions,
selectedSort = selectedSort,
onSortSelected = { selectedSort = it }
)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\profile\PrivacySettings.kt
```kt
package com.divadventure.ui.screens.main.profile
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.divadventure.ui.BackCompose
import com.divadventure.ui.SelectionRow
import com.divadventure.ui.SimpleSelectionImage
import com.divadventure.ui.SortDivider
@Composable
fun PrivacySettings(padding: PaddingValues) {
val options = listOf("Only Me", "Friends Only", "Public")
val titles = listOf("Bio", "Location", "Birthday", "Adventures", "Friends List")
var selections = remember { mutableStateListOf<String>("", "", "", "", "") }
Box(
modifier = Modifier
.background(Color.White)
.padding(padding)
.background(Color(0xFFEFEFF4))
.fillMaxSize()
) {
Column(modifier = Modifier.fillMaxSize()) {
// Back button at the top
BackCompose(
"Privacy Settings", modifier = Modifier
) {}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
// Implement custom scroll handling if needed
}
}
// LazyColumn for displaying titles and options with constrained height
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
// Occupy remaining space in the Column
) {
itemsIndexed(titles) { index, title ->
TripleOptions(
title,
option1 = options[0],
option2 = options[1],
option3 = options[2],
selectedOption = selections[index],
onOptionSelected = { selected ->
selections[index] = (selected)
}
)
}
}
}
}
}
@Composable
fun TripleOptions(
title: String,
option1: String,
option2: String,
option3: String,
selectedOption: String,
onOptionSelected: (String) -> Unit
) {
Column {
Text(
modifier = Modifier.padding(start = 20.dp),
text = title, style = androidx.compose.ui.text.TextStyle(
color = Color.Black,
fontSize = with(LocalDensity.current) { 14.dp.toSp() },
fontWeight = FontWeight.W300
)
)
Column(
modifier = Modifier
.padding(20.dp)
.background(color = Color.White, shape = RoundedCornerShape(10.dp))
// Optional global padding for the column
) {
// First Option
SelectionRow(
text = option1,
isSelected = selectedOption == option1,
selectionImage = { isSelected -> SimpleSelectionImage(isSelected) },
onClick = { onOptionSelected(option1) }
)
SortDivider() // Divider after the first item
// Second Option
SelectionRow(
text = option2,
isSelected = selectedOption == option2,
selectionImage = { isSelected -> SimpleSelectionImage(isSelected) },
onClick = { onOptionSelected(option2) }
)
SortDivider() // Divider after the second item
// Third Option
SelectionRow(
text = option3,
isSelected = selectedOption == option3,
selectionImage = { isSelected -> SimpleSelectionImage(isSelected) },
onClick = { onOptionSelected(option3) }
)
// No divider after the third item (last one)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\main\profile\Profile.kt
```kt
package com.divadventure.ui.screens.main.profile
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.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.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
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.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
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 coil.compose.rememberAsyncImagePainter
import com.divadventure.R
import com.divadventure.data.navigation.NavigationEvent
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.domain.models.Adventurer
import com.divadventure.domain.models.Friend
import com.divadventure.ui.BackComposeMoreButton
import com.divadventure.ui.ControlButton
import com.divadventure.ui.Friends
import com.divadventure.ui.TabbedProfileSwitcher
import com.divadventure.ui.screens.main.home.AdventuresList
import com.divadventure.ui.screens.main.home.BinarySwitcher
import com.divadventure.ui.screens.main.home.SelectableCalendar
import com.divadventure.ui.screens.main.home.notifications.BottomSheetContent
import com.divadventure.ui.screens.main.home.notifications.GeneralBottomSheet
import com.divadventure.ui.screens.main.home.showTypes
import com.divadventure.viewmodel.MainIntent
import com.divadventure.viewmodel.MainUiEvent
import com.divadventure.viewmodel.MainViewModel
import com.divadventure.viewmodel.ProfileIntent
import com.divadventure.viewmodel.ProfileUIEvent
import com.divadventure.viewmodel.ProfileViewModel
import com.kizitonwose.calendar.compose.rememberCalendarState
import java.time.YearMonth
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyProfile(
navigationViewModel: NavigationViewModel,
paddings: PaddingValues = PaddingValues(),
mainViewModel: MainViewModel,
profileViewModel: ProfileViewModel,
) {
LaunchedEffect(true) {
profileViewModel.sendIntent(ProfileIntent.LoadMyFriends)
profileViewModel.sendIntent(ProfileIntent.LoadMyUserData)
}
var showBottomSheet by remember { mutableStateOf(false) }
LaunchedEffect(key1 = true) {
profileViewModel.uiEvent.collect { event ->
when (event) {
ProfileUIEvent.AnimateItem -> {}
is ProfileUIEvent.NavigateToNextScreen -> {}
is ProfileUIEvent.ShowBottomSheet -> {
showBottomSheet = true
}
ProfileUIEvent.ShowDialog -> {}
is ProfileUIEvent.ShowDim -> {}
is ProfileUIEvent.ShowSnackbar -> {}
}
}
}
LaunchedEffect(key1 = true) {
mainViewModel.uiEvent.collect { event ->
when (event) {
MainUiEvent.AnimateItem -> {}
is MainUiEvent.NavigateToNextScreen -> {
navigationViewModel.navigate(event.navigationEvent)
}
MainUiEvent.ShowDialog -> {}
is MainUiEvent.ShowDim -> {}
is MainUiEvent.ShowSnackbar -> {}
is MainUiEvent.AdventureAction -> {
}
}
}
}
// Get the Back Press Dispatcher
val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
// Register the Back Press Callback
DisposableEffect(backPressedDispatcher) {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Trigger PopSpecific to navigate back without additional NavigateTo call
profileViewModel.navigate(
NavigationEvent.PopBackStack
)
}
}
backPressedDispatcher?.addCallback(callback)
// Cleanup callback
onDispose {
callback.remove()
}
}
if (showBottomSheet) {
val options = listOf(
"Account Settings", "Notification Settings", "Privacy Settings", "Logout"
)
GeneralBottomSheet(
Modifier.offset(y = -paddings.calculateBottomPadding()),
showBottomSheet = showBottomSheet,
onDismissRequest = { showBottomSheet = false },
content = {
BottomSheetContent(options = options, onOptionClick = { option ->
// Handle option click (yes, maybe, no)
when (option) {
options[0] -> {
mainViewModel.sendIntent(MainIntent.GoAccountSettings)
}
options[1] -> {
mainViewModel.sendIntent(MainIntent.GoNotificationsSettings)
}
options[2] -> {
mainViewModel.sendIntent(MainIntent.GoPrivacySettings)
}
options[3] -> {
}
}
println("Option selected: $option")
showBottomSheet = false
}, onCancelClick = {
// Handle cancel click
println("Cancel clicked")
showBottomSheet = false
}, optionStyles = List(4) {
TextStyle(
color = Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontSize = with(LocalDensity.current) { 14.dp.toSp() })
})
})
}
ProfileContent(
paddings = paddings,
profileViewModel = profileViewModel,
mainViewModel = mainViewModel,
"Account Settings",
adventuresContent = { showTypes, selectedShowType, onSelectOption ->
MyAdventureContent(
showTypes = showTypes,
profileViewModel,
mainViewModel,
selectedShowType = selectedShowType,
onSelectOption = onSelectOption
)
},
friendsContent = {
FriendsContent(profileViewModel)
})
}
@Composable
fun ElseProfile(
paddings: PaddingValues = PaddingValues(),
mainViewModel: MainViewModel,
profileId: String,
profileViewModel: ProfileViewModel,
) {
val profileState = profileViewModel.state.collectAsState().value
LaunchedEffect(true) {
profileViewModel.sendIntent(ProfileIntent.CheckId(profileId))
profileViewModel.sendIntent(ProfileIntent.LoadElseFriends(profileId))
profileViewModel.sendIntent(ProfileIntent.LoadElseProfileData(profileId))
}
ProfileContent(
paddings = paddings,
profileViewModel = profileViewModel,
mainViewModel = mainViewModel,
buttonTitle = if (profileState.statusWithUser.equals("friends", true)
) "Remove Friend" else "Add Friend",
adventuresContent = { showTypes, selectedShowType, onSelectOption ->
ElseAdventureContent(
showTypes = showTypes,
profileViewModel,
mainViewModel,
selectedShowType = selectedShowType,
onSelectOption = onSelectOption
)
},
friendsContent = {
FriendsContent(profileViewModel)
})
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProfileContent(
paddings: PaddingValues,
profileViewModel: ProfileViewModel,
mainViewModel: MainViewModel,
buttonTitle: String,
adventuresContent: @Composable (List<String>, String, (String) -> Unit) -> Unit,
friendsContent: @Composable () -> Unit,
) {
val showTypes = showTypes
var selectedShowType by remember { mutableStateOf(showTypes.first()) }
// Dispatcher and connection to enable nested scrolling
val parentNestedScrollDispatcher = remember { NestedScrollDispatcher() }
val parentNestedScrollConnection = remember { object : NestedScrollConnection {} }
// Main Profile Content Layout
Box(
modifier = Modifier
.background(Color.White)
.fillMaxSize()
.padding(paddings)
) {
var showBottomSheet by remember { mutableStateOf(false) }
// Create scroll states
val columnScrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
val verticalScroll = rememberScrollState()
// Observe scroll position of the ScrollState for the Column
/* LaunchedEffect(columnScrollState.value) {
// Sync the LazyColumn scroll positions
coroutineScope.launch {
lazyListState1.scrollToItem(
columnScrollState.value / 100 // Adjust to match offset scales
)
lazyListState2.scrollToItem(
columnScrollState.value / 100
)
}
}*/
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item {
BackComposeMoreButton(onBack = {}, onMore = {
profileViewModel.sendIntent(ProfileIntent.OpenProfileBottomSheet)
})
}
item {
ImageProfile(profileViewModel)
}
item {
ControlButton(
text = buttonTitle, onClick = {
})
}
item {
Characteristics(profileViewModel)
}
// Tabbed Profile Switcher
item {
TabbedProfileSwitcher(adventuresContent = {
adventuresContent(
showTypes, selectedShowType
) { selectedOption ->
selectedShowType = selectedOption
}
}, friendsContent = {
friendsContent()
})
}
}
}
}
@Composable
fun ElseAdventureContent(
showTypes: List<String>,
profileViewModel: ProfileViewModel,
mainViewModel: MainViewModel,
selectedShowType: String,
onSelectOption: (String) -> Unit,
) {
val context = LocalContext.current
BinarySwitcher(
modifier = Modifier,
options = showTypes,
selectedOption = selectedShowType,
onSelectOption = { selectedOption ->
onSelectOption(selectedOption)
Toast.makeText(context, "Option selected: $selectedOption", Toast.LENGTH_SHORT).show()
},
calendarContent = {
SelectableCalendar(
PaddingValues(),
false,
adventuresList = profileViewModel.state.collectAsState().value.adventuresList,
isAdventuresLoading = false
)
},
listContent = {
var onClickItem: ((Adventurer) -> Unit) = { adventurer ->
mainViewModel.sendIntent(MainIntent.GotoProfile(adventurer.id))
}
AdventuresList(viewModel = profileViewModel, mainViewModel, onClickItem, false)
LaunchedEffect(true) {
profileViewModel.sendIntent(ProfileIntent.LoadElseAdventures)
}
})
}
@Composable
fun MyAdventureContent(
showTypes: List<String>,
profileViewModel: ProfileViewModel,
mainViewModel: MainViewModel,
selectedShowType: String,
onSelectOption: (String) -> Unit,
) {
val context = LocalContext.current
val calendarState = rememberCalendarState(
firstVisibleMonth = YearMonth.now(),
startMonth = YearMonth.now().minusYears(50),
endMonth = YearMonth.now().plusMonths(50)
)
var currentMonth by remember {
mutableStateOf(
calendarState.layoutInfo.visibleMonthsInfo.maxByOrNull { it.size }?.month?.yearMonth
)
}
val totalAdventures by remember(profileViewModel.state.value.adventuresList, currentMonth) {
derivedStateOf {
profileViewModel.state.value.adventuresList.filter {
it.startsAt.split("-")[0] == currentMonth?.year.toString() &&
it.startsAt.split("-")[1] == currentMonth?.month?.value.toString()
}.toMutableStateList()
}
}
BinarySwitcher(
modifier = Modifier,
options = showTypes,
selectedOption = selectedShowType,
onSelectOption = { selectedOption ->
onSelectOption(selectedOption)
Toast.makeText(context, "Option selected: $selectedOption", Toast.LENGTH_SHORT).show()
},
calendarContent = {
SelectableCalendar(
PaddingValues(),
false,
adventuresList = totalAdventures,
onChangeMonth = { yearMonth ->
currentMonth = yearMonth
},
isAdventuresLoading = false
)
},
listContent = {
var onClickItem: ((Adventurer) -> Unit) = { adventurer ->
mainViewModel.sendIntent(MainIntent.GotoProfile(adventurer.id))
}
AdventuresList(viewModel = profileViewModel, mainViewModel, onClickItem, false)
LaunchedEffect(true) {
profileViewModel.sendIntent(ProfileIntent.LoadMyAdventures)
}
})
}
@Composable
fun FriendsContent(viewModel: ProfileViewModel) {
var friendsList = remember { mutableStateListOf<Friend>() }
/*
if (viewModel is ProfileViewModel) {
friendsList = viewModel.state.collectAsState().value.friends.toMutableStateList()
} else if (viewModel is MainViewModel) {
friendsList = viewModel.state.collectAsState().value.friends.toMutableStateList()
}
*/
friendsList = viewModel.state.collectAsState().value.friends.toMutableStateList()
Friends(
friends = friendsList
)
}
@Composable
fun ImageProfile(profileViewModel: ProfileViewModel) {
val state = profileViewModel.state.collectAsState().value
Column(
modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
contentScale = ContentScale.Crop,
painter = rememberAsyncImagePainter(R.drawable.random_image_5),
contentDescription = "Profile Image",
modifier = Modifier
.size(150.dp)
.clip(CircleShape)
.background(Color.Gray, CircleShape)
.align(Alignment.CenterHorizontally)
)
Text(
modifier = Modifier.padding(5.dp),
text = "${state.firstName} ${state.lastName}",
style = TextStyle(fontWeight = FontWeight.W600),
fontFamily = FontFamily(Font(R.font.sf_pro))
)
Text(
text = "@${state.username}",
style = TextStyle(color = Color(0xFF848484)),
modifier = Modifier.padding(horizontal = 5.dp)
)
}
}
@Composable
fun Characteristics(profileViewModel: ProfileViewModel) {
val state = profileViewModel.state.collectAsState().value
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
horizontalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically
) {
CharacteristicsItem(
modifier = Modifier.align(Alignment.CenterVertically),
imageId = R.drawable.ic_locate,
text = state.location,
)
CharacteristicsItem(
modifier = Modifier.align(Alignment.CenterVertically),
imageId = R.drawable.ic_calendar2, text = state.birthdate
)
}
}
@Composable
fun CharacteristicsItem(modifier: Modifier = Modifier, imageId: Int, text: String) {
Row(
modifier = modifier, horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
Image(
painter = painterResource(id = imageId),
contentDescription = "Characteristic Image",
modifier = Modifier.size(20.dp)
)
Box(
modifier = Modifier.height(20.dp), contentAlignment = Alignment.CenterStart
) {
Text(
text = text,
modifier = Modifier.align(Alignment.Center),
style = TextStyle(color = Color.Black)
)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\Onboarding.kt
```kt
package com.divadventure.ui.screens
import android.os.CountDownTimer
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.divadventure.R
import com.divadventure.ui.AuthTextField
import com.divadventure.ui.TopSnackBar
import com.divadventure.ui.theme.SystemUIManager
import com.divadventure.viewmodel.AuthIntent
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.viewmodel.AuthUiEvent
import kotlinx.coroutines.launch
@Composable
fun OnboardingScreen(
navigationViewModel: NavigationViewModel, viewModel: AuthViewModel, padding: PaddingValues
) {
val state by viewModel.state.collectAsState()
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
Color.Transparent,
Color.Transparent,
hideNavigationBar = !state.navigationBarVisibility,
hideStatusBar = !state.statusBarVisibility,
onSystemBarsVisibilityChange = { statusbar, navigationBar ->
/*
viewModel.sendIntent(
AuthIntent.MutualIntent.ChangeInsetsVisibility(
statusbar, navigationBar
)
)
*/
})
Scaffold(
containerColor = Color.White
) { innerPadding ->
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is AuthUiEvent.ExecuteNavigation -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
// Handle timer and auto-dismiss
LaunchedEffect(showTopSnackBar) {
if (showTopSnackBar) {
timer.value = object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
coroutineScope.launch {
showTopSnackBar = false
}
}
}.start()
} else {
timer.value?.cancel()
}
}
var userName = remember { mutableStateOf("") }
var firstName = remember { mutableStateOf("") }
var lastName = remember { mutableStateOf("") }
var enterButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
when (state.onboardState!!.formDataValid) {
(true) -> {
if (enterButtonColor != Color(0xff30D158)) {
enterButtonColor = Color(0xff30D158)
}
}
(false) -> {
if (enterButtonColor != Color(0xffBFBFBF)) {
enterButtonColor = Color(0xffBFBFBF)
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(0.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(modifier = Modifier.weight(0.3f, true)) {
TopSnackBar(
paddingTop = padding.calculateTopPadding(),
title = topSnackBarTitle,
message = topSnackBarMessage,
show = showTopSnackBar,
onDismiss = { showTopSnackBar = false })
Text(
modifier = Modifier
.align(
Alignment.Center
)
.padding(0.dp, 0.dp), text = "DivAdventure", style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
color = Color(0xff30D158),
)
)
}
Column(
modifier = Modifier.weight(0.4f, true)
) {
AuthTextField(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 40.dp),
hint = "UserName",
text = userName,
onValueChange = {
viewModel.sendIntent(
AuthIntent.OnboardIntent.OnUserNameChanged(
userName = userName.value
)
)
},
explain = "Your user name",
essential = true,
isPassword = false,
isEmail = false,
isNormalText = true,
)
AuthTextField(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 40.dp),
hint = "First Name",
text = firstName,
onValueChange = {
viewModel.sendIntent(
AuthIntent.OnboardIntent.OnFirstNameChanged(
firstName = firstName.value
)
)
},
explain = "Your Name",
essential = true,
isPassword = false,
isEmail = false,
isNormalText = true,
)
AuthTextField(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 40.dp),
hint = "Last Name",
text = lastName,
onValueChange = {
viewModel.sendIntent(
AuthIntent.OnboardIntent.OnLastNameChanged(
lastName = lastName.value
)
)
},
explain = "Your Last name",
essential = true,
isPassword = false,
isEmail = false,
isNormalText = true,
)
}
Box(
modifier = Modifier
.weight(0.3f, true)
.padding(15.dp, 0.dp)
) {
Card(
colors = CardDefaults.cardColors(
containerColor = enterButtonColor,
contentColor = Color.White,
disabledContainerColor = enterButtonColor,
disabledContentColor = Color.White
),
modifier = Modifier
.clickable {
viewModel.sendIntent(
AuthIntent.OnboardIntent.Onboard(
firstName = firstName.value,
lastName = lastName.value,
userName = userName.value
)
)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(50.dp),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Continue"
)
}
// show forms error
Text(state.onboardState!!.error, color = Color.Red)
}
}
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\ResetPassword.kt
```kt
package com.divadventure.ui.screens
import android.os.CountDownTimer
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
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.fillMaxSize
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.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
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.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.divadventure.R
import com.divadventure.ui.AuthTextField
import com.divadventure.ui.CARD_HEIGHT
import com.divadventure.ui.TopSnackBar
import com.divadventure.ui.theme.SystemUIManager
import com.divadventure.viewmodel.AuthIntent
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.viewmodel.AuthUiEvent
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import timber.log.Timber
@Composable
fun ResetPassword(
navigationViewModel: NavigationViewModel, viewModel: AuthViewModel, padding: PaddingValues
) {
val state by viewModel.state.collectAsState()
// Reset Password Screen
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
Color.Transparent,
Color.Transparent,
hideNavigationBar = !state.navigationBarVisibility,
hideStatusBar = !state.statusBarVisibility,
onSystemBarsVisibilityChange = { statusbar, navigationBar ->
/*
viewModel.sendIntent(
AuthIntent.MutualIntent.ChangeInsetsVisibility(
statusbar, navigationBar
)
)
*/
})
val systemUiController = rememberSystemUiController()
val darkTheme = isSystemInDarkTheme()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.White, darkIcons = !darkTheme
)
systemUiController.setNavigationBarColor(
color = Color.White, darkIcons = !darkTheme
)
}
var showError by remember { mutableStateOf(false) }
Timber.d("state: $state")
val email = remember {
mutableStateOf("")
}
val password = remember {
mutableStateOf("")
}
val passwordConfirmation = remember {
mutableStateOf("")
}
var enterButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is AuthUiEvent.ExecuteNavigation -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
// Handle timer and auto-dismiss
LaunchedEffect(showTopSnackBar) {
if (showTopSnackBar) {
timer.value = object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
coroutineScope.launch {
showTopSnackBar = false
}
}
}.start()
} else {
timer.value?.cancel()
}
}
when (state.resetPasswordState!!.formDataValid) {
true -> {
enterButtonColor = Color(0xff30D158)
}
false -> {
enterButtonColor = Color(0xffBFBFBF)
}
}
Box(
modifier = Modifier.background(Color.White)/*
.padding(padding)
*/
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(0.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(modifier = Modifier.weight(0.3f, true)) {
TopSnackBar(
paddingTop = padding.calculateTopPadding(),
title = topSnackBarTitle,
message = topSnackBarMessage,
show = showTopSnackBar,
onDismiss = { showTopSnackBar = false })
Text(
modifier = Modifier
.align(
Alignment.Center
)
.padding(0.dp, 0.dp), text = "DivAdventure", style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
color = Color(0xff30D158),
)
)
}
Column(
modifier = Modifier.weight(0.4f, true)
) {
AuthTextField(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 40.dp),
hint = "Email",
text = email,
enabled = false,
onValueChange = {},
explain = state.resetPasswordState!!.email,
essential = false,
isEmail = true
)
AuthTextField(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 20.dp),
hint = "Password",
text = password,
onValueChange = {
viewModel.sendIntent(
AuthIntent.ResetPasswordIntent.OnPasswordChanged(
password = password.value
)
)
},
explain = "Your Password",
essential = true,
isPassword = true
)
AuthTextField(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp),
hint = "",
text = passwordConfirmation,
onValueChange = {
viewModel.sendIntent(
AuthIntent.ResetPasswordIntent.OnConfirmPasswordChanged(
passwordConfirmation.value
)
)
},
explain = "Confirm Password",
essential = true,
isPassword = true
)
Column(
modifier = Modifier.padding(15.dp, 0.dp, 0.dp, 0.dp),
verticalArrangement = Arrangement.Center
) {
AnimatedVisibility(
!state.resetPasswordState!!.is8Characters &&
state.resetPasswordState!!.startedTyping
) {
Row(
modifier = Modifier.padding(0.dp, 10.dp, 0.dp, 0.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Image(
modifier = Modifier
.size(10.dp)
.padding(0.dp),
contentDescription = "dot",
colorFilter = ColorFilter.tint(Color(0xffEA4335)),
imageVector = ImageVector.vectorResource(
id = R.drawable.ic_dot,
),
)
Text(
"Must be at least 8 characters.",
style = TextStyle(color = Color(0xffEA4335))
)
}
}
AnimatedVisibility(
!state.resetPasswordState!!.passwordsMatch &&
state.resetPasswordState!!.startedTyping
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Image(
modifier = Modifier
.size(10.dp)
.padding(0.dp),
contentDescription = "dot",
colorFilter = ColorFilter.tint(Color(0xffEA4335)),
imageVector = ImageVector.vectorResource(
id = R.drawable.ic_dot,
),
)
Text(
"Both passwords must match.",
style = TextStyle(color = Color(0xffEA4335))
)
}
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(0.3f, true)
.padding(15.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(15.dp, Alignment.Top)
) {
Card(
modifier = Modifier
.clickable {
if (state.resetPasswordState!!.resetClickable) {
viewModel.sendIntent(
AuthIntent.ResetPasswordIntent.UpdatePassword(
password.value, passwordConfirmation.value
)
)
}
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(CARD_HEIGHT),
colors = CardDefaults.cardColors(
containerColor = enterButtonColor,
disabledContainerColor = enterButtonColor,
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Update Password"
)
}
// show forms error
Text(state.resetPasswordState!!.error, color = Color.Red)
}
}
}
if (showError) {
Toast.makeText(
LocalContext.current,
state.resetPasswordState!!.error ?: "An error has occurred!",
Toast.LENGTH_SHORT
).show()
showError = false
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\SignUp.kt
```kt
package com.divadventure.ui.screens
import android.os.CountDownTimer
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
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.fillMaxSize
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.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
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.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.divadventure.R
import com.divadventure.ui.AuthTextField
import com.divadventure.ui.CARD_HEIGHT
import com.divadventure.ui.TopSnackBar
import com.divadventure.ui.theme.SystemUIManager
import com.divadventure.viewmodel.AuthIntent
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.viewmodel.AuthUiEvent
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import timber.log.Timber
@Composable
fun SignUpScreen(
navigationViewModel: NavigationViewModel,
viewModel: AuthViewModel,
padding: PaddingValues
) {
val state by viewModel.state.collectAsState()
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
Color.Transparent,
Color.Transparent,
hideNavigationBar = !state.navigationBarVisibility,
hideStatusBar = !state.statusBarVisibility,
onSystemBarsVisibilityChange = { statusbar, navigationBar ->
/*
viewModel.sendIntent(
AuthIntent.MutualIntent.ChangeInsetsVisibility(
statusbar, navigationBar
)
)
*/
})
val systemUiController = rememberSystemUiController()
val darkTheme = isSystemInDarkTheme()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.White,
darkIcons = !darkTheme
)
systemUiController.setNavigationBarColor(
color = Color.White,
darkIcons = !darkTheme
)
}
var showError by remember { mutableStateOf(false) }
Timber.d("state: $state")
val email = remember {
mutableStateOf("")
}
val password = remember {
mutableStateOf("")
}
val passwordConfirmation = remember {
mutableStateOf("")
}
var enterButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is AuthUiEvent.ExecuteNavigation -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
// Handle timer and auto-dismiss
LaunchedEffect(showTopSnackBar) {
if (showTopSnackBar) {
timer.value = object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
coroutineScope.launch {
showTopSnackBar = false
}
}
}.start()
} else {
timer.value?.cancel()
}
}
when (state.signupState!!.formDataValid) {
true -> {
enterButtonColor = Color(0xff30D158)
}
false -> {
enterButtonColor = Color(0xffBFBFBF)
}
}
Box(
modifier = Modifier
.background(Color.White)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(0.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(modifier = Modifier.weight(0.3f, true)) {
TopSnackBar(
paddingTop = padding.calculateTopPadding(),
title = topSnackBarTitle,
message = topSnackBarMessage,
show = showTopSnackBar,
onDismiss = { showTopSnackBar = false }
)
Text(
modifier = Modifier
.align(
Alignment.Center
)
.padding(0.dp, 0.dp),
text = "DivAdventure",
style = TextStyle(
fontFamily = FontFamily(Font(R.font.sf_pro)),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
fontSize = 32.sp,
color = Color(0xff30D158),
)
)
}
Column(
modifier = Modifier.weight(0.4f, true)
) {
AuthTextField(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 40.dp),
hint = "Email",
text = email,
onValueChange = {
viewModel.sendIntent(
AuthIntent.SignupIntent.OnEmailChanged(
email = email.value
)
)
},
explain = "Your Email",
essential = true,
isEmail = true
)
AuthTextField(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 20.dp),
hint = "Password",
text = password,
onValueChange = {
viewModel.sendIntent(
AuthIntent.SignupIntent.OnPasswordChanged(
password = password.value
)
)
},
explain = "Your Password",
essential = true,
isPassword = true
)
AuthTextField(
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 0.dp),
hint = "",
text = passwordConfirmation,
onValueChange = {
viewModel.sendIntent(
AuthIntent.SignupIntent.OnPasswordConfirmationChanged(
passwordConfirmation = passwordConfirmation.value
)
)
},
explain = "Confirm Password",
essential = true,
isPassword = true
)
Column(
modifier = Modifier.padding(15.dp, 0.dp, 0.dp, 0.dp),
verticalArrangement = Arrangement.Center
) {
AnimatedVisibility(
!state.signupState!!.is8Characters &&
state.signupState!!.startedTyping
) {
Row(
modifier = Modifier.padding(0.dp, 10.dp, 0.dp, 0.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Image(
modifier = Modifier
.size(10.dp)
.padding(0.dp),
contentDescription = "dot",
colorFilter = ColorFilter.tint(Color(0xffEA4335)),
imageVector = ImageVector.vectorResource(
id = R.drawable.ic_dot,
),
)
Text(
"Must be at least 8 characters.",
style = TextStyle(color = Color(0xffEA4335))
)
}
}
AnimatedVisibility(
state.signupState!!.startedTyping && !state.signupState!!.passwordsMatch
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Image(
modifier = Modifier
.size(10.dp)
.padding(0.dp),
contentDescription = "dot",
colorFilter = ColorFilter.tint(Color(0xffEA4335)),
imageVector = ImageVector.vectorResource(
id = R.drawable.ic_dot,
),
)
Text(
"Both passwords must match.",
style = TextStyle(color = Color(0xffEA4335))
)
}
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.weight(0.3f, true)
.padding(15.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(15.dp, Alignment.Top)
) {
Card(
modifier = Modifier
.clickable {
if (state.signupState!!.signupClickable) {
viewModel.sendIntent(
AuthIntent.SignupIntent.SignUp(
email.value, password.value, passwordConfirmation.value
)
)
}
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(CARD_HEIGHT),
colors = CardDefaults.cardColors(
containerColor = enterButtonColor,
disabledContainerColor = enterButtonColor,
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Create Account"
)
}
// show forms error
Text(state.signupState!!.error, color = Color.Red)
}
Card(
modifier = Modifier
.clickable {
viewModel.sendIntent(AuthIntent.SignupIntent.SignUpWithGoogle)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(CARD_HEIGHT),
border = BorderStroke(1.dp, Color(0xffBFBFBF)),
colors = CardDefaults.cardColors(
containerColor = Color.White,
disabledContainerColor = Color.White,
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier.padding(0.dp, 0.dp, 10.dp, 0.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_google),
contentDescription = "google",
)
Text(
modifier = Modifier.padding(
10.dp, 0.dp, 0.dp, 0.dp
), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color.Black
), text = "Sign Up with Google"
)
}
}
}
Card(
modifier = Modifier
.clickable {}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(CARD_HEIGHT),
colors = CardDefaults.cardColors(
containerColor = Color(0xff2553B4),
disabledContainerColor = Color(0xff2553B4),
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier.align(Alignment.Center),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier.padding(0.dp, 0.dp, 10.dp, 0.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_facebook),
contentDescription = "facebook",
alignment = Alignment.BottomCenter
)
Text(
modifier = Modifier.padding(
10.dp, 0.dp, 0.dp, 0.dp
), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color.White
), text = "Sign Up with Facebook"
)
}
}
}
}
}
if (showError) {
Toast.makeText(
LocalContext.current, state.signupState!!.error ?: "An error has occurred!",
Toast.LENGTH_SHORT
).show()
showError = false
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\SplashScreen.kt
```kt
package com.divadventure.ui.screens
// In MainActivity.kt (or your activity)
import android.os.CountDownTimer
import androidx.activity.ComponentActivity
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import coil.compose.rememberAsyncImagePainter
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.divadventure.R
import com.divadventure.viewmodel.AuthIntent
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.viewmodel.AuthUiEvent
import kotlinx.coroutines.launch
/*@Composable
fun Splash(
) {
SplashScreen()
}
@Preview(
showBackground = true, device = Devices.PIXEL
)*/
@Composable
fun SplashScreen(
navigationViewModel: NavigationViewModel,
viewModel: AuthViewModel
) {
// This method will change the color of the status bar to the
// color that you prefer
val view = LocalView.current
val window = (view.context as ComponentActivity).window
window.statusBarColor = Color.Transparent.toArgb()
window.statusBarColor = Color.Transparent.toArgb()
val windowInsetsController = WindowInsetsControllerCompat(window, view)
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is AuthUiEvent.ExecuteNavigation -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
// Handle timer and auto-dismiss
LaunchedEffect(showTopSnackBar) {
if (showTopSnackBar) {
timer.value = object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
coroutineScope.launch {
showTopSnackBar = false
}
}
}.start()
} else {
timer.value?.cancel()
}
}
LaunchedEffect(key1 = true) {
//delay(2000)
viewModel.sendIntent(AuthIntent.SplashIntent.CheckDecision)
}
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxSize()
.background(
color = Color(0xFF30D158)
)
) {
Image(
painter = rememberAsyncImagePainter(model = R.drawable.background),
contentDescription = "Splash Screen",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.FillBounds,
)
Loader(Modifier.align(Alignment.Center))
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(modifier = Modifier.weight(0.5f, true)) {
Text(
modifier = Modifier.padding(0.dp, 200.dp, 0.dp, 0.dp),
text = "DivAdventure",
textAlign = TextAlign.Center,
style = TextStyle(
color = Color.White,
fontWeight = FontWeight.Bold
),
fontSize = 24.sp
)
}
Box(
modifier = Modifier.weight(0.5f, true)
) {
Image(
modifier = Modifier
.padding(0.dp, 250.dp, 0.dp, 0.dp)
.align(
Alignment.Center
),
contentDescription = "Logo",
contentScale = ContentScale.FillBounds,
painter = painterResource(id = R.drawable.divnotes_logo)
)
}
}
}
}
@Composable
fun Loader(modifier: Modifier) {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading))
val progress by animateLottieCompositionAsState(
composition = composition,
iterations = LottieConstants.IterateForever
)
LottieAnimation(
modifier = modifier.size(100.dp),
composition = composition,
progress = { progress },
)
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\UserDataManager.kt
```kt
package com.divadventure.ui.screens
import com.divadventure.di.UserPrefs.KEY_ADVENTURES_PRIVACY
import com.divadventure.di.UserPrefs.KEY_AVATAR
import com.divadventure.di.UserPrefs.KEY_BIO
import com.divadventure.di.UserPrefs.KEY_BIO_PRIVACY
import com.divadventure.di.UserPrefs.KEY_BIRTH_DATE
import com.divadventure.di.UserPrefs.KEY_BIRTH_DATE_PRIVACY
import com.divadventure.di.UserPrefs.KEY_EMAIL
import com.divadventure.di.UserPrefs.KEY_FIRST_NAME
import com.divadventure.di.UserPrefs.KEY_FRIENDS_PRIVACY
import com.divadventure.di.UserPrefs.KEY_ID
import com.divadventure.di.UserPrefs.KEY_LAST_NAME
import com.divadventure.di.UserPrefs.KEY_LOCATION
import com.divadventure.di.UserPrefs.KEY_LOCATION_PRIVACY
import com.divadventure.di.UserPrefs.KEY_PLATFORM
import com.divadventure.di.UserPrefs.KEY_REFRESH_TOKEN
import com.divadventure.di.UserPrefs.KEY_REFRESH_TOKEN_EXPIRES_AT
import com.divadventure.di.UserPrefs.KEY_TOKEN
import com.divadventure.di.UserPrefs.KEY_TOKEN_EXPIRES_AT
import com.divadventure.di.UserPrefs.KEY_USERNAME
import com.divadventure.di.SharedPrefs
import com.divadventure.domain.models.SignUpResponse
class UserDataManager(private val sharedPrefs: SharedPrefs) {
fun savePrimaryData(body: SignUpResponse) {
val user = body.data
val privacy = user.privacySettings
val token = user.currentAccessToken
val location = user.location
val keyValuePairs = mapOf(
KEY_ID to user.id,
KEY_AVATAR to user.avatar,
KEY_EMAIL to user.email,
KEY_FIRST_NAME to user.firstName,
KEY_LAST_NAME to user.lastName,
KEY_USERNAME to user.username,
KEY_BIRTH_DATE to user.birthdate,
KEY_BIO to user.bio,
/*
KEY_LOCATION to location.id,
*/
KEY_BIO_PRIVACY to privacy.bio,
KEY_LOCATION_PRIVACY to privacy.bio,
KEY_BIRTH_DATE_PRIVACY to privacy.birthdate,
KEY_FRIENDS_PRIVACY to privacy.friends,
KEY_ADVENTURES_PRIVACY to privacy.adventures,
KEY_TOKEN to token.token,
KEY_REFRESH_TOKEN to token.refreshToken,
KEY_PLATFORM to token.platform,
KEY_TOKEN_EXPIRES_AT to token.expiresAt,
KEY_REFRESH_TOKEN_EXPIRES_AT to token.refreshTokenExpiresAt
)
keyValuePairs.forEach { (key, value) ->
sharedPrefs.setString(key, value)
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\Verification.kt
```kt
package com.divadventure.ui.screens
import android.os.CountDownTimer
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
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.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.composeuisuite.ohteepee.OhTeePeeDefaults
import com.composeuisuite.ohteepee.OhTeePeeInput
import com.divadventure.R
import com.divadventure.di.SharedPrefs
import com.divadventure.ui.TopSnackBar
import com.divadventure.ui.theme.SystemUIManager
import com.divadventure.viewmodel.AuthIntent
import com.divadventure.viewmodel.AuthViewModel
import com.divadventure.data.navigation.NavigationViewModel
import com.divadventure.viewmodel.AuthUiEvent
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import timber.log.Timber
@Composable
fun VerificationScreen(
viewModel: AuthViewModel,
navigationViewModel: NavigationViewModel,
padding: PaddingValues,
sharedPrefs: SharedPrefs
) {
val state by viewModel.state.collectAsState()
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
Color.Transparent,
Color.Transparent,
hideNavigationBar = !state.navigationBarVisibility,
hideStatusBar = !state.statusBarVisibility,
onSystemBarsVisibilityChange = { statusbar, navigationBar ->
// viewModel.sendIntent(AuthIntent.MutualIntent.ChangeInsetsVisibility(statusbar, navigationBar))
})
var paddingValues by remember { mutableStateOf(padding) }
val systemUiController = rememberSystemUiController()
val darkTheme = isSystemInDarkTheme()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.White, darkIcons = !darkTheme
)
systemUiController.setNavigationBarColor(
color = Color(0xffefeff4), darkIcons = !darkTheme
)
}
Scaffold(
containerColor = Color(0xffefeff4)
) { paddingValues: PaddingValues ->
var otpString by remember { mutableStateOf("") }
val email = remember {
mutableStateOf("")
}
remember {
FocusRequester()
}
var revisionEmailButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
var continueButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
var resendCodeColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
revisionEmailButtonColor = if (state.verificationState!!.permitEmailRevision) {
Color(0xff30D158)
} else {
Color(0xffBFBFBF)
}
continueButtonColor = if (state.verificationState!!.isOtpCorrect) {
Color(0xff30D158)
} else {
Color(0xffBFBFBF)
}
resendCodeColor = if (state.verificationState!!.otpRemainTime == 0L) {
Color(0xff007AFF)
} else {
Color(0xffBFBFBF)
}
// this config will be used for each cell
val defaultCellConfig = OhTeePeeDefaults.cellConfiguration(
borderColor = Color.LightGray,
borderWidth = 0.dp,
shape = RoundedCornerShape(8.dp),
textStyle = TextStyle(
color = Color.Black
)
)
val filledCellConfig = OhTeePeeDefaults.cellConfiguration(
borderColor = Color(0xff007AFF),
borderWidth = 1.dp,
shape = RoundedCornerShape(8.dp),
textStyle = TextStyle(
color = Color.Black
)
)
var countdownTime by remember { mutableStateOf(120L) }
Box(
modifier = Modifier.fillMaxSize()
) {
DisposableEffect(Unit) {
val timer = object : CountDownTimer(120000, 1000) {
override fun onTick(millisUntilFinished: Long) {
countdownTime = millisUntilFinished / 1000
viewModel.sendIntent(
AuthIntent.VerificationIntent.UpdateTimer(countdownTime)
)
}
override fun onFinish() {
countdownTime = 0
viewModel.sendIntent(
AuthIntent.VerificationIntent.UpdateTimer(0)
)
}
}
timer.start()
onDispose {
timer.cancel()
}
}
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
AuthUiEvent.AnimateItem -> {}
AuthUiEvent.ShowDialog -> {
showDialog = true
}
is AuthUiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is AuthUiEvent.ExecuteNavigation -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
// Handle timer and auto-dismiss
LaunchedEffect(showTopSnackBar) {
if (showTopSnackBar) {
timer.value = object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
coroutineScope.launch {
showTopSnackBar = false
}
}
}.start()
} else {
timer.value?.cancel()
}
}
TopSnackBar(
paddingTop = paddingValues.calculateTopPadding(),
title = topSnackBarTitle,
message = topSnackBarMessage,
show = showTopSnackBar,
onDismiss = { showTopSnackBar = false })
Column(
modifier = Modifier, horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.background(color = Color.White)
.padding(
50.dp,
top = paddingValues.calculateTopPadding() + 15.dp,
bottom = 15.dp
)
.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
Text(
modifier = Modifier.align(Alignment.CenterStart),
text = "Verification Code",
style = TextStyle(
textAlign = TextAlign.Left,
color = Color(0xff1C1C1E),
fontSize = 20.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Bold
)
)
}
Box(
modifier = Modifier
.padding(30.dp)
.fillMaxWidth()
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier
.padding(0.dp, 5.dp)
.fillMaxWidth(),
text = "A verification code has been sent to",
style = TextStyle(
color = Color.Black,
textAlign = TextAlign.Center,
fontFamily = FontFamily(Font(R.font.sf_pro))
)
)
Text(
modifier = Modifier.fillMaxWidth(),
text = state.verificationState?.email ?: "",
style = TextStyle(
color = Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontSize = 16.sp,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold
)
)
}
}
Text(
modifier = Modifier
.padding(30.dp, 0.dp)
.fillMaxWidth(),
text = buildAnnotatedString {
val verificationState = state.verificationState
append(
if (verificationState?.otpRemainTime ?: 0 > 0) "Please check your inbox and enter the verification code below to verify your email address. The code will expire in "
else "The time has been finished"
)
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(
if (verificationState?.otpRemainTime ?: 0 > 0) "${verificationState?.otpRemainTime} seconds" else ""
)
}
},
style = TextStyle(
color = Color.Black,
fontFamily = FontFamily(Font(R.font.sf_pro))
),
textAlign = TextAlign.Center,
)
Box(modifier = Modifier.fillMaxWidth()) {
OhTeePeeInput(
modifier = Modifier
.align(Alignment.Center)
.padding(0.dp, 20.dp),
value = otpString,
onValueChange = { newValue: String, isValid: Boolean ->
otpString = newValue
viewModel.sendIntent(
AuthIntent.VerificationIntent.OnOtpChanged(
otpString.trim()
)
)
Timber.d("OTP: $newValue")
},
autoFocusByDefault = true,
horizontalArrangement = Arrangement.spacedBy(1.dp),
configurations = OhTeePeeDefaults.inputConfiguration(
cellsCount = 6,
cellModifier = Modifier
.width(46.dp)
.height(54.dp),
activeCellConfig = filledCellConfig,
emptyCellConfig = defaultCellConfig,
filledCellConfig = filledCellConfig,
),
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(30.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
) {
Card(
modifier = Modifier
.clickable {
viewModel.sendIntent(
AuthIntent.VerificationIntent.OnOtpVerifyPressed(
otp = otpString
)
)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xff30D158),
disabledContainerColor = Color(0xff30D158),
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Verify"
)
}
// show forms error
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 10.dp)
) {
Text(modifier = if (state.verificationState!!.otpRemainTime == 0L) Modifier.clickable {
viewModel.sendIntent(
AuthIntent.VerificationIntent.ResendCode
)
} else Modifier,
text = "Resend Code",
fontSize = 17.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
style = TextStyle(
color = resendCodeColor
),
textAlign = TextAlign.Left)
Spacer(modifier = Modifier.weight(1f, true))
Text(
modifier = Modifier.clickable {
viewModel.sendIntent(
AuthIntent.VerificationIntent.GotoChangeEmail(
state.verificationState?.email ?: ""
)
)
},
text = "Change email",
fontSize = 17.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
style = TextStyle(
color = Color(0xff007AFF), textAlign = TextAlign.Right
)
)
}
}
}
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\screens\VerificationResendEmail.kt
```kt
package com.divadventure.ui.screens
/*
@Composable
fun VerificationResendEmail(
viewModel: AuthViewModel,
navigationViewModel: NavigationViewModel,
padding: PaddingValues
) {
SystemUIManager(
isDarkThemeForBottom = false,
isDarkThemeForStatusBar = false,
Color.Transparent,
Color.Transparent
)
val systemUiController = rememberSystemUiController()
val darkTheme = isSystemInDarkTheme()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.White,
darkIcons = !darkTheme
)
systemUiController.setNavigationBarColor(
color = Color(0xffefeff4),
darkIcons = !darkTheme
)
}
val state by viewModel.state.collectAsState()
Box(
modifier = Modifier.background(color = Color(0xffefeff4))
) {
var showTopSnackBar by remember { mutableStateOf(false) }
var topSnackBarMessage by remember { mutableStateOf("") }
var topSnackBarTitle by remember { mutableStateOf("") }
val timer = remember { mutableStateOf<CountDownTimer?>(null) }
val coroutineScope = rememberCoroutineScope()
var showDialog by remember { mutableStateOf(false) }
// Handle UiEvents and SnackBar
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
UiEvent.AnimateItem -> {}
UiEvent.ShowDialog -> {
showDialog = true
}
is UiEvent.ShowSnackbar -> {
topSnackBarMessage = event.message
topSnackBarTitle = event.title
showTopSnackBar = true
}
is UiEvent.NavigateToNextScreen -> {
navigationViewModel.navigate(
event.navigationEvent
)
}
}
}
}
TopSnackBar(
paddingTop = padding.calculateTopPadding(),
title = topSnackBarTitle,
message = topSnackBarMessage,
show = showTopSnackBar,
onDismiss = { showTopSnackBar = false })
// Handle timer and auto-dismiss
LaunchedEffect(showTopSnackBar) {
if (showTopSnackBar) {
timer.value = object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
coroutineScope.launch {
showTopSnackBar = false
}
}
}.start()
} else {
timer.value?.cancel()
}
}
var email by remember {
mutableStateOf(state.verificationResendEmailState!!.email)
}
val focusRequester = remember {
FocusRequester()
}
var revisionEmailButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
var continueButtonColor by remember {
mutableStateOf(
Color(0xffBFBFBF)
)
}
when (state.verificationState!!.permitEmailRevision) {
(true) -> {
if (revisionEmailButtonColor != Color(0xff30D158)) {
revisionEmailButtonColor = Color(0xff30D158)
}
}
(false) -> {
if (revisionEmailButtonColor != Color(0xffBFBFBF)) {
revisionEmailButtonColor = Color(0xffBFBFBF)
}
}
}
when (state.verificationState!!.isOtpCorrect) {
true -> {
if (continueButtonColor != Color(0xff30D158)) {
continueButtonColor = Color(0xff30D158)
}
}
false -> {
if (continueButtonColor != Color(0xffBFBFBF)) {
continueButtonColor = Color(0xffBFBFBF)
}
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.padding(0.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.background(color = Color.White)
.padding(50.dp, 32.dp)
.fillMaxWidth()
) {
Text(
"Email Verification", style = TextStyle(
textAlign = TextAlign.Left,
color = Color(0xff1C1C1E),
fontSize = 20.sp,
fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.Bold
)
)
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(30.dp, 20.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White,
disabledContainerColor = Color.White,
contentColor = Color(0xff1C1C1E),
disabledContentColor = Color(0xff1C1C1E)
)
) {
Box(modifier = Modifier.fillMaxSize()) {
BasicTextField(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center)
.padding(15.dp, 0.dp),
textStyle = TextStyle(fontSize = 17.sp, textAlign = TextAlign.Left),
value = email,
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
keyboardType = KeyboardType.Email
),
onValueChange = { value: String ->
email = value
})
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(30.dp, 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(15.dp, Alignment.CenterVertically),
) {
Card(
modifier = Modifier
.clickable {
Timber.d("Verification: OTP is incorrect")
viewModel.sendIntent(
AuthIntent.VerificationResendEmailIntent.ChangeEmail(
email
)
)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xff30D158),
disabledContainerColor = Color(0xff30D158),
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Change Email"
)
}
// show forms error
}
Card(
modifier = Modifier
.clickable {
viewModel.sendIntent(
AuthIntent.VerificationResendEmailIntent.ResendCode
)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White,
disabledContainerColor = Color.White,
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
color = Color(0xff007AFF),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Resend Code"
)
}
// show forms error
}
Card(
modifier = Modifier
.clickable {
viewModel.sendIntent(
AuthIntent.VerificationResendEmailIntent.BackToLogin
)
}
.fillMaxWidth()
.padding(0.dp, 0.dp)
.height(50.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White,
disabledContainerColor = Color.White,
contentColor = Color.White,
disabledContentColor = Color.White
),
shape = RoundedCornerShape(4.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center), style = TextStyle(
fontSize = 17.sp,
color = Color(0xff007AFF),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
), text = "Back To Login"
)
}
// show forms error
}
}
}
}
}
}
*/
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\theme\Color.kt
```kt
package com.divadventure.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\theme\Theme.kt
```kt
package com.divadventure.ui.theme
import android.os.Build
import android.view.View
import android.view.WindowInsetsController
import androidx.activity.ComponentActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun DivAdventureTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> LightColorScheme//DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@Composable
fun SystemUIManager(
isDarkThemeForBottom: Boolean,
isDarkThemeForStatusBar: Boolean,
statusBarColor: Color,
navigationBarColor: Color,
hideStatusBar: Boolean = true,
hideNavigationBar: Boolean = true,
onSystemBarsVisibilityChange: (Boolean, Boolean) -> Unit
) {
var isProgrammaticChange = false // Flag to track programmatic changes
val view = LocalView.current
val window = (view.context as ComponentActivity).window
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window.statusBarColor = statusBarColor.toArgb()
window.navigationBarColor = navigationBarColor.toArgb()
}
val windowInsetsController = WindowInsetsControllerCompat(window, view)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.statusBarColor = statusBarColor.toArgb()
window.navigationBarColor = navigationBarColor.toArgb()
view.setOnApplyWindowInsetsListener { _, insets ->
val isStatusBarVisible = insets.isVisible(WindowInsetsCompat.Type.statusBars())
val isNavigationBarVisible = insets.isVisible(WindowInsetsCompat.Type.navigationBars())
if (!isProgrammaticChange) {
onSystemBarsVisibilityChange(isStatusBarVisible, isNavigationBarVisible)
}
isProgrammaticChange = false // Reset the flag
insets
}
windowInsetsController.isAppearanceLightStatusBars = !isDarkThemeForStatusBar
windowInsetsController.isAppearanceLightNavigationBars = !isDarkThemeForBottom
if (hideStatusBar) {
isProgrammaticChange = true // Set the flag before programmatic change
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
} else {
isProgrammaticChange = true // Set the flag before programmatic change
windowInsetsController.show(WindowInsetsCompat.Type.statusBars())
}
if (hideNavigationBar) {
isProgrammaticChange = true // Set the flag before programmatic change
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
} else {
isProgrammaticChange = true // Set the flag before programmatic change
windowInsetsController.show(WindowInsetsCompat.Type.navigationBars())
}
} else {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or if (hideStatusBar) View.SYSTEM_UI_FLAG_FULLSCREEN else 0
or if (hideNavigationBar) View.SYSTEM_UI_FLAG_HIDE_NAVIGATION else 0
or if (!isDarkThemeForStatusBar)
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
else 0
or if (!isDarkThemeForBottom)
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else 0)
window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
val isStatusBarVisible = visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0
val isNavigationBarVisible = visibility and View.SYSTEM_UI_FLAG_HIDE_NAVIGATION == 0
if (!isProgrammaticChange) {
onSystemBarsVisibilityChange(isStatusBarVisible, isNavigationBarVisible)
}
isProgrammaticChange = false
}
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\ui\theme\Type.kt
```kt
package com.divadventure.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\AccountViewModel.kt
```kt
package com.divadventure.viewmodel
class AccountViewModel {
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\auth\LoginModels.kt
```kt
package com.divadventure.viewmodel.auth
sealed class LoginIntent {
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()
}
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
)
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\auth\LoginViewModel.kt
```kt
package com.divadventure.viewmodel.auth
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.NavigateTo
import com.divadventure.data.navigation.NavigationEvent.PopSpecific
import com.divadventure.data.navigation.Screen
import com.divadventure.di.SharedPrefs
import com.divadventure.domain.models.ReqLogin
import com.divadventure.domain.models.ResVerifyEmail
import com.divadventure.domain.models.SignUpResponse
import com.divadventure.ui.screens.UserDataManager
import com.divadventure.util.NetworkManager
import com.divadventure.viewmodel.AuthUiEvent
import com.divadventure.viewmodel.AuthUiEvent.ExecuteNavigation
import com.divadventure.viewmodel.AuthUiEvent.ShowSnackbar
import com.divadventure.viewmodel.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import javax.inject.Inject
@HiltViewModel
class LoginViewModel @Inject constructor(
private val sharedService: SharedService,
private val sharedPrefs: SharedPrefs,
private val networkManager: NetworkManager
) : BaseViewModel<LoginIntent, LoginState>(LoginState()) {
private val _uiEvent = MutableSharedFlow<AuthUiEvent>()
val uiEvent = _uiEvent.asSharedFlow()
override suspend fun handleIntent(intent: LoginIntent) {
when (intent) {
is LoginIntent.Login -> handleLoginIntent(intent)
is LoginIntent.OnForgotPasswordChanged -> handleForgotPasswordChange(intent)
is LoginIntent.ForgotPassword -> handleForgotPassword(intent)
is LoginIntent.OnEmailChanged -> handleEmailChange(intent)
is LoginIntent.OnPasswordChanged -> handlePasswordChange(intent)
}
}
private fun handleLoginIntent(intent: LoginIntent.Login) {
if (!state.value.loginClickable) return
checkLogin(intent.emailOrUsername, intent.password)
}
private fun handleForgotPasswordChange(intent: LoginIntent.OnForgotPasswordChanged) {
updateState(
state.value.copy(
forgetPasswordEmailCorrect = isEmailValid(intent.email),
email = intent.email
)
)
}
private fun handleForgotPassword(intent: LoginIntent.ForgotPassword) {
if (!state.value.forgetPasswordEmailCorrect) return
if (!networkManager.isNetworkConnected()) {
emitUiEvent("Internet Issue", "No Internet Connection")
return
}
sharedService.forgotPassword(ResVerifyEmail(email = intent.forgotEmail))
.enqueue(createForgotPasswordCallback(intent.forgotEmail))
}
private fun handleEmailChange(intent: LoginIntent.OnEmailChanged) {
updateState(state.value.copy(email = intent.email))
validationLogin(state.value.email, state.value.password)
}
private fun handlePasswordChange(intent: LoginIntent.OnPasswordChanged) {
updateState(state.value.copy(password = intent.password))
validationLogin(state.value.email, state.value.password)
}
private fun checkLogin(username: String, password: String) {
val loginState = validationLogin(username, password)
if (!loginState.isValid) {
updateState(
state.value.copy(
formError = loginState.errorMessage,
formDataValid = loginState.isValid,
loginClickable = isEmailValid(state.value.email) && state.value.password.isNotEmpty()
)
)
return
}
if (!networkManager.isNetworkConnected()) {
emitUiEvent("Internet Issue", "No Internet Connection")
return
}
updateState(state.value.copy(loginClickable = false))
sharedService.login(ReqLogin(username, password)).enqueue(createLoginCallback())
}
private fun validationLogin(username: String, password: String): FormValidationState {
return when {
username.isEmpty() || password.isEmpty() -> FormValidationState.ERROR_EMPTY_FIELDS
!isEmailValid(username) -> FormValidationState.ERROR_INVALID_EMAIL
else -> FormValidationState.VALID
}
}
private fun createForgotPasswordCallback(email: String) = object : Callback<Unit> {
override fun onResponse(call: Call<Unit?>, response: Response<Unit?>) {
if (response.isSuccessful) {
viewModelScope.launch {
navigateToForgotPasswordVerification()
}
} else {
emitUiEvent("An error occurred", "Forgot Password email not sent successfully")
}
}
override fun onFailure(call: Call<Unit?>, t: Throwable) {
emitUiEvent("An error occurred", "Forgot Password email not sent successfully")
}
}
private suspend fun navigateToForgotPasswordVerification() {
updateState(state.value.copy())
_uiEvent.emit(
ExecuteNavigation(
NavigateTo(
screen = Screen.ForgotPassword,
popUpTo = Screen.Login,
inclusive = true,
onDestinationChangedListener = object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
controller.removeOnDestinationChangedListener(this)
}
}
)
)
)
}
private fun createLoginCallback() = object : Callback<SignUpResponse> {
override fun onResponse(
call: Call<SignUpResponse?>,
response: Response<SignUpResponse?>
) {
if (response.isSuccessful && response.body() != null) {
val body = response.body()!!
UserDataManager(sharedPrefs).savePrimaryData(body)
onLoginSuccess()
} else {
emitUiEvent("Login failed", "Invalid credentials")
}
}
override fun onFailure(call: Call<SignUpResponse?>, t: Throwable) {
val errorMessage = t.localizedMessage ?: "An unexpected error occurred. Please try again later."
emitUiEvent("Login failed", errorMessage)
}
}
private fun onLoginSuccess() {
viewModelScope.launch {
_uiEvent.emit(ExecuteNavigation(PopSpecific(Screen.Landing, false)))
}
viewModelScope.launch {
_uiEvent.emit(
ExecuteNavigation(
NavigateTo(
screen = Screen.Main,
popUpTo = Screen.Landing,
inclusive = true,
onDestinationChangedListener = object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
controller.removeOnDestinationChangedListener(this)
}
}
)
)
)
}
}
private fun emitUiEvent(title: String, message: String) {
viewModelScope.launch {
_uiEvent.emit(ShowSnackbar(title, message))
}
}
private fun isEmailValid(email: String): Boolean {
return Patterns.EMAIL_ADDRESS.matcher(email).matches()
}
private enum class FormValidationState(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)
}
}
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\auth\SignupModels.kt
```kt
package com.divadventure.viewmodel.auth
sealed class SignupIntent {
data class SignUp(val email: String, val password: String, val passwordConfirmation: String) : SignupIntent()
data class OnEmailChanged(val email: String) : SignupIntent()
data class OnPasswordChanged(val password: String) : SignupIntent()
data class OnPasswordConfirmationChanged(val passwordConfirmation: String) : SignupIntent()
object SignUpWithGoogle : SignupIntent()
}
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 = "",
)
```
Filename: C:\Users\SajjadY\StudioProjects\DivAdventure\app\src\main\java\com\divadventure\viewmodel\auth\SignupViewModel.kt
```kt
package com.divadventure.viewmodel.auth
import android.os.Bundle
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.Screen
import com.divadventure.di.SharedPrefs
import com.divadventure.domain.models.Message
import com.divadventure.domain.models.SignUpResponse
import com.divadventure.domain.models.SignupRequest
import com.divadventure.ui.screens.UserDataManager
import com.divadventure.util.Helper.Companion.isEmailValid
import com.divadventure.util.NetworkManager
import com.divadventure.viewmodel.AuthUiEvent
import com.divadventure.viewmodel.AuthUiEvent.ExecuteNavigation
import com.divadventure.viewmodel.AuthUiEvent.ShowSnackbar
import com.divadventure.viewmodel.BaseViewModel
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
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
@HiltViewModel
class SignupViewModel @Inject constructor(
private val sharedService: SharedService,
private val sharedPrefs: SharedPrefs,
private val networkManager: NetworkManager
) : BaseViewModel<SignupIntent, SignupState>(SignupState()) {
private val _uiEvent = MutableSharedFlow<AuthUiEvent>()
val uiEvent = _uiEvent.asSharedFlow()
override suspend fun handleIntent(intent: SignupIntent) {
when (intent) {
is SignupIntent.SignUp -> handleSignUp(intent)
is SignupIntent.SignUpWithGoogle -> Unit
is SignupIntent.OnEmailChanged -> updateSignupState { it.copy(email = intent.email) }
is SignupIntent.OnPasswordChanged -> updateSignupState {
it.copy(password = intent.password, startedTyping = true)
}
is SignupIntent.OnPasswordConfirmationChanged -> updateSignupState {
it.copy(passwordConfirmation = intent.passwordConfirmation, startedTyping = true)
}
}
}
private fun handleSignUp(intent: 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) {
viewModelScope.launch { processSignUpResponse(response) }
} else {
emitSnackbar("Error Sign up", extractErrorMessage(response))
}
}
override fun onFailure(call: Call<SignUpResponse>, t: Throwable) {
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")
viewModelScope.launch {
_uiEvent.emit(ShowSnackbar(title, message))
}
}
private suspend fun processSignUpResponse(response: Response<SignUpResponse>) {
response.body()?.let {
if (response.isSuccessful) {
saveUserData(it)
resetSignupState()
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?
) {
controller.removeOnDestinationChangedListener(this)
}
})
)
}
private suspend fun updateSignupState(update: (SignupState) -> SignupState) {
updateState(update(state.value))
validateSignupForm()
}
private fun validateSignupForm() {
val st = state.value
val formError = validateSignupFields(st.email, st.password, st.passwordConfirmation)
updateState(
st.copy(
formError = formError.orEmpty(),
formDataValid = formError == null,
passwordsMatch = st.password == st.passwordConfirmation,
is8Characters = st.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()) {
viewModelScope.launch {
showSnackbar("Internet Issue", "No Internet Connection")
}
return false
}
return state.value.formDataValid
}
private fun finalizeSignupProcess() {
enableSignupButton()
updateLoadingState(false)
}
private fun showSnackbar(title: String, message: String) {
viewModelScope.launch {
_uiEvent.emit(ShowSnackbar(title, message))
}
}
private fun updateLoadingState(isLoading: Boolean) {
updateState(state.value.copy(isLoadingSignUp = isLoading))
}
private fun enableSignupButton() {
updateState(state.value.copy(signupClickable = true))
}
private fun saveUserData(body: SignUpResponse) {
UserDataManager(sharedPrefs).savePrimaryData(body)
}
private fun resetSignupState() {
updateState(
SignupState(
email = state.value.email
)
)
}
private fun emitNavigationEvent(navigationEvent: NavigateTo) {
viewModelScope.launch {
_uiEvent.emit(ExecuteNavigation(navigationEvent))
}
}
private fun extractErrorMessage(response: Response<SignUpResponse>) =
response.errorBody()?.string()
?.let { Gson().fromJson(it, Message::class.java)?.message } ?: "Unknown Error"
}
```
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\build.gradle.kts
```kts
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
id("com.google.devtools.ksp") version "2.1.0-1.0.29" apply false
id("com.google.dagger.hilt.android") version "2.51.1" apply false // or latest version
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22" // Replace with your current Kotlin version
}
```
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:
- **BaseViewModel**:
- The app uses a custom `BaseViewModel` abstract class that serves as the foundation for all ViewModels in the application.
- It implements a standardized MVI approach for handling intents through a Kotlin `Channel`, managing state via `StateFlow`, and dealing with navigation and other one-time UI events (like snackbars or dialogs) using `SharedFlow`. This ensures events are not duplicated during configuration changes and maintains a clear separation between persistent state and transient events.
- 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 (e.g., `AdventuresState`, `AuthState`) that encapsulates all UI-relevant data.
- These states are immutable, and updates are handled through controlled state transitions (often using the `copy()` method of data classes) to ensure state integrity.
- ViewModels expose a single `StateFlow` for Composables to observe, simplifying the reactive data flow to the UI.
- **Intent Processing**:
- User actions from the UI are translated into strongly-typed `Intent` sealed classes/interfaces.
- These intents are sent to the ViewModel and processed asynchronously, typically through a Kotlin `Channel`, to avoid state conflicts and manage backpressure.
- Each ViewModel implements specialized intent handlers that encapsulate the business logic for specific user actions, leading to state updates or UI events.
---
### **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. **Networking and API Communication**:
- **Retrofit**: Used for defining and making network API calls.
- **OkHttp**: Serves as the underlying HTTP client for Retrofit, handling efficient network communication for all API interactions, including those related to user authentication.
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
│ ├── App.kt
│ ├── 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
│ ├── MainActivity.kt
│ ├── ui
│ │ ├── AddShared.kt
│ │ ├── AuthShared.kt
│ │ ├── components
│ │ │ ├── AdventureActionButton.kt
│ │ │ ├── AdventureItem.kt
│ │ │ ├── AdventureTypeButton.kt
│ │ │ └── StaticMap.kt
│ │ ├── HomeShared.kt
│ │ ├── ManageShared.kt
│ │ ├── otp
│ │ │ ├── ModifierExt.kt
│ │ │ └── OtpInputField.kt
│ │ ├── ProfileShared.kt
│ │ ├── screens
│ │ │ ├── ChangeEmail.kt
│ │ │ ├── ForgotPasswordScreen.kt
│ │ │ ├── LandingScreen.kt
│ │ │ ├── Login.kt
│ │ │ ├── main
│ │ │ │ ├── add
│ │ │ │ │ ├── Add.kt
│ │ │ │ │ ├── AdventureInformation.kt
│ │ │ │ │ ├── AdventureOptions.kt
│ │ │ │ │ ├── AdventurePreview.kt
│ │ │ │ │ ├── manage
│ │ │ │ │ │ └── Manage.kt
│ │ │ │ │ ├── ManageAdventure.kt
│ │ │ │ │ └── ManageAdventureOwnerModerator.kt
│ │ │ │ ├── home
│ │ │ │ │ ├── Home.kt
│ │ │ │ │ ├── notifications
│ │ │ │ │ │ ├── Notifications.kt
│ │ │ │ │ │ └── search
│ │ │ │ │ │ ├── filter
│ │ │ │ │ │ │ ├── Filter.kt
│ │ │ │ │ │ │ ├── Interests.kt
│ │ │ │ │ │ │ ├── Location.kt
│ │ │ │ │ │ │ └── Status.kt
│ │ │ │ │ │ ├── Search.kt
│ │ │ │ │ │ └── sortby
│ │ │ │ │ │ └── SortBy.kt
│ │ │ │ │ └── profile
│ │ │ │ ├── Main.kt
│ │ │ │ └── profile
│ │ │ │ ├── AccountSettings.kt
│ │ │ │ ├── NotificationsSettings.kt
│ │ │ │ ├── PrivacySettings.kt
│ │ │ │ └── Profile.kt
│ │ │ ├── Onboarding.kt
│ │ │ ├── ResetPassword.kt
│ │ │ ├── SignUp.kt
│ │ │ ├── SplashScreen.kt
│ │ │ ├── UserDataManager.kt
│ │ │ ├── Verification.kt
│ │ │ └── VerificationResendEmail.kt
│ │ └── theme
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ └── viewmodel
│ ├── AccountViewModel.kt
│ ├── auth
│ │ ├── LoginModels.kt
│ │ ├── LoginViewModel.kt
│ │ ├── SignupModels.kt
│ │ └── SignupViewModel.kt
│ ├── AuthViewModel.kt
│ ├── BaseViewModel.kt
│ ├── HomeViewModel.kt
│ ├── MainViewModel.kt
│ ├── ManageAdventureViewModel.kt
│ ├── NotificationsViewModel.kt
│ └── ProfileViewModel.kt
├── build.gradle.kts
└── readme.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment