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

@@ -80,9 +80,15 @@ dependencies {
implementation(project(":architecture")) implementation(project(":architecture"))
implementation(project(":database")) implementation(project(":database"))
testImplementation(project(":database_test"))
implementation(project(":datastore")) implementation(project(":datastore"))
testImplementation(project(":datastore_test"))
implementation(project(":datasource")) implementation(project(":datasource"))
testImplementation(project(":datasource_test"))
implementation(project(":logging")) implementation(project(":logging"))
implementation(project(":ui"))
implementation(project(":features:shopping_lists"))
implementation(project(":model_mapper"))
testImplementation(project(":testing")) testImplementation(project(":testing"))
implementation(libs.android.material.material) implementation(libs.android.material.material)

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,11 @@
package gq.kirmanak.mealient.data.auth package gq.kirmanak.mealient.data.auth
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo
import kotlinx.coroutines.flow.Flow 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) suspend fun authenticate(email: String, password: String)

View File

@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.data.baseurl package gq.kirmanak.mealient.data.baseurl
import kotlinx.coroutines.flow.Flow
interface ServerInfoRepo { interface ServerInfoRepo {
suspend fun getUrl(): String? suspend fun getUrl(): String?
@@ -8,5 +10,7 @@ interface ServerInfoRepo {
suspend fun tryBaseURL(baseURL: String): Result<Unit> 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.ServerUrlProvider
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger 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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -55,4 +58,11 @@ class ServerInfoRepoImpl @Inject constructor(
serverInfoStorage.storeBaseURL(oldBaseUrl, oldVersion) 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 package gq.kirmanak.mealient.data.baseurl
import kotlinx.coroutines.flow.Flow
interface ServerInfoStorage { interface ServerInfoStorage {
suspend fun getBaseURL(): String? suspend fun getBaseURL(): String?
@@ -12,4 +14,5 @@ interface ServerInfoStorage {
suspend fun getServerVersion(): String? suspend fun getServerVersion(): String?
fun serverVersionUpdates(): Flow<String?>
} }

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.data.baseurl.impl
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage
import gq.kirmanak.mealient.data.storage.PreferencesStorage import gq.kirmanak.mealient.data.storage.PreferencesStorage
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -49,6 +50,10 @@ class ServerInfoStorageImpl @Inject constructor(
preferencesStorage.storeValues(Pair(serverVersionKey, version)) 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) 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 package gq.kirmanak.mealient.data.network
import gq.kirmanak.mealient.data.add.AddRecipeDataSource 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.ServerInfoRepo
import gq.kirmanak.mealient.data.baseurl.ServerVersion 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.RecipeDataSource
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.data.share.ParseRecipeDataSource 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.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.extensions.toFullRecipeInfo import gq.kirmanak.mealient.model_mapper.ModelMapper
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 javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -25,15 +20,16 @@ class MealieDataSourceWrapper @Inject constructor(
private val serverInfoRepo: ServerInfoRepo, private val serverInfoRepo: ServerInfoRepo,
private val v0Source: MealieDataSourceV0, private val v0Source: MealieDataSourceV0,
private val v1Source: MealieDataSourceV1, private val v1Source: MealieDataSourceV1,
private val modelMapper: ModelMapper,
) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource { ) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource {
private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion() private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion()
override suspend fun addRecipe(recipe: AddRecipeInfo): String = when (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 -> { ServerVersion.V1 -> {
val slug = v1Source.createRecipe(recipe.toV1CreateRequest()) val slug = v1Source.createRecipe(modelMapper.toV1CreateRequest(recipe))
v1Source.updateRecipe(slug, recipe.toV1UpdateRequest()) v1Source.updateRecipe(slug, modelMapper.toV1UpdateRequest(recipe))
slug slug
} }
} }
@@ -43,25 +39,25 @@ class MealieDataSourceWrapper @Inject constructor(
limit: Int, limit: Int,
): List<RecipeSummaryInfo> = when (getVersion()) { ): List<RecipeSummaryInfo> = when (getVersion()) {
ServerVersion.V0 -> { ServerVersion.V0 -> {
v0Source.requestRecipes(start, limit).map { it.toRecipeSummaryInfo() } v0Source.requestRecipes(start, limit).map { modelMapper.toRecipeSummaryInfo(it) }
} }
ServerVersion.V1 -> { 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 // 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 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()) { override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = when (getVersion()) {
ServerVersion.V0 -> v0Source.requestRecipeInfo(slug).toFullRecipeInfo() ServerVersion.V0 -> modelMapper.toFullRecipeInfo(v0Source.requestRecipeInfo(slug))
ServerVersion.V1 -> v1Source.requestRecipeInfo(slug).toFullRecipeInfo() ServerVersion.V1 -> modelMapper.toFullRecipeInfo(v1Source.requestRecipeInfo(slug))
} }
override suspend fun parseRecipeFromURL( override suspend fun parseRecipeFromURL(
parseRecipeURLInfo: ParseRecipeURLInfo, parseRecipeURLInfo: ParseRecipeURLInfo,
): String = when (getVersion()) { ): String = when (getVersion()) {
ServerVersion.V0 -> v0Source.parseRecipeFromURL(parseRecipeURLInfo.toV0Request()) ServerVersion.V0 -> v0Source.parseRecipeFromURL(modelMapper.toV0Request(parseRecipeURLInfo))
ServerVersion.V1 -> v1Source.parseRecipeFromURL(parseRecipeURLInfo.toV1Request()) ServerVersion.V1 -> v1Source.parseRecipeFromURL(modelMapper.toV1Request(parseRecipeURLInfo))
} }
override suspend fun getFavoriteRecipes(): List<String> = when (getVersion()) { override suspend fun getFavoriteRecipes(): List<String> = when (getVersion()) {

View File

@@ -1,8 +1,8 @@
package gq.kirmanak.mealient.data.recipes package gq.kirmanak.mealient.data.recipes
import androidx.paging.Pager 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.RecipeSummaryEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
interface RecipeRepo { interface RecipeRepo {
@@ -12,7 +12,7 @@ interface RecipeRepo {
suspend fun refreshRecipeInfo(recipeSlug: String): Result<Unit> suspend fun refreshRecipeInfo(recipeSlug: String): Result<Unit>
suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity? suspend fun loadRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions?
fun updateNameQuery(name: String?) 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,7 +1,7 @@
package gq.kirmanak.mealient.data.recipes.impl package gq.kirmanak.mealient.data.recipes.impl
import androidx.paging.InvalidatingPagingSourceFactory 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 gq.kirmanak.mealient.logging.Logger
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject import javax.inject.Inject

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package gq.kirmanak.mealient.data.share package gq.kirmanak.mealient.data.share
import androidx.core.util.PatternsCompat import androidx.core.util.PatternsCompat
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton 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.AuthRepoImpl
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl
import gq.kirmanak.mealient.datasource.AuthenticationProvider import gq.kirmanak.mealient.datasource.AuthenticationProvider
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -45,4 +46,8 @@ interface AuthModule {
@Binds @Binds
@Singleton @Singleton
fun bindAuthStorage(authStorageImpl: AuthStorageImpl): AuthStorage 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.R
import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
import gq.kirmanak.mealient.data.recipes.RecipeRepo 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.impl.*
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
@@ -27,10 +25,6 @@ interface RecipeModule {
@Singleton @Singleton
fun provideRecipeDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): RecipeDataSource fun provideRecipeDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): RecipeDataSource
@Binds
@Singleton
fun provideRecipeStorage(recipeStorageImpl: RecipeStorageImpl): RecipeStorage
@Binds @Binds
@Singleton @Singleton
fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo

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

@@ -15,11 +15,14 @@ import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAddRecipeFr
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAuthenticationFragment import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAuthenticationFragment
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalBaseURLFragment import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalBaseURLFragment
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalRecipesListFragment import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalRecipesListFragment
import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalShoppingListsFragment
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.MainActivityBinding import gq.kirmanak.mealient.databinding.MainActivityBinding
import gq.kirmanak.mealient.extensions.collectWhenResumed import gq.kirmanak.mealient.extensions.collectWhenResumed
import gq.kirmanak.mealient.extensions.observeOnce import gq.kirmanak.mealient.extensions.observeOnce
import gq.kirmanak.mealient.ui.ActivityUiState
import gq.kirmanak.mealient.ui.BaseActivity import gq.kirmanak.mealient.ui.BaseActivity
import gq.kirmanak.mealient.ui.CheckableMenuItem
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : BaseActivity<MainActivityBinding>( class MainActivity : BaseActivity<MainActivityBinding>(
@@ -60,7 +63,7 @@ class MainActivity : BaseActivity<MainActivityBinding>(
viewModel.onSearchQuery(query.trim().takeUnless { it.isEmpty() }) viewModel.onSearchQuery(query.trim().takeUnless { it.isEmpty() })
} }
binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected)
viewModel.uiStateLive.observe(this, ::onUiStateChange) collectWhenResumed(viewModel.uiState, ::onUiStateChange)
collectWhenResumed(viewModel.clearSearchViewFocus) { collectWhenResumed(viewModel.clearSearchViewFocus) {
logger.d { "clearSearchViewFocus(): received event" } logger.d { "clearSearchViewFocus(): received event" }
binding.toolbar.clearSearchFocus() binding.toolbar.clearSearchFocus()
@@ -77,6 +80,7 @@ class MainActivity : BaseActivity<MainActivityBinding>(
val directions = when (menuItem.itemId) { val directions = when (menuItem.itemId) {
R.id.add_recipe -> actionGlobalAddRecipeFragment() R.id.add_recipe -> actionGlobalAddRecipeFragment()
R.id.recipes_list -> actionGlobalRecipesListFragment() R.id.recipes_list -> actionGlobalRecipesListFragment()
R.id.shopping_lists -> actionGlobalShoppingListsFragment()
R.id.change_url -> actionGlobalBaseURLFragment(false) R.id.change_url -> actionGlobalBaseURLFragment(false)
R.id.login -> actionGlobalAuthenticationFragment() R.id.login -> actionGlobalAuthenticationFragment()
R.id.logout -> { R.id.logout -> {
@@ -90,15 +94,24 @@ class MainActivity : BaseActivity<MainActivityBinding>(
return true return true
} }
private fun onUiStateChange(uiState: MainActivityUiState) { private fun onUiStateChange(uiState: ActivityUiState) {
logger.v { "onUiStateChange() called with: uiState = $uiState" } 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()) { for (menuItem in binding.navigationView.menu.iterator()) {
val itemId = menuItem.itemId val itemId = menuItem.itemId
when (itemId) { when (itemId) {
R.id.logout -> menuItem.isVisible = uiState.canShowLogout R.id.logout -> menuItem.isVisible = uiState.canShowLogout
R.id.login -> menuItem.isVisible = uiState.canShowLogin 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 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 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 dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo 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.disclaimer.DisclaimerStorage
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.logging.Logger 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 gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentArgs
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
@@ -24,14 +31,10 @@ class MainActivityViewModel @Inject constructor(
private val disclaimerStorage: DisclaimerStorage, private val disclaimerStorage: DisclaimerStorage,
private val serverInfoRepo: ServerInfoRepo, private val serverInfoRepo: ServerInfoRepo,
private val recipeRepo: RecipeRepo, private val recipeRepo: RecipeRepo,
private val activityUiStateController: ActivityUiStateController,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableLiveData(MainActivityUiState()) val uiState: StateFlow<ActivityUiState> = activityUiStateController.getUiStateFlow()
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 _startDestination = MutableLiveData<StartDestinationInfo>() private val _startDestination = MutableLiveData<StartDestinationInfo>()
val startDestination: LiveData<StartDestinationInfo> = _startDestination val startDestination: LiveData<StartDestinationInfo> = _startDestination
@@ -44,6 +47,10 @@ class MainActivityViewModel @Inject constructor(
.onEach { isAuthorized -> updateUiState { it.copy(isAuthorized = isAuthorized) } } .onEach { isAuthorized -> updateUiState { it.copy(isAuthorized = isAuthorized) } }
.launchIn(viewModelScope) .launchIn(viewModelScope)
serverInfoRepo.versionUpdates()
.onEach { version -> updateUiState { it.copy(v1MenuItemsVisible = version == ServerVersion.V1) } }
.launchIn(viewModelScope)
viewModelScope.launch { viewModelScope.launch {
_startDestination.value = when { _startDestination.value = when {
!disclaimerStorage.isDisclaimerAccepted() -> { !disclaimerStorage.isDisclaimerAccepted() -> {
@@ -59,8 +66,8 @@ class MainActivityViewModel @Inject constructor(
} }
} }
fun updateUiState(updater: (MainActivityUiState) -> MainActivityUiState) { fun updateUiState(updater: (ActivityUiState) -> ActivityUiState) {
uiState = updater(uiState) activityUiStateController.updateUiState(updater)
} }
fun logout() { fun logout() {

View File

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

View File

@@ -3,8 +3,8 @@ package gq.kirmanak.mealient.ui.add
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.add.AddRecipeInfo
import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.channels.Channel 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.datasource.NetworkError
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.CheckableMenuItem
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import javax.inject.Inject import javax.inject.Inject
@@ -32,7 +33,11 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
binding.button.setOnClickListener { onLoginClicked() } binding.button.setOnClickListener { onLoginClicked() }
activityViewModel.updateUiState { 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) 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.datasource.NetworkError
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.CheckableMenuItem
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentDirections.Companion.actionBaseURLFragmentToRecipesListFragment import gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentDirections.Companion.actionBaseURLFragmentToRecipesListFragment
@@ -39,7 +40,7 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
it.copy( it.copy(
navigationVisible = !args.isOnboarding, navigationVisible = !args.isOnboarding,
searchVisible = false, 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.data.recipes.RecipeRepo
import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -20,6 +21,7 @@ class BaseURLViewModel @Inject constructor(
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
private val recipeRepo: RecipeRepo, private val recipeRepo: RecipeRepo,
private val logger: Logger, private val logger: Logger,
private val shoppingListsRepo: ShoppingListsRepo,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial()) private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())

View File

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

View File

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

View File

@@ -12,6 +12,13 @@
android:icon="@drawable/ic_add" android:icon="@drawable/ic_add"
android:title="@string/menu_navigation_drawer_add_recipe" /> 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 <item
android:id="@+id/change_url" android:id="@+id/change_url"
android:checkable="true" android:checkable="true"

View File

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

View File

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

View File

@@ -68,4 +68,5 @@
<string name="view_holder_recipe_delete_content_description">Delete recipe</string> <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_added">Added %1$s to favorites</string>
<string name="fragment_recipes_favorite_removed">Removed %1$s from 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> </resources>

View File

@@ -3,10 +3,12 @@ package gq.kirmanak.mealient.data.add.impl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.add.AddRecipeDataSource import gq.kirmanak.mealient.data.add.AddRecipeDataSource
import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_INFO
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
import gq.kirmanak.mealient.datastore_test.PORRIDGE_RECIPE_DRAFT
import gq.kirmanak.mealient.model_mapper.ModelMapper
import gq.kirmanak.mealient.model_mapper.ModelMapperImpl
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_DRAFT
import io.mockk.* import io.mockk.*
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -24,12 +26,14 @@ class AddRecipeRepoTest : BaseUnitTest() {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var storage: AddRecipeStorage lateinit var storage: AddRecipeStorage
private val modelMapper: ModelMapper = ModelMapperImpl()
private lateinit var subject: AddRecipeRepo private lateinit var subject: AddRecipeRepo
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
subject = AddRecipeRepoImpl(dataSource, storage, logger) subject = AddRecipeRepoImpl(dataSource, storage, logger, modelMapper)
} }
@Test @Test

View File

@@ -1,10 +1,11 @@
package gq.kirmanak.mealient.data.baseurl package gq.kirmanak.mealient.data.baseurl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.datasource.models.VersionInfo
import gq.kirmanak.mealient.datasource_test.VERSION_INFO_V0
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_INFO_V0
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK

View File

@@ -5,6 +5,18 @@ import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_INFO
import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_REQUEST_V0
import gq.kirmanak.mealient.datasource_test.PORRIDGE_CREATE_RECIPE_REQUEST_V1
import gq.kirmanak.mealient.datasource_test.PORRIDGE_FULL_RECIPE_INFO
import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_RESPONSE_V1
import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0
import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1
import gq.kirmanak.mealient.datasource_test.PORRIDGE_UPDATE_RECIPE_REQUEST_V1
import gq.kirmanak.mealient.datasource_test.RECIPE_SUMMARY_PORRIDGE_V0
import gq.kirmanak.mealient.datasource_test.RECIPE_SUMMARY_PORRIDGE_V1
import gq.kirmanak.mealient.model_mapper.ModelMapper
import gq.kirmanak.mealient.model_mapper.ModelMapperImpl
import gq.kirmanak.mealient.test.AuthImplTestData.FAVORITE_RECIPES_LIST import gq.kirmanak.mealient.test.AuthImplTestData.FAVORITE_RECIPES_LIST
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION_V0 import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION_V0
@@ -12,16 +24,6 @@ import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION_V1
import gq.kirmanak.mealient.test.AuthImplTestData.USER_INFO_V0 import gq.kirmanak.mealient.test.AuthImplTestData.USER_INFO_V0
import gq.kirmanak.mealient.test.AuthImplTestData.USER_INFO_V1 import gq.kirmanak.mealient.test.AuthImplTestData.USER_INFO_V1
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_REQUEST_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_CREATE_RECIPE_REQUEST_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_FULL_RECIPE_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_RESPONSE_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_UPDATE_RECIPE_REQUEST_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.RECIPE_SUMMARY_PORRIDGE_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.RECIPE_SUMMARY_PORRIDGE_V1
import io.mockk.* import io.mockk.*
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -45,12 +47,14 @@ class MealieDataSourceWrapperTest : BaseUnitTest() {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var v1Source: MealieDataSourceV1 lateinit var v1Source: MealieDataSourceV1
private val modelMapper: ModelMapper = ModelMapperImpl()
lateinit var subject: MealieDataSourceWrapper lateinit var subject: MealieDataSourceWrapper
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
subject = MealieDataSourceWrapper(serverInfoRepo, v0Source, v1Source) subject = MealieDataSourceWrapper(serverInfoRepo, v0Source, v1Source, modelMapper)
coEvery { v0Source.requestUserInfo() } returns USER_INFO_V0 coEvery { v0Source.requestUserInfo() } returns USER_INFO_V0
coEvery { v1Source.requestUserInfo() } returns USER_INFO_V1 coEvery { v1Source.requestUserInfo() } returns USER_INFO_V1
} }

View File

@@ -1,97 +0,0 @@
package gq.kirmanak.mealient.data.recipes.db
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.database.AppDb
import gq.kirmanak.mealient.test.HiltRobolectricTest
import gq.kirmanak.mealient.test.RecipeImplTestData.BREAD_INGREDIENT
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_BREAD_RECIPE_INGREDIENT_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_FULL_RECIPE_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_PORRIDGE_INFO_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_CAKE_RECIPE_INSTRUCTION_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_INSTRUCTION
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_FULL_RECIPE_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARY_ENTITIES
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
@OptIn(ExperimentalCoroutinesApi::class)
class RecipeStorageImplTest : HiltRobolectricTest() {
@Inject
lateinit var subject: RecipeStorageImpl
@Inject
lateinit var appDb: AppDb
@Test
fun `when saveRecipes then saves recipes`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES)
val actualTags = appDb.recipeDao().queryAllRecipes()
assertThat(actualTags).containsExactly(
CAKE_RECIPE_SUMMARY_ENTITY,
PORRIDGE_RECIPE_SUMMARY_ENTITY
)
}
@Test
fun `when refreshAll then old recipes aren't preserved`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES)
subject.refreshAll(listOf(CAKE_RECIPE_SUMMARY_ENTITY))
val actual = appDb.recipeDao().queryAllRecipes()
assertThat(actual).containsExactly(CAKE_RECIPE_SUMMARY_ENTITY)
}
@Test
fun `when clearAllLocalData then recipes aren't preserved`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES)
subject.clearAllLocalData()
val actual = appDb.recipeDao().queryAllRecipes()
assertThat(actual).isEmpty()
}
@Test
fun `when saveRecipeInfo then saves recipe info`() = runTest {
subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY))
subject.saveRecipeInfo(CAKE_FULL_RECIPE_INFO)
val actual = appDb.recipeDao().queryFullRecipeInfo("1")
assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY)
}
@Test
fun `when saveRecipeInfo with two then saves second`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES)
subject.saveRecipeInfo(CAKE_FULL_RECIPE_INFO)
subject.saveRecipeInfo(PORRIDGE_FULL_RECIPE_INFO)
val actual = appDb.recipeDao().queryFullRecipeInfo("2")
assertThat(actual).isEqualTo(FULL_PORRIDGE_INFO_ENTITY)
}
@Test
fun `when saveRecipeInfo secondly then overwrites ingredients`() = runTest {
subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY))
subject.saveRecipeInfo(CAKE_FULL_RECIPE_INFO)
val newRecipe = CAKE_FULL_RECIPE_INFO.copy(recipeIngredients = listOf(BREAD_INGREDIENT))
subject.saveRecipeInfo(newRecipe)
val actual = appDb.recipeDao().queryFullRecipeInfo("1")?.recipeIngredients
val expected = listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY.copy(localId = 3))
assertThat(actual).isEqualTo(expected)
}
@Test
fun `when saveRecipeInfo secondly then overwrites instructions`() = runTest {
subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY))
subject.saveRecipeInfo(CAKE_FULL_RECIPE_INFO)
val newRecipe = CAKE_FULL_RECIPE_INFO.copy(recipeInstructions = listOf(MIX_INSTRUCTION))
subject.saveRecipeInfo(newRecipe)
val actual = appDb.recipeDao().queryFullRecipeInfo("1")?.recipeInstructions
val expected = listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY.copy(localId = 3))
assertThat(actual).isEqualTo(expected)
}
}

View File

@@ -3,12 +3,12 @@ package gq.kirmanak.mealient.data.recipes.impl
import androidx.paging.PagingSource import androidx.paging.PagingSource
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.database.CAKE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.database.PORRIDGE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.database.TEST_RECIPE_SUMMARY_ENTITIES
import gq.kirmanak.mealient.database.recipe.RecipeStorage
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.test.HiltRobolectricTest import gq.kirmanak.mealient.test.HiltRobolectricTest
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARY_ENTITIES
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test

View File

@@ -3,13 +3,20 @@ package gq.kirmanak.mealient.data.recipes.impl
import androidx.paging.LoadType import androidx.paging.LoadType
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.recipes.RecipeRepo 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.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY
import gq.kirmanak.mealient.database.CAKE_BREAD_RECIPE_INGREDIENT_ENTITY
import gq.kirmanak.mealient.database.CAKE_RECIPE_ENTITY
import gq.kirmanak.mealient.database.CAKE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.database.CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY
import gq.kirmanak.mealient.database.FULL_CAKE_INFO_ENTITY
import gq.kirmanak.mealient.database.MIX_CAKE_RECIPE_INSTRUCTION_ENTITY
import gq.kirmanak.mealient.database.recipe.RecipeStorage
import gq.kirmanak.mealient.datasource.NetworkError.Unauthorized import gq.kirmanak.mealient.datasource.NetworkError.Unauthorized
import gq.kirmanak.mealient.datasource_test.CAKE_FULL_RECIPE_INFO
import gq.kirmanak.mealient.model_mapper.ModelMapper
import gq.kirmanak.mealient.model_mapper.ModelMapperImpl
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_FULL_RECIPE_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.coVerifyOrder import io.mockk.coVerifyOrder
@@ -36,12 +43,21 @@ class RecipeRepoTest : BaseUnitTest() {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var pagingSourceFactory: RecipePagingSourceFactory lateinit var pagingSourceFactory: RecipePagingSourceFactory
private val modelMapper: ModelMapper = ModelMapperImpl()
lateinit var subject: RecipeRepo lateinit var subject: RecipeRepo
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
subject = RecipeRepoImpl(remoteMediator, storage, pagingSourceFactory, dataSource, logger) subject = RecipeRepoImpl(
remoteMediator,
storage,
pagingSourceFactory,
dataSource,
logger,
modelMapper,
)
} }
@Test @Test
@@ -55,7 +71,18 @@ class RecipeRepoTest : BaseUnitTest() {
fun `when refreshRecipeInfo expect call to storage`() = runTest { fun `when refreshRecipeInfo expect call to storage`() = runTest {
coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns CAKE_FULL_RECIPE_INFO coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns CAKE_FULL_RECIPE_INFO
subject.refreshRecipeInfo("cake") subject.refreshRecipeInfo("cake")
coVerify { storage.saveRecipeInfo(eq(CAKE_FULL_RECIPE_INFO)) } coVerify {
storage.saveRecipeInfo(
eq(CAKE_RECIPE_ENTITY),
eq(
listOf(
CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY,
CAKE_BREAD_RECIPE_INGREDIENT_ENTITY
)
),
eq(listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY))
)
}
} }
@Test @Test

View File

@@ -3,13 +3,15 @@ package gq.kirmanak.mealient.data.recipes.impl
import androidx.paging.* import androidx.paging.*
import androidx.paging.LoadType.* import androidx.paging.LoadType.*
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.TEST_RECIPE_SUMMARY_ENTITIES
import gq.kirmanak.mealient.database.recipe.RecipeStorage
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.datasource.NetworkError.Unauthorized import gq.kirmanak.mealient.datasource.NetworkError.Unauthorized
import gq.kirmanak.mealient.datasource_test.TEST_RECIPE_SUMMARIES
import gq.kirmanak.mealient.model_mapper.ModelMapper
import gq.kirmanak.mealient.model_mapper.ModelMapperImpl
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARIES
import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARY_ENTITIES
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
@@ -41,6 +43,8 @@ class RecipesRemoteMediatorTest : BaseUnitTest() {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var pagingSourceFactory: RecipePagingSourceFactory lateinit var pagingSourceFactory: RecipePagingSourceFactory
private val modelMapper: ModelMapper = ModelMapperImpl()
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
@@ -50,6 +54,7 @@ class RecipesRemoteMediatorTest : BaseUnitTest() {
pagingSourceFactory = pagingSourceFactory, pagingSourceFactory = pagingSourceFactory,
logger = logger, logger = logger,
dispatchers = dispatchers, dispatchers = dispatchers,
modelMapper = modelMapper,
) )
coEvery { dataSource.getFavoriteRecipes() } returns emptyList() coEvery { dataSource.getFavoriteRecipes() } returns emptyList()
} }

View File

@@ -1,5 +1,6 @@
package gq.kirmanak.mealient.data.share package gq.kirmanak.mealient.data.share
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify

View File

@@ -1,146 +0,0 @@
package gq.kirmanak.mealient.extensions
import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_FULL_RECIPE_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.MILK_RECIPE_INGREDIENT_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.MILK_RECIPE_INGREDIENT_RESPONSE_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.MILK_RECIPE_INGREDIENT_RESPONSE_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_CAKE_RECIPE_INSTRUCTION_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_INSTRUCTION
import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_RECIPE_INSTRUCTION_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_RECIPE_INSTRUCTION_RESPONSE_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_RECIPE_INSTRUCTION_RESPONSE_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_REQUEST_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_CREATE_RECIPE_REQUEST_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_FULL_RECIPE_INFO
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_DRAFT
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_RESPONSE_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_RESPONSE_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_UPDATE_RECIPE_REQUEST_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.RECIPE_SUMMARY_PORRIDGE_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.RECIPE_SUMMARY_PORRIDGE_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.SUGAR_INGREDIENT
import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_INFO_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_INFO_V1
import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_RESPONSE_V0
import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_RESPONSE_V1
import org.junit.Test
class ModelMappingsTest : BaseUnitTest() {
@Test
fun `when toAddRecipeRequest then fills fields correctly`() {
assertThat(PORRIDGE_RECIPE_DRAFT.toAddRecipeInfo()).isEqualTo(PORRIDGE_ADD_RECIPE_INFO)
}
@Test
fun `when toDraft then fills fields correctly`() {
assertThat(PORRIDGE_ADD_RECIPE_INFO.toDraft()).isEqualTo(PORRIDGE_RECIPE_DRAFT)
}
@Test
fun `when full recipe info to entity expect correct entity`() {
assertThat(CAKE_FULL_RECIPE_INFO.toRecipeEntity()).isEqualTo(CAKE_RECIPE_ENTITY)
}
@Test
fun `when ingredient info to entity expect correct entity`() {
val actual = SUGAR_INGREDIENT.toRecipeIngredientEntity("1")
assertThat(actual).isEqualTo(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY)
}
@Test
fun `when instruction info to entity expect correct entity`() {
val actual = MIX_INSTRUCTION.toRecipeInstructionEntity("1")
assertThat(actual).isEqualTo(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY)
}
@Test
fun `when summary v0 to info expect correct info`() {
val actual = PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0.toRecipeSummaryInfo()
assertThat(actual).isEqualTo(RECIPE_SUMMARY_PORRIDGE_V0)
}
@Test
fun `when summary v1 to info expect correct info`() {
val actual = PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1.toRecipeSummaryInfo()
assertThat(actual).isEqualTo(RECIPE_SUMMARY_PORRIDGE_V1)
}
@Test
fun `when summary info to entity expect correct entity`() {
val actual = RECIPE_SUMMARY_PORRIDGE_V0.toRecipeSummaryEntity(isFavorite = false)
assertThat(actual).isEqualTo(PORRIDGE_RECIPE_SUMMARY_ENTITY)
}
@Test
fun `when version response v0 to info expect correct info`() {
assertThat(VERSION_RESPONSE_V0.toVersionInfo()).isEqualTo(VERSION_INFO_V0)
}
@Test
fun `when version response v1 to info expect correct info`() {
assertThat(VERSION_RESPONSE_V1.toVersionInfo()).isEqualTo(VERSION_INFO_V1)
}
@Test
fun `when recipe ingredient response v0 to info expect correct info`() {
val actual = MILK_RECIPE_INGREDIENT_RESPONSE_V0.toRecipeIngredientInfo()
assertThat(actual).isEqualTo(MILK_RECIPE_INGREDIENT_INFO)
}
@Test
fun `when recipe ingredient response v1 to info expect correct info`() {
val actual = MILK_RECIPE_INGREDIENT_RESPONSE_V1.toRecipeIngredientInfo()
assertThat(actual).isEqualTo(MILK_RECIPE_INGREDIENT_INFO)
}
@Test
fun `when recipe instruction response v0 to info expect correct info`() {
val actual = MIX_RECIPE_INSTRUCTION_RESPONSE_V0.toRecipeInstructionInfo()
assertThat(actual).isEqualTo(MIX_RECIPE_INSTRUCTION_INFO)
}
@Test
fun `when recipe instruction response v1 to info expect correct info`() {
val actual = MIX_RECIPE_INSTRUCTION_RESPONSE_V1.toRecipeInstructionInfo()
assertThat(actual).isEqualTo(MIX_RECIPE_INSTRUCTION_INFO)
}
@Test
fun `when recipe response v0 to info expect correct info`() {
val actual = PORRIDGE_RECIPE_RESPONSE_V0.toFullRecipeInfo()
assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO)
}
@Test
fun `when recipe response v1 to info expect correct info`() {
val actual = PORRIDGE_RECIPE_RESPONSE_V1.toFullRecipeInfo()
assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO)
}
@Test
fun `when add recipe info to request v0 expect correct request`() {
val actual = PORRIDGE_ADD_RECIPE_INFO.toV0Request()
assertThat(actual).isEqualTo(PORRIDGE_ADD_RECIPE_REQUEST_V0)
}
@Test
fun `when add recipe info to create request v1 expect correct request`() {
val actual = PORRIDGE_ADD_RECIPE_INFO.toV1CreateRequest()
assertThat(actual).isEqualTo(PORRIDGE_CREATE_RECIPE_REQUEST_V1)
}
@Test
fun `when add recipe info to update request v1 expect correct request`() {
val actual = PORRIDGE_ADD_RECIPE_INFO.toV1UpdateRequest()
assertThat(actual).isEqualTo(PORRIDGE_UPDATE_RECIPE_REQUEST_V1)
}
}

View File

@@ -1,440 +0,0 @@
package gq.kirmanak.mealient.test
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.database.recipe.entity.FullRecipeEntity
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.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.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
object RecipeImplTestData {
val RECIPE_SUMMARY_CAKE = RecipeSummaryInfo(
remoteId = "1",
name = "Cake",
slug = "cake",
description = "A tasty cake",
dateAdded = LocalDate.parse("2021-11-13"),
dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"),
imageId = "cake",
)
val RECIPE_SUMMARY_PORRIDGE_V0 = RecipeSummaryInfo(
remoteId = "2",
name = "Porridge",
slug = "porridge",
description = "A tasty porridge",
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
imageId = "porridge",
)
val RECIPE_SUMMARY_PORRIDGE_V1 = RecipeSummaryInfo(
remoteId = "2",
name = "Porridge",
slug = "porridge",
description = "A tasty porridge",
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
imageId = "2",
)
val TEST_RECIPE_SUMMARIES = listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE_V0)
val CAKE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity(
remoteId = "1",
name = "Cake",
slug = "cake",
description = "A tasty cake",
dateAdded = LocalDate.parse("2021-11-13"),
dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"),
imageId = "cake",
isFavorite = false,
)
val PORRIDGE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity(
remoteId = "2",
name = "Porridge",
slug = "porridge",
description = "A tasty porridge",
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
imageId = "porridge",
isFavorite = false,
)
val TEST_RECIPE_SUMMARY_ENTITIES =
listOf(CAKE_RECIPE_SUMMARY_ENTITY, PORRIDGE_RECIPE_SUMMARY_ENTITY)
val SUGAR_INGREDIENT = RecipeIngredientInfo(
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val BREAD_INGREDIENT = RecipeIngredientInfo(
note = "2 oz of white bread",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
private val MILK_INGREDIENT = RecipeIngredientInfo(
note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val MIX_INSTRUCTION = RecipeInstructionInfo(
text = "Mix the ingredients"
)
private val BAKE_INSTRUCTION = RecipeInstructionInfo(
text = "Bake the ingredients"
)
private val BOIL_INSTRUCTION = RecipeInstructionInfo(
text = "Boil the ingredients"
)
val CAKE_FULL_RECIPE_INFO = FullRecipeInfo(
remoteId = "1",
name = "Cake",
recipeYield = "4 servings",
recipeIngredients = listOf(SUGAR_INGREDIENT, BREAD_INGREDIENT),
recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION),
settings = RecipeSettingsInfo(disableAmounts = true)
)
val PORRIDGE_FULL_RECIPE_INFO = FullRecipeInfo(
remoteId = "2",
name = "Porridge",
recipeYield = "3 servings",
recipeIngredients = listOf(SUGAR_INGREDIENT, MILK_INGREDIENT),
recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION),
settings = RecipeSettingsInfo(disableAmounts = true)
)
val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
recipeId = "1",
text = "Mix the ingredients",
)
private val BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
recipeId = "1",
text = "Bake the ingredients",
)
val CAKE_RECIPE_ENTITY = RecipeEntity(
remoteId = "1",
recipeYield = "4 servings",
disableAmounts = true,
)
val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "1",
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "1",
note = "2 oz of white bread",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val FULL_CAKE_INFO_ENTITY = FullRecipeEntity(
recipeEntity = CAKE_RECIPE_ENTITY,
recipeSummaryEntity = CAKE_RECIPE_SUMMARY_ENTITY,
recipeIngredients = listOf(
CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY,
CAKE_BREAD_RECIPE_INGREDIENT_ENTITY,
),
recipeInstructions = listOf(
MIX_CAKE_RECIPE_INSTRUCTION_ENTITY,
BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY,
),
)
private val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity(
remoteId = "2",
recipeYield = "3 servings",
disableAmounts = true,
)
private val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "2",
note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
private val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "2",
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
private val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
recipeId = "2",
text = "Mix the ingredients"
)
private val PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
recipeId = "2",
text = "Boil the ingredients"
)
val FULL_PORRIDGE_INFO_ENTITY = FullRecipeEntity(
recipeEntity = PORRIDGE_RECIPE_ENTITY_FULL,
recipeSummaryEntity = PORRIDGE_RECIPE_SUMMARY_ENTITY,
recipeIngredients = listOf(
PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY,
PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY,
),
recipeInstructions = listOf(
PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY,
PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY,
)
)
val SUGAR_ADD_RECIPE_INGREDIENT_INFO = AddRecipeIngredientInfo("2 oz of white sugar")
val MILK_ADD_RECIPE_INGREDIENT_INFO = AddRecipeIngredientInfo("2 oz of white milk")
val BOIL_ADD_RECIPE_INSTRUCTION_INFO = AddRecipeInstructionInfo("Boil the ingredients")
val MIX_ADD_RECIPE_INSTRUCTION_INFO = AddRecipeInstructionInfo("Mix the ingredients")
val ADD_RECIPE_INFO_SETTINGS = AddRecipeSettingsInfo(disableComments = false, public = true)
val PORRIDGE_ADD_RECIPE_INFO = AddRecipeInfo(
name = "Porridge",
description = "A tasty porridge",
recipeYield = "3 servings",
recipeIngredient = listOf(
MILK_ADD_RECIPE_INGREDIENT_INFO,
SUGAR_ADD_RECIPE_INGREDIENT_INFO,
),
recipeInstructions = listOf(
MIX_ADD_RECIPE_INSTRUCTION_INFO,
BOIL_ADD_RECIPE_INSTRUCTION_INFO,
),
settings = ADD_RECIPE_INFO_SETTINGS,
)
val PORRIDGE_RECIPE_DRAFT = AddRecipeDraft(
recipeName = "Porridge",
recipeDescription = "A tasty porridge",
recipeYield = "3 servings",
recipeInstructions = listOf("Mix the ingredients", "Boil the ingredients"),
recipeIngredients = listOf("2 oz of white milk", "2 oz of white sugar"),
isRecipePublic = true,
areCommentsDisabled = false,
)
val PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0 = GetRecipeSummaryResponseV0(
remoteId = 2,
name = "Porridge",
slug = "porridge",
description = "A tasty porridge",
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
)
val PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1 = GetRecipeSummaryResponseV1(
remoteId = "2",
name = "Porridge",
slug = "porridge",
description = "A tasty porridge",
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
)
val VERSION_RESPONSE_V0 = VersionResponseV0("v0.5.6")
val VERSION_INFO_V0 = VersionInfo("v0.5.6")
val VERSION_RESPONSE_V1 = VersionResponseV1("v1.0.0-beta05")
val VERSION_INFO_V1 = VersionInfo("v1.0.0-beta05")
val MILK_RECIPE_INGREDIENT_RESPONSE_V0 = GetRecipeIngredientResponseV0("2 oz of white milk")
val SUGAR_RECIPE_INGREDIENT_RESPONSE_V0 = GetRecipeIngredientResponseV0("2 oz of white sugar")
val MILK_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1(
note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val SUGAR_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1(
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val MILK_RECIPE_INGREDIENT_INFO = RecipeIngredientInfo(
note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val MIX_RECIPE_INSTRUCTION_RESPONSE_V0 = GetRecipeInstructionResponseV0("Mix the ingredients")
val BOIL_RECIPE_INSTRUCTION_RESPONSE_V0 = GetRecipeInstructionResponseV0("Boil the ingredients")
val MIX_RECIPE_INSTRUCTION_RESPONSE_V1 = GetRecipeInstructionResponseV1("Mix the ingredients")
val BOIL_RECIPE_INSTRUCTION_RESPONSE_V1 = GetRecipeInstructionResponseV1("Boil the ingredients")
val MIX_RECIPE_INSTRUCTION_INFO = RecipeInstructionInfo("Mix the ingredients")
val PORRIDGE_RECIPE_RESPONSE_V0 = GetRecipeResponseV0(
remoteId = 2,
name = "Porridge",
recipeYield = "3 servings",
recipeIngredients = listOf(
SUGAR_RECIPE_INGREDIENT_RESPONSE_V0,
MILK_RECIPE_INGREDIENT_RESPONSE_V0,
),
recipeInstructions = listOf(
MIX_RECIPE_INSTRUCTION_RESPONSE_V0,
BOIL_RECIPE_INSTRUCTION_RESPONSE_V0
),
)
val PORRIDGE_RECIPE_RESPONSE_V1 = GetRecipeResponseV1(
remoteId = "2",
name = "Porridge",
recipeYield = "3 servings",
recipeIngredients = listOf(
SUGAR_RECIPE_INGREDIENT_RESPONSE_V1,
MILK_RECIPE_INGREDIENT_RESPONSE_V1,
),
recipeInstructions = listOf(
MIX_RECIPE_INSTRUCTION_RESPONSE_V1,
BOIL_RECIPE_INSTRUCTION_RESPONSE_V1
),
settings = GetRecipeSettingsResponseV1(disableAmount = true),
)
val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Mix the ingredients")
val BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Boil the ingredients")
val SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V0 = AddRecipeIngredientV0("2 oz of white sugar")
val MILK_ADD_RECIPE_INGREDIENT_REQUEST_V0 = AddRecipeIngredientV0("2 oz of white milk")
val ADD_RECIPE_REQUEST_SETTINGS_V0 = AddRecipeSettingsV0(disableComments = false, public = true)
val PORRIDGE_ADD_RECIPE_REQUEST_V0 = AddRecipeRequestV0(
name = "Porridge",
description = "A tasty porridge",
recipeYield = "3 servings",
recipeInstructions = listOf(
MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0,
BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V0,
),
recipeIngredient = listOf(
MILK_ADD_RECIPE_INGREDIENT_REQUEST_V0,
SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V0,
),
settings = ADD_RECIPE_REQUEST_SETTINGS_V0
)
val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V1 = AddRecipeInstructionV1(
id = "1",
text = "Mix the ingredients",
ingredientReferences = emptyList()
)
val BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V1 = AddRecipeInstructionV1(
id = "2",
text = "Boil the ingredients",
ingredientReferences = emptyList()
)
val SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V1 = AddRecipeIngredientV1(
id = "3",
note = "2 oz of white sugar"
)
val MILK_ADD_RECIPE_INGREDIENT_REQUEST_V1 = AddRecipeIngredientV1(
id = "4",
note = "2 oz of white milk"
)
val ADD_RECIPE_REQUEST_SETTINGS_V1 = AddRecipeSettingsV1(disableComments = false, public = true)
val PORRIDGE_CREATE_RECIPE_REQUEST_V1 = CreateRecipeRequestV1(name = "Porridge")
val PORRIDGE_UPDATE_RECIPE_REQUEST_V1 = UpdateRecipeRequestV1(
description = "A tasty porridge",
recipeYield = "3 servings",
recipeInstructions = listOf(
MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V1,
BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V1,
),
recipeIngredient = listOf(
MILK_ADD_RECIPE_INGREDIENT_REQUEST_V1,
SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V1,
),
settings = ADD_RECIPE_REQUEST_SETTINGS_V1
)
}

View File

@@ -6,10 +6,13 @@ import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.ui.ActivityUiState
import gq.kirmanak.mealient.ui.ActivityUiStateController
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@@ -28,6 +31,9 @@ class MainActivityViewModelTest : BaseUnitTest() {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var recipeRepo: RecipeRepo lateinit var recipeRepo: RecipeRepo
@MockK(relaxUnitFun = true)
lateinit var activityUiStateController: ActivityUiStateController
private lateinit var subject: MainActivityViewModel private lateinit var subject: MainActivityViewModel
@Before @Before
@@ -36,12 +42,17 @@ class MainActivityViewModelTest : BaseUnitTest() {
every { authRepo.isAuthorizedFlow } returns emptyFlow() every { authRepo.isAuthorizedFlow } returns emptyFlow()
coEvery { disclaimerStorage.isDisclaimerAccepted() } returns true coEvery { disclaimerStorage.isDisclaimerAccepted() } returns true
coEvery { serverInfoRepo.getUrl() } returns TEST_BASE_URL coEvery { serverInfoRepo.getUrl() } returns TEST_BASE_URL
every { activityUiStateController.getUiStateFlow() } returns MutableStateFlow(
ActivityUiState()
)
coEvery { serverInfoRepo.versionUpdates() } returns emptyFlow()
subject = MainActivityViewModel( subject = MainActivityViewModel(
authRepo = authRepo, authRepo = authRepo,
logger = logger, logger = logger,
disclaimerStorage = disclaimerStorage, disclaimerStorage = disclaimerStorage,
serverInfoRepo = serverInfoRepo, serverInfoRepo = serverInfoRepo,
recipeRepo = recipeRepo, recipeRepo = recipeRepo,
activityUiStateController = activityUiStateController,
) )
} }

View File

@@ -2,8 +2,8 @@ package gq.kirmanak.mealient.ui.add
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_INFO
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK

View File

@@ -5,6 +5,7 @@ import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
@@ -31,6 +32,9 @@ class BaseURLViewModelTest : BaseUnitTest() {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var recipeRepo: RecipeRepo lateinit var recipeRepo: RecipeRepo
@MockK(relaxUnitFun = true)
lateinit var shoppingListsRepo: ShoppingListsRepo
lateinit var subject: BaseURLViewModel lateinit var subject: BaseURLViewModel
@Before @Before
@@ -41,6 +45,7 @@ class BaseURLViewModelTest : BaseUnitTest() {
authRepo = authRepo, authRepo = authRepo,
recipeRepo = recipeRepo, recipeRepo = recipeRepo,
logger = logger, logger = logger,
shoppingListsRepo = shoppingListsRepo,
) )
} }

View File

@@ -4,8 +4,8 @@ import androidx.lifecycle.asFlow
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.database.CAKE_RECIPE_SUMMARY_ENTITY
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every

View File

@@ -3,8 +3,8 @@ package gq.kirmanak.mealient.ui.recipes.info
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.database.FULL_CAKE_INFO_ENTITY
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi

View File

@@ -11,4 +11,10 @@ android {
dependencies { dependencies {
implementation(libs.google.dagger.hiltAndroid) implementation(libs.google.dagger.hiltAndroid)
kapt(libs.google.dagger.hiltCompiler) kapt(libs.google.dagger.hiltCompiler)
testImplementation(libs.jetbrains.kotlinx.coroutinesTest)
testImplementation(libs.androidx.test.junit)
testImplementation(libs.androidx.coreTesting)
testImplementation(libs.google.truth)
testImplementation(project(":testing"))
} }

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.extensions package gq.kirmanak.mealient.architecture
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.extensions package gq.kirmanak.mealient.architecture
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest

View File

@@ -19,5 +19,9 @@ gradlePlugin {
id = "gq.kirmanak.mealient.library" id = "gq.kirmanak.mealient.library"
implementationClass = "AndroidLibraryConventionPlugin" implementationClass = "AndroidLibraryConventionPlugin"
} }
register("compose") {
id = "gq.kirmanak.mealient.compose"
implementationClass = "AndroidLibraryComposeConventionPlugin"
}
} }
} }

View File

@@ -0,0 +1,16 @@
import com.android.build.gradle.LibraryExtension
import gq.kirmanak.mealient.configureAndroidCompose
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
extensions.configure<LibraryExtension> {
configureAndroidCompose(this)
}
}
}
}

View File

@@ -0,0 +1,74 @@
package gq.kirmanak.mealient
import com.android.build.api.dsl.CommonExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.dependencies
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension<*, *, *, *>,
) {
val variants = when (commonExtension) {
is BaseAppModuleExtension -> commonExtension.applicationVariants
is LibraryExtension -> commonExtension.libraryVariants
else -> error("Unsupported extension type")
}
commonExtension.apply {
buildFeatures {
compose = true
}
composeOptions {
val version = libs.findVersion("composeKotlinCompilerExtension")
kotlinCompilerExtensionVersion = version.get().toString()
}
// Add compose-destinations generated code to Gradle source sets
variants.all {
kotlin.sourceSets {
getByName(name) {
kotlin.srcDir("build/generated/ksp/$name/kotlin")
}
}
}
dependencies {
val bom = library("androidx-compose-bom")
add("implementation", platform(bom))
add("androidTestImplementation", platform(bom))
add("implementation", library("androidx-compose-material3"))
add("implementation", library("androidx-compose-ui-toolingPreview"))
add("implementation", library("androidx-compose-runtime-livedata"))
add("implementation", library("androidx-lifecycle-viewmodelCompose"))
add("implementation", library("google-accompanist-themeadapter-material3"))
add("debugImplementation", library("androidx-compose-ui-tooling"))
add("debugImplementation", library("androidx-compose-ui-testManifest"))
add("androidTestImplementation", library("androidx-compose-ui-testJunit"))
add("implementation", library("composeDestinations-core"))
add("ksp", library("composeDestinations-ksp"))
}
}
}
private fun Project.library(name: String): Provider<MinimalExternalModuleDependency> {
return libs.findLibrary(name).get()
}
private val Project.kotlin: KotlinAndroidProjectExtension
get() = (this as ExtensionAware).extensions.getByName("kotlin") as KotlinAndroidProjectExtension
private fun KotlinAndroidProjectExtension.sourceSets(configure: Action<NamedDomainObjectContainer<KotlinSourceSet>>): Unit =
(this as ExtensionAware).extensions.configure("sourceSets", configure)

View File

@@ -27,7 +27,11 @@ internal fun Project.configureKotlinAndroid(
} }
lint { lint {
disable += listOf("ObsoleteLintCustomCheck", "IconMissingDensityFolder") disable += listOf(
"ObsoleteLintCustomCheck",
"IconMissingDensityFolder",
"MissingTranslation"
)
enable += listOf( enable += listOf(
"ConvertToWebp", "ConvertToWebp",
"DuplicateStrings", "DuplicateStrings",

View File

@@ -6,30 +6,27 @@ plugins {
} }
android { android {
defaultConfig {
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
namespace = "gq.kirmanak.mealient.database" namespace = "gq.kirmanak.mealient.database"
} }
dependencies { dependencies {
implementation(project(":logging"))
testImplementation(project(":testing"))
testImplementation(project(":database_test"))
implementation(libs.google.dagger.hiltAndroid) implementation(libs.google.dagger.hiltAndroid)
kapt(libs.google.dagger.hiltCompiler) kapt(libs.google.dagger.hiltCompiler)
kaptTest(libs.google.dagger.hiltAndroidCompiler) kaptTest(libs.google.dagger.hiltAndroidCompiler)
testImplementation(libs.google.dagger.hiltAndroidTesting) testImplementation(libs.google.dagger.hiltAndroidTesting)
// withTransaction is used in the app module implementation(libs.androidx.room.ktx)
api(libs.androidx.room.ktx)
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.paging) implementation(libs.androidx.room.paging)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
testImplementation(libs.androidx.room.testing) testImplementation(libs.androidx.room.testing)
implementation(libs.jetbrains.kotlinx.datetime) api(libs.jetbrains.kotlinx.datetime)
implementation(libs.jetbrains.kotlinx.coroutinesAndroid) implementation(libs.jetbrains.kotlinx.coroutinesAndroid)
testImplementation(libs.jetbrains.kotlinx.coroutinesTest) testImplementation(libs.jetbrains.kotlinx.coroutinesTest)

View File

@@ -1,404 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "cac9e9a2f4082b071336eff342e0c01f",
"entities": [
{
"tableName": "categories",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_categories_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "category_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"category_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_category_recipe_category_id_recipe_id",
"unique": true,
"columnNames": [
"category_id",
"recipe_id"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)"
},
{
"name": "index_category_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "categories",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"category_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_tags_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "tag_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` INTEGER NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "tagId",
"columnName": "tag_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_tag_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "tags",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"tag_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "recipe_summaries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "slug",
"columnName": "slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "image",
"columnName": "image",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rating",
"columnName": "rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateUpdated",
"columnName": "date_updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` INTEGER NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeYield",
"columnName": "recipe_yield",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_ingredient",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note",
"columnName": "note",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "food",
"columnName": "food",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "disableAmount",
"columnName": "disable_amount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "quantity",
"columnName": "quantity",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_instruction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cac9e9a2f4082b071336eff342e0c01f')"
]
}
}

View File

@@ -1,404 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "cac9e9a2f4082b071336eff342e0c01f",
"entities": [
{
"tableName": "categories",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_categories_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "category_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"category_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_category_recipe_category_id_recipe_id",
"unique": true,
"columnNames": [
"category_id",
"recipe_id"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)"
},
{
"name": "index_category_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "categories",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"category_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_tags_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "tag_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` INTEGER NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "tagId",
"columnName": "tag_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_tag_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "tags",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"tag_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "recipe_summaries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "slug",
"columnName": "slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "image",
"columnName": "image",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rating",
"columnName": "rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateUpdated",
"columnName": "date_updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` INTEGER NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeYield",
"columnName": "recipe_yield",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_ingredient",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note",
"columnName": "note",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "food",
"columnName": "food",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "disableAmount",
"columnName": "disable_amount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "quantity",
"columnName": "quantity",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_instruction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cac9e9a2f4082b071336eff342e0c01f')"
]
}
}

View File

@@ -1,404 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "28c896eb34e95c0cff33148178252f72",
"entities": [
{
"tableName": "categories",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_categories_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "category_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"category_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_category_recipe_category_id_recipe_id",
"unique": true,
"columnNames": [
"category_id",
"recipe_id"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)"
},
{
"name": "index_category_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "categories",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"category_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_tags_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "tag_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "tagId",
"columnName": "tag_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_tag_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "tags",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"tag_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "recipe_summaries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "slug",
"columnName": "slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "image",
"columnName": "image",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rating",
"columnName": "rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateUpdated",
"columnName": "date_updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recipeYield",
"columnName": "recipe_yield",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_ingredient",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` REAL NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note",
"columnName": "note",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "food",
"columnName": "food",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "disableAmount",
"columnName": "disable_amount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "quantity",
"columnName": "quantity",
"affinity": "REAL",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_instruction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '28c896eb34e95c0cff33148178252f72')"
]
}
}

View File

@@ -1,410 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "13be83018f147e1f6e864790656da4a7",
"entities": [
{
"tableName": "categories",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_categories_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "category_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"category_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_category_recipe_category_id_recipe_id",
"unique": true,
"columnNames": [
"category_id",
"recipe_id"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)"
},
{
"name": "index_category_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "categories",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"category_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_tags_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "tag_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "tagId",
"columnName": "tag_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_tag_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "tags",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"tag_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "recipe_summaries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "slug",
"columnName": "slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "image",
"columnName": "image",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rating",
"columnName": "rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateUpdated",
"columnName": "date_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "imageId",
"columnName": "image_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recipeYield",
"columnName": "recipe_yield",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_ingredient",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` REAL NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note",
"columnName": "note",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "food",
"columnName": "food",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "disableAmount",
"columnName": "disable_amount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "quantity",
"columnName": "quantity",
"affinity": "REAL",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_instruction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '13be83018f147e1f6e864790656da4a7')"
]
}
}

View File

@@ -1,374 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "e75a1e16503fdf60c62b7f9d17ec0bc6",
"entities": [
{
"tableName": "categories",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_categories_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "category_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"category_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_category_recipe_category_id_recipe_id",
"unique": true,
"columnNames": [
"category_id",
"recipe_id"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)"
},
{
"name": "index_category_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "categories",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"category_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "tags",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_tags_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "tag_recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "tagId",
"columnName": "tag_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"tag_id",
"recipe_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_tag_recipe_recipe_id",
"unique": false,
"columnNames": [
"recipe_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)"
}
],
"foreignKeys": [
{
"table": "tags",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"tag_id"
],
"referencedColumns": [
"local_id"
]
},
{
"table": "recipe_summaries",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"recipe_id"
],
"referencedColumns": [
"remote_id"
]
}
]
},
{
"tableName": "recipe_summaries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "slug",
"columnName": "slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "image",
"columnName": "image",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rating",
"columnName": "rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateUpdated",
"columnName": "date_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "imageId",
"columnName": "image_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recipeYield",
"columnName": "recipe_yield",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_ingredient",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note",
"columnName": "note",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_instruction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e75a1e16503fdf60c62b7f9d17ec0bc6')"
]
}
}

View File

@@ -1,160 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "f6e28dd617e4d4a6843a7865c9da736d",
"entities": [
{
"tableName": "recipe_summaries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "slug",
"columnName": "slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateUpdated",
"columnName": "date_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "imageId",
"columnName": "image_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recipeYield",
"columnName": "recipe_yield",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_ingredient",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note",
"columnName": "note",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_instruction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f6e28dd617e4d4a6843a7865c9da736d')"
]
}
}

View File

@@ -1,191 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "d2679aea13d3c18e58c537164f70e249",
"entities": [
{
"tableName": "recipe_summaries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "slug",
"columnName": "slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateUpdated",
"columnName": "date_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "imageId",
"columnName": "image_id",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, `disable_amounts` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recipeYield",
"columnName": "recipe_yield",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "disableAmounts",
"columnName": "disable_amounts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_ingredient",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL, `food` TEXT, `unit` TEXT, `quantity` REAL, `title` TEXT)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note",
"columnName": "note",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "food",
"columnName": "food",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "quantity",
"columnName": "quantity",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_instruction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd2679aea13d3c18e58c537164f70e249')"
]
}
}

View File

@@ -1,198 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "793673e401425db36544918dae6bf4c1",
"entities": [
{
"tableName": "recipe_summaries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, `is_favorite` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "slug",
"columnName": "slug",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dateUpdated",
"columnName": "date_updated",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "imageId",
"columnName": "image_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFavorite",
"columnName": "is_favorite",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, `disable_amounts` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`remote_id`))",
"fields": [
{
"fieldPath": "remoteId",
"columnName": "remote_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recipeYield",
"columnName": "recipe_yield",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "disableAmounts",
"columnName": "disable_amounts",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "true"
}
],
"primaryKey": {
"columnNames": [
"remote_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_ingredient",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL, `food` TEXT, `unit` TEXT, `quantity` REAL, `title` TEXT)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note",
"columnName": "note",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "food",
"columnName": "food",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "unit",
"columnName": "unit",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "quantity",
"columnName": "quantity",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recipe_instruction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "localId",
"columnName": "local_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recipeId",
"columnName": "recipe_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"local_id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '793673e401425db36544918dae6bf4c1')"
]
}
}

View File

@@ -1,45 +1,20 @@
package gq.kirmanak.mealient.database package gq.kirmanak.mealient.database
import androidx.room.* import androidx.room.*
import androidx.room.migration.AutoMigrationSpec
import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.entity.* import gq.kirmanak.mealient.database.recipe.entity.*
@Database( @Database(
version = 8, version = 10,
entities = [ entities = [
RecipeSummaryEntity::class, RecipeSummaryEntity::class,
RecipeEntity::class, RecipeEntity::class,
RecipeIngredientEntity::class, RecipeIngredientEntity::class,
RecipeInstructionEntity::class, RecipeInstructionEntity::class,
],
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class),
AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class),
AutoMigration(from = 6, to = 7),
AutoMigration(from = 7, to = 8),
] ]
) )
@TypeConverters(RoomTypeConverters::class) @TypeConverters(RoomTypeConverters::class)
abstract class AppDb : RoomDatabase() { internal abstract class AppDb : RoomDatabase() {
abstract fun recipeDao(): RecipeDao abstract fun recipeDao(): RecipeDao
@DeleteColumn(tableName = "recipe_instruction", columnName = "title")
@DeleteColumn(tableName = "recipe_ingredient", columnName = "title")
@DeleteColumn(tableName = "recipe_ingredient", columnName = "unit")
@DeleteColumn(tableName = "recipe_ingredient", columnName = "food")
@DeleteColumn(tableName = "recipe_ingredient", columnName = "disable_amount")
@DeleteColumn(tableName = "recipe_ingredient", columnName = "quantity")
class From4To5Migration : AutoMigrationSpec
@DeleteColumn(tableName = "recipe_summaries", columnName = "image")
@DeleteColumn(tableName = "recipe_summaries", columnName = "rating")
@DeleteTable(tableName = "tag_recipe")
@DeleteTable(tableName = "tags")
@DeleteTable(tableName = "categories")
@DeleteTable(tableName = "category_recipe")
class From5To6Migration : AutoMigrationSpec
} }

View File

@@ -2,23 +2,35 @@ package gq.kirmanak.mealient.database
import android.content.Context import android.content.Context
import androidx.room.Room import androidx.room.Room
import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.RecipeStorage
import gq.kirmanak.mealient.database.recipe.RecipeStorageImpl
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
interface DatabaseModule { internal interface DatabaseModule {
companion object { companion object {
@Provides @Provides
@Singleton @Singleton
fun createDb(@ApplicationContext context: Context): AppDb = fun createDb(@ApplicationContext context: Context): AppDb =
Room.databaseBuilder(context, AppDb::class.java, "app.db") Room.databaseBuilder(context, AppDb::class.java, "app.db")
.fallbackToDestructiveMigrationFrom(2) .fallbackToDestructiveMigration()
.build() .build()
@Provides
@Singleton
fun provideRecipeDao(db: AppDb): RecipeDao = db.recipeDao()
} }
@Binds
@Singleton
fun provideRecipeStorage(recipeStorageImpl: RecipeStorageImpl): RecipeStorage
} }

View File

@@ -5,25 +5,30 @@ import androidx.room.*
import gq.kirmanak.mealient.database.recipe.entity.* import gq.kirmanak.mealient.database.recipe.entity.*
@Dao @Dao
interface RecipeDao { internal interface RecipeDao {
@Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC") @Query("SELECT * FROM recipe_summaries ORDER BY recipe_summaries_date_added DESC")
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity> fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
@Query("SELECT * FROM recipe_summaries WHERE recipe_summaries.name LIKE '%' || :query || '%' ORDER BY date_added DESC") @Query("SELECT * FROM recipe_summaries WHERE recipe_summaries_name LIKE '%' || :query || '%' ORDER BY recipe_summaries_date_added DESC")
fun queryRecipesByPages(query: String): PagingSource<Int, RecipeSummaryEntity> fun queryRecipesByPages(query: String): PagingSource<Int, RecipeSummaryEntity>
@Transaction
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipes(recipeSummaryEntity: Iterable<RecipeSummaryEntity>) suspend fun insertRecipeSummaries(recipeSummaryEntity: Iterable<RecipeSummaryEntity>)
@Transaction
@Query("DELETE FROM recipe_summaries") @Query("DELETE FROM recipe_summaries")
suspend fun removeAllRecipes() suspend fun removeAllRecipes()
@Query("SELECT * FROM recipe_summaries ORDER BY date_updated DESC") @Query("SELECT * FROM recipe_summaries ORDER BY recipe_summaries_date_added DESC")
suspend fun queryAllRecipes(): List<RecipeSummaryEntity> suspend fun queryAllRecipes(): List<RecipeSummaryEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipe(recipe: RecipeEntity) suspend fun insertRecipe(recipe: RecipeEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipes(recipe: List<RecipeEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipeInstructions(instructions: List<RecipeInstructionEntity>) suspend fun insertRecipeInstructions(instructions: List<RecipeInstructionEntity>)
@@ -32,19 +37,25 @@ interface RecipeDao {
@Transaction @Transaction
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) // The lint is wrong, the columns are actually used @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) // The lint is wrong, the columns are actually used
@Query("SELECT * FROM recipe JOIN recipe_summaries ON recipe.remote_id = recipe_summaries.remote_id JOIN recipe_ingredient ON recipe_ingredient.recipe_id = recipe.remote_id JOIN recipe_instruction ON recipe_instruction.recipe_id = recipe.remote_id WHERE recipe.remote_id = :recipeId") @Query(
suspend fun queryFullRecipeInfo(recipeId: String): FullRecipeEntity? "SELECT * FROM recipe " +
"JOIN recipe_summaries USING(recipe_id) " +
"JOIN recipe_ingredient USING(recipe_id) " +
"JOIN recipe_instruction USING(recipe_id) " +
"WHERE recipe.recipe_id = :recipeId"
)
suspend fun queryFullRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions?
@Query("DELETE FROM recipe_ingredient WHERE recipe_id = :recipeId") @Query("DELETE FROM recipe_ingredient WHERE recipe_id IN (:recipeIds)")
suspend fun deleteRecipeIngredients(recipeId: String) suspend fun deleteRecipeIngredients(vararg recipeIds: String)
@Query("DELETE FROM recipe_instruction WHERE recipe_id = :recipeId") @Query("DELETE FROM recipe_instruction WHERE recipe_id IN (:recipeIds)")
suspend fun deleteRecipeInstructions(recipeId: String) suspend fun deleteRecipeInstructions(vararg recipeIds: String)
@Query("UPDATE recipe_summaries SET is_favorite = 1 WHERE slug IN (:favorites)") @Query("UPDATE recipe_summaries SET recipe_summaries_is_favorite = 1 WHERE recipe_summaries_slug IN (:favorites)")
suspend fun setFavorite(favorites: List<String>) suspend fun setFavorite(favorites: List<String>)
@Query("UPDATE recipe_summaries SET is_favorite = 0 WHERE slug NOT IN (:favorites)") @Query("UPDATE recipe_summaries SET recipe_summaries_is_favorite = 0 WHERE recipe_summaries_slug NOT IN (:favorites)")
suspend fun setNonFavorite(favorites: List<String>) suspend fun setNonFavorite(favorites: List<String>)
@Delete @Delete

View File

@@ -0,0 +1,30 @@
package gq.kirmanak.mealient.database.recipe
import androidx.paging.PagingSource
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.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
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: RecipeEntity,
ingredients: List<RecipeIngredientEntity>,
instructions: List<RecipeInstructionEntity>
)
suspend fun queryRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions?
suspend fun updateFavoriteRecipes(favorites: List<String>)
suspend fun deleteRecipe(entity: RecipeSummaryEntity)
}

View File

@@ -1,29 +1,27 @@
package gq.kirmanak.mealient.data.recipes.db package gq.kirmanak.mealient.database.recipe
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.room.withTransaction import androidx.room.withTransaction
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.database.AppDb import gq.kirmanak.mealient.database.AppDb
import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity 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.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.extensions.toRecipeEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity
import gq.kirmanak.mealient.extensions.toRecipeInstructionEntity
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class RecipeStorageImpl @Inject constructor( internal class RecipeStorageImpl @Inject constructor(
private val db: AppDb, private val db: AppDb,
private val logger: Logger, private val logger: Logger,
private val recipeDao: RecipeDao,
) : RecipeStorage { ) : RecipeStorage {
private val recipeDao: RecipeDao by lazy { db.recipeDao() }
override suspend fun saveRecipes(recipes: List<RecipeSummaryEntity>) { override suspend fun saveRecipes(recipes: List<RecipeSummaryEntity>) {
logger.v { "saveRecipes() called with $recipes" } logger.v { "saveRecipes() called with $recipes" }
db.withTransaction { recipeDao.insertRecipes(recipes) } recipeDao.insertRecipeSummaries(recipes)
} }
override fun queryRecipes(query: String?): PagingSource<Int, RecipeSummaryEntity> { override fun queryRecipes(query: String?): PagingSource<Int, RecipeSummaryEntity> {
@@ -42,31 +40,27 @@ class RecipeStorageImpl @Inject constructor(
override suspend fun clearAllLocalData() { override suspend fun clearAllLocalData() {
logger.v { "clearAllLocalData() called" } logger.v { "clearAllLocalData() called" }
db.withTransaction { recipeDao.removeAllRecipes()
recipeDao.removeAllRecipes()
}
} }
override suspend fun saveRecipeInfo(recipe: FullRecipeInfo) { override suspend fun saveRecipeInfo(
recipe: RecipeEntity,
ingredients: List<RecipeIngredientEntity>,
instructions: List<RecipeInstructionEntity>
) {
logger.v { "saveRecipeInfo() called with: recipe = $recipe" } logger.v { "saveRecipeInfo() called with: recipe = $recipe" }
db.withTransaction { db.withTransaction {
recipeDao.insertRecipe(recipe.toRecipeEntity()) recipeDao.insertRecipe(recipe)
recipeDao.deleteRecipeIngredients(recipe.remoteId) recipeDao.deleteRecipeIngredients(recipe.remoteId)
val ingredients = recipe.recipeIngredients.map {
it.toRecipeIngredientEntity(recipe.remoteId)
}
recipeDao.insertRecipeIngredients(ingredients) recipeDao.insertRecipeIngredients(ingredients)
recipeDao.deleteRecipeInstructions(recipe.remoteId) recipeDao.deleteRecipeInstructions(recipe.remoteId)
val instructions = recipe.recipeInstructions.map {
it.toRecipeInstructionEntity(recipe.remoteId)
}
recipeDao.insertRecipeInstructions(instructions) recipeDao.insertRecipeInstructions(instructions)
} }
} }
override suspend fun queryRecipeInfo(recipeId: String): FullRecipeEntity? { override suspend fun queryRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions? {
logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" } logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" }
val fullRecipeInfo = recipeDao.queryFullRecipeInfo(recipeId) val fullRecipeInfo = recipeDao.queryFullRecipeInfo(recipeId)
logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" } logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" }

View File

@@ -6,7 +6,7 @@ import androidx.room.PrimaryKey
@Entity(tableName = "recipe") @Entity(tableName = "recipe")
data class RecipeEntity( data class RecipeEntity(
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String, @PrimaryKey @ColumnInfo(name = "recipe_id") val remoteId: String,
@ColumnInfo(name = "recipe_yield") val recipeYield: String, @ColumnInfo(name = "recipe_yield") val recipeYield: String,
@ColumnInfo(name = "disable_amounts", defaultValue = "true") val disableAmounts: Boolean, @ColumnInfo(name = "recipe_disable_amounts", defaultValue = "true") val disableAmounts: Boolean,
) )

View File

@@ -2,17 +2,28 @@ package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "recipe_ingredient") @Entity(
tableName = "recipe_ingredient",
foreignKeys = [
ForeignKey(
entity = RecipeEntity::class,
parentColumns = ["recipe_id"],
childColumns = ["recipe_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class RecipeIngredientEntity( data class RecipeIngredientEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "recipe_ingredient_local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: String, @ColumnInfo(name = "recipe_id", index = true) val recipeId: String,
@ColumnInfo(name = "note") val note: String, @ColumnInfo(name = "recipe_ingredient_note") val note: String,
@ColumnInfo(name = "food") val food: String?, @ColumnInfo(name = "recipe_ingredient_food") val food: String?,
@ColumnInfo(name = "unit") val unit: String?, @ColumnInfo(name = "recipe_ingredient_unit") val unit: String?,
@ColumnInfo(name = "quantity") val quantity: Double?, @ColumnInfo(name = "recipe_ingredient_quantity") val quantity: Double?,
@ColumnInfo(name = "title") val title: String?, @ColumnInfo(name = "recipe_ingredient_title") val title: String?,
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View File

@@ -2,13 +2,24 @@ package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "recipe_instruction") @Entity(
tableName = "recipe_instruction",
foreignKeys = [
ForeignKey(
entity = RecipeEntity::class,
parentColumns = ["recipe_id"],
childColumns = ["recipe_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class RecipeInstructionEntity( data class RecipeInstructionEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "recipe_instruction_local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: String, @ColumnInfo(name = "recipe_id", index = true) val recipeId: String,
@ColumnInfo(name = "text") val text: String, @ColumnInfo(name = "recipe_instruction_text") val text: String,
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View File

@@ -8,12 +8,15 @@ import kotlinx.datetime.LocalDateTime
@Entity(tableName = "recipe_summaries") @Entity(tableName = "recipe_summaries")
data class RecipeSummaryEntity( data class RecipeSummaryEntity(
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String, @PrimaryKey @ColumnInfo(name = "recipe_id") val remoteId: String,
@ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "recipe_summaries_name") val name: String,
@ColumnInfo(name = "slug") val slug: String, @ColumnInfo(name = "recipe_summaries_slug") val slug: String,
@ColumnInfo(name = "description") val description: String, @ColumnInfo(name = "recipe_summaries_description") val description: String,
@ColumnInfo(name = "date_added") val dateAdded: LocalDate, @ColumnInfo(name = "recipe_summaries_date_added") val dateAdded: LocalDate,
@ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime, @ColumnInfo(name = "recipe_summaries_date_updated") val dateUpdated: LocalDateTime,
@ColumnInfo(name = "image_id") val imageId: String?, @ColumnInfo(name = "recipe_summaries_image_id") val imageId: String?,
@ColumnInfo(name = "is_favorite", defaultValue = "false") val isFavorite: Boolean, @ColumnInfo(
name = "recipe_summaries_is_favorite",
defaultValue = "false"
) val isFavorite: Boolean,
) )

View File

@@ -3,20 +3,20 @@ package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Relation import androidx.room.Relation
data class FullRecipeEntity( data class RecipeWithSummaryAndIngredientsAndInstructions(
@Embedded val recipeEntity: RecipeEntity, @Embedded val recipeEntity: RecipeEntity,
@Relation( @Relation(
parentColumn = "remote_id", parentColumn = "recipe_id",
entityColumn = "remote_id" entityColumn = "recipe_id"
) )
val recipeSummaryEntity: RecipeSummaryEntity, val recipeSummaryEntity: RecipeSummaryEntity,
@Relation( @Relation(
parentColumn = "remote_id", parentColumn = "recipe_id",
entityColumn = "recipe_id" entityColumn = "recipe_id"
) )
val recipeIngredients: List<RecipeIngredientEntity>, val recipeIngredients: List<RecipeIngredientEntity>,
@Relation( @Relation(
parentColumn = "remote_id", parentColumn = "recipe_id",
entityColumn = "recipe_id" entityColumn = "recipe_id"
) )
val recipeInstructions: List<RecipeInstructionEntity>, val recipeInstructions: List<RecipeInstructionEntity>,

View File

@@ -0,0 +1,113 @@
package gq.kirmanak.mealient.database
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.RecipeStorageImpl
import gq.kirmanak.mealient.test.HiltRobolectricTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
@OptIn(ExperimentalCoroutinesApi::class)
internal class RecipeStorageImplTest : HiltRobolectricTest() {
@Inject
lateinit var subject: RecipeStorageImpl
@Inject
lateinit var recipeDao: RecipeDao
@Test
fun `when saveRecipes then saves recipes`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES)
val actualTags = recipeDao.queryAllRecipes()
assertThat(actualTags).containsExactly(
CAKE_RECIPE_SUMMARY_ENTITY,
PORRIDGE_RECIPE_SUMMARY_ENTITY
)
}
@Test
fun `when refreshAll then old recipes aren't preserved`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES)
subject.refreshAll(listOf(CAKE_RECIPE_SUMMARY_ENTITY))
val actual = recipeDao.queryAllRecipes()
assertThat(actual).containsExactly(CAKE_RECIPE_SUMMARY_ENTITY)
}
@Test
fun `when clearAllLocalData then recipes aren't preserved`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES)
subject.clearAllLocalData()
val actual = recipeDao.queryAllRecipes()
assertThat(actual).isEmpty()
}
@Test
fun `when saveRecipeInfo then saves recipe info`() = runTest {
subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY))
subject.saveRecipeInfo(
CAKE_RECIPE_ENTITY,
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY)
)
val actual = recipeDao.queryFullRecipeInfo("1")
assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY)
}
@Test
fun `when saveRecipeInfo with two then saves second`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES)
subject.saveRecipeInfo(
CAKE_RECIPE_ENTITY,
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY),
)
subject.saveRecipeInfo(
PORRIDGE_RECIPE_ENTITY_FULL,
listOf(PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY, PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY),
listOf(PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY, PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY),
)
val actual = recipeDao.queryFullRecipeInfo("2")
assertThat(actual).isEqualTo(FULL_PORRIDGE_INFO_ENTITY)
}
@Test
fun `when saveRecipeInfo twice then overwrites ingredients`() = runTest {
subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY))
subject.saveRecipeInfo(
CAKE_RECIPE_ENTITY,
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY),
)
subject.saveRecipeInfo(
CAKE_RECIPE_ENTITY,
listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY),
)
val actual = recipeDao.queryFullRecipeInfo("1")?.recipeIngredients
val expected = listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY.copy(localId = 3))
assertThat(actual).isEqualTo(expected)
}
@Test
fun `when saveRecipeInfo twice then overwrites instructions`() = runTest {
subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY))
subject.saveRecipeInfo(
CAKE_RECIPE_ENTITY,
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY),
)
subject.saveRecipeInfo(
CAKE_RECIPE_ENTITY,
listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY),
listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY),
)
val actual = recipeDao.queryFullRecipeInfo("1")?.recipeInstructions
val expected = listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY.copy(localId = 3))
assertThat(actual).isEqualTo(expected)
}
}

1
database_test/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,11 @@
plugins {
id("gq.kirmanak.mealient.library")
}
android {
namespace = "gq.kirmanak.mealient.database_test"
}
dependencies {
implementation(project(":database"))
}

View File

@@ -0,0 +1,128 @@
package gq.kirmanak.mealient.database
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.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
val CAKE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity(
remoteId = "1",
name = "Cake",
slug = "cake",
description = "A tasty cake",
dateAdded = LocalDate.parse("2021-11-13"),
dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"),
imageId = "cake",
isFavorite = false,
)
val PORRIDGE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity(
remoteId = "2",
name = "Porridge",
slug = "porridge",
description = "A tasty porridge",
dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
imageId = "porridge",
isFavorite = false,
)
val TEST_RECIPE_SUMMARY_ENTITIES =
listOf(CAKE_RECIPE_SUMMARY_ENTITY, PORRIDGE_RECIPE_SUMMARY_ENTITY)
val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
recipeId = "1",
text = "Mix the ingredients",
)
val BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
recipeId = "1",
text = "Bake the ingredients",
)
val CAKE_RECIPE_ENTITY = RecipeEntity(
remoteId = "1",
recipeYield = "4 servings",
disableAmounts = true,
)
val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "1",
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "1",
note = "2 oz of white bread",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val FULL_CAKE_INFO_ENTITY = RecipeWithSummaryAndIngredientsAndInstructions(
recipeEntity = CAKE_RECIPE_ENTITY,
recipeSummaryEntity = CAKE_RECIPE_SUMMARY_ENTITY,
recipeIngredients = listOf(
CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY,
CAKE_BREAD_RECIPE_INGREDIENT_ENTITY,
),
recipeInstructions = listOf(
MIX_CAKE_RECIPE_INSTRUCTION_ENTITY,
BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY,
),
)
val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity(
remoteId = "2",
recipeYield = "3 servings",
disableAmounts = true,
)
val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "2",
note = "2 oz of white milk",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
recipeId = "2",
note = "2 oz of white sugar",
quantity = 1.0,
unit = null,
food = null,
title = null,
)
val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
recipeId = "2",
text = "Mix the ingredients"
)
val PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
recipeId = "2",
text = "Boil the ingredients"
)
val FULL_PORRIDGE_INFO_ENTITY = RecipeWithSummaryAndIngredientsAndInstructions(
recipeEntity = PORRIDGE_RECIPE_ENTITY_FULL,
recipeSummaryEntity = PORRIDGE_RECIPE_SUMMARY_ENTITY,
recipeIngredients = listOf(
PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY,
PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY,
),
recipeInstructions = listOf(
PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY,
PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY,
)
)

View File

@@ -21,7 +21,7 @@ dependencies {
kaptTest(libs.google.dagger.hiltAndroidCompiler) kaptTest(libs.google.dagger.hiltAndroidCompiler)
testImplementation(libs.google.dagger.hiltAndroidTesting) testImplementation(libs.google.dagger.hiltAndroidTesting)
implementation(libs.jetbrains.kotlinx.datetime) api(libs.jetbrains.kotlinx.datetime)
implementation(libs.jetbrains.kotlinx.serialization) implementation(libs.jetbrains.kotlinx.serialization)

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.data.add package gq.kirmanak.mealient.datasource.models
data class AddRecipeInfo( data class AddRecipeInfo(
val name: String, val name: String,

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.data.recipes.network package gq.kirmanak.mealient.datasource.models
data class FullRecipeInfo( data class FullRecipeInfo(
val remoteId: String, val remoteId: String,

View File

@@ -0,0 +1,28 @@
package gq.kirmanak.mealient.datasource.models
data class FullShoppingListInfo(
val id: String,
val name: String,
val items: List<ShoppingListItemInfo>,
)
data class ShoppingListItemInfo(
val shoppingListId: String,
val id: String,
val checked: Boolean,
val position: Int,
val isFood: Boolean,
val note: String,
val quantity: Double,
val unit: String,
val food: String,
val recipeReferences: List<ShoppingListItemRecipeReferenceInfo>,
)
data class ShoppingListItemRecipeReferenceInfo(
val recipeId: String,
val recipeQuantity: Double,
val id: String,
val shoppingListId: String,
val recipe: FullRecipeInfo,
)

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.data.share package gq.kirmanak.mealient.datasource.models
data class ParseRecipeURLInfo( data class ParseRecipeURLInfo(
val url: String, val url: String,

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.data.recipes.network package gq.kirmanak.mealient.datasource.models
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime

View File

@@ -0,0 +1,14 @@
package gq.kirmanak.mealient.datasource.models
data class ShoppingListsInfo(
val page: Int,
val perPage: Int,
val totalPages: Int,
val totalItems: Int,
val items: List<ShoppingListInfo>,
)
data class ShoppingListInfo(
val name: String,
val id: String,
)

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.data.baseurl package gq.kirmanak.mealient.datasource.models
data class VersionInfo( data class VersionInfo(
val version: String, val version: String,

View File

@@ -5,6 +5,8 @@ import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1 import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
@@ -54,5 +56,12 @@ interface MealieDataSourceV1 {
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String) suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String) suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun deleteRecipe(slug: String) suspend fun deleteRecipe(slug: String)
suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponseV1
suspend fun getShoppingList(id: String): GetShoppingListResponseV1
suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean)
} }

View File

@@ -9,12 +9,20 @@ import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1 import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1 import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1 import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import retrofit2.HttpException import retrofit2.HttpException
import java.net.ConnectException import java.net.ConnectException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
@@ -134,5 +142,52 @@ class MealieDataSourceV1Impl @Inject constructor(
logMethod = { "deleteRecipe" }, logMethod = { "deleteRecipe" },
logParameters = { "slug = $slug" } logParameters = { "slug = $slug" }
) )
}
override suspend fun getShoppingLists(
page: Int,
perPage: Int,
): GetShoppingListsResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getShoppingLists(page, perPage) },
logMethod = { "getShoppingLists" },
logParameters = { "page = $page, perPage = $perPage" }
)
override suspend fun getShoppingList(
id: String
): GetShoppingListResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getShoppingList(id) },
logMethod = { "getShoppingList" },
logParameters = { "id = $id" }
)
private suspend fun getShoppingListItem(
id: String,
): JsonElement = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getShoppingListItem(id) },
logMethod = { "getShoppingListItem" },
logParameters = { "id = $id" }
)
private suspend fun updateShoppingListItem(
id: String,
request: JsonElement,
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.updateShoppingListItem(id, request) },
logMethod = { "updateShoppingListItem" },
logParameters = { "id = $id, request = $request" }
)
override suspend fun updateIsShoppingListItemChecked(
id: String,
isChecked: Boolean
) {
// Has to be done in two steps because the API doesn't support updating the checked state
val item = getShoppingListItem(id)
val wasChecked = item.jsonObject.getValue("checked").jsonPrimitive.boolean
if (wasChecked == isChecked) return
val updatedItem = item.jsonObject.toMutableMap().apply {
put("checked", JsonPrimitive(isChecked))
}
updateShoppingListItem(id, JsonObject(updatedItem))
}
}

View File

@@ -1,6 +1,7 @@
package gq.kirmanak.mealient.datasource.v1 package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.v1.models.* import gq.kirmanak.mealient.datasource.v1.models.*
import kotlinx.serialization.json.JsonElement
import retrofit2.http.* import retrofit2.http.*
interface MealieServiceV1 { interface MealieServiceV1 {
@@ -66,4 +67,26 @@ interface MealieServiceV1 {
suspend fun deleteRecipe( suspend fun deleteRecipe(
@Path("slug") slug: String @Path("slug") slug: String
) )
@GET("/api/groups/shopping/lists")
suspend fun getShoppingLists(
@Query("page") page: Int,
@Query("perPage") perPage: Int,
): GetShoppingListsResponseV1
@GET("/api/groups/shopping/lists/{id}")
suspend fun getShoppingList(
@Path("id") id: String,
): GetShoppingListResponseV1
@GET("/api/groups/shopping/items/{id}")
suspend fun getShoppingListItem(
@Path("id") id: String,
): JsonElement
@PUT("/api/groups/shopping/items/{id}")
suspend fun updateShoppingListItem(
@Path("id") id: String,
@Body request: JsonElement,
)
} }

View File

@@ -0,0 +1,9 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeIngredientFoodResponseV1(
@SerialName("name") val name: String = "",
)

View File

@@ -0,0 +1,9 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeIngredientUnitResponseV1(
@SerialName("name") val name: String = "",
)

View File

@@ -10,7 +10,7 @@ data class GetRecipeResponseV1(
@SerialName("recipeYield") val recipeYield: String = "", @SerialName("recipeYield") val recipeYield: String = "",
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponseV1> = emptyList(), @SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponseV1> = emptyList(),
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1> = emptyList(), @SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1> = emptyList(),
@SerialName("settings") val settings: GetRecipeSettingsResponseV1, @SerialName("settings") val settings: GetRecipeSettingsResponseV1? = null,
) )
@Serializable @Serializable
@@ -27,16 +27,6 @@ data class GetRecipeIngredientResponseV1(
@SerialName("title") val title: String?, @SerialName("title") val title: String?,
) )
@Serializable
data class GetRecipeIngredientFoodResponseV1(
@SerialName("name") val name: String = "",
)
@Serializable
data class GetRecipeIngredientUnitResponseV1(
@SerialName("name") val name: String = "",
)
@Serializable @Serializable
data class GetRecipeInstructionResponseV1( data class GetRecipeInstructionResponseV1(
@SerialName("text") val text: String, @SerialName("text") val text: String,

View File

@@ -0,0 +1,42 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetShoppingListResponseV1(
@SerialName("id") val id: String,
@SerialName("groupId") val groupId: String,
@SerialName("name") val name: String = "",
@SerialName("listItems") val listItems: List<GetShoppingListItemResponseV1> = emptyList(),
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceFullResponseV1>,
)
@Serializable
data class GetShoppingListItemResponseV1(
@SerialName("shoppingListId") val shoppingListId: String,
@SerialName("id") val id: String,
@SerialName("checked") val checked: Boolean = false,
@SerialName("position") val position: Int = 0,
@SerialName("isFood") val isFood: Boolean = false,
@SerialName("note") val note: String = "",
@SerialName("quantity") val quantity: Double = 0.0,
@SerialName("unit") val unit: GetRecipeIngredientUnitResponseV1? = null,
@SerialName("food") val food: GetRecipeIngredientFoodResponseV1? = null,
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceResponseV1> = emptyList(),
)
@Serializable
data class GetShoppingListItemRecipeReferenceResponseV1(
@SerialName("recipeId") val recipeId: String,
@SerialName("recipeQuantity") val recipeQuantity: Double = 0.0
)
@Serializable
data class GetShoppingListItemRecipeReferenceFullResponseV1(
@SerialName("id") val id: String,
@SerialName("shoppingListId") val shoppingListId: String,
@SerialName("recipeId") val recipeId: String,
@SerialName("recipeQuantity") val recipeQuantity: Double = 0.0,
@SerialName("recipe") val recipe: GetRecipeResponseV1,
)

View File

@@ -0,0 +1,13 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetShoppingListsResponseV1(
@SerialName("page") val page: Int,
@SerialName("per_page") val perPage: Int,
@SerialName("total") val total: Int,
@SerialName("total_pages") val totalPages: Int,
@SerialName("items") val items: List<GetShoppingListsSummaryResponseV1>,
)

View File

@@ -0,0 +1,10 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetShoppingListsSummaryResponseV1(
@SerialName("id") val id: String,
@SerialName("name") val name: String?,
)

Some files were not shown because too many files have changed in this diff Show More