diff --git a/app/src/debug/java/gq/kirmanak/mealient/App.kt b/app/src/debug/java/gq/kirmanak/mealient/App.kt index 072de6f..eb4f24f 100644 --- a/app/src/debug/java/gq/kirmanak/mealient/App.kt +++ b/app/src/debug/java/gq/kirmanak/mealient/App.kt @@ -11,23 +11,23 @@ import javax.inject.Inject @HiltAndroidApp class App : Application() { - // Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382) - @Inject - lateinit var flipperPlugins: Set<@JvmSuppressWildcards FlipperPlugin> + // Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382) + @Inject + lateinit var flipperPlugins: Set<@JvmSuppressWildcards FlipperPlugin> - override fun onCreate() { - super.onCreate() - Timber.plant(Timber.DebugTree()) - Timber.v("onCreate() called") - setupFlipper() - } - - private fun setupFlipper() { - if (FlipperUtils.shouldEnableFlipper(this)) { - SoLoader.init(this, false) - val flipperClient = AndroidFlipperClient.getInstance(this) - for (flipperPlugin in flipperPlugins) flipperClient.addPlugin(flipperPlugin) - flipperClient.start() + override fun onCreate() { + super.onCreate() + Timber.plant(Timber.DebugTree()) + Timber.v("onCreate() called") + setupFlipper() + } + + private fun setupFlipper() { + if (FlipperUtils.shouldEnableFlipper(this)) { + SoLoader.init(this, false) + val flipperClient = AndroidFlipperClient.getInstance(this) + for (flipperPlugin in flipperPlugins) flipperClient.addPlugin(flipperPlugin) + flipperClient.start() + } } - } } diff --git a/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt index 9618238..99a1b54 100644 --- a/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt @@ -9,6 +9,9 @@ import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.databinding.MainActivityBinding +import gq.kirmanak.mealient.ui.auth.AuthenticationState +import gq.kirmanak.mealient.ui.auth.AuthenticationState.AUTHORIZED +import gq.kirmanak.mealient.ui.auth.AuthenticationState.UNAUTHORIZED import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel import timber.log.Timber @@ -16,7 +19,8 @@ import timber.log.Timber class MainActivity : AppCompatActivity() { private lateinit var binding: MainActivityBinding private val authViewModel by viewModels() - private var isAuthenticated = false + private val authenticationState: AuthenticationState + get() = authViewModel.currentAuthenticationState override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -48,32 +52,34 @@ class MainActivity : AppCompatActivity() { private fun listenToAuthStatuses() { Timber.v("listenToAuthStatuses() called") - authViewModel.authenticationStatuses().observe(this) { - changeAuthStatus(it) - } + authViewModel.authenticationState.observe(this, ::onAuthStateUpdate) } - private fun changeAuthStatus(it: Boolean) { - Timber.v("changeAuthStatus() called with: it = $it") - if (isAuthenticated == it) return - isAuthenticated = it + private fun onAuthStateUpdate(authState: AuthenticationState) { + Timber.v("onAuthStateUpdate() called with: it = $authState") invalidateOptionsMenu() } override fun onCreateOptionsMenu(menu: Menu): Boolean { Timber.v("onCreateOptionsMenu() called with: menu = $menu") menuInflater.inflate(R.menu.main_toolbar, menu) - menu.findItem(R.id.logout).isVisible = isAuthenticated + menu.findItem(R.id.logout).isVisible = authenticationState == AUTHORIZED + menu.findItem(R.id.login).isVisible = authenticationState == UNAUTHORIZED return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { Timber.v("onOptionsItemSelected() called with: item = $item") - val result = if (item.itemId == R.id.logout) { - authViewModel.logout() - true - } else { - super.onOptionsItemSelected(item) + val result = when (item.itemId) { + R.id.logout -> { + authViewModel.logout() + true + } + R.id.login -> { + authViewModel.login() + true + } + else -> super.onOptionsItemSelected(item) } return result } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt index da7e524..b0589b2 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt @@ -4,13 +4,13 @@ import kotlinx.coroutines.flow.Flow interface AuthRepo { + val isAuthorizedFlow: Flow + suspend fun authenticate(username: String, password: String) suspend fun getAuthHeader(): String? suspend fun requireAuthHeader(): String - fun authenticationStatuses(): Flow - suspend fun logout() } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt index 5bcacf5..5955e7f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt @@ -3,11 +3,12 @@ package gq.kirmanak.mealient.data.auth import kotlinx.coroutines.flow.Flow interface AuthStorage { + + val authHeaderFlow: Flow + suspend fun storeAuthData(authHeader: String) suspend fun getAuthHeader(): String? - fun authHeaderObservable(): Flow - suspend fun clearAuthData() } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt index 92c0d98..6e26772 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt @@ -15,6 +15,9 @@ class AuthRepoImpl @Inject constructor( private val storage: AuthStorage, ) : AuthRepo { + override val isAuthorizedFlow: Flow + get() = storage.authHeaderFlow.map { it != null } + override suspend fun authenticate(username: String, password: String) { Timber.v("authenticate() called with: username = $username, password = $password") val accessToken = dataSource.authenticate(username, password) @@ -27,11 +30,6 @@ class AuthRepoImpl @Inject constructor( override suspend fun requireAuthHeader(): String = checkNotNull(getAuthHeader()) { "Auth header is null when it was required" } - override fun authenticationStatuses(): Flow { - Timber.v("authenticationStatuses() called") - return storage.authHeaderObservable().map { it != null } - } - override suspend fun logout() { Timber.v("logout() called") storage.clearAuthData() diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt index 8a5c575..94243d4 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt @@ -1,5 +1,6 @@ package gq.kirmanak.mealient.data.auth.impl +import androidx.datastore.preferences.core.Preferences import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.storage.PreferencesStorage import kotlinx.coroutines.flow.Flow @@ -12,7 +13,10 @@ class AuthStorageImpl @Inject constructor( private val preferencesStorage: PreferencesStorage, ) : AuthStorage { - private val authHeaderKey by preferencesStorage::authHeaderKey + private val authHeaderKey: Preferences.Key + get() = preferencesStorage.authHeaderKey + override val authHeaderFlow: Flow + get() = preferencesStorage.valueUpdates(authHeaderKey) override suspend fun storeAuthData(authHeader: String) { Timber.v("storeAuthData() called with: authHeader = $authHeader") @@ -26,11 +30,6 @@ class AuthStorageImpl @Inject constructor( return token } - override fun authHeaderObservable(): Flow { - Timber.v("authHeaderObservable() called") - return preferencesStorage.valueUpdates(authHeaderKey) - } - override suspend fun clearAuthData() { Timber.v("clearAuthData() called") preferencesStorage.removeValues(authHeaderKey) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorageImpl.kt index 2f75d00..2081bac 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorageImpl.kt @@ -1,5 +1,6 @@ package gq.kirmanak.mealient.data.baseurl +import androidx.datastore.preferences.core.Preferences import gq.kirmanak.mealient.data.storage.PreferencesStorage import javax.inject.Inject import javax.inject.Singleton @@ -9,7 +10,8 @@ class BaseURLStorageImpl @Inject constructor( private val preferencesStorage: PreferencesStorage, ) : BaseURLStorage { - private val baseUrlKey by preferencesStorage::baseUrlKey + private val baseUrlKey: Preferences.Key + get() = preferencesStorage.baseUrlKey override suspend fun getBaseURL(): String? = preferencesStorage.getValue(baseUrlKey) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt index 22ffbb4..b72c110 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt @@ -1,6 +1,11 @@ package gq.kirmanak.mealient.data.disclaimer +import kotlinx.coroutines.flow.Flow + interface DisclaimerStorage { + + val isDisclaimerAcceptedFlow: Flow + suspend fun isDisclaimerAccepted(): Boolean suspend fun acceptDisclaimer() diff --git a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt index f7e313b..78fc2a7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt @@ -1,6 +1,9 @@ package gq.kirmanak.mealient.data.disclaimer +import androidx.datastore.preferences.core.Preferences import gq.kirmanak.mealient.data.storage.PreferencesStorage +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -10,7 +13,10 @@ class DisclaimerStorageImpl @Inject constructor( private val preferencesStorage: PreferencesStorage, ) : DisclaimerStorage { - private val isDisclaimerAcceptedKey by preferencesStorage::isDisclaimerAcceptedKey + private val isDisclaimerAcceptedKey: Preferences.Key + get() = preferencesStorage.isDisclaimerAcceptedKey + override val isDisclaimerAcceptedFlow: Flow + get() = preferencesStorage.valueUpdates(isDisclaimerAcceptedKey).map { it == true } override suspend fun isDisclaimerAccepted(): Boolean { Timber.v("isDisclaimerAccepted() called") diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt index b182ba2..0f68663 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt @@ -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() + private val viewModel by activityViewModels() - private val authStatuses by lazy { viewModel.authenticationStatuses() } - private val authStatusObserver = Observer { onAuthStatusChange(it) } - private fun onAuthStatusChange(isAuthenticated: Boolean) { - Timber.v("onAuthStatusChange() called with: isAuthenticated = $isAuthenticated") - if (isAuthenticated) { - authStatuses.removeObserver(authStatusObserver) - navigateToRecipes() - } - } + private val authStatuses: LiveData + 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) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt new file mode 100644 index 0000000..b9b4a6d --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationState.kt @@ -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 + } + } +} diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt index ac8fedd..a5e0592 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt @@ -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 = 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> { Timber.v("authenticate() called with: username = $username, password = $password") val result = MutableLiveData>() @@ -31,16 +39,16 @@ class AuthenticationViewModel @Inject constructor( return result } - fun authenticationStatuses(): LiveData { - 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) } + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt index 892e260..1a76b25 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt @@ -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) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt index d8d9227..33b932e 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt @@ -24,15 +24,20 @@ class BaseURLViewModel @Inject constructor( private set(value) { _screenState.value = value } - val screenState: LiveData by ::_screenState + val screenState: LiveData + 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" + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt index cf20d3b..b9ac72e 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt @@ -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() { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt index 9f3535a..0c796d7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt @@ -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 = _isAccepted + val isAccepted: LiveData + get() = disclaimerStorage.isDisclaimerAcceptedFlow.asLiveData() private val _okayCountDown = MutableLiveData(FULL_COUNT_DOWN_SEC) val okayCountDown: LiveData = _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() { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt index 57d894f..cf4b556 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt @@ -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() + private val authViewModel by activityViewModels() + + 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) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt index 1027e7b..2613863 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt @@ -19,58 +19,58 @@ import javax.inject.Inject @AndroidEntryPoint class RecipeInfoFragment : BottomSheetDialogFragment() { - private val binding by viewBinding(FragmentRecipeInfoBinding::bind) - private val arguments by navArgs() - private val viewModel by viewModels() + private val binding by viewBinding(FragmentRecipeInfoBinding::bind) + private val arguments by navArgs() + private val viewModel by viewModels() - @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 + } } - } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt index 7195021..92fdec5 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt @@ -24,9 +24,11 @@ constructor( ) : ViewModel() { private val _recipeInfo = MutableLiveData() - val recipeInfo: LiveData by ::_recipeInfo + val recipeInfo: LiveData + get() = _recipeInfo private val _listsVisibility = MutableLiveData(RecipeInfoListsVisibility()) - val listsVisibility: LiveData by ::_listsVisibility + val listsVisibility: LiveData + get() = _listsVisibility fun loadRecipeImage(view: ImageView, recipeSlug: String) { Timber.v("loadRecipeImage() called with: view = $view, recipeSlug = $recipeSlug") diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt index b5439ca..32af21d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt @@ -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?) { diff --git a/app/src/main/res/menu/main_toolbar.xml b/app/src/main/res/menu/main_toolbar.xml index 0c27937..bad8b2b 100644 --- a/app/src/main/res/menu/main_toolbar.xml +++ b/app/src/main/res/menu/main_toolbar.xml @@ -1,11 +1,17 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> - + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 0c6a44b..1803ed3 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -9,13 +9,7 @@ android:id="@+id/authenticationFragment" android:name="gq.kirmanak.mealient.ui.auth.AuthenticationFragment" android:label="AuthenticationFragment" - tools:layout="@layout/fragment_authentication"> - - + tools:layout="@layout/fragment_authentication" /> + app:destination="@id/authenticationFragment" /> @@ -58,11 +50,6 @@ android:name="gq.kirmanak.mealient.ui.splash.SplashFragment" android:label="fragment_splash" tools:layout="@layout/fragment_splash"> - Этот проект разрабатывается независимо от основного проекта Meale. Он не связан с разработчиками Mealie. О любых проблемах следует писать в репозиторий Mealient, НЕ в репозиторий Mealie. E-mail не может быть пустым Пароль не может быть пустым - URL не может быть пустым + URL не может быть пустым E-mail или пароль не подходит. Ошибка подключения, проверьте адрес. Неожиданный ответ. Это Mealie? Что-то пошло не так, попробуйте еще раз. Проверьте формат URL: %s Продолжить + Войти \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51004c5..532e868 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ Step: %d E-mail can\'t be empty Password can\'t be empty - URL can\'t be empty + URL can\'t be empty E-mail or password is incorrect. Can\'t connect, check address. Unexpected response. Is it Mealie? @@ -24,4 +24,6 @@ Check URL format: %s Proceed @string/fragment_authentication_unknown_error + @string/menu_main_toolbar_login + Login \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt index bf2904e..466a26c 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt @@ -39,14 +39,14 @@ class AuthRepoImplTest : RobolectricTest() { @Test fun `when not authenticated then first auth status is false`() = runTest { - coEvery { storage.authHeaderObservable() } returns flowOf(null) - assertThat(subject.authenticationStatuses().first()).isFalse() + coEvery { storage.authHeaderFlow } returns flowOf(null) + assertThat(subject.isAuthorizedFlow.first()).isFalse() } @Test fun `when authenticated then first auth status is true`() = runTest { - coEvery { storage.authHeaderObservable() } returns flowOf(TEST_AUTH_HEADER) - assertThat(subject.authenticationStatuses().first()).isTrue() + coEvery { storage.authHeaderFlow } returns flowOf(TEST_AUTH_HEADER) + assertThat(subject.isAuthorizedFlow.first()).isTrue() } @Test(expected = Unauthorized::class) diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt index b9640f0..77e2ea6 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt @@ -35,20 +35,20 @@ class AuthStorageImplTest : HiltRobolectricTest() { @Test fun `when didn't store auth data then first token is null`() = runTest { - assertThat(subject.authHeaderObservable().first()).isNull() + assertThat(subject.authHeaderFlow.first()).isNull() } @Test fun `when stored auth data then first token is correct`() = runTest { subject.storeAuthData(TEST_AUTH_HEADER) - assertThat(subject.authHeaderObservable().first()).isEqualTo(TEST_AUTH_HEADER) + assertThat(subject.authHeaderFlow.first()).isEqualTo(TEST_AUTH_HEADER) } @Test fun `when clearAuthData then first token is null`() = runTest { subject.storeAuthData(TEST_AUTH_HEADER) subject.clearAuthData() - assertThat(subject.authHeaderObservable().first()).isNull() + assertThat(subject.authHeaderFlow.first()).isNull() } @Test