diff --git a/android/Omnivore/app/build.gradle.kts b/android/Omnivore/app/build.gradle.kts index 46456f00f..c29f05525 100644 --- a/android/Omnivore/app/build.gradle.kts +++ b/android/Omnivore/app/build.gradle.kts @@ -118,6 +118,7 @@ dependencies { implementation(libs.androidx.compose.ui.util) implementation(libs.androidx.activity.compose) implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.hilt.navigation.compose) androidTestImplementation(libs.androidx.compose.ui.test) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling.preview) @@ -165,6 +166,8 @@ dependencies { implementation(libs.compose.markdown) implementation(libs.chiptextfield.m3) + implementation(libs.androidx.lifecycle.runtimeCompose) + } apollo { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt index b6e878e04..688db9601 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt @@ -35,7 +35,6 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) val loginViewModel: LoginViewModel by viewModels() - val libraryViewModel: LibraryViewModel by viewModels() val settingsViewModel: SettingsViewModel by viewModels() val searchViewModel: SearchViewModel by viewModels() val labelsViewModel: LabelsViewModel by viewModels() @@ -65,7 +64,6 @@ class MainActivity : ComponentActivity() { RootView( loginViewModel, searchViewModel, - libraryViewModel, settingsViewModel, labelsViewModel, saveViewModel, 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..3ff92541d 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.OmnivoreDatabase 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, + omnivoreDatabase: OmnivoreDatabase ) { - 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 = omnivoreDatabase - 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/HighlightActionHandlers.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/HighlightActionHandlers.kt index bc2d8e9a7..c4e7700ac 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/HighlightActionHandlers.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/HighlightActionHandlers.kt @@ -1,176 +1,185 @@ 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.Highlight +import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef +import app.omnivore.omnivore.core.database.entities.saveHighlightChange import app.omnivore.omnivore.core.network.CreateHighlightParams import app.omnivore.omnivore.core.network.DeleteHighlightParams import app.omnivore.omnivore.core.network.MergeHighlightsParams import app.omnivore.omnivore.core.network.UpdateHighlightParams -import app.omnivore.omnivore.core.model.ServerSyncStatus -import app.omnivore.omnivore.core.database.entities.Highlight -import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef -import app.omnivore.omnivore.core.database.entities.saveHighlightChange import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.util.* +import java.util.UUID suspend fun DataService.createWebHighlight(jsonString: String, colorName: String?) { - val createHighlightInput = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput() + val createHighlightInput = + Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput() - withContext(Dispatchers.IO) { - val highlight = Highlight( - type = "HIGHLIGHT", - highlightId = createHighlightInput.id, - shortId = createHighlightInput.shortId, - quote = createHighlightInput.quote.getOrNull(), - prefix = null, - suffix = null, - patch = createHighlightInput.patch.getOrNull(), - annotation = createHighlightInput.annotation.getOrNull(), - createdAt = null, - updatedAt = null, - createdByMe = false, - color = colorName ?: createHighlightInput.color.getOrNull(), - highlightPositionPercent = createHighlightInput.highlightPositionPercent.getOrNull() ?: 0.0, - highlightPositionAnchorIndex = createHighlightInput.highlightPositionAnchorIndex.getOrNull() ?: 0 - ) + withContext(Dispatchers.IO) { + val highlight = Highlight( + type = "HIGHLIGHT", + highlightId = createHighlightInput.id, + shortId = createHighlightInput.shortId, + quote = createHighlightInput.quote.getOrNull(), + prefix = null, + suffix = null, + patch = createHighlightInput.patch.getOrNull(), + annotation = createHighlightInput.annotation.getOrNull(), + createdAt = null, + updatedAt = null, + createdByMe = false, + color = colorName ?: createHighlightInput.color.getOrNull(), + highlightPositionPercent = createHighlightInput.highlightPositionPercent.getOrNull() + ?: 0.0, + highlightPositionAnchorIndex = createHighlightInput.highlightPositionAnchorIndex.getOrNull() + ?: 0 + ) - highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue - val highlightChange = saveHighlightChange(db.highlightChangesDao(), createHighlightInput.articleId, highlight) + val highlightChange = + saveHighlightChange(db.highlightChangesDao(), createHighlightInput.articleId, highlight) - val crossRef = SavedItemAndHighlightCrossRef( - highlightId = createHighlightInput.id, - savedItemId = createHighlightInput.articleId - ) + val crossRef = SavedItemAndHighlightCrossRef( + highlightId = createHighlightInput.id, savedItemId = createHighlightInput.articleId + ) - db.highlightDao().insertAll(listOf(highlight)) - db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) + db.highlightDao().insertAll(listOf(highlight)) + db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) - performHighlightChange(highlightChange) - } + performHighlightChange(highlightChange) + } } suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String { - val shortId = NanoId.generate(size = 14) - val createHighlightId = UUID.randomUUID().toString() + val shortId = NanoId.generate(size = 14) + val createHighlightId = UUID.randomUUID().toString() - withContext(Dispatchers.IO) { - val highlight = Highlight( - type = "NOTE", - highlightId = createHighlightId, - shortId = shortId, - quote = null, - prefix = null, - suffix = null, - patch =null, - annotation = note, - createdAt = null, - updatedAt = null, - createdByMe = true, - color = null, - highlightPositionAnchorIndex = 0, - highlightPositionPercent = 0.0 - ) + withContext(Dispatchers.IO) { + val highlight = Highlight( + type = "NOTE", + highlightId = createHighlightId, + shortId = shortId, + quote = null, + prefix = null, + suffix = null, + patch = null, + annotation = note, + createdAt = null, + updatedAt = null, + createdByMe = true, + color = null, + highlightPositionAnchorIndex = 0, + highlightPositionPercent = 0.0 + ) - highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue - val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight) + val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight) - val crossRef = SavedItemAndHighlightCrossRef( - highlightId = createHighlightId, - savedItemId = savedItemId - ) + val crossRef = SavedItemAndHighlightCrossRef( + highlightId = createHighlightId, savedItemId = savedItemId + ) - db.highlightDao().insertAll(listOf(highlight)) - db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) + db.highlightDao().insertAll(listOf(highlight)) + db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) - performHighlightChange(highlightChange) - } + performHighlightChange(highlightChange) + } - return createHighlightId + return createHighlightId } suspend fun DataService.mergeWebHighlights(jsonString: String) { - val mergeHighlightInput = Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput() - Log.d("sync", "mergeHighlightInput: " + mergeHighlightInput.id + ": " + mergeHighlightInput) + val mergeHighlightInput = + Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput() + Log.d("sync", "mergeHighlightInput: " + mergeHighlightInput.id + ": " + mergeHighlightInput) - withContext(Dispatchers.IO) { - val highlight = Highlight( - type = "HIGHLIGHT", - highlightId = mergeHighlightInput.id, - shortId = mergeHighlightInput.shortId, - quote = mergeHighlightInput.quote, - prefix = null, - suffix = null, - patch = mergeHighlightInput.patch, - annotation = mergeHighlightInput.annotation.getOrNull(), - createdAt = null, - updatedAt = null, - createdByMe = false, - color = mergeHighlightInput.color.getOrNull(), - highlightPositionPercent = mergeHighlightInput.highlightPositionPercent.getOrNull() ?: 0.0, - highlightPositionAnchorIndex = mergeHighlightInput.highlightPositionAnchorIndex.getOrNull() ?: 0 - ) + withContext(Dispatchers.IO) { + val highlight = Highlight( + type = "HIGHLIGHT", + highlightId = mergeHighlightInput.id, + shortId = mergeHighlightInput.shortId, + quote = mergeHighlightInput.quote, + prefix = null, + suffix = null, + patch = mergeHighlightInput.patch, + annotation = mergeHighlightInput.annotation.getOrNull(), + createdAt = null, + updatedAt = null, + createdByMe = false, + color = mergeHighlightInput.color.getOrNull(), + highlightPositionPercent = mergeHighlightInput.highlightPositionPercent.getOrNull() + ?: 0.0, + highlightPositionAnchorIndex = mergeHighlightInput.highlightPositionAnchorIndex.getOrNull() + ?: 0 + ) - highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue - val highlightChange = saveHighlightChange( - db.highlightChangesDao(), - mergeHighlightInput.articleId, - highlight, - html = mergeHighlightInput.html.getOrNull(), - overlappingIDs = mergeHighlightInput.overlapHighlightIdList - ) + val highlightChange = saveHighlightChange( + db.highlightChangesDao(), + mergeHighlightInput.articleId, + highlight, + html = mergeHighlightInput.html.getOrNull(), + overlappingIDs = mergeHighlightInput.overlapHighlightIdList + ) - val crossRef = SavedItemAndHighlightCrossRef( - highlightId = mergeHighlightInput.id, - savedItemId = mergeHighlightInput.articleId - ) + val crossRef = SavedItemAndHighlightCrossRef( + highlightId = mergeHighlightInput.id, savedItemId = mergeHighlightInput.articleId + ) - db.highlightDao().insertAll(listOf(highlight)) - db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) + db.highlightDao().insertAll(listOf(highlight)) + db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef)) - Log.d("sync", "Setting up highlight merge") - performHighlightChange(highlightChange) - } + Log.d("sync", "Setting up highlight merge") + performHighlightChange(highlightChange) + } } suspend fun DataService.updateWebHighlight(jsonString: String) { - val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java) + val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java) - if (updateHighlightParams.highlightId == null || updateHighlightParams.libraryItemId == null) { - Log.d("error","ERROR INVALID HIGHLIGHT DATA") - return - } + if (updateHighlightParams.highlightId == null || updateHighlightParams.libraryItemId == null) { + Log.d("error", "ERROR INVALID HIGHLIGHT DATA") + return + } - withContext(Dispatchers.IO) { - val highlight = db.highlightDao().findById(highlightId = updateHighlightParams.highlightId ?: "") ?: return@withContext + withContext(Dispatchers.IO) { + val highlight = + db.highlightDao().findById(highlightId = updateHighlightParams.highlightId) + ?: return@withContext - highlight.annotation = updateHighlightParams.annotation - highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue - db.highlightDao().update(highlight) + highlight.annotation = updateHighlightParams.annotation + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue + db.highlightDao().update(highlight) - val highlightChange = saveHighlightChange(db.highlightChangesDao(), updateHighlightParams.libraryItemId ?: "", highlight) - performHighlightChange(highlightChange) - } + val highlightChange = saveHighlightChange( + db.highlightChangesDao(), updateHighlightParams.libraryItemId, highlight + ) + performHighlightChange(highlightChange) + } } suspend fun DataService.deleteHighlightFromJSON(jsonString: String) { - val deleteHighlightParams = Gson().fromJson(jsonString, DeleteHighlightParams::class.java) - deleteHighlight(deleteHighlightParams.libraryItemId, deleteHighlightParams.highlightId) + val deleteHighlightParams = Gson().fromJson(jsonString, DeleteHighlightParams::class.java) + deleteHighlight(deleteHighlightParams.libraryItemId, deleteHighlightParams.highlightId) } private suspend fun DataService.deleteHighlight(savedItemId: String, highlightID: String) { - withContext(Dispatchers.IO) { - val highlight = db.highlightDao().findById(highlightId = highlightID) + withContext(Dispatchers.IO) { + val highlight = db.highlightDao().findById(highlightId = highlightID) - highlight?.let { - highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue - db.highlightDao().update(highlight) + highlight?.let { + highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue + db.highlightDao().update(highlight) - val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight) - performHighlightChange(highlightChange) + val highlightChange = + saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight) + performHighlightChange(highlightChange) + } } - } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt index dabe14122..f3ffba8a6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/LibrarySync.kt @@ -8,7 +8,7 @@ import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighli import app.omnivore.omnivore.core.network.savedItem import app.omnivore.omnivore.core.network.savedItemUpdates import app.omnivore.omnivore.core.network.search -import app.omnivore.omnivore.core.model.ServerSyncStatus +import app.omnivore.omnivore.core.data.model.ServerSyncStatus suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult { val searchResult = networker.search(cursor = cursor, limit = 10, query = query) 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 4f06bc07e..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.model.ServerSyncStatus +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/SavedItemMenuActionHandlers.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SavedItemMenuActionHandlers.kt index 471e5afb7..c6246c786 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SavedItemMenuActionHandlers.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/SavedItemMenuActionHandlers.kt @@ -1,6 +1,6 @@ package app.omnivore.omnivore.core.data -import app.omnivore.omnivore.core.model.ServerSyncStatus +import app.omnivore.omnivore.core.data.model.ServerSyncStatus import app.omnivore.omnivore.core.network.archiveSavedItem import app.omnivore.omnivore.core.network.deleteSavedItem import app.omnivore.omnivore.core.network.unarchiveSavedItem 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 c818af698..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.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/model/LibraryQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/model/LibraryQuery.kt new file mode 100644 index 000000000..fe33fa46b --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/model/LibraryQuery.kt @@ -0,0 +1,9 @@ +package app.omnivore.omnivore.core.data.model + +data class LibraryQuery( + val allowedArchiveStates: List, + val sortKey: String, + val requiredLabels: List, + val excludedLabels: List, + val allowedContentReaders: List +) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/model/ServerSyncStatus.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/model/ServerSyncStatus.kt similarity index 79% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/model/ServerSyncStatus.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/model/ServerSyncStatus.kt index 2bbb6c930..dbff112b3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/model/ServerSyncStatus.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/model/ServerSyncStatus.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore.core.model +package app.omnivore.omnivore.core.data.model enum class ServerSyncStatus(val rawValue: Int) { IS_SYNCED(0), 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 new file mode 100644 index 000000000..fe290b209 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/data/repository/LibraryRepository.kt @@ -0,0 +1,16 @@ +package app.omnivore.omnivore.core.data.repository + +import app.omnivore.omnivore.core.data.model.LibraryQuery +import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights +import kotlinx.coroutines.flow.Flow + +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/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/AppDatabase.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt similarity index 55% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/AppDatabase.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/OmnivoreDatabase.kt index 188787184..cb8db0c08 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/OmnivoreDatabase.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 @@ -19,24 +19,24 @@ import app.omnivore.omnivore.core.database.entities.Viewer import app.omnivore.omnivore.core.database.entities.ViewerDao @Database( - entities = [ - Viewer::class, - SavedItem::class, - SavedItemLabel::class, - Highlight::class, - HighlightChange::class, - SavedItemAndSavedItemLabelCrossRef::class, - SavedItemAndHighlightCrossRef::class - ], - version = 24 + entities = [ + Viewer::class, + SavedItem::class, + SavedItemLabel::class, + Highlight::class, + HighlightChange::class, + SavedItemAndSavedItemLabelCrossRef::class, + SavedItemAndHighlightCrossRef::class], + version = 24, + exportSchema = true ) -abstract class AppDatabase : RoomDatabase() { - abstract fun viewerDao(): ViewerDao - abstract fun savedItemDao(): SavedItemDao - abstract fun highlightDao(): HighlightDao - abstract fun highlightChangesDao(): HighlightChangesDao - abstract fun savedItemLabelDao(): SavedItemLabelDao - abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao - abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao - abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao +abstract class OmnivoreDatabase : RoomDatabase() { + abstract fun viewerDao(): ViewerDao + abstract fun savedItemDao(): SavedItemDao + abstract fun highlightDao(): HighlightDao + abstract fun highlightChangesDao(): HighlightChangesDao + abstract fun savedItemLabelDao(): SavedItemLabelDao + abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao + abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao + abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao } 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..3e26960e0 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/dao/SavedItemDao.kt @@ -0,0 +1,103 @@ +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 { + + @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/Highlight.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt index 6d52e6203..50c078ab0 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/Highlight.kt @@ -1,7 +1,7 @@ package app.omnivore.omnivore.core.database.entities import androidx.room.* -import app.omnivore.omnivore.core.model.ServerSyncStatus +import app.omnivore.omnivore.core.data.model.ServerSyncStatus import com.google.gson.annotations.SerializedName @@ -73,7 +73,20 @@ data class SavedItemWithLabelsAndHighlights( associateBy = Junction(SavedItemAndHighlightCrossRef::class) ) val highlights: List -) +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SavedItemWithLabelsAndHighlights + + return savedItem.savedItemId == other.savedItem.savedItemId + } + + override fun hashCode(): Int { + return savedItem.savedItemId.hashCode() + } +} @Dao interface HighlightDao { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt index 33be6c46c..8a09cb0cb 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/database/entities/HighlightChange.kt @@ -9,7 +9,7 @@ import androidx.room.PrimaryKey import androidx.room.Query import androidx.room.TypeConverter import androidx.room.TypeConverters -import app.omnivore.omnivore.core.model.ServerSyncStatus +import app.omnivore.omnivore.core.data.model.ServerSyncStatus import com.google.gson.Gson import com.google.gson.reflect.TypeToken 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 33df145a5..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 java.util.* +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 - ): LiveData> - - fun filteredLibraryData( - allowedArchiveStates: List, - sortKey: String, - requiredLabels: List, - excludedLabels: List, - allowedContentReaders: List - ): LiveData> { - 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 485238ff5..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 app.omnivore.omnivore.core.model.ServerSyncStatus +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/core/network/SearchQuery.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt index 31664e652..f12a73a42 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/core/network/SearchQuery.kt @@ -4,7 +4,7 @@ import app.omnivore.omnivore.core.database.entities.Highlight import app.omnivore.omnivore.core.database.entities.SavedItem import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.graphql.generated.SearchQuery -import app.omnivore.omnivore.core.model.ServerSyncStatus +import app.omnivore.omnivore.core.data.model.ServerSyncStatus import com.apollographql.apollo3.api.Optional data class LibrarySearchQueryResponse( 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 70de06127..3f4b315e2 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 @@ -1,10 +1,11 @@ package app.omnivore.omnivore.di import android.content.Context -import app.omnivore.omnivore.core.datastore.DatastoreRepository import app.omnivore.omnivore.core.analytics.EventTracker -import app.omnivore.omnivore.core.datastore.OmnivoreDatastore import app.omnivore.omnivore.core.data.DataService +import app.omnivore.omnivore.core.database.OmnivoreDatabase +import app.omnivore.omnivore.core.datastore.DatastoreRepository +import app.omnivore.omnivore.core.datastore.OmnivoreDatastore import app.omnivore.omnivore.core.network.Networker import dagger.Module import dagger.Provides @@ -34,7 +35,8 @@ object AppModule { @Singleton @Provides fun provideDataService( - @ApplicationContext app: Context, - networker: Networker - ) = DataService(app, networker) + networker: Networker, + omnivoreDatabase: OmnivoreDatabase + ) = DataService(networker, omnivoreDatabase) + } 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..f2175bb1f --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/di/DaosModule.kt @@ -0,0 +1,18 @@ +package app.omnivore.omnivore.di + +import app.omnivore.omnivore.core.database.OmnivoreDatabase +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 providesSavedItemDao( + database: OmnivoreDatabase, + ): 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..4b77f3267 --- /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.OmnivoreDatabase +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, + ): OmnivoreDatabase = Room.databaseBuilder( + context, + OmnivoreDatabase::class.java, + "omnivore-database", + ).build() +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsViewModel.kt index ecc1bc741..212446e5f 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/components/LabelsViewModel.kt @@ -1,7 +1,7 @@ package app.omnivore.omnivore.feature.components import androidx.lifecycle.* -import app.omnivore.omnivore.core.model.ServerSyncStatus +import app.omnivore.omnivore.core.data.model.ServerSyncStatus import app.omnivore.omnivore.core.database.entities.SavedItemLabel import dagger.hilt.android.lifecycle.HiltViewModel import java.time.LocalDate 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 4c9c317ec..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,117 +1,124 @@ 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 import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import app.omnivore.omnivore.R import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.feature.components.LabelChipColors @Composable -fun LibraryFilterBar(viewModel: LibraryViewModel) { - var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) } - val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(SavedItemFilter.INBOX) - val activeLabels: List by viewModel.activeLabelsLiveData.observeAsState(listOf()) +fun LibraryFilterBar( + 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 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/LibraryNavigationBar.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryNavigationBar.kt index a65973a40..74ae4438d 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryNavigationBar.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/library/LibraryNavigationBar.kt @@ -131,7 +131,7 @@ fun LibraryNavigationBar( contentDescription = null ) } -/* IconButton(onClick = { isMenuExpanded = true } ) { + IconButton(onClick = { isMenuExpanded = true } ) { Icon( imageVector = Icons.Default.MoreVert, contentDescription = null @@ -144,7 +144,7 @@ fun LibraryNavigationBar( onDismiss = { isMenuExpanded = false }, ) } - }*/ + } } ?: run { IconButton(onClick = onSearchClicked) { Icon( 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 c45230d77..a654b4bef 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 @@ -20,7 +20,6 @@ import androidx.compose.material.DismissValue import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.FractionalThreshold import androidx.compose.material.Icon -import androidx.compose.material.Scaffold import androidx.compose.material.ScaffoldState import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons @@ -32,9 +31,11 @@ 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 +import androidx.compose.material3.Scaffold import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -50,8 +51,11 @@ 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 +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights @@ -70,50 +74,52 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @Composable -fun LibraryView( - libraryViewModel: LibraryViewModel, +internal fun LibraryView( labelsViewModel: LabelsViewModel, saveViewModel: SaveViewModel, editInfoViewModel: EditInfoViewModel, - navController: NavHostController + navController: NavHostController, + viewModel: LibraryViewModel = hiltViewModel() ) { val scaffoldState: ScaffoldState = rememberScaffoldState() val coroutineScope = rememberCoroutineScope() - val showBottomSheet: LibraryBottomSheetState by libraryViewModel.bottomSheetState.observeAsState( + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + 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 } } @@ -122,21 +128,38 @@ fun LibraryView( } Scaffold( - 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) } ) }, ) { paddingValues -> - LibraryViewContent( - libraryViewModel, - modifier = Modifier - .padding(top = paddingValues.calculateTopPadding()) - ) + when (uiState) { + is LibraryUiState.Success -> { + LibraryViewContent( + viewModel, + modifier = Modifier + .padding(top = paddingValues.calculateTopPadding()), + uiState = uiState + ) + } + is LibraryUiState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(strokeCap = StrokeCap.Round) + } + } + else -> { + // TODO + } + } } } @@ -169,7 +192,7 @@ fun LabelBottomSheet( labelsViewModel = labelsViewModel, initialSelectedLabels = currentSavedItemData.labels, onCancel = { - libraryViewModel.currentItemLiveData.value = null + libraryViewModel.currentItem.value = null onDismiss() }, isLibraryMode = false, @@ -180,7 +203,7 @@ fun LabelBottomSheet( labels = it ) } - libraryViewModel.currentItemLiveData.value = null + libraryViewModel.currentItem.value = null onDismiss() }, onCreateLabel = { newLabelName, labelHexValue -> @@ -196,7 +219,7 @@ fun LabelBottomSheet( isLibraryMode = true, onSave = { libraryViewModel.updateAppliedLabels(it) - libraryViewModel.currentItemLiveData.value = null + libraryViewModel.currentItem.value = null onDismiss() }, onCreateLabel = { newLabelName, labelHexValue -> @@ -257,11 +280,11 @@ fun EditBottomSheet( description = currentSavedItemData?.savedItem?.descriptionText, viewModel = editInfoViewModel, onCancel = { - libraryViewModel.currentItemLiveData.value = null + libraryViewModel.currentItem.value = null onDismiss() }, onUpdated = { - libraryViewModel.currentItemLiveData.value = null + libraryViewModel.currentItem.value = null libraryViewModel.refresh() onDismiss() } @@ -272,7 +295,11 @@ fun EditBottomSheet( @OptIn(ExperimentalMaterialApi::class) @Composable -fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { +fun LibraryViewContent( + libraryViewModel: LibraryViewModel, + modifier: Modifier, + uiState: LibraryUiState +) { val context = LocalContext.current val listState = rememberLazyListState() @@ -282,9 +309,6 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { ) val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState() - val cardsData: List by libraryViewModel.itemsLiveData.observeAsState( - listOf() - ) Box( modifier = Modifier @@ -299,13 +323,12 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { modifier = modifier .background(MaterialTheme.colorScheme.background) .fillMaxSize() - .padding(horizontal = 6.dp) ) { item { LibraryFilterBar(libraryViewModel) } items( - items = cardsData, + items = (uiState as LibraryUiState.Success).items, key = { item -> item.savedItem.savedItemId } ) { cardDataWithLabels -> val swipeThreshold = 0.45f @@ -388,10 +411,15 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { dismissContent = { val selected = currentItem.savedItemId == selectedItem?.savedItem?.savedItemId + val test = SavedItemWithLabelsAndHighlights( + savedItem = cardDataWithLabels.savedItem, + labels = listOf(), + highlights = listOf() + ) SavedItemCard( selected = selected, savedItemViewModel = libraryViewModel, - savedItem = cardDataWithLabels, + savedItem = test, onClickHandler = { libraryViewModel.actionsMenuItemLiveData.postValue(null) val activityClass = @@ -417,7 +445,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) { } 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 a18e33ef3..c42f30cd4 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 @@ -7,8 +7,6 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.omnivore.omnivore.utils.DatastoreKeys -import app.omnivore.omnivore.core.datastore.DatastoreRepository import app.omnivore.omnivore.R import app.omnivore.omnivore.core.data.DataService import app.omnivore.omnivore.core.data.archiveSavedItem @@ -16,25 +14,29 @@ import app.omnivore.omnivore.core.data.deleteSavedItem import app.omnivore.omnivore.core.data.fetchSavedItemContent import app.omnivore.omnivore.core.data.isSavedItemContentStoredInDB import app.omnivore.omnivore.core.data.librarySearch +import app.omnivore.omnivore.core.data.model.LibraryQuery +import app.omnivore.omnivore.core.data.repository.LibraryRepository 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.graphql.generated.type.CreateLabelInput -import app.omnivore.omnivore.core.network.Networker -import app.omnivore.omnivore.core.network.createNewLabel import app.omnivore.omnivore.core.database.entities.SavedItemLabel import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights +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.feature.ResourceProvider 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 import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -46,34 +48,60 @@ class LibraryViewModel @Inject constructor( private val networker: Networker, private val dataService: DataService, private val datastoreRepo: DatastoreRepository, - private val resourceProvider: ResourceProvider + private val resourceProvider: ResourceProvider, + private val libraryRepository: LibraryRepository, ) : ViewModel(), SavedItemViewModel { private val contentRequestChannel = Channel(capacity = Channel.UNLIMITED) private var cursor: String? = null private var librarySearchCursor: String? = null - // These are used to make sure we handle search result - // responses in the right order - private var searchIdx = 0 - private var receivedIdx = 0 - var snackbarMessage by mutableStateOf(null) private set - // Live Data - private var itemsLiveDataInternal = dataService.db.savedItemDao().filteredLibraryData( - allowedArchiveStates = listOf(0), - sortKey = "newest", - requiredLabels = listOf(), - excludedLabels = listOf(), - allowedContentReaders = listOf("WEB", "PDF", "EPUB") + private val _libraryQuery = MutableStateFlow( + LibraryQuery( + allowedArchiveStates = listOf(0), + sortKey = "newest", + requiredLabels = listOf(), + excludedLabels = listOf(), + allowedContentReaders = listOf("WEB", "PDF", "EPUB") + ) ) - val itemsLiveData = MediatorLiveData>() + + // Correct way - but not working +/* val uiState: StateFlow = _libraryQuery.flatMapLatest { query -> + libraryRepository.getSavedItems(query) + } + .map(LibraryUiState::Success) + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = LibraryUiState.Loading + )*/ + + // This approach needs to be replaced with the StateFlow above after fixing Room Flow + private val _uiState = MutableStateFlow(LibraryUiState.Loading) + val uiState: StateFlow = _uiState + + init { + loadSavedItems() + } + + private fun loadSavedItems() { + viewModelScope.launch { + libraryRepository.getSavedItems(_libraryQuery.value) + .collect { favoriteNews -> + _uiState.value = LibraryUiState.Success(favoriteNews) + } + } + } + + private val itemsLiveData = MediatorLiveData>() val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX) val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST) val bottomSheetState = MutableLiveData(LibraryBottomSheetState.HIDDEN) - val currentItemLiveData = MutableLiveData(null) + val currentItem = mutableStateOf(null) val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData() val activeLabelsLiveData = MutableLiveData>(listOf()) @@ -83,6 +111,7 @@ class LibraryViewModel @Inject constructor( private var hasLoadedInitialFilters = false private fun loadInitialFilterValues() { + if (hasLoadedInitialFilters) { return } @@ -130,8 +159,6 @@ class LibraryViewModel @Inject constructor( hasLoadedInitialFilters = false cursor = null librarySearchCursor = null - searchIdx = 0 - receivedIdx = 0 } if (hasLoadedInitialFilters) { @@ -153,8 +180,7 @@ class LibraryViewModel @Inject constructor( viewModelScope.launch { withContext(Dispatchers.IO) { val result = dataService.librarySearch( - cursor = librarySearchCursor, - query = searchQueryString() + cursor = librarySearchCursor, query = searchQueryString() ) result.cursor?.let { librarySearchCursor = it @@ -237,17 +263,14 @@ class LibraryViewModel @Inject constructor( else -> listOf() } - val newData = dataService.db.savedItemDao().filteredLibraryData( + _libraryQuery.value = LibraryQuery( allowedArchiveStates = allowedArchiveStates, sortKey = sortKey, requiredLabels = requiredLabels, excludedLabels = excludeLabels, allowedContentReaders = allowedContentReaders ) - - itemsLiveData.removeSource(itemsLiveDataInternal) - itemsLiveDataInternal = newData - itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue) + loadSavedItems() } } @@ -316,42 +339,28 @@ class LibraryViewModel @Inject constructor( } SavedItemAction.EditLabels -> { - currentItemLiveData.value = itemID + currentItem.value = itemID bottomSheetState.value = LibraryBottomSheetState.LABEL } SavedItemAction.EditInfo -> { - currentItemLiveData.value = itemID + currentItem.value = itemID bottomSheetState.value = LibraryBottomSheetState.EDIT } SavedItemAction.MarkRead -> { viewModelScope.launch { - dataService.updateWebReadingProgress( - jsonString = Gson().toJson( - mapOf( - "id" to itemID, - "readingProgressPercent" to 100.0, - "readingProgressAnchorIndex" to 0, - "force" to true - ) - ) - ) + _uiState.value = LibraryUiState.Success(emptyList()) + libraryRepository.updateReadingProgress(itemID, 100.0, 0) + loadSavedItems() } } SavedItemAction.MarkUnread -> { viewModelScope.launch { - dataService.updateWebReadingProgress( - jsonString = Gson().toJson( - mapOf( - "id" to itemID, - "readingProgressPercent" to 0, - "readingProgressAnchorIndex" to 0, - "force" to true - ) - ) - ) + _uiState.value = LibraryUiState.Success(emptyList()) + libraryRepository.updateReadingProgress(itemID, 0.0, 0) + loadSavedItems() } } } @@ -424,7 +433,7 @@ class LibraryViewModel @Inject constructor( } fun currentSavedItemUnderEdit(): SavedItemWithLabelsAndHighlights? { - currentItemLiveData.value?.let { itemID -> + currentItem.value?.let { itemID -> return itemsLiveData.value?.first { it.savedItem.savedItemId == itemID } } @@ -446,12 +455,16 @@ class LibraryViewModel @Inject constructor( } } -enum class SavedItemAction { - Delete, - Archive, - Unarchive, - EditLabels, - EditInfo, - MarkRead, - MarkUnread +sealed interface LibraryUiState { + data object Loading : LibraryUiState + + data class Success( + val items: List, + ) : LibraryUiState + + data object Error : LibraryUiState +} + +enum class SavedItemAction { + Delete, Archive, Unarchive, EditLabels, EditInfo, MarkRead, MarkUnread } 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 43848ceeb..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() @@ -333,31 +335,11 @@ class WebReaderViewModel @Inject constructor( } SavedItemAction.MarkRead -> { - viewModelScope.launch { - dataService.updateWebReadingProgress( - jsonString = Gson().toJson( - mapOf( - "id" to itemID, - "readingProgressPercent" to 100.0, - "readingProgressAnchorIndex" to 0 - ) - ) - ) - } + // TODO } SavedItemAction.MarkUnread -> { - viewModelScope.launch { - dataService.updateWebReadingProgress( - jsonString = Gson().toJson( - mapOf( - "id" to itemID, - "readingProgressPercent" to 0, - "readingProgressAnchorIndex" to 0 - ) - ) - ) - } + // TODO } } } @@ -381,14 +363,8 @@ class WebReaderViewModel @Inject constructor( } } -// fun setHighlightColor(color: HighlightColor) { -// CoroutineScope(Dispatchers.Main).launch { -// highlightColor.postValue(color) -// } -// } - fun handleIncomingWebMessage(actionID: String, jsonString: String) { - Log.d("sync", "incoming change: ${actionID}: ${jsonString}") + Log.d("sync", "incoming change: ${actionID}: $jsonString") when (actionID) { "createHighlight" -> { viewModelScope.launch { @@ -410,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/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt index 42643e2f3..f68b250b8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/feature/root/RootView.kt @@ -8,25 +8,23 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import app.omnivore.omnivore.navigation.Routes import app.omnivore.omnivore.feature.auth.LoginViewModel import app.omnivore.omnivore.feature.auth.WelcomeScreen import app.omnivore.omnivore.feature.components.LabelsViewModel import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel import app.omnivore.omnivore.feature.library.LibraryView -import app.omnivore.omnivore.feature.library.LibraryViewModel import app.omnivore.omnivore.feature.library.SearchView import app.omnivore.omnivore.feature.library.SearchViewModel import app.omnivore.omnivore.feature.save.SaveViewModel import app.omnivore.omnivore.feature.settings.PolicyWebView import app.omnivore.omnivore.feature.settings.SettingsView import app.omnivore.omnivore.feature.settings.SettingsViewModel +import app.omnivore.omnivore.navigation.Routes @Composable fun RootView( loginViewModel: LoginViewModel, searchViewModel: SearchViewModel, - libraryViewModel: LibraryViewModel, settingsViewModel: SettingsViewModel, labelsViewModel: LabelsViewModel, saveViewModel: SaveViewModel, @@ -39,7 +37,6 @@ fun RootView( PrimaryNavigator( loginViewModel = loginViewModel, searchViewModel = searchViewModel, - libraryViewModel = libraryViewModel, settingsViewModel = settingsViewModel, labelsViewModel = labelsViewModel, saveViewModel = saveViewModel, @@ -61,7 +58,6 @@ fun RootView( @Composable fun PrimaryNavigator( loginViewModel: LoginViewModel, - libraryViewModel: LibraryViewModel, searchViewModel: SearchViewModel, settingsViewModel: SettingsViewModel, labelsViewModel: LabelsViewModel, @@ -73,7 +69,6 @@ fun PrimaryNavigator( NavHost(navController = navController, startDestination = Routes.Library.route) { composable(Routes.Library.route) { LibraryView( - libraryViewModel = libraryViewModel, navController = navController, labelsViewModel = labelsViewModel, saveViewModel = saveViewModel, 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 diff --git a/android/Omnivore/gradle/libs.versions.toml b/android/Omnivore/gradle/libs.versions.toml index 6ebf7b841..b67815b52 100644 --- a/android/Omnivore/gradle/libs.versions.toml +++ b/android/Omnivore/gradle/libs.versions.toml @@ -9,6 +9,7 @@ androidxComposeCompiler = "1.5.9" androidxCore = "1.12.0" androidxDataStore = "1.0.0" androidxEspresso = "3.5.1" +androidxHiltNavigationCompose = "1.1.0" androidxLifecycle = "2.7.0" androidxNavigation = "2.7.7" androidxSecurity = "1.0.0" @@ -48,9 +49,11 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } androidx-dataStore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDataStore" } +androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } androidx-lifecycle-viewModelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidxLifecycle" } +androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-viewmodelSavedstate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "androidxLifecycle" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "androidxSecurity" }