mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1637 from omnivore-app/feature/android-library-sync
Library Syncing - Android
This commit is contained in:
commit
f36eb0ed32
33 changed files with 953 additions and 260 deletions
File diff suppressed because one or more lines are too long
56
android/Omnivore/app/src/main/graphql/UpdatesSince.graphql
Normal file
56
android/Omnivore/app/src/main/graphql/UpdatesSince.graphql
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
query UpdatesSince($after: String, $first: Int, $since: Date!) {
|
||||
updatesSince(after: $after, first: $first, since: $since) {
|
||||
... on UpdatesSinceSuccess {
|
||||
edges {
|
||||
cursor
|
||||
itemID
|
||||
updateReason
|
||||
node {
|
||||
id
|
||||
title
|
||||
slug
|
||||
url
|
||||
pageType
|
||||
contentReader
|
||||
createdAt
|
||||
isArchived
|
||||
readingProgressPercent
|
||||
readingProgressAnchorIndex
|
||||
author
|
||||
image
|
||||
description
|
||||
publishedAt
|
||||
ownedByViewer
|
||||
originalArticleUrl
|
||||
uploadFileId
|
||||
labels {
|
||||
id
|
||||
name
|
||||
color
|
||||
}
|
||||
pageId
|
||||
shortId
|
||||
quote
|
||||
annotation
|
||||
state
|
||||
siteName
|
||||
subscription
|
||||
readAt
|
||||
savedAt
|
||||
updatedAt
|
||||
language
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
... on UpdatesSinceError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package app.omnivore.omnivore
|
|||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import app.omnivore.omnivore.dataService.DataService
|
||||
import app.omnivore.omnivore.networking.Networker
|
||||
import app.omnivore.omnivore.persistence.AppDatabase
|
||||
import dagger.Module
|
||||
|
|
@ -31,5 +32,8 @@ object AppModule {
|
|||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideDataService(@ApplicationContext app: Context) = DataService(app)
|
||||
fun provideDataService(
|
||||
@ApplicationContext app: Context,
|
||||
networker: Networker
|
||||
) = DataService(app, networker)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ object DatastoreKeys {
|
|||
const val omnivoreAuthToken = "omnivoreAuthToken"
|
||||
const val omnivoreAuthCookieString = "omnivoreAuthCookieString"
|
||||
const val omnivorePendingUserToken = "omnivorePendingUserToken"
|
||||
const val libraryLastSyncTimestamp = "libraryLastSyncTimestamp"
|
||||
const val preferredWebFontSize = "preferredWebFontSize"
|
||||
const val preferredWebLineHeight = "preferredWebLineHeight"
|
||||
const val preferredWebMaxWidthPercentage = "preferredWebMaxWidthPercentage"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,5 @@ package app.omnivore.omnivore
|
|||
|
||||
sealed class Routes(val route: String) {
|
||||
object Library : Routes("Library")
|
||||
object WebAppReader : Routes("WebAppReader")
|
||||
object Settings: Routes("Settings")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
package app.omnivore.omnivore
|
||||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.AppDatabase
|
||||
import javax.inject.Inject
|
||||
|
||||
class DataService @Inject constructor(
|
||||
context: Context
|
||||
context: Context,
|
||||
val networker: Networker
|
||||
) {
|
||||
val db = Room.databaseBuilder(
|
||||
context,
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemAndHighlightCrossRef
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun DataService.createWebHighlight(jsonString: String) {
|
||||
val createHighlightInput = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = Highlight(
|
||||
highlightId = createHighlightInput.id,
|
||||
shortId = createHighlightInput.shortId,
|
||||
quote = createHighlightInput.quote,
|
||||
prefix = null,
|
||||
suffix = null,
|
||||
patch = createHighlightInput.patch,
|
||||
annotation = createHighlightInput.annotation.getOrNull(),
|
||||
createdAt = null,
|
||||
updatedAt = null,
|
||||
createdByMe = false
|
||||
)
|
||||
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = createHighlightInput.id,
|
||||
savedItemId = createHighlightInput.articleId
|
||||
)
|
||||
|
||||
db.highlightDao().insertAll(listOf(highlight))
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
|
||||
val newHighlight = networker.createHighlight(createHighlightInput)
|
||||
|
||||
newHighlight?.let {
|
||||
db.highlightDao().update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.mergeWebHighlights(jsonString: String) {
|
||||
val mergeHighlightInput = Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = mergeHighlightInput.id) ?: return@withContext
|
||||
highlight.shortId = mergeHighlightInput.shortId
|
||||
highlight.quote = mergeHighlightInput.quote
|
||||
highlight.patch = mergeHighlightInput.patch
|
||||
highlight.prefix = mergeHighlightInput.prefix.getOrNull()
|
||||
highlight.annotation = mergeHighlightInput.annotation.getOrNull()
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
|
||||
for (highlightID in mergeHighlightInput.overlapHighlightIdList) {
|
||||
deleteHighlight(highlightID)
|
||||
}
|
||||
|
||||
val crossRef = SavedItemAndHighlightCrossRef(
|
||||
highlightId = mergeHighlightInput.id,
|
||||
savedItemId = mergeHighlightInput.articleId
|
||||
)
|
||||
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
val isUpdatedOnServer = networker.mergeHighlights(mergeHighlightInput)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
highlight.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.updateWebHighlight(jsonString: String) {
|
||||
val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java).asUpdateHighlightInput()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = updateHighlightParams.highlightId) ?: return@withContext
|
||||
|
||||
highlight.annotation = updateHighlightParams.annotation.getOrNull()
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
val isUpdatedOnServer = networker.updateHighlight(updateHighlightParams)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
highlight.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.deleteHighlights(jsonString: String) {
|
||||
val highlightIDs = Gson().fromJson(jsonString, DeleteHighlightParams::class.java).asIdList()
|
||||
|
||||
for (highlightID in highlightIDs) {
|
||||
deleteHighlight(highlightID)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.deleteHighlight(highlightID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val highlight = db.highlightDao().findById(highlightId = highlightID) ?: return@withContext
|
||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||
db.highlightDao().update(highlight)
|
||||
|
||||
val isUpdatedOnServer = networker.deleteHighlights(listOf(highlightID))
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
db.highlightDao().deleteById(highlightId = highlightID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
|
||||
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 savedItems = syncResult.items.map {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
db.savedItemDao().insertAll(savedItems)
|
||||
|
||||
val labels: MutableList<SavedItemLabel> = mutableListOf()
|
||||
val crossRefs: MutableList<SavedItemAndSavedItemLabelCrossRef> = mutableListOf()
|
||||
|
||||
// save labels
|
||||
for (item in syncResult.items) {
|
||||
val itemLabels = (item.labels ?: listOf()).map {
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = it.id,
|
||||
name = it.name,
|
||||
color = it.color,
|
||||
createdAt = null,
|
||||
labelDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
labels.addAll(itemLabels)
|
||||
|
||||
val newCrossRefs = itemLabels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = item.id)
|
||||
}
|
||||
|
||||
crossRefs.addAll(newCrossRefs)
|
||||
}
|
||||
|
||||
db.savedItemLabelDao().insertAll(labels)
|
||||
db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
|
||||
|
||||
return SavedItemSyncResult(
|
||||
hasError = false,
|
||||
hasMoreItems = syncResult.hasMoreItems,
|
||||
cursor = syncResult.cursor,
|
||||
count = syncResult.items.size,
|
||||
savedItemSlugs = syncResult.items.map { it.slug }
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun DataService.syncSavedItemContent(slug: String) {
|
||||
val syncResult = networker.savedItem(slug)
|
||||
|
||||
val savedItem = syncResult.item ?: return
|
||||
db.savedItemDao().insert(savedItem)
|
||||
|
||||
// Persist Labels
|
||||
db.savedItemLabelDao().insertAll(syncResult.labels)
|
||||
|
||||
val labelCrossRefs = syncResult.labels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = savedItem.savedItemId)
|
||||
}
|
||||
|
||||
db.savedItemAndSavedItemLabelCrossRefDao().insertAll(labelCrossRefs)
|
||||
|
||||
// Persist Highlights
|
||||
db.highlightDao().insertAll(syncResult.highlights)
|
||||
|
||||
val highlightCrossRefs = syncResult.highlights.map {
|
||||
SavedItemAndHighlightCrossRef(highlightId = it.highlightId, savedItemId = savedItem.savedItemId)
|
||||
}
|
||||
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs)
|
||||
|
||||
Log.d("sync", "saved content for item with id: ${savedItem.savedItemId}")
|
||||
}
|
||||
|
||||
data class SavedItemSyncResult(
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.ReadingProgressParams
|
||||
import app.omnivore.omnivore.networking.updateReadingProgress
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun DataService.updateWebReadingProgress(jsonString: String) {
|
||||
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 isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.archiveSavedItem
|
||||
import app.omnivore.omnivore.networking.deleteSavedItem
|
||||
import app.omnivore.omnivore.networking.unarchiveSavedItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun DataService.deleteSavedItem(itemID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.deleteSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
db.savedItemDao().deleteById(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.archiveSavedItem(itemID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext
|
||||
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
savedItem.isArchived = true
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.archiveSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun DataService.unarchiveSavedItem(itemID: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(itemID = itemID) ?: return@withContext
|
||||
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
savedItem.isArchived = false
|
||||
db.savedItemDao().update(savedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.unarchiveSavedItem(itemID)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
suspend fun DataService.syncOfflineItemsWithServerIfNeeded() {
|
||||
val unSyncedSavedItems = db.savedItemDao().getUnSynced()
|
||||
val unSyncedHighlights = db.highlightDao().getUnSynced()
|
||||
|
||||
for (savedItem in unSyncedSavedItems) {
|
||||
syncSavedItem(savedItem)
|
||||
}
|
||||
|
||||
for (highlight in unSyncedHighlights) {
|
||||
syncHighlight(highlight)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncSavedItem(item: SavedItem) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncHighlight(highlight: Highlight) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
ServerSyncStatus.NEEDS_CREATION.rawValue -> {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCING)
|
||||
|
||||
val savedItemID = db.savedItemAndHighlightCrossRefDao()
|
||||
.associatedSavedItemID(highlightId = highlight.highlightId)
|
||||
|
||||
val isCreatedOnServer = networker.createHighlight(
|
||||
CreateHighlightInput(
|
||||
annotation = Optional.presentIfNotNull(highlight.annotation),
|
||||
articleId = savedItemID ?: "",
|
||||
id = highlight.highlightId,
|
||||
patch = highlight.patch ?: "",
|
||||
quote = highlight.quote ?: "",
|
||||
shortId = highlight.shortId ?: ""
|
||||
)
|
||||
)
|
||||
|
||||
if (isCreatedOnServer != null) {
|
||||
updateSyncStatus(ServerSyncStatus.IS_SYNCED)
|
||||
} else {
|
||||
updateSyncStatus(ServerSyncStatus.NEEDS_UPDATE)
|
||||
}
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package app.omnivore.omnivore.models
|
||||
|
||||
public enum class ServerSyncStatus(
|
||||
public val rawValue: Int,
|
||||
) {
|
||||
IS_SYNCED(0),
|
||||
IS_SYNCING(1),
|
||||
NEEDS_DELETION(2),
|
||||
NEEDS_CREATION(3),
|
||||
NEEDS_UPDATE(4)
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
|||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
import java.time.LocalDate
|
||||
|
||||
data class CreateHighlightParams(
|
||||
val shortId: String?,
|
||||
|
|
@ -140,7 +139,7 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
|
|||
// val updatedAtString = createdHighlight.highlightFields.updatedAt as? String
|
||||
|
||||
return Highlight(
|
||||
id = createdHighlight.highlightFields.id,
|
||||
highlightId = createdHighlight.highlightFields.id,
|
||||
shortId = createdHighlight.highlightFields.shortId,
|
||||
quote = createdHighlight.highlightFields.quote,
|
||||
prefix = createdHighlight.highlightFields.prefix,
|
||||
|
|
@ -149,9 +148,7 @@ suspend fun Networker.createHighlight(input: CreateHighlightInput): Highlight? {
|
|||
annotation = createdHighlight.highlightFields.annotation,
|
||||
createdAt = null, // TODO: update gql query to get this
|
||||
updatedAt = null, // TODO: fix updatedAtString?.let { LocalDate.parse(it) },
|
||||
createdByMe = createdHighlight.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
serverSyncStatus = 1 // TODO: create enum for this
|
||||
createdByMe = createdHighlight.highlightFields.createdByMe
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ suspend fun Networker.unarchiveSavedItem(itemID: String): Boolean {
|
|||
return updateArchiveStatusSavedItem(itemID, false)
|
||||
}
|
||||
|
||||
private suspend fun Networker.updateArchiveStatusSavedItem(itemID: String, setAsArchived: Boolean): Boolean {
|
||||
suspend fun Networker.updateArchiveStatusSavedItem(itemID: String, setAsArchived: Boolean): Boolean {
|
||||
return try {
|
||||
val input = ArchiveLinkInput(setAsArchived, itemID)
|
||||
val result = authenticatedApolloClient().mutation(SetLinkArchivedMutation(input)).execute()
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
|
|||
|
||||
val savedItemLabels = labels.map {
|
||||
SavedItemLabel(
|
||||
id = it.labelFields.id,
|
||||
savedItemLabelId = it.labelFields.id,
|
||||
name = it.labelFields.name,
|
||||
color = it.labelFields.color,
|
||||
createdAt = it.labelFields.createdAt as String?,
|
||||
|
|
@ -42,7 +42,7 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
|
|||
// val updatedAtString = it.highlightFields.updatedAt as? String
|
||||
|
||||
Highlight(
|
||||
id = it.highlightFields.id,
|
||||
highlightId = it.highlightFields.id,
|
||||
shortId = it.highlightFields.shortId,
|
||||
quote = it.highlightFields.quote,
|
||||
prefix = it.highlightFields.prefix,
|
||||
|
|
@ -51,16 +51,14 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
|
|||
annotation = it.highlightFields.annotation,
|
||||
createdAt = null, // TODO: update gql query to get this
|
||||
updatedAt = null, //updatedAtString?.let { str -> LocalDate.parse(str) }, TODO: fix date parsing
|
||||
createdByMe = it.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
serverSyncStatus = 1 // TODO: create enum for this
|
||||
createdByMe = it.highlightFields.createdByMe
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: handle errors
|
||||
|
||||
val savedItem = SavedItem(
|
||||
id = article.articleFields.id,
|
||||
savedItemId = article.articleFields.id,
|
||||
title = article.articleFields.title,
|
||||
createdAt = article.articleFields.createdAt as String,
|
||||
savedAt = article.articleFields.savedAt as String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.UpdatesSinceQuery
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateReason
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
data class SavedItemUpdatesQueryResponse(
|
||||
val cursor: String?,
|
||||
val hasMoreItems: Boolean,
|
||||
val deletedItemIDs: List<String>,
|
||||
val items: List<UpdatesSinceQuery.Node>
|
||||
)
|
||||
|
||||
suspend fun Networker.savedItemUpdates(
|
||||
cursor: String? = null,
|
||||
limit: Int = 15,
|
||||
since: String
|
||||
): SavedItemUpdatesQueryResponse? {
|
||||
try {
|
||||
val result = authenticatedApolloClient().query(
|
||||
UpdatesSinceQuery(
|
||||
after = Optional.presentIfNotNull(cursor),
|
||||
first = Optional.presentIfNotNull(limit),
|
||||
since = since
|
||||
)
|
||||
).execute()
|
||||
|
||||
val payload = result.data?.updatesSince?.onUpdatesSinceSuccess ?: return null
|
||||
val itemNodes: MutableList<UpdatesSinceQuery.Node> = mutableListOf()
|
||||
val deletedItemIDs: MutableList<String> = mutableListOf()
|
||||
|
||||
for (edge in payload.edges) {
|
||||
if (edge.updateReason == UpdateReason.DELETED) {
|
||||
deletedItemIDs.add(edge.itemID)
|
||||
} else if (edge.node != null) {
|
||||
itemNodes.add(edge.node)
|
||||
}
|
||||
}
|
||||
|
||||
return SavedItemUpdatesQueryResponse(
|
||||
cursor = payload.pageInfo.endCursor,
|
||||
hasMoreItems = payload.pageInfo.hasNextPage,
|
||||
deletedItemIDs = deletedItemIDs,
|
||||
items = itemNodes
|
||||
)
|
||||
} catch (e: java.lang.Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package app.omnivore.omnivore.networking
|
|||
|
||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||
import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ suspend fun Networker.typeaheadSearch(
|
|||
|
||||
val cardsData = itemList.map {
|
||||
SavedItemCardData(
|
||||
id = it.id,
|
||||
savedItemId = it.id,
|
||||
slug = it.slug,
|
||||
publisherURLString = "",
|
||||
title = it.title,
|
||||
|
|
@ -55,13 +54,12 @@ suspend fun Networker.search(
|
|||
)
|
||||
).execute()
|
||||
|
||||
|
||||
val newCursor = result.data?.search?.onSearchSuccess?.pageInfo?.endCursor
|
||||
val itemList = result.data?.search?.onSearchSuccess?.edges ?: listOf()
|
||||
|
||||
val cardsData = itemList.map {
|
||||
SavedItemCardData(
|
||||
id = it.node.id,
|
||||
savedItemId = it.node.id,
|
||||
slug = it.node.slug,
|
||||
publisherURLString = it.node.originalArticleUrl,
|
||||
title = it.node.title,
|
||||
|
|
|
|||
|
|
@ -2,19 +2,24 @@ package app.omnivore.omnivore.persistence
|
|||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemDao
|
||||
import app.omnivore.omnivore.persistence.entities.Viewer
|
||||
import app.omnivore.omnivore.persistence.entities.ViewerDao
|
||||
import app.omnivore.omnivore.persistence.entities.*
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
Viewer::class,
|
||||
SavedItem::class
|
||||
SavedItem::class,
|
||||
SavedItemLabel::class,
|
||||
Highlight::class,
|
||||
SavedItemAndSavedItemLabelCrossRef::class,
|
||||
SavedItemAndHighlightCrossRef::class
|
||||
],
|
||||
version = 1
|
||||
version = 2
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun viewerDao(): ViewerDao
|
||||
abstract fun savedItemDao(): SavedItemDao
|
||||
abstract fun highlightDao(): HighlightDao
|
||||
abstract fun savedItemLabelDao(): SavedItemLabelDao
|
||||
abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao
|
||||
abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,92 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.time.LocalDate
|
||||
import java.util.Date
|
||||
import androidx.room.*
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
|
||||
|
||||
@Entity
|
||||
data class Highlight(
|
||||
@PrimaryKey val id: String,
|
||||
val annotation: String?,
|
||||
val createdAt: Date?,
|
||||
@PrimaryKey val highlightId: String,
|
||||
var annotation: String?,
|
||||
val createdAt: String?,
|
||||
val createdByMe: Boolean,
|
||||
val markedForDeletion: Boolean, // default false
|
||||
val patch: String,
|
||||
val prefix: String?,
|
||||
val quote: String,
|
||||
val serverSyncStatus: Int, // default 0
|
||||
val shortId: String,
|
||||
val markedForDeletion: Boolean = false,
|
||||
var patch: String,
|
||||
var prefix: String?,
|
||||
var quote: String,
|
||||
var serverSyncStatus: Int = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
var shortId: String,
|
||||
val suffix: String?,
|
||||
val updatedAt: LocalDate?
|
||||
val updatedAt: String?
|
||||
|
||||
// has many SavedItemLabels (inverse: labels have many highlights)
|
||||
// has one savedItem (inverse: savedItem has many highlights
|
||||
// has a UserProfile (no inverse)
|
||||
)
|
||||
|
||||
@Entity(
|
||||
primaryKeys = ["highlightId", "savedItemId"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Highlight::class,
|
||||
parentColumns = arrayOf("highlightId"),
|
||||
childColumns = arrayOf("highlightId"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = SavedItem::class,
|
||||
parentColumns = arrayOf("savedItemId"),
|
||||
childColumns = arrayOf("savedItemId"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class SavedItemAndHighlightCrossRef(
|
||||
val highlightId: String,
|
||||
val savedItemId: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface SavedItemAndHighlightCrossRefDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemAndHighlightCrossRef>)
|
||||
|
||||
@Query("SELECT savedItemId FROM savedItemAndHighlightCrossRef WHERE highlightId = :highlightId")
|
||||
fun associatedSavedItemID(highlightId: String): String?
|
||||
}
|
||||
|
||||
data class SavedItemWithLabelsAndHighlights(
|
||||
@Embedded val savedItem: SavedItem,
|
||||
|
||||
@Relation(
|
||||
parentColumn = "savedItemId",
|
||||
entityColumn = "savedItemLabelId",
|
||||
associateBy = Junction(SavedItemAndSavedItemLabelCrossRef::class)
|
||||
)
|
||||
val labels: List<SavedItemLabel>,
|
||||
|
||||
@Relation(
|
||||
parentColumn = "savedItemId",
|
||||
entityColumn = "highlightId",
|
||||
associateBy = Junction(SavedItemAndHighlightCrossRef::class)
|
||||
)
|
||||
val highlights: List<Highlight>
|
||||
)
|
||||
|
||||
@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?
|
||||
|
||||
@Update
|
||||
fun update(highlight: Highlight)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import androidx.room.PrimaryKey
|
|||
|
||||
@Entity
|
||||
data class NewsletterEmail(
|
||||
@PrimaryKey val userID: String,
|
||||
@PrimaryKey val newsletterEmailId: String?,
|
||||
val confirmationCode: String?,
|
||||
val email: String?,
|
||||
val emailID: String?
|
||||
val email: String?
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.room.PrimaryKey
|
|||
|
||||
@Entity
|
||||
data class RecentSearchItem(
|
||||
@PrimaryKey val id: String,
|
||||
@PrimaryKey val recentSearchItemId: String,
|
||||
val savedAt: String?,
|
||||
val term: String?
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.room.PrimaryKey
|
|||
|
||||
@Entity
|
||||
data class RecommendationGroup(
|
||||
@PrimaryKey val id: String,
|
||||
@PrimaryKey val recommendationGroupId: String,
|
||||
val name: String?,
|
||||
val canPost: Boolean,
|
||||
val canSeeMembers: Boolean,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import app.omnivore.omnivore.persistence.BaseDao
|
||||
import java.util.*
|
||||
|
||||
@Entity
|
||||
data class SavedItem(
|
||||
@PrimaryKey val id: String,
|
||||
@PrimaryKey val savedItemId: String,
|
||||
val title: String,
|
||||
val createdAt: String,
|
||||
val savedAt: String,
|
||||
val readAt: String?,
|
||||
val updatedAt: String?,
|
||||
val readingProgress: Double,
|
||||
val readingProgressAnchor: Int,
|
||||
var readingProgress: Double,
|
||||
var readingProgressAnchor: Int,
|
||||
val imageURLString: String?,
|
||||
val pageURLString: String,
|
||||
val descriptionText: String?,
|
||||
|
|
@ -22,7 +23,7 @@ data class SavedItem(
|
|||
val author: String?,
|
||||
val publishDate: String?,
|
||||
val slug: String,
|
||||
val isArchived: Boolean,
|
||||
var isArchived: Boolean,
|
||||
val contentReader: String? = null,
|
||||
val content: String? = null,
|
||||
val createdId: String? = null,
|
||||
|
|
@ -35,7 +36,7 @@ data class SavedItem(
|
|||
val onDeviceImageURLString: String? = null,
|
||||
val originalHtml: String? = null,
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) val pdfData: ByteArray? = null,
|
||||
val serverSyncStatus: Int = 0, // TODO: implement,
|
||||
var serverSyncStatus: Int = 0,
|
||||
val tempPDFURL: String? = null
|
||||
|
||||
// hasMany highlights
|
||||
|
|
@ -46,43 +47,24 @@ data class SavedItem(
|
|||
return publisherURLString?.toUri()?.host
|
||||
}
|
||||
|
||||
fun isPDF(): Boolean {
|
||||
val hasPDFSuffix = pageURLString.endsWith("pdf")
|
||||
return contentReader == "PDF" || hasPDFSuffix
|
||||
}
|
||||
|
||||
fun asSavedItemCardData(): SavedItemCardData {
|
||||
return SavedItemCardData(
|
||||
id = id,
|
||||
slug = slug,
|
||||
publisherURLString = publisherURLString,
|
||||
title = title,
|
||||
author = author,
|
||||
imageURLString = imageURLString,
|
||||
isArchived = isArchived,
|
||||
pageURLString = pageURLString,
|
||||
contentReader = contentReader,
|
||||
)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SavedItem
|
||||
|
||||
if (id != other.id) return false
|
||||
if (savedItemId != other.savedItemId) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
return savedItemId.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
data class SavedItemCardData(
|
||||
val id: String,
|
||||
val savedItemId: String,
|
||||
val slug: String,
|
||||
val publisherURLString: String?,
|
||||
val title: String,
|
||||
|
|
@ -104,12 +86,34 @@ data class SavedItemCardData(
|
|||
|
||||
@Dao
|
||||
interface SavedItemDao {
|
||||
@Query("SELECT id, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem")
|
||||
fun getLibraryData(): List<SavedItemCardData>
|
||||
@Query("SELECT savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem ORDER BY savedAt DESC")
|
||||
fun getLibraryLiveData(): LiveData<List<SavedItemCardData>>
|
||||
|
||||
@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?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItem>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(item: SavedItem)
|
||||
|
||||
@Query("DELETE FROM savedItem WHERE savedItemId = :itemID")
|
||||
fun deleteById(itemID: String)
|
||||
|
||||
@Update
|
||||
fun update(savedItem: SavedItem)
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT savedItemId, slug, publisherURLString, title, author, imageURLString, isArchived, pageURLString, contentReader FROM SavedItem ORDER BY savedAt DESC")
|
||||
fun getLibraryLiveDataWithLabels(): LiveData<List<SavedItemCardDataWithLabels>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.*
|
||||
|
||||
@Entity
|
||||
data class SavedItemLabel(
|
||||
@PrimaryKey val id: String,
|
||||
@PrimaryKey val savedItemLabelId: String,
|
||||
val name: String,
|
||||
val color: String,
|
||||
val createdAt: String?,
|
||||
|
|
@ -13,5 +12,59 @@ data class SavedItemLabel(
|
|||
val serverSyncStatus: Int = 0
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface SavedItemLabelDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: 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"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class SavedItemAndSavedItemLabelCrossRef(
|
||||
val savedItemLabelId: String,
|
||||
val savedItemId: String
|
||||
)
|
||||
|
||||
data class SavedItemWithLabels(
|
||||
@Embedded val savedItem: SavedItem,
|
||||
@Relation(
|
||||
parentColumn = "savedItemId",
|
||||
entityColumn = "savedItemLabelId",
|
||||
associateBy = Junction(SavedItemAndSavedItemLabelCrossRef::class)
|
||||
)
|
||||
val labels: List<SavedItemLabel>
|
||||
)
|
||||
|
||||
data class SavedItemCardDataWithLabels(
|
||||
@Embedded val cardData: SavedItemCardData,
|
||||
@Relation(
|
||||
parentColumn = "savedItemId",
|
||||
entityColumn = "savedItemLabelId",
|
||||
associateBy = Junction(SavedItemAndSavedItemLabelCrossRef::class)
|
||||
)
|
||||
val labels: List<SavedItemLabel>
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface SavedItemAndSavedItemLabelCrossRefDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||
}
|
||||
|
||||
// has many highlights
|
||||
// has many savedItems
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemAndSavedItemLabelCrossRef
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels
|
||||
import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard
|
||||
import app.omnivore.omnivore.ui.reader.PDFReaderActivity
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -71,7 +73,9 @@ fun LibraryViewContent(
|
|||
onRefresh = { libraryViewModel.refresh() }
|
||||
)
|
||||
|
||||
val cardsData: List<SavedItemCardData> by libraryViewModel.itemsLiveData.observeAsState(listOf())
|
||||
val cardsData: List<SavedItemCardDataWithLabels> by libraryViewModel.itemsLiveData.observeAsState(listOf())
|
||||
val searchedCardsData: List<SavedItemCardDataWithLabels> by libraryViewModel.searchItemsLiveData.observeAsState(listOf())
|
||||
val searchText: String by libraryViewModel.searchTextLiveData.observeAsState("")
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -87,19 +91,19 @@ fun LibraryViewContent(
|
|||
.fillMaxSize()
|
||||
.padding(horizontal = 6.dp)
|
||||
) {
|
||||
items(cardsData) { cardData ->
|
||||
items(if (searchText.isNotEmpty()) searchedCardsData else cardsData) { cardDataWithLabels ->
|
||||
SavedItemCard(
|
||||
cardData = cardData,
|
||||
cardData = cardDataWithLabels.cardData,
|
||||
onClickHandler = {
|
||||
if (cardData.isPDF()) {
|
||||
if (cardDataWithLabels.cardData.isPDF()) {
|
||||
val intent = Intent(context, PDFReaderActivity::class.java)
|
||||
intent.putExtra("SAVED_ITEM_SLUG", cardData.slug)
|
||||
intent.putExtra("SAVED_ITEM_SLUG", cardDataWithLabels.cardData.slug)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
navController.navigate("WebReader/${cardData.slug}")
|
||||
navController.navigate("WebReader/${cardDataWithLabels.cardData.slug}")
|
||||
}
|
||||
},
|
||||
actionHandler = { libraryViewModel.handleSavedItemAction(cardData.id, it) }
|
||||
actionHandler = { libraryViewModel.handleSavedItemAction(cardDataWithLabels.cardData.savedItemId, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,24 +7,23 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.DataService
|
||||
import app.omnivore.omnivore.*
|
||||
import app.omnivore.omnivore.dataService.*
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.*
|
||||
import java.time.LocalDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LibraryViewModel @Inject constructor(
|
||||
private val networker: Networker,
|
||||
private val dataService: DataService
|
||||
private val dataService: DataService,
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
): ViewModel() {
|
||||
private var cursor: String? = null
|
||||
private var items: List<SavedItemCardData> = listOf()
|
||||
private var searchedItems: List<SavedItemCardData> = listOf()
|
||||
|
||||
// These are used to make sure we handle search result
|
||||
// responses in the right order
|
||||
|
|
@ -33,14 +32,16 @@ class LibraryViewModel @Inject constructor(
|
|||
|
||||
// Live Data
|
||||
val searchTextLiveData = MutableLiveData("")
|
||||
val itemsLiveData = MutableLiveData<List<SavedItemCardData>>(listOf())
|
||||
val searchItemsLiveData = MutableLiveData<List<SavedItemCardDataWithLabels>>(listOf())
|
||||
val itemsLiveData = dataService.db.savedItemDao().getLibraryLiveDataWithLabels()
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
|
||||
fun updateSearchText(text: String) {
|
||||
searchTextLiveData.value = text
|
||||
|
||||
if (text == "") {
|
||||
itemsLiveData.value = items
|
||||
searchItemsLiveData.value = listOf()
|
||||
} else {
|
||||
load(clearPreviousSearch = true)
|
||||
}
|
||||
|
|
@ -51,96 +52,113 @@ class LibraryViewModel @Inject constructor(
|
|||
load(true)
|
||||
}
|
||||
|
||||
fun getLastSyncTime(): LocalDateTime? = runBlocking {
|
||||
datastoreRepo.getString(DatastoreKeys.libraryLastSyncTimestamp)?.let {
|
||||
LocalDateTime.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun load(clearPreviousSearch: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
if (searchTextLiveData.value != "") {
|
||||
performSearch(clearPreviousSearch)
|
||||
} else {
|
||||
syncItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun syncItems() {
|
||||
val syncStart = LocalDateTime.now()
|
||||
val lastSyncDate = getLastSyncTime() ?: LocalDateTime.MIN
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
performItemSync(cursor = null, since = lastSyncDate.toString(), count = 0, startTime = syncStart.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performItemSync(cursor: String?, since: String, count: Int, startTime: String, isInitialBatch: Boolean = true) {
|
||||
dataService.syncOfflineItemsWithServerIfNeeded()
|
||||
val result = dataService.sync(since = since, cursor = cursor, limit = 20)
|
||||
|
||||
// Fetch content for the initial batch only
|
||||
if (isInitialBatch) {
|
||||
for (slug in result.savedItemSlugs) {
|
||||
dataService.syncSavedItemContent(slug)
|
||||
}
|
||||
}
|
||||
|
||||
val totalCount = count + result.count
|
||||
|
||||
Log.d("sync", "fetched ${result.count} items")
|
||||
|
||||
if (!result.hasError && result.hasMoreItems && result.cursor != null) {
|
||||
performItemSync(
|
||||
cursor = result.cursor,
|
||||
since = since,
|
||||
count = totalCount,
|
||||
startTime = startTime,
|
||||
isInitialBatch = false
|
||||
)
|
||||
} else {
|
||||
datastoreRepo.putString(DatastoreKeys.libraryLastSyncTimestamp, startTime)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun performSearch(clearPreviousSearch: Boolean) {
|
||||
if (clearPreviousSearch) {
|
||||
cursor = null
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val thisSearchIdx = searchIdx
|
||||
searchIdx += 1
|
||||
val thisSearchIdx = searchIdx
|
||||
searchIdx += 1
|
||||
|
||||
// Execute the search
|
||||
val searchResult =
|
||||
if (searchTextLiveData.value != "") {
|
||||
networker.typeaheadSearch(searchTextLiveData.value ?: "")
|
||||
} else {
|
||||
networker.search(cursor = cursor, query = searchQuery())
|
||||
}
|
||||
// Execute the search
|
||||
val searchResult = networker.typeaheadSearch(searchTextLiveData.value ?: "")
|
||||
|
||||
// Search results aren't guaranteed to return in order so this
|
||||
// will discard old results that are returned while a user is typing.
|
||||
// For example if a user types 'Canucks', often the search results
|
||||
// for 'C' are returned after 'Canucks' because it takes the backend
|
||||
// much longer to compute.
|
||||
if (thisSearchIdx in 1..receivedIdx) {
|
||||
return@launch
|
||||
}
|
||||
// Search results aren't guaranteed to return in order so this
|
||||
// will discard old results that are returned while a user is typing.
|
||||
// For example if a user types 'Canucks', often the search results
|
||||
// for 'C' are returned after 'Canucks' because it takes the backend
|
||||
// much longer to compute.
|
||||
if (thisSearchIdx in 1..receivedIdx) {
|
||||
return
|
||||
}
|
||||
|
||||
receivedIdx = thisSearchIdx
|
||||
cursor = searchResult.cursor
|
||||
val cardsDataWithLabels = searchResult.cardsData.map {
|
||||
SavedItemCardDataWithLabels(cardData = it, labels = listOf())
|
||||
}
|
||||
|
||||
if (searchTextLiveData.value != "" || clearPreviousSearch) {
|
||||
val previousItems = if (clearPreviousSearch) listOf() else searchedItems
|
||||
searchedItems = previousItems.plus(searchResult.cardsData)
|
||||
itemsLiveData.postValue(searchedItems)
|
||||
} else {
|
||||
items = items.plus(searchResult.cardsData)
|
||||
itemsLiveData.postValue(items)
|
||||
}
|
||||
searchItemsLiveData.postValue(cardsDataWithLabels)
|
||||
|
||||
// withContext(Dispatchers.IO) {
|
||||
// dataService.db.savedItemDao().insertAll(items)
|
||||
// val items = dataService.db.savedItemDao().getLibraryData()
|
||||
// Log.d("appDatabase", "libraryData: $items")
|
||||
// }
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
isRefreshing = false
|
||||
}
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
|
||||
when (action) {
|
||||
SavedItemAction.Delete -> {
|
||||
removeItemFromList(itemID)
|
||||
|
||||
viewModelScope.launch {
|
||||
networker.deleteSavedItem(itemID)
|
||||
dataService.deleteSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
SavedItemAction.Archive -> {
|
||||
removeItemFromList(itemID)
|
||||
viewModelScope.launch {
|
||||
networker.archiveSavedItem(itemID)
|
||||
dataService.archiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
SavedItemAction.Unarchive -> {
|
||||
removeItemFromList(itemID)
|
||||
viewModelScope.launch {
|
||||
networker.unarchiveSavedItem(itemID)
|
||||
dataService.unarchiveSavedItem(itemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeItemFromList(itemID: String) {
|
||||
itemsLiveData.value?.let {
|
||||
val newList = it.filter { item -> item.id != itemID }
|
||||
itemsLiveData.postValue(newList)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchQuery(): String {
|
||||
var query = "in:inbox sort:saved"
|
||||
|
||||
if (searchTextLiveData.value != "") {
|
||||
query = query.plus(" ${searchTextLiveData.value}")
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
||||
enum class SavedItemAction {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class PDFReaderViewModel @Inject constructor(
|
|||
currentReadingProgress = percent
|
||||
viewModelScope.launch {
|
||||
val params = ReadingProgressParams(
|
||||
id = pdfReaderParamsLiveData.value?.item?.id,
|
||||
id = pdfReaderParamsLiveData.value?.item?.savedItemId,
|
||||
readingProgressPercent = percent,
|
||||
readingProgressAnchorIndex = currentPageIndex
|
||||
)
|
||||
|
|
@ -102,7 +102,7 @@ class PDFReaderViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun syncHighlightUpdates(newAnnotation: Annotation, quote: String, overlapIds: List<String>, note: String? = null) {
|
||||
val itemID = pdfReaderParamsLiveData.value?.item?.id ?: return
|
||||
val itemID = pdfReaderParamsLiveData.value?.item?.savedItemId ?: return
|
||||
val highlightID = UUID.randomUUID().toString()
|
||||
val shortID = UUID.randomUUID().toString().replace("-","").substring(0,8)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun ArticleWebView(slug: String, authCookieString: String) {
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
|
||||
val url = BuildConfig.OMNIVORE_WEB_URL + "/app/me/$slug"
|
||||
|
||||
AndroidView(factory = {
|
||||
WebView(it).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
settings.javaScriptEnabled = true
|
||||
settings.allowContentAccess = true
|
||||
settings.allowFileAccess = true
|
||||
settings.domStorageEnabled = true
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
}
|
||||
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)
|
||||
CookieManager.getInstance().setAcceptCookie(true)
|
||||
CookieManager.getInstance().setCookie(BuildConfig.OMNIVORE_API_URL, authCookieString)
|
||||
CookieManager.getInstance().setCookie(BuildConfig.OMNIVORE_WEB_URL, authCookieString) {
|
||||
loadUrl(url)
|
||||
}
|
||||
}
|
||||
}, update = {
|
||||
it.loadUrl(url)
|
||||
})
|
||||
}
|
||||
|
|
@ -10,11 +10,14 @@ import android.view.*
|
|||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ModalBottomSheetValue
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
|
|
@ -29,6 +32,7 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.save.SaveSheetActivityBase
|
||||
import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -40,11 +44,14 @@ import kotlin.math.roundToInt
|
|||
|
||||
@Composable
|
||||
fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewModel) {
|
||||
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
var showWebPreferencesDialog by remember { mutableStateOf(false ) }
|
||||
|
||||
val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null)
|
||||
val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null)
|
||||
val shouldPopView: Boolean by webReaderViewModel.shouldPopViewLiveData.observeAsState(false)
|
||||
|
||||
val maxToolbarHeight = 48.dp
|
||||
val maxToolbarHeightPx = with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() }
|
||||
|
|
@ -97,12 +104,12 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod
|
|||
title = {},
|
||||
actions = {
|
||||
// Disabling menu until we implement local persistence
|
||||
// IconButton(onClick = { isMenuExpanded = true }) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Filled.Menu,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
IconButton(onClick = { isMenuExpanded = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Menu,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { showWebPreferencesDialog = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Settings, // TODO: set a better icon
|
||||
|
|
@ -113,7 +120,7 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod
|
|||
isExpanded = isMenuExpanded,
|
||||
isArchived = webReaderParams!!.item.isArchived,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
actionHandler = { webReaderViewModel.handleSavedItemAction(webReaderParams!!.item.id, it) }
|
||||
actionHandler = { webReaderViewModel.handleSavedItemAction(webReaderParams!!.item.savedItemId, it) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -139,6 +146,12 @@ fun WebReaderLoadingContainer(slug: String, webReaderViewModel: WebReaderViewMod
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldPopView) {
|
||||
if (shouldPopView) {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: add a proper loading view
|
||||
Text("Loading...")
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ data class WebReaderContent(
|
|||
}
|
||||
|
||||
window.omnivoreArticle = {
|
||||
id: "${item.id}",
|
||||
linkId: "${item.id}",
|
||||
id: "${item.savedItemId}",
|
||||
linkId: "${item.savedItemId}",
|
||||
slug: "${item.slug}",
|
||||
createdAt: new Date(1662571290735.0).toISOString(),
|
||||
savedAt: new Date(1662571290981.0).toISOString(),
|
||||
|
|
|
|||
|
|
@ -7,15 +7,13 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.dataService.*
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.networking.*
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -31,6 +29,7 @@ data class AnnotationWebViewMessage(
|
|||
@HiltViewModel
|
||||
class WebReaderViewModel @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
private val dataService: DataService,
|
||||
private val networker: Networker
|
||||
): ViewModel() {
|
||||
var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID()
|
||||
|
|
@ -41,56 +40,87 @@ class WebReaderViewModel @Inject constructor(
|
|||
val webReaderParamsLiveData = MutableLiveData<WebReaderParams?>(null)
|
||||
val annotationLiveData = MutableLiveData<String?>(null)
|
||||
val javascriptActionLoopUUIDLiveData = MutableLiveData(lastJavascriptActionLoopUUID)
|
||||
val shouldPopViewLiveData = MutableLiveData<Boolean>(false)
|
||||
|
||||
var hasTappedExistingHighlight = false
|
||||
var lastTapCoordinates: TapCoordinates? = null
|
||||
|
||||
|
||||
fun loadItem(slug: String) {
|
||||
viewModelScope.launch {
|
||||
val articleQueryResult = networker.savedItem(slug)
|
||||
val webReaderParams = loadItemFromServer(slug)
|
||||
|
||||
val article = articleQueryResult.item ?: return@launch
|
||||
|
||||
val articleContent = ArticleContent(
|
||||
title = article.title,
|
||||
htmlContent = article.content ?: "",
|
||||
highlights = articleQueryResult.highlights,
|
||||
contentStatus = "SUCCEEDED",
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(articleQueryResult.labels)
|
||||
)
|
||||
|
||||
webReaderParamsLiveData.value = WebReaderParams(article, articleContent)
|
||||
if (webReaderParams != null) {
|
||||
Log.d("sync", "data loaded from server")
|
||||
webReaderParamsLiveData.postValue(webReaderParams)
|
||||
} else {
|
||||
loadItemFromDB(slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadItemFromDB(slug: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val persistedItem = dataService.db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
|
||||
|
||||
if (persistedItem?.savedItem?.content != null) {
|
||||
val articleContent = ArticleContent(
|
||||
title = persistedItem.savedItem.title,
|
||||
htmlContent = persistedItem.savedItem.content,
|
||||
highlights = persistedItem.highlights,
|
||||
contentStatus = "SUCCEEDED",
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(persistedItem.labels)
|
||||
)
|
||||
|
||||
Log.d("sync", "data loaded from db")
|
||||
webReaderParamsLiveData.postValue(WebReaderParams(persistedItem.savedItem, articleContent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadItemFromServer(slug: String): WebReaderParams? {
|
||||
val articleQueryResult = networker.savedItem(slug)
|
||||
|
||||
val article = articleQueryResult.item ?: return null
|
||||
|
||||
val articleContent = ArticleContent(
|
||||
title = article.title,
|
||||
htmlContent = article.content ?: "",
|
||||
highlights = articleQueryResult.highlights,
|
||||
contentStatus = "SUCCEEDED",
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(articleQueryResult.labels)
|
||||
)
|
||||
|
||||
return WebReaderParams(article, articleContent)
|
||||
}
|
||||
|
||||
fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
|
||||
when (action) {
|
||||
SavedItemAction.Delete -> {
|
||||
viewModelScope.launch {
|
||||
networker.deleteSavedItem(itemID)
|
||||
popToLibraryView(itemID)
|
||||
dataService.deleteSavedItem(itemID)
|
||||
popToLibraryView()
|
||||
}
|
||||
}
|
||||
SavedItemAction.Archive -> {
|
||||
viewModelScope.launch {
|
||||
networker.archiveSavedItem(itemID)
|
||||
popToLibraryView(itemID)
|
||||
dataService.archiveSavedItem(itemID)
|
||||
popToLibraryView()
|
||||
}
|
||||
}
|
||||
SavedItemAction.Unarchive -> {
|
||||
viewModelScope.launch {
|
||||
networker.unarchiveSavedItem(itemID)
|
||||
popToLibraryView(itemID)
|
||||
dataService.unarchiveSavedItem(itemID)
|
||||
popToLibraryView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun popToLibraryView(itemID: String) {
|
||||
private fun popToLibraryView() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
// TODO: pop to library
|
||||
Log.d("maxx", "should pop to library and remove item with ID: $itemID")
|
||||
shouldPopViewLiveData.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,28 +128,24 @@ class WebReaderViewModel @Inject constructor(
|
|||
when (actionID) {
|
||||
"createHighlight" -> {
|
||||
viewModelScope.launch {
|
||||
val isHighlightSynced = networker.createWebHighlight(jsonString)
|
||||
Log.d("Network", "isHighlightSynced = $isHighlightSynced")
|
||||
dataService.createWebHighlight(jsonString)
|
||||
}
|
||||
}
|
||||
"deleteHighlight" -> {
|
||||
Log.d("Loggo", "receive delete highlight action: $jsonString")
|
||||
viewModelScope.launch {
|
||||
val isHighlightDeletionSynced = networker.deleteHighlight(jsonString)
|
||||
Log.d("Network", "isHighlightDeletionSynced = $isHighlightDeletionSynced")
|
||||
dataService.deleteHighlights(jsonString)
|
||||
}
|
||||
}
|
||||
"updateHighlight" -> {
|
||||
Log.d("Loggo", "receive update highlight action: $jsonString")
|
||||
viewModelScope.launch {
|
||||
val isHighlightUpdateSynced = networker.updateWebHighlight(jsonString)
|
||||
Log.d("Network", "isHighlightUpdateSynced = $isHighlightUpdateSynced")
|
||||
dataService.updateWebHighlight(jsonString)
|
||||
}
|
||||
}
|
||||
"articleReadingProgress" -> {
|
||||
viewModelScope.launch {
|
||||
val isReadingProgressSynced = networker.updateWebReadingProgress(jsonString)
|
||||
Log.d("Network", "isReadingProgressSynced = $isReadingProgressSynced")
|
||||
dataService.updateWebReadingProgress(jsonString)
|
||||
}
|
||||
}
|
||||
"annotate" -> {
|
||||
|
|
@ -135,8 +161,7 @@ class WebReaderViewModel @Inject constructor(
|
|||
}
|
||||
"mergeHighlight" -> {
|
||||
viewModelScope.launch {
|
||||
val isHighlightSynced = networker.mergeWebHighlights(jsonString)
|
||||
Log.d("Network", "isMergedHighlightSynced = $isHighlightSynced")
|
||||
dataService.mergeWebHighlights(jsonString)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -146,6 +171,7 @@ class WebReaderViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun reset() {
|
||||
shouldPopViewLiveData.postValue(false)
|
||||
webReaderParamsLiveData.value = null
|
||||
annotationLiveData.value = null
|
||||
scrollState = ScrollState(0)
|
||||
|
|
|
|||
|
|
@ -79,14 +79,6 @@ fun PrimaryNavigator(
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: delete this route and views
|
||||
composable("WebAppReader/{slug}") {
|
||||
ArticleWebView(
|
||||
it.arguments?.getString("slug") ?: "",
|
||||
authCookieString = loginViewModel.getAuthCookieString() ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
composable("WebReader/{slug}") {
|
||||
webReaderViewModel.reset() // clear previously loaded item
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue