add nested navigation for Home screens
This commit is contained in:
@ -5,7 +5,6 @@ import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -13,12 +12,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import app.omnivore.omnivore.feature.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.feature.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
|
||||
import app.omnivore.omnivore.feature.library.SearchViewModel
|
||||
import app.omnivore.omnivore.feature.root.RootView
|
||||
import app.omnivore.omnivore.feature.save.SaveViewModel
|
||||
import app.omnivore.omnivore.feature.theme.OmnivoreTheme
|
||||
import com.pspdfkit.PSPDFKit
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@ -36,12 +30,6 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val loginViewModel: LoginViewModel by viewModels()
|
||||
val searchViewModel: SearchViewModel by viewModels()
|
||||
val labelsViewModel: LabelsViewModel by viewModels()
|
||||
val saveViewModel: SaveViewModel by viewModels()
|
||||
val editInfoViewModel: EditInfoViewModel by viewModels()
|
||||
|
||||
val context = this
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
@ -59,16 +47,9 @@ class MainActivity : ComponentActivity() {
|
||||
setContent {
|
||||
OmnivoreTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
RootView(
|
||||
loginViewModel,
|
||||
searchViewModel,
|
||||
labelsViewModel,
|
||||
saveViewModel,
|
||||
editInfoViewModel
|
||||
)
|
||||
RootView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,146 @@
|
||||
package app.omnivore.omnivore.core.designsystem.motion
|
||||
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
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.slideInVertically
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
|
||||
/**
|
||||
* Returns the provided [Dp] as an [Int] value by the [LocalDensity].
|
||||
*
|
||||
* @param slideDistance Value to the slide distance dimension, 30dp by default.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberSlideDistance(
|
||||
slideDistance: Dp = MotionConstants.DefaultSlideDistance,
|
||||
): Int {
|
||||
val density = LocalDensity.current
|
||||
return remember(density, slideDistance) {
|
||||
with(density) { slideDistance.roundToPx() }
|
||||
}
|
||||
}
|
||||
|
||||
private const val ProgressThreshold = 0.35f
|
||||
|
||||
private val Int.ForOutgoing: Int
|
||||
get() = (this * ProgressThreshold).toInt()
|
||||
|
||||
private val Int.ForIncoming: Int
|
||||
get() = this - this.ForOutgoing
|
||||
|
||||
/**
|
||||
* [materialSharedAxisXIn] allows to switch a layout with shared X-axis enter transition.
|
||||
*/
|
||||
fun materialSharedAxisXIn(
|
||||
initialOffsetX: (fullWidth: Int) -> Int,
|
||||
durationMillis: Int = MotionConstants.DefaultMotionDuration,
|
||||
): EnterTransition = slideInHorizontally(
|
||||
animationSpec = tween(
|
||||
durationMillis = durationMillis,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
initialOffsetX = initialOffsetX
|
||||
) + fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = durationMillis.ForIncoming,
|
||||
delayMillis = durationMillis.ForOutgoing,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* [materialSharedAxisXOut] allows to switch a layout with shared X-axis exit transition.
|
||||
*
|
||||
*/
|
||||
fun materialSharedAxisXOut(
|
||||
targetOffsetX: (fullWidth: Int) -> Int,
|
||||
durationMillis: Int = MotionConstants.DefaultMotionDuration,
|
||||
): ExitTransition = slideOutHorizontally(
|
||||
animationSpec = tween(
|
||||
durationMillis = durationMillis,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
targetOffsetX = targetOffsetX
|
||||
) + fadeOut(
|
||||
animationSpec = tween(
|
||||
durationMillis = durationMillis.ForOutgoing,
|
||||
delayMillis = 0,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* [materialSharedAxisY] allows to switch a layout with shared Y-axis transition.
|
||||
*
|
||||
*/
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
public fun materialSharedAxisY(
|
||||
initialOffsetY: (fullWidth: Int) -> Int,
|
||||
targetOffsetY: (fullWidth: Int) -> Int,
|
||||
durationMillis: Int = MotionConstants.DefaultMotionDuration,
|
||||
): ContentTransform = ContentTransform(
|
||||
materialSharedAxisYIn(
|
||||
initialOffsetY = initialOffsetY,
|
||||
durationMillis = durationMillis
|
||||
), materialSharedAxisYOut(
|
||||
targetOffsetY = targetOffsetY,
|
||||
durationMillis = durationMillis
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* [materialSharedAxisYIn] allows to switch a layout with shared Y-axis enter transition.
|
||||
*/
|
||||
public fun materialSharedAxisYIn(
|
||||
initialOffsetY: (fullWidth: Int) -> Int,
|
||||
durationMillis: Int = MotionConstants.DefaultMotionDuration,
|
||||
): EnterTransition = slideInVertically(
|
||||
animationSpec = tween(
|
||||
durationMillis = durationMillis,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
initialOffsetY = initialOffsetY
|
||||
) + fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = durationMillis.ForIncoming,
|
||||
delayMillis = durationMillis.ForOutgoing,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* [materialSharedAxisYOut] allows to switch a layout with shared X-axis exit transition.
|
||||
*
|
||||
*/
|
||||
public fun materialSharedAxisYOut(
|
||||
targetOffsetY: (fullWidth: Int) -> Int,
|
||||
durationMillis: Int = MotionConstants.DefaultMotionDuration,
|
||||
): ExitTransition = slideOutVertically (
|
||||
animationSpec = tween(
|
||||
durationMillis = durationMillis,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
targetOffsetY = targetOffsetY
|
||||
) + fadeOut(
|
||||
animationSpec = tween(
|
||||
durationMillis = durationMillis.ForOutgoing,
|
||||
delayMillis = 0,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
@ -0,0 +1,11 @@
|
||||
package app.omnivore.omnivore.core.designsystem.motion
|
||||
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object MotionConstants {
|
||||
const val DefaultMotionDuration: Int = 300
|
||||
const val DefaultFadeInDuration: Int = 150
|
||||
const val DefaultFadeOutDuration: Int = 75
|
||||
val DefaultSlideDistance: Dp = 30.dp
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
package app.omnivore.omnivore.core.designsystem.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun rememberSlideDistance(
|
||||
slideDistance: Dp = MotionConstants.DefaultSlideDistance,
|
||||
): Int {
|
||||
val density = LocalDensity.current
|
||||
return remember(density, slideDistance) {
|
||||
with(density) { slideDistance.roundToPx() }
|
||||
}
|
||||
}
|
||||
|
||||
object MotionConstants {
|
||||
const val DEFAULT_MOTION_DURATION: Int = 300
|
||||
val DefaultSlideDistance: Dp = 30.dp
|
||||
}
|
||||
|
||||
const val ProgressThreshold = 0.35f
|
||||
|
||||
val Int.ForOutgoing: Int
|
||||
get() = (this * ProgressThreshold).toInt()
|
||||
|
||||
val Int.ForIncoming: Int
|
||||
get() = this - this.ForOutgoing
|
||||
@ -15,6 +15,7 @@ import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.datastore.followingTabActive
|
||||
import app.omnivore.omnivore.core.datastore.omnivoreAuthCookieString
|
||||
import app.omnivore.omnivore.core.datastore.omnivoreAuthToken
|
||||
import app.omnivore.omnivore.core.datastore.omnivorePendingUserToken
|
||||
@ -44,389 +45,405 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.intercom.android.sdk.Intercom
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
enum class RegistrationState {
|
||||
SocialLogin,
|
||||
EmailSignIn,
|
||||
EmailSignUp,
|
||||
PendingUser,
|
||||
SelfHosted
|
||||
SocialLogin, EmailSignIn, EmailSignUp, PendingUser, SelfHosted
|
||||
}
|
||||
|
||||
data class PendingEmailUserCreds(
|
||||
val email: String,
|
||||
val password: String
|
||||
val email: String, val password: String
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
private val eventTracker: EventTracker,
|
||||
private val networker: Networker,
|
||||
private val dataService: DataService,
|
||||
private val resourceProvider: ResourceProvider
|
||||
): ViewModel() {
|
||||
private var validateUsernameJob: Job? = null
|
||||
private val datastoreRepository: DatastoreRepository,
|
||||
private val eventTracker: EventTracker,
|
||||
private val networker: Networker,
|
||||
private val dataService: DataService,
|
||||
private val resourceProvider: ResourceProvider
|
||||
) : ViewModel() {
|
||||
private var validateUsernameJob: Job? = null
|
||||
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var errorMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var errorMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
var hasValidUsername by mutableStateOf<Boolean>(false)
|
||||
private set
|
||||
var hasValidUsername by mutableStateOf<Boolean>(false)
|
||||
private set
|
||||
|
||||
var usernameValidationErrorMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var usernameValidationErrorMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
var pendingEmailUserCreds by mutableStateOf<PendingEmailUserCreds?>(null)
|
||||
private set
|
||||
var pendingEmailUserCreds by mutableStateOf<PendingEmailUserCreds?>(null)
|
||||
private set
|
||||
|
||||
val hasAuthTokenLiveData: LiveData<Boolean> = datastoreRepo
|
||||
.hasAuthTokenFlow
|
||||
.distinctUntilChanged()
|
||||
.asLiveData()
|
||||
val hasAuthTokenLiveData: LiveData<Boolean> =
|
||||
datastoreRepository.hasAuthTokenFlow.distinctUntilChanged().asLiveData()
|
||||
|
||||
val registrationStateLiveData = MutableLiveData(RegistrationState.SocialLogin)
|
||||
val registrationStateLiveData = MutableLiveData(RegistrationState.SocialLogin)
|
||||
|
||||
fun getAuthCookieString(): String? = runBlocking {
|
||||
datastoreRepo.getString(omnivoreAuthCookieString)
|
||||
}
|
||||
val followingTabActiveState: StateFlow<Boolean> = datastoreRepository.getBoolean(
|
||||
followingTabActive
|
||||
).stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = false
|
||||
)
|
||||
|
||||
fun setSelfHostingDetails(context: Context, apiServer: String, webServer: String) {
|
||||
viewModelScope.launch {
|
||||
datastoreRepo.putString(omnivoreSelfHostedApiServer, apiServer)
|
||||
datastoreRepo.putString(omnivoreSelfHostedWebServer, webServer)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_view_model_self_hosting_settings_updated),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSelfHostingDetails(context: Context) {
|
||||
viewModelScope.launch {
|
||||
datastoreRepo.clearValue(omnivoreSelfHostedApiServer)
|
||||
datastoreRepo.clearValue(omnivoreSelfHostedWebServer)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_view_model_self_hosting_settings_reset),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
fun setSelfHostingDetails(context: Context, apiServer: String, webServer: String) {
|
||||
viewModelScope.launch {
|
||||
datastoreRepository.putString(omnivoreSelfHostedApiServer, apiServer)
|
||||
datastoreRepository.putString(omnivoreSelfHostedWebServer, webServer)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_view_model_self_hosting_settings_updated),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSelfHostingDetails(context: Context) {
|
||||
viewModelScope.launch {
|
||||
datastoreRepository.clearValue(omnivoreSelfHostedApiServer)
|
||||
datastoreRepository.clearValue(omnivoreSelfHostedWebServer)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_view_model_self_hosting_settings_reset),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
}
|
||||
fun showSocialLogin() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.SocialLogin
|
||||
}
|
||||
|
||||
fun showEmailSignIn() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.EmailSignIn
|
||||
}
|
||||
|
||||
fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) {
|
||||
resetState()
|
||||
pendingEmailUserCreds = pendingCreds
|
||||
registrationStateLiveData.value = RegistrationState.EmailSignUp
|
||||
}
|
||||
|
||||
fun showSelfHostedSettings() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.SelfHosted
|
||||
}
|
||||
|
||||
fun cancelNewUserSignUp() {
|
||||
resetState()
|
||||
viewModelScope.launch {
|
||||
datastoreRepo.clearValue(omnivorePendingUserToken)
|
||||
}
|
||||
showSocialLogin()
|
||||
}
|
||||
|
||||
fun registerUser() {
|
||||
viewModelScope.launch {
|
||||
val viewer = networker.viewer()
|
||||
viewer?.let {
|
||||
eventTracker.registerUser(viewer.userID, viewer.intercomHash, BuildConfig.DEBUG)
|
||||
}
|
||||
fun showSocialLogin() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.SocialLogin
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetState() {
|
||||
validateUsernameJob = null
|
||||
isLoading = false
|
||||
errorMessage = null
|
||||
hasValidUsername = false
|
||||
usernameValidationErrorMessage = null
|
||||
pendingEmailUserCreds = null
|
||||
}
|
||||
fun showEmailSignIn() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.EmailSignIn
|
||||
}
|
||||
|
||||
fun validateUsername(potentialUsername: String) {
|
||||
validateUsernameJob?.cancel()
|
||||
fun showEmailSignUp(pendingCreds: PendingEmailUserCreds? = null) {
|
||||
resetState()
|
||||
pendingEmailUserCreds = pendingCreds
|
||||
registrationStateLiveData.value = RegistrationState.EmailSignUp
|
||||
}
|
||||
|
||||
validateUsernameJob = viewModelScope.launch {
|
||||
delay(2000)
|
||||
fun showSelfHostedSettings() {
|
||||
resetState()
|
||||
registrationStateLiveData.value = RegistrationState.SelfHosted
|
||||
}
|
||||
|
||||
// Check the username requirements first
|
||||
if (potentialUsername.isEmpty()) {
|
||||
fun cancelNewUserSignUp() {
|
||||
resetState()
|
||||
viewModelScope.launch {
|
||||
datastoreRepository.clearValue(omnivorePendingUserToken)
|
||||
}
|
||||
showSocialLogin()
|
||||
}
|
||||
|
||||
fun registerUser() {
|
||||
viewModelScope.launch {
|
||||
val viewer = networker.viewer()
|
||||
viewer?.let {
|
||||
eventTracker.registerUser(viewer.userID, viewer.intercomHash, BuildConfig.DEBUG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetState() {
|
||||
validateUsernameJob = null
|
||||
isLoading = false
|
||||
errorMessage = null
|
||||
hasValidUsername = false
|
||||
usernameValidationErrorMessage = null
|
||||
hasValidUsername = false
|
||||
return@launch
|
||||
}
|
||||
pendingEmailUserCreds = null
|
||||
}
|
||||
|
||||
if (potentialUsername.length < 4 || potentialUsername.length > 15) {
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_username_validation_length_error_msg)
|
||||
hasValidUsername = false
|
||||
return@launch
|
||||
}
|
||||
fun validateUsername(potentialUsername: String) {
|
||||
validateUsernameJob?.cancel()
|
||||
|
||||
val isValidPattern = Pattern.compile("^[a-z0-9][a-z0-9_]+[a-z0-9]$")
|
||||
.matcher(potentialUsername)
|
||||
.matches()
|
||||
validateUsernameJob = viewModelScope.launch {
|
||||
delay(2000)
|
||||
|
||||
if (!isValidPattern) {
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_username_validation_alphanumeric_error_msg)
|
||||
hasValidUsername = false
|
||||
return@launch
|
||||
}
|
||||
// Check the username requirements first
|
||||
if (potentialUsername.isEmpty()) {
|
||||
usernameValidationErrorMessage = null
|
||||
hasValidUsername = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
val apolloClient = ApolloClient.Builder()
|
||||
.serverUrl("${Constants.apiURL}/api/graphql")
|
||||
.build()
|
||||
if (potentialUsername.length < 4 || potentialUsername.length > 15) {
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_username_validation_length_error_msg
|
||||
)
|
||||
hasValidUsername = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
try {
|
||||
val response = apolloClient.query(
|
||||
ValidateUsernameQuery(username = potentialUsername)
|
||||
).execute()
|
||||
val isValidPattern =
|
||||
Pattern.compile("^[a-z0-9][a-z0-9_]+[a-z0-9]$").matcher(potentialUsername).matches()
|
||||
|
||||
if (response.data?.validateUsername == true) {
|
||||
usernameValidationErrorMessage = null
|
||||
hasValidUsername = true
|
||||
if (!isValidPattern) {
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_username_validation_alphanumeric_error_msg
|
||||
)
|
||||
hasValidUsername = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
val apolloClient =
|
||||
ApolloClient.Builder().serverUrl("${Constants.apiURL}/api/graphql").build()
|
||||
|
||||
try {
|
||||
val response = apolloClient.query(
|
||||
ValidateUsernameQuery(username = potentialUsername)
|
||||
).execute()
|
||||
|
||||
if (response.data?.validateUsername == true) {
|
||||
usernameValidationErrorMessage = null
|
||||
hasValidUsername = true
|
||||
} else {
|
||||
hasValidUsername = false
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_username_not_available_error_msg
|
||||
)
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
hasValidUsername = false
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_connection_error_msg
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(email: String, password: String) {
|
||||
|
||||
viewModelScope.launch {
|
||||
val emailLogin =
|
||||
RetrofitHelper.getInstance(networker).create(EmailLoginSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val result = emailLogin.submitEmailLogin(
|
||||
EmailLoginCredentials(email = email, password = password)
|
||||
)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.body()?.pendingEmailVerification == true) {
|
||||
showEmailSignUp(
|
||||
pendingCreds = PendingEmailUserCreds(
|
||||
email = email, password = password
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (result.body()?.authToken != null) {
|
||||
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||
} else {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_error_msg
|
||||
)
|
||||
}
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
datastoreRepository.putString(
|
||||
omnivoreAuthCookieString, result.body()?.authCookieString!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun submitEmailSignUp(
|
||||
email: String,
|
||||
password: String,
|
||||
username: String,
|
||||
name: String,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val request =
|
||||
RetrofitHelper.getInstance(networker).create(CreateEmailAccountSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val params = EmailSignUpParams(
|
||||
email = email, password = password, name = name, username = username
|
||||
)
|
||||
|
||||
val result = request.submitCreateEmailAccount(params)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.errorBody() != null) {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
||||
)
|
||||
} else {
|
||||
pendingEmailUserCreds = PendingEmailUserCreds(email, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPendingAuthToken(): String? = runBlocking {
|
||||
datastoreRepository.getString(omnivorePendingUserToken)
|
||||
}
|
||||
|
||||
fun submitProfile(username: String, name: String) {
|
||||
viewModelScope.launch {
|
||||
val request =
|
||||
RetrofitHelper.getInstance(networker).create(CreateAccountSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val pendingUserToken = getPendingAuthToken() ?: ""
|
||||
|
||||
val userProfile = UserProfile(name = name, username = username)
|
||||
val params = CreateAccountParams(
|
||||
pendingUserToken = pendingUserToken, userProfile = userProfile
|
||||
)
|
||||
|
||||
val result = request.submitCreateAccount(params)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.body()?.authToken != null) {
|
||||
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||
} else {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_error_msg
|
||||
)
|
||||
}
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
datastoreRepository.putString(
|
||||
omnivoreAuthCookieString, result.body()?.authCookieString!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleAppleToken(authToken: String) {
|
||||
submitAuthProviderPayload(
|
||||
params = SignInParams(token = authToken, provider = "APPLE")
|
||||
)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
datastoreRepository.clear()
|
||||
dataService.clearDatabase()
|
||||
Intercom.client().logout()
|
||||
eventTracker.logout()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetErrorMessage() {
|
||||
errorMessage = null
|
||||
}
|
||||
|
||||
fun showGoogleErrorMessage() {
|
||||
errorMessage = resourceProvider.getString(R.string.login_view_model_google_auth_error_msg)
|
||||
}
|
||||
|
||||
fun handleGoogleAuthTask(task: Task<GoogleSignInAccount>) {
|
||||
val result = task.getResult(ApiException::class.java)
|
||||
val googleIdToken = result?.idToken ?: ""
|
||||
|
||||
// If token is missing then set the error message
|
||||
if (googleIdToken.isEmpty()) {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_missing_auth_token_error_msg
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
submitAuthProviderPayload(
|
||||
params = SignInParams(token = googleIdToken, provider = "GOOGLE")
|
||||
)
|
||||
}
|
||||
|
||||
private fun submitAuthProviderPayload(params: SignInParams) {
|
||||
|
||||
viewModelScope.launch {
|
||||
val login =
|
||||
RetrofitHelper.getInstance(networker).create(AuthProviderLoginSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val result = login.submitAuthProviderLogin(params)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.body()?.authToken != null) {
|
||||
datastoreRepository.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
datastoreRepository.putString(
|
||||
omnivoreAuthCookieString, result.body()?.authCookieString!!
|
||||
)
|
||||
}
|
||||
} else {
|
||||
when (result.code()) {
|
||||
401, 403 -> {
|
||||
// This is a new user so they should go through the new user flow
|
||||
submitAuthProviderPayloadForPendingToken(params = params)
|
||||
}
|
||||
|
||||
418 -> {
|
||||
// Show pending email state
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun submitAuthProviderPayloadForPendingToken(params: SignInParams) {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val request = RetrofitHelper.getInstance(networker).create(PendingUserSubmit::class.java)
|
||||
val result = request.submitPendingUser(params)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.body()?.pendingUserToken != null) {
|
||||
datastoreRepository.putString(
|
||||
omnivorePendingUserToken, result.body()?.pendingUserToken!!
|
||||
)
|
||||
registrationStateLiveData.value = RegistrationState.PendingUser
|
||||
} else {
|
||||
hasValidUsername = false
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_username_not_available_error_msg)
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
hasValidUsername = false
|
||||
usernameValidationErrorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_connection_error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(email: String, password: String) {
|
||||
|
||||
viewModelScope.launch {
|
||||
val emailLogin = RetrofitHelper.getInstance(networker).create(EmailLoginSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val result = emailLogin.submitEmailLogin(
|
||||
EmailLoginCredentials(email = email, password = password)
|
||||
)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.body()?.pendingEmailVerification == true) {
|
||||
showEmailSignUp(pendingCreds = PendingEmailUserCreds(email = email, password = password))
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (result.body()?.authToken != null) {
|
||||
datastoreRepo.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||
} else {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_error_msg)
|
||||
}
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
datastoreRepo.putString(
|
||||
omnivoreAuthCookieString, result.body()?.authCookieString!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun submitEmailSignUp(
|
||||
email: String,
|
||||
password: String,
|
||||
username: String,
|
||||
name: String,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val request = RetrofitHelper.getInstance(networker).create(CreateEmailAccountSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val params = EmailSignUpParams(
|
||||
email = email,
|
||||
password = password,
|
||||
name = name,
|
||||
username = username
|
||||
)
|
||||
|
||||
val result = request.submitCreateEmailAccount(params)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.errorBody() != null) {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg)
|
||||
} else {
|
||||
pendingEmailUserCreds = PendingEmailUserCreds(email, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPendingAuthToken(): String? = runBlocking {
|
||||
datastoreRepo.getString(omnivorePendingUserToken)
|
||||
}
|
||||
|
||||
fun submitProfile(username: String, name: String) {
|
||||
viewModelScope.launch {
|
||||
val request = RetrofitHelper.getInstance(networker).create(CreateAccountSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val pendingUserToken = getPendingAuthToken() ?: ""
|
||||
|
||||
val userProfile = UserProfile(name = name, username = username)
|
||||
val params = CreateAccountParams(
|
||||
pendingUserToken = pendingUserToken,
|
||||
userProfile = userProfile
|
||||
)
|
||||
|
||||
val result = request.submitCreateAccount(params)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.body()?.authToken != null) {
|
||||
datastoreRepo.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||
} else {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_error_msg)
|
||||
}
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
datastoreRepo.putString(
|
||||
omnivoreAuthCookieString, result.body()?.authCookieString!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleAppleToken(authToken: String) {
|
||||
submitAuthProviderPayload(
|
||||
params = SignInParams(token = authToken, provider = "APPLE")
|
||||
)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
datastoreRepo.clear()
|
||||
dataService.clearDatabase()
|
||||
Intercom.client().logout()
|
||||
eventTracker.logout()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetErrorMessage() {
|
||||
errorMessage = null
|
||||
}
|
||||
|
||||
fun showGoogleErrorMessage() {
|
||||
errorMessage = resourceProvider.getString(R.string.login_view_model_google_auth_error_msg)
|
||||
}
|
||||
|
||||
fun handleGoogleAuthTask(task: Task<GoogleSignInAccount>) {
|
||||
val result = task.getResult(ApiException::class.java)
|
||||
val googleIdToken = result?.idToken ?: ""
|
||||
|
||||
// If token is missing then set the error message
|
||||
if (googleIdToken.isEmpty()) {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_missing_auth_token_error_msg)
|
||||
return
|
||||
}
|
||||
|
||||
submitAuthProviderPayload(
|
||||
params = SignInParams(token = googleIdToken, provider = "GOOGLE")
|
||||
)
|
||||
}
|
||||
|
||||
private fun submitAuthProviderPayload(params: SignInParams) {
|
||||
|
||||
viewModelScope.launch {
|
||||
val login = RetrofitHelper.getInstance(networker).create(AuthProviderLoginSubmit::class.java)
|
||||
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val result = login.submitAuthProviderLogin(params)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.body()?.authToken != null) {
|
||||
datastoreRepo.putString(omnivoreAuthToken, result.body()?.authToken!!)
|
||||
|
||||
if (result.body()?.authCookieString != null) {
|
||||
datastoreRepo.putString(
|
||||
omnivoreAuthCookieString, result.body()?.authCookieString!!
|
||||
)
|
||||
}
|
||||
} else {
|
||||
when (result.code()) {
|
||||
401, 403 -> {
|
||||
// This is a new user so they should go through the new user flow
|
||||
submitAuthProviderPayloadForPendingToken(params = params)
|
||||
}
|
||||
418 -> {
|
||||
// Show pending email state
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg)
|
||||
}
|
||||
else -> {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg)
|
||||
}
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun submitAuthProviderPayloadForPendingToken(params: SignInParams) {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
|
||||
val request = RetrofitHelper.getInstance(networker).create(PendingUserSubmit::class.java)
|
||||
val result = request.submitPendingUser(params)
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (result.body()?.pendingUserToken != null) {
|
||||
datastoreRepo.putString(
|
||||
omnivorePendingUserToken, result.body()?.pendingUserToken!!
|
||||
)
|
||||
registrationStateLiveData.value = RegistrationState.PendingUser
|
||||
} else {
|
||||
errorMessage = resourceProvider.getString(
|
||||
R.string.login_view_model_something_went_wrong_two_error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,10 +35,10 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun FollowingScreen(
|
||||
labelsViewModel: LabelsViewModel,
|
||||
saveViewModel: SaveViewModel,
|
||||
editInfoViewModel: EditInfoViewModel,
|
||||
navController: NavHostController,
|
||||
labelsViewModel: LabelsViewModel = hiltViewModel(),
|
||||
saveViewModel: SaveViewModel = hiltViewModel(),
|
||||
editInfoViewModel: EditInfoViewModel = hiltViewModel(),
|
||||
viewModel: FollowingViewModel = hiltViewModel()
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
@ -79,10 +79,10 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun LibraryView(
|
||||
labelsViewModel: LabelsViewModel,
|
||||
saveViewModel: SaveViewModel,
|
||||
editInfoViewModel: EditInfoViewModel,
|
||||
navController: NavHostController,
|
||||
labelsViewModel: LabelsViewModel = hiltViewModel(),
|
||||
saveViewModel: SaveViewModel = hiltViewModel(),
|
||||
editInfoViewModel: EditInfoViewModel = hiltViewModel(),
|
||||
viewModel: LibraryViewModel = hiltViewModel()
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
@ -34,6 +34,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
@ -46,8 +47,8 @@ import app.omnivore.omnivore.feature.savedItemViews.TypeaheadSearchCard
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchView(
|
||||
viewModel: SearchViewModel,
|
||||
navController: NavHostController
|
||||
navController: NavHostController,
|
||||
viewModel: SearchViewModel = hiltViewModel()
|
||||
) {
|
||||
val isRefreshing: Boolean by viewModel.isRefreshing.observeAsState(false)
|
||||
val typeaheadMode: Boolean by viewModel.typeaheadMode.observeAsState(true)
|
||||
|
||||
@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.designsystem.component.TextPreferenceWidget
|
||||
@ -25,8 +26,8 @@ internal const val RELEASE_URL = "https://github.com/omnivore-app/omnivore/relea
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun SettingsScreen(
|
||||
loginViewModel: LoginViewModel,
|
||||
navController: NavHostController
|
||||
navController: NavHostController,
|
||||
loginViewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
package app.omnivore.omnivore.feature.root
|
||||
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
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.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
@ -35,6 +29,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
@ -42,48 +38,43 @@ import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import app.omnivore.omnivore.core.designsystem.util.ForIncoming
|
||||
import app.omnivore.omnivore.core.designsystem.util.ForOutgoing
|
||||
import app.omnivore.omnivore.core.designsystem.util.MotionConstants.DEFAULT_MOTION_DURATION
|
||||
import app.omnivore.omnivore.core.designsystem.util.rememberSlideDistance
|
||||
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXIn
|
||||
import app.omnivore.omnivore.core.designsystem.motion.materialSharedAxisXOut
|
||||
import app.omnivore.omnivore.feature.auth.LoginViewModel
|
||||
import app.omnivore.omnivore.feature.auth.WelcomeScreen
|
||||
import app.omnivore.omnivore.feature.components.LabelsViewModel
|
||||
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
|
||||
import app.omnivore.omnivore.feature.following.FollowingScreen
|
||||
import app.omnivore.omnivore.feature.library.LibraryView
|
||||
import app.omnivore.omnivore.feature.library.SearchView
|
||||
import app.omnivore.omnivore.feature.library.SearchViewModel
|
||||
import app.omnivore.omnivore.feature.profile.SettingsScreen
|
||||
import app.omnivore.omnivore.feature.profile.about.AboutScreen
|
||||
import app.omnivore.omnivore.feature.profile.account.AccountScreen
|
||||
import app.omnivore.omnivore.feature.profile.filters.FiltersScreen
|
||||
import app.omnivore.omnivore.feature.save.SaveViewModel
|
||||
import app.omnivore.omnivore.feature.web.WebViewScreen
|
||||
import app.omnivore.omnivore.navigation.Routes
|
||||
import app.omnivore.omnivore.navigation.TopLevelDestination
|
||||
|
||||
@Composable
|
||||
fun RootView(
|
||||
loginViewModel: LoginViewModel,
|
||||
searchViewModel: SearchViewModel,
|
||||
labelsViewModel: LabelsViewModel,
|
||||
saveViewModel: SaveViewModel,
|
||||
editInfoViewModel: EditInfoViewModel,
|
||||
loginViewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
val hasAuthToken: Boolean by loginViewModel.hasAuthTokenLiveData.observeAsState(false)
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val navController = rememberNavController()
|
||||
val followingTabActive by loginViewModel.followingTabActiveState.collectAsStateWithLifecycle()
|
||||
|
||||
val destinations = if (!followingTabActive) {
|
||||
TopLevelDestination.entries
|
||||
} else {
|
||||
TopLevelDestination.entries.filter { it.route != Routes.Following.route }
|
||||
}
|
||||
|
||||
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }, bottomBar = {
|
||||
if (
|
||||
navController.currentBackStackEntryAsState().value?.destination?.route in
|
||||
TopLevelDestination.entries.map { it.route }
|
||||
) {
|
||||
if (navController.currentBackStackEntryAsState().value?.destination?.route in TopLevelDestination.entries.map { it.route }) {
|
||||
OmnivoreBottomBar(
|
||||
navController,
|
||||
TopLevelDestination.entries,
|
||||
destinations,
|
||||
navController.currentBackStackEntryAsState().value?.destination
|
||||
)
|
||||
}
|
||||
@ -102,13 +93,7 @@ fun RootView(
|
||||
if (hasAuthToken) {
|
||||
PrimaryNavigator(
|
||||
navController = navController,
|
||||
loginViewModel = loginViewModel,
|
||||
searchViewModel = searchViewModel,
|
||||
labelsViewModel = labelsViewModel,
|
||||
saveViewModel = saveViewModel,
|
||||
editInfoViewModel = editInfoViewModel,
|
||||
snackbarHostState = snackbarHostState
|
||||
|
||||
)
|
||||
} else {
|
||||
WelcomeScreen(viewModel = loginViewModel)
|
||||
@ -124,100 +109,51 @@ fun RootView(
|
||||
}
|
||||
}
|
||||
|
||||
private const val INITIAL_OFFSET_FACTOR = 0.10f
|
||||
|
||||
@Composable
|
||||
fun PrimaryNavigator(
|
||||
navController: NavHostController,
|
||||
loginViewModel: LoginViewModel,
|
||||
searchViewModel: SearchViewModel,
|
||||
labelsViewModel: LabelsViewModel,
|
||||
saveViewModel: SaveViewModel,
|
||||
editInfoViewModel: EditInfoViewModel,
|
||||
snackbarHostState: SnackbarHostState
|
||||
) {
|
||||
|
||||
val slideDistance = rememberSlideDistance()
|
||||
val duration = DEFAULT_MOTION_DURATION
|
||||
NavHost(navController = navController,
|
||||
startDestination = Routes.Home.route,
|
||||
enterTransition = { materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
||||
exitTransition = { materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
||||
popEnterTransition = { materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) },
|
||||
popExitTransition = { materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) }) {
|
||||
|
||||
val enterTransition = slideInHorizontally(
|
||||
animationSpec = tween(
|
||||
durationMillis = duration,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
initialOffsetX = {
|
||||
slideDistance
|
||||
}
|
||||
) + fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = duration.ForIncoming,
|
||||
delayMillis = duration.ForOutgoing,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
)
|
||||
navigation(startDestination = Routes.Inbox.route,
|
||||
route = Routes.Home.route,
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
popEnterTransition = { EnterTransition.None },
|
||||
popExitTransition = { ExitTransition.None }) {
|
||||
|
||||
val exitTransition = slideOutHorizontally(
|
||||
animationSpec = tween(
|
||||
durationMillis = duration,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
targetOffsetX = {
|
||||
-slideDistance
|
||||
}
|
||||
) + fadeOut(
|
||||
animationSpec = tween(
|
||||
durationMillis = duration.ForOutgoing,
|
||||
delayMillis = 0,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
composable(Routes.Inbox.route) {
|
||||
LibraryView(navController = navController)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController, startDestination = Routes.Inbox.route,
|
||||
enterTransition = { enterTransition },
|
||||
popEnterTransition = { enterTransition },
|
||||
exitTransition = { exitTransition },
|
||||
popExitTransition = { exitTransition }
|
||||
composable(Routes.Following.route) {
|
||||
FollowingScreen(navController = navController)
|
||||
}
|
||||
|
||||
) {
|
||||
composable(Routes.Inbox.route) {
|
||||
LibraryView(
|
||||
navController = navController,
|
||||
labelsViewModel = labelsViewModel,
|
||||
saveViewModel = saveViewModel,
|
||||
editInfoViewModel = editInfoViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.Following.route) {
|
||||
FollowingScreen(
|
||||
navController = navController,
|
||||
labelsViewModel = labelsViewModel,
|
||||
saveViewModel = saveViewModel,
|
||||
editInfoViewModel = editInfoViewModel,
|
||||
)
|
||||
composable(Routes.Settings.route) {
|
||||
SettingsScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
|
||||
composable(Routes.Search.route) {
|
||||
SearchView(
|
||||
viewModel = searchViewModel, navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
composable(Routes.Settings.route) {
|
||||
SettingsScreen(
|
||||
loginViewModel = loginViewModel, navController = navController
|
||||
)
|
||||
SearchView(navController = navController)
|
||||
}
|
||||
|
||||
composable(Routes.About.route) {
|
||||
AboutScreen(
|
||||
navController = navController
|
||||
)
|
||||
AboutScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable(Routes.Filters.route) {
|
||||
FiltersScreen(
|
||||
navController = navController
|
||||
)
|
||||
FiltersScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable(Routes.Account.route) {
|
||||
@ -259,8 +195,7 @@ private fun OmnivoreBottomBar(
|
||||
}
|
||||
NavigationBarItem(icon = {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = stringResource(id = screen.iconTextId)
|
||||
icon, contentDescription = stringResource(id = screen.iconTextId)
|
||||
)
|
||||
},
|
||||
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package app.omnivore.omnivore.navigation
|
||||
|
||||
sealed class Routes(val route: String) {
|
||||
data object Home : Routes("Home")
|
||||
data object Following : Routes("Following")
|
||||
data object Inbox : Routes("Inbox")
|
||||
data object Settings : Routes("Settings")
|
||||
|
||||
Reference in New Issue
Block a user