diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt index 998043e..c6805c1 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt @@ -12,4 +12,6 @@ interface RecipeRepo { suspend fun refreshRecipeInfo(recipeSlug: String): Result suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity? + + fun setSearchName(name: String?) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt index 89f9439..324282d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt @@ -9,7 +9,7 @@ import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity interface RecipeStorage { suspend fun saveRecipes(recipes: List) - fun queryRecipes(): PagingSource + fun queryRecipes(query: String?): PagingSource suspend fun refreshAll(recipes: List) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt index 3431207..796dd41 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt @@ -35,9 +35,11 @@ class RecipeStorageImpl @Inject constructor( } - override fun queryRecipes(): PagingSource { - logger.v { "queryRecipes() called" } - return recipeDao.queryRecipesByPages() + override fun queryRecipes(query: String?): PagingSource { + logger.v { "queryRecipes() called with: query = $query" } + return if (query == null) recipeDao.queryRecipesByPages() + else recipeDao.queryRecipesByPages(query) + } override suspend fun refreshAll(recipes: List) { diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactory.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactory.kt new file mode 100644 index 0000000..f3c8fc0 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactory.kt @@ -0,0 +1,8 @@ +package gq.kirmanak.mealient.data.recipes.impl + +import androidx.paging.PagingSource +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity + +interface RecipePagingSourceFactory : () -> PagingSource { + fun setQuery(newQuery: String?) +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt new file mode 100644 index 0000000..2de9e54 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt @@ -0,0 +1,29 @@ +package gq.kirmanak.mealient.data.recipes.impl + +import androidx.paging.PagingSource +import gq.kirmanak.mealient.data.recipes.db.RecipeStorage +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.logging.Logger +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RecipePagingSourceFactoryImpl @Inject constructor( + private val recipeStorage: RecipeStorage, + private val logger: Logger, +) : RecipePagingSourceFactory { + + private val query = AtomicReference(null) + + override fun invoke(): PagingSource { + val currentQuery = query.get() + logger.d { "Creating paging source, query is $currentQuery" } + return recipeStorage.queryRecipes(currentQuery) + } + + override fun setQuery(newQuery: String?) { + logger.v { "setQuery() called with: newQuery = $newQuery" } + query.set(newQuery) + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt index 4bedf65..e49161f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt @@ -19,10 +19,12 @@ import javax.inject.Singleton class RecipeRepoImpl @Inject constructor( private val mediator: RecipesRemoteMediator, private val storage: RecipeStorage, - private val pagingSourceFactory: InvalidatingPagingSourceFactory, + private val pagingSourceFactory: RecipePagingSourceFactory, + private val invalidatingPagingSourceFactory: InvalidatingPagingSourceFactory, private val dataSource: RecipeDataSource, private val logger: Logger, ) : RecipeRepo { + override fun createPager(): Pager { logger.v { "createPager() called" } val pagingConfig = PagingConfig(pageSize = 5, enablePlaceholders = true) @@ -53,4 +55,10 @@ class RecipeRepoImpl @Inject constructor( logger.v { "loadRecipeInfo() returned: $recipeInfo" } return recipeInfo } + + override fun setSearchName(name: String?) { + logger.v { "setSearchName() called with: name = $name" } + pagingSourceFactory.setQuery(name) + invalidatingPagingSourceFactory.invalidate() + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt index 8e6213d..418e310 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt @@ -13,9 +13,7 @@ import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl -import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider -import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProviderImpl -import gq.kirmanak.mealient.data.recipes.impl.RecipeRepoImpl +import gq.kirmanak.mealient.data.recipes.impl.* import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory @@ -46,13 +44,17 @@ interface RecipeModule { @Singleton fun bindModelLoaderFactory(recipeModelLoaderFactory: RecipeModelLoaderFactory): ModelLoaderFactory + @Binds + @Singleton + fun bindRecipePagingSourceFactory(recipePagingSourceFactoryImpl: RecipePagingSourceFactoryImpl): RecipePagingSourceFactory + companion object { @Provides @Singleton fun provideRecipePagingSourceFactory( - recipeStorage: RecipeStorage - ) = InvalidatingPagingSourceFactory { recipeStorage.queryRecipes() } + factory: RecipePagingSourceFactory, + ) = InvalidatingPagingSourceFactory(factory) @Provides @Singleton diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt index 731358b..7cb8fd5 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt @@ -6,6 +6,7 @@ import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage +import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -18,6 +19,7 @@ class MainActivityViewModel @Inject constructor( private val logger: Logger, private val disclaimerStorage: DisclaimerStorage, private val serverInfoRepo: ServerInfoRepo, + private val recipeRepo: RecipeRepo, ) : ViewModel() { private val _uiState = MutableLiveData(MainActivityUiState()) @@ -55,5 +57,6 @@ class MainActivityViewModel @Inject constructor( fun onSearchQuery(query: String) { logger.v { "onSearchQuery() called with: query = $query" } + recipeRepo.setSearchName(query) } } \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json new file mode 100644 index 0000000..518ecca --- /dev/null +++ b/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json @@ -0,0 +1,201 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "1def22b22cb1f09a27de1b3188b857d2", + "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": [] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "recipe_summaries", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_BEFORE_UPDATE BEFORE UPDATE ON `recipe_summaries` BEGIN DELETE FROM `recipe_summaries_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_BEFORE_DELETE BEFORE DELETE ON `recipe_summaries` BEGIN DELETE FROM `recipe_summaries_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_AFTER_UPDATE AFTER UPDATE ON `recipe_summaries` BEGIN INSERT INTO `recipe_summaries_fts`(`docid`, `remote_id`, `name`) VALUES (NEW.`rowid`, NEW.`remote_id`, NEW.`name`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_AFTER_INSERT AFTER INSERT ON `recipe_summaries` BEGIN INSERT INTO `recipe_summaries_fts`(`docid`, `remote_id`, `name`) VALUES (NEW.`rowid`, NEW.`remote_id`, NEW.`name`); END" + ], + "tableName": "recipe_summaries_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, content=`recipe_summaries`)", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [], + "autoGenerate": false + }, + "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, '1def22b22cb1f09a27de1b3188b857d2')" + ] + } +} \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt index 37571cd..239d072 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt @@ -6,12 +6,13 @@ import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.entity.* @Database( - version = 6, + version = 7, entities = [ RecipeSummaryEntity::class, RecipeEntity::class, RecipeIngredientEntity::class, - RecipeInstructionEntity::class + RecipeInstructionEntity::class, + RecipeSummaryFtsEntity::class, ], exportSchema = true, autoMigrations = [ @@ -19,6 +20,7 @@ import gq.kirmanak.mealient.database.recipe.entity.* 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), ] ) @TypeConverters(RoomTypeConverters::class) diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt index b3d1f72..d105404 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt @@ -9,6 +9,9 @@ interface RecipeDao { @Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC") fun queryRecipesByPages(): PagingSource + @Query("SELECT * FROM recipe_summaries JOIN recipe_summaries_fts ON recipe_summaries_fts.remote_id == recipe_summaries.remote_id WHERE recipe_summaries_fts.name MATCH :query ORDER BY date_added DESC") + fun queryRecipesByPages(query: String): PagingSource + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity) diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt new file mode 100644 index 0000000..8336c1b --- /dev/null +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt @@ -0,0 +1,13 @@ +package gq.kirmanak.mealient.database.recipe.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Fts4 + + +@Entity(tableName = "recipe_summaries_fts") +@Fts4(contentEntity = RecipeSummaryEntity::class) +data class RecipeSummaryFtsEntity( + @ColumnInfo(name = "remote_id") val remoteId: String, + @ColumnInfo(name = "name") val name: String, +)