Merge pull request #3549 from stefanosansone/feature/android-mark-ad-read
Android - Mark items as read/unread from library screen
This commit is contained in:
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<SavedItem>(capacity = Channel.UNLIMITED)
|
||||
val highlightSyncChannel = Channel<Highlight>(capacity = Channel.UNLIMITED)
|
||||
val savedItemSyncChannel = Channel<SavedItem>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package app.omnivore.omnivore.core.data.model
|
||||
|
||||
data class LibraryQuery(
|
||||
val allowedArchiveStates: List<Int>,
|
||||
val sortKey: String,
|
||||
val requiredLabels: List<String>,
|
||||
val excludedLabels: List<String>,
|
||||
val allowedContentReaders: List<String>
|
||||
)
|
||||
@ -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),
|
||||
@ -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<List<SavedItemWithLabelsAndHighlights>>
|
||||
|
||||
suspend fun updateReadingProgress(
|
||||
itemId: String,
|
||||
readingProgressPercentage: Double,
|
||||
readingProgressAnchorIndex: Int
|
||||
)
|
||||
}
|
||||
@ -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<List<SavedItemWithLabelsAndHighlights>> =
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<T> {
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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<List<SavedItem>>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
|
||||
suspend fun findById(itemID: String): SavedItem?
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0")
|
||||
fun getUnSynced(): List<SavedItem>
|
||||
|
||||
@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<String>)
|
||||
|
||||
@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<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.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<Int>,
|
||||
sortKey: String,
|
||||
hasRequiredLabels: Int,
|
||||
hasExcludedLabels: Int,
|
||||
requiredLabels: List<String>,
|
||||
excludedLabels: List<String>,
|
||||
allowedContentReaders: List<String>
|
||||
): Flow<List<SavedItemWithLabelsAndHighlights>>
|
||||
}
|
||||
@ -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<Highlight>
|
||||
)
|
||||
) {
|
||||
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 {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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<SavedItem>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
|
||||
fun findById(itemID: String): SavedItem?
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0")
|
||||
fun getUnSynced(): List<SavedItem>
|
||||
|
||||
@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<String>)
|
||||
|
||||
@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<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.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<Int>,
|
||||
sortKey: String,
|
||||
hasRequiredLabels: Int,
|
||||
hasExcludedLabels: Int,
|
||||
requiredLabels: List<String>,
|
||||
excludedLabels: List<String>,
|
||||
allowedContentReaders: List<String>
|
||||
): LiveData<List<SavedItemWithLabelsAndHighlights>>
|
||||
|
||||
fun filteredLibraryData(
|
||||
allowedArchiveStates: List<Int>,
|
||||
sortKey: String,
|
||||
requiredLabels: List<String>,
|
||||
excludedLabels: List<String>,
|
||||
allowedContentReaders: List<String>
|
||||
): LiveData<List<SavedItemWithLabelsAndHighlights>> {
|
||||
return _filteredLibraryData(
|
||||
allowedArchiveStates = allowedArchiveStates,
|
||||
sortKey = sortKey,
|
||||
hasRequiredLabels = requiredLabels.size,
|
||||
hasExcludedLabels = excludedLabels.size,
|
||||
requiredLabels = requiredLabels,
|
||||
excludedLabels = excludedLabels,
|
||||
allowedContentReaders = allowedContentReaders
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object SavedItemQueryConstants {
|
||||
|
||||
@ -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<SavedItemLabel>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemLabel>)
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
|
||||
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
|
||||
@Transaction
|
||||
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
|
||||
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
|
||||
|
||||
@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<String>): List<SavedItemLabel>
|
||||
@Transaction
|
||||
@Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC")
|
||||
fun namedLabels(names: List<String>): List<SavedItemLabel>
|
||||
}
|
||||
|
||||
@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<SavedItemAndSavedItemLabelCrossRef>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||
|
||||
@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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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<SavedItemLabel> 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<SavedItemLabel> 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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<SavedItemWithLabelsAndHighlights> 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 {
|
||||
|
||||
@ -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<String>(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<String?>(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<List<SavedItemWithLabelsAndHighlights>>()
|
||||
|
||||
// Correct way - but not working
|
||||
/* val uiState: StateFlow<LibraryUiState> = _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>(LibraryUiState.Loading)
|
||||
val uiState: StateFlow<LibraryUiState> = _uiState
|
||||
|
||||
init {
|
||||
loadSavedItems()
|
||||
}
|
||||
|
||||
private fun loadSavedItems() {
|
||||
viewModelScope.launch {
|
||||
libraryRepository.getSavedItems(_libraryQuery.value)
|
||||
.collect { favoriteNews ->
|
||||
_uiState.value = LibraryUiState.Success(favoriteNews)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val itemsLiveData = MediatorLiveData<List<SavedItemWithLabelsAndHighlights>>()
|
||||
val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX)
|
||||
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
|
||||
val bottomSheetState = MutableLiveData(LibraryBottomSheetState.HIDDEN)
|
||||
val currentItemLiveData = MutableLiveData<String?>(null)
|
||||
val currentItem = mutableStateOf<String?>(null)
|
||||
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
|
||||
val activeLabelsLiveData = MutableLiveData<List<SavedItemLabel>>(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<SavedItemWithLabelsAndHighlights>,
|
||||
) : LibraryUiState
|
||||
|
||||
data object Error : LibraryUiState
|
||||
}
|
||||
|
||||
enum class SavedItemAction {
|
||||
Delete, Archive, Unarchive, EditLabels, EditInfo, MarkRead, MarkUnread
|
||||
}
|
||||
|
||||
@ -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<String> = 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user