Skip to content
This repository has been archived by the owner on Nov 9, 2024. It is now read-only.

Commit

Permalink
Implement shopping lists screen (#129)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kirmanak authored Jul 3, 2023
1 parent a40f9a7 commit 1e5e727
Show file tree
Hide file tree
Showing 157 changed files with 3,363 additions and 3,718 deletions.
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,15 @@ dependencies {

implementation(project(":architecture"))
implementation(project(":database"))
testImplementation(project(":database_test"))
implementation(project(":datastore"))
testImplementation(project(":datastore_test"))
implementation(project(":datasource"))
testImplementation(project(":datasource_test"))
implementation(project(":logging"))
implementation(project(":ui"))
implementation(project(":features:shopping_lists"))
implementation(project(":model_mapper"))
testImplementation(project(":testing"))

implementation(libs.android.material.material)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package gq.kirmanak.mealient.data.add

import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
import kotlinx.coroutines.flow.Flow

interface AddRecipeRepo {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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() {
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.data.baseurl

import kotlinx.coroutines.flow.Flow

interface ServerInfoRepo {

suspend fun getUrl(): String?
Expand All @@ -8,5 +10,7 @@ interface ServerInfoRepo {

suspend fun tryBaseURL(baseURL: String): Result<Unit>

fun versionUpdates(): Flow<ServerVersion>

}

Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -55,4 +58,11 @@ class ServerInfoRepoImpl @Inject constructor(
serverInfoStorage.storeBaseURL(oldBaseUrl, oldVersion)
}
}

override fun versionUpdates(): Flow<ServerVersion> {
return serverInfoStorage
.serverVersionUpdates()
.filterNotNull()
.map { determineServerVersion(it) }
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.data.baseurl

import kotlinx.coroutines.flow.Flow

interface ServerInfoStorage {

suspend fun getBaseURL(): String?
Expand All @@ -12,4 +14,5 @@ interface ServerInfoStorage {

suspend fun getServerVersion(): String?

fun serverVersionUpdates(): Flow<String?>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.data.baseurl

import gq.kirmanak.mealient.datasource.models.VersionInfo

interface VersionDataSource {

suspend fun getVersionInfo(): VersionInfo
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
}
}
Expand All @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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?)

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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> {
Expand All @@ -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" }
Expand Down
Loading

0 comments on commit 1e5e727

Please sign in to comment.