Save Android offline content to files instead of in DB
This works around max size constraints in the Room DB, we will need to add something to clean up old files though.
This commit is contained in:
@ -6,6 +6,8 @@ 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.loadLibraryItemContent
|
||||
import app.omnivore.omnivore.core.network.saveLibraryItemContentToFile
|
||||
import app.omnivore.omnivore.core.network.savedItem
|
||||
import app.omnivore.omnivore.core.network.savedItemUpdates
|
||||
import app.omnivore.omnivore.core.network.search
|
||||
@ -46,6 +48,7 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
|
||||
}
|
||||
|
||||
val savedItems = syncResult.items.map {
|
||||
saveLibraryItemContentToFile(it.id, it.content)
|
||||
val savedItem = SavedItem(
|
||||
savedItemId = it.id,
|
||||
title = it.title,
|
||||
@ -67,7 +70,6 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
|
||||
isArchived = it.isArchived,
|
||||
contentReader = it.contentReader.rawValue,
|
||||
wordsCount = it.wordsCount,
|
||||
content = it.content
|
||||
)
|
||||
val labels = it.labels?.map { label ->
|
||||
SavedItemLabel(
|
||||
@ -116,8 +118,11 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
|
||||
|
||||
suspend fun DataService.isSavedItemContentStoredInDB(slug: String): Boolean {
|
||||
val existingItem = db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
|
||||
val content = existingItem?.savedItem?.content ?: ""
|
||||
return content.length > 10
|
||||
existingItem?.savedItem?.savedItemId?.let { savedItemId ->
|
||||
val htmlContent = loadLibraryItemContent(savedItemId)
|
||||
return (htmlContent ?: "").length > 10
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun DataService.fetchSavedItemContent(slug: String) {
|
||||
|
||||
@ -27,7 +27,9 @@ 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.loadLibraryItemContent
|
||||
import app.omnivore.omnivore.core.network.mergeHighlights
|
||||
import app.omnivore.omnivore.core.network.saveLibraryItemContentToFile
|
||||
import app.omnivore.omnivore.core.network.savedItem
|
||||
import app.omnivore.omnivore.core.network.savedItemLabels
|
||||
import app.omnivore.omnivore.core.network.savedItemUpdates
|
||||
@ -219,8 +221,11 @@ class LibraryRepositoryImpl @Inject constructor(
|
||||
|
||||
override suspend fun isSavedItemContentStoredInDB(slug: String): Boolean {
|
||||
val existingItem = savedItemDao.getSavedItemWithLabelsAndHighlights(slug)
|
||||
val content = existingItem?.savedItem?.content ?: ""
|
||||
return content.length > 10
|
||||
existingItem?.savedItem?.savedItemId?.let { savedItemId ->
|
||||
val htmlContent = loadLibraryItemContent(savedItemId)
|
||||
return (htmlContent ?: "").length > 10
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override suspend fun deleteSavedItem(itemID: String) {
|
||||
@ -416,6 +421,7 @@ class LibraryRepositoryImpl @Inject constructor(
|
||||
}
|
||||
|
||||
val savedItems = syncResult.items.map {
|
||||
saveLibraryItemContentToFile(it.id, it.content)
|
||||
val savedItem = SavedItem(
|
||||
savedItemId = it.id,
|
||||
title = it.title,
|
||||
@ -436,7 +442,6 @@ class LibraryRepositoryImpl @Inject constructor(
|
||||
slug = it.slug,
|
||||
isArchived = it.isArchived,
|
||||
contentReader = it.contentReader.rawValue,
|
||||
content = it.content,
|
||||
wordsCount = it.wordsCount
|
||||
)
|
||||
val labels = it.labels?.map { label ->
|
||||
|
||||
@ -27,7 +27,7 @@ import app.omnivore.omnivore.core.database.entities.ViewerDao
|
||||
HighlightChange::class,
|
||||
SavedItemAndSavedItemLabelCrossRef::class,
|
||||
SavedItemAndHighlightCrossRef::class],
|
||||
version = 27,
|
||||
version = 28,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class OmnivoreDatabase : RoomDatabase() {
|
||||
|
||||
@ -26,9 +26,7 @@ data class SavedItem(
|
||||
val slug: String,
|
||||
var isArchived: Boolean,
|
||||
val contentReader: String? = null,
|
||||
val content: String? = null,
|
||||
val createdId: String? = null,
|
||||
val htmlContent: String? = null,
|
||||
val language: String? = null,
|
||||
val listenPositionIndex: Int? = null,
|
||||
val listenPositionOffset: Double? = null,
|
||||
|
||||
@ -80,6 +80,8 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
|
||||
localPDFPath = localFile.toPath().toString()
|
||||
}
|
||||
|
||||
saveLibraryItemContentToFile(article.articleFields.id, article.articleFields.content)
|
||||
|
||||
val savedItem = SavedItem(
|
||||
savedItemId = article.articleFields.id,
|
||||
title = article.articleFields.title,
|
||||
@ -100,7 +102,6 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
|
||||
slug = article.articleFields.slug,
|
||||
isArchived = article.articleFields.isArchived,
|
||||
contentReader = article.articleFields.contentReader.rawValue,
|
||||
content = article.articleFields.content,
|
||||
wordsCount = article.articleFields.wordsCount,
|
||||
localPDFPath = localPDFPath
|
||||
)
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import android.os.Environment
|
||||
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.graphql.generated.SearchQuery
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
|
||||
data class LibrarySearchQueryResponse(
|
||||
val cursor: String?, val items: List<LibrarySearchItem>
|
||||
@ -31,6 +36,7 @@ suspend fun Networker.search(
|
||||
val itemList = result.data?.search?.onSearchSuccess?.edges ?: listOf()
|
||||
|
||||
val searchItems = itemList.map {
|
||||
saveLibraryItemContentToFile(it.node.id, it.node.content)
|
||||
LibrarySearchItem(item = SavedItem(
|
||||
savedItemId = it.node.id,
|
||||
title = it.node.title,
|
||||
@ -51,7 +57,6 @@ suspend fun Networker.search(
|
||||
slug = it.node.slug,
|
||||
isArchived = it.node.isArchived,
|
||||
contentReader = it.node.contentReader.rawValue,
|
||||
content = it.node.content,
|
||||
wordsCount = it.node.wordsCount
|
||||
), labels = (it.node.labels ?: listOf()).map { label ->
|
||||
SavedItemLabel(
|
||||
@ -89,3 +94,33 @@ suspend fun Networker.search(
|
||||
return LibrarySearchQueryResponse(null, listOf())
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLibraryItemContentToFile(libraryItemId: String, content: String?): Boolean {
|
||||
return try {
|
||||
content?.let { content ->
|
||||
val directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
|
||||
val file = File(directory, "${libraryItemId}.html")
|
||||
FileOutputStream(file).use { it.write(content.toByteArray()) }
|
||||
return false
|
||||
}
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLibraryItemContent(libraryItemId: String): String? {
|
||||
return try {
|
||||
val directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
|
||||
val file = File(directory, "${libraryItemId}.html")
|
||||
if (file.exists()) {
|
||||
return FileInputStream(file).bufferedReader().use { it.readText() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +73,6 @@ class PDFReaderViewModel @Inject constructor(
|
||||
htmlContent = "",
|
||||
highlights = item.highlights,
|
||||
contentStatus = "SUCCEEDED",
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(item.labels)
|
||||
)
|
||||
|
||||
@ -103,10 +102,9 @@ class PDFReaderViewModel @Inject constructor(
|
||||
override fun onComplete(output: File) {
|
||||
val articleContent = ArticleContent(
|
||||
title = article.title,
|
||||
htmlContent = article.content ?: "",
|
||||
htmlContent = "",
|
||||
highlights = articleQueryResult.highlights,
|
||||
contentStatus = "SUCCEEDED",
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(articleQueryResult.labels)
|
||||
)
|
||||
|
||||
|
||||
@ -37,7 +37,6 @@ data class ArticleContent(
|
||||
val htmlContent: String,
|
||||
val highlights: List<Highlight>,
|
||||
val contentStatus: String, // ArticleContentStatus,
|
||||
val objectID: String?, // whatever the Room Equivalent of objectID is
|
||||
val labelsJSONString: String
|
||||
) {
|
||||
fun highlightsJSONString(): String {
|
||||
|
||||
@ -39,6 +39,7 @@ import app.omnivore.omnivore.core.datastore.prefersWebHighContrastText
|
||||
import app.omnivore.omnivore.core.datastore.volumeForScroll
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.createNewLabel
|
||||
import app.omnivore.omnivore.core.network.loadLibraryItemContent
|
||||
import app.omnivore.omnivore.core.network.saveUrl
|
||||
import app.omnivore.omnivore.core.network.savedItem
|
||||
import app.omnivore.omnivore.feature.components.HighlightColor
|
||||
@ -263,35 +264,36 @@ class WebReaderViewModel @Inject constructor(
|
||||
|
||||
private suspend fun loadItemFromDB(slug: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val persistedItem =
|
||||
dataService.db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
|
||||
val persistedItem = dataService.db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
|
||||
val savedItemId = persistedItem?.savedItem?.savedItemId
|
||||
if (savedItemId != null) {
|
||||
val htmlContent = loadLibraryItemContent(savedItemId)
|
||||
if (htmlContent != null) {
|
||||
val articleContent = ArticleContent(
|
||||
title = persistedItem.savedItem.title,
|
||||
htmlContent = htmlContent,
|
||||
highlights = persistedItem.highlights,
|
||||
contentStatus = "SUCCEEDED",
|
||||
labelsJSONString = Gson().toJson(persistedItem.labels)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
val webReaderParams = WebReaderParams(
|
||||
persistedItem.savedItem,
|
||||
articleContent,
|
||||
persistedItem.labels
|
||||
)
|
||||
|
||||
val webReaderParams = WebReaderParams(
|
||||
persistedItem.savedItem,
|
||||
articleContent,
|
||||
persistedItem.labels
|
||||
)
|
||||
|
||||
Log.d("sync", "data loaded from db")
|
||||
eventTracker.track(
|
||||
"link_read",
|
||||
com.posthog.android.Properties()
|
||||
.putValue("linkID", webReaderParams.item.savedItemId)
|
||||
.putValue("slug", webReaderParams.item.slug)
|
||||
.putValue("originalArticleURL", webReaderParams.item.pageURLString)
|
||||
.putValue("loaded_from", "db")
|
||||
)
|
||||
webReaderParamsLiveData.postValue(webReaderParams)
|
||||
Log.d("sync", "data loaded from db")
|
||||
eventTracker.track(
|
||||
"link_read",
|
||||
com.posthog.android.Properties()
|
||||
.putValue("linkID", webReaderParams.item.savedItemId)
|
||||
.putValue("slug", webReaderParams.item.slug)
|
||||
.putValue("originalArticleURL", webReaderParams.item.pageURLString)
|
||||
.putValue("loaded_from", "db")
|
||||
)
|
||||
webReaderParamsLiveData.postValue(webReaderParams)
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
@ -301,13 +303,13 @@ class WebReaderViewModel @Inject constructor(
|
||||
val articleQueryResult = networker.savedItem(slug)
|
||||
|
||||
val article = articleQueryResult.item ?: return null
|
||||
val htmlContent = loadLibraryItemContent(article.savedItemId)
|
||||
|
||||
val articleContent = ArticleContent(
|
||||
title = article.title,
|
||||
htmlContent = article.content ?: "",
|
||||
htmlContent = htmlContent ?: "",
|
||||
highlights = articleQueryResult.highlights,
|
||||
contentStatus = articleQueryResult.state,
|
||||
objectID = "",
|
||||
labelsJSONString = Gson().toJson(articleQueryResult.labels)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user