move network calls in data layer
This commit is contained in:
@ -1,160 +1,151 @@
|
||||
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.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
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.data.model.ServerSyncStatus
|
||||
|
||||
suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult {
|
||||
val searchResult = networker.search(cursor = cursor, limit = 10, query = query)
|
||||
val searchResult = networker.search(cursor = cursor, limit = 10, query = query)
|
||||
|
||||
val savedItems = searchResult.items.map {
|
||||
SavedItemWithLabelsAndHighlights(
|
||||
savedItem = it.item,
|
||||
labels = it.labels,
|
||||
highlights = it.highlights,
|
||||
val savedItems = searchResult.items.map {
|
||||
SavedItemWithLabelsAndHighlights(
|
||||
savedItem = it.item,
|
||||
labels = it.labels,
|
||||
highlights = it.highlights,
|
||||
)
|
||||
}
|
||||
|
||||
db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems)
|
||||
|
||||
Log.d(
|
||||
"sync",
|
||||
"found ${searchResult.items.size} items with search api. Query: $query cursor: $cursor"
|
||||
)
|
||||
}
|
||||
|
||||
db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems)
|
||||
|
||||
Log.d("sync", "found ${searchResult.items.size} items with search api. Query: $query cursor: $cursor")
|
||||
|
||||
return SearchResult(
|
||||
hasError = false,
|
||||
hasMoreItems = false,
|
||||
cursor = searchResult.cursor,
|
||||
count = searchResult.items.size,
|
||||
savedItems = savedItems
|
||||
)
|
||||
return SearchResult(
|
||||
hasError = false,
|
||||
hasMoreItems = false,
|
||||
cursor = searchResult.cursor,
|
||||
count = searchResult.items.size,
|
||||
savedItems = savedItems
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult {
|
||||
val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since)
|
||||
?: return SavedItemSyncResult.errorResult
|
||||
val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since)
|
||||
?: return SavedItemSyncResult.errorResult
|
||||
|
||||
if (syncResult.deletedItemIDs.isNotEmpty()) {
|
||||
db.savedItemDao().deleteByIds(syncResult.deletedItemIDs)
|
||||
}
|
||||
if (syncResult.deletedItemIDs.isNotEmpty()) {
|
||||
db.savedItemDao().deleteByIds(syncResult.deletedItemIDs)
|
||||
}
|
||||
|
||||
val savedItems = syncResult.items.map {
|
||||
val savedItem = SavedItem(
|
||||
savedItemId = it.id,
|
||||
title = it.title,
|
||||
createdAt = it.createdAt as String,
|
||||
savedAt = it.savedAt as String,
|
||||
readAt = it.readAt as String?,
|
||||
updatedAt = it.updatedAt as String?,
|
||||
readingProgress = it.readingProgressPercent,
|
||||
readingProgressAnchor = it.readingProgressAnchorIndex,
|
||||
imageURLString = it.image,
|
||||
pageURLString = it.url,
|
||||
descriptionText = it.description,
|
||||
publisherURLString = it.originalArticleUrl,
|
||||
siteName = it.siteName,
|
||||
author = it.author,
|
||||
publishDate = it.publishedAt as String?,
|
||||
slug = it.slug,
|
||||
isArchived = it.isArchived,
|
||||
contentReader = it.contentReader.rawValue,
|
||||
content = null,
|
||||
wordsCount = it.wordsCount
|
||||
)
|
||||
val labels = it.labels?.map { label ->
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = label.labelFields.id,
|
||||
name = label.labelFields.name,
|
||||
color = label.labelFields.color,
|
||||
createdAt = null,
|
||||
labelDescription = null
|
||||
)
|
||||
} ?: listOf()
|
||||
val highlights = it.highlights?.map { highlight ->
|
||||
Highlight(
|
||||
type = highlight.highlightFields.type.toString(),
|
||||
highlightId = highlight.highlightFields.id,
|
||||
annotation = highlight.highlightFields.annotation,
|
||||
createdByMe = highlight.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
patch = highlight.highlightFields.patch,
|
||||
prefix = highlight.highlightFields.prefix,
|
||||
quote = highlight.highlightFields.quote,
|
||||
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
shortId = highlight.highlightFields.shortId,
|
||||
suffix = highlight.highlightFields.suffix,
|
||||
createdAt = null,
|
||||
updatedAt = highlight.highlightFields.updatedAt as String?,
|
||||
color = highlight.highlightFields.color,
|
||||
highlightPositionPercent = highlight.highlightFields.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = highlight.highlightFields.highlightPositionAnchorIndex,
|
||||
)
|
||||
} ?: listOf()
|
||||
SavedItemWithLabelsAndHighlights(
|
||||
savedItem = savedItem,
|
||||
labels = labels,
|
||||
highlights = highlights
|
||||
)
|
||||
}
|
||||
val savedItems = syncResult.items.map {
|
||||
val savedItem = SavedItem(
|
||||
savedItemId = it.id,
|
||||
title = it.title,
|
||||
createdAt = it.createdAt as String,
|
||||
savedAt = it.savedAt as String,
|
||||
readAt = it.readAt as String?,
|
||||
updatedAt = it.updatedAt as String?,
|
||||
readingProgress = it.readingProgressPercent,
|
||||
readingProgressAnchor = it.readingProgressAnchorIndex,
|
||||
imageURLString = it.image,
|
||||
pageURLString = it.url,
|
||||
descriptionText = it.description,
|
||||
publisherURLString = it.originalArticleUrl,
|
||||
siteName = it.siteName,
|
||||
author = it.author,
|
||||
publishDate = it.publishedAt as String?,
|
||||
slug = it.slug,
|
||||
isArchived = it.isArchived,
|
||||
contentReader = it.contentReader.rawValue,
|
||||
content = null,
|
||||
wordsCount = it.wordsCount
|
||||
)
|
||||
val labels = it.labels?.map { label ->
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = label.labelFields.id,
|
||||
name = label.labelFields.name,
|
||||
color = label.labelFields.color,
|
||||
createdAt = null,
|
||||
labelDescription = null
|
||||
)
|
||||
} ?: listOf()
|
||||
val highlights = it.highlights?.map { highlight ->
|
||||
Highlight(
|
||||
type = highlight.highlightFields.type.toString(),
|
||||
highlightId = highlight.highlightFields.id,
|
||||
annotation = highlight.highlightFields.annotation,
|
||||
createdByMe = highlight.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
patch = highlight.highlightFields.patch,
|
||||
prefix = highlight.highlightFields.prefix,
|
||||
quote = highlight.highlightFields.quote,
|
||||
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
shortId = highlight.highlightFields.shortId,
|
||||
suffix = highlight.highlightFields.suffix,
|
||||
createdAt = null,
|
||||
updatedAt = highlight.highlightFields.updatedAt as String?,
|
||||
color = highlight.highlightFields.color,
|
||||
highlightPositionPercent = highlight.highlightFields.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = highlight.highlightFields.highlightPositionAnchorIndex,
|
||||
)
|
||||
} ?: listOf()
|
||||
SavedItemWithLabelsAndHighlights(
|
||||
savedItem = savedItem, labels = labels, highlights = highlights
|
||||
)
|
||||
}
|
||||
|
||||
db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems)
|
||||
db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems)
|
||||
|
||||
Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since")
|
||||
Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since")
|
||||
|
||||
return SavedItemSyncResult(
|
||||
hasError = false,
|
||||
hasMoreItems = syncResult.hasMoreItems,
|
||||
cursor = syncResult.cursor,
|
||||
count = syncResult.items.size,
|
||||
savedItemSlugs = syncResult.items.map { it.slug }
|
||||
)
|
||||
return SavedItemSyncResult(hasError = false,
|
||||
hasMoreItems = syncResult.hasMoreItems,
|
||||
cursor = syncResult.cursor,
|
||||
count = syncResult.items.size,
|
||||
savedItemSlugs = syncResult.items.map { it.slug })
|
||||
}
|
||||
|
||||
fun DataService.isSavedItemContentStoredInDB(slug: String): Boolean {
|
||||
val existingItem = db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
|
||||
val content = existingItem?.savedItem?.content ?: ""
|
||||
return content.length > 10
|
||||
suspend fun DataService.isSavedItemContentStoredInDB(slug: String): Boolean {
|
||||
val existingItem = db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
|
||||
val content = existingItem?.savedItem?.content ?: ""
|
||||
return content.length > 10
|
||||
}
|
||||
|
||||
suspend fun DataService.fetchSavedItemContent(slug: String) {
|
||||
val syncResult = networker.savedItem(slug)
|
||||
|
||||
val savedItem = syncResult.item
|
||||
savedItem?.let {
|
||||
val item = SavedItemWithLabelsAndHighlights(
|
||||
savedItem = savedItem,
|
||||
labels = syncResult.labels,
|
||||
highlights = syncResult.highlights
|
||||
)
|
||||
db.savedItemWithLabelsAndHighlightsDao().insertAll(listOf(item))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class SavedItemSyncResult(
|
||||
val hasError: Boolean,
|
||||
val hasMoreItems: Boolean,
|
||||
val count: Int,
|
||||
val savedItemSlugs: List<String>,
|
||||
val cursor: String?
|
||||
val hasError: Boolean,
|
||||
val hasMoreItems: Boolean,
|
||||
val count: Int,
|
||||
val savedItemSlugs: List<String>,
|
||||
val cursor: String?
|
||||
) {
|
||||
companion object {
|
||||
val errorResult = SavedItemSyncResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItemSlugs = listOf())
|
||||
}
|
||||
companion object {
|
||||
val errorResult = SavedItemSyncResult(
|
||||
hasError = true,
|
||||
hasMoreItems = true,
|
||||
cursor = null,
|
||||
count = 0,
|
||||
savedItemSlugs = listOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class SearchResult(
|
||||
val hasError: Boolean,
|
||||
val hasMoreItems: Boolean,
|
||||
val count: Int,
|
||||
val savedItems: List<SavedItemWithLabelsAndHighlights>,
|
||||
val cursor: String?
|
||||
val hasError: Boolean,
|
||||
val hasMoreItems: Boolean,
|
||||
val count: Int,
|
||||
val savedItems: List<SavedItemWithLabelsAndHighlights>,
|
||||
val cursor: String?
|
||||
) {
|
||||
companion object {
|
||||
val errorResult = SearchResult(hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItems = listOf())
|
||||
}
|
||||
companion object {
|
||||
val errorResult = SearchResult(
|
||||
hasError = true, hasMoreItems = true, cursor = null, count = 0, savedItems = listOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,198 +21,204 @@ import com.apollographql.apollo3.api.Optional
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
suspend fun DataService.startSyncChannels() {
|
||||
Log.d("sync", "Starting sync channels")
|
||||
for (savedItem in savedItemSyncChannel) {
|
||||
syncSavedItem(savedItem)
|
||||
}
|
||||
Log.d("sync", "Starting sync channels")
|
||||
for (savedItem in savedItemSyncChannel) {
|
||||
syncSavedItem(savedItem)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.performHighlightChange(highlightChange: HighlightChange) {
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
if (syncHighlightChange(highlightChange)) {
|
||||
db.highlightChangesDao().deleteById(highlight.highlightId)
|
||||
}
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
if (syncHighlightChange(highlightChange)) {
|
||||
db.highlightChangesDao().deleteById(highlight.highlightId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun DataService.syncOfflineItemsWithServerIfNeeded() {
|
||||
val unSyncedSavedItems = db.savedItemDao().getUnSynced()
|
||||
val unSyncedHighlights = db.highlightChangesDao().getUnSynced()
|
||||
val unSyncedSavedItems = db.savedItemDao().getUnSynced()
|
||||
val unSyncedHighlights = db.highlightChangesDao().getUnSynced()
|
||||
|
||||
for (savedItem in unSyncedSavedItems) {
|
||||
delay(250)
|
||||
savedItemSyncChannel.send(savedItem)
|
||||
}
|
||||
for (savedItem in unSyncedSavedItems) {
|
||||
delay(250)
|
||||
savedItemSyncChannel.send(savedItem)
|
||||
}
|
||||
|
||||
for (change in unSyncedHighlights) {
|
||||
performHighlightChange(change)
|
||||
}
|
||||
for (change in unSyncedHighlights) {
|
||||
performHighlightChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncSavedItem(item: SavedItem) {
|
||||
suspend fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
item.serverSyncStatus = status.rawValue
|
||||
db.savedItemDao().update(item)
|
||||
}
|
||||
|
||||
when (item.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isDeletedOnServer = networker.deleteSavedItem(item.savedItemId)
|
||||
|
||||
if (isDeletedOnServer) {
|
||||
db.savedItemDao().deleteById(item.savedItemId)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
suspend fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
item.serverSyncStatus = status.rawValue
|
||||
db.savedItemDao().update(item)
|
||||
}
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isArchiveServerSynced = networker.updateArchiveStatusSavedItem(itemID = item.savedItemId, setAsArchived = item.isArchived)
|
||||
when (item.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isReadingProgressSynced = networker.updateReadingProgress(
|
||||
ReadingProgressParams(
|
||||
id = item.savedItemId,
|
||||
force = item.contentReader == "PDF",
|
||||
readingProgressPercent = item.readingProgress,
|
||||
readingProgressAnchorIndex = item.readingProgressAnchor
|
||||
)
|
||||
)
|
||||
val isDeletedOnServer = networker.deleteSavedItem(item.savedItemId)
|
||||
|
||||
if (isArchiveServerSynced && isReadingProgressSynced) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
if (isDeletedOnServer) {
|
||||
db.savedItemDao().deleteById(item.savedItemId)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isArchiveServerSynced = networker.updateArchiveStatusSavedItem(
|
||||
itemID = item.savedItemId, setAsArchived = item.isArchived
|
||||
)
|
||||
|
||||
val isReadingProgressSynced = networker.updateReadingProgress(
|
||||
ReadingProgressParams(
|
||||
id = item.savedItemId,
|
||||
force = item.contentReader == "PDF",
|
||||
readingProgressPercent = item.readingProgress,
|
||||
readingProgressAnchorIndex = item.readingProgressAnchor
|
||||
)
|
||||
)
|
||||
|
||||
if (isArchiveServerSynced && isReadingProgressSynced) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
// TODO: implement when we are able to create content on device
|
||||
// updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
// send update to server
|
||||
// update db
|
||||
}
|
||||
|
||||
else -> return
|
||||
}
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
// TODO: implement when we are able to create content on device
|
||||
// updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
// send update to server
|
||||
// update db
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncHighlightChange(highlightChange: HighlightChange): Boolean {
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
|
||||
fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
highlight.serverSyncStatus = status.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
|
||||
when (highlight.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId))
|
||||
|
||||
if (isDeletedOnServer) {
|
||||
db.highlightDao().deleteById(highlight.highlightId)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
return isDeletedOnServer != null
|
||||
fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
highlight.serverSyncStatus = status.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
when (highlight.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId))
|
||||
|
||||
val isUpdatedOnServer = networker.updateHighlight(
|
||||
UpdateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
highlightId = highlight.highlightId,
|
||||
sharedAt = Optional.absent()
|
||||
)
|
||||
)
|
||||
if (isDeletedOnServer) {
|
||||
db.highlightDao().deleteById(highlight.highlightId)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
return isDeletedOnServer != null
|
||||
}
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
return isUpdatedOnServer != null
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isUpdatedOnServer = networker.updateHighlight(
|
||||
UpdateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
highlightId = highlight.highlightId,
|
||||
sharedAt = Optional.absent()
|
||||
)
|
||||
)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
return isUpdatedOnServer != null
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val input = CreateHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
type = Optional.presentIfNotNull(HighlightType.safeValueOf(highlight.type)),
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
patch = Optional.presentIfNotNull(highlight.patch),
|
||||
quote = Optional.presentIfNotNull(highlight.quote),
|
||||
)
|
||||
Log.d("sync", "Creating highlight from input: ${input}")
|
||||
val createResult = networker.createHighlight(
|
||||
input
|
||||
)
|
||||
return if (createResult.newHighlight != null || createResult.alreadyExists) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
true
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_MERGE.rawValue -> {
|
||||
Log.d("sync", "NEEDS MERGE: ${highlightChange}")
|
||||
|
||||
val mergeHighlightInput = MergeHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
color = Optional.presentIfNotNull(highlight.color),
|
||||
highlightPositionAnchorIndex = Optional.presentIfNotNull(highlight.highlightPositionAnchorIndex),
|
||||
highlightPositionPercent = Optional.presentIfNotNull(highlight.highlightPositionPercent),
|
||||
html = Optional.presentIfNotNull(highlightChange.html),
|
||||
overlapHighlightIdList = highlightChange.overlappingIDs ?: emptyList(),
|
||||
patch = highlight.patch ?: "",
|
||||
prefix = Optional.presentIfNotNull(highlight.prefix),
|
||||
quote = highlight.quote ?: "",
|
||||
suffix = Optional.presentIfNotNull(highlight.suffix)
|
||||
)
|
||||
|
||||
val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput)
|
||||
if (!isUpdatedOnServer) {
|
||||
Log.d("sync", "FAILED TO MERGE HIGHLIGHT")
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
||||
return false
|
||||
}
|
||||
|
||||
for (highlightID in mergeHighlightInput.overlapHighlightIdList) {
|
||||
Log.d("sync", "DELETING MERGED HIGHLIGHT: ${highlightID}")
|
||||
val deleteChange = HighlightChange(
|
||||
highlightId = highlightID,
|
||||
savedItemId = highlightChange.savedItemId,
|
||||
type = "",
|
||||
shortId = "",
|
||||
annotation = null,
|
||||
createdAt = null,
|
||||
patch = null,
|
||||
prefix = null,
|
||||
quote = null,
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue,
|
||||
html = null,
|
||||
suffix = null,
|
||||
updatedAt = null,
|
||||
color = null,
|
||||
highlightPositionPercent = null,
|
||||
highlightPositionAnchorIndex = null,
|
||||
overlappingIDs = null
|
||||
)
|
||||
performHighlightChange(deleteChange)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val input = CreateHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
type = Optional.presentIfNotNull(HighlightType.safeValueOf(highlight.type)),
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
patch = Optional.presentIfNotNull(highlight.patch),
|
||||
quote = Optional.presentIfNotNull(highlight.quote),
|
||||
)
|
||||
Log.d("sync", "Creating highlight from input: ${input}")
|
||||
val createResult = networker.createHighlight(
|
||||
input
|
||||
)
|
||||
if (createResult.newHighlight != null || createResult.alreadyExists) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
return true
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_MERGE.rawValue -> {
|
||||
Log.d("sync", "NEEDS MERGE: ${highlightChange}")
|
||||
|
||||
val mergeHighlightInput = MergeHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
color = Optional.presentIfNotNull(highlight.color),
|
||||
highlightPositionAnchorIndex = Optional.presentIfNotNull(highlight.highlightPositionAnchorIndex),
|
||||
highlightPositionPercent = Optional.presentIfNotNull(highlight.highlightPositionPercent),
|
||||
html = Optional.presentIfNotNull(highlightChange.html),
|
||||
overlapHighlightIdList = highlightChange.overlappingIDs ?: emptyList(),
|
||||
patch = highlight.patch ?: "",
|
||||
prefix = Optional.presentIfNotNull(highlight.prefix),
|
||||
quote = highlight.quote ?: "",
|
||||
suffix = Optional.presentIfNotNull(highlight.suffix)
|
||||
)
|
||||
|
||||
val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput)
|
||||
if (!isUpdatedOnServer) {
|
||||
Log.d("sync", "FAILED TO MERGE HIGHLIGHT")
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
||||
return false
|
||||
}
|
||||
|
||||
for (highlightID in mergeHighlightInput.overlapHighlightIdList) {
|
||||
Log.d("sync", "DELETING MERGED HIGHLIGHT: ${highlightID}")
|
||||
val deleteChange = HighlightChange(
|
||||
highlightId = highlightID,
|
||||
savedItemId = highlightChange.savedItemId,
|
||||
type = "",
|
||||
shortId = "",
|
||||
annotation = null,
|
||||
createdAt = null,
|
||||
patch = null,
|
||||
prefix = null,
|
||||
quote = null,
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue,
|
||||
html = null,
|
||||
suffix = null,
|
||||
updatedAt = null,
|
||||
color = null,
|
||||
highlightPositionPercent = null,
|
||||
highlightPositionAnchorIndex = null,
|
||||
overlappingIDs = null
|
||||
)
|
||||
performHighlightChange(deleteChange)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
package app.omnivore.omnivore.core.data.repository
|
||||
|
||||
import app.omnivore.omnivore.core.data.SavedItemSyncResult
|
||||
import app.omnivore.omnivore.core.data.SearchResult
|
||||
import app.omnivore.omnivore.core.data.model.LibraryQuery
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@ -8,9 +12,37 @@ interface LibraryRepository {
|
||||
|
||||
fun getSavedItems(query: LibraryQuery): Flow<List<SavedItemWithLabelsAndHighlights>>
|
||||
|
||||
fun getSavedItemsLabels(): Flow<List<SavedItemLabel>>
|
||||
|
||||
suspend fun getLabels(): List<SavedItemLabel>
|
||||
|
||||
suspend fun fetchSavedItemContent(slug: String)
|
||||
|
||||
suspend fun insertAllLabels(labels: List<SavedItemLabel>)
|
||||
|
||||
suspend fun setSavedItemLabels(itemId: String, labels: List<SavedItemLabel>): Boolean
|
||||
|
||||
suspend fun updateReadingProgress(
|
||||
itemId: String,
|
||||
readingProgressPercentage: Double,
|
||||
readingProgressAnchorIndex: Int
|
||||
)
|
||||
|
||||
suspend fun createNewSavedItemLabel(labelName: String, hexColorValue: String)
|
||||
|
||||
suspend fun librarySearch(cursor: String?, query: String): SearchResult
|
||||
|
||||
suspend fun isSavedItemContentStoredInDB(slug: String): Boolean
|
||||
|
||||
suspend fun deleteSavedItem(itemID: String)
|
||||
|
||||
suspend fun archiveSavedItem(itemID: String)
|
||||
|
||||
suspend fun unarchiveSavedItem(itemID: String)
|
||||
|
||||
suspend fun syncOfflineItemsWithServerIfNeeded()
|
||||
|
||||
suspend fun syncHighlightChange(highlightChange: HighlightChange): Boolean
|
||||
|
||||
suspend fun sync(since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult
|
||||
}
|
||||
|
||||
@ -1,20 +1,62 @@
|
||||
package app.omnivore.omnivore.core.data.repository.impl
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.data.SavedItemSyncResult
|
||||
import app.omnivore.omnivore.core.data.SearchResult
|
||||
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.HighlightChangesDao
|
||||
import app.omnivore.omnivore.core.database.dao.HighlightDao
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemAndSavedItemLabelCrossRefDao
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemLabelDao
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemWithLabelsAndHighlightsDao
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||
import app.omnivore.omnivore.core.network.archiveSavedItem
|
||||
import app.omnivore.omnivore.core.network.createHighlight
|
||||
import app.omnivore.omnivore.core.network.createNewLabel
|
||||
import app.omnivore.omnivore.core.network.deleteHighlights
|
||||
import app.omnivore.omnivore.core.network.deleteSavedItem
|
||||
import app.omnivore.omnivore.core.network.mergeHighlights
|
||||
import app.omnivore.omnivore.core.network.savedItem
|
||||
import app.omnivore.omnivore.core.network.savedItemLabels
|
||||
import app.omnivore.omnivore.core.network.savedItemUpdates
|
||||
import app.omnivore.omnivore.core.network.search
|
||||
import app.omnivore.omnivore.core.network.unarchiveSavedItem
|
||||
import app.omnivore.omnivore.core.network.updateHighlight
|
||||
import app.omnivore.omnivore.core.network.updateLabelsForSavedItem
|
||||
import app.omnivore.omnivore.core.network.updateReadingProgress
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.SetLabelsInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class LibraryRepositoryImpl @Inject constructor(
|
||||
private val savedItemDao: SavedItemDao,
|
||||
private val networker: Networker
|
||||
private val savedItemLabelDao: SavedItemLabelDao,
|
||||
private val savedItemWithLabelsAndHighlightsDao: SavedItemWithLabelsAndHighlightsDao,
|
||||
private val savedItemAndSavedItemLabelCrossRefDao: SavedItemAndSavedItemLabelCrossRefDao,
|
||||
private val highlightDao: HighlightDao,
|
||||
private val highlightChangesDao: HighlightChangesDao,
|
||||
private val networker: Networker,
|
||||
private val dataService: DataService
|
||||
): LibraryRepository {
|
||||
|
||||
override fun getSavedItems(query: LibraryQuery): Flow<List<SavedItemWithLabelsAndHighlights>> =
|
||||
@ -28,6 +70,26 @@ class LibraryRepositoryImpl @Inject constructor(
|
||||
query.allowedContentReaders
|
||||
)
|
||||
|
||||
override fun getSavedItemsLabels(): Flow<List<SavedItemLabel>> = savedItemLabelDao.getSavedItemLabels()
|
||||
|
||||
override suspend fun getLabels(): List<SavedItemLabel> = networker.savedItemLabels()
|
||||
|
||||
override suspend fun insertAllLabels(labels: List<SavedItemLabel>) {
|
||||
savedItemLabelDao.insertAll(labels)
|
||||
}
|
||||
|
||||
override suspend fun fetchSavedItemContent(slug: String) {
|
||||
val syncResult = networker.savedItem(slug)
|
||||
|
||||
val savedItem = syncResult.item
|
||||
savedItem?.let {
|
||||
val item = SavedItemWithLabelsAndHighlights(
|
||||
savedItem = savedItem, labels = syncResult.labels, highlights = syncResult.highlights
|
||||
)
|
||||
savedItemWithLabelsAndHighlightsDao.insertAll(listOf(item))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateReadingProgress(
|
||||
itemId: String,
|
||||
readingProgressPercentage: Double,
|
||||
@ -63,4 +125,360 @@ class LibraryRepositoryImpl @Inject constructor(
|
||||
updatedItem?.let { savedItemDao.update(updatedItem) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setSavedItemLabels(
|
||||
itemId: String,
|
||||
labels: List<SavedItemLabel>
|
||||
): Boolean {
|
||||
val input = SetLabelsInput(
|
||||
pageId = itemId,
|
||||
labels = Optional.presentIfNotNull(labels.map { CreateLabelInput(color = Optional.presentIfNotNull(it.color), name = it.name) }),
|
||||
)
|
||||
|
||||
val updatedLabels = networker.updateLabelsForSavedItem(input)
|
||||
|
||||
// Figure out which of the labels are new
|
||||
updatedLabels?.let { updatedLabels ->
|
||||
val existingNamedLabels = savedItemLabelDao.namedLabels(updatedLabels.map { it.labelFields.name })
|
||||
val existingNames = existingNamedLabels.map { it.name }
|
||||
val newNamedLabels = updatedLabels.filter { !existingNames.contains(it.labelFields.name) }
|
||||
|
||||
savedItemLabelDao.insertAll(newNamedLabels.map {
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = it.labelFields.id,
|
||||
name = it.labelFields.name,
|
||||
color = it.labelFields.color,
|
||||
createdAt = null,
|
||||
labelDescription = null
|
||||
)
|
||||
})
|
||||
|
||||
val allNamedLabels = savedItemLabelDao.namedLabels(updatedLabels.map { it.labelFields.name })
|
||||
val crossRefs = allNamedLabels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(
|
||||
savedItemLabelId = it.savedItemLabelId,
|
||||
savedItemId = itemId
|
||||
)
|
||||
}
|
||||
savedItemAndSavedItemLabelCrossRefDao.deleteRefsBySavedItemId(itemId)
|
||||
savedItemAndSavedItemLabelCrossRefDao.insertAll(crossRefs)
|
||||
|
||||
return true
|
||||
} ?: run {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createNewSavedItemLabel(labelName: String, hexColorValue: String) {
|
||||
val newLabel = networker.createNewLabel(
|
||||
CreateLabelInput(
|
||||
color = Optional.presentIfNotNull(hexColorValue), name = labelName
|
||||
)
|
||||
)
|
||||
|
||||
newLabel?.let {
|
||||
val savedItemLabel = SavedItemLabel(
|
||||
savedItemLabelId = it.id,
|
||||
name = it.name,
|
||||
color = it.color,
|
||||
createdAt = it.createdAt as String?,
|
||||
labelDescription = it.description
|
||||
)
|
||||
|
||||
savedItemLabelDao.insertAll(listOf(savedItemLabel))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun librarySearch(cursor: String?, query: String): SearchResult {
|
||||
val searchResult = networker.search(cursor = cursor, limit = 10, query = query)
|
||||
|
||||
val savedItems = searchResult.items.map {
|
||||
SavedItemWithLabelsAndHighlights(
|
||||
savedItem = it.item,
|
||||
labels = it.labels,
|
||||
highlights = it.highlights,
|
||||
)
|
||||
}
|
||||
|
||||
savedItemWithLabelsAndHighlightsDao.insertAll(savedItems)
|
||||
|
||||
Log.d(
|
||||
"sync",
|
||||
"found ${searchResult.items.size} items with search api. Query: $query cursor: $cursor"
|
||||
)
|
||||
|
||||
return SearchResult(
|
||||
hasError = false,
|
||||
hasMoreItems = false,
|
||||
cursor = searchResult.cursor,
|
||||
count = searchResult.items.size,
|
||||
savedItems = savedItems
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun isSavedItemContentStoredInDB(slug: String): Boolean {
|
||||
val existingItem = savedItemDao.getSavedItemWithLabelsAndHighlights(slug)
|
||||
val content = existingItem?.savedItem?.content ?: ""
|
||||
return content.length > 10
|
||||
}
|
||||
|
||||
override suspend fun deleteSavedItem(itemID: String) {
|
||||
val savedItem = savedItemDao.findById(itemID = itemID) ?: return
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||
savedItemDao.update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.deleteSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItemDao.deleteById(itemID)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun archiveSavedItem(itemID: String) {
|
||||
val savedItem = savedItemDao.findById(itemID = itemID) ?: return
|
||||
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
savedItem.isArchived = true
|
||||
savedItemDao.update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.archiveSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
savedItemDao.update(savedItem)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unarchiveSavedItem(itemID: String) {
|
||||
val savedItem = savedItemDao.findById(itemID = itemID) ?: return
|
||||
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
savedItem.isArchived = false
|
||||
savedItemDao.update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.unarchiveSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
savedItemDao.update(savedItem)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun syncOfflineItemsWithServerIfNeeded() {
|
||||
val unSyncedSavedItems = savedItemDao.getUnSynced()
|
||||
val unSyncedHighlights = highlightChangesDao.getUnSynced()
|
||||
|
||||
for (savedItem in unSyncedSavedItems) {
|
||||
delay(250)
|
||||
dataService.savedItemSyncChannel.send(savedItem)
|
||||
}
|
||||
|
||||
for (change in unSyncedHighlights) {
|
||||
performHighlightChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun syncHighlightChange(highlightChange: HighlightChange): Boolean {
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
|
||||
fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
highlight.serverSyncStatus = status.rawValue
|
||||
highlightDao.update(highlight)
|
||||
}
|
||||
|
||||
when (highlight.serverSyncStatus) {
|
||||
ServerSyncStatus.NEEDS_DELETION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
val isDeletedOnServer = networker.deleteHighlights(listOf(highlight.highlightId))
|
||||
|
||||
if (isDeletedOnServer) {
|
||||
highlightDao.deleteById(highlight.highlightId)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_DELETION)
|
||||
}
|
||||
return isDeletedOnServer != null
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_UPDATE.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val isUpdatedOnServer = networker.updateHighlight(
|
||||
UpdateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
highlightId = highlight.highlightId,
|
||||
sharedAt = Optional.absent()
|
||||
)
|
||||
)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
return isUpdatedOnServer != null
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val input = CreateHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
type = Optional.presentIfNotNull(HighlightType.safeValueOf(highlight.type)),
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
patch = Optional.presentIfNotNull(highlight.patch),
|
||||
quote = Optional.presentIfNotNull(highlight.quote),
|
||||
)
|
||||
Log.d("sync", "Creating highlight from input: ${input}")
|
||||
val createResult = networker.createHighlight(
|
||||
input
|
||||
)
|
||||
if (createResult.newHighlight != null || createResult.alreadyExists) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
return true
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
ServerSyncStatus.NEEDS_MERGE.rawValue -> {
|
||||
Log.d("sync", "NEEDS MERGE: ${highlightChange}")
|
||||
|
||||
val mergeHighlightInput = MergeHighlightInput(
|
||||
id = highlight.highlightId,
|
||||
shortId = highlight.shortId,
|
||||
articleId = highlightChange.savedItemId,
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
color = Optional.presentIfNotNull(highlight.color),
|
||||
highlightPositionAnchorIndex = Optional.presentIfNotNull(highlight.highlightPositionAnchorIndex),
|
||||
highlightPositionPercent = Optional.presentIfNotNull(highlight.highlightPositionPercent),
|
||||
html = Optional.presentIfNotNull(highlightChange.html),
|
||||
overlapHighlightIdList = highlightChange.overlappingIDs ?: emptyList(),
|
||||
patch = highlight.patch ?: "",
|
||||
prefix = Optional.presentIfNotNull(highlight.prefix),
|
||||
quote = highlight.quote ?: "",
|
||||
suffix = Optional.presentIfNotNull(highlight.suffix)
|
||||
)
|
||||
|
||||
val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput)
|
||||
if (!isUpdatedOnServer) {
|
||||
Log.d("sync", "FAILED TO MERGE HIGHLIGHT")
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
||||
return false
|
||||
}
|
||||
|
||||
for (highlightID in mergeHighlightInput.overlapHighlightIdList) {
|
||||
Log.d("sync", "DELETING MERGED HIGHLIGHT: ${highlightID}")
|
||||
val deleteChange = HighlightChange(
|
||||
highlightId = highlightID,
|
||||
savedItemId = highlightChange.savedItemId,
|
||||
type = "",
|
||||
shortId = "",
|
||||
annotation = null,
|
||||
createdAt = null,
|
||||
patch = null,
|
||||
prefix = null,
|
||||
quote = null,
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue,
|
||||
html = null,
|
||||
suffix = null,
|
||||
updatedAt = null,
|
||||
color = null,
|
||||
highlightPositionPercent = null,
|
||||
highlightPositionAnchorIndex = null,
|
||||
overlappingIDs = null
|
||||
)
|
||||
performHighlightChange(deleteChange)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performHighlightChange(highlightChange: HighlightChange) {
|
||||
val highlight = highlightChangeToHighlight(highlightChange)
|
||||
if (syncHighlightChange(highlightChange)) {
|
||||
highlightChangesDao.deleteById(highlight.highlightId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sync(since: String, cursor: String?, limit: Int): SavedItemSyncResult {
|
||||
val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since)
|
||||
?: return SavedItemSyncResult.errorResult
|
||||
|
||||
if (syncResult.deletedItemIDs.isNotEmpty()) {
|
||||
savedItemDao.deleteByIds(syncResult.deletedItemIDs)
|
||||
}
|
||||
|
||||
val savedItems = syncResult.items.map {
|
||||
val savedItem = SavedItem(
|
||||
savedItemId = it.id,
|
||||
title = it.title,
|
||||
createdAt = it.createdAt as String,
|
||||
savedAt = it.savedAt as String,
|
||||
readAt = it.readAt as String?,
|
||||
updatedAt = it.updatedAt as String?,
|
||||
readingProgress = it.readingProgressPercent,
|
||||
readingProgressAnchor = it.readingProgressAnchorIndex,
|
||||
imageURLString = it.image,
|
||||
pageURLString = it.url,
|
||||
descriptionText = it.description,
|
||||
publisherURLString = it.originalArticleUrl,
|
||||
siteName = it.siteName,
|
||||
author = it.author,
|
||||
publishDate = it.publishedAt as String?,
|
||||
slug = it.slug,
|
||||
isArchived = it.isArchived,
|
||||
contentReader = it.contentReader.rawValue,
|
||||
content = null,
|
||||
wordsCount = it.wordsCount
|
||||
)
|
||||
val labels = it.labels?.map { label ->
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = label.labelFields.id,
|
||||
name = label.labelFields.name,
|
||||
color = label.labelFields.color,
|
||||
createdAt = null,
|
||||
labelDescription = null
|
||||
)
|
||||
} ?: listOf()
|
||||
val highlights = it.highlights?.map { highlight ->
|
||||
Highlight(
|
||||
type = highlight.highlightFields.type.toString(),
|
||||
highlightId = highlight.highlightFields.id,
|
||||
annotation = highlight.highlightFields.annotation,
|
||||
createdByMe = highlight.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
patch = highlight.highlightFields.patch,
|
||||
prefix = highlight.highlightFields.prefix,
|
||||
quote = highlight.highlightFields.quote,
|
||||
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
shortId = highlight.highlightFields.shortId,
|
||||
suffix = highlight.highlightFields.suffix,
|
||||
createdAt = null,
|
||||
updatedAt = highlight.highlightFields.updatedAt as String?,
|
||||
color = highlight.highlightFields.color,
|
||||
highlightPositionPercent = highlight.highlightFields.highlightPositionPercent,
|
||||
highlightPositionAnchorIndex = highlight.highlightFields.highlightPositionAnchorIndex,
|
||||
)
|
||||
} ?: listOf()
|
||||
SavedItemWithLabelsAndHighlights(
|
||||
savedItem = savedItem, labels = labels, highlights = highlights
|
||||
)
|
||||
}
|
||||
|
||||
savedItemWithLabelsAndHighlightsDao.insertAll(savedItems)
|
||||
|
||||
Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since")
|
||||
|
||||
return SavedItemSyncResult(hasError = false,
|
||||
hasMoreItems = syncResult.hasMoreItems,
|
||||
cursor = syncResult.cursor,
|
||||
count = syncResult.items.size,
|
||||
savedItemSlugs = syncResult.items.map { it.slug })
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,19 +2,19 @@ package app.omnivore.omnivore.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import app.omnivore.omnivore.core.database.dao.HighlightChangesDao
|
||||
import app.omnivore.omnivore.core.database.dao.HighlightDao
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemAndSavedItemLabelCrossRefDao
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemLabelDao
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemWithLabelsAndHighlightsDao
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChangesDao
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef
|
||||
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.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabelDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlightsDao
|
||||
import app.omnivore.omnivore.core.database.entities.Viewer
|
||||
import app.omnivore.omnivore.core.database.entities.ViewerDao
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ interface SavedItemDao {
|
||||
fun getUnSynced(): List<SavedItem>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE slug = :slug")
|
||||
fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights?
|
||||
suspend fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights?
|
||||
|
||||
@Query("DELETE FROM savedItem WHERE savedItemId = :itemID")
|
||||
fun deleteById(itemID: String)
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Junction
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.Relation
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@ -87,25 +96,3 @@ data class SavedItemWithLabelsAndHighlights(
|
||||
return savedItem.savedItemId.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface HighlightDao {
|
||||
@Query("SELECT * FROM highlight WHERE serverSyncStatus != 0")
|
||||
fun getUnSynced(): List<Highlight>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<Highlight>)
|
||||
|
||||
@Query("DELETE FROM highlight WHERE highlightId = :highlightId")
|
||||
fun deleteById(highlightId: String)
|
||||
|
||||
@Query("SELECT * FROM highlight WHERE highlightId = :highlightId")
|
||||
fun findById(highlightId: String): Highlight?
|
||||
|
||||
// Server sync status is passed in here to work around Room compile-time query rules, but should always be NEEDS_UPDATE
|
||||
@Query("UPDATE highlight SET annotation = :note, serverSyncStatus = :serverSyncStatus WHERE highlightId = :highlightId")
|
||||
fun updateNote(highlightId: String, note: String, serverSyncStatus: Int = ServerSyncStatus.NEEDS_UPDATE.rawValue)
|
||||
|
||||
@Update
|
||||
fun update(highlight: Highlight)
|
||||
}
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.database.dao.HighlightChangesDao
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
@ -110,16 +107,3 @@ fun highlightChangeToHighlight(change: HighlightChange): Highlight {
|
||||
serverSyncStatus = change.serverSyncStatus
|
||||
)
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface HighlightChangesDao {
|
||||
@Query("SELECT * FROM highlightChange WHERE serverSyncStatus != 0 ORDER BY updatedAt ASC")
|
||||
fun getUnSynced(): List<HighlightChange>
|
||||
|
||||
@Query("DELETE FROM highlightChange WHERE highlightId = :highlightId")
|
||||
fun deleteById(highlightId: String)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<HighlightChange>)
|
||||
}
|
||||
|
||||
|
||||
@ -2,12 +2,8 @@ package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.core.net.toUri
|
||||
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(
|
||||
@ -74,67 +70,6 @@ data class TypeaheadCardData(
|
||||
val isArchived: Boolean,
|
||||
)
|
||||
|
||||
@Dao
|
||||
abstract class SavedItemWithLabelsAndHighlightsDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertSavedItems(items: List<SavedItem>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertLabelCrossRefs(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertLabels(items: List<SavedItemLabel>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertHighlights(items: List<Highlight>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertHighlightCrossRefs(items: List<SavedItemAndHighlightCrossRef>)
|
||||
|
||||
@Transaction
|
||||
open fun insertAll(savedItems: List<SavedItemWithLabelsAndHighlights>) {
|
||||
insertSavedItems(savedItems.map { it.savedItem })
|
||||
|
||||
val labels: MutableList<SavedItemLabel> = mutableListOf()
|
||||
val highlights: MutableList<Highlight> = mutableListOf()
|
||||
|
||||
val labelCrossRefs: MutableList<SavedItemAndSavedItemLabelCrossRef> = mutableListOf()
|
||||
val highlightCrossRefs: MutableList<SavedItemAndHighlightCrossRef> = mutableListOf()
|
||||
|
||||
for (searchItem in savedItems) {
|
||||
labels.addAll(searchItem.labels)
|
||||
highlights.addAll(searchItem.highlights)
|
||||
|
||||
val newLabelCrossRefs = searchItem.labels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(
|
||||
savedItemLabelId = it.savedItemLabelId,
|
||||
savedItemId = searchItem.savedItem.savedItemId
|
||||
)
|
||||
}
|
||||
|
||||
val newHighlightCrossRefs = searchItem.highlights.map {
|
||||
SavedItemAndHighlightCrossRef(
|
||||
highlightId = it.highlightId,
|
||||
savedItemId = searchItem.savedItem.savedItemId
|
||||
)
|
||||
}
|
||||
|
||||
labelCrossRefs.addAll(newLabelCrossRefs)
|
||||
highlightCrossRefs.addAll(newHighlightCrossRefs)
|
||||
}
|
||||
|
||||
insertLabels(labels)
|
||||
insertLabelCrossRefs(labelCrossRefs)
|
||||
|
||||
insertHighlights(highlights)
|
||||
insertHighlightCrossRefs(highlightCrossRefs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
object SavedItemQueryConstants {
|
||||
const val columns =
|
||||
"savedItemId, slug, publisherURLString, title, author, descriptionText, imageURLString, isArchived, pageURLString, contentReader, savedAt, readingProgress, wordsCount"
|
||||
|
||||
@ -1,15 +1,8 @@
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
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(
|
||||
@ -21,26 +14,6 @@ data class SavedItemLabel(
|
||||
val serverSyncStatus: Int = 0
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface SavedItemLabelDao {
|
||||
@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("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>
|
||||
}
|
||||
|
||||
@Entity(
|
||||
primaryKeys = ["savedItemLabelId", "savedItemId"], foreignKeys = [ForeignKey(
|
||||
entity = SavedItem::class,
|
||||
@ -56,16 +29,3 @@ interface SavedItemLabelDao {
|
||||
data class SavedItemAndSavedItemLabelCrossRef(
|
||||
val savedItemLabelId: String, val savedItemId: String
|
||||
)
|
||||
|
||||
|
||||
@Dao
|
||||
interface SavedItemAndSavedItemLabelCrossRefDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||
|
||||
@Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId")
|
||||
fun deleteRefsBySavedItemId(savedItemId: String)
|
||||
}
|
||||
|
||||
// has many highlights
|
||||
// has many savedItems
|
||||
|
||||
@ -1,49 +1,41 @@
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.SaveArticleReadingProgressMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.SaveArticleReadingProgressInput
|
||||
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.graphql.generated.SaveArticleReadingProgressMutation
|
||||
import app.omnivore.omnivore.graphql.generated.type.SaveArticleReadingProgressInput
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
|
||||
data class ReadingProgressParams(
|
||||
val id: String?,
|
||||
val readingProgressPercent: Double?,
|
||||
val readingProgressAnchorIndex: Int?,
|
||||
val force: Boolean?
|
||||
val id: String?,
|
||||
val readingProgressPercent: Double?,
|
||||
val readingProgressAnchorIndex: Int?,
|
||||
val force: Boolean?
|
||||
) {
|
||||
fun asSaveReadingProgressInput() = SaveArticleReadingProgressInput(
|
||||
id = id ?: "",
|
||||
force = Optional.presentIfNotNull(force),
|
||||
readingProgressPercent = readingProgressPercent ?: 0.0,
|
||||
readingProgressAnchorIndex = Optional.presentIfNotNull(readingProgressAnchorIndex ?: 0)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun Networker.updateWebReadingProgress(jsonString: String): Boolean {
|
||||
val params = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
|
||||
return updateReadingProgress(params)
|
||||
fun asSaveReadingProgressInput() = SaveArticleReadingProgressInput(
|
||||
id = id ?: "",
|
||||
force = Optional.presentIfNotNull(force),
|
||||
readingProgressPercent = readingProgressPercent ?: 0.0,
|
||||
readingProgressAnchorIndex = Optional.presentIfNotNull(readingProgressAnchorIndex ?: 0)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun Networker.updateReadingProgress(params: ReadingProgressParams): Boolean {
|
||||
try {
|
||||
val input = params.asSaveReadingProgressInput()
|
||||
try {
|
||||
val input = params.asSaveReadingProgressInput()
|
||||
|
||||
Log.d("Loggo", "created reading progress input: $input")
|
||||
Log.d("Loggo", "created reading progress input: $input")
|
||||
|
||||
val result = authenticatedApolloClient()
|
||||
.mutation(SaveArticleReadingProgressMutation(input))
|
||||
.execute()
|
||||
val result = authenticatedApolloClient().mutation(SaveArticleReadingProgressMutation(input))
|
||||
.execute()
|
||||
|
||||
val articleID =
|
||||
result.data?.saveArticleReadingProgress?.onSaveArticleReadingProgressSuccess?.updatedArticle?.id
|
||||
val articleID =
|
||||
result.data?.saveArticleReadingProgress?.onSaveArticleReadingProgressSuccess?.updatedArticle?.id
|
||||
|
||||
Log.d("Loggo", "updated article with id: $articleID")
|
||||
Log.d("Loggo", "updated article with id: $articleID")
|
||||
|
||||
return articleID != null
|
||||
} catch (e: java.lang.Exception) {
|
||||
return false
|
||||
}
|
||||
return articleID != null
|
||||
} catch (e: java.lang.Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.GetLabelsQuery
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.graphql.generated.GetLabelsQuery
|
||||
|
||||
|
||||
suspend fun Networker.savedItemLabels(): List<SavedItemLabel> {
|
||||
try {
|
||||
val result = authenticatedApolloClient().query(GetLabelsQuery()).execute()
|
||||
val labels = result.data?.labels?.onLabelsSuccess?.labels ?: listOf()
|
||||
try {
|
||||
val result = authenticatedApolloClient().query(GetLabelsQuery()).execute()
|
||||
val labels = result.data?.labels?.onLabelsSuccess?.labels ?: listOf()
|
||||
|
||||
return labels.map {
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = it.labelFields.id,
|
||||
name = it.labelFields.name,
|
||||
color = it.labelFields.color,
|
||||
createdAt = it.labelFields.createdAt as String?,
|
||||
labelDescription = it.labelFields.description
|
||||
)
|
||||
return labels.map {
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = it.labelFields.id,
|
||||
name = it.labelFields.name,
|
||||
color = it.labelFields.color,
|
||||
createdAt = it.labelFields.createdAt as String?,
|
||||
labelDescription = it.labelFields.description
|
||||
)
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
return listOf()
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
return listOf()
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,11 +33,13 @@ import app.omnivore.omnivore.feature.components.LabelChipColors
|
||||
|
||||
@Composable
|
||||
fun LibraryFilterBar(
|
||||
isFollowingScreen: Boolean,
|
||||
viewModel: LibraryViewModel = hiltViewModel()
|
||||
) {
|
||||
|
||||
var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) }
|
||||
val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(
|
||||
SavedItemFilter.INBOX
|
||||
if (isFollowingScreen) SavedItemFilter.FOLLOWING else SavedItemFilter.INBOX
|
||||
)
|
||||
val activeLabels: List<SavedItemLabel> by viewModel.activeLabelsLiveData.observeAsState(listOf())
|
||||
|
||||
@ -58,7 +60,9 @@ fun LibraryFilterBar(
|
||||
) {
|
||||
item {
|
||||
AssistChip(onClick = { isSavedItemFilterMenuExpanded = true },
|
||||
label = { Text(activeSavedItemFilter.displayText) },
|
||||
label = { Text(
|
||||
activeSavedItemFilter.displayText
|
||||
) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
@ -113,9 +117,12 @@ fun LibraryFilterBar(
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemFilterContextMenu(isExpanded = isSavedItemFilterMenuExpanded,
|
||||
SavedItemFilterContextMenu(
|
||||
isFollowingScreen = isFollowingScreen,
|
||||
isExpanded = isSavedItemFilterMenuExpanded,
|
||||
onDismiss = { isSavedItemFilterMenuExpanded = false },
|
||||
actionHandler = { viewModel.updateSavedItemFilter(it) })
|
||||
actionHandler = { viewModel.updateSavedItemFilter(it) }
|
||||
)
|
||||
|
||||
SavedItemSortFilterContextMenu(isExpanded = isSavedItemSortFilterMenuExpanded,
|
||||
onDismiss = { isSavedItemSortFilterMenuExpanded = false },
|
||||
|
||||
@ -21,7 +21,6 @@ import androidx.compose.material.DismissState
|
||||
import androidx.compose.material.DismissValue
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.FractionalThreshold
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.ScaffoldState
|
||||
import androidx.compose.material.SwipeToDismiss
|
||||
import androidx.compose.material.icons.Icons
|
||||
@ -33,6 +32,7 @@ import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
@ -60,7 +60,6 @@ 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
|
||||
import app.omnivore.omnivore.feature.components.AddLinkSheetContent
|
||||
import app.omnivore.omnivore.feature.components.LabelsSelectionSheetContent
|
||||
@ -147,6 +146,7 @@ internal fun LibraryView(
|
||||
when (uiState) {
|
||||
is LibraryUiState.Success -> {
|
||||
LibraryViewContent(
|
||||
isFollowingScreen = currentTopLevelDestination == TopLevelDestination.FOLLOWING,
|
||||
viewModel,
|
||||
paddingValues = paddingValues,
|
||||
uiState = uiState
|
||||
@ -189,9 +189,9 @@ fun LabelBottomSheet(
|
||||
) {
|
||||
|
||||
val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit()
|
||||
val labels: List<SavedItemLabel> by libraryViewModel.savedItemLabelsLiveData.observeAsState(
|
||||
listOf()
|
||||
)
|
||||
|
||||
val labels by libraryViewModel.labelsState.collectAsStateWithLifecycle()
|
||||
|
||||
if (currentSavedItemData != null) {
|
||||
LabelsSelectionSheetContent(
|
||||
labels = labels,
|
||||
@ -302,6 +302,7 @@ fun EditBottomSheet(
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LibraryViewContent(
|
||||
isFollowingScreen: Boolean,
|
||||
libraryViewModel: LibraryViewModel,
|
||||
paddingValues: PaddingValues,
|
||||
uiState: LibraryUiState
|
||||
@ -328,7 +329,7 @@ fun LibraryViewContent(
|
||||
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
||||
) {
|
||||
Column {
|
||||
LibraryFilterBar()
|
||||
LibraryFilterBar(isFollowingScreen)
|
||||
HorizontalDivider()
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -7,30 +8,14 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.data.archiveSavedItem
|
||||
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.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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@ -50,11 +35,9 @@ import javax.inject.Inject
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltViewModel
|
||||
class LibraryViewModel @Inject constructor(
|
||||
private val networker: Networker,
|
||||
private val dataService: DataService,
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
private val resourceProvider: ResourceProvider,
|
||||
private val libraryRepository: LibraryRepository,
|
||||
@ApplicationContext private val applicationContext: Context
|
||||
) : ViewModel(), SavedItemViewModel {
|
||||
|
||||
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
|
||||
@ -62,7 +45,7 @@ class LibraryViewModel @Inject constructor(
|
||||
|
||||
var snackbarMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
|
||||
private val _libraryQuery = MutableStateFlow(
|
||||
LibraryQuery(
|
||||
allowedArchiveStates = listOf(0),
|
||||
@ -75,48 +58,43 @@ class LibraryViewModel @Inject constructor(
|
||||
|
||||
val uiState: StateFlow<LibraryUiState> = _libraryQuery.flatMapLatest { query ->
|
||||
libraryRepository.getSavedItems(query)
|
||||
}
|
||||
.map(LibraryUiState::Success)
|
||||
.stateIn(
|
||||
}.map(LibraryUiState::Success).stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = LibraryUiState.Loading
|
||||
)
|
||||
|
||||
|
||||
val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX)
|
||||
val appliedFilterLiveData = MutableLiveData<SavedItemFilter>()
|
||||
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
|
||||
val bottomSheetState = MutableLiveData(LibraryBottomSheetState.HIDDEN)
|
||||
val currentItem = mutableStateOf<String?>(null)
|
||||
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
|
||||
|
||||
val labelsState = libraryRepository.getSavedItemsLabels().stateIn(
|
||||
scope = viewModelScope, started = SharingStarted.Lazily, initialValue = listOf()
|
||||
)
|
||||
|
||||
val activeLabelsLiveData = MutableLiveData<List<SavedItemLabel>>(listOf())
|
||||
|
||||
override val actionsMenuItemLiveData = MutableLiveData<SavedItemWithLabelsAndHighlights?>(null)
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private var hasLoadedInitialFilters = false
|
||||
|
||||
private fun loadInitialFilterValues() {
|
||||
|
||||
if (hasLoadedInitialFilters) {
|
||||
return
|
||||
}
|
||||
hasLoadedInitialFilters = false
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
dataService.syncLabels()
|
||||
}
|
||||
}
|
||||
syncLabels()
|
||||
|
||||
viewModelScope.launch {
|
||||
handleFilterChanges()
|
||||
for (slug in contentRequestChannel) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
dataService.fetchSavedItemContent(slug)
|
||||
}
|
||||
libraryRepository.fetchSavedItemContent(slug)
|
||||
}
|
||||
}
|
||||
|
||||
updateSavedItemFilter(appliedFilterLiveData.value ?: SavedItemFilter.INBOX)
|
||||
}
|
||||
|
||||
private fun syncLabels() {
|
||||
viewModelScope.launch {
|
||||
val labels = libraryRepository.getLabels()
|
||||
libraryRepository.insertAllLabels(labels)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSnackbarMessage() {
|
||||
@ -125,7 +103,6 @@ class LibraryViewModel @Inject constructor(
|
||||
|
||||
fun refresh() {
|
||||
librarySearchCursor = null
|
||||
isRefreshing = true
|
||||
load()
|
||||
}
|
||||
|
||||
@ -141,13 +118,8 @@ class LibraryViewModel @Inject constructor(
|
||||
|
||||
fun initialLoad() {
|
||||
if (getLastSyncTime() == null) {
|
||||
hasLoadedInitialFilters = false
|
||||
librarySearchCursor = null
|
||||
}
|
||||
|
||||
if (hasLoadedInitialFilters) {
|
||||
return
|
||||
}
|
||||
load()
|
||||
}
|
||||
|
||||
@ -162,24 +134,18 @@ class LibraryViewModel @Inject constructor(
|
||||
|
||||
fun loadUsingSearchAPI() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = dataService.librarySearch(
|
||||
cursor = librarySearchCursor, query = searchQueryString()
|
||||
)
|
||||
result.cursor?.let {
|
||||
librarySearchCursor = it
|
||||
}
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
isRefreshing = false
|
||||
}
|
||||
val result = libraryRepository.librarySearch(
|
||||
cursor = librarySearchCursor, query = searchQueryString()
|
||||
)
|
||||
result.cursor?.let {
|
||||
librarySearchCursor = it
|
||||
}
|
||||
result.savedItems.map {
|
||||
val isSavedInDB = libraryRepository.isSavedItemContentStoredInDB(it.savedItem.slug)
|
||||
|
||||
result.savedItems.map {
|
||||
val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.savedItem.slug)
|
||||
|
||||
if (!isSavedInDB) {
|
||||
delay(2000)
|
||||
contentRequestChannel.send(it.savedItem.slug)
|
||||
}
|
||||
if (!isSavedInDB) {
|
||||
delay(2000)
|
||||
contentRequestChannel.send(it.savedItem.slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -232,6 +198,7 @@ class LibraryViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
var requiredLabels = when (appliedFilterLiveData.value) {
|
||||
SavedItemFilter.FOLLOWING -> listOf("Newsletter", "RSS")
|
||||
SavedItemFilter.NEWSLETTERS -> listOf("Newsletter")
|
||||
SavedItemFilter.FEEDS -> listOf("RSS")
|
||||
else -> (activeLabelsLiveData.value ?: listOf()).map { it.name }
|
||||
@ -268,9 +235,6 @@ class LibraryViewModel @Inject constructor(
|
||||
count = 0,
|
||||
startTime = syncStart.toString()
|
||||
)
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,8 +245,8 @@ class LibraryViewModel @Inject constructor(
|
||||
startTime: String,
|
||||
isInitialBatch: Boolean = true
|
||||
) {
|
||||
dataService.syncOfflineItemsWithServerIfNeeded()
|
||||
val result = dataService.sync(since = since, cursor = cursor, limit = 20)
|
||||
libraryRepository.syncOfflineItemsWithServerIfNeeded()
|
||||
val result = libraryRepository.sync(since = since, cursor = cursor, limit = 20)
|
||||
|
||||
// Fetch content for the initial batch only
|
||||
if (isInitialBatch) {
|
||||
@ -348,66 +312,39 @@ class LibraryViewModel @Inject constructor(
|
||||
|
||||
fun deleteSavedItem(itemID: String) {
|
||||
viewModelScope.launch {
|
||||
dataService.deleteSavedItem(itemID)
|
||||
libraryRepository.deleteSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
|
||||
fun archiveSavedItem(itemID: String) {
|
||||
viewModelScope.launch {
|
||||
dataService.archiveSavedItem(itemID)
|
||||
libraryRepository.archiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
|
||||
fun unarchiveSavedItem(itemID: String) {
|
||||
viewModelScope.launch {
|
||||
dataService.unarchiveSavedItem(itemID)
|
||||
libraryRepository.unarchiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = setSavedItemLabels(
|
||||
networker = networker,
|
||||
dataService = dataService,
|
||||
savedItemID = savedItemID,
|
||||
labels = labels
|
||||
)
|
||||
|
||||
snackbarMessage = if (result) {
|
||||
resourceProvider.getString(R.string.library_view_model_snackbar_success)
|
||||
} else {
|
||||
resourceProvider.getString(R.string.library_view_model_snackbar_error)
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
handleFilterChanges()
|
||||
}
|
||||
val result = libraryRepository.setSavedItemLabels(
|
||||
itemId = savedItemID, labels = labels
|
||||
)
|
||||
snackbarMessage = if (result) {
|
||||
applicationContext.getString(R.string.library_view_model_snackbar_success)
|
||||
} else {
|
||||
applicationContext.getString(R.string.library_view_model_snackbar_error)
|
||||
}
|
||||
handleFilterChanges()
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewSavedItemLabel(labelName: String, hexColorValue: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newLabel = networker.createNewLabel(
|
||||
CreateLabelInput(
|
||||
color = Optional.presentIfNotNull(hexColorValue), name = labelName
|
||||
)
|
||||
)
|
||||
|
||||
newLabel?.let {
|
||||
val savedItemLabel = SavedItemLabel(
|
||||
savedItemLabelId = it.id,
|
||||
name = it.name,
|
||||
color = it.color,
|
||||
createdAt = it.createdAt as String?,
|
||||
labelDescription = it.description
|
||||
)
|
||||
|
||||
dataService.db.savedItemLabelDao().insertAll(listOf(savedItemLabel))
|
||||
}
|
||||
}
|
||||
libraryRepository.createNewSavedItemLabel(labelName, hexColorValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,38 +7,45 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
enum class SavedItemFilter(val displayText: String, val rawValue: String, val queryString: String) {
|
||||
INBOX("Inbox", rawValue = "inbox", "in:inbox"), READ_LATER(
|
||||
"Non-Feed Items", "nonFeed", "no:subscription"
|
||||
),
|
||||
FEEDS("Feeds", "feeds", "in:inbox label:RSS"), NEWSLETTERS(
|
||||
"Newsletters", "newsletters", "in:inbox label:Newsletter"
|
||||
),
|
||||
|
||||
// RECOMMENDED("Recommended", "recommended", "recommendedBy:*"),
|
||||
ALL("All", "all", "in:all"), ARCHIVED("Archived", "archived", "in:archive"),
|
||||
|
||||
FOLLOWING("Following", "following", "in:following use:folders"),
|
||||
INBOX("Inbox", rawValue = "inbox", "in:inbox"),
|
||||
READ_LATER("Non-Feed Items", "nonFeed", "no:subscription"),
|
||||
FEEDS("Feeds", "feeds", "in:inbox label:RSS"),
|
||||
NEWSLETTERS("Newsletters", "newsletters", "in:inbox label:Newsletter"),
|
||||
ALL("All", "all", "in:all"),
|
||||
ARCHIVED("Archived", "archived", "in:archive"),
|
||||
// HAS_HIGHLIGHTS("Highlighted", "hasHighlights", "has:highlights"),
|
||||
FILES("Files", "files", "type:file"),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SavedItemFilterContextMenu(
|
||||
isExpanded: Boolean, onDismiss: () -> Unit, actionHandler: (SavedItemFilter) -> Unit
|
||||
isFollowingScreen: Boolean,
|
||||
isExpanded: Boolean, onDismiss: () -> Unit,
|
||||
actionHandler: (SavedItemFilter) -> Unit
|
||||
) {
|
||||
|
||||
val filters = if (isFollowingScreen) {
|
||||
listOf(
|
||||
SavedItemFilter.FOLLOWING,
|
||||
SavedItemFilter.FEEDS,
|
||||
SavedItemFilter.NEWSLETTERS
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
SavedItemFilter.INBOX,
|
||||
SavedItemFilter.READ_LATER,
|
||||
SavedItemFilter.ALL,
|
||||
SavedItemFilter.ARCHIVED,
|
||||
SavedItemFilter.FILES
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = isExpanded, onDismissRequest = onDismiss
|
||||
) {
|
||||
// Displaying only a subset of filters until we figure out the Room DB queries (and labels)
|
||||
// SavedItemFilter.values().forEach {
|
||||
listOf(
|
||||
SavedItemFilter.INBOX,
|
||||
SavedItemFilter.READ_LATER,
|
||||
SavedItemFilter.NEWSLETTERS,
|
||||
SavedItemFilter.FEEDS,
|
||||
SavedItemFilter.ALL,
|
||||
SavedItemFilter.ARCHIVED,
|
||||
SavedItemFilter.FILES
|
||||
).forEach {
|
||||
filters.forEach {
|
||||
DropdownMenuItem(text = { Text(text = it.displayText, fontWeight = FontWeight.Normal) },
|
||||
onClick = {
|
||||
actionHandler(it)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package app.omnivore.omnivore.feature
|
||||
package app.omnivore.omnivore.feature.reader
|
||||
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
@ -34,7 +34,6 @@ import app.omnivore.omnivore.core.network.saveUrl
|
||||
import app.omnivore.omnivore.core.network.savedItem
|
||||
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
|
||||
@ -464,7 +463,7 @@ class WebReaderViewModel @Inject constructor(
|
||||
val storedThemePreference =
|
||||
datastoreRepo.getString(DatastoreKeys.preferredTheme) ?: "System"
|
||||
val storedWebFont =
|
||||
WebFont.values().firstOrNull { it.rawValue == storedFontFamily } ?: WebFont.values()
|
||||
WebFont.entries.firstOrNull { it.rawValue == storedFontFamily } ?: WebFont.entries
|
||||
.first()
|
||||
|
||||
val prefersHighContrastFont =
|
||||
|
||||
Reference in New Issue
Block a user