Merge pull request #41 from kirmanak/material-3

Start migration to material 3
This commit is contained in:
Kirill Kamakin
2022-04-09 17:36:28 +05:00
committed by GitHub
34 changed files with 252 additions and 184 deletions

View File

@@ -22,6 +22,9 @@ android {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
buildConfigField "Boolean", "DEBUG_PICASSO", "false"
buildConfigField "Boolean", "LOG_NETWORK", "false"
}
signingConfigs {

View File

@@ -16,6 +16,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import gq.kirmanak.mealient.BuildConfig
import leakcanary.LeakCanary
import okhttp3.Interceptor
import okhttp3.logging.HttpLoggingInterceptor
@@ -30,7 +31,10 @@ object DebugModule {
@IntoSet
fun provideLoggingInterceptor(): Interceptor {
val interceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) }
interceptor.level = HttpLoggingInterceptor.Level.BODY
interceptor.level = when {
BuildConfig.LOG_NETWORK -> HttpLoggingInterceptor.Level.BODY
else -> HttpLoggingInterceptor.Level.BASIC
}
return interceptor
}

View File

@@ -15,7 +15,7 @@
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
tools:ignore="UnusedAttribute"
android:theme="@style/Theme.Mealient">
android:theme="@style/AppTheme">
<activity
android:name=".ui.activity.MainActivity"
android:exported="true">

View File

@@ -4,8 +4,8 @@ import android.widget.ImageView
import androidx.annotation.VisibleForTesting
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
import gq.kirmanak.mealient.ui.ImageLoader
import gq.kirmanak.mealient.ui.images.ImageLoader
import gq.kirmanak.mealient.ui.recipes.RecipeImageLoader
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import timber.log.Timber
import javax.inject.Inject

View File

@@ -10,7 +10,6 @@ import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.data.network.RetrofitBuilder
import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.data.network.createServiceFactory
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl
@@ -19,6 +18,7 @@ import gq.kirmanak.mealient.data.recipes.impl.RecipeRepoImpl
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl
import gq.kirmanak.mealient.data.recipes.network.RecipeService
import gq.kirmanak.mealient.ui.recipes.RecipeImageLoader
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import javax.inject.Named

View File

@@ -6,9 +6,9 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.ui.ImageLoader
import gq.kirmanak.mealient.ui.picasso.ImageLoaderPicasso
import gq.kirmanak.mealient.ui.picasso.PicassoBuilder
import gq.kirmanak.mealient.ui.images.ImageLoader
import gq.kirmanak.mealient.ui.images.ImageLoaderPicasso
import gq.kirmanak.mealient.ui.images.PicassoBuilder
import javax.inject.Singleton
@Module

View File

@@ -12,16 +12,14 @@ import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R
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 timber.log.Timber
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: MainActivityBinding
private val viewModel by viewModels<MainActivityViewModel>()
private var lastAuthenticationState: AuthenticationState? = null
private val title: String by lazy { getString(R.string.app_name) }
private val uiState: MainActivityUiState get() = viewModel.uiState
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -31,7 +29,13 @@ class MainActivity : AppCompatActivity() {
setSupportActionBar(binding.toolbar)
supportActionBar?.setIcon(R.drawable.ic_toolbar)
setToolbarRoundCorner()
listenToAuthStatuses()
viewModel.uiStateLive.observe(this, ::onUiStateChange)
}
private fun onUiStateChange(uiState: MainActivityUiState) {
Timber.v("onUiStateChange() called with: uiState = $uiState")
supportActionBar?.title = if (uiState.titleVisible) title else null
invalidateOptionsMenu()
}
private fun setToolbarRoundCorner() {
@@ -51,22 +55,11 @@ class MainActivity : AppCompatActivity() {
}
}
private fun listenToAuthStatuses() {
Timber.v("listenToAuthStatuses() called")
viewModel.authenticationStateLive.observe(this, ::onAuthStateUpdate)
}
private fun onAuthStateUpdate(authState: AuthenticationState) {
Timber.v("onAuthStateUpdate() called with: it = $authState")
lastAuthenticationState = 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 = lastAuthenticationState == AUTHORIZED
menu.findItem(R.id.login).isVisible = lastAuthenticationState == UNAUTHORIZED
menu.findItem(R.id.logout).isVisible = uiState.canShowLogout
menu.findItem(R.id.login).isVisible = uiState.canShowLogin
return true
}

View File

@@ -0,0 +1,13 @@
package gq.kirmanak.mealient.ui.activity
data class MainActivityUiState(
val loginButtonVisible: Boolean = false,
val titleVisible: Boolean = true,
val isAuthorized: Boolean = false,
) {
val canShowLogin: Boolean
get() = !isAuthorized && loginButtonVisible
val canShowLogout: Boolean
get() = isAuthorized && loginButtonVisible
}

View File

@@ -1,14 +1,10 @@
package gq.kirmanak.mealient.ui.activity
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.*
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.ui.auth.AuthenticationState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -18,16 +14,22 @@ class MainActivityViewModel @Inject constructor(
private val authRepo: AuthRepo,
) : ViewModel() {
private val showLoginButtonFlow = MutableStateFlow(false)
var showLoginButton: Boolean by showLoginButtonFlow::value
private val _uiState = MutableLiveData(MainActivityUiState())
val uiStateLive: LiveData<MainActivityUiState>
get() = _uiState.distinctUntilChanged()
var uiState: MainActivityUiState
get() = checkNotNull(_uiState.value) { "UiState must not be null" }
private set(value) = _uiState.postValue(value)
private val authenticationStateFlow = combine(
showLoginButtonFlow,
authRepo.isAuthorizedFlow,
AuthenticationState::determineState
)
val authenticationStateLive: LiveData<AuthenticationState>
get() = authenticationStateFlow.asLiveData()
init {
authRepo.isAuthorizedFlow
.onEach { isAuthorized -> updateUiState { it.copy(isAuthorized = isAuthorized) } }
.launchIn(viewModelScope)
}
fun updateUiState(updater: (MainActivityUiState) -> MainActivityUiState) {
uiState = updater(uiState)
}
fun logout() {
Timber.v("logout() called")

View File

@@ -2,8 +2,8 @@ package gq.kirmanak.mealient.ui.auth
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.navigation.fragment.findNavController
import by.kirich1409.viewbindingdelegate.viewBinding
@@ -12,19 +12,20 @@ import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.network.NetworkError
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
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 activityViewModel by activityViewModels<MainActivityViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
binding.button.setOnClickListener { onLoginClicked() }
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title =
getString(R.string.app_name)
activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) }
viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult)
}

View File

@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.ui.baseurl
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import by.kirich1409.viewbindingdelegate.viewBinding
@@ -11,6 +12,7 @@ import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.network.NetworkError
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber
@AndroidEntryPoint
@@ -18,12 +20,14 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
private val binding by viewBinding(FragmentBaseUrlBinding::bind)
private val viewModel by viewModels<BaseURLViewModel>()
private val activityViewModel by activityViewModels<MainActivityViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
binding.button.setOnClickListener(::onProceedClick)
viewModel.checkURLResult.observe(viewLifecycleOwner, ::onCheckURLResult)
activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) }
}
private fun onProceedClick(view: View) {

View File

@@ -2,20 +2,22 @@ package gq.kirmanak.mealient.ui.disclaimer
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.navigation.fragment.findNavController
import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentDisclaimerBinding
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import timber.log.Timber
@AndroidEntryPoint
class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
private val binding by viewBinding(FragmentDisclaimerBinding::bind)
private val viewModel by viewModels<DisclaimerViewModel>()
private val activityViewModel by activityViewModels<MainActivityViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -48,7 +50,6 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
binding.okay.isClickable = it == 0
}
viewModel.startCountDown()
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title =
getString(R.string.app_name)
activityViewModel.updateUiState { it.copy(loginButtonVisible = false, titleVisible = true) }
}
}

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.ui
package gq.kirmanak.mealient.ui.images
import android.widget.ImageView
import androidx.annotation.DrawableRes

View File

@@ -1,8 +1,7 @@
package gq.kirmanak.mealient.ui.picasso
package gq.kirmanak.mealient.ui.images
import android.widget.ImageView
import com.squareup.picasso.Picasso
import gq.kirmanak.mealient.ui.ImageLoader
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.ui.picasso
package gq.kirmanak.mealient.ui.images
import android.content.Context
import com.squareup.picasso.OkHttp3Downloader
@@ -22,7 +22,7 @@ class PicassoBuilder @Inject constructor(
Timber.v("buildPicasso() called")
val builder = Picasso.Builder(context)
builder.downloader(OkHttp3Downloader(okHttpClient))
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG_PICASSO) {
builder.loggingEnabled(true)
builder.indicatorsEnabled(true)
builder.listener { _, uri, exception ->

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.data.recipes
package gq.kirmanak.mealient.ui.recipes
import android.widget.ImageView

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import kotlinx.coroutines.launch

View File

@@ -2,7 +2,6 @@ package gq.kirmanak.mealient.ui.recipes
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
@@ -26,9 +25,8 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
activityViewModel.showLoginButton = true
activityViewModel.updateUiState { it.copy(loginButtonVisible = true, titleVisible = false) }
setupRecipeAdapter()
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = null
}
private fun navigateToRecipeInfo(recipeSummaryEntity: RecipeSummaryEntity) {
@@ -64,6 +62,5 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
Timber.v("onDestroyView() called")
// Prevent RV leaking through mObservers list in adapter
binding.recipes.adapter = null
activityViewModel.showLoginButton = false
}
}

View File

@@ -15,19 +15,15 @@ import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding
import timber.log.Timber
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>()
@Inject
lateinit var ingredientsAdapter: RecipeIngredientsAdapter
@Inject
lateinit var instructionsAdapter: RecipeInstructionsAdapter
private val ingredientsAdapter = RecipeIngredientsAdapter()
private val instructionsAdapter = RecipeInstructionsAdapter()
override fun onCreateView(
inflater: LayoutInflater,
@@ -42,22 +38,27 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
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
with(binding) {
ingredientsList.adapter = ingredientsAdapter
instructionsList.adapter = instructionsAdapter
}
viewModel.listsVisibility.observe(viewLifecycleOwner) {
Timber.d("onViewCreated: lists visibility $it")
binding.ingredientsHolder.isVisible = it.areIngredientsVisible
binding.instructionsGroup.isVisible = it.areInstructionsVisible
with(viewModel) {
loadRecipeImage(binding.image, arguments.recipeSlug)
loadRecipeInfo(arguments.recipeId, arguments.recipeSlug)
uiState.observe(viewLifecycleOwner, ::onUiStateChange)
}
}
private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) {
Timber.v("onUiStateChange() called")
ingredientsHolder.isVisible = uiState.areIngredientsVisible
instructionsGroup.isVisible = uiState.areInstructionsVisible
uiState.recipeInfo?.let {
title.text = it.recipeSummaryEntity.name
description.text = it.recipeSummaryEntity.description
ingredientsAdapter.submitList(it.recipeIngredients)
instructionsAdapter.submitList(it.recipeInstructions)
}
}

View File

@@ -1,6 +1,9 @@
package gq.kirmanak.mealient.ui.recipes.info
data class RecipeInfoListsVisibility(
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
data class RecipeInfoUiState(
val areIngredientsVisible: Boolean = false,
val areInstructionsVisible: Boolean = false,
val recipeInfo: FullRecipeInfo? = null,
)

View File

@@ -6,30 +6,21 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.ui.recipes.RecipeImageLoader
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class RecipeInfoViewModel
@Inject
constructor(
class RecipeInfoViewModel @Inject constructor(
private val recipeRepo: RecipeRepo,
private val recipeImageLoader: RecipeImageLoader,
private val recipeIngredientsAdapter: RecipeIngredientsAdapter,
private val recipeInstructionsAdapter: RecipeInstructionsAdapter,
) : ViewModel() {
private val _recipeInfo = MutableLiveData<FullRecipeInfo>()
val recipeInfo: LiveData<FullRecipeInfo>
get() = _recipeInfo
private val _listsVisibility = MutableLiveData(RecipeInfoListsVisibility())
val listsVisibility: LiveData<RecipeInfoListsVisibility>
get() = _listsVisibility
private val _uiState = MutableLiveData(RecipeInfoUiState())
val uiState: LiveData<RecipeInfoUiState> get() = _uiState
fun loadRecipeImage(view: ImageView, recipeSlug: String) {
Timber.v("loadRecipeImage() called with: view = $view, recipeSlug = $recipeSlug")
@@ -38,21 +29,16 @@ constructor(
fun loadRecipeInfo(recipeId: Long, recipeSlug: String) {
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug")
_listsVisibility.value = RecipeInfoListsVisibility()
recipeIngredientsAdapter.submitList(null)
recipeInstructionsAdapter.submitList(null)
_uiState.value = RecipeInfoUiState()
viewModelScope.launch {
runCatchingExceptCancel { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) }
.onSuccess {
Timber.d("loadRecipeInfo: received recipe info = $it")
_recipeInfo.value = it
recipeIngredientsAdapter.submitList(it.recipeIngredients)
recipeInstructionsAdapter.submitList(it.recipeInstructions)
_listsVisibility.value =
RecipeInfoListsVisibility(
areIngredientsVisible = it.recipeIngredients.isNotEmpty(),
areInstructionsVisible = it.recipeInstructions.isNotEmpty()
)
_uiState.value = RecipeInfoUiState(
areIngredientsVisible = it.recipeIngredients.isNotEmpty(),
areInstructionsVisible = it.recipeInstructions.isNotEmpty(),
recipeInfo = it,
)
}
.onFailure { Timber.e(it, "loadRecipeInfo: can't load recipe info") }
}

View File

@@ -9,11 +9,8 @@ import gq.kirmanak.mealient.data.recipes.db.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding
import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecipeIngredientsAdapter @Inject constructor() :
class RecipeIngredientsAdapter :
ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) {
class RecipeIngredientViewHolder(

View File

@@ -10,11 +10,8 @@ import gq.kirmanak.mealient.data.recipes.db.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding
import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecipeInstructionsAdapter @Inject constructor() :
class RecipeInstructionsAdapter :
ListAdapter<RecipeInstructionEntity, RecipeInstructionViewHolder>(RecipeInstructionDiffCallback) {
private object RecipeInstructionDiffCallback :

View File

@@ -23,12 +23,11 @@ class SplashViewModel @Inject constructor(
init {
viewModelScope.launch {
delay(1000)
_nextDestination.value = if (!disclaimerStorage.isDisclaimerAccepted())
SplashFragmentDirections.actionSplashFragmentToDisclaimerFragment()
else if (baseURLStorage.getBaseURL() == null)
SplashFragmentDirections.actionSplashFragmentToBaseURLFragment()
else
SplashFragmentDirections.actionSplashFragmentToRecipesFragment()
_nextDestination.value = when {
!disclaimerStorage.isDisclaimerAccepted() -> SplashFragmentDirections.actionSplashFragmentToDisclaimerFragment()
baseURLStorage.getBaseURL() == null -> SplashFragmentDirections.actionSplashFragmentToBaseURLFragment()
else -> SplashFragmentDirections.actionSplashFragmentToRecipesFragment()
}
}
}
}

View File

@@ -42,11 +42,11 @@
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button"
android:text="@string/fragment_authentication_button_login"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
style="@style/SmallMarginButton"
app:layout_constraintTop_toBottomOf="@+id/password_input_layout" />
android:id="@+id/button"
style="@style/SmallMarginButton"
android:text="@string/fragment_authentication_button_login"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password_input_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -8,20 +8,16 @@
<com.google.android.material.card.MaterialCardView
android:id="@+id/main_text_holder"
style="@style/Widget.MaterialComponents.CardView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="40dp"
android:elevation="0dp"
app:cardCornerRadius="@dimen/rounded_corner_size_default"
app:cardForegroundColor="#26C4C4C4"
app:cardElevation="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/okay"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:strokeColor="#FFDCC8BF"
app:strokeWidth="1dp">
app:shapeAppearance="@style/ShapeAppearance.AllCornersRounded">
<TextView
android:id="@+id/main_text"
@@ -36,11 +32,9 @@
<Button
android:id="@+id/okay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:clickable="false"
tools:text="Okay (3 seconds)"
style="@style/SmallMarginButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_text_holder" />

View File

@@ -77,7 +77,7 @@
android:layout_marginHorizontal="8dp"
android:layout_marginTop="11dp"
android:layout_marginBottom="20dp"
app:cardCornerRadius="@dimen/rounded_corner_size_default"
app:shapeAppearance="@style/ShapeAppearance.AllCornersRounded"
app:cardElevation="10dp"
app:layout_constraintBottom_toTopOf="@+id/instructions_header"
app:layout_constraintEnd_toStartOf="@+id/end_guide"

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_margin="8dp"
app:cardCornerRadius="@dimen/rounded_corner_size_default"
app:shapeAppearance="@style/ShapeAppearance.AllCornersRounded"
app:cardElevation="10dp"
android:layout_height="wrap_content">

View File

@@ -6,7 +6,7 @@
android:id="@+id/login"
android:contentDescription="@string/menu_main_toolbar_content_description_login"
android:title="@string/menu_main_toolbar_login"
app:showAsAction="never" />
app:showAsAction="ifRoom" />
<item
android:id="@+id/logout"

View File

@@ -1,17 +1,31 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Mealient" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:overScrollMode">never</item>
</style>
<style name="AppTheme" parent="Theme.Material3.Dark.NoActionBar">
<item name="colorPrimary">@color/md_theme_dark_primary</item>
<item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
<item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
<item name="colorSecondary">@color/md_theme_dark_secondary</item>
<item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
<item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
<item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
<item name="colorTertiary">@color/md_theme_dark_tertiary</item>
<item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
<item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
<item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
<item name="colorError">@color/md_theme_dark_error</item>
<item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
<item name="colorOnError">@color/md_theme_dark_onError</item>
<item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
<item name="android:colorBackground">@color/md_theme_dark_background</item>
<item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
<item name="colorSurface">@color/md_theme_dark_surface</item>
<item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
<item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
<item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
<item name="colorOutline">@color/md_theme_dark_outline</item>
<item name="colorOnSurfaceInverse">@color/md_theme_dark_inverseOnSurface</item>
<item name="colorSurfaceInverse">@color/md_theme_dark_inverseSurface</item>
<item name="colorPrimaryInverse">@color/md_theme_dark_primaryInverse</item>
</style>
</resources>

View File

@@ -1,12 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="primary">#FF9D76DC</color>
<color name="md_theme_light_primary">#7743B5</color>
<color name="md_theme_light_onPrimary">#FFFFFF</color>
<color name="md_theme_light_primaryContainer">#EFDBFF</color>
<color name="md_theme_light_onPrimaryContainer">#290054</color>
<color name="md_theme_light_secondary">#655A70</color>
<color name="md_theme_light_onSecondary">#FFFFFF</color>
<color name="md_theme_light_secondaryContainer">#ECDDF7</color>
<color name="md_theme_light_onSecondaryContainer">#201829</color>
<color name="md_theme_light_tertiary">#805159</color>
<color name="md_theme_light_onTertiary">#FFFFFF</color>
<color name="md_theme_light_tertiaryContainer">#FFD9DF</color>
<color name="md_theme_light_onTertiaryContainer">#321118</color>
<color name="md_theme_light_error">#BA1B1B</color>
<color name="md_theme_light_errorContainer">#FFDAD4</color>
<color name="md_theme_light_onError">#FFFFFF</color>
<color name="md_theme_light_onErrorContainer">#410001</color>
<color name="md_theme_light_background">#FFFBFC</color>
<color name="md_theme_light_onBackground">#1D1B1E</color>
<color name="md_theme_light_surface">#FFFBFC</color>
<color name="md_theme_light_onSurface">#1D1B1E</color>
<color name="md_theme_light_surfaceVariant">#E8DFEB</color>
<color name="md_theme_light_onSurfaceVariant">#4A454E</color>
<color name="md_theme_light_outline">#7B757E</color>
<color name="md_theme_light_inverseOnSurface">#F5EFF3</color>
<color name="md_theme_light_inverseSurface">#322F33</color>
<color name="md_theme_light_inversePrimary">#DBB8FF</color>
<color name="md_theme_light_shadow">#000000</color>
<color name="md_theme_light_primaryInverse">#DBB8FF</color>
<color name="md_theme_dark_primary">#DBB8FF</color>
<color name="md_theme_dark_onPrimary">#460283</color>
<color name="md_theme_dark_primaryContainer">#5E289B</color>
<color name="md_theme_dark_onPrimaryContainer">#EFDBFF</color>
<color name="md_theme_dark_secondary">#CFC1DA</color>
<color name="md_theme_dark_onSecondary">#362D40</color>
<color name="md_theme_dark_secondaryContainer">#4D4357</color>
<color name="md_theme_dark_onSecondaryContainer">#ECDDF7</color>
<color name="md_theme_dark_tertiary">#F2B7C0</color>
<color name="md_theme_dark_onTertiary">#4B252C</color>
<color name="md_theme_dark_tertiaryContainer">#653A42</color>
<color name="md_theme_dark_onTertiaryContainer">#FFD9DF</color>
<color name="md_theme_dark_error">#FFB4A9</color>
<color name="md_theme_dark_errorContainer">#930006</color>
<color name="md_theme_dark_onError">#680003</color>
<color name="md_theme_dark_onErrorContainer">#FFDAD4</color>
<color name="md_theme_dark_background">#1D1B1E</color>
<color name="md_theme_dark_onBackground">#E7E1E5</color>
<color name="md_theme_dark_surface">#1D1B1E</color>
<color name="md_theme_dark_onSurface">#E7E1E5</color>
<color name="md_theme_dark_surfaceVariant">#4A454E</color>
<color name="md_theme_dark_onSurfaceVariant">#CCC4CF</color>
<color name="md_theme_dark_outline">#958E98</color>
<color name="md_theme_dark_inverseOnSurface">#1D1B1E</color>
<color name="md_theme_dark_inverseSurface">#E7E1E5</color>
<color name="md_theme_dark_inversePrimary">#7743B5</color>
<color name="md_theme_dark_shadow">#000000</color>
<color name="md_theme_dark_primaryInverse">#7743B5</color>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="SmallMarginTextInputLayoutStyle" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<style name="SmallMarginTextInputLayoutStyle">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_margin">@dimen/margin_small</item>
@@ -13,17 +13,17 @@
<item name="android:layout_margin">@dimen/margin_small</item>
</style>
<style name="ShapeAppearance.AllCornersRounded" parent="ShapeAppearance.MaterialComponents">
<style name="ShapeAppearance.AllCornersRounded" parent="ShapeAppearance.Material3.LargeComponent">
<item name="cornerSize">@dimen/rounded_corner_size_default</item>
</style>
<!-- This is a workaround to support always round corners of the bottom sheet
See more at https://github.com/material-components/material-components-android/pull/437#issuecomment-852461685 -->
<style name="NoShapeBottomSheetDialog" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog">
<style name="NoShapeBottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
<item name="bottomSheetStyle">@style/NoShapeBottomSheet</item>
</style>
<style name="NoShapeBottomSheet" parent="Widget.MaterialComponents.BottomSheet.Modal">
<style name="NoShapeBottomSheet" parent="Widget.Material3.BottomSheet.Modal">
<item name="shapeAppearance">@null</item>
<item name="shapeAppearanceOverlay">@null</item>
<item name="android:background">@drawable/recipe_info_background</item>

View File

@@ -1,17 +1,32 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Mealient" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimary</item>
<!-- Customize your theme here. -->
<item name="android:overScrollMode">never</item>
</style>
<style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">@color/md_theme_light_primary</item>
<item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
<item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/md_theme_light_onPrimaryContainer</item>
<item name="colorSecondary">@color/md_theme_light_secondary</item>
<item name="colorOnSecondary">@color/md_theme_light_onSecondary</item>
<item name="colorSecondaryContainer">@color/md_theme_light_secondaryContainer</item>
<item name="colorOnSecondaryContainer">@color/md_theme_light_onSecondaryContainer</item>
<item name="colorTertiary">@color/md_theme_light_tertiary</item>
<item name="colorOnTertiary">@color/md_theme_light_onTertiary</item>
<item name="colorTertiaryContainer">@color/md_theme_light_tertiaryContainer</item>
<item name="colorOnTertiaryContainer">@color/md_theme_light_onTertiaryContainer</item>
<item name="colorError">@color/md_theme_light_error</item>
<item name="colorErrorContainer">@color/md_theme_light_errorContainer</item>
<item name="colorOnError">@color/md_theme_light_onError</item>
<item name="colorOnErrorContainer">@color/md_theme_light_onErrorContainer</item>
<item name="android:colorBackground">@color/md_theme_light_background</item>
<item name="colorOnBackground">@color/md_theme_light_onBackground</item>
<item name="colorSurface">@color/md_theme_light_surface</item>
<item name="colorOnSurface">@color/md_theme_light_onSurface</item>
<item name="colorSurfaceVariant">@color/md_theme_light_surfaceVariant</item>
<item name="colorOnSurfaceVariant">@color/md_theme_light_onSurfaceVariant</item>
<item name="colorOutline">@color/md_theme_light_outline</item>
<item name="colorOnSurfaceInverse">@color/md_theme_light_inverseOnSurface</item>
<item name="colorSurfaceInverse">@color/md_theme_light_inverseSurface</item>
<item name="colorPrimaryInverse">@color/md_theme_light_primaryInverse</item>
<item name="android:overScrollMode">never</item>
</style>
</resources>

View File

@@ -2,7 +2,7 @@ package gq.kirmanak.mealient.data.recipes.impl
import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
import gq.kirmanak.mealient.ui.ImageLoader
import gq.kirmanak.mealient.ui.images.ImageLoader
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK