Implement shopping lists screen (#129)

* Initialize shopping lists feature

* Start shopping lists screen with Compose

* Add icon to shopping list name

* Add shopping lists to menu

* Set max size for the list

* Replace compose-adapter with accompanist

* Remove unused fields from shopping lists response

* Show list of shopping lists from BE

* Hide shopping lists if Mealie is 0.5.6

* Add shopping list item click listener

* Create material app theme for Compose

* Use shorter names

* Load shopping lists by pages and save to db

* Make page handling logic match recipes

* Add swipe to refresh to shopping lists

* Extract SwipeToRefresh Composable

* Make LazyPagingColumn generic

* Show refresh only when mediator is refreshing

* Do not refresh automatically

* Allow controlling Activity state from modules

* Implement navigating to shopping list screen

* Move Compose libraries setup to a plugin

* Implement loading full shopping list info

* Move Storage classes to database module

* Save shopping list items to DB

* Use separate names for separate ids

* Do only one DB version update

* Use unique names for all columns

* Display shopping list items

* Move OperationUiState to ui module

* Subscribe to shopping lists updates

* Indicate progress with progress bar

* Use strings from resources

* Format shopping list item quantities

* Hide unit/food/note/quantity if they are not set

* Implement updating shopping list item checked state

* Remove unnecessary null checks

* Disable checkbox when it is being updated

* Split shopping list screen into composables

* Show items immediately if they are saved

* Fix showing "list is empty" before the items

* Show Snackbar when error happens

* Reduce shopping list items paddings

* Remove shopping lists when URL is changed

* Add error/empty state handling to shopping lists

* Fix empty error state

* Fix tests compilation

* Add margin between text and button

* Add divider between checked and unchecked items

* Move divider to the item

* Refresh the shopping lists on authentication

* Use retry when necessary

* Remove excessive logging

* Fix pages bounds check

* Move FlowExtensionsTest

* Update Compose version

* Fix showing loading indicator for shopping lists

* Add Russian translation

* Fix SDK version lint check

* Rename parameter to match interface

* Add DB migration TODO

* Get rid of DB migrations

* Do not use pagination with shopping lists

* Cleanup after the pagination removal

* Load shopping list items

* Remove shopping lists storage

* Rethrow CancellationException in LoadingHelper

* Add pull-to-refresh on shopping list screen

* Extract LazyColumnWithLoadingState

* Split refresh errors and loading state

* Reuse LazyColumnWithLoadingState for shopping list items

* Remove paging-compose dependency

* Refresh shopping list items on authentication

* Disable missing translation lint check

* Update Compose and Kotlin versions

* Fix order of checked items

* Hide useless information from a shopping list item
This commit is contained in:
Kirill Kamakin
2023-07-03 15:07:19 +02:00
committed by GitHub
parent a40f9a78ea
commit 1e5e727e92
157 changed files with 3360 additions and 3715 deletions

View File

@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.data.add
import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
interface AddRecipeDataSource {
suspend fun addRecipe(recipe: AddRecipeInfo): String

View File

@@ -1,23 +0,0 @@
package gq.kirmanak.mealient.data.add
data class AddRecipeInfo(
val name: String,
val description: String,
val recipeYield: String,
val recipeIngredient: List<AddRecipeIngredientInfo>,
val recipeInstructions: List<AddRecipeInstructionInfo>,
val settings: AddRecipeSettingsInfo,
)
data class AddRecipeSettingsInfo(
val disableComments: Boolean,
val public: Boolean,
)
data class AddRecipeIngredientInfo(
val note: String,
)
data class AddRecipeInstructionInfo(
val text: String,
)

View File

@@ -1,5 +1,6 @@
package gq.kirmanak.mealient.data.add
import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
import kotlinx.coroutines.flow.Flow
interface AddRecipeRepo {

View File

@@ -1,12 +1,11 @@
package gq.kirmanak.mealient.data.add.impl
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
import gq.kirmanak.mealient.data.add.AddRecipeInfo
import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
import gq.kirmanak.mealient.extensions.toAddRecipeInfo
import gq.kirmanak.mealient.extensions.toDraft
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.model_mapper.ModelMapper
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -18,14 +17,15 @@ class AddRecipeRepoImpl @Inject constructor(
private val addRecipeDataSource: AddRecipeDataSource,
private val addRecipeStorage: AddRecipeStorage,
private val logger: Logger,
private val modelMapper: ModelMapper,
) : AddRecipeRepo {
override val addRecipeRequestFlow: Flow<AddRecipeInfo>
get() = addRecipeStorage.updates.map { it.toAddRecipeInfo() }
get() = addRecipeStorage.updates.map { modelMapper.toAddRecipeInfo(it) }
override suspend fun preserve(recipe: AddRecipeInfo) {
logger.v { "preserveRecipe() called with: recipe = $recipe" }
addRecipeStorage.save(recipe.toDraft())
addRecipeStorage.save(modelMapper.toDraft(recipe))
}
override suspend fun clear() {

View File

@@ -1,10 +1,11 @@
package gq.kirmanak.mealient.data.auth
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo
import kotlinx.coroutines.flow.Flow
interface AuthRepo {
interface AuthRepo : ShoppingListsAuthRepo {
val isAuthorizedFlow: Flow<Boolean>
override val isAuthorizedFlow: Flow<Boolean>
suspend fun authenticate(email: String, password: String)

View File

@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.data.baseurl
import kotlinx.coroutines.flow.Flow
interface ServerInfoRepo {
suspend fun getUrl(): String?
@@ -8,5 +10,7 @@ interface ServerInfoRepo {
suspend fun tryBaseURL(baseURL: String): Result<Unit>
fun versionUpdates(): Flow<ServerVersion>
}

View File

@@ -3,6 +3,9 @@ package gq.kirmanak.mealient.data.baseurl
import gq.kirmanak.mealient.datasource.ServerUrlProvider
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
@@ -55,4 +58,11 @@ class ServerInfoRepoImpl @Inject constructor(
serverInfoStorage.storeBaseURL(oldBaseUrl, oldVersion)
}
}
override fun versionUpdates(): Flow<ServerVersion> {
return serverInfoStorage
.serverVersionUpdates()
.filterNotNull()
.map { determineServerVersion(it) }
}
}

View File

@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.data.baseurl
import kotlinx.coroutines.flow.Flow
interface ServerInfoStorage {
suspend fun getBaseURL(): String?
@@ -12,4 +14,5 @@ interface ServerInfoStorage {
suspend fun getServerVersion(): String?
fun serverVersionUpdates(): Flow<String?>
}

View File

@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.data.baseurl
import gq.kirmanak.mealient.datasource.models.VersionInfo
interface VersionDataSource {
suspend fun getVersionInfo(): VersionInfo

View File

@@ -1,9 +1,10 @@
package gq.kirmanak.mealient.data.baseurl
import gq.kirmanak.mealient.datasource.models.VersionInfo
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.extensions.toVersionInfo
import gq.kirmanak.mealient.model_mapper.ModelMapper
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@@ -14,15 +15,16 @@ import javax.inject.Singleton
class VersionDataSourceImpl @Inject constructor(
private val v0Source: MealieDataSourceV0,
private val v1Source: MealieDataSourceV1,
private val modelMapper: ModelMapper,
) : VersionDataSource {
override suspend fun getVersionInfo(): VersionInfo {
val responses = coroutineScope {
val v0Deferred = async {
runCatchingExceptCancel { v0Source.getVersionInfo().toVersionInfo() }
runCatchingExceptCancel { modelMapper.toVersionInfo(v0Source.getVersionInfo()) }
}
val v1Deferred = async {
runCatchingExceptCancel { v1Source.getVersionInfo().toVersionInfo() }
runCatchingExceptCancel { modelMapper.toVersionInfo(v1Source.getVersionInfo()) }
}
listOf(v0Deferred, v1Deferred).awaitAll()
}

View File

@@ -1,5 +0,0 @@
package gq.kirmanak.mealient.data.baseurl
data class VersionInfo(
val version: String,
)

View File

@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.data.baseurl.impl
import androidx.datastore.preferences.core.Preferences
import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
import gq.kirmanak.mealient.data.storage.PreferencesStorage
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton
@@ -49,6 +50,10 @@ class ServerInfoStorageImpl @Inject constructor(
preferencesStorage.storeValues(Pair(serverVersionKey, version))
}
override fun serverVersionUpdates(): Flow<String?> {
return preferencesStorage.valueUpdates(serverVersionKey)
}
private suspend fun <T> getValue(key: Preferences.Key<T>): T? = preferencesStorage.getValue(key)
}

View File

@@ -1,22 +1,17 @@
package gq.kirmanak.mealient.data.network
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
import gq.kirmanak.mealient.data.add.AddRecipeInfo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.baseurl.ServerVersion
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.data.share.ParseRecipeDataSource
import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo
import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
import gq.kirmanak.mealient.datasource.models.RecipeSummaryInfo
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.extensions.toFullRecipeInfo
import gq.kirmanak.mealient.extensions.toRecipeSummaryInfo
import gq.kirmanak.mealient.extensions.toV0Request
import gq.kirmanak.mealient.extensions.toV1CreateRequest
import gq.kirmanak.mealient.extensions.toV1Request
import gq.kirmanak.mealient.extensions.toV1UpdateRequest
import gq.kirmanak.mealient.model_mapper.ModelMapper
import javax.inject.Inject
import javax.inject.Singleton
@@ -25,15 +20,16 @@ class MealieDataSourceWrapper @Inject constructor(
private val serverInfoRepo: ServerInfoRepo,
private val v0Source: MealieDataSourceV0,
private val v1Source: MealieDataSourceV1,
private val modelMapper: ModelMapper,
) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource {
private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion()
override suspend fun addRecipe(recipe: AddRecipeInfo): String = when (getVersion()) {
ServerVersion.V0 -> v0Source.addRecipe(recipe.toV0Request())
ServerVersion.V0 -> v0Source.addRecipe(modelMapper.toV0Request(recipe))
ServerVersion.V1 -> {
val slug = v1Source.createRecipe(recipe.toV1CreateRequest())
v1Source.updateRecipe(slug, recipe.toV1UpdateRequest())
val slug = v1Source.createRecipe(modelMapper.toV1CreateRequest(recipe))
v1Source.updateRecipe(slug, modelMapper.toV1UpdateRequest(recipe))
slug
}
}
@@ -43,25 +39,25 @@ class MealieDataSourceWrapper @Inject constructor(
limit: Int,
): List<RecipeSummaryInfo> = when (getVersion()) {
ServerVersion.V0 -> {
v0Source.requestRecipes(start, limit).map { it.toRecipeSummaryInfo() }
v0Source.requestRecipes(start, limit).map { modelMapper.toRecipeSummaryInfo(it) }
}
ServerVersion.V1 -> {
// Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3
val page = start / limit + 1
v1Source.requestRecipes(page, limit).map { it.toRecipeSummaryInfo() }
v1Source.requestRecipes(page, limit).map { modelMapper.toRecipeSummaryInfo(it) }
}
}
override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = when (getVersion()) {
ServerVersion.V0 -> v0Source.requestRecipeInfo(slug).toFullRecipeInfo()
ServerVersion.V1 -> v1Source.requestRecipeInfo(slug).toFullRecipeInfo()
ServerVersion.V0 -> modelMapper.toFullRecipeInfo(v0Source.requestRecipeInfo(slug))
ServerVersion.V1 -> modelMapper.toFullRecipeInfo(v1Source.requestRecipeInfo(slug))
}
override suspend fun parseRecipeFromURL(
parseRecipeURLInfo: ParseRecipeURLInfo,
): String = when (getVersion()) {
ServerVersion.V0 -> v0Source.parseRecipeFromURL(parseRecipeURLInfo.toV0Request())
ServerVersion.V1 -> v1Source.parseRecipeFromURL(parseRecipeURLInfo.toV1Request())
ServerVersion.V0 -> v0Source.parseRecipeFromURL(modelMapper.toV0Request(parseRecipeURLInfo))
ServerVersion.V1 -> v1Source.parseRecipeFromURL(modelMapper.toV1Request(parseRecipeURLInfo))
}
override suspend fun getFavoriteRecipes(): List<String> = when (getVersion()) {

View File

@@ -1,8 +1,8 @@
package gq.kirmanak.mealient.data.recipes
import androidx.paging.Pager
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
interface RecipeRepo {
@@ -12,7 +12,7 @@ interface RecipeRepo {
suspend fun refreshRecipeInfo(recipeSlug: String): Result<Unit>
suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity?
suspend fun loadRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions?
fun updateNameQuery(name: String?)

View File

@@ -1,24 +0,0 @@
package gq.kirmanak.mealient.data.recipes.db
import androidx.paging.PagingSource
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
interface RecipeStorage {
suspend fun saveRecipes(recipes: List<RecipeSummaryEntity>)
fun queryRecipes(query: String?): PagingSource<Int, RecipeSummaryEntity>
suspend fun refreshAll(recipes: List<RecipeSummaryEntity>)
suspend fun clearAllLocalData()
suspend fun saveRecipeInfo(recipe: FullRecipeInfo)
suspend fun queryRecipeInfo(recipeId: String): FullRecipeEntity?
suspend fun updateFavoriteRecipes(favorites: List<String>)
suspend fun deleteRecipe(entity: RecipeSummaryEntity)
}

View File

@@ -1,88 +0,0 @@
package gq.kirmanak.mealient.data.recipes.db
import androidx.paging.PagingSource
import androidx.room.withTransaction
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.database.AppDb
import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.extensions.toRecipeEntity
import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity
import gq.kirmanak.mealient.extensions.toRecipeInstructionEntity
import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecipeStorageImpl @Inject constructor(
private val db: AppDb,
private val logger: Logger,
) : RecipeStorage {
private val recipeDao: RecipeDao by lazy { db.recipeDao() }
override suspend fun saveRecipes(recipes: List<RecipeSummaryEntity>) {
logger.v { "saveRecipes() called with $recipes" }
db.withTransaction { recipeDao.insertRecipes(recipes) }
}
override fun queryRecipes(query: String?): PagingSource<Int, RecipeSummaryEntity> {
logger.v { "queryRecipes() called with: query = $query" }
return if (query == null) recipeDao.queryRecipesByPages()
else recipeDao.queryRecipesByPages(query)
}
override suspend fun refreshAll(recipes: List<RecipeSummaryEntity>) {
logger.v { "refreshAll() called with: recipes = $recipes" }
db.withTransaction {
recipeDao.removeAllRecipes()
saveRecipes(recipes)
}
}
override suspend fun clearAllLocalData() {
logger.v { "clearAllLocalData() called" }
db.withTransaction {
recipeDao.removeAllRecipes()
}
}
override suspend fun saveRecipeInfo(recipe: FullRecipeInfo) {
logger.v { "saveRecipeInfo() called with: recipe = $recipe" }
db.withTransaction {
recipeDao.insertRecipe(recipe.toRecipeEntity())
recipeDao.deleteRecipeIngredients(recipe.remoteId)
val ingredients = recipe.recipeIngredients.map {
it.toRecipeIngredientEntity(recipe.remoteId)
}
recipeDao.insertRecipeIngredients(ingredients)
recipeDao.deleteRecipeInstructions(recipe.remoteId)
val instructions = recipe.recipeInstructions.map {
it.toRecipeInstructionEntity(recipe.remoteId)
}
recipeDao.insertRecipeInstructions(instructions)
}
}
override suspend fun queryRecipeInfo(recipeId: String): FullRecipeEntity? {
logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" }
val fullRecipeInfo = recipeDao.queryFullRecipeInfo(recipeId)
logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" }
return fullRecipeInfo
}
override suspend fun updateFavoriteRecipes(favorites: List<String>) {
logger.v { "updateFavoriteRecipes() called with: favorites = $favorites" }
db.withTransaction {
recipeDao.setFavorite(favorites)
recipeDao.setNonFavorite(favorites)
}
}
override suspend fun deleteRecipe(entity: RecipeSummaryEntity) {
logger.v { "deleteRecipeBySlug() called with: entity = $entity" }
recipeDao.deleteRecipe(entity)
}
}

View File

@@ -1,7 +1,7 @@
package gq.kirmanak.mealient.data.recipes.impl
import androidx.paging.InvalidatingPagingSourceFactory
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.database.recipe.RecipeStorage
import gq.kirmanak.mealient.logging.Logger
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject

View File

@@ -4,12 +4,13 @@ import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity
import gq.kirmanak.mealient.database.recipe.RecipeStorage
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.model_mapper.ModelMapper
import javax.inject.Inject
import javax.inject.Singleton
@@ -21,6 +22,7 @@ class RecipeRepoImpl @Inject constructor(
private val pagingSourceFactory: RecipePagingSourceFactory,
private val dataSource: RecipeDataSource,
private val logger: Logger,
private val modelMapper: ModelMapper,
) : RecipeRepo {
override fun createPager(): Pager<Int, RecipeSummaryEntity> {
@@ -45,13 +47,21 @@ class RecipeRepoImpl @Inject constructor(
override suspend fun refreshRecipeInfo(recipeSlug: String): Result<Unit> {
logger.v { "refreshRecipeInfo() called with: recipeSlug = $recipeSlug" }
return runCatchingExceptCancel {
storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug))
val info = dataSource.requestRecipeInfo(recipeSlug)
val entity = modelMapper.toRecipeEntity(info)
val ingredients = info.recipeIngredients.map {
modelMapper.toRecipeIngredientEntity(it, entity.remoteId)
}
val instructions = info.recipeInstructions.map {
modelMapper.toRecipeInstructionEntity(it, entity.remoteId)
}
storage.saveRecipeInfo(entity, ingredients, instructions)
}.onFailure {
logger.e(it) { "loadRecipeInfo: can't update full recipe info" }
}
}
override suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity? {
override suspend fun loadRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions? {
logger.v { "loadRecipeInfo() called with: recipeId = $recipeId" }
val recipeInfo = storage.queryRecipeInfo(recipeId)
logger.v { "loadRecipeInfo() returned: $recipeInfo" }

View File

@@ -5,12 +5,12 @@ import androidx.paging.*
import androidx.paging.LoadType.PREPEND
import androidx.paging.LoadType.REFRESH
import gq.kirmanak.mealient.architecture.configuration.AppDispatchers
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.recipe.RecipeStorage
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.extensions.toRecipeSummaryEntity
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.model_mapper.ModelMapper
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
@@ -24,6 +24,7 @@ class RecipesRemoteMediator @Inject constructor(
private val network: RecipeDataSource,
private val pagingSourceFactory: RecipePagingSourceFactory,
private val logger: Logger,
private val modelMapper: ModelMapper,
private val dispatchers: AppDispatchers,
) : RemoteMediator<Int, RecipeSummaryEntity>() {
@@ -75,7 +76,7 @@ class RecipesRemoteMediator @Inject constructor(
val entities = withContext(dispatchers.default) {
recipes.map { recipe ->
val isFavorite = favorites.contains(recipe.slug)
recipe.toRecipeSummaryEntity(isFavorite)
modelMapper.toRecipeSummaryEntity(recipe, isFavorite)
}
}
if (loadType == REFRESH) storage.refreshAll(entities)

View File

@@ -1,26 +0,0 @@
package gq.kirmanak.mealient.data.recipes.network
data class FullRecipeInfo(
val remoteId: String,
val name: String,
val recipeYield: String,
val recipeIngredients: List<RecipeIngredientInfo>,
val recipeInstructions: List<RecipeInstructionInfo>,
val settings: RecipeSettingsInfo,
)
data class RecipeSettingsInfo(
val disableAmounts: Boolean,
)
data class RecipeIngredientInfo(
val note: String,
val quantity: Double?,
val unit: String?,
val food: String?,
val title: String?,
)
data class RecipeInstructionInfo(
val text: String,
)

View File

@@ -1,5 +1,8 @@
package gq.kirmanak.mealient.data.recipes.network
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
import gq.kirmanak.mealient.datasource.models.RecipeSummaryInfo
interface RecipeDataSource {
suspend fun requestRecipes(start: Int, limit: Int): List<RecipeSummaryInfo>

View File

@@ -1,14 +0,0 @@
package gq.kirmanak.mealient.data.recipes.network
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
data class RecipeSummaryInfo(
val remoteId: String,
val name: String,
val slug: String,
val description: String = "",
val imageId: String,
val dateAdded: LocalDate,
val dateUpdated: LocalDateTime
)

View File

@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.data.share
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
interface ParseRecipeDataSource {
suspend fun parseRecipeFromURL(parseRecipeURLInfo: ParseRecipeURLInfo): String

View File

@@ -1,6 +0,0 @@
package gq.kirmanak.mealient.data.share
data class ParseRecipeURLInfo(
val url: String,
val includeTags: Boolean
)

View File

@@ -1,6 +1,7 @@
package gq.kirmanak.mealient.data.share
import androidx.core.util.PatternsCompat
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
import javax.inject.Singleton

View File

@@ -15,6 +15,7 @@ import gq.kirmanak.mealient.data.auth.impl.AuthDataSourceImpl
import gq.kirmanak.mealient.data.auth.impl.AuthRepoImpl
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl
import gq.kirmanak.mealient.datasource.AuthenticationProvider
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo
import javax.inject.Singleton
@Module
@@ -45,4 +46,8 @@ interface AuthModule {
@Binds
@Singleton
fun bindAuthStorage(authStorageImpl: AuthStorageImpl): AuthStorage
@Binds
@Singleton
fun bindShoppingListsAuthRepo(impl: AuthRepoImpl): ShoppingListsAuthRepo
}

View File

@@ -10,8 +10,6 @@ import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl
import gq.kirmanak.mealient.data.recipes.impl.*
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
@@ -27,10 +25,6 @@ interface RecipeModule {
@Singleton
fun provideRecipeDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): RecipeDataSource
@Binds
@Singleton
fun provideRecipeStorage(recipeStorageImpl: RecipeStorageImpl): RecipeStorage
@Binds
@Singleton
fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo

View File

@@ -1,23 +0,0 @@
package gq.kirmanak.mealient.extensions
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
fun <T> Flow<T>.valueUpdatesOnly(): Flow<T> = when (this) {
is ValueUpdateOnlyFlowImpl<T> -> this
else -> ValueUpdateOnlyFlowImpl(this)
}
private class ValueUpdateOnlyFlowImpl<T>(private val upstream: Flow<T>) : Flow<T> {
override suspend fun collect(collector: FlowCollector<T>) {
var previousValue: T? = null
upstream.collect { value ->
if (previousValue != null && previousValue != value) {
collector.emit(value)
}
previousValue = value
}
}
}

View File

@@ -1,224 +0,0 @@
package gq.kirmanak.mealient.extensions
import gq.kirmanak.mealient.data.add.AddRecipeInfo
import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo
import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo
import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo
import gq.kirmanak.mealient.data.baseurl.VersionInfo
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeSettingsInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo
import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeSettingsV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeIngredientResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeInstructionResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0
import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import java.util.*
fun FullRecipeInfo.toRecipeEntity() = RecipeEntity(
remoteId = remoteId,
recipeYield = recipeYield,
disableAmounts = settings.disableAmounts,
)
fun RecipeIngredientInfo.toRecipeIngredientEntity(remoteId: String) = RecipeIngredientEntity(
recipeId = remoteId,
note = note,
unit = unit,
food = food,
quantity = quantity,
title = title,
)
fun RecipeInstructionInfo.toRecipeInstructionEntity(remoteId: String) = RecipeInstructionEntity(
recipeId = remoteId,
text = text
)
fun GetRecipeSummaryResponseV0.toRecipeSummaryInfo() = RecipeSummaryInfo(
remoteId = remoteId.toString(),
name = name,
slug = slug,
description = description,
dateAdded = dateAdded,
dateUpdated = dateUpdated,
imageId = slug,
)
fun GetRecipeSummaryResponseV1.toRecipeSummaryInfo() = RecipeSummaryInfo(
remoteId = remoteId,
name = name,
slug = slug,
description = description,
dateAdded = dateAdded,
dateUpdated = dateUpdated,
imageId = remoteId,
)
fun RecipeSummaryInfo.toRecipeSummaryEntity(isFavorite: Boolean) = RecipeSummaryEntity(
remoteId = remoteId,
name = name,
slug = slug,
description = description,
dateAdded = dateAdded,
dateUpdated = dateUpdated,
imageId = imageId,
isFavorite = isFavorite,
)
fun VersionResponseV0.toVersionInfo() = VersionInfo(version)
fun VersionResponseV1.toVersionInfo() = VersionInfo(version)
fun AddRecipeDraft.toAddRecipeInfo() = AddRecipeInfo(
name = recipeName,
description = recipeDescription,
recipeYield = recipeYield,
recipeIngredient = recipeIngredients.map { AddRecipeIngredientInfo(note = it) },
recipeInstructions = recipeInstructions.map { AddRecipeInstructionInfo(text = it) },
settings = AddRecipeSettingsInfo(
public = isRecipePublic,
disableComments = areCommentsDisabled,
)
)
fun AddRecipeInfo.toDraft(): AddRecipeDraft = AddRecipeDraft(
recipeName = name,
recipeDescription = description,
recipeYield = recipeYield,
recipeInstructions = recipeInstructions.map { it.text },
recipeIngredients = recipeIngredient.map { it.note },
isRecipePublic = settings.public,
areCommentsDisabled = settings.disableComments,
)
fun GetRecipeResponseV0.toFullRecipeInfo() = FullRecipeInfo(
remoteId = remoteId.toString(),
name = name,
recipeYield = recipeYield,
recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() },
recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() },
settings = RecipeSettingsInfo(disableAmounts = true)
)
fun GetRecipeIngredientResponseV0.toRecipeIngredientInfo() = RecipeIngredientInfo(
note = note,
unit = null,
food = null,
quantity = 1.0,
title = null,
)
fun GetRecipeInstructionResponseV0.toRecipeInstructionInfo() = RecipeInstructionInfo(
text = text
)
fun GetRecipeResponseV1.toFullRecipeInfo() = FullRecipeInfo(
remoteId = remoteId,
name = name,
recipeYield = recipeYield,
recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() },
recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() },
settings = settings.toRecipeSettingsInfo(),
)
private fun GetRecipeSettingsResponseV1.toRecipeSettingsInfo() = RecipeSettingsInfo(
disableAmounts = disableAmount,
)
fun GetRecipeIngredientResponseV1.toRecipeIngredientInfo() = RecipeIngredientInfo(
note = note,
unit = unit?.name,
food = food?.name,
quantity = quantity,
title = title,
)
fun GetRecipeInstructionResponseV1.toRecipeInstructionInfo() = RecipeInstructionInfo(
text = text
)
fun AddRecipeInfo.toV0Request() = AddRecipeRequestV0(
name = name,
description = description,
recipeYield = recipeYield,
recipeIngredient = recipeIngredient.map { it.toV0Ingredient() },
recipeInstructions = recipeInstructions.map { it.toV0Instruction() },
settings = settings.toV0Settings(),
)
private fun AddRecipeSettingsInfo.toV0Settings() = AddRecipeSettingsV0(
disableComments = disableComments,
public = public,
)
private fun AddRecipeIngredientInfo.toV0Ingredient() = AddRecipeIngredientV0(
note = note,
)
private fun AddRecipeInstructionInfo.toV0Instruction() = AddRecipeInstructionV0(
text = text,
)
fun AddRecipeInfo.toV1CreateRequest() = CreateRecipeRequestV1(
name = name,
)
fun AddRecipeInfo.toV1UpdateRequest() = UpdateRecipeRequestV1(
description = description,
recipeYield = recipeYield,
recipeIngredient = recipeIngredient.map { it.toV1Ingredient() },
recipeInstructions = recipeInstructions.map { it.toV1Instruction() },
settings = settings.toV1Settings(),
)
private fun AddRecipeSettingsInfo.toV1Settings() = AddRecipeSettingsV1(
disableComments = disableComments,
public = public,
)
private fun AddRecipeIngredientInfo.toV1Ingredient() = AddRecipeIngredientV1(
id = UUID.randomUUID().toString(),
note = note,
)
private fun AddRecipeInstructionInfo.toV1Instruction() = AddRecipeInstructionV1(
id = UUID.randomUUID().toString(),
text = text,
ingredientReferences = emptyList(),
)
fun ParseRecipeURLInfo.toV1Request() = ParseRecipeURLRequestV1(
url = url,
includeTags = includeTags,
)
fun ParseRecipeURLInfo.toV0Request() = ParseRecipeURLRequestV0(
url = url,
)

View File

@@ -1,61 +0,0 @@
package gq.kirmanak.mealient.ui
import android.widget.Button
import android.widget.ProgressBar
import androidx.core.view.isVisible
sealed class OperationUiState<T> {
val exceptionOrNull: Throwable?
get() = (this as? Failure)?.exception
val isSuccess: Boolean
get() = this is Success
val isProgress: Boolean
get() = this is Progress
val isFailure: Boolean
get() = this is Failure
fun updateButtonState(button: Button) {
button.isEnabled = !isProgress
button.isClickable = !isProgress
}
fun updateProgressState(progressBar: ProgressBar) {
progressBar.isVisible = isProgress
}
class Initial<T> : OperationUiState<T>() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
class Progress<T> : OperationUiState<T>() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
data class Failure<T>(val exception: Throwable) : OperationUiState<T>()
data class Success<T>(val value: T) : OperationUiState<T>()
companion object {
fun <T> fromResult(result: Result<T>) = result.fold({ Success(it) }, { Failure(it) })
}
}

View File

@@ -15,11 +15,14 @@ import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAddRecipeFr
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAuthenticationFragment
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalBaseURLFragment
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalRecipesListFragment
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalShoppingListsFragment
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.MainActivityBinding
import gq.kirmanak.mealient.extensions.collectWhenResumed
import gq.kirmanak.mealient.extensions.observeOnce
import gq.kirmanak.mealient.ui.ActivityUiState
import gq.kirmanak.mealient.ui.BaseActivity
import gq.kirmanak.mealient.ui.CheckableMenuItem
@AndroidEntryPoint
class MainActivity : BaseActivity<MainActivityBinding>(
@@ -60,7 +63,7 @@ class MainActivity : BaseActivity<MainActivityBinding>(
viewModel.onSearchQuery(query.trim().takeUnless { it.isEmpty() })
}
binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected)
viewModel.uiStateLive.observe(this, ::onUiStateChange)
collectWhenResumed(viewModel.uiState, ::onUiStateChange)
collectWhenResumed(viewModel.clearSearchViewFocus) {
logger.d { "clearSearchViewFocus(): received event" }
binding.toolbar.clearSearchFocus()
@@ -77,6 +80,7 @@ class MainActivity : BaseActivity<MainActivityBinding>(
val directions = when (menuItem.itemId) {
R.id.add_recipe -> actionGlobalAddRecipeFragment()
R.id.recipes_list -> actionGlobalRecipesListFragment()
R.id.shopping_lists -> actionGlobalShoppingListsFragment()
R.id.change_url -> actionGlobalBaseURLFragment(false)
R.id.login -> actionGlobalAuthenticationFragment()
R.id.logout -> {
@@ -90,15 +94,24 @@ class MainActivity : BaseActivity<MainActivityBinding>(
return true
}
private fun onUiStateChange(uiState: MainActivityUiState) {
private fun onUiStateChange(uiState: ActivityUiState) {
logger.v { "onUiStateChange() called with: uiState = $uiState" }
val checkedMenuItem = when (uiState.checkedMenuItem) {
CheckableMenuItem.ShoppingLists -> R.id.shopping_lists
CheckableMenuItem.RecipesList -> R.id.recipes_list
CheckableMenuItem.AddRecipe -> R.id.add_recipe
CheckableMenuItem.ChangeUrl -> R.id.change_url
CheckableMenuItem.Login -> R.id.login
null -> null
}
for (menuItem in binding.navigationView.menu.iterator()) {
val itemId = menuItem.itemId
when (itemId) {
R.id.logout -> menuItem.isVisible = uiState.canShowLogout
R.id.login -> menuItem.isVisible = uiState.canShowLogin
R.id.shopping_lists -> menuItem.isVisible = uiState.v1MenuItemsVisible
}
menuItem.isChecked = itemId == uiState.checkedMenuItemId
menuItem.isChecked = itemId == checkedMenuItem
}
binding.toolbar.isVisible = uiState.navigationVisible

View File

@@ -1,14 +0,0 @@
package gq.kirmanak.mealient.ui.activity
import androidx.annotation.IdRes
data class MainActivityUiState(
val isAuthorized: Boolean = false,
val navigationVisible: Boolean = false,
val searchVisible: Boolean = false,
@IdRes val checkedMenuItemId: Int? = null,
) {
val canShowLogin: Boolean get() = !isAuthorized
val canShowLogout: Boolean get() = isAuthorized
}

View File

@@ -1,16 +1,23 @@
package gq.kirmanak.mealient.ui.activity
import androidx.lifecycle.*
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.baseurl.ServerVersion
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.ActivityUiState
import gq.kirmanak.mealient.ui.ActivityUiStateController
import gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentArgs
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
@@ -24,14 +31,10 @@ class MainActivityViewModel @Inject constructor(
private val disclaimerStorage: DisclaimerStorage,
private val serverInfoRepo: ServerInfoRepo,
private val recipeRepo: RecipeRepo,
private val activityUiStateController: ActivityUiStateController,
) : ViewModel() {
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)
val uiState: StateFlow<ActivityUiState> = activityUiStateController.getUiStateFlow()
private val _startDestination = MutableLiveData<StartDestinationInfo>()
val startDestination: LiveData<StartDestinationInfo> = _startDestination
@@ -44,6 +47,10 @@ class MainActivityViewModel @Inject constructor(
.onEach { isAuthorized -> updateUiState { it.copy(isAuthorized = isAuthorized) } }
.launchIn(viewModelScope)
serverInfoRepo.versionUpdates()
.onEach { version -> updateUiState { it.copy(v1MenuItemsVisible = version == ServerVersion.V1) } }
.launchIn(viewModelScope)
viewModelScope.launch {
_startDestination.value = when {
!disclaimerStorage.isDisclaimerAccepted() -> {
@@ -59,8 +66,8 @@ class MainActivityViewModel @Inject constructor(
}
}
fun updateUiState(updater: (MainActivityUiState) -> MainActivityUiState) {
uiState = updater(uiState)
fun updateUiState(updater: (ActivityUiState) -> ActivityUiState) {
activityUiStateController.updateUiState(updater)
}
fun logout() {

View File

@@ -12,15 +12,16 @@ import androidx.fragment.app.viewModels
import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.add.AddRecipeInfo
import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo
import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo
import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo
import gq.kirmanak.mealient.databinding.FragmentAddRecipeBinding
import gq.kirmanak.mealient.databinding.ViewSingleInputBinding
import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
import gq.kirmanak.mealient.datasource.models.AddRecipeIngredientInfo
import gq.kirmanak.mealient.datasource.models.AddRecipeInstructionInfo
import gq.kirmanak.mealient.datasource.models.AddRecipeSettingsInfo
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.extensions.collectWhenViewResumed
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.CheckableMenuItem
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import javax.inject.Inject
@@ -41,7 +42,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
it.copy(
navigationVisible = true,
searchVisible = false,
checkedMenuItemId = R.id.add_recipe,
checkedMenuItem = CheckableMenuItem.AddRecipe,
)
}
viewModel.loadPreservedRequest()

View File

@@ -3,8 +3,8 @@ package gq.kirmanak.mealient.ui.add
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.add.AddRecipeInfo
import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.channels.Channel

View File

@@ -13,6 +13,7 @@ import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.CheckableMenuItem
import gq.kirmanak.mealient.ui.OperationUiState
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import javax.inject.Inject
@@ -32,7 +33,11 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
binding.button.setOnClickListener { onLoginClicked() }
activityViewModel.updateUiState {
it.copy(navigationVisible = true, searchVisible = false, checkedMenuItemId = R.id.login)
it.copy(
navigationVisible = true,
searchVisible = false,
checkedMenuItem = CheckableMenuItem.AddRecipe
)
}
viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
}

View File

@@ -14,6 +14,7 @@ import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.CheckableMenuItem
import gq.kirmanak.mealient.ui.OperationUiState
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentDirections.Companion.actionBaseURLFragmentToRecipesListFragment
@@ -39,7 +40,7 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
it.copy(
navigationVisible = !args.isOnboarding,
searchVisible = false,
checkedMenuItemId = R.id.change_url,
checkedMenuItem = CheckableMenuItem.ChangeUrl,
)
}
}

View File

@@ -10,6 +10,7 @@ import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo
import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -20,6 +21,7 @@ class BaseURLViewModel @Inject constructor(
private val authRepo: AuthRepo,
private val recipeRepo: RecipeRepo,
private val logger: Logger,
private val shoppingListsRepo: ShoppingListsRepo,
) : ViewModel() {
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())

View File

@@ -58,7 +58,11 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
}
viewModel.startCountDown()
activityViewModel.updateUiState {
it.copy(navigationVisible = false, searchVisible = false, checkedMenuItemId = null)
it.copy(
navigationVisible = false,
searchVisible = false,
checkedMenuItem = null
)
}
}
}

View File

@@ -17,11 +17,13 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.architecture.valueUpdatesOnly
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.FragmentRecipesListBinding
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.extensions.*
import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.CheckableMenuItem
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import gq.kirmanak.mealient.ui.recipes.RecipesListFragmentDirections.Companion.actionRecipesFragmentToRecipeInfoFragment
import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory
@@ -54,7 +56,7 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) {
it.copy(
navigationVisible = true,
searchVisible = true,
checkedMenuItemId = R.id.recipes_list
checkedMenuItem = CheckableMenuItem.RecipesList,
)
}
collectWhenViewResumed(viewModel.showFavoriteIcon) { showFavoriteIcon ->

View File

@@ -7,10 +7,10 @@ import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.architecture.valueUpdatesOnly
import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.extensions.valueUpdatesOnly
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow

View File

@@ -12,6 +12,13 @@
android:icon="@drawable/ic_add"
android:title="@string/menu_navigation_drawer_add_recipe" />
<item
android:id="@+id/shopping_lists"
android:visible="false"
android:checkable="true"
android:icon="@drawable/ic_shopping_cart"
android:title="@string/menu_navigation_drawer_shopping_lists" />
<item
android:id="@+id/change_url"
android:checkable="true"

View File

@@ -63,6 +63,11 @@
android:label="fragment_add_recipe"
tools:layout="@layout/fragment_add_recipe" />
<fragment
android:id="@+id/shoppingListsFragment"
android:name="gq.kirmanak.mealient.shopping_lists.ui.ShoppingListsFragment" />
<action
android:id="@+id/action_global_authenticationFragment"
app:destination="@id/authenticationFragment" />
@@ -81,4 +86,9 @@
android:id="@+id/action_global_baseURLFragment"
app:destination="@id/baseURLFragment"
app:popUpTo="@id/recipesListFragment" />
<action
android:id="@+id/action_global_shoppingListsFragment"
app:destination="@id/shoppingListsFragment"
app:popUpTo="@id/recipesListFragment" />
</navigation>

View File

@@ -65,4 +65,5 @@
<string name="view_holder_recipe_delete_content_description">Удалить рецепт</string>
<string name="fragment_recipes_favorite_added">%1$s добавлено в избранное</string>
<string name="fragment_recipes_favorite_removed">%1$s удалено из избранного</string>
<string name="menu_navigation_drawer_shopping_lists">Списки покупок</string>
</resources>

View File

@@ -68,4 +68,5 @@
<string name="view_holder_recipe_delete_content_description">Delete recipe</string>
<string name="fragment_recipes_favorite_added">Added %1$s to favorites</string>
<string name="fragment_recipes_favorite_removed">Removed %1$s from favorites</string>
<string name="menu_navigation_drawer_shopping_lists">Shopping lists</string>
</resources>