Merge pull request #3549 from stefanosansone/feature/android-mark-ad-read
Android - Mark items as read/unread from library screen
This commit is contained in:
@ -118,6 +118,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.compose.ui.util)
|
implementation(libs.androidx.compose.ui.util)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
androidTestImplementation(libs.androidx.compose.ui.test)
|
androidTestImplementation(libs.androidx.compose.ui.test)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling.preview)
|
debugImplementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
@ -165,6 +166,8 @@ dependencies {
|
|||||||
implementation(libs.compose.markdown)
|
implementation(libs.compose.markdown)
|
||||||
implementation(libs.chiptextfield.m3)
|
implementation(libs.chiptextfield.m3)
|
||||||
|
|
||||||
|
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apollo {
|
apollo {
|
||||||
|
|||||||
@ -35,7 +35,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val loginViewModel: LoginViewModel by viewModels()
|
val loginViewModel: LoginViewModel by viewModels()
|
||||||
val libraryViewModel: LibraryViewModel by viewModels()
|
|
||||||
val settingsViewModel: SettingsViewModel by viewModels()
|
val settingsViewModel: SettingsViewModel by viewModels()
|
||||||
val searchViewModel: SearchViewModel by viewModels()
|
val searchViewModel: SearchViewModel by viewModels()
|
||||||
val labelsViewModel: LabelsViewModel by viewModels()
|
val labelsViewModel: LabelsViewModel by viewModels()
|
||||||
@ -65,7 +64,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
RootView(
|
RootView(
|
||||||
loginViewModel,
|
loginViewModel,
|
||||||
searchViewModel,
|
searchViewModel,
|
||||||
libraryViewModel,
|
|
||||||
settingsViewModel,
|
settingsViewModel,
|
||||||
labelsViewModel,
|
labelsViewModel,
|
||||||
saveViewModel,
|
saveViewModel,
|
||||||
|
|||||||
@ -1,38 +1,31 @@
|
|||||||
package app.omnivore.omnivore.core.data
|
package app.omnivore.omnivore.core.data
|
||||||
|
|
||||||
import android.content.Context
|
import app.omnivore.omnivore.core.database.OmnivoreDatabase
|
||||||
import androidx.room.Room
|
|
||||||
import app.omnivore.omnivore.core.network.Networker
|
|
||||||
import app.omnivore.omnivore.core.database.AppDatabase
|
|
||||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||||
import kotlinx.coroutines.*
|
import app.omnivore.omnivore.core.network.Networker
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DataService @Inject constructor(
|
class DataService @Inject constructor(
|
||||||
context: Context,
|
val networker: Networker,
|
||||||
val networker: Networker
|
omnivoreDatabase: OmnivoreDatabase
|
||||||
) {
|
) {
|
||||||
val savedItemSyncChannel = Channel<SavedItem>(capacity = Channel.UNLIMITED)
|
val savedItemSyncChannel = Channel<SavedItem>(capacity = Channel.UNLIMITED)
|
||||||
val highlightSyncChannel = Channel<Highlight>(capacity = Channel.UNLIMITED)
|
|
||||||
|
|
||||||
val db = Room.databaseBuilder(
|
val db = omnivoreDatabase
|
||||||
context,
|
|
||||||
AppDatabase::class.java, "omnivore-database"
|
|
||||||
)
|
|
||||||
.fallbackToDestructiveMigration()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
startSyncChannels()
|
startSyncChannels()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun clearDatabase() {
|
fun clearDatabase() {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
db.clearAllTables()
|
db.clearAllTables()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,176 +1,185 @@
|
|||||||
package app.omnivore.omnivore.core.data
|
package app.omnivore.omnivore.core.data
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
|
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef
|
||||||
|
import app.omnivore.omnivore.core.database.entities.saveHighlightChange
|
||||||
import app.omnivore.omnivore.core.network.CreateHighlightParams
|
import app.omnivore.omnivore.core.network.CreateHighlightParams
|
||||||
import app.omnivore.omnivore.core.network.DeleteHighlightParams
|
import app.omnivore.omnivore.core.network.DeleteHighlightParams
|
||||||
import app.omnivore.omnivore.core.network.MergeHighlightsParams
|
import app.omnivore.omnivore.core.network.MergeHighlightsParams
|
||||||
import app.omnivore.omnivore.core.network.UpdateHighlightParams
|
import app.omnivore.omnivore.core.network.UpdateHighlightParams
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
|
||||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef
|
|
||||||
import app.omnivore.omnivore.core.database.entities.saveHighlightChange
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
suspend fun DataService.createWebHighlight(jsonString: String, colorName: String?) {
|
suspend fun DataService.createWebHighlight(jsonString: String, colorName: String?) {
|
||||||
val createHighlightInput = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
|
val createHighlightInput =
|
||||||
|
Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val highlight = Highlight(
|
val highlight = Highlight(
|
||||||
type = "HIGHLIGHT",
|
type = "HIGHLIGHT",
|
||||||
highlightId = createHighlightInput.id,
|
highlightId = createHighlightInput.id,
|
||||||
shortId = createHighlightInput.shortId,
|
shortId = createHighlightInput.shortId,
|
||||||
quote = createHighlightInput.quote.getOrNull(),
|
quote = createHighlightInput.quote.getOrNull(),
|
||||||
prefix = null,
|
prefix = null,
|
||||||
suffix = null,
|
suffix = null,
|
||||||
patch = createHighlightInput.patch.getOrNull(),
|
patch = createHighlightInput.patch.getOrNull(),
|
||||||
annotation = createHighlightInput.annotation.getOrNull(),
|
annotation = createHighlightInput.annotation.getOrNull(),
|
||||||
createdAt = null,
|
createdAt = null,
|
||||||
updatedAt = null,
|
updatedAt = null,
|
||||||
createdByMe = false,
|
createdByMe = false,
|
||||||
color = colorName ?: createHighlightInput.color.getOrNull(),
|
color = colorName ?: createHighlightInput.color.getOrNull(),
|
||||||
highlightPositionPercent = createHighlightInput.highlightPositionPercent.getOrNull() ?: 0.0,
|
highlightPositionPercent = createHighlightInput.highlightPositionPercent.getOrNull()
|
||||||
highlightPositionAnchorIndex = createHighlightInput.highlightPositionAnchorIndex.getOrNull() ?: 0
|
?: 0.0,
|
||||||
)
|
highlightPositionAnchorIndex = createHighlightInput.highlightPositionAnchorIndex.getOrNull()
|
||||||
|
?: 0
|
||||||
|
)
|
||||||
|
|
||||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||||
|
|
||||||
val highlightChange = saveHighlightChange(db.highlightChangesDao(), createHighlightInput.articleId, highlight)
|
val highlightChange =
|
||||||
|
saveHighlightChange(db.highlightChangesDao(), createHighlightInput.articleId, highlight)
|
||||||
|
|
||||||
val crossRef = SavedItemAndHighlightCrossRef(
|
val crossRef = SavedItemAndHighlightCrossRef(
|
||||||
highlightId = createHighlightInput.id,
|
highlightId = createHighlightInput.id, savedItemId = createHighlightInput.articleId
|
||||||
savedItemId = createHighlightInput.articleId
|
)
|
||||||
)
|
|
||||||
|
|
||||||
db.highlightDao().insertAll(listOf(highlight))
|
db.highlightDao().insertAll(listOf(highlight))
|
||||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||||
|
|
||||||
performHighlightChange(highlightChange)
|
performHighlightChange(highlightChange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String {
|
suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String {
|
||||||
val shortId = NanoId.generate(size = 14)
|
val shortId = NanoId.generate(size = 14)
|
||||||
val createHighlightId = UUID.randomUUID().toString()
|
val createHighlightId = UUID.randomUUID().toString()
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val highlight = Highlight(
|
val highlight = Highlight(
|
||||||
type = "NOTE",
|
type = "NOTE",
|
||||||
highlightId = createHighlightId,
|
highlightId = createHighlightId,
|
||||||
shortId = shortId,
|
shortId = shortId,
|
||||||
quote = null,
|
quote = null,
|
||||||
prefix = null,
|
prefix = null,
|
||||||
suffix = null,
|
suffix = null,
|
||||||
patch =null,
|
patch = null,
|
||||||
annotation = note,
|
annotation = note,
|
||||||
createdAt = null,
|
createdAt = null,
|
||||||
updatedAt = null,
|
updatedAt = null,
|
||||||
createdByMe = true,
|
createdByMe = true,
|
||||||
color = null,
|
color = null,
|
||||||
highlightPositionAnchorIndex = 0,
|
highlightPositionAnchorIndex = 0,
|
||||||
highlightPositionPercent = 0.0
|
highlightPositionPercent = 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
|
||||||
|
|
||||||
val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
|
val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
|
||||||
|
|
||||||
val crossRef = SavedItemAndHighlightCrossRef(
|
val crossRef = SavedItemAndHighlightCrossRef(
|
||||||
highlightId = createHighlightId,
|
highlightId = createHighlightId, savedItemId = savedItemId
|
||||||
savedItemId = savedItemId
|
)
|
||||||
)
|
|
||||||
|
|
||||||
db.highlightDao().insertAll(listOf(highlight))
|
db.highlightDao().insertAll(listOf(highlight))
|
||||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||||
|
|
||||||
performHighlightChange(highlightChange)
|
performHighlightChange(highlightChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
return createHighlightId
|
return createHighlightId
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun DataService.mergeWebHighlights(jsonString: String) {
|
suspend fun DataService.mergeWebHighlights(jsonString: String) {
|
||||||
val mergeHighlightInput = Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput()
|
val mergeHighlightInput =
|
||||||
Log.d("sync", "mergeHighlightInput: " + mergeHighlightInput.id + ": " + mergeHighlightInput)
|
Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput()
|
||||||
|
Log.d("sync", "mergeHighlightInput: " + mergeHighlightInput.id + ": " + mergeHighlightInput)
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val highlight = Highlight(
|
val highlight = Highlight(
|
||||||
type = "HIGHLIGHT",
|
type = "HIGHLIGHT",
|
||||||
highlightId = mergeHighlightInput.id,
|
highlightId = mergeHighlightInput.id,
|
||||||
shortId = mergeHighlightInput.shortId,
|
shortId = mergeHighlightInput.shortId,
|
||||||
quote = mergeHighlightInput.quote,
|
quote = mergeHighlightInput.quote,
|
||||||
prefix = null,
|
prefix = null,
|
||||||
suffix = null,
|
suffix = null,
|
||||||
patch = mergeHighlightInput.patch,
|
patch = mergeHighlightInput.patch,
|
||||||
annotation = mergeHighlightInput.annotation.getOrNull(),
|
annotation = mergeHighlightInput.annotation.getOrNull(),
|
||||||
createdAt = null,
|
createdAt = null,
|
||||||
updatedAt = null,
|
updatedAt = null,
|
||||||
createdByMe = false,
|
createdByMe = false,
|
||||||
color = mergeHighlightInput.color.getOrNull(),
|
color = mergeHighlightInput.color.getOrNull(),
|
||||||
highlightPositionPercent = mergeHighlightInput.highlightPositionPercent.getOrNull() ?: 0.0,
|
highlightPositionPercent = mergeHighlightInput.highlightPositionPercent.getOrNull()
|
||||||
highlightPositionAnchorIndex = mergeHighlightInput.highlightPositionAnchorIndex.getOrNull() ?: 0
|
?: 0.0,
|
||||||
)
|
highlightPositionAnchorIndex = mergeHighlightInput.highlightPositionAnchorIndex.getOrNull()
|
||||||
|
?: 0
|
||||||
|
)
|
||||||
|
|
||||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
|
||||||
|
|
||||||
val highlightChange = saveHighlightChange(
|
val highlightChange = saveHighlightChange(
|
||||||
db.highlightChangesDao(),
|
db.highlightChangesDao(),
|
||||||
mergeHighlightInput.articleId,
|
mergeHighlightInput.articleId,
|
||||||
highlight,
|
highlight,
|
||||||
html = mergeHighlightInput.html.getOrNull(),
|
html = mergeHighlightInput.html.getOrNull(),
|
||||||
overlappingIDs = mergeHighlightInput.overlapHighlightIdList
|
overlappingIDs = mergeHighlightInput.overlapHighlightIdList
|
||||||
)
|
)
|
||||||
|
|
||||||
val crossRef = SavedItemAndHighlightCrossRef(
|
val crossRef = SavedItemAndHighlightCrossRef(
|
||||||
highlightId = mergeHighlightInput.id,
|
highlightId = mergeHighlightInput.id, savedItemId = mergeHighlightInput.articleId
|
||||||
savedItemId = mergeHighlightInput.articleId
|
)
|
||||||
)
|
|
||||||
|
|
||||||
db.highlightDao().insertAll(listOf(highlight))
|
db.highlightDao().insertAll(listOf(highlight))
|
||||||
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
|
||||||
|
|
||||||
Log.d("sync", "Setting up highlight merge")
|
Log.d("sync", "Setting up highlight merge")
|
||||||
performHighlightChange(highlightChange)
|
performHighlightChange(highlightChange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun DataService.updateWebHighlight(jsonString: String) {
|
suspend fun DataService.updateWebHighlight(jsonString: String) {
|
||||||
val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java)
|
val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java)
|
||||||
|
|
||||||
if (updateHighlightParams.highlightId == null || updateHighlightParams.libraryItemId == null) {
|
if (updateHighlightParams.highlightId == null || updateHighlightParams.libraryItemId == null) {
|
||||||
Log.d("error","ERROR INVALID HIGHLIGHT DATA")
|
Log.d("error", "ERROR INVALID HIGHLIGHT DATA")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val highlight = db.highlightDao().findById(highlightId = updateHighlightParams.highlightId ?: "") ?: return@withContext
|
val highlight =
|
||||||
|
db.highlightDao().findById(highlightId = updateHighlightParams.highlightId)
|
||||||
|
?: return@withContext
|
||||||
|
|
||||||
highlight.annotation = updateHighlightParams.annotation
|
highlight.annotation = updateHighlightParams.annotation
|
||||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||||
db.highlightDao().update(highlight)
|
db.highlightDao().update(highlight)
|
||||||
|
|
||||||
val highlightChange = saveHighlightChange(db.highlightChangesDao(), updateHighlightParams.libraryItemId ?: "", highlight)
|
val highlightChange = saveHighlightChange(
|
||||||
performHighlightChange(highlightChange)
|
db.highlightChangesDao(), updateHighlightParams.libraryItemId, highlight
|
||||||
}
|
)
|
||||||
|
performHighlightChange(highlightChange)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun DataService.deleteHighlightFromJSON(jsonString: String) {
|
suspend fun DataService.deleteHighlightFromJSON(jsonString: String) {
|
||||||
val deleteHighlightParams = Gson().fromJson(jsonString, DeleteHighlightParams::class.java)
|
val deleteHighlightParams = Gson().fromJson(jsonString, DeleteHighlightParams::class.java)
|
||||||
deleteHighlight(deleteHighlightParams.libraryItemId, deleteHighlightParams.highlightId)
|
deleteHighlight(deleteHighlightParams.libraryItemId, deleteHighlightParams.highlightId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun DataService.deleteHighlight(savedItemId: String, highlightID: String) {
|
private suspend fun DataService.deleteHighlight(savedItemId: String, highlightID: String) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val highlight = db.highlightDao().findById(highlightId = highlightID)
|
val highlight = db.highlightDao().findById(highlightId = highlightID)
|
||||||
|
|
||||||
highlight?.let {
|
highlight?.let {
|
||||||
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
|
||||||
db.highlightDao().update(highlight)
|
db.highlightDao().update(highlight)
|
||||||
|
|
||||||
val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
|
val highlightChange =
|
||||||
performHighlightChange(highlightChange)
|
saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
|
||||||
|
performHighlightChange(highlightChange)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighli
|
|||||||
import app.omnivore.omnivore.core.network.savedItem
|
import app.omnivore.omnivore.core.network.savedItem
|
||||||
import app.omnivore.omnivore.core.network.savedItemUpdates
|
import app.omnivore.omnivore.core.network.savedItemUpdates
|
||||||
import app.omnivore.omnivore.core.network.search
|
import app.omnivore.omnivore.core.network.search
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
|
|
||||||
suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult {
|
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)
|
||||||
|
|||||||
@ -1,28 +1,34 @@
|
|||||||
package app.omnivore.omnivore.core.data
|
package app.omnivore.omnivore.core.data
|
||||||
|
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
|
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||||
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||||
import app.omnivore.omnivore.core.network.updateReadingProgress
|
import app.omnivore.omnivore.core.network.updateReadingProgress
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
suspend fun DataService.updateWebReadingProgress(jsonString: String) {
|
suspend fun DataService.updateWebReadingProgress(
|
||||||
|
jsonString: String,
|
||||||
|
savedItemDao: SavedItemDao
|
||||||
|
) {
|
||||||
val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
|
val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
|
||||||
val savedItemId = readingProgressParams.id ?: return
|
val savedItemId = readingProgressParams.id ?: return
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val savedItem = db.savedItemDao().findById(savedItemId) ?: return@withContext
|
val savedItem = savedItemDao.findById(savedItemId) ?: return@withContext
|
||||||
savedItem.readingProgress = readingProgressParams.readingProgressPercent ?: 0.0
|
val updatedItem = savedItem.copy(
|
||||||
savedItem.readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0
|
readingProgress = readingProgressParams.readingProgressPercent ?: 0.0,
|
||||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0,
|
||||||
db.savedItemDao().update(savedItem)
|
serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||||
|
)
|
||||||
|
savedItemDao.update(updatedItem)
|
||||||
|
|
||||||
val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
|
val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
|
||||||
|
|
||||||
if (isUpdatedOnServer) {
|
if (isUpdatedOnServer) {
|
||||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
updatedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||||
db.savedItemDao().update(savedItem)
|
savedItemDao.update(updatedItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package app.omnivore.omnivore.core.data
|
package app.omnivore.omnivore.core.data
|
||||||
|
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
import app.omnivore.omnivore.core.network.archiveSavedItem
|
import app.omnivore.omnivore.core.network.archiveSavedItem
|
||||||
import app.omnivore.omnivore.core.network.deleteSavedItem
|
import app.omnivore.omnivore.core.network.deleteSavedItem
|
||||||
import app.omnivore.omnivore.core.network.unarchiveSavedItem
|
import app.omnivore.omnivore.core.network.unarchiveSavedItem
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
package app.omnivore.omnivore.core.data
|
package app.omnivore.omnivore.core.data
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
|
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||||
|
import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight
|
||||||
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||||
import app.omnivore.omnivore.core.network.createHighlight
|
import app.omnivore.omnivore.core.network.createHighlight
|
||||||
import app.omnivore.omnivore.core.network.deleteHighlights
|
import app.omnivore.omnivore.core.network.deleteHighlights
|
||||||
@ -13,10 +17,6 @@ import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
|||||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
|
||||||
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
|
||||||
import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight
|
|
||||||
import com.apollographql.apollo3.api.Optional
|
import com.apollographql.apollo3.api.Optional
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ suspend fun DataService.syncOfflineItemsWithServerIfNeeded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun DataService.syncSavedItem(item: SavedItem) {
|
private suspend fun DataService.syncSavedItem(item: SavedItem) {
|
||||||
fun updateSyncStatus(status: ServerSyncStatus) {
|
suspend fun updateSyncStatus(status: ServerSyncStatus) {
|
||||||
item.serverSyncStatus = status.rawValue
|
item.serverSyncStatus = status.rawValue
|
||||||
db.savedItemDao().update(item)
|
db.savedItemDao().update(item)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
package app.omnivore.omnivore.core.data.model
|
||||||
|
|
||||||
|
data class LibraryQuery(
|
||||||
|
val allowedArchiveStates: List<Int>,
|
||||||
|
val sortKey: String,
|
||||||
|
val requiredLabels: List<String>,
|
||||||
|
val excludedLabels: List<String>,
|
||||||
|
val allowedContentReaders: List<String>
|
||||||
|
)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package app.omnivore.omnivore.core.model
|
package app.omnivore.omnivore.core.data.model
|
||||||
|
|
||||||
enum class ServerSyncStatus(val rawValue: Int) {
|
enum class ServerSyncStatus(val rawValue: Int) {
|
||||||
IS_SYNCED(0),
|
IS_SYNCED(0),
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package app.omnivore.omnivore.core.data.repository
|
||||||
|
|
||||||
|
import app.omnivore.omnivore.core.data.model.LibraryQuery
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface LibraryRepository {
|
||||||
|
|
||||||
|
fun getSavedItems(query: LibraryQuery): Flow<List<SavedItemWithLabelsAndHighlights>>
|
||||||
|
|
||||||
|
suspend fun updateReadingProgress(
|
||||||
|
itemId: String,
|
||||||
|
readingProgressPercentage: Double,
|
||||||
|
readingProgressAnchorIndex: Int
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package app.omnivore.omnivore.core.data.repository.impl
|
||||||
|
|
||||||
|
import app.omnivore.omnivore.core.data.model.LibraryQuery
|
||||||
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
|
import app.omnivore.omnivore.core.data.repository.LibraryRepository
|
||||||
|
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||||
|
import app.omnivore.omnivore.core.network.Networker
|
||||||
|
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||||
|
import app.omnivore.omnivore.core.network.updateReadingProgress
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LibraryRepositoryImpl @Inject constructor(
|
||||||
|
private val savedItemDao: SavedItemDao,
|
||||||
|
private val networker: Networker
|
||||||
|
): LibraryRepository {
|
||||||
|
|
||||||
|
override fun getSavedItems(query: LibraryQuery): Flow<List<SavedItemWithLabelsAndHighlights>> =
|
||||||
|
savedItemDao.filteredLibraryData(
|
||||||
|
query.allowedArchiveStates,
|
||||||
|
query.sortKey,
|
||||||
|
hasRequiredLabels = query.requiredLabels.size,
|
||||||
|
hasExcludedLabels = query.excludedLabels.size,
|
||||||
|
query.requiredLabels,
|
||||||
|
query.excludedLabels,
|
||||||
|
query.allowedContentReaders
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun updateReadingProgress(
|
||||||
|
itemId: String,
|
||||||
|
readingProgressPercentage: Double,
|
||||||
|
readingProgressAnchorIndex: Int
|
||||||
|
) {
|
||||||
|
|
||||||
|
val jsonString = Gson().toJson(
|
||||||
|
mapOf(
|
||||||
|
"id" to itemId,
|
||||||
|
"readingProgressPercent" to readingProgressPercentage,
|
||||||
|
"readingProgressAnchorIndex" to readingProgressAnchorIndex,
|
||||||
|
"force" to true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
|
||||||
|
val savedItemId = readingProgressParams.id ?: return
|
||||||
|
|
||||||
|
|
||||||
|
val savedItem = savedItemDao.findById(savedItemId)
|
||||||
|
val updatedItem = savedItem?.copy(
|
||||||
|
readingProgress = readingProgressParams.readingProgressPercent ?: 0.0,
|
||||||
|
readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0,
|
||||||
|
serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||||
|
)
|
||||||
|
|
||||||
|
updatedItem?.let { savedItemDao.update(updatedItem) }
|
||||||
|
|
||||||
|
val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
|
||||||
|
|
||||||
|
if (isUpdatedOnServer) {
|
||||||
|
updatedItem?.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||||
|
updatedItem?.let { savedItemDao.update(updatedItem) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,42 +0,0 @@
|
|||||||
package app.omnivore.omnivore.core.database
|
|
||||||
|
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.OnConflictStrategy
|
|
||||||
import androidx.room.Update
|
|
||||||
|
|
||||||
|
|
||||||
interface BaseDao<T> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert an object in the database.
|
|
||||||
*
|
|
||||||
* @param obj the object to be inserted.
|
|
||||||
*/
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
fun insert(obj: T)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert an array of objects in the database.
|
|
||||||
*
|
|
||||||
* @param obj the objects to be inserted.
|
|
||||||
*/
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
fun insert(vararg obj: T)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an object from the database.
|
|
||||||
*
|
|
||||||
* @param obj the object to be updated
|
|
||||||
*/
|
|
||||||
@Update
|
|
||||||
fun update(obj: T)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an object from the database
|
|
||||||
*
|
|
||||||
* @param obj the object to be deleted
|
|
||||||
*/
|
|
||||||
@Delete
|
|
||||||
fun delete(obj: T)
|
|
||||||
}
|
|
||||||
@ -2,6 +2,7 @@ package app.omnivore.omnivore.core.database
|
|||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||||
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
||||||
import app.omnivore.omnivore.core.database.entities.HighlightChangesDao
|
import app.omnivore.omnivore.core.database.entities.HighlightChangesDao
|
||||||
@ -11,7 +12,6 @@ import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRe
|
|||||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRefDao
|
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRefDao
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef
|
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRefDao
|
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRefDao
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemDao
|
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabelDao
|
import app.omnivore.omnivore.core.database.entities.SavedItemLabelDao
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlightsDao
|
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlightsDao
|
||||||
@ -19,24 +19,24 @@ import app.omnivore.omnivore.core.database.entities.Viewer
|
|||||||
import app.omnivore.omnivore.core.database.entities.ViewerDao
|
import app.omnivore.omnivore.core.database.entities.ViewerDao
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
Viewer::class,
|
Viewer::class,
|
||||||
SavedItem::class,
|
SavedItem::class,
|
||||||
SavedItemLabel::class,
|
SavedItemLabel::class,
|
||||||
Highlight::class,
|
Highlight::class,
|
||||||
HighlightChange::class,
|
HighlightChange::class,
|
||||||
SavedItemAndSavedItemLabelCrossRef::class,
|
SavedItemAndSavedItemLabelCrossRef::class,
|
||||||
SavedItemAndHighlightCrossRef::class
|
SavedItemAndHighlightCrossRef::class],
|
||||||
],
|
version = 24,
|
||||||
version = 24
|
exportSchema = true
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class OmnivoreDatabase : RoomDatabase() {
|
||||||
abstract fun viewerDao(): ViewerDao
|
abstract fun viewerDao(): ViewerDao
|
||||||
abstract fun savedItemDao(): SavedItemDao
|
abstract fun savedItemDao(): SavedItemDao
|
||||||
abstract fun highlightDao(): HighlightDao
|
abstract fun highlightDao(): HighlightDao
|
||||||
abstract fun highlightChangesDao(): HighlightChangesDao
|
abstract fun highlightChangesDao(): HighlightChangesDao
|
||||||
abstract fun savedItemLabelDao(): SavedItemLabelDao
|
abstract fun savedItemLabelDao(): SavedItemLabelDao
|
||||||
abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao
|
abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao
|
||||||
abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao
|
abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao
|
||||||
abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao
|
abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao
|
||||||
}
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package app.omnivore.omnivore.core.database.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItemQueryConstants
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface SavedItemDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM savedItem")
|
||||||
|
fun getAll(): Flow<List<SavedItem>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
|
||||||
|
suspend fun findById(itemID: String): SavedItem?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0")
|
||||||
|
fun getUnSynced(): List<SavedItem>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM savedItem WHERE slug = :slug")
|
||||||
|
fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights?
|
||||||
|
|
||||||
|
@Query("DELETE FROM savedItem WHERE savedItemId = :itemID")
|
||||||
|
fun deleteById(itemID: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM savedItem WHERE savedItemId in (:itemIDs)")
|
||||||
|
fun deleteByIds(itemIDs: List<String>)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun update(savedItem: SavedItem)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||||
|
"FROM SavedItem " +
|
||||||
|
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||||
|
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||||
|
|
||||||
|
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||||
|
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||||
|
|
||||||
|
"WHERE SavedItem.savedItemId = :savedItemId " +
|
||||||
|
|
||||||
|
"GROUP BY SavedItem.savedItemId "
|
||||||
|
)
|
||||||
|
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||||
|
"FROM SavedItem " +
|
||||||
|
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||||
|
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||||
|
|
||||||
|
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||||
|
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||||
|
|
||||||
|
"WHERE SavedItem.savedItemId = :savedItemId " +
|
||||||
|
|
||||||
|
"GROUP BY SavedItem.savedItemId "
|
||||||
|
)
|
||||||
|
suspend fun getById(savedItemId: String): SavedItemWithLabelsAndHighlights?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||||
|
"FROM SavedItem " +
|
||||||
|
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||||
|
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||||
|
|
||||||
|
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||||
|
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||||
|
|
||||||
|
"WHERE SavedItem.serverSyncStatus != 2 " +
|
||||||
|
"AND SavedItem.isArchived IN (:allowedArchiveStates) " +
|
||||||
|
"AND SavedItem.contentReader IN (:allowedContentReaders) " +
|
||||||
|
"AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " +
|
||||||
|
"AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " +
|
||||||
|
|
||||||
|
"GROUP BY SavedItem.savedItemId " +
|
||||||
|
|
||||||
|
"ORDER BY \n" +
|
||||||
|
"CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" +
|
||||||
|
"CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" +
|
||||||
|
|
||||||
|
"CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" +
|
||||||
|
"CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC"
|
||||||
|
)
|
||||||
|
fun filteredLibraryData(
|
||||||
|
allowedArchiveStates: List<Int>,
|
||||||
|
sortKey: String,
|
||||||
|
hasRequiredLabels: Int,
|
||||||
|
hasExcludedLabels: Int,
|
||||||
|
requiredLabels: List<String>,
|
||||||
|
excludedLabels: List<String>,
|
||||||
|
allowedContentReaders: List<String>
|
||||||
|
): Flow<List<SavedItemWithLabelsAndHighlights>>
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package app.omnivore.omnivore.core.database.entities
|
package app.omnivore.omnivore.core.database.entities
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
|
||||||
@ -73,7 +73,20 @@ data class SavedItemWithLabelsAndHighlights(
|
|||||||
associateBy = Junction(SavedItemAndHighlightCrossRef::class)
|
associateBy = Junction(SavedItemAndHighlightCrossRef::class)
|
||||||
)
|
)
|
||||||
val highlights: List<Highlight>
|
val highlights: List<Highlight>
|
||||||
)
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as SavedItemWithLabelsAndHighlights
|
||||||
|
|
||||||
|
return savedItem.savedItemId == other.savedItem.savedItemId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return savedItem.savedItemId.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface HighlightDao {
|
interface HighlightDao {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import androidx.room.PrimaryKey
|
|||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
package app.omnivore.omnivore.core.database.entities
|
package app.omnivore.omnivore.core.database.entities
|
||||||
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
import java.util.*
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Transaction
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
data class SavedItem(
|
data class SavedItem(
|
||||||
@ -128,114 +132,7 @@ abstract class SavedItemWithLabelsAndHighlightsDao {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface SavedItemDao {
|
|
||||||
@Query("SELECT * FROM savedItem")
|
|
||||||
fun getAll(): List<SavedItem>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
|
|
||||||
fun findById(itemID: String): SavedItem?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0")
|
|
||||||
fun getUnSynced(): List<SavedItem>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM savedItem WHERE slug = :slug")
|
|
||||||
fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights?
|
|
||||||
|
|
||||||
@Query("DELETE FROM savedItem WHERE savedItemId = :itemID")
|
|
||||||
fun deleteById(itemID: String)
|
|
||||||
|
|
||||||
@Query("DELETE FROM savedItem WHERE savedItemId in (:itemIDs)")
|
|
||||||
fun deleteByIds(itemIDs: List<String>)
|
|
||||||
|
|
||||||
@Update
|
|
||||||
fun update(savedItem: SavedItem)
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query(
|
|
||||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
|
||||||
"FROM SavedItem " +
|
|
||||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
|
||||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
|
||||||
|
|
||||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
|
||||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
|
||||||
|
|
||||||
"WHERE SavedItem.savedItemId = :savedItemId " +
|
|
||||||
|
|
||||||
"GROUP BY SavedItem.savedItemId "
|
|
||||||
)
|
|
||||||
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights>
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query(
|
|
||||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
|
||||||
"FROM SavedItem " +
|
|
||||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
|
||||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
|
||||||
|
|
||||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
|
||||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
|
||||||
|
|
||||||
"WHERE SavedItem.savedItemId = :savedItemId " +
|
|
||||||
|
|
||||||
"GROUP BY SavedItem.savedItemId "
|
|
||||||
)
|
|
||||||
suspend fun getById(savedItemId: String): SavedItemWithLabelsAndHighlights?
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query(
|
|
||||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
|
||||||
"FROM SavedItem " +
|
|
||||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
|
||||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
|
||||||
|
|
||||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
|
||||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
|
||||||
|
|
||||||
"WHERE SavedItem.serverSyncStatus != 2 " +
|
|
||||||
"AND SavedItem.isArchived IN (:allowedArchiveStates) " +
|
|
||||||
"AND SavedItem.contentReader IN (:allowedContentReaders) " +
|
|
||||||
"AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " +
|
|
||||||
"AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " +
|
|
||||||
|
|
||||||
"GROUP BY SavedItem.savedItemId " +
|
|
||||||
|
|
||||||
"ORDER BY \n" +
|
|
||||||
"CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" +
|
|
||||||
"CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" +
|
|
||||||
|
|
||||||
"CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" +
|
|
||||||
"CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC"
|
|
||||||
)
|
|
||||||
fun _filteredLibraryData(
|
|
||||||
allowedArchiveStates: List<Int>,
|
|
||||||
sortKey: String,
|
|
||||||
hasRequiredLabels: Int,
|
|
||||||
hasExcludedLabels: Int,
|
|
||||||
requiredLabels: List<String>,
|
|
||||||
excludedLabels: List<String>,
|
|
||||||
allowedContentReaders: List<String>
|
|
||||||
): LiveData<List<SavedItemWithLabelsAndHighlights>>
|
|
||||||
|
|
||||||
fun filteredLibraryData(
|
|
||||||
allowedArchiveStates: List<Int>,
|
|
||||||
sortKey: String,
|
|
||||||
requiredLabels: List<String>,
|
|
||||||
excludedLabels: List<String>,
|
|
||||||
allowedContentReaders: List<String>
|
|
||||||
): LiveData<List<SavedItemWithLabelsAndHighlights>> {
|
|
||||||
return _filteredLibraryData(
|
|
||||||
allowedArchiveStates = allowedArchiveStates,
|
|
||||||
sortKey = sortKey,
|
|
||||||
hasRequiredLabels = requiredLabels.size,
|
|
||||||
hasExcludedLabels = excludedLabels.size,
|
|
||||||
requiredLabels = requiredLabels,
|
|
||||||
excludedLabels = excludedLabels,
|
|
||||||
allowedContentReaders = allowedContentReaders
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
object SavedItemQueryConstants {
|
object SavedItemQueryConstants {
|
||||||
|
|||||||
@ -1,66 +1,70 @@
|
|||||||
package app.omnivore.omnivore.core.database.entities
|
package app.omnivore.omnivore.core.database.entities
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
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
|
@Entity
|
||||||
data class SavedItemLabel(
|
data class SavedItemLabel(
|
||||||
@PrimaryKey val savedItemLabelId: String,
|
@PrimaryKey val savedItemLabelId: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val color: String,
|
val color: String,
|
||||||
val createdAt: String?,
|
val createdAt: String?,
|
||||||
val labelDescription: String?,
|
val labelDescription: String?,
|
||||||
val serverSyncStatus: Int = 0
|
val serverSyncStatus: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SavedItemLabelDao {
|
interface SavedItemLabelDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insertAll(items: List<SavedItemLabel>)
|
fun insertAll(items: List<SavedItemLabel>)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
|
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
|
||||||
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
|
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId")
|
@Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId")
|
||||||
fun updateTempLabel(tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED)
|
fun updateTempLabel(
|
||||||
|
tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED
|
||||||
|
)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC")
|
@Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC")
|
||||||
fun namedLabels(names: List<String>): List<SavedItemLabel>
|
fun namedLabels(names: List<String>): List<SavedItemLabel>
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
primaryKeys = ["savedItemLabelId", "savedItemId"],
|
primaryKeys = ["savedItemLabelId", "savedItemId"], foreignKeys = [ForeignKey(
|
||||||
foreignKeys = [
|
entity = SavedItem::class,
|
||||||
ForeignKey(
|
parentColumns = arrayOf("savedItemId"),
|
||||||
entity = SavedItem::class,
|
childColumns = arrayOf("savedItemId"),
|
||||||
parentColumns = arrayOf("savedItemId"),
|
onDelete = ForeignKey.CASCADE
|
||||||
childColumns = arrayOf("savedItemId"),
|
), ForeignKey(
|
||||||
onDelete = ForeignKey.CASCADE
|
entity = SavedItemLabel::class,
|
||||||
),
|
parentColumns = arrayOf("savedItemLabelId"),
|
||||||
ForeignKey(
|
childColumns = arrayOf("savedItemLabelId")
|
||||||
entity = SavedItemLabel::class,
|
)]
|
||||||
parentColumns = arrayOf("savedItemLabelId"),
|
|
||||||
childColumns = arrayOf("savedItemLabelId")
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
data class SavedItemAndSavedItemLabelCrossRef(
|
data class SavedItemAndSavedItemLabelCrossRef(
|
||||||
val savedItemLabelId: String,
|
val savedItemLabelId: String, val savedItemId: String
|
||||||
val savedItemId: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SavedItemAndSavedItemLabelCrossRefDao {
|
interface SavedItemAndSavedItemLabelCrossRefDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||||
|
|
||||||
@Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId")
|
@Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId")
|
||||||
fun deleteRefsBySavedItemId(savedItemId: String)
|
fun deleteRefsBySavedItemId(savedItemId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
// has many highlights
|
// has many highlights
|
||||||
|
|||||||
@ -1,25 +1,20 @@
|
|||||||
package app.omnivore.omnivore.core.network
|
package app.omnivore.omnivore.core.network
|
||||||
|
|
||||||
|
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||||
import app.omnivore.omnivore.utils.Constants
|
import app.omnivore.omnivore.utils.Constants
|
||||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
|
||||||
import com.apollographql.apollo3.ApolloClient
|
import com.apollographql.apollo3.ApolloClient
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class Networker @Inject constructor(
|
class Networker @Inject constructor(
|
||||||
private val datastoreRepo: DatastoreRepository
|
private val datastoreRepo: DatastoreRepository
|
||||||
) {
|
) {
|
||||||
suspend fun baseUrl() = datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL
|
suspend fun baseUrl() =
|
||||||
|
datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL
|
||||||
|
|
||||||
suspend fun serverUrl() = "${baseUrl()}/api/graphql"
|
private suspend fun serverUrl() = "${baseUrl()}/api/graphql"
|
||||||
private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: ""
|
private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: ""
|
||||||
|
|
||||||
suspend fun publicApolloClient() = ApolloClient.Builder()
|
suspend fun authenticatedApolloClient() = ApolloClient.Builder().serverUrl(serverUrl())
|
||||||
.serverUrl(serverUrl())
|
.addHttpHeader("Authorization", value = authToken()).build()
|
||||||
.build()
|
|
||||||
|
|
||||||
suspend fun authenticatedApolloClient() = ApolloClient.Builder()
|
|
||||||
.serverUrl(serverUrl())
|
|
||||||
.addHttpHeader("Authorization", value = authToken())
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import app.omnivore.omnivore.core.database.entities.Highlight
|
|||||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
import com.apollographql.apollo3.api.Optional
|
import com.apollographql.apollo3.api.Optional
|
||||||
|
|
||||||
data class LibrarySearchQueryResponse(
|
data class LibrarySearchQueryResponse(
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
package app.omnivore.omnivore.di
|
package app.omnivore.omnivore.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
|
||||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||||
import app.omnivore.omnivore.core.datastore.OmnivoreDatastore
|
|
||||||
import app.omnivore.omnivore.core.data.DataService
|
import app.omnivore.omnivore.core.data.DataService
|
||||||
|
import app.omnivore.omnivore.core.database.OmnivoreDatabase
|
||||||
|
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||||
|
import app.omnivore.omnivore.core.datastore.OmnivoreDatastore
|
||||||
import app.omnivore.omnivore.core.network.Networker
|
import app.omnivore.omnivore.core.network.Networker
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@ -34,7 +35,8 @@ object AppModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun provideDataService(
|
fun provideDataService(
|
||||||
@ApplicationContext app: Context,
|
networker: Networker,
|
||||||
networker: Networker
|
omnivoreDatabase: OmnivoreDatabase
|
||||||
) = DataService(app, networker)
|
) = DataService(networker, omnivoreDatabase)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
package app.omnivore.omnivore.di
|
||||||
|
|
||||||
|
import app.omnivore.omnivore.core.database.OmnivoreDatabase
|
||||||
|
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object DaosModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun providesSavedItemDao(
|
||||||
|
database: OmnivoreDatabase,
|
||||||
|
): SavedItemDao = database.savedItemDao()
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package app.omnivore.omnivore.di
|
||||||
|
|
||||||
|
import app.omnivore.omnivore.core.data.repository.LibraryRepository
|
||||||
|
import app.omnivore.omnivore.core.data.repository.impl.LibraryRepositoryImpl
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface DataModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindsLibraryRepository(
|
||||||
|
libraryRepository: LibraryRepositoryImpl,
|
||||||
|
): LibraryRepository
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package app.omnivore.omnivore.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import app.omnivore.omnivore.core.database.OmnivoreDatabase
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object DatabaseModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesOmnivoreDatabase(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
): OmnivoreDatabase = Room.databaseBuilder(
|
||||||
|
context,
|
||||||
|
OmnivoreDatabase::class.java,
|
||||||
|
"omnivore-database",
|
||||||
|
).build()
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package app.omnivore.omnivore.feature.components
|
package app.omnivore.omnivore.feature.components
|
||||||
|
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import app.omnivore.omnivore.core.model.ServerSyncStatus
|
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|||||||
@ -1,117 +1,124 @@
|
|||||||
package app.omnivore.omnivore.feature.library
|
package app.omnivore.omnivore.feature.library
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.AssistChip
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.SuggestionChipDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.text.toLowerCase
|
import androidx.compose.ui.text.toLowerCase
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import app.omnivore.omnivore.R
|
import app.omnivore.omnivore.R
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||||
import app.omnivore.omnivore.feature.components.LabelChipColors
|
import app.omnivore.omnivore.feature.components.LabelChipColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryFilterBar(viewModel: LibraryViewModel) {
|
fun LibraryFilterBar(
|
||||||
var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) }
|
viewModel: LibraryViewModel = hiltViewModel()
|
||||||
val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(SavedItemFilter.INBOX)
|
) {
|
||||||
val activeLabels: List<SavedItemLabel> by viewModel.activeLabelsLiveData.observeAsState(listOf())
|
var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(
|
||||||
|
SavedItemFilter.INBOX
|
||||||
|
)
|
||||||
|
val activeLabels: List<SavedItemLabel> by viewModel.activeLabelsLiveData.observeAsState(listOf())
|
||||||
|
|
||||||
var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) }
|
var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) }
|
||||||
val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(SavedItemSortFilter.NEWEST)
|
val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(
|
||||||
val listState = rememberLazyListState()
|
SavedItemSortFilter.NEWEST
|
||||||
|
)
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
state = listState,
|
state = listState,
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 6.dp)
|
.padding(start = 6.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
AssistChip(
|
AssistChip(onClick = { isSavedItemFilterMenuExpanded = true },
|
||||||
onClick = { isSavedItemFilterMenuExpanded = true },
|
label = { Text(activeSavedItemFilter.displayText) },
|
||||||
label = { Text(activeSavedItemFilter.displayText) },
|
trailingIcon = {
|
||||||
trailingIcon = {
|
Icon(
|
||||||
Icon(
|
Icons.Default.ArrowDropDown,
|
||||||
Icons.Default.ArrowDropDown,
|
contentDescription = "drop down button to change primary library filter"
|
||||||
contentDescription = "drop down button to change primary library filter"
|
)
|
||||||
)
|
},
|
||||||
},
|
modifier = Modifier.padding(end = 6.dp)
|
||||||
modifier = Modifier.padding(end = 6.dp)
|
)
|
||||||
)
|
AssistChip(onClick = { isSavedItemSortFilterMenuExpanded = true },
|
||||||
AssistChip(
|
label = { Text(activeSavedItemSortFilter.displayText) },
|
||||||
onClick = { isSavedItemSortFilterMenuExpanded = true },
|
trailingIcon = {
|
||||||
label = { Text(activeSavedItemSortFilter.displayText) },
|
Icon(
|
||||||
trailingIcon = {
|
Icons.Default.ArrowDropDown,
|
||||||
Icon(
|
contentDescription = "drop down button to change library sort order"
|
||||||
Icons.Default.ArrowDropDown,
|
)
|
||||||
contentDescription = "drop down button to change library sort order"
|
},
|
||||||
)
|
modifier = Modifier.padding(end = 6.dp)
|
||||||
},
|
)
|
||||||
modifier = Modifier.padding(end = 6.dp)
|
AssistChip(
|
||||||
)
|
onClick = { viewModel.bottomSheetState.value = LibraryBottomSheetState.LABEL },
|
||||||
AssistChip(
|
label = { Text(stringResource(R.string.library_filter_bar_label_labels)) },
|
||||||
onClick = { viewModel.bottomSheetState.value = LibraryBottomSheetState.LABEL },
|
trailingIcon = {
|
||||||
label = { Text(stringResource(R.string.library_filter_bar_label_labels)) },
|
Icon(
|
||||||
trailingIcon = {
|
Icons.Default.ArrowDropDown,
|
||||||
Icon(
|
contentDescription = "drop down button to open label selection sheet"
|
||||||
Icons.Default.ArrowDropDown,
|
)
|
||||||
contentDescription = "drop down button to open label selection sheet"
|
},
|
||||||
)
|
modifier = Modifier.padding(end = 6.dp)
|
||||||
},
|
)
|
||||||
modifier = Modifier.padding(end = 6.dp)
|
}
|
||||||
)
|
items(activeLabels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label ->
|
||||||
}
|
val chipColors = LabelChipColors.fromHex(label.color)
|
||||||
items(activeLabels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label ->
|
|
||||||
val chipColors = LabelChipColors.fromHex(label.color)
|
|
||||||
|
|
||||||
AssistChip(
|
AssistChip(onClick = {
|
||||||
onClick = {
|
viewModel.updateAppliedLabels((viewModel.activeLabelsLiveData.value
|
||||||
viewModel.updateAppliedLabels(
|
?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId })
|
||||||
(viewModel.activeLabelsLiveData.value ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId }
|
},
|
||||||
)
|
label = { Text(label.name) },
|
||||||
},
|
border = null,
|
||||||
label = { Text(label.name) },
|
colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
|
||||||
border = null,
|
containerColor = chipColors.containerColor,
|
||||||
colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
|
labelColor = chipColors.textColor,
|
||||||
containerColor = chipColors.containerColor,
|
iconContentColor = chipColors.textColor
|
||||||
labelColor = chipColors.textColor,
|
),
|
||||||
iconContentColor = chipColors.textColor
|
trailingIcon = {
|
||||||
),
|
Icon(
|
||||||
trailingIcon = {
|
Icons.Default.Close, contentDescription = "close icon to remove label"
|
||||||
Icon(
|
)
|
||||||
Icons.Default.Close,
|
},
|
||||||
contentDescription = "close icon to remove label"
|
modifier = Modifier.padding(horizontal = 4.dp)
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
modifier = Modifier
|
}
|
||||||
.padding(horizontal = 4.dp)
|
|
||||||
)
|
SavedItemFilterContextMenu(isExpanded = isSavedItemFilterMenuExpanded,
|
||||||
}
|
onDismiss = { isSavedItemFilterMenuExpanded = false },
|
||||||
|
actionHandler = { viewModel.updateSavedItemFilter(it) })
|
||||||
|
|
||||||
|
SavedItemSortFilterContextMenu(isExpanded = isSavedItemSortFilterMenuExpanded,
|
||||||
|
onDismiss = { isSavedItemSortFilterMenuExpanded = false },
|
||||||
|
actionHandler = { viewModel.updateSavedItemSortFilter(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
SavedItemFilterContextMenu(
|
|
||||||
isExpanded = isSavedItemFilterMenuExpanded,
|
|
||||||
onDismiss = { isSavedItemFilterMenuExpanded = false },
|
|
||||||
actionHandler = { viewModel.updateSavedItemFilter(it) }
|
|
||||||
)
|
|
||||||
|
|
||||||
SavedItemSortFilterContextMenu(
|
|
||||||
isExpanded = isSavedItemSortFilterMenuExpanded,
|
|
||||||
onDismiss = { isSavedItemSortFilterMenuExpanded = false },
|
|
||||||
actionHandler = { viewModel.updateSavedItemSortFilter(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,7 +131,7 @@ fun LibraryNavigationBar(
|
|||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/* IconButton(onClick = { isMenuExpanded = true } ) {
|
IconButton(onClick = { isMenuExpanded = true } ) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.MoreVert,
|
imageVector = Icons.Default.MoreVert,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
@ -144,7 +144,7 @@ fun LibraryNavigationBar(
|
|||||||
onDismiss = { isMenuExpanded = false },
|
onDismiss = { isMenuExpanded = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
IconButton(onClick = onSearchClicked) {
|
IconButton(onClick = onSearchClicked) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import androidx.compose.material.DismissValue
|
|||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.FractionalThreshold
|
import androidx.compose.material.FractionalThreshold
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.ScaffoldState
|
import androidx.compose.material.ScaffoldState
|
||||||
import androidx.compose.material.SwipeToDismiss
|
import androidx.compose.material.SwipeToDismiss
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@ -32,9 +31,11 @@ import androidx.compose.material.pullrefresh.pullRefresh
|
|||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.material.rememberDismissState
|
import androidx.compose.material.rememberDismissState
|
||||||
import androidx.compose.material.rememberScaffoldState
|
import androidx.compose.material.rememberScaffoldState
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@ -50,8 +51,11 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||||
@ -70,50 +74,52 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryView(
|
internal fun LibraryView(
|
||||||
libraryViewModel: LibraryViewModel,
|
|
||||||
labelsViewModel: LabelsViewModel,
|
labelsViewModel: LabelsViewModel,
|
||||||
saveViewModel: SaveViewModel,
|
saveViewModel: SaveViewModel,
|
||||||
editInfoViewModel: EditInfoViewModel,
|
editInfoViewModel: EditInfoViewModel,
|
||||||
navController: NavHostController
|
navController: NavHostController,
|
||||||
|
viewModel: LibraryViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val scaffoldState: ScaffoldState = rememberScaffoldState()
|
val scaffoldState: ScaffoldState = rememberScaffoldState()
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val showBottomSheet: LibraryBottomSheetState by libraryViewModel.bottomSheetState.observeAsState(
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val showBottomSheet: LibraryBottomSheetState by viewModel.bottomSheetState.observeAsState(
|
||||||
LibraryBottomSheetState.HIDDEN
|
LibraryBottomSheetState.HIDDEN
|
||||||
)
|
)
|
||||||
|
|
||||||
libraryViewModel.snackbarMessage?.let {
|
viewModel.snackbarMessage?.let {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
scaffoldState.snackbarHostState.showSnackbar(it)
|
scaffoldState.snackbarHostState.showSnackbar(it)
|
||||||
libraryViewModel.clearSnackbarMessage()
|
viewModel.clearSnackbarMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (showBottomSheet) {
|
when (showBottomSheet) {
|
||||||
LibraryBottomSheetState.ADD_LINK -> {
|
LibraryBottomSheetState.ADD_LINK -> {
|
||||||
AddLinkBottomSheet(saveViewModel) {
|
AddLinkBottomSheet(saveViewModel) {
|
||||||
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryBottomSheetState.LABEL -> {
|
LibraryBottomSheetState.LABEL -> {
|
||||||
LabelBottomSheet(
|
LabelBottomSheet(
|
||||||
libraryViewModel,
|
viewModel,
|
||||||
labelsViewModel
|
labelsViewModel
|
||||||
) {
|
) {
|
||||||
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryBottomSheetState.EDIT -> {
|
LibraryBottomSheetState.EDIT -> {
|
||||||
EditBottomSheet(
|
EditBottomSheet(
|
||||||
editInfoViewModel,
|
editInfoViewModel,
|
||||||
libraryViewModel
|
viewModel
|
||||||
) {
|
) {
|
||||||
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,21 +128,38 @@ fun LibraryView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
scaffoldState = scaffoldState,
|
|
||||||
topBar = {
|
topBar = {
|
||||||
LibraryNavigationBar(
|
LibraryNavigationBar(
|
||||||
savedItemViewModel = libraryViewModel,
|
savedItemViewModel = viewModel,
|
||||||
onSearchClicked = { navController.navigate(Routes.Search.route) },
|
onSearchClicked = { navController.navigate(Routes.Search.route) },
|
||||||
onAddLinkClicked = { showAddLinkBottomSheet(libraryViewModel) },
|
onAddLinkClicked = { showAddLinkBottomSheet(viewModel) },
|
||||||
onSettingsIconClick = { navController.navigate(Routes.Settings.route) }
|
onSettingsIconClick = { navController.navigate(Routes.Settings.route) }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
LibraryViewContent(
|
when (uiState) {
|
||||||
libraryViewModel,
|
is LibraryUiState.Success -> {
|
||||||
modifier = Modifier
|
LibraryViewContent(
|
||||||
.padding(top = paddingValues.calculateTopPadding())
|
viewModel,
|
||||||
)
|
modifier = Modifier
|
||||||
|
.padding(top = paddingValues.calculateTopPadding()),
|
||||||
|
uiState = uiState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is LibraryUiState.Loading -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(strokeCap = StrokeCap.Round)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +192,7 @@ fun LabelBottomSheet(
|
|||||||
labelsViewModel = labelsViewModel,
|
labelsViewModel = labelsViewModel,
|
||||||
initialSelectedLabels = currentSavedItemData.labels,
|
initialSelectedLabels = currentSavedItemData.labels,
|
||||||
onCancel = {
|
onCancel = {
|
||||||
libraryViewModel.currentItemLiveData.value = null
|
libraryViewModel.currentItem.value = null
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
isLibraryMode = false,
|
isLibraryMode = false,
|
||||||
@ -180,7 +203,7 @@ fun LabelBottomSheet(
|
|||||||
labels = it
|
labels = it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
libraryViewModel.currentItemLiveData.value = null
|
libraryViewModel.currentItem.value = null
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
onCreateLabel = { newLabelName, labelHexValue ->
|
onCreateLabel = { newLabelName, labelHexValue ->
|
||||||
@ -196,7 +219,7 @@ fun LabelBottomSheet(
|
|||||||
isLibraryMode = true,
|
isLibraryMode = true,
|
||||||
onSave = {
|
onSave = {
|
||||||
libraryViewModel.updateAppliedLabels(it)
|
libraryViewModel.updateAppliedLabels(it)
|
||||||
libraryViewModel.currentItemLiveData.value = null
|
libraryViewModel.currentItem.value = null
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
onCreateLabel = { newLabelName, labelHexValue ->
|
onCreateLabel = { newLabelName, labelHexValue ->
|
||||||
@ -257,11 +280,11 @@ fun EditBottomSheet(
|
|||||||
description = currentSavedItemData?.savedItem?.descriptionText,
|
description = currentSavedItemData?.savedItem?.descriptionText,
|
||||||
viewModel = editInfoViewModel,
|
viewModel = editInfoViewModel,
|
||||||
onCancel = {
|
onCancel = {
|
||||||
libraryViewModel.currentItemLiveData.value = null
|
libraryViewModel.currentItem.value = null
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
onUpdated = {
|
onUpdated = {
|
||||||
libraryViewModel.currentItemLiveData.value = null
|
libraryViewModel.currentItem.value = null
|
||||||
libraryViewModel.refresh()
|
libraryViewModel.refresh()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
@ -272,7 +295,11 @@ fun EditBottomSheet(
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
fun LibraryViewContent(
|
||||||
|
libraryViewModel: LibraryViewModel,
|
||||||
|
modifier: Modifier,
|
||||||
|
uiState: LibraryUiState
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
@ -282,9 +309,6 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState()
|
val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState()
|
||||||
val cardsData: List<SavedItemWithLabelsAndHighlights> by libraryViewModel.itemsLiveData.observeAsState(
|
|
||||||
listOf()
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -299,13 +323,12 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 6.dp)
|
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
LibraryFilterBar(libraryViewModel)
|
LibraryFilterBar(libraryViewModel)
|
||||||
}
|
}
|
||||||
items(
|
items(
|
||||||
items = cardsData,
|
items = (uiState as LibraryUiState.Success).items,
|
||||||
key = { item -> item.savedItem.savedItemId }
|
key = { item -> item.savedItem.savedItemId }
|
||||||
) { cardDataWithLabels ->
|
) { cardDataWithLabels ->
|
||||||
val swipeThreshold = 0.45f
|
val swipeThreshold = 0.45f
|
||||||
@ -388,10 +411,15 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
|||||||
dismissContent = {
|
dismissContent = {
|
||||||
val selected =
|
val selected =
|
||||||
currentItem.savedItemId == selectedItem?.savedItem?.savedItemId
|
currentItem.savedItemId == selectedItem?.savedItem?.savedItemId
|
||||||
|
val test = SavedItemWithLabelsAndHighlights(
|
||||||
|
savedItem = cardDataWithLabels.savedItem,
|
||||||
|
labels = listOf(),
|
||||||
|
highlights = listOf()
|
||||||
|
)
|
||||||
SavedItemCard(
|
SavedItemCard(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
savedItemViewModel = libraryViewModel,
|
savedItemViewModel = libraryViewModel,
|
||||||
savedItem = cardDataWithLabels,
|
savedItem = test,
|
||||||
onClickHandler = {
|
onClickHandler = {
|
||||||
libraryViewModel.actionsMenuItemLiveData.postValue(null)
|
libraryViewModel.actionsMenuItemLiveData.postValue(null)
|
||||||
val activityClass =
|
val activityClass =
|
||||||
@ -417,7 +445,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InfiniteListHandler(listState = listState) {
|
InfiniteListHandler(listState = listState) {
|
||||||
if (cardsData.isEmpty()) {
|
if ((uiState as LibraryUiState.Success).items.isEmpty()) {
|
||||||
Log.d("sync", "loading with load func")
|
Log.d("sync", "loading with load func")
|
||||||
libraryViewModel.initialLoad()
|
libraryViewModel.initialLoad()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import androidx.lifecycle.MediatorLiveData
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
|
||||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
|
||||||
import app.omnivore.omnivore.R
|
import app.omnivore.omnivore.R
|
||||||
import app.omnivore.omnivore.core.data.DataService
|
import app.omnivore.omnivore.core.data.DataService
|
||||||
import app.omnivore.omnivore.core.data.archiveSavedItem
|
import app.omnivore.omnivore.core.data.archiveSavedItem
|
||||||
@ -16,25 +14,29 @@ import app.omnivore.omnivore.core.data.deleteSavedItem
|
|||||||
import app.omnivore.omnivore.core.data.fetchSavedItemContent
|
import app.omnivore.omnivore.core.data.fetchSavedItemContent
|
||||||
import app.omnivore.omnivore.core.data.isSavedItemContentStoredInDB
|
import app.omnivore.omnivore.core.data.isSavedItemContentStoredInDB
|
||||||
import app.omnivore.omnivore.core.data.librarySearch
|
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.sync
|
||||||
import app.omnivore.omnivore.core.data.syncLabels
|
import app.omnivore.omnivore.core.data.syncLabels
|
||||||
import app.omnivore.omnivore.core.data.syncOfflineItemsWithServerIfNeeded
|
import app.omnivore.omnivore.core.data.syncOfflineItemsWithServerIfNeeded
|
||||||
import app.omnivore.omnivore.core.data.unarchiveSavedItem
|
import app.omnivore.omnivore.core.data.unarchiveSavedItem
|
||||||
import app.omnivore.omnivore.core.data.updateWebReadingProgress
|
|
||||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
|
||||||
import app.omnivore.omnivore.core.network.Networker
|
|
||||||
import app.omnivore.omnivore.core.network.createNewLabel
|
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
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.ResourceProvider
|
||||||
import app.omnivore.omnivore.feature.setSavedItemLabels
|
import app.omnivore.omnivore.feature.setSavedItemLabels
|
||||||
|
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||||
|
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||||
import com.apollographql.apollo3.api.Optional
|
import com.apollographql.apollo3.api.Optional
|
||||||
import com.google.gson.Gson
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -46,34 +48,60 @@ class LibraryViewModel @Inject constructor(
|
|||||||
private val networker: Networker,
|
private val networker: Networker,
|
||||||
private val dataService: DataService,
|
private val dataService: DataService,
|
||||||
private val datastoreRepo: DatastoreRepository,
|
private val datastoreRepo: DatastoreRepository,
|
||||||
private val resourceProvider: ResourceProvider
|
private val resourceProvider: ResourceProvider,
|
||||||
|
private val libraryRepository: LibraryRepository,
|
||||||
) : ViewModel(), SavedItemViewModel {
|
) : ViewModel(), SavedItemViewModel {
|
||||||
|
|
||||||
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
|
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
|
||||||
private var cursor: String? = null
|
private var cursor: String? = null
|
||||||
private var librarySearchCursor: String? = null
|
private var librarySearchCursor: String? = null
|
||||||
|
|
||||||
// These are used to make sure we handle search result
|
|
||||||
// responses in the right order
|
|
||||||
private var searchIdx = 0
|
|
||||||
private var receivedIdx = 0
|
|
||||||
|
|
||||||
var snackbarMessage by mutableStateOf<String?>(null)
|
var snackbarMessage by mutableStateOf<String?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
// Live Data
|
private val _libraryQuery = MutableStateFlow(
|
||||||
private var itemsLiveDataInternal = dataService.db.savedItemDao().filteredLibraryData(
|
LibraryQuery(
|
||||||
allowedArchiveStates = listOf(0),
|
allowedArchiveStates = listOf(0),
|
||||||
sortKey = "newest",
|
sortKey = "newest",
|
||||||
requiredLabels = listOf(),
|
requiredLabels = listOf(),
|
||||||
excludedLabels = listOf(),
|
excludedLabels = listOf(),
|
||||||
allowedContentReaders = listOf("WEB", "PDF", "EPUB")
|
allowedContentReaders = listOf("WEB", "PDF", "EPUB")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
val itemsLiveData = MediatorLiveData<List<SavedItemWithLabelsAndHighlights>>()
|
|
||||||
|
// Correct way - but not working
|
||||||
|
/* val uiState: StateFlow<LibraryUiState> = _libraryQuery.flatMapLatest { query ->
|
||||||
|
libraryRepository.getSavedItems(query)
|
||||||
|
}
|
||||||
|
.map(LibraryUiState::Success)
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.Lazily,
|
||||||
|
initialValue = LibraryUiState.Loading
|
||||||
|
)*/
|
||||||
|
|
||||||
|
// This approach needs to be replaced with the StateFlow above after fixing Room Flow
|
||||||
|
private val _uiState = MutableStateFlow<LibraryUiState>(LibraryUiState.Loading)
|
||||||
|
val uiState: StateFlow<LibraryUiState> = _uiState
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadSavedItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSavedItems() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
libraryRepository.getSavedItems(_libraryQuery.value)
|
||||||
|
.collect { favoriteNews ->
|
||||||
|
_uiState.value = LibraryUiState.Success(favoriteNews)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val itemsLiveData = MediatorLiveData<List<SavedItemWithLabelsAndHighlights>>()
|
||||||
val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX)
|
val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX)
|
||||||
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
|
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
|
||||||
val bottomSheetState = MutableLiveData(LibraryBottomSheetState.HIDDEN)
|
val bottomSheetState = MutableLiveData(LibraryBottomSheetState.HIDDEN)
|
||||||
val currentItemLiveData = MutableLiveData<String?>(null)
|
val currentItem = mutableStateOf<String?>(null)
|
||||||
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
|
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
|
||||||
val activeLabelsLiveData = MutableLiveData<List<SavedItemLabel>>(listOf())
|
val activeLabelsLiveData = MutableLiveData<List<SavedItemLabel>>(listOf())
|
||||||
|
|
||||||
@ -83,6 +111,7 @@ class LibraryViewModel @Inject constructor(
|
|||||||
private var hasLoadedInitialFilters = false
|
private var hasLoadedInitialFilters = false
|
||||||
|
|
||||||
private fun loadInitialFilterValues() {
|
private fun loadInitialFilterValues() {
|
||||||
|
|
||||||
if (hasLoadedInitialFilters) {
|
if (hasLoadedInitialFilters) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -130,8 +159,6 @@ class LibraryViewModel @Inject constructor(
|
|||||||
hasLoadedInitialFilters = false
|
hasLoadedInitialFilters = false
|
||||||
cursor = null
|
cursor = null
|
||||||
librarySearchCursor = null
|
librarySearchCursor = null
|
||||||
searchIdx = 0
|
|
||||||
receivedIdx = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLoadedInitialFilters) {
|
if (hasLoadedInitialFilters) {
|
||||||
@ -153,8 +180,7 @@ class LibraryViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val result = dataService.librarySearch(
|
val result = dataService.librarySearch(
|
||||||
cursor = librarySearchCursor,
|
cursor = librarySearchCursor, query = searchQueryString()
|
||||||
query = searchQueryString()
|
|
||||||
)
|
)
|
||||||
result.cursor?.let {
|
result.cursor?.let {
|
||||||
librarySearchCursor = it
|
librarySearchCursor = it
|
||||||
@ -237,17 +263,14 @@ class LibraryViewModel @Inject constructor(
|
|||||||
else -> listOf()
|
else -> listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
val newData = dataService.db.savedItemDao().filteredLibraryData(
|
_libraryQuery.value = LibraryQuery(
|
||||||
allowedArchiveStates = allowedArchiveStates,
|
allowedArchiveStates = allowedArchiveStates,
|
||||||
sortKey = sortKey,
|
sortKey = sortKey,
|
||||||
requiredLabels = requiredLabels,
|
requiredLabels = requiredLabels,
|
||||||
excludedLabels = excludeLabels,
|
excludedLabels = excludeLabels,
|
||||||
allowedContentReaders = allowedContentReaders
|
allowedContentReaders = allowedContentReaders
|
||||||
)
|
)
|
||||||
|
loadSavedItems()
|
||||||
itemsLiveData.removeSource(itemsLiveDataInternal)
|
|
||||||
itemsLiveDataInternal = newData
|
|
||||||
itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,42 +339,28 @@ class LibraryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SavedItemAction.EditLabels -> {
|
SavedItemAction.EditLabels -> {
|
||||||
currentItemLiveData.value = itemID
|
currentItem.value = itemID
|
||||||
bottomSheetState.value = LibraryBottomSheetState.LABEL
|
bottomSheetState.value = LibraryBottomSheetState.LABEL
|
||||||
}
|
}
|
||||||
|
|
||||||
SavedItemAction.EditInfo -> {
|
SavedItemAction.EditInfo -> {
|
||||||
currentItemLiveData.value = itemID
|
currentItem.value = itemID
|
||||||
bottomSheetState.value = LibraryBottomSheetState.EDIT
|
bottomSheetState.value = LibraryBottomSheetState.EDIT
|
||||||
}
|
}
|
||||||
|
|
||||||
SavedItemAction.MarkRead -> {
|
SavedItemAction.MarkRead -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dataService.updateWebReadingProgress(
|
_uiState.value = LibraryUiState.Success(emptyList())
|
||||||
jsonString = Gson().toJson(
|
libraryRepository.updateReadingProgress(itemID, 100.0, 0)
|
||||||
mapOf(
|
loadSavedItems()
|
||||||
"id" to itemID,
|
|
||||||
"readingProgressPercent" to 100.0,
|
|
||||||
"readingProgressAnchorIndex" to 0,
|
|
||||||
"force" to true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SavedItemAction.MarkUnread -> {
|
SavedItemAction.MarkUnread -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dataService.updateWebReadingProgress(
|
_uiState.value = LibraryUiState.Success(emptyList())
|
||||||
jsonString = Gson().toJson(
|
libraryRepository.updateReadingProgress(itemID, 0.0, 0)
|
||||||
mapOf(
|
loadSavedItems()
|
||||||
"id" to itemID,
|
|
||||||
"readingProgressPercent" to 0,
|
|
||||||
"readingProgressAnchorIndex" to 0,
|
|
||||||
"force" to true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -424,7 +433,7 @@ class LibraryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun currentSavedItemUnderEdit(): SavedItemWithLabelsAndHighlights? {
|
fun currentSavedItemUnderEdit(): SavedItemWithLabelsAndHighlights? {
|
||||||
currentItemLiveData.value?.let { itemID ->
|
currentItem.value?.let { itemID ->
|
||||||
return itemsLiveData.value?.first { it.savedItem.savedItemId == itemID }
|
return itemsLiveData.value?.first { it.savedItem.savedItemId == itemID }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,12 +455,16 @@ class LibraryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SavedItemAction {
|
sealed interface LibraryUiState {
|
||||||
Delete,
|
data object Loading : LibraryUiState
|
||||||
Archive,
|
|
||||||
Unarchive,
|
data class Success(
|
||||||
EditLabels,
|
val items: List<SavedItemWithLabelsAndHighlights>,
|
||||||
EditInfo,
|
) : LibraryUiState
|
||||||
MarkRead,
|
|
||||||
MarkUnread
|
data object Error : LibraryUiState
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SavedItemAction {
|
||||||
|
Delete, Archive, Unarchive, EditLabels, EditInfo, MarkRead, MarkUnread
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,10 +13,8 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
|
||||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
|
||||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
|
||||||
import app.omnivore.omnivore.R
|
import app.omnivore.omnivore.R
|
||||||
|
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||||
import app.omnivore.omnivore.core.data.DataService
|
import app.omnivore.omnivore.core.data.DataService
|
||||||
import app.omnivore.omnivore.core.data.archiveSavedItem
|
import app.omnivore.omnivore.core.data.archiveSavedItem
|
||||||
import app.omnivore.omnivore.core.data.createWebHighlight
|
import app.omnivore.omnivore.core.data.createWebHighlight
|
||||||
@ -26,16 +24,19 @@ import app.omnivore.omnivore.core.data.mergeWebHighlights
|
|||||||
import app.omnivore.omnivore.core.data.unarchiveSavedItem
|
import app.omnivore.omnivore.core.data.unarchiveSavedItem
|
||||||
import app.omnivore.omnivore.core.data.updateWebHighlight
|
import app.omnivore.omnivore.core.data.updateWebHighlight
|
||||||
import app.omnivore.omnivore.core.data.updateWebReadingProgress
|
import app.omnivore.omnivore.core.data.updateWebReadingProgress
|
||||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||||
|
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||||
|
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||||
import app.omnivore.omnivore.core.network.Networker
|
import app.omnivore.omnivore.core.network.Networker
|
||||||
import app.omnivore.omnivore.core.network.createNewLabel
|
import app.omnivore.omnivore.core.network.createNewLabel
|
||||||
import app.omnivore.omnivore.core.network.saveUrl
|
import app.omnivore.omnivore.core.network.saveUrl
|
||||||
import app.omnivore.omnivore.core.network.savedItem
|
import app.omnivore.omnivore.core.network.savedItem
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
|
||||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
|
||||||
import app.omnivore.omnivore.feature.components.HighlightColor
|
import app.omnivore.omnivore.feature.components.HighlightColor
|
||||||
import app.omnivore.omnivore.feature.library.SavedItemAction
|
import app.omnivore.omnivore.feature.library.SavedItemAction
|
||||||
import app.omnivore.omnivore.feature.setSavedItemLabels
|
import app.omnivore.omnivore.feature.setSavedItemLabels
|
||||||
|
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||||
|
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||||
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
|
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@ -80,6 +81,7 @@ class WebReaderViewModel @Inject constructor(
|
|||||||
private val dataService: DataService,
|
private val dataService: DataService,
|
||||||
private val networker: Networker,
|
private val networker: Networker,
|
||||||
private val eventTracker: EventTracker,
|
private val eventTracker: EventTracker,
|
||||||
|
private val savedItemDao: SavedItemDao // TODO - Use repo
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID()
|
var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID()
|
||||||
var javascriptDispatchQueue: MutableList<String> = mutableListOf()
|
var javascriptDispatchQueue: MutableList<String> = mutableListOf()
|
||||||
@ -333,31 +335,11 @@ class WebReaderViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SavedItemAction.MarkRead -> {
|
SavedItemAction.MarkRead -> {
|
||||||
viewModelScope.launch {
|
// TODO
|
||||||
dataService.updateWebReadingProgress(
|
|
||||||
jsonString = Gson().toJson(
|
|
||||||
mapOf(
|
|
||||||
"id" to itemID,
|
|
||||||
"readingProgressPercent" to 100.0,
|
|
||||||
"readingProgressAnchorIndex" to 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SavedItemAction.MarkUnread -> {
|
SavedItemAction.MarkUnread -> {
|
||||||
viewModelScope.launch {
|
// TODO
|
||||||
dataService.updateWebReadingProgress(
|
|
||||||
jsonString = Gson().toJson(
|
|
||||||
mapOf(
|
|
||||||
"id" to itemID,
|
|
||||||
"readingProgressPercent" to 0,
|
|
||||||
"readingProgressAnchorIndex" to 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -381,14 +363,8 @@ class WebReaderViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun setHighlightColor(color: HighlightColor) {
|
|
||||||
// CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
// highlightColor.postValue(color)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun handleIncomingWebMessage(actionID: String, jsonString: String) {
|
fun handleIncomingWebMessage(actionID: String, jsonString: String) {
|
||||||
Log.d("sync", "incoming change: ${actionID}: ${jsonString}")
|
Log.d("sync", "incoming change: ${actionID}: $jsonString")
|
||||||
when (actionID) {
|
when (actionID) {
|
||||||
"createHighlight" -> {
|
"createHighlight" -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -410,7 +386,7 @@ class WebReaderViewModel @Inject constructor(
|
|||||||
|
|
||||||
"articleReadingProgress" -> {
|
"articleReadingProgress" -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
dataService.updateWebReadingProgress(jsonString)
|
dataService.updateWebReadingProgress(jsonString, savedItemDao)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,25 +8,23 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import app.omnivore.omnivore.navigation.Routes
|
|
||||||
import app.omnivore.omnivore.feature.auth.LoginViewModel
|
import app.omnivore.omnivore.feature.auth.LoginViewModel
|
||||||
import app.omnivore.omnivore.feature.auth.WelcomeScreen
|
import app.omnivore.omnivore.feature.auth.WelcomeScreen
|
||||||
import app.omnivore.omnivore.feature.components.LabelsViewModel
|
import app.omnivore.omnivore.feature.components.LabelsViewModel
|
||||||
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
|
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
|
||||||
import app.omnivore.omnivore.feature.library.LibraryView
|
import app.omnivore.omnivore.feature.library.LibraryView
|
||||||
import app.omnivore.omnivore.feature.library.LibraryViewModel
|
|
||||||
import app.omnivore.omnivore.feature.library.SearchView
|
import app.omnivore.omnivore.feature.library.SearchView
|
||||||
import app.omnivore.omnivore.feature.library.SearchViewModel
|
import app.omnivore.omnivore.feature.library.SearchViewModel
|
||||||
import app.omnivore.omnivore.feature.save.SaveViewModel
|
import app.omnivore.omnivore.feature.save.SaveViewModel
|
||||||
import app.omnivore.omnivore.feature.settings.PolicyWebView
|
import app.omnivore.omnivore.feature.settings.PolicyWebView
|
||||||
import app.omnivore.omnivore.feature.settings.SettingsView
|
import app.omnivore.omnivore.feature.settings.SettingsView
|
||||||
import app.omnivore.omnivore.feature.settings.SettingsViewModel
|
import app.omnivore.omnivore.feature.settings.SettingsViewModel
|
||||||
|
import app.omnivore.omnivore.navigation.Routes
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RootView(
|
fun RootView(
|
||||||
loginViewModel: LoginViewModel,
|
loginViewModel: LoginViewModel,
|
||||||
searchViewModel: SearchViewModel,
|
searchViewModel: SearchViewModel,
|
||||||
libraryViewModel: LibraryViewModel,
|
|
||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
labelsViewModel: LabelsViewModel,
|
labelsViewModel: LabelsViewModel,
|
||||||
saveViewModel: SaveViewModel,
|
saveViewModel: SaveViewModel,
|
||||||
@ -39,7 +37,6 @@ fun RootView(
|
|||||||
PrimaryNavigator(
|
PrimaryNavigator(
|
||||||
loginViewModel = loginViewModel,
|
loginViewModel = loginViewModel,
|
||||||
searchViewModel = searchViewModel,
|
searchViewModel = searchViewModel,
|
||||||
libraryViewModel = libraryViewModel,
|
|
||||||
settingsViewModel = settingsViewModel,
|
settingsViewModel = settingsViewModel,
|
||||||
labelsViewModel = labelsViewModel,
|
labelsViewModel = labelsViewModel,
|
||||||
saveViewModel = saveViewModel,
|
saveViewModel = saveViewModel,
|
||||||
@ -61,7 +58,6 @@ fun RootView(
|
|||||||
@Composable
|
@Composable
|
||||||
fun PrimaryNavigator(
|
fun PrimaryNavigator(
|
||||||
loginViewModel: LoginViewModel,
|
loginViewModel: LoginViewModel,
|
||||||
libraryViewModel: LibraryViewModel,
|
|
||||||
searchViewModel: SearchViewModel,
|
searchViewModel: SearchViewModel,
|
||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
labelsViewModel: LabelsViewModel,
|
labelsViewModel: LabelsViewModel,
|
||||||
@ -73,7 +69,6 @@ fun PrimaryNavigator(
|
|||||||
NavHost(navController = navController, startDestination = Routes.Library.route) {
|
NavHost(navController = navController, startDestination = Routes.Library.route) {
|
||||||
composable(Routes.Library.route) {
|
composable(Routes.Library.route) {
|
||||||
LibraryView(
|
LibraryView(
|
||||||
libraryViewModel = libraryViewModel,
|
|
||||||
navController = navController,
|
navController = navController,
|
||||||
labelsViewModel = labelsViewModel,
|
labelsViewModel = labelsViewModel,
|
||||||
saveViewModel = saveViewModel,
|
saveViewModel = saveViewModel,
|
||||||
|
|||||||
@ -160,36 +160,6 @@ fun readingProgress(item: SavedItemWithLabelsAndHighlights): String {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
//var highlightsText: String {
|
|
||||||
// item.hig ?.let {
|
|
||||||
// if let highlights = item.highlights, highlights.count > 0 {
|
|
||||||
// let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count)
|
|
||||||
// if item.wordsCount > 0 {
|
|
||||||
// return " • \(fmted)"
|
|
||||||
// }
|
|
||||||
// return fmted
|
|
||||||
// }
|
|
||||||
// return ""
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//var notesText: String {
|
|
||||||
// let notes = item.highlights?.filter { item in
|
|
||||||
// if let highlight = item as? Highlight {
|
|
||||||
// return !(highlight.annotation ?? "").isEmpty
|
|
||||||
// }
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if let notes = notes, notes.count > 0 {
|
|
||||||
// let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count)
|
|
||||||
// if item.wordsCount > 0 {
|
|
||||||
// return " • \(fmted)"
|
|
||||||
// }
|
|
||||||
// return fmted
|
|
||||||
// }
|
|
||||||
// return ""
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
enum class FlairIcon(
|
enum class FlairIcon(
|
||||||
val rawValue: String, val sortOrder: Int
|
val rawValue: String, val sortOrder: Int
|
||||||
|
|||||||
@ -9,6 +9,7 @@ androidxComposeCompiler = "1.5.9"
|
|||||||
androidxCore = "1.12.0"
|
androidxCore = "1.12.0"
|
||||||
androidxDataStore = "1.0.0"
|
androidxDataStore = "1.0.0"
|
||||||
androidxEspresso = "3.5.1"
|
androidxEspresso = "3.5.1"
|
||||||
|
androidxHiltNavigationCompose = "1.1.0"
|
||||||
androidxLifecycle = "2.7.0"
|
androidxLifecycle = "2.7.0"
|
||||||
androidxNavigation = "2.7.7"
|
androidxNavigation = "2.7.7"
|
||||||
androidxSecurity = "1.0.0"
|
androidxSecurity = "1.0.0"
|
||||||
@ -48,9 +49,11 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
|
|||||||
androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" }
|
androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" }
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
|
||||||
androidx-dataStore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDataStore" }
|
androidx-dataStore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDataStore" }
|
||||||
|
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }
|
||||||
androidx-lifecycle-viewModelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
|
androidx-lifecycle-viewModelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
|
||||||
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
|
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
|
||||||
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidxLifecycle" }
|
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidxLifecycle" }
|
||||||
|
androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
|
||||||
androidx-lifecycle-viewmodelSavedstate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "androidxLifecycle" }
|
androidx-lifecycle-viewmodelSavedstate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "androidxLifecycle" }
|
||||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
|
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
|
||||||
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "androidxSecurity" }
|
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "androidxSecurity" }
|
||||||
|
|||||||
Reference in New Issue
Block a user