diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/BaseActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/BaseActivity.kt new file mode 100644 index 0000000..495c696 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/BaseActivity.kt @@ -0,0 +1,36 @@ +package gq.kirmanak.mealient.ui + +import android.os.Bundle +import android.view.View +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowInsetsControllerCompat +import androidx.viewbinding.ViewBinding +import by.kirich1409.viewbindingdelegate.viewBinding +import gq.kirmanak.mealient.extensions.isDarkThemeOn +import gq.kirmanak.mealient.logging.Logger +import javax.inject.Inject + +abstract class BaseActivity( + binder: (View) -> T, + @IdRes containerId: Int, + @LayoutRes layoutRes: Int, +) : AppCompatActivity(layoutRes) { + + protected val binding by viewBinding(binder, containerId) + + @Inject + lateinit var logger: Logger + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } + setContentView(binding.root) + with(WindowInsetsControllerCompat(window, window.decorView)) { + val isAppearanceLightBars = !isDarkThemeOn() + isAppearanceLightNavigationBars = isAppearanceLightBars + isAppearanceLightStatusBars = isAppearanceLightBars + } + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt index faf6f21..042f699 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt @@ -15,6 +15,9 @@ sealed class OperationUiState { val isProgress: Boolean get() = this is Progress + val isFailure: Boolean + get() = this is Failure + fun updateButtonState(button: Button) { button.isEnabled = !isProgress button.isClickable = !isProgress diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt index cf4c5ac..af6c010 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt @@ -3,16 +3,13 @@ package gq.kirmanak.mealient.ui.activity import android.os.Bundle import android.view.MenuItem import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.isVisible import androidx.core.view.iterator import androidx.drawerlayout.widget.DrawerLayout import androidx.navigation.NavController import androidx.navigation.NavDirections import androidx.navigation.fragment.NavHostFragment -import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAddRecipeFragment import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAuthenticationFragment @@ -21,28 +18,24 @@ import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalRecipesList import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.MainActivityBinding import gq.kirmanak.mealient.extensions.collectWhenResumed -import gq.kirmanak.mealient.extensions.isDarkThemeOn import gq.kirmanak.mealient.extensions.observeOnce -import gq.kirmanak.mealient.logging.Logger -import javax.inject.Inject +import gq.kirmanak.mealient.ui.BaseActivity @AndroidEntryPoint -class MainActivity : AppCompatActivity(R.layout.main_activity) { +class MainActivity : BaseActivity( + binder = MainActivityBinding::bind, + containerId = R.id.drawer, + layoutRes = R.layout.main_activity, +) { - private val binding: MainActivityBinding by viewBinding(MainActivityBinding::bind, R.id.drawer) private val viewModel by viewModels() private val navController: NavController get() = binding.navHost.getFragment().navController - @Inject - lateinit var logger: Logger - override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) - logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } splashScreen.setKeepOnScreenCondition { viewModel.startDestination.value == null } - setContentView(binding.root) setupUi() configureNavGraph() } @@ -67,11 +60,6 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { viewModel.onSearchQuery(query.trim().takeUnless { it.isEmpty() }) } binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) - with(WindowInsetsControllerCompat(window, window.decorView)) { - val isAppearanceLightBars = !isDarkThemeOn() - isAppearanceLightNavigationBars = isAppearanceLightBars - isAppearanceLightStatusBars = isAppearanceLightBars - } viewModel.uiStateLive.observe(this, ::onUiStateChange) collectWhenResumed(viewModel.clearSearchViewFocus) { logger.d { "clearSearchViewFocus(): received event" } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/share/ShareRecipeActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/share/ShareRecipeActivity.kt index 437dd30..6b30c8d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/share/ShareRecipeActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/share/ShareRecipeActivity.kt @@ -1,26 +1,31 @@ package gq.kirmanak.mealient.ui.share import android.content.Intent +import android.graphics.drawable.Animatable2 +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable import android.os.Bundle import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isInvisible +import androidx.core.view.postDelayed import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R +import gq.kirmanak.mealient.databinding.ActivityShareRecipeBinding import gq.kirmanak.mealient.extensions.showLongToast -import gq.kirmanak.mealient.logging.Logger -import javax.inject.Inject +import gq.kirmanak.mealient.ui.BaseActivity +import gq.kirmanak.mealient.ui.OperationUiState @AndroidEntryPoint -class ShareRecipeActivity : AppCompatActivity() { +class ShareRecipeActivity : BaseActivity( + binder = ActivityShareRecipeBinding::bind, + containerId = R.id.root, + layoutRes = R.layout.activity_share_recipe, +) { private val viewModel: ShareRecipeViewModel by viewModels() - @Inject - lateinit var logger: Logger - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } if (intent.action != Intent.ACTION_SEND || intent.type != "text/plain") { logger.w { "onCreate: intent.action = ${intent.action}, intent.type = ${intent.type}" } @@ -35,14 +40,54 @@ class ShareRecipeActivity : AppCompatActivity() { return } - viewModel.saveOperationResult.observe(this) { + restartAnimationOnEnd() + viewModel.saveResult.observe(this, ::onStateUpdate) + viewModel.saveRecipeByURL(url) + } + + private fun onStateUpdate(state: OperationUiState) { + binding.progress.isInvisible = !state.isProgress + withAnimatedDrawable { + if (state.isProgress) start() else stop() + } + if (state.isSuccess || state.isFailure) { showLongToast( - if (it.isSuccess) R.string.activity_share_recipe_success_toast + if (state.isSuccess) R.string.activity_share_recipe_success_toast else R.string.activity_share_recipe_failure_toast ) finish() } - - viewModel.saveRecipeByURL(url) } + + private fun restartAnimationOnEnd() { + withAnimatedDrawable { + onAnimationEnd { + if (viewModel.saveResult.value?.isProgress == true) { + binding.progress.postDelayed(250) { start() } + } + } + } + } + + private inline fun withAnimatedDrawable(block: AnimatedVectorDrawable.() -> Unit) { + binding.progress.drawable.let { drawable -> + if (drawable is AnimatedVectorDrawable) { + drawable.block() + } else { + logger.w { "withAnimatedDrawable: progress's drawable is not AnimatedVectorDrawable" } + } + } + } +} + +private inline fun AnimatedVectorDrawable.onAnimationEnd( + crossinline block: AnimatedVectorDrawable.() -> Unit, +): Animatable2.AnimationCallback { + val callback = object : Animatable2.AnimationCallback() { + override fun onAnimationEnd(drawable: Drawable?) { + block() + } + } + registerAnimationCallback(callback) + return callback } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/share/ShareRecipeViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/share/ShareRecipeViewModel.kt index b82569d..8172e88 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/share/ShareRecipeViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/share/ShareRecipeViewModel.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.share.ShareRecipeRepo import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.ui.OperationUiState import kotlinx.coroutines.launch import javax.inject.Inject @@ -17,21 +18,17 @@ class ShareRecipeViewModel @Inject constructor( private val logger: Logger, ) : ViewModel() { - private val _saveOperationResult = MutableLiveData>() - val saveOperationResult: LiveData> get() = _saveOperationResult + private val _saveResult = MutableLiveData>(OperationUiState.Initial()) + val saveResult: LiveData> get() = _saveResult fun saveRecipeByURL(url: CharSequence) { logger.v { "saveRecipeByURL() called with: url = $url" } + _saveResult.postValue(OperationUiState.Progress()) viewModelScope.launch { - runCatchingExceptCancel { - shareRecipeRepo.saveRecipeByURL(url) - }.onSuccess { - logger.d { "Successfully saved recipe by URL" } - _saveOperationResult.postValue(Result.success(it)) - }.onFailure { - logger.e(it) { "Can't save recipe by URL" } - _saveOperationResult.postValue(Result.failure(it)) - } + val result = runCatchingExceptCancel { shareRecipeRepo.saveRecipeByURL(url) } + .onSuccess { logger.d { "Successfully saved recipe by URL" } } + .onFailure { logger.e(it) { "Can't save recipe by URL" } } + _saveResult.postValue(OperationUiState.fromResult(result)) } } } \ No newline at end of file diff --git a/app/src/main/res/drawable-v31/ic_splash_screen.xml b/app/src/main/res/drawable/ic_progress_bar.xml similarity index 100% rename from app/src/main/res/drawable-v31/ic_splash_screen.xml rename to app/src/main/res/drawable/ic_progress_bar.xml diff --git a/app/src/main/res/layout/activity_share_recipe.xml b/app/src/main/res/layout/activity_share_recipe.xml new file mode 100644 index 0000000..7919d60 --- /dev/null +++ b/app/src/main/res/layout/activity_share_recipe.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9ea13b7..3d341a7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -53,4 +53,5 @@ Нет рецептов Рецепт успешно сохранен. Что-то пошло не так. + Индикатор прогресса \ No newline at end of file diff --git a/app/src/main/res/values-v31/drawable.xml b/app/src/main/res/values-v31/drawable.xml new file mode 100644 index 0000000..3983709 --- /dev/null +++ b/app/src/main/res/values-v31/drawable.xml @@ -0,0 +1,4 @@ + + + @drawable/ic_progress_bar + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index abdee58..c0e1fcf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,4 +56,5 @@ No recipes Recipe saved successfully. Something went wrong. + Progress indicator \ No newline at end of file