From 9d800366c2667086526dbb9cf2e7457bfdde2e36 Mon Sep 17 00:00:00 2001 From: Stefano Sansone Date: Mon, 19 Feb 2024 23:36:07 +0000 Subject: [PATCH] add dao and repository modules --- .../omnivore/core/data/DataService.kt | 39 ++-- .../core/data/ReadingProgressChangeHandler.kt | 22 +- .../omnivore/core/data/SyncOfflineChanges.kt | 10 +- .../core/data/repository/LibraryRepository.kt | 21 +- .../repository/impl/LibraryRepositoryImpl.kt | 66 ++++++ .../omnivore/core/database/AppDatabase.kt | 5 +- .../omnivore/core/database/BaseDao.kt | 42 ---- .../core/database/dao/SavedItemDao.kt | 104 ++++++++++ .../core/database/entities/SavedItem.kt | 117 +---------- .../core/database/entities/SavedItemLabel.kt | 80 ++++---- .../omnivore/core/network/Networker.kt | 21 +- .../app/omnivore/omnivore/di/AppModule.kt | 7 +- .../app/omnivore/omnivore/di/DaosModule.kt | 17 ++ .../app/omnivore/omnivore/di/DataModule.kt | 18 ++ .../omnivore/omnivore/di/DatabaseModule.kt | 25 +++ .../feature/library/LibraryFilterBar.kt | 188 +++++++++--------- .../omnivore/feature/library/LibraryView.kt | 61 +++--- .../feature/library/LibraryViewModel.kt | 32 +-- .../feature/reader/WebReaderViewModel.kt | 16 +- .../feature/savedItemViews/SavedItemCard.kt | 30 --- 20 files changed, 486 insertions(+), 435 deletions(-) create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/impl/LibraryRepositoryImpl.kt delete mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/BaseDao.kt create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/dao/SavedItemDao.kt create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DaosModule.kt create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DataModule.kt create mode 100644 android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DatabaseModule.kt diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/DataService.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/DataService.kt index 921eaeed2..f21c742de 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/DataService.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/DataService.kt @@ -1,38 +1,31 @@ package app.omnivore.omnivore.core.data -import android.content.Context -import androidx.room.Room -import app.omnivore.omnivore.core.network.Networker import app.omnivore.omnivore.core.database.AppDatabase -import app.omnivore.omnivore.core.database.entities.Highlight import app.omnivore.omnivore.core.database.entities.SavedItem -import kotlinx.coroutines.* +import app.omnivore.omnivore.core.network.Networker +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch import javax.inject.Inject class DataService @Inject constructor( - context: Context, - val networker: Networker + val networker: Networker, + appDatabase: AppDatabase ) { - val savedItemSyncChannel = Channel(capacity = Channel.UNLIMITED) - val highlightSyncChannel = Channel(capacity = Channel.UNLIMITED) + val savedItemSyncChannel = Channel(capacity = Channel.UNLIMITED) - val db = Room.databaseBuilder( - context, - AppDatabase::class.java, "omnivore-database" - ) - .fallbackToDestructiveMigration() - .build() + val db = appDatabase - init { - CoroutineScope(Dispatchers.IO).launch { - startSyncChannels() + init { + CoroutineScope(Dispatchers.IO).launch { + startSyncChannels() + } } - } - fun clearDatabase() { - CoroutineScope(Dispatchers.IO).launch { - db.clearAllTables() + fun clearDatabase() { + CoroutineScope(Dispatchers.IO).launch { + db.clearAllTables() + } } - } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/ReadingProgressChangeHandler.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/ReadingProgressChangeHandler.kt index 319340b92..3a6047c71 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/ReadingProgressChangeHandler.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/ReadingProgressChangeHandler.kt @@ -1,28 +1,34 @@ package app.omnivore.omnivore.core.data import app.omnivore.omnivore.core.data.model.ServerSyncStatus +import app.omnivore.omnivore.core.database.dao.SavedItemDao import app.omnivore.omnivore.core.network.ReadingProgressParams import app.omnivore.omnivore.core.network.updateReadingProgress import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -suspend fun DataService.updateWebReadingProgress(jsonString: String) { +suspend fun DataService.updateWebReadingProgress( + jsonString: String, + savedItemDao: SavedItemDao +) { val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java) val savedItemId = readingProgressParams.id ?: return withContext(Dispatchers.IO) { - val savedItem = db.savedItemDao().findById(savedItemId) ?: return@withContext - savedItem.readingProgress = readingProgressParams.readingProgressPercent ?: 0.0 - savedItem.readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0 - savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue - db.savedItemDao().update(savedItem) + val savedItem = savedItemDao.findById(savedItemId) ?: return@withContext + val updatedItem = savedItem.copy( + readingProgress = readingProgressParams.readingProgressPercent ?: 0.0, + readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0, + serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + ) + savedItemDao.update(updatedItem) val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams) if (isUpdatedOnServer) { - savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue - db.savedItemDao().update(savedItem) + updatedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + savedItemDao.update(updatedItem) } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt index 7c217a8e2..7247d8702 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SyncOfflineChanges.kt @@ -1,6 +1,10 @@ package app.omnivore.omnivore.core.data import android.util.Log +import app.omnivore.omnivore.core.data.model.ServerSyncStatus +import app.omnivore.omnivore.core.database.entities.HighlightChange +import app.omnivore.omnivore.core.database.entities.SavedItem +import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight import app.omnivore.omnivore.core.network.ReadingProgressParams import app.omnivore.omnivore.core.network.createHighlight import app.omnivore.omnivore.core.network.deleteHighlights @@ -13,10 +17,6 @@ import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput import app.omnivore.omnivore.graphql.generated.type.HighlightType import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput -import app.omnivore.omnivore.core.data.model.ServerSyncStatus -import app.omnivore.omnivore.core.database.entities.HighlightChange -import app.omnivore.omnivore.core.database.entities.SavedItem -import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight import com.apollographql.apollo3.api.Optional import kotlinx.coroutines.delay @@ -50,7 +50,7 @@ suspend fun DataService.syncOfflineItemsWithServerIfNeeded() { } private suspend fun DataService.syncSavedItem(item: SavedItem) { - fun updateSyncStatus(status: ServerSyncStatus) { + suspend fun updateSyncStatus(status: ServerSyncStatus) { item.serverSyncStatus = status.rawValue db.savedItemDao().update(item) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/LibraryRepository.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/LibraryRepository.kt index 0dd64d275..fe290b209 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/LibraryRepository.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/LibraryRepository.kt @@ -1,21 +1,16 @@ package app.omnivore.omnivore.core.data.repository -import app.omnivore.omnivore.core.data.DataService import app.omnivore.omnivore.core.data.model.LibraryQuery import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights import kotlinx.coroutines.flow.Flow -import javax.inject.Inject -class LibraryRepository @Inject constructor( - private val dataService: DataService, -) { - fun getSavedItems(query: LibraryQuery): Flow> = - dataService.db.savedItemDao().filteredLibraryData( - query.allowedArchiveStates, - query.sortKey, - query.requiredLabels, - query.excludedLabels, - query.allowedContentReaders - ) +interface LibraryRepository { + fun getSavedItems(query: LibraryQuery): Flow> + + suspend fun updateReadingProgress( + itemId: String, + readingProgressPercentage: Double, + readingProgressAnchorIndex: Int + ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/impl/LibraryRepositoryImpl.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/impl/LibraryRepositoryImpl.kt new file mode 100644 index 000000000..f7f748a19 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/impl/LibraryRepositoryImpl.kt @@ -0,0 +1,66 @@ +package app.omnivore.omnivore.core.data.repository.impl + +import app.omnivore.omnivore.core.data.model.LibraryQuery +import app.omnivore.omnivore.core.data.model.ServerSyncStatus +import app.omnivore.omnivore.core.data.repository.LibraryRepository +import app.omnivore.omnivore.core.database.dao.SavedItemDao +import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights +import app.omnivore.omnivore.core.network.Networker +import app.omnivore.omnivore.core.network.ReadingProgressParams +import app.omnivore.omnivore.core.network.updateReadingProgress +import com.google.gson.Gson +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class LibraryRepositoryImpl @Inject constructor( + private val savedItemDao: SavedItemDao, + private val networker: Networker +): LibraryRepository { + + override fun getSavedItems(query: LibraryQuery): Flow> = + savedItemDao.filteredLibraryData( + query.allowedArchiveStates, + query.sortKey, + hasRequiredLabels = query.requiredLabels.size, + hasExcludedLabels = query.excludedLabels.size, + query.requiredLabels, + query.excludedLabels, + query.allowedContentReaders + ) + + override suspend fun updateReadingProgress( + itemId: String, + readingProgressPercentage: Double, + readingProgressAnchorIndex: Int + ) { + + val jsonString = Gson().toJson( + mapOf( + "id" to itemId, + "readingProgressPercent" to readingProgressPercentage, + "readingProgressAnchorIndex" to readingProgressAnchorIndex, + "force" to true + ) + ) + + val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java) + val savedItemId = readingProgressParams.id ?: return + + + val savedItem = savedItemDao.findById(savedItemId) + val updatedItem = savedItem?.copy( + readingProgress = readingProgressParams.readingProgressPercent ?: 0.0, + readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0, + serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + ) + + updatedItem?.let { savedItemDao.update(updatedItem) } + + val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams) + + if (isUpdatedOnServer) { + updatedItem?.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue + updatedItem?.let { savedItemDao.update(updatedItem) } + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/AppDatabase.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/AppDatabase.kt index 188787184..031e39686 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/AppDatabase.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/AppDatabase.kt @@ -2,6 +2,7 @@ package app.omnivore.omnivore.core.database import androidx.room.Database import androidx.room.RoomDatabase +import app.omnivore.omnivore.core.database.dao.SavedItemDao import app.omnivore.omnivore.core.database.entities.Highlight import app.omnivore.omnivore.core.database.entities.HighlightChange import app.omnivore.omnivore.core.database.entities.HighlightChangesDao @@ -11,7 +12,6 @@ import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRe import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRefDao import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRefDao -import app.omnivore.omnivore.core.database.entities.SavedItemDao import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.core.database.entities.SavedItemLabelDao import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlightsDao @@ -28,7 +28,8 @@ import app.omnivore.omnivore.core.database.entities.ViewerDao SavedItemAndSavedItemLabelCrossRef::class, SavedItemAndHighlightCrossRef::class ], - version = 24 + version = 24, + exportSchema = false ) abstract class AppDatabase : RoomDatabase() { abstract fun viewerDao(): ViewerDao diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/BaseDao.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/BaseDao.kt deleted file mode 100644 index 2b5bcbc7f..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/BaseDao.kt +++ /dev/null @@ -1,42 +0,0 @@ -package app.omnivore.omnivore.core.database - -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Update - - -interface BaseDao { - - /** - * Insert an object in the database. - * - * @param obj the object to be inserted. - */ - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(obj: T) - - /** - * Insert an array of objects in the database. - * - * @param obj the objects to be inserted. - */ - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(vararg obj: T) - - /** - * Update an object from the database. - * - * @param obj the object to be updated - */ - @Update - fun update(obj: T) - - /** - * Delete an object from the database - * - * @param obj the object to be deleted - */ - @Delete - fun delete(obj: T) -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/dao/SavedItemDao.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/dao/SavedItemDao.kt new file mode 100644 index 000000000..67eecba8d --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/dao/SavedItemDao.kt @@ -0,0 +1,104 @@ +package app.omnivore.omnivore.core.database.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import app.omnivore.omnivore.core.database.entities.SavedItem +import app.omnivore.omnivore.core.database.entities.SavedItemQueryConstants +import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights +import kotlinx.coroutines.flow.Flow + +@Dao +interface SavedItemDao { + + @Transaction + @Query("SELECT * FROM savedItem") + fun getAll(): Flow> + + @Query("SELECT * FROM savedItem WHERE savedItemId = :itemID") + suspend fun findById(itemID: String): SavedItem? + + @Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0") + fun getUnSynced(): List + + @Query("SELECT * FROM savedItem WHERE slug = :slug") + fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights? + + @Query("DELETE FROM savedItem WHERE savedItemId = :itemID") + fun deleteById(itemID: String) + + @Query("DELETE FROM savedItem WHERE savedItemId in (:itemIDs)") + fun deleteByIds(itemIDs: List) + + @Update + suspend fun update(savedItem: SavedItem) + + @Transaction + @Query( + "SELECT ${SavedItemQueryConstants.libraryColumns} " + + "FROM SavedItem " + + "LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " + + "LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " + + + "LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " + + "LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " + + + "WHERE SavedItem.savedItemId = :savedItemId " + + + "GROUP BY SavedItem.savedItemId " + ) + fun getLibraryItemById(savedItemId: String): LiveData + + @Transaction + @Query( + "SELECT ${SavedItemQueryConstants.libraryColumns} " + + "FROM SavedItem " + + "LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " + + "LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " + + + "LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " + + "LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " + + + "WHERE SavedItem.savedItemId = :savedItemId " + + + "GROUP BY SavedItem.savedItemId " + ) + suspend fun getById(savedItemId: String): SavedItemWithLabelsAndHighlights? + + @Transaction + @Query( + "SELECT ${SavedItemQueryConstants.libraryColumns} " + + "FROM SavedItem " + + "LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " + + "LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " + + + "LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " + + "LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " + + + "WHERE SavedItem.serverSyncStatus != 2 " + + "AND SavedItem.isArchived IN (:allowedArchiveStates) " + + "AND SavedItem.contentReader IN (:allowedContentReaders) " + + "AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " + + "AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " + + + "GROUP BY SavedItem.savedItemId " + + + "ORDER BY \n" + + "CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" + + "CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" + + + "CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" + + "CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC" + ) + fun filteredLibraryData( + allowedArchiveStates: List, + sortKey: String, + hasRequiredLabels: Int, + hasExcludedLabels: Int, + requiredLabels: List, + excludedLabels: List, + allowedContentReaders: List + ): Flow> +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt index f47198316..fe4abc810 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItem.kt @@ -1,9 +1,13 @@ package app.omnivore.omnivore.core.database.entities import androidx.core.net.toUri -import androidx.lifecycle.LiveData -import androidx.room.* -import kotlinx.coroutines.flow.Flow +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Transaction @Entity data class SavedItem( @@ -128,114 +132,7 @@ abstract class SavedItemWithLabelsAndHighlightsDao { } } -@Dao -interface SavedItemDao { - @Query("SELECT * FROM savedItem") - fun getAll(): List - @Query("SELECT * FROM savedItem WHERE savedItemId = :itemID") - fun findById(itemID: String): SavedItem? - - @Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0") - fun getUnSynced(): List - - @Query("SELECT * FROM savedItem WHERE slug = :slug") - fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights? - - @Query("DELETE FROM savedItem WHERE savedItemId = :itemID") - fun deleteById(itemID: String) - - @Query("DELETE FROM savedItem WHERE savedItemId in (:itemIDs)") - fun deleteByIds(itemIDs: List) - - @Update - fun update(savedItem: SavedItem) - - @Transaction - @Query( - "SELECT ${SavedItemQueryConstants.libraryColumns} " + - "FROM SavedItem " + - "LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " + - "LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " + - - "LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " + - "LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " + - - "WHERE SavedItem.savedItemId = :savedItemId " + - - "GROUP BY SavedItem.savedItemId " - ) - fun getLibraryItemById(savedItemId: String): LiveData - - @Transaction - @Query( - "SELECT ${SavedItemQueryConstants.libraryColumns} " + - "FROM SavedItem " + - "LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " + - "LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " + - - "LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " + - "LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " + - - "WHERE SavedItem.savedItemId = :savedItemId " + - - "GROUP BY SavedItem.savedItemId " - ) - suspend fun getById(savedItemId: String): SavedItemWithLabelsAndHighlights? - - @Transaction - @Query( - "SELECT ${SavedItemQueryConstants.libraryColumns} " + - "FROM SavedItem " + - "LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " + - "LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " + - - "LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " + - "LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " + - - "WHERE SavedItem.serverSyncStatus != 2 " + - "AND SavedItem.isArchived IN (:allowedArchiveStates) " + - "AND SavedItem.contentReader IN (:allowedContentReaders) " + - "AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " + - "AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " + - - "GROUP BY SavedItem.savedItemId " + - - "ORDER BY \n" + - "CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" + - "CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" + - - "CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" + - "CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC" - ) - fun _filteredLibraryData( - allowedArchiveStates: List, - sortKey: String, - hasRequiredLabels: Int, - hasExcludedLabels: Int, - requiredLabels: List, - excludedLabels: List, - allowedContentReaders: List - ): Flow> - - fun filteredLibraryData( - allowedArchiveStates: List, - sortKey: String, - requiredLabels: List, - excludedLabels: List, - allowedContentReaders: List - ): Flow> { - return _filteredLibraryData( - allowedArchiveStates = allowedArchiveStates, - sortKey = sortKey, - hasRequiredLabels = requiredLabels.size, - hasExcludedLabels = excludedLabels.size, - requiredLabels = requiredLabels, - excludedLabels = excludedLabels, - allowedContentReaders = allowedContentReaders - ) - } -} object SavedItemQueryConstants { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt index f65515c3b..a014cc3e2 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/SavedItemLabel.kt @@ -1,66 +1,70 @@ package app.omnivore.omnivore.core.database.entities import androidx.lifecycle.LiveData -import androidx.room.* +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Transaction import app.omnivore.omnivore.core.data.model.ServerSyncStatus @Entity data class SavedItemLabel( - @PrimaryKey val savedItemLabelId: String, - val name: String, - val color: String, - val createdAt: String?, - val labelDescription: String?, - val serverSyncStatus: Int = 0 + @PrimaryKey val savedItemLabelId: String, + val name: String, + val color: String, + val createdAt: String?, + val labelDescription: String?, + val serverSyncStatus: Int = 0 ) @Dao interface SavedItemLabelDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAll(items: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(items: List) - @Transaction - @Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC") - fun getSavedItemLabelsLiveData(): LiveData> + @Transaction + @Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC") + fun getSavedItemLabelsLiveData(): LiveData> - @Transaction - @Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId") - fun updateTempLabel(tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED) + @Transaction + @Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId") + fun updateTempLabel( + tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED + ) - @Transaction - @Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC") - fun namedLabels(names: List): List + @Transaction + @Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC") + fun namedLabels(names: List): List } @Entity( - primaryKeys = ["savedItemLabelId", "savedItemId"], - foreignKeys = [ - ForeignKey( - entity = SavedItem::class, - parentColumns = arrayOf("savedItemId"), - childColumns = arrayOf("savedItemId"), - onDelete = ForeignKey.CASCADE - ), - ForeignKey( - entity = SavedItemLabel::class, - parentColumns = arrayOf("savedItemLabelId"), - childColumns = arrayOf("savedItemLabelId") - ) - ] + primaryKeys = ["savedItemLabelId", "savedItemId"], foreignKeys = [ForeignKey( + entity = SavedItem::class, + parentColumns = arrayOf("savedItemId"), + childColumns = arrayOf("savedItemId"), + onDelete = ForeignKey.CASCADE + ), ForeignKey( + entity = SavedItemLabel::class, + parentColumns = arrayOf("savedItemLabelId"), + childColumns = arrayOf("savedItemLabelId") + )] ) data class SavedItemAndSavedItemLabelCrossRef( - val savedItemLabelId: String, - val savedItemId: String + val savedItemLabelId: String, val savedItemId: String ) @Dao interface SavedItemAndSavedItemLabelCrossRefDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAll(items: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(items: List) - @Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId") - fun deleteRefsBySavedItemId(savedItemId: String) + @Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId") + fun deleteRefsBySavedItemId(savedItemId: String) } // has many highlights diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/Networker.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/Networker.kt index 6a2194b96..32bdb6e16 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/Networker.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/Networker.kt @@ -1,25 +1,20 @@ package app.omnivore.omnivore.core.network +import app.omnivore.omnivore.core.datastore.DatastoreRepository import app.omnivore.omnivore.utils.Constants import app.omnivore.omnivore.utils.DatastoreKeys -import app.omnivore.omnivore.core.datastore.DatastoreRepository import com.apollographql.apollo3.ApolloClient import javax.inject.Inject class Networker @Inject constructor( - private val datastoreRepo: DatastoreRepository + private val datastoreRepo: DatastoreRepository ) { - suspend fun baseUrl() = datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL + suspend fun baseUrl() = + datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL - suspend fun serverUrl() = "${baseUrl()}/api/graphql" - private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: "" + private suspend fun serverUrl() = "${baseUrl()}/api/graphql" + private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: "" - suspend fun publicApolloClient() = ApolloClient.Builder() - .serverUrl(serverUrl()) - .build() - - suspend fun authenticatedApolloClient() = ApolloClient.Builder() - .serverUrl(serverUrl()) - .addHttpHeader("Authorization", value = authToken()) - .build() + suspend fun authenticatedApolloClient() = ApolloClient.Builder().serverUrl(serverUrl()) + .addHttpHeader("Authorization", value = authToken()).build() } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/AppModule.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/AppModule.kt index 2e5bd7ad1..a4d178eca 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/AppModule.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/AppModule.kt @@ -3,6 +3,7 @@ package app.omnivore.omnivore.di import android.content.Context import app.omnivore.omnivore.core.analytics.EventTracker import app.omnivore.omnivore.core.data.DataService +import app.omnivore.omnivore.core.database.AppDatabase import app.omnivore.omnivore.core.datastore.DatastoreRepository import app.omnivore.omnivore.core.datastore.OmnivoreDatastore import app.omnivore.omnivore.core.network.Networker @@ -34,8 +35,8 @@ object AppModule { @Singleton @Provides fun provideDataService( - @ApplicationContext app: Context, - networker: Networker - ) = DataService(app, networker) + networker: Networker, + appDatabase: AppDatabase + ) = DataService(networker, appDatabase) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DaosModule.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DaosModule.kt new file mode 100644 index 000000000..28d5f3422 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DaosModule.kt @@ -0,0 +1,17 @@ +package app.omnivore.omnivore.di + +import app.omnivore.omnivore.core.database.AppDatabase +import app.omnivore.omnivore.core.database.dao.SavedItemDao +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object DaosModule { + @Provides + fun providesTopicsDao( + database: AppDatabase, + ): SavedItemDao = database.savedItemDao() +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DataModule.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DataModule.kt new file mode 100644 index 000000000..e13ded673 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DataModule.kt @@ -0,0 +1,18 @@ +package app.omnivore.omnivore.di + +import app.omnivore.omnivore.core.data.repository.LibraryRepository +import app.omnivore.omnivore.core.data.repository.impl.LibraryRepositoryImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface DataModule { + + @Binds + fun bindsLibraryRepository( + libraryRepository: LibraryRepositoryImpl, + ): LibraryRepository +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DatabaseModule.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DatabaseModule.kt new file mode 100644 index 000000000..e0433c1b5 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DatabaseModule.kt @@ -0,0 +1,25 @@ +package app.omnivore.omnivore.di + +import android.content.Context +import androidx.room.Room +import app.omnivore.omnivore.core.database.AppDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + @Provides + @Singleton + fun providesOmnivoreDatabase( + @ApplicationContext context: Context, + ): AppDatabase = Room.databaseBuilder( + context, + AppDatabase::class.java, + "omnivore-database", + ).build() +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt index 379dbbb0f..c9b1fa236 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryFilterBar.kt @@ -1,15 +1,25 @@ package app.omnivore.omnivore.feature.library -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.AssistChip +import androidx.compose.material3.Icon +import androidx.compose.material3.SuggestionChipDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -23,98 +33,92 @@ import app.omnivore.omnivore.feature.components.LabelChipColors @Composable fun LibraryFilterBar( - viewModel: LibraryViewModel = hiltViewModel() + viewModel: LibraryViewModel = hiltViewModel() ) { - var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) } - val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(SavedItemFilter.INBOX) - val activeLabels: List by viewModel.activeLabelsLiveData.observeAsState(listOf()) + var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) } + val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState( + SavedItemFilter.INBOX + ) + val activeLabels: List by viewModel.activeLabelsLiveData.observeAsState(listOf()) - var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) } - val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(SavedItemSortFilter.NEWEST) - val listState = rememberLazyListState() + var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) } + val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState( + SavedItemSortFilter.NEWEST + ) + val listState = rememberLazyListState() - Column { - LazyRow( - state = listState, - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(start = 6.dp) - .fillMaxWidth() - ) { - item { - AssistChip( - onClick = { isSavedItemFilterMenuExpanded = true }, - label = { Text(activeSavedItemFilter.displayText) }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "drop down button to change primary library filter" - ) - }, - modifier = Modifier.padding(end = 6.dp) - ) - AssistChip( - onClick = { isSavedItemSortFilterMenuExpanded = true }, - label = { Text(activeSavedItemSortFilter.displayText) }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "drop down button to change library sort order" - ) - }, - modifier = Modifier.padding(end = 6.dp) - ) - AssistChip( - onClick = { viewModel.bottomSheetState.value = LibraryBottomSheetState.LABEL }, - label = { Text(stringResource(R.string.library_filter_bar_label_labels)) }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "drop down button to open label selection sheet" - ) - }, - modifier = Modifier.padding(end = 6.dp) - ) - } - items(activeLabels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label -> - val chipColors = LabelChipColors.fromHex(label.color) + Column { + LazyRow( + state = listState, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(start = 6.dp) + .fillMaxWidth() + ) { + item { + AssistChip(onClick = { isSavedItemFilterMenuExpanded = true }, + label = { Text(activeSavedItemFilter.displayText) }, + trailingIcon = { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = "drop down button to change primary library filter" + ) + }, + modifier = Modifier.padding(end = 6.dp) + ) + AssistChip(onClick = { isSavedItemSortFilterMenuExpanded = true }, + label = { Text(activeSavedItemSortFilter.displayText) }, + trailingIcon = { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = "drop down button to change library sort order" + ) + }, + modifier = Modifier.padding(end = 6.dp) + ) + AssistChip( + onClick = { viewModel.bottomSheetState.value = LibraryBottomSheetState.LABEL }, + label = { Text(stringResource(R.string.library_filter_bar_label_labels)) }, + trailingIcon = { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = "drop down button to open label selection sheet" + ) + }, + modifier = Modifier.padding(end = 6.dp) + ) + } + items(activeLabels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label -> + val chipColors = LabelChipColors.fromHex(label.color) - AssistChip( - onClick = { - viewModel.updateAppliedLabels( - (viewModel.activeLabelsLiveData.value ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId } - ) - }, - label = { Text(label.name) }, - border = null, - colors = SuggestionChipDefaults.elevatedSuggestionChipColors( - containerColor = chipColors.containerColor, - labelColor = chipColors.textColor, - iconContentColor = chipColors.textColor - ), - trailingIcon = { - Icon( - Icons.Default.Close, - contentDescription = "close icon to remove label" - ) - }, - modifier = Modifier - .padding(horizontal = 4.dp) - ) - } + AssistChip(onClick = { + viewModel.updateAppliedLabels((viewModel.activeLabelsLiveData.value + ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId }) + }, + label = { Text(label.name) }, + border = null, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + containerColor = chipColors.containerColor, + labelColor = chipColors.textColor, + iconContentColor = chipColors.textColor + ), + trailingIcon = { + Icon( + Icons.Default.Close, contentDescription = "close icon to remove label" + ) + }, + modifier = Modifier.padding(horizontal = 4.dp) + ) + } + } + + SavedItemFilterContextMenu(isExpanded = isSavedItemFilterMenuExpanded, + onDismiss = { isSavedItemFilterMenuExpanded = false }, + actionHandler = { viewModel.updateSavedItemFilter(it) }) + + SavedItemSortFilterContextMenu(isExpanded = isSavedItemSortFilterMenuExpanded, + onDismiss = { isSavedItemSortFilterMenuExpanded = false }, + actionHandler = { viewModel.updateSavedItemSortFilter(it) }) } - - SavedItemFilterContextMenu( - isExpanded = isSavedItemFilterMenuExpanded, - onDismiss = { isSavedItemFilterMenuExpanded = false }, - actionHandler = { viewModel.updateSavedItemFilter(it) } - ) - - SavedItemSortFilterContextMenu( - isExpanded = isSavedItemSortFilterMenuExpanded, - onDismiss = { isSavedItemSortFilterMenuExpanded = false }, - actionHandler = { viewModel.updateSavedItemSortFilter(it) } - ) - } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt index 211df85d1..70728d4d5 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryView.kt @@ -32,6 +32,7 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.rememberDismissState import androidx.compose.material.rememberScaffoldState +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet @@ -44,12 +45,12 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -72,52 +73,52 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @Composable -fun LibraryView( - libraryViewModel: LibraryViewModel = hiltViewModel(), +internal fun LibraryView( labelsViewModel: LabelsViewModel, saveViewModel: SaveViewModel, editInfoViewModel: EditInfoViewModel, - navController: NavHostController + navController: NavHostController, + viewModel: LibraryViewModel = hiltViewModel() ) { val scaffoldState: ScaffoldState = rememberScaffoldState() val coroutineScope = rememberCoroutineScope() - val uiState by libraryViewModel.uiState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val showBottomSheet: LibraryBottomSheetState by libraryViewModel.bottomSheetState.observeAsState( + val showBottomSheet: LibraryBottomSheetState by viewModel.bottomSheetState.observeAsState( LibraryBottomSheetState.HIDDEN ) - libraryViewModel.snackbarMessage?.let { + viewModel.snackbarMessage?.let { coroutineScope.launch { scaffoldState.snackbarHostState.showSnackbar(it) - libraryViewModel.clearSnackbarMessage() + viewModel.clearSnackbarMessage() } } when (showBottomSheet) { LibraryBottomSheetState.ADD_LINK -> { AddLinkBottomSheet(saveViewModel) { - libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN + viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN } } LibraryBottomSheetState.LABEL -> { LabelBottomSheet( - libraryViewModel, + viewModel, labelsViewModel ) { - libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN + viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN } } LibraryBottomSheetState.EDIT -> { EditBottomSheet( editInfoViewModel, - libraryViewModel + viewModel ) { - libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN + viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN } } @@ -129,9 +130,9 @@ fun LibraryView( scaffoldState = scaffoldState, topBar = { LibraryNavigationBar( - savedItemViewModel = libraryViewModel, + savedItemViewModel = viewModel, onSearchClicked = { navController.navigate(Routes.Search.route) }, - onAddLinkClicked = { showAddLinkBottomSheet(libraryViewModel) }, + onAddLinkClicked = { showAddLinkBottomSheet(viewModel) }, onSettingsIconClick = { navController.navigate(Routes.Settings.route) } ) }, @@ -139,13 +140,25 @@ fun LibraryView( when (uiState) { is LibraryUiState.Success -> { LibraryViewContent( - libraryViewModel, + viewModel, modifier = Modifier .padding(top = paddingValues.calculateTopPadding()), - cardsData = (uiState as LibraryUiState.Success).items + uiState = uiState ) } - else -> {} + is LibraryUiState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(strokeCap = StrokeCap.Round) + } + } + else -> { + // TODO + } } } } @@ -285,7 +298,7 @@ fun EditBottomSheet( fun LibraryViewContent( libraryViewModel: LibraryViewModel, modifier: Modifier, - cardsData: List + uiState: LibraryUiState ) { val context = LocalContext.current val listState = rememberLazyListState() @@ -296,9 +309,6 @@ fun LibraryViewContent( ) val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState() -/* val cardsData: List by libraryViewModel.itemsLiveData.observeAsState( - listOf() - )*/ Box( modifier = Modifier @@ -318,13 +328,14 @@ fun LibraryViewContent( LibraryFilterBar(libraryViewModel) } items( - items = cardsData, + items = (uiState as LibraryUiState.Success).items, key = { item -> item.savedItem.savedItemId } ) { cardDataWithLabels -> val swipeThreshold = 0.45f val currentThresholdFraction = remember { mutableStateOf(0f) } - val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem) + //val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem) + val currentItem = cardDataWithLabels.savedItem val swipeState = rememberDismissState( confirmStateChange = { when(it) { @@ -430,7 +441,7 @@ fun LibraryViewContent( } InfiniteListHandler(listState = listState) { - if (cardsData.isEmpty()) { + if ((uiState as LibraryUiState.Success).items.isEmpty()) { Log.d("sync", "loading with load func") libraryViewModel.initialLoad() } else { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt index 4812d9434..68f0e2a36 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryViewModel.kt @@ -20,7 +20,6 @@ import app.omnivore.omnivore.core.data.sync import app.omnivore.omnivore.core.data.syncLabels import app.omnivore.omnivore.core.data.syncOfflineItemsWithServerIfNeeded import app.omnivore.omnivore.core.data.unarchiveSavedItem -import app.omnivore.omnivore.core.data.updateWebReadingProgress import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights import app.omnivore.omnivore.core.datastore.DatastoreRepository @@ -31,7 +30,6 @@ import app.omnivore.omnivore.feature.setSavedItemLabels import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput import app.omnivore.omnivore.utils.DatastoreKeys import com.apollographql.apollo3.api.Optional -import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -79,12 +77,15 @@ class LibraryViewModel @Inject constructor( val uiState: StateFlow = _libraryQuery.flatMapLatest { query -> libraryRepository.getSavedItems(query) - }.map(LibraryUiState::Success).stateIn( + } + .map(LibraryUiState::Success) + .stateIn( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), // Adjust as needed + started = SharingStarted.Eagerly, initialValue = LibraryUiState.Loading ) + private val itemsLiveData = MediatorLiveData>() val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) @@ -99,6 +100,7 @@ class LibraryViewModel @Inject constructor( private var hasLoadedInitialFilters = false private fun loadInitialFilterValues() { + if (hasLoadedInitialFilters) { return } @@ -336,31 +338,13 @@ class LibraryViewModel @Inject constructor( SavedItemAction.MarkRead -> { viewModelScope.launch { - dataService.updateWebReadingProgress( - jsonString = Gson().toJson( - mapOf( - "id" to itemID, - "readingProgressPercent" to 100.0, - "readingProgressAnchorIndex" to 0, - "force" to true - ) - ) - ) + libraryRepository.updateReadingProgress(itemID, 100.0, 0) } } SavedItemAction.MarkUnread -> { viewModelScope.launch { - dataService.updateWebReadingProgress( - jsonString = Gson().toJson( - mapOf( - "id" to itemID, - "readingProgressPercent" to 0, - "readingProgressAnchorIndex" to 0, - "force" to true - ) - ) - ) + libraryRepository.updateReadingProgress(itemID, 0.0, 0) } } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt index f5bcc8680..925c03c7c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/reader/WebReaderViewModel.kt @@ -13,10 +13,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope -import app.omnivore.omnivore.utils.DatastoreKeys -import app.omnivore.omnivore.core.datastore.DatastoreRepository -import app.omnivore.omnivore.core.analytics.EventTracker import app.omnivore.omnivore.R +import app.omnivore.omnivore.core.analytics.EventTracker import app.omnivore.omnivore.core.data.DataService import app.omnivore.omnivore.core.data.archiveSavedItem import app.omnivore.omnivore.core.data.createWebHighlight @@ -26,16 +24,19 @@ import app.omnivore.omnivore.core.data.mergeWebHighlights import app.omnivore.omnivore.core.data.unarchiveSavedItem import app.omnivore.omnivore.core.data.updateWebHighlight import app.omnivore.omnivore.core.data.updateWebReadingProgress -import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput +import app.omnivore.omnivore.core.database.dao.SavedItemDao +import app.omnivore.omnivore.core.database.entities.SavedItem +import app.omnivore.omnivore.core.database.entities.SavedItemLabel +import app.omnivore.omnivore.core.datastore.DatastoreRepository import app.omnivore.omnivore.core.network.Networker import app.omnivore.omnivore.core.network.createNewLabel import app.omnivore.omnivore.core.network.saveUrl import app.omnivore.omnivore.core.network.savedItem -import app.omnivore.omnivore.core.database.entities.SavedItem -import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.feature.components.HighlightColor import app.omnivore.omnivore.feature.library.SavedItemAction import app.omnivore.omnivore.feature.setSavedItemLabels +import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput +import app.omnivore.omnivore.utils.DatastoreKeys import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel @@ -80,6 +81,7 @@ class WebReaderViewModel @Inject constructor( private val dataService: DataService, private val networker: Networker, private val eventTracker: EventTracker, + private val savedItemDao: SavedItemDao // TODO - Use repo ) : ViewModel() { var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID() var javascriptDispatchQueue: MutableList = mutableListOf() @@ -384,7 +386,7 @@ class WebReaderViewModel @Inject constructor( "articleReadingProgress" -> { viewModelScope.launch { - dataService.updateWebReadingProgress(jsonString) + dataService.updateWebReadingProgress(jsonString, savedItemDao) } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemCard.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemCard.kt index f8fd58f3e..f0c966793 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemCard.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/savedItemViews/SavedItemCard.kt @@ -160,36 +160,6 @@ fun readingProgress(item: SavedItemWithLabelsAndHighlights): String { return "" } -//var highlightsText: String { -// item.hig ?.let { -// if let highlights = item.highlights, highlights.count > 0 { -// let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count) -// if item.wordsCount > 0 { -// return " • \(fmted)" -// } -// return fmted -// } -// return "" -//} -// -//var notesText: String { -// let notes = item.highlights?.filter { item in -// if let highlight = item as? Highlight { -// return !(highlight.annotation ?? "").isEmpty -// } -// return false -// } -// -// if let notes = notes, notes.count > 0 { -// let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count) -// if item.wordsCount > 0 { -// return " • \(fmted)" -// } -// return fmted -// } -// return "" -//} - enum class FlairIcon( val rawValue: String, val sortOrder: Int