Implement login/logout functionality
This commit is contained in:
@@ -4,8 +4,8 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
@@ -19,22 +19,15 @@ import timber.log.Timber
|
||||
@AndroidEntryPoint
|
||||
class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
||||
private val binding by viewBinding(FragmentAuthenticationBinding::bind)
|
||||
private val viewModel by viewModels<AuthenticationViewModel>()
|
||||
private val viewModel by activityViewModels<AuthenticationViewModel>()
|
||||
|
||||
private val authStatuses by lazy { viewModel.authenticationStatuses() }
|
||||
private val authStatusObserver = Observer<Boolean> { onAuthStatusChange(it) }
|
||||
private fun onAuthStatusChange(isAuthenticated: Boolean) {
|
||||
Timber.v("onAuthStatusChange() called with: isAuthenticated = $isAuthenticated")
|
||||
if (isAuthenticated) {
|
||||
authStatuses.removeObserver(authStatusObserver)
|
||||
navigateToRecipes()
|
||||
}
|
||||
}
|
||||
private val authStatuses: LiveData<AuthenticationState>
|
||||
get() = viewModel.authenticationState
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
|
||||
authStatuses.observe(this, authStatusObserver)
|
||||
authStatuses.observe(this, ::onAuthStatusChange)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -45,9 +38,11 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
||||
getString(R.string.app_name)
|
||||
}
|
||||
|
||||
private fun navigateToRecipes() {
|
||||
Timber.v("navigateToRecipes() called")
|
||||
findNavController().navigate(AuthenticationFragmentDirections.actionAuthenticationFragmentToRecipesFragment())
|
||||
private fun onAuthStatusChange(isAuthenticated: AuthenticationState) {
|
||||
Timber.v("onAuthStatusChange() called with: isAuthenticated = $isAuthenticated")
|
||||
if (isAuthenticated == AuthenticationState.AUTHORIZED) {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLoginClicked(): Unit = with(binding) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package gq.kirmanak.mealient.ui.auth
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
enum class AuthenticationState {
|
||||
AUTHORIZED,
|
||||
AUTH_REQUESTED,
|
||||
UNAUTHORIZED;
|
||||
|
||||
companion object {
|
||||
|
||||
fun determineState(
|
||||
isLoginRequested: Boolean,
|
||||
isAuthorized: Boolean,
|
||||
): AuthenticationState {
|
||||
Timber.v("determineState() called with: isLoginRequested = $isLoginRequested, isAuthorized = $isAuthorized")
|
||||
val result = when {
|
||||
isAuthorized -> AUTHORIZED
|
||||
isLoginRequested -> AUTH_REQUESTED
|
||||
else -> UNAUTHORIZED
|
||||
}
|
||||
Timber.v("determineState() returned: $result")
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ package gq.kirmanak.mealient.ui.auth
|
||||
import androidx.lifecycle.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -11,9 +12,16 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class AuthenticationViewModel @Inject constructor(
|
||||
private val authRepo: AuthRepo,
|
||||
private val recipeRepo: RecipeRepo
|
||||
) : ViewModel() {
|
||||
|
||||
private val loginRequestsFlow = MutableStateFlow(false)
|
||||
val authenticationState: LiveData<AuthenticationState> = loginRequestsFlow.combine(
|
||||
flow = authRepo.isAuthorizedFlow,
|
||||
transform = AuthenticationState::determineState
|
||||
).asLiveData()
|
||||
val currentAuthenticationState: AuthenticationState
|
||||
get() = checkNotNull(authenticationState.value) { "Auth state flow mustn't be null" }
|
||||
|
||||
fun authenticate(username: String, password: String): LiveData<Result<Unit>> {
|
||||
Timber.v("authenticate() called with: username = $username, password = $password")
|
||||
val result = MutableLiveData<Result<Unit>>()
|
||||
@@ -31,16 +39,16 @@ class AuthenticationViewModel @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
fun authenticationStatuses(): LiveData<Boolean> {
|
||||
Timber.v("authenticationStatuses() called")
|
||||
return authRepo.authenticationStatuses().asLiveData()
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
Timber.v("logout() called")
|
||||
viewModelScope.launch {
|
||||
loginRequestsFlow.emit(false)
|
||||
authRepo.logout()
|
||||
recipeRepo.clearLocalData()
|
||||
}
|
||||
}
|
||||
|
||||
fun login() {
|
||||
Timber.v("login() called")
|
||||
viewModelScope.launch { loginRequestsFlow.emit(true) }
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
|
||||
import gq.kirmanak.mealient.ui.checkIfInputIsEmpty
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -22,9 +24,15 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
||||
viewModel.screenState.observe(viewLifecycleOwner, ::updateState)
|
||||
binding.button.setOnClickListener {
|
||||
viewModel.saveBaseUrl(binding.urlInput.text.toString())
|
||||
}
|
||||
binding.button.setOnClickListener(::onProceedClick)
|
||||
}
|
||||
|
||||
private fun onProceedClick(view: View) {
|
||||
Timber.v("onProceedClick() called with: view = $view")
|
||||
val url = binding.urlInput.checkIfInputIsEmpty(binding.urlInputLayout, lifecycleScope) {
|
||||
getString(R.string.fragment_baseurl_url_input_empty)
|
||||
} ?: return
|
||||
viewModel.saveBaseUrl(url)
|
||||
}
|
||||
|
||||
private fun updateState(baseURLScreenState: BaseURLScreenState) {
|
||||
|
||||
@@ -24,15 +24,20 @@ class BaseURLViewModel @Inject constructor(
|
||||
private set(value) {
|
||||
_screenState.value = value
|
||||
}
|
||||
val screenState: LiveData<BaseURLScreenState> by ::_screenState
|
||||
val screenState: LiveData<BaseURLScreenState>
|
||||
get() = _screenState
|
||||
|
||||
fun saveBaseUrl(baseURL: String) {
|
||||
Timber.v("saveBaseUrl() called with: baseURL = $baseURL")
|
||||
viewModelScope.launch { checkBaseURL(baseURL) }
|
||||
val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) }
|
||||
val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL)
|
||||
viewModelScope.launch { checkBaseURL(url) }
|
||||
}
|
||||
|
||||
private suspend fun checkBaseURL(baseURL: String) {
|
||||
Timber.v("checkBaseURL() called with: baseURL = $baseURL")
|
||||
val version = try {
|
||||
// If it returns proper version info then it must be a Mealie
|
||||
versionDataSource.getVersionInfo(baseURL)
|
||||
} catch (e: NetworkError) {
|
||||
Timber.e(e, "checkBaseURL: can't get version info")
|
||||
@@ -43,4 +48,9 @@ class BaseURLViewModel @Inject constructor(
|
||||
baseURLStorage.storeBaseURL(baseURL)
|
||||
currentScreenState = BaseURLScreenState(null, true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ALLOWED_PREFIXES = listOf("http://", "https://")
|
||||
private const val WITH_PREFIX_FORMAT = "https://%s"
|
||||
}
|
||||
}
|
||||
@@ -20,16 +20,12 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
|
||||
listenToAcceptStatus()
|
||||
viewModel.isAccepted.observe(this, ::onAcceptStateChange)
|
||||
}
|
||||
|
||||
private fun listenToAcceptStatus() {
|
||||
Timber.v("listenToAcceptStatus() called")
|
||||
viewModel.isAccepted.observe(this) {
|
||||
Timber.d("listenToAcceptStatus: new status = $it")
|
||||
if (it) navigateNext()
|
||||
}
|
||||
viewModel.checkIsAccepted()
|
||||
private fun onAcceptStateChange(isAccepted: Boolean) {
|
||||
Timber.v("onAcceptStateChange() called with: isAccepted = $isAccepted")
|
||||
if (isAccepted) navigateNext()
|
||||
}
|
||||
|
||||
private fun navigateNext() {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package gq.kirmanak.mealient.ui.disclaimer
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -21,25 +18,15 @@ import javax.inject.Inject
|
||||
class DisclaimerViewModel @Inject constructor(
|
||||
private val disclaimerStorage: DisclaimerStorage
|
||||
) : ViewModel() {
|
||||
private val _isAccepted = MutableLiveData(false)
|
||||
val isAccepted: LiveData<Boolean> = _isAccepted
|
||||
val isAccepted: LiveData<Boolean>
|
||||
get() = disclaimerStorage.isDisclaimerAcceptedFlow.asLiveData()
|
||||
|
||||
private val _okayCountDown = MutableLiveData(FULL_COUNT_DOWN_SEC)
|
||||
val okayCountDown: LiveData<Int> = _okayCountDown
|
||||
|
||||
fun checkIsAccepted() {
|
||||
Timber.v("checkIsAccepted() called")
|
||||
viewModelScope.launch {
|
||||
_isAccepted.value = disclaimerStorage.isDisclaimerAccepted()
|
||||
}
|
||||
}
|
||||
|
||||
fun acceptDisclaimer() {
|
||||
Timber.v("acceptDisclaimer() called")
|
||||
viewModelScope.launch {
|
||||
disclaimerStorage.acceptDisclaimer()
|
||||
_isAccepted.value = true
|
||||
}
|
||||
viewModelScope.launch { disclaimerStorage.acceptDisclaimer() }
|
||||
}
|
||||
|
||||
fun startCountDown() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
@@ -12,6 +13,8 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
|
||||
import gq.kirmanak.mealient.ui.auth.AuthenticationState
|
||||
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
|
||||
import gq.kirmanak.mealient.ui.refreshesLiveData
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import timber.log.Timber
|
||||
@@ -20,6 +23,19 @@ import timber.log.Timber
|
||||
class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
||||
private val binding by viewBinding(FragmentRecipesBinding::bind)
|
||||
private val viewModel by viewModels<RecipeViewModel>()
|
||||
private val authViewModel by activityViewModels<AuthenticationViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
authViewModel.authenticationState.observe(this, ::onAuthStateChange)
|
||||
}
|
||||
|
||||
private fun onAuthStateChange(authenticationState: AuthenticationState) {
|
||||
Timber.v("onAuthStateChange() called with: authenticationState = $authenticationState")
|
||||
if (authenticationState == AuthenticationState.AUTH_REQUESTED) {
|
||||
findNavController().navigate(RecipesFragmentDirections.actionRecipesFragmentToAuthenticationFragment())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
@@ -19,58 +19,58 @@ import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RecipeInfoFragment : BottomSheetDialogFragment() {
|
||||
private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
|
||||
private val arguments by navArgs<RecipeInfoFragmentArgs>()
|
||||
private val viewModel by viewModels<RecipeInfoViewModel>()
|
||||
private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
|
||||
private val arguments by navArgs<RecipeInfoFragmentArgs>()
|
||||
private val viewModel by viewModels<RecipeInfoViewModel>()
|
||||
|
||||
@Inject
|
||||
lateinit var ingredientsAdapter: RecipeIngredientsAdapter
|
||||
@Inject
|
||||
lateinit var ingredientsAdapter: RecipeIngredientsAdapter
|
||||
|
||||
@Inject
|
||||
lateinit var instructionsAdapter: RecipeInstructionsAdapter
|
||||
@Inject
|
||||
lateinit var instructionsAdapter: RecipeInstructionsAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
Timber.v("onCreateView() called")
|
||||
return FragmentRecipeInfoBinding.inflate(inflater, container, false).root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.v("onViewCreated() called")
|
||||
|
||||
binding.ingredientsList.adapter = ingredientsAdapter
|
||||
binding.instructionsList.adapter = instructionsAdapter
|
||||
|
||||
viewModel.loadRecipeImage(binding.image, arguments.recipeSlug)
|
||||
viewModel.loadRecipeInfo(arguments.recipeId, arguments.recipeSlug)
|
||||
|
||||
viewModel.recipeInfo.observe(viewLifecycleOwner) {
|
||||
Timber.d("onViewCreated: full info $it")
|
||||
binding.title.text = it.recipeSummaryEntity.name
|
||||
binding.description.text = it.recipeSummaryEntity.description
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
Timber.v("onCreateView() called")
|
||||
return FragmentRecipeInfoBinding.inflate(inflater, container, false).root
|
||||
}
|
||||
|
||||
viewModel.listsVisibility.observe(viewLifecycleOwner) {
|
||||
Timber.d("onViewCreated: lists visibility $it")
|
||||
binding.ingredientsHolder.isVisible = it.areIngredientsVisible
|
||||
binding.instructionsGroup.isVisible = it.areInstructionsVisible
|
||||
}
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.v("onViewCreated() called")
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
BottomSheetDialog(requireContext(), R.style.NoShapeBottomSheetDialog)
|
||||
binding.ingredientsList.adapter = ingredientsAdapter
|
||||
binding.instructionsList.adapter = instructionsAdapter
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
Timber.v("onDestroyView() called")
|
||||
// Prevent RV leaking through mObservers list in adapter
|
||||
with(binding) {
|
||||
ingredientsList.adapter = null
|
||||
instructionsList.adapter = null
|
||||
viewModel.loadRecipeImage(binding.image, arguments.recipeSlug)
|
||||
viewModel.loadRecipeInfo(arguments.recipeId, arguments.recipeSlug)
|
||||
|
||||
viewModel.recipeInfo.observe(viewLifecycleOwner) {
|
||||
Timber.d("onViewCreated: full info $it")
|
||||
binding.title.text = it.recipeSummaryEntity.name
|
||||
binding.description.text = it.recipeSummaryEntity.description
|
||||
}
|
||||
|
||||
viewModel.listsVisibility.observe(viewLifecycleOwner) {
|
||||
Timber.d("onViewCreated: lists visibility $it")
|
||||
binding.ingredientsHolder.isVisible = it.areIngredientsVisible
|
||||
binding.instructionsGroup.isVisible = it.areInstructionsVisible
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
BottomSheetDialog(requireContext(), R.style.NoShapeBottomSheetDialog)
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
Timber.v("onDestroyView() called")
|
||||
// Prevent RV leaking through mObservers list in adapter
|
||||
with(binding) {
|
||||
ingredientsList.adapter = null
|
||||
instructionsList.adapter = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ constructor(
|
||||
) : ViewModel() {
|
||||
|
||||
private val _recipeInfo = MutableLiveData<FullRecipeInfo>()
|
||||
val recipeInfo: LiveData<FullRecipeInfo> by ::_recipeInfo
|
||||
val recipeInfo: LiveData<FullRecipeInfo>
|
||||
get() = _recipeInfo
|
||||
private val _listsVisibility = MutableLiveData(RecipeInfoListsVisibility())
|
||||
val listsVisibility: LiveData<RecipeInfoListsVisibility> by ::_listsVisibility
|
||||
val listsVisibility: LiveData<RecipeInfoListsVisibility>
|
||||
get() = _listsVisibility
|
||||
|
||||
fun loadRecipeImage(view: ImageView, recipeSlug: String) {
|
||||
Timber.v("loadRecipeImage() called with: view = $view, recipeSlug = $recipeSlug")
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavDirections
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
@@ -19,10 +20,12 @@ class SplashFragment : Fragment(R.layout.fragment_splash) {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
|
||||
viewModel.nextDestination.observe(this) {
|
||||
Timber.d("onCreate: next destination $it")
|
||||
findNavController().navigate(it)
|
||||
}
|
||||
viewModel.nextDestination.observe(this, ::onNextDestination)
|
||||
}
|
||||
|
||||
private fun onNextDestination(navDirections: NavDirections) {
|
||||
Timber.v("onNextDestination() called with: navDirections = $navDirections")
|
||||
findNavController().navigate(navDirections)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
Reference in New Issue
Block a user