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:
Jackson Harper
2024-02-21 09:48:20 +08:00
committed by GitHub
34 changed files with 785 additions and 660 deletions

View File

@ -118,6 +118,7 @@ dependencies {
implementation(libs.androidx.compose.ui.util)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
androidTestImplementation(libs.androidx.compose.ui.test)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.tooling.preview)
@ -165,6 +166,8 @@ dependencies {
implementation(libs.compose.markdown)
implementation(libs.chiptextfield.m3)
implementation(libs.androidx.lifecycle.runtimeCompose)
}
apollo {

View File

@ -35,7 +35,6 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
val loginViewModel: LoginViewModel by viewModels()
val libraryViewModel: LibraryViewModel by viewModels()
val settingsViewModel: SettingsViewModel by viewModels()
val searchViewModel: SearchViewModel by viewModels()
val labelsViewModel: LabelsViewModel by viewModels()
@ -65,7 +64,6 @@ class MainActivity : ComponentActivity() {
RootView(
loginViewModel,
searchViewModel,
libraryViewModel,
settingsViewModel,
labelsViewModel,
saveViewModel,

View File

@ -1,38 +1,31 @@
package app.omnivore.omnivore.core.data
import android.content.Context
import androidx.room.Room
import app.omnivore.omnivore.core.network.Networker
import app.omnivore.omnivore.core.database.AppDatabase
import app.omnivore.omnivore.core.database.entities.Highlight
import app.omnivore.omnivore.core.database.OmnivoreDatabase
import app.omnivore.omnivore.core.database.entities.SavedItem
import kotlinx.coroutines.*
import app.omnivore.omnivore.core.network.Networker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import javax.inject.Inject
class DataService @Inject constructor(
context: Context,
val networker: Networker
val networker: Networker,
omnivoreDatabase: OmnivoreDatabase
) {
val savedItemSyncChannel = Channel<SavedItem>(capacity = Channel.UNLIMITED)
val highlightSyncChannel = Channel<Highlight>(capacity = Channel.UNLIMITED)
val savedItemSyncChannel = Channel<SavedItem>(capacity = Channel.UNLIMITED)
val db = Room.databaseBuilder(
context,
AppDatabase::class.java, "omnivore-database"
)
.fallbackToDestructiveMigration()
.build()
val db = omnivoreDatabase
init {
CoroutineScope(Dispatchers.IO).launch {
startSyncChannels()
init {
CoroutineScope(Dispatchers.IO).launch {
startSyncChannels()
}
}
}
fun clearDatabase() {
CoroutineScope(Dispatchers.IO).launch {
db.clearAllTables()
fun clearDatabase() {
CoroutineScope(Dispatchers.IO).launch {
db.clearAllTables()
}
}
}
}

View File

@ -1,176 +1,185 @@
package app.omnivore.omnivore.core.data
import android.util.Log
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import app.omnivore.omnivore.core.database.entities.Highlight
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef
import app.omnivore.omnivore.core.database.entities.saveHighlightChange
import app.omnivore.omnivore.core.network.CreateHighlightParams
import app.omnivore.omnivore.core.network.DeleteHighlightParams
import app.omnivore.omnivore.core.network.MergeHighlightsParams
import app.omnivore.omnivore.core.network.UpdateHighlightParams
import app.omnivore.omnivore.core.model.ServerSyncStatus
import app.omnivore.omnivore.core.database.entities.Highlight
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRef
import app.omnivore.omnivore.core.database.entities.saveHighlightChange
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
import java.util.UUID
suspend fun DataService.createWebHighlight(jsonString: String, colorName: String?) {
val createHighlightInput = Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
val createHighlightInput =
Gson().fromJson(jsonString, CreateHighlightParams::class.java).asCreateHighlightInput()
withContext(Dispatchers.IO) {
val highlight = Highlight(
type = "HIGHLIGHT",
highlightId = createHighlightInput.id,
shortId = createHighlightInput.shortId,
quote = createHighlightInput.quote.getOrNull(),
prefix = null,
suffix = null,
patch = createHighlightInput.patch.getOrNull(),
annotation = createHighlightInput.annotation.getOrNull(),
createdAt = null,
updatedAt = null,
createdByMe = false,
color = colorName ?: createHighlightInput.color.getOrNull(),
highlightPositionPercent = createHighlightInput.highlightPositionPercent.getOrNull() ?: 0.0,
highlightPositionAnchorIndex = createHighlightInput.highlightPositionAnchorIndex.getOrNull() ?: 0
)
withContext(Dispatchers.IO) {
val highlight = Highlight(
type = "HIGHLIGHT",
highlightId = createHighlightInput.id,
shortId = createHighlightInput.shortId,
quote = createHighlightInput.quote.getOrNull(),
prefix = null,
suffix = null,
patch = createHighlightInput.patch.getOrNull(),
annotation = createHighlightInput.annotation.getOrNull(),
createdAt = null,
updatedAt = null,
createdByMe = false,
color = colorName ?: createHighlightInput.color.getOrNull(),
highlightPositionPercent = createHighlightInput.highlightPositionPercent.getOrNull()
?: 0.0,
highlightPositionAnchorIndex = createHighlightInput.highlightPositionAnchorIndex.getOrNull()
?: 0
)
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
val highlightChange = saveHighlightChange(db.highlightChangesDao(), createHighlightInput.articleId, highlight)
val highlightChange =
saveHighlightChange(db.highlightChangesDao(), createHighlightInput.articleId, highlight)
val crossRef = SavedItemAndHighlightCrossRef(
highlightId = createHighlightInput.id,
savedItemId = createHighlightInput.articleId
)
val crossRef = SavedItemAndHighlightCrossRef(
highlightId = createHighlightInput.id, savedItemId = createHighlightInput.articleId
)
db.highlightDao().insertAll(listOf(highlight))
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
db.highlightDao().insertAll(listOf(highlight))
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
performHighlightChange(highlightChange)
}
performHighlightChange(highlightChange)
}
}
suspend fun DataService.createNoteHighlight(savedItemId: String, note: String): String {
val shortId = NanoId.generate(size = 14)
val createHighlightId = UUID.randomUUID().toString()
val shortId = NanoId.generate(size = 14)
val createHighlightId = UUID.randomUUID().toString()
withContext(Dispatchers.IO) {
val highlight = Highlight(
type = "NOTE",
highlightId = createHighlightId,
shortId = shortId,
quote = null,
prefix = null,
suffix = null,
patch =null,
annotation = note,
createdAt = null,
updatedAt = null,
createdByMe = true,
color = null,
highlightPositionAnchorIndex = 0,
highlightPositionPercent = 0.0
)
withContext(Dispatchers.IO) {
val highlight = Highlight(
type = "NOTE",
highlightId = createHighlightId,
shortId = shortId,
quote = null,
prefix = null,
suffix = null,
patch = null,
annotation = note,
createdAt = null,
updatedAt = null,
createdByMe = true,
color = null,
highlightPositionAnchorIndex = 0,
highlightPositionPercent = 0.0
)
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_CREATION.rawValue
val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
val crossRef = SavedItemAndHighlightCrossRef(
highlightId = createHighlightId,
savedItemId = savedItemId
)
val crossRef = SavedItemAndHighlightCrossRef(
highlightId = createHighlightId, savedItemId = savedItemId
)
db.highlightDao().insertAll(listOf(highlight))
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
db.highlightDao().insertAll(listOf(highlight))
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
performHighlightChange(highlightChange)
}
performHighlightChange(highlightChange)
}
return createHighlightId
return createHighlightId
}
suspend fun DataService.mergeWebHighlights(jsonString: String) {
val mergeHighlightInput = Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput()
Log.d("sync", "mergeHighlightInput: " + mergeHighlightInput.id + ": " + mergeHighlightInput)
val mergeHighlightInput =
Gson().fromJson(jsonString, MergeHighlightsParams::class.java).asMergeHighlightInput()
Log.d("sync", "mergeHighlightInput: " + mergeHighlightInput.id + ": " + mergeHighlightInput)
withContext(Dispatchers.IO) {
val highlight = Highlight(
type = "HIGHLIGHT",
highlightId = mergeHighlightInput.id,
shortId = mergeHighlightInput.shortId,
quote = mergeHighlightInput.quote,
prefix = null,
suffix = null,
patch = mergeHighlightInput.patch,
annotation = mergeHighlightInput.annotation.getOrNull(),
createdAt = null,
updatedAt = null,
createdByMe = false,
color = mergeHighlightInput.color.getOrNull(),
highlightPositionPercent = mergeHighlightInput.highlightPositionPercent.getOrNull() ?: 0.0,
highlightPositionAnchorIndex = mergeHighlightInput.highlightPositionAnchorIndex.getOrNull() ?: 0
)
withContext(Dispatchers.IO) {
val highlight = Highlight(
type = "HIGHLIGHT",
highlightId = mergeHighlightInput.id,
shortId = mergeHighlightInput.shortId,
quote = mergeHighlightInput.quote,
prefix = null,
suffix = null,
patch = mergeHighlightInput.patch,
annotation = mergeHighlightInput.annotation.getOrNull(),
createdAt = null,
updatedAt = null,
createdByMe = false,
color = mergeHighlightInput.color.getOrNull(),
highlightPositionPercent = mergeHighlightInput.highlightPositionPercent.getOrNull()
?: 0.0,
highlightPositionAnchorIndex = mergeHighlightInput.highlightPositionAnchorIndex.getOrNull()
?: 0
)
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_MERGE.rawValue
val highlightChange = saveHighlightChange(
db.highlightChangesDao(),
mergeHighlightInput.articleId,
highlight,
html = mergeHighlightInput.html.getOrNull(),
overlappingIDs = mergeHighlightInput.overlapHighlightIdList
)
val highlightChange = saveHighlightChange(
db.highlightChangesDao(),
mergeHighlightInput.articleId,
highlight,
html = mergeHighlightInput.html.getOrNull(),
overlappingIDs = mergeHighlightInput.overlapHighlightIdList
)
val crossRef = SavedItemAndHighlightCrossRef(
highlightId = mergeHighlightInput.id,
savedItemId = mergeHighlightInput.articleId
)
val crossRef = SavedItemAndHighlightCrossRef(
highlightId = mergeHighlightInput.id, savedItemId = mergeHighlightInput.articleId
)
db.highlightDao().insertAll(listOf(highlight))
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
db.highlightDao().insertAll(listOf(highlight))
db.savedItemAndHighlightCrossRefDao().insertAll(listOf(crossRef))
Log.d("sync", "Setting up highlight merge")
performHighlightChange(highlightChange)
}
Log.d("sync", "Setting up highlight merge")
performHighlightChange(highlightChange)
}
}
suspend fun DataService.updateWebHighlight(jsonString: String) {
val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java)
val updateHighlightParams = Gson().fromJson(jsonString, UpdateHighlightParams::class.java)
if (updateHighlightParams.highlightId == null || updateHighlightParams.libraryItemId == null) {
Log.d("error","ERROR INVALID HIGHLIGHT DATA")
return
}
if (updateHighlightParams.highlightId == null || updateHighlightParams.libraryItemId == null) {
Log.d("error", "ERROR INVALID HIGHLIGHT DATA")
return
}
withContext(Dispatchers.IO) {
val highlight = db.highlightDao().findById(highlightId = updateHighlightParams.highlightId ?: "") ?: return@withContext
withContext(Dispatchers.IO) {
val highlight =
db.highlightDao().findById(highlightId = updateHighlightParams.highlightId)
?: return@withContext
highlight.annotation = updateHighlightParams.annotation
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
db.highlightDao().update(highlight)
highlight.annotation = updateHighlightParams.annotation
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
db.highlightDao().update(highlight)
val highlightChange = saveHighlightChange(db.highlightChangesDao(), updateHighlightParams.libraryItemId ?: "", highlight)
performHighlightChange(highlightChange)
}
val highlightChange = saveHighlightChange(
db.highlightChangesDao(), updateHighlightParams.libraryItemId, highlight
)
performHighlightChange(highlightChange)
}
}
suspend fun DataService.deleteHighlightFromJSON(jsonString: String) {
val deleteHighlightParams = Gson().fromJson(jsonString, DeleteHighlightParams::class.java)
deleteHighlight(deleteHighlightParams.libraryItemId, deleteHighlightParams.highlightId)
val deleteHighlightParams = Gson().fromJson(jsonString, DeleteHighlightParams::class.java)
deleteHighlight(deleteHighlightParams.libraryItemId, deleteHighlightParams.highlightId)
}
private suspend fun DataService.deleteHighlight(savedItemId: String, highlightID: String) {
withContext(Dispatchers.IO) {
val highlight = db.highlightDao().findById(highlightId = highlightID)
withContext(Dispatchers.IO) {
val highlight = db.highlightDao().findById(highlightId = highlightID)
highlight?.let {
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
db.highlightDao().update(highlight)
highlight?.let {
highlight.serverSyncStatus = ServerSyncStatus.NEEDS_DELETION.rawValue
db.highlightDao().update(highlight)
val highlightChange = saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
performHighlightChange(highlightChange)
val highlightChange =
saveHighlightChange(db.highlightChangesDao(), savedItemId, highlight)
performHighlightChange(highlightChange)
}
}
}
}

View File

@ -8,7 +8,7 @@ import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighli
import app.omnivore.omnivore.core.network.savedItem
import app.omnivore.omnivore.core.network.savedItemUpdates
import app.omnivore.omnivore.core.network.search
import app.omnivore.omnivore.core.model.ServerSyncStatus
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult {
val searchResult = networker.search(cursor = cursor, limit = 10, query = query)

View File

@ -1,28 +1,34 @@
package app.omnivore.omnivore.core.data
import app.omnivore.omnivore.core.model.ServerSyncStatus
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import app.omnivore.omnivore.core.database.dao.SavedItemDao
import app.omnivore.omnivore.core.network.ReadingProgressParams
import app.omnivore.omnivore.core.network.updateReadingProgress
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun DataService.updateWebReadingProgress(jsonString: String) {
suspend fun DataService.updateWebReadingProgress(
jsonString: String,
savedItemDao: SavedItemDao
) {
val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
val savedItemId = readingProgressParams.id ?: return
withContext(Dispatchers.IO) {
val savedItem = db.savedItemDao().findById(savedItemId) ?: return@withContext
savedItem.readingProgress = readingProgressParams.readingProgressPercent ?: 0.0
savedItem.readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
db.savedItemDao().update(savedItem)
val savedItem = savedItemDao.findById(savedItemId) ?: return@withContext
val updatedItem = savedItem.copy(
readingProgress = readingProgressParams.readingProgressPercent ?: 0.0,
readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0,
serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
)
savedItemDao.update(updatedItem)
val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
if (isUpdatedOnServer) {
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
db.savedItemDao().update(savedItem)
updatedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
savedItemDao.update(updatedItem)
}
}
}

View File

@ -1,6 +1,6 @@
package app.omnivore.omnivore.core.data
import app.omnivore.omnivore.core.model.ServerSyncStatus
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import app.omnivore.omnivore.core.network.archiveSavedItem
import app.omnivore.omnivore.core.network.deleteSavedItem
import app.omnivore.omnivore.core.network.unarchiveSavedItem

View File

@ -1,6 +1,10 @@
package app.omnivore.omnivore.core.data
import android.util.Log
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import app.omnivore.omnivore.core.database.entities.HighlightChange
import app.omnivore.omnivore.core.database.entities.SavedItem
import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight
import app.omnivore.omnivore.core.network.ReadingProgressParams
import app.omnivore.omnivore.core.network.createHighlight
import app.omnivore.omnivore.core.network.deleteHighlights
@ -13,10 +17,6 @@ import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
import app.omnivore.omnivore.graphql.generated.type.HighlightType
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
import app.omnivore.omnivore.core.model.ServerSyncStatus
import app.omnivore.omnivore.core.database.entities.HighlightChange
import app.omnivore.omnivore.core.database.entities.SavedItem
import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight
import com.apollographql.apollo3.api.Optional
import kotlinx.coroutines.delay
@ -50,7 +50,7 @@ suspend fun DataService.syncOfflineItemsWithServerIfNeeded() {
}
private suspend fun DataService.syncSavedItem(item: SavedItem) {
fun updateSyncStatus(status: ServerSyncStatus) {
suspend fun updateSyncStatus(status: ServerSyncStatus) {
item.serverSyncStatus = status.rawValue
db.savedItemDao().update(item)
}

View File

@ -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>
)

View File

@ -1,4 +1,4 @@
package app.omnivore.omnivore.core.model
package app.omnivore.omnivore.core.data.model
enum class ServerSyncStatus(val rawValue: Int) {
IS_SYNCED(0),

View File

@ -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
)
}

View File

@ -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) }
}
}
}

View File

@ -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)
}

View File

@ -2,6 +2,7 @@ package app.omnivore.omnivore.core.database
import androidx.room.Database
import androidx.room.RoomDatabase
import app.omnivore.omnivore.core.database.dao.SavedItemDao
import app.omnivore.omnivore.core.database.entities.Highlight
import app.omnivore.omnivore.core.database.entities.HighlightChange
import app.omnivore.omnivore.core.database.entities.HighlightChangesDao
@ -11,7 +12,6 @@ import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRe
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRefDao
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRefDao
import app.omnivore.omnivore.core.database.entities.SavedItemDao
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.core.database.entities.SavedItemLabelDao
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlightsDao
@ -19,24 +19,24 @@ import app.omnivore.omnivore.core.database.entities.Viewer
import app.omnivore.omnivore.core.database.entities.ViewerDao
@Database(
entities = [
Viewer::class,
SavedItem::class,
SavedItemLabel::class,
Highlight::class,
HighlightChange::class,
SavedItemAndSavedItemLabelCrossRef::class,
SavedItemAndHighlightCrossRef::class
],
version = 24
entities = [
Viewer::class,
SavedItem::class,
SavedItemLabel::class,
Highlight::class,
HighlightChange::class,
SavedItemAndSavedItemLabelCrossRef::class,
SavedItemAndHighlightCrossRef::class],
version = 24,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun viewerDao(): ViewerDao
abstract fun savedItemDao(): SavedItemDao
abstract fun highlightDao(): HighlightDao
abstract fun highlightChangesDao(): HighlightChangesDao
abstract fun savedItemLabelDao(): SavedItemLabelDao
abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao
abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao
abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao
abstract class OmnivoreDatabase : RoomDatabase() {
abstract fun viewerDao(): ViewerDao
abstract fun savedItemDao(): SavedItemDao
abstract fun highlightDao(): HighlightDao
abstract fun highlightChangesDao(): HighlightChangesDao
abstract fun savedItemLabelDao(): SavedItemLabelDao
abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao
abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao
abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao
}

View File

@ -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>>
}

View File

@ -1,7 +1,7 @@
package app.omnivore.omnivore.core.database.entities
import androidx.room.*
import app.omnivore.omnivore.core.model.ServerSyncStatus
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import com.google.gson.annotations.SerializedName
@ -73,7 +73,20 @@ data class SavedItemWithLabelsAndHighlights(
associateBy = Junction(SavedItemAndHighlightCrossRef::class)
)
val highlights: List<Highlight>
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SavedItemWithLabelsAndHighlights
return savedItem.savedItemId == other.savedItem.savedItemId
}
override fun hashCode(): Int {
return savedItem.savedItemId.hashCode()
}
}
@Dao
interface HighlightDao {

View File

@ -9,7 +9,7 @@ import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import app.omnivore.omnivore.core.model.ServerSyncStatus
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

View File

@ -1,9 +1,13 @@
package app.omnivore.omnivore.core.database.entities
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.room.*
import java.util.*
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Transaction
@Entity
data class SavedItem(
@ -128,114 +132,7 @@ abstract class SavedItemWithLabelsAndHighlightsDao {
}
}
@Dao
interface SavedItemDao {
@Query("SELECT * FROM savedItem")
fun getAll(): List<SavedItem>
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
fun findById(itemID: String): SavedItem?
@Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0")
fun getUnSynced(): List<SavedItem>
@Query("SELECT * FROM savedItem WHERE slug = :slug")
fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights?
@Query("DELETE FROM savedItem WHERE savedItemId = :itemID")
fun deleteById(itemID: String)
@Query("DELETE FROM savedItem WHERE savedItemId in (:itemIDs)")
fun deleteByIds(itemIDs: List<String>)
@Update
fun update(savedItem: SavedItem)
@Transaction
@Query(
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
"FROM SavedItem " +
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
"WHERE SavedItem.savedItemId = :savedItemId " +
"GROUP BY SavedItem.savedItemId "
)
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights>
@Transaction
@Query(
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
"FROM SavedItem " +
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
"WHERE SavedItem.savedItemId = :savedItemId " +
"GROUP BY SavedItem.savedItemId "
)
suspend fun getById(savedItemId: String): SavedItemWithLabelsAndHighlights?
@Transaction
@Query(
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
"FROM SavedItem " +
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
"WHERE SavedItem.serverSyncStatus != 2 " +
"AND SavedItem.isArchived IN (:allowedArchiveStates) " +
"AND SavedItem.contentReader IN (:allowedContentReaders) " +
"AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " +
"AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " +
"GROUP BY SavedItem.savedItemId " +
"ORDER BY \n" +
"CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" +
"CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" +
"CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" +
"CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC"
)
fun _filteredLibraryData(
allowedArchiveStates: List<Int>,
sortKey: String,
hasRequiredLabels: Int,
hasExcludedLabels: Int,
requiredLabels: List<String>,
excludedLabels: List<String>,
allowedContentReaders: List<String>
): LiveData<List<SavedItemWithLabelsAndHighlights>>
fun filteredLibraryData(
allowedArchiveStates: List<Int>,
sortKey: String,
requiredLabels: List<String>,
excludedLabels: List<String>,
allowedContentReaders: List<String>
): LiveData<List<SavedItemWithLabelsAndHighlights>> {
return _filteredLibraryData(
allowedArchiveStates = allowedArchiveStates,
sortKey = sortKey,
hasRequiredLabels = requiredLabels.size,
hasExcludedLabels = excludedLabels.size,
requiredLabels = requiredLabels,
excludedLabels = excludedLabels,
allowedContentReaders = allowedContentReaders
)
}
}
object SavedItemQueryConstants {

View File

@ -1,66 +1,70 @@
package app.omnivore.omnivore.core.database.entities
import androidx.lifecycle.LiveData
import androidx.room.*
import app.omnivore.omnivore.core.model.ServerSyncStatus
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Transaction
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
@Entity
data class SavedItemLabel(
@PrimaryKey val savedItemLabelId: String,
val name: String,
val color: String,
val createdAt: String?,
val labelDescription: String?,
val serverSyncStatus: Int = 0
@PrimaryKey val savedItemLabelId: String,
val name: String,
val color: String,
val createdAt: String?,
val labelDescription: String?,
val serverSyncStatus: Int = 0
)
@Dao
interface SavedItemLabelDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(items: List<SavedItemLabel>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(items: List<SavedItemLabel>)
@Transaction
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
@Transaction
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
@Transaction
@Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId")
fun updateTempLabel(tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED)
@Transaction
@Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId")
fun updateTempLabel(
tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED
)
@Transaction
@Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC")
fun namedLabels(names: List<String>): List<SavedItemLabel>
@Transaction
@Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC")
fun namedLabels(names: List<String>): List<SavedItemLabel>
}
@Entity(
primaryKeys = ["savedItemLabelId", "savedItemId"],
foreignKeys = [
ForeignKey(
entity = SavedItem::class,
parentColumns = arrayOf("savedItemId"),
childColumns = arrayOf("savedItemId"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = SavedItemLabel::class,
parentColumns = arrayOf("savedItemLabelId"),
childColumns = arrayOf("savedItemLabelId")
)
]
primaryKeys = ["savedItemLabelId", "savedItemId"], foreignKeys = [ForeignKey(
entity = SavedItem::class,
parentColumns = arrayOf("savedItemId"),
childColumns = arrayOf("savedItemId"),
onDelete = ForeignKey.CASCADE
), ForeignKey(
entity = SavedItemLabel::class,
parentColumns = arrayOf("savedItemLabelId"),
childColumns = arrayOf("savedItemLabelId")
)]
)
data class SavedItemAndSavedItemLabelCrossRef(
val savedItemLabelId: String,
val savedItemId: String
val savedItemLabelId: String, val savedItemId: String
)
@Dao
interface SavedItemAndSavedItemLabelCrossRefDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
@Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId")
fun deleteRefsBySavedItemId(savedItemId: String)
@Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId")
fun deleteRefsBySavedItemId(savedItemId: String)
}
// has many highlights

View File

@ -1,25 +1,20 @@
package app.omnivore.omnivore.core.network
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.utils.Constants
import app.omnivore.omnivore.utils.DatastoreKeys
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import com.apollographql.apollo3.ApolloClient
import javax.inject.Inject
class Networker @Inject constructor(
private val datastoreRepo: DatastoreRepository
private val datastoreRepo: DatastoreRepository
) {
suspend fun baseUrl() = datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL
suspend fun baseUrl() =
datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL
suspend fun serverUrl() = "${baseUrl()}/api/graphql"
private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: ""
private suspend fun serverUrl() = "${baseUrl()}/api/graphql"
private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: ""
suspend fun publicApolloClient() = ApolloClient.Builder()
.serverUrl(serverUrl())
.build()
suspend fun authenticatedApolloClient() = ApolloClient.Builder()
.serverUrl(serverUrl())
.addHttpHeader("Authorization", value = authToken())
.build()
suspend fun authenticatedApolloClient() = ApolloClient.Builder().serverUrl(serverUrl())
.addHttpHeader("Authorization", value = authToken()).build()
}

View File

@ -4,7 +4,7 @@ import app.omnivore.omnivore.core.database.entities.Highlight
import app.omnivore.omnivore.core.database.entities.SavedItem
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.graphql.generated.SearchQuery
import app.omnivore.omnivore.core.model.ServerSyncStatus
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import com.apollographql.apollo3.api.Optional
data class LibrarySearchQueryResponse(

View File

@ -1,10 +1,11 @@
package app.omnivore.omnivore.di
import android.content.Context
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.core.analytics.EventTracker
import app.omnivore.omnivore.core.datastore.OmnivoreDatastore
import app.omnivore.omnivore.core.data.DataService
import app.omnivore.omnivore.core.database.OmnivoreDatabase
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.core.datastore.OmnivoreDatastore
import app.omnivore.omnivore.core.network.Networker
import dagger.Module
import dagger.Provides
@ -34,7 +35,8 @@ object AppModule {
@Singleton
@Provides
fun provideDataService(
@ApplicationContext app: Context,
networker: Networker
) = DataService(app, networker)
networker: Networker,
omnivoreDatabase: OmnivoreDatabase
) = DataService(networker, omnivoreDatabase)
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -1,7 +1,7 @@
package app.omnivore.omnivore.feature.components
import androidx.lifecycle.*
import app.omnivore.omnivore.core.model.ServerSyncStatus
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import dagger.hilt.android.lifecycle.HiltViewModel
import java.time.LocalDate

View File

@ -1,117 +1,124 @@
package app.omnivore.omnivore.feature.library
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Icon
import androidx.compose.material3.SuggestionChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import app.omnivore.omnivore.R
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.feature.components.LabelChipColors
@Composable
fun LibraryFilterBar(viewModel: LibraryViewModel) {
var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) }
val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(SavedItemFilter.INBOX)
val activeLabels: List<SavedItemLabel> by viewModel.activeLabelsLiveData.observeAsState(listOf())
fun LibraryFilterBar(
viewModel: LibraryViewModel = hiltViewModel()
) {
var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) }
val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(
SavedItemFilter.INBOX
)
val activeLabels: List<SavedItemLabel> by viewModel.activeLabelsLiveData.observeAsState(listOf())
var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) }
val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(SavedItemSortFilter.NEWEST)
val listState = rememberLazyListState()
var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) }
val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(
SavedItemSortFilter.NEWEST
)
val listState = rememberLazyListState()
Column {
LazyRow(
state = listState,
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 6.dp)
.fillMaxWidth()
) {
item {
AssistChip(
onClick = { isSavedItemFilterMenuExpanded = true },
label = { Text(activeSavedItemFilter.displayText) },
trailingIcon = {
Icon(
Icons.Default.ArrowDropDown,
contentDescription = "drop down button to change primary library filter"
)
},
modifier = Modifier.padding(end = 6.dp)
)
AssistChip(
onClick = { isSavedItemSortFilterMenuExpanded = true },
label = { Text(activeSavedItemSortFilter.displayText) },
trailingIcon = {
Icon(
Icons.Default.ArrowDropDown,
contentDescription = "drop down button to change library sort order"
)
},
modifier = Modifier.padding(end = 6.dp)
)
AssistChip(
onClick = { viewModel.bottomSheetState.value = LibraryBottomSheetState.LABEL },
label = { Text(stringResource(R.string.library_filter_bar_label_labels)) },
trailingIcon = {
Icon(
Icons.Default.ArrowDropDown,
contentDescription = "drop down button to open label selection sheet"
)
},
modifier = Modifier.padding(end = 6.dp)
)
}
items(activeLabels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label ->
val chipColors = LabelChipColors.fromHex(label.color)
Column {
LazyRow(
state = listState,
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 6.dp)
.fillMaxWidth()
) {
item {
AssistChip(onClick = { isSavedItemFilterMenuExpanded = true },
label = { Text(activeSavedItemFilter.displayText) },
trailingIcon = {
Icon(
Icons.Default.ArrowDropDown,
contentDescription = "drop down button to change primary library filter"
)
},
modifier = Modifier.padding(end = 6.dp)
)
AssistChip(onClick = { isSavedItemSortFilterMenuExpanded = true },
label = { Text(activeSavedItemSortFilter.displayText) },
trailingIcon = {
Icon(
Icons.Default.ArrowDropDown,
contentDescription = "drop down button to change library sort order"
)
},
modifier = Modifier.padding(end = 6.dp)
)
AssistChip(
onClick = { viewModel.bottomSheetState.value = LibraryBottomSheetState.LABEL },
label = { Text(stringResource(R.string.library_filter_bar_label_labels)) },
trailingIcon = {
Icon(
Icons.Default.ArrowDropDown,
contentDescription = "drop down button to open label selection sheet"
)
},
modifier = Modifier.padding(end = 6.dp)
)
}
items(activeLabels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label ->
val chipColors = LabelChipColors.fromHex(label.color)
AssistChip(
onClick = {
viewModel.updateAppliedLabels(
(viewModel.activeLabelsLiveData.value ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId }
)
},
label = { Text(label.name) },
border = null,
colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
containerColor = chipColors.containerColor,
labelColor = chipColors.textColor,
iconContentColor = chipColors.textColor
),
trailingIcon = {
Icon(
Icons.Default.Close,
contentDescription = "close icon to remove label"
)
},
modifier = Modifier
.padding(horizontal = 4.dp)
)
}
AssistChip(onClick = {
viewModel.updateAppliedLabels((viewModel.activeLabelsLiveData.value
?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId })
},
label = { Text(label.name) },
border = null,
colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
containerColor = chipColors.containerColor,
labelColor = chipColors.textColor,
iconContentColor = chipColors.textColor
),
trailingIcon = {
Icon(
Icons.Default.Close, contentDescription = "close icon to remove label"
)
},
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}
SavedItemFilterContextMenu(isExpanded = isSavedItemFilterMenuExpanded,
onDismiss = { isSavedItemFilterMenuExpanded = false },
actionHandler = { viewModel.updateSavedItemFilter(it) })
SavedItemSortFilterContextMenu(isExpanded = isSavedItemSortFilterMenuExpanded,
onDismiss = { isSavedItemSortFilterMenuExpanded = false },
actionHandler = { viewModel.updateSavedItemSortFilter(it) })
}
SavedItemFilterContextMenu(
isExpanded = isSavedItemFilterMenuExpanded,
onDismiss = { isSavedItemFilterMenuExpanded = false },
actionHandler = { viewModel.updateSavedItemFilter(it) }
)
SavedItemSortFilterContextMenu(
isExpanded = isSavedItemSortFilterMenuExpanded,
onDismiss = { isSavedItemSortFilterMenuExpanded = false },
actionHandler = { viewModel.updateSavedItemSortFilter(it) }
)
}
}

View File

@ -131,7 +131,7 @@ fun LibraryNavigationBar(
contentDescription = null
)
}
/* IconButton(onClick = { isMenuExpanded = true } ) {
IconButton(onClick = { isMenuExpanded = true } ) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null
@ -144,7 +144,7 @@ fun LibraryNavigationBar(
onDismiss = { isMenuExpanded = false },
)
}
}*/
}
} ?: run {
IconButton(onClick = onSearchClicked) {
Icon(

View File

@ -20,7 +20,6 @@ import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.ScaffoldState
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
@ -32,9 +31,11 @@ import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.rememberDismissState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -50,8 +51,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
@ -70,50 +74,52 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@Composable
fun LibraryView(
libraryViewModel: LibraryViewModel,
internal fun LibraryView(
labelsViewModel: LabelsViewModel,
saveViewModel: SaveViewModel,
editInfoViewModel: EditInfoViewModel,
navController: NavHostController
navController: NavHostController,
viewModel: LibraryViewModel = hiltViewModel()
) {
val scaffoldState: ScaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
val showBottomSheet: LibraryBottomSheetState by libraryViewModel.bottomSheetState.observeAsState(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val showBottomSheet: LibraryBottomSheetState by viewModel.bottomSheetState.observeAsState(
LibraryBottomSheetState.HIDDEN
)
libraryViewModel.snackbarMessage?.let {
viewModel.snackbarMessage?.let {
coroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(it)
libraryViewModel.clearSnackbarMessage()
viewModel.clearSnackbarMessage()
}
}
when (showBottomSheet) {
LibraryBottomSheetState.ADD_LINK -> {
AddLinkBottomSheet(saveViewModel) {
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
}
}
LibraryBottomSheetState.LABEL -> {
LabelBottomSheet(
libraryViewModel,
viewModel,
labelsViewModel
) {
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
}
}
LibraryBottomSheetState.EDIT -> {
EditBottomSheet(
editInfoViewModel,
libraryViewModel
viewModel
) {
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
}
}
@ -122,21 +128,38 @@ fun LibraryView(
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
LibraryNavigationBar(
savedItemViewModel = libraryViewModel,
savedItemViewModel = viewModel,
onSearchClicked = { navController.navigate(Routes.Search.route) },
onAddLinkClicked = { showAddLinkBottomSheet(libraryViewModel) },
onAddLinkClicked = { showAddLinkBottomSheet(viewModel) },
onSettingsIconClick = { navController.navigate(Routes.Settings.route) }
)
},
) { paddingValues ->
LibraryViewContent(
libraryViewModel,
modifier = Modifier
.padding(top = paddingValues.calculateTopPadding())
)
when (uiState) {
is LibraryUiState.Success -> {
LibraryViewContent(
viewModel,
modifier = Modifier
.padding(top = paddingValues.calculateTopPadding()),
uiState = uiState
)
}
is LibraryUiState.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(strokeCap = StrokeCap.Round)
}
}
else -> {
// TODO
}
}
}
}
@ -169,7 +192,7 @@ fun LabelBottomSheet(
labelsViewModel = labelsViewModel,
initialSelectedLabels = currentSavedItemData.labels,
onCancel = {
libraryViewModel.currentItemLiveData.value = null
libraryViewModel.currentItem.value = null
onDismiss()
},
isLibraryMode = false,
@ -180,7 +203,7 @@ fun LabelBottomSheet(
labels = it
)
}
libraryViewModel.currentItemLiveData.value = null
libraryViewModel.currentItem.value = null
onDismiss()
},
onCreateLabel = { newLabelName, labelHexValue ->
@ -196,7 +219,7 @@ fun LabelBottomSheet(
isLibraryMode = true,
onSave = {
libraryViewModel.updateAppliedLabels(it)
libraryViewModel.currentItemLiveData.value = null
libraryViewModel.currentItem.value = null
onDismiss()
},
onCreateLabel = { newLabelName, labelHexValue ->
@ -257,11 +280,11 @@ fun EditBottomSheet(
description = currentSavedItemData?.savedItem?.descriptionText,
viewModel = editInfoViewModel,
onCancel = {
libraryViewModel.currentItemLiveData.value = null
libraryViewModel.currentItem.value = null
onDismiss()
},
onUpdated = {
libraryViewModel.currentItemLiveData.value = null
libraryViewModel.currentItem.value = null
libraryViewModel.refresh()
onDismiss()
}
@ -272,7 +295,11 @@ fun EditBottomSheet(
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
fun LibraryViewContent(
libraryViewModel: LibraryViewModel,
modifier: Modifier,
uiState: LibraryUiState
) {
val context = LocalContext.current
val listState = rememberLazyListState()
@ -282,9 +309,6 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
)
val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState()
val cardsData: List<SavedItemWithLabelsAndHighlights> by libraryViewModel.itemsLiveData.observeAsState(
listOf()
)
Box(
modifier = Modifier
@ -299,13 +323,12 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
modifier = modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize()
.padding(horizontal = 6.dp)
) {
item {
LibraryFilterBar(libraryViewModel)
}
items(
items = cardsData,
items = (uiState as LibraryUiState.Success).items,
key = { item -> item.savedItem.savedItemId }
) { cardDataWithLabels ->
val swipeThreshold = 0.45f
@ -388,10 +411,15 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
dismissContent = {
val selected =
currentItem.savedItemId == selectedItem?.savedItem?.savedItemId
val test = SavedItemWithLabelsAndHighlights(
savedItem = cardDataWithLabels.savedItem,
labels = listOf(),
highlights = listOf()
)
SavedItemCard(
selected = selected,
savedItemViewModel = libraryViewModel,
savedItem = cardDataWithLabels,
savedItem = test,
onClickHandler = {
libraryViewModel.actionsMenuItemLiveData.postValue(null)
val activityClass =
@ -417,7 +445,7 @@ fun LibraryViewContent(libraryViewModel: LibraryViewModel, modifier: Modifier) {
}
InfiniteListHandler(listState = listState) {
if (cardsData.isEmpty()) {
if ((uiState as LibraryUiState.Success).items.isEmpty()) {
Log.d("sync", "loading with load func")
libraryViewModel.initialLoad()
} else {

View File

@ -7,8 +7,6 @@ import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.omnivore.omnivore.utils.DatastoreKeys
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.R
import app.omnivore.omnivore.core.data.DataService
import app.omnivore.omnivore.core.data.archiveSavedItem
@ -16,25 +14,29 @@ import app.omnivore.omnivore.core.data.deleteSavedItem
import app.omnivore.omnivore.core.data.fetchSavedItemContent
import app.omnivore.omnivore.core.data.isSavedItemContentStoredInDB
import app.omnivore.omnivore.core.data.librarySearch
import app.omnivore.omnivore.core.data.model.LibraryQuery
import app.omnivore.omnivore.core.data.repository.LibraryRepository
import app.omnivore.omnivore.core.data.sync
import app.omnivore.omnivore.core.data.syncLabels
import app.omnivore.omnivore.core.data.syncOfflineItemsWithServerIfNeeded
import app.omnivore.omnivore.core.data.unarchiveSavedItem
import app.omnivore.omnivore.core.data.updateWebReadingProgress
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
import app.omnivore.omnivore.core.network.Networker
import app.omnivore.omnivore.core.network.createNewLabel
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.core.network.Networker
import app.omnivore.omnivore.core.network.createNewLabel
import app.omnivore.omnivore.feature.ResourceProvider
import app.omnivore.omnivore.feature.setSavedItemLabels
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
import app.omnivore.omnivore.utils.DatastoreKeys
import com.apollographql.apollo3.api.Optional
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -46,34 +48,60 @@ class LibraryViewModel @Inject constructor(
private val networker: Networker,
private val dataService: DataService,
private val datastoreRepo: DatastoreRepository,
private val resourceProvider: ResourceProvider
private val resourceProvider: ResourceProvider,
private val libraryRepository: LibraryRepository,
) : ViewModel(), SavedItemViewModel {
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
private var cursor: String? = null
private var librarySearchCursor: String? = null
// These are used to make sure we handle search result
// responses in the right order
private var searchIdx = 0
private var receivedIdx = 0
var snackbarMessage by mutableStateOf<String?>(null)
private set
// Live Data
private var itemsLiveDataInternal = dataService.db.savedItemDao().filteredLibraryData(
allowedArchiveStates = listOf(0),
sortKey = "newest",
requiredLabels = listOf(),
excludedLabels = listOf(),
allowedContentReaders = listOf("WEB", "PDF", "EPUB")
private val _libraryQuery = MutableStateFlow(
LibraryQuery(
allowedArchiveStates = listOf(0),
sortKey = "newest",
requiredLabels = listOf(),
excludedLabels = listOf(),
allowedContentReaders = listOf("WEB", "PDF", "EPUB")
)
)
val itemsLiveData = MediatorLiveData<List<SavedItemWithLabelsAndHighlights>>()
// Correct way - but not working
/* val uiState: StateFlow<LibraryUiState> = _libraryQuery.flatMapLatest { query ->
libraryRepository.getSavedItems(query)
}
.map(LibraryUiState::Success)
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = LibraryUiState.Loading
)*/
// This approach needs to be replaced with the StateFlow above after fixing Room Flow
private val _uiState = MutableStateFlow<LibraryUiState>(LibraryUiState.Loading)
val uiState: StateFlow<LibraryUiState> = _uiState
init {
loadSavedItems()
}
private fun loadSavedItems() {
viewModelScope.launch {
libraryRepository.getSavedItems(_libraryQuery.value)
.collect { favoriteNews ->
_uiState.value = LibraryUiState.Success(favoriteNews)
}
}
}
private val itemsLiveData = MediatorLiveData<List<SavedItemWithLabelsAndHighlights>>()
val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX)
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
val bottomSheetState = MutableLiveData(LibraryBottomSheetState.HIDDEN)
val currentItemLiveData = MutableLiveData<String?>(null)
val currentItem = mutableStateOf<String?>(null)
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
val activeLabelsLiveData = MutableLiveData<List<SavedItemLabel>>(listOf())
@ -83,6 +111,7 @@ class LibraryViewModel @Inject constructor(
private var hasLoadedInitialFilters = false
private fun loadInitialFilterValues() {
if (hasLoadedInitialFilters) {
return
}
@ -130,8 +159,6 @@ class LibraryViewModel @Inject constructor(
hasLoadedInitialFilters = false
cursor = null
librarySearchCursor = null
searchIdx = 0
receivedIdx = 0
}
if (hasLoadedInitialFilters) {
@ -153,8 +180,7 @@ class LibraryViewModel @Inject constructor(
viewModelScope.launch {
withContext(Dispatchers.IO) {
val result = dataService.librarySearch(
cursor = librarySearchCursor,
query = searchQueryString()
cursor = librarySearchCursor, query = searchQueryString()
)
result.cursor?.let {
librarySearchCursor = it
@ -237,17 +263,14 @@ class LibraryViewModel @Inject constructor(
else -> listOf()
}
val newData = dataService.db.savedItemDao().filteredLibraryData(
_libraryQuery.value = LibraryQuery(
allowedArchiveStates = allowedArchiveStates,
sortKey = sortKey,
requiredLabels = requiredLabels,
excludedLabels = excludeLabels,
allowedContentReaders = allowedContentReaders
)
itemsLiveData.removeSource(itemsLiveDataInternal)
itemsLiveDataInternal = newData
itemsLiveData.addSource(itemsLiveDataInternal, itemsLiveData::setValue)
loadSavedItems()
}
}
@ -316,42 +339,28 @@ class LibraryViewModel @Inject constructor(
}
SavedItemAction.EditLabels -> {
currentItemLiveData.value = itemID
currentItem.value = itemID
bottomSheetState.value = LibraryBottomSheetState.LABEL
}
SavedItemAction.EditInfo -> {
currentItemLiveData.value = itemID
currentItem.value = itemID
bottomSheetState.value = LibraryBottomSheetState.EDIT
}
SavedItemAction.MarkRead -> {
viewModelScope.launch {
dataService.updateWebReadingProgress(
jsonString = Gson().toJson(
mapOf(
"id" to itemID,
"readingProgressPercent" to 100.0,
"readingProgressAnchorIndex" to 0,
"force" to true
)
)
)
_uiState.value = LibraryUiState.Success(emptyList())
libraryRepository.updateReadingProgress(itemID, 100.0, 0)
loadSavedItems()
}
}
SavedItemAction.MarkUnread -> {
viewModelScope.launch {
dataService.updateWebReadingProgress(
jsonString = Gson().toJson(
mapOf(
"id" to itemID,
"readingProgressPercent" to 0,
"readingProgressAnchorIndex" to 0,
"force" to true
)
)
)
_uiState.value = LibraryUiState.Success(emptyList())
libraryRepository.updateReadingProgress(itemID, 0.0, 0)
loadSavedItems()
}
}
}
@ -424,7 +433,7 @@ class LibraryViewModel @Inject constructor(
}
fun currentSavedItemUnderEdit(): SavedItemWithLabelsAndHighlights? {
currentItemLiveData.value?.let { itemID ->
currentItem.value?.let { itemID ->
return itemsLiveData.value?.first { it.savedItem.savedItemId == itemID }
}
@ -446,12 +455,16 @@ class LibraryViewModel @Inject constructor(
}
}
enum class SavedItemAction {
Delete,
Archive,
Unarchive,
EditLabels,
EditInfo,
MarkRead,
MarkUnread
sealed interface LibraryUiState {
data object Loading : LibraryUiState
data class Success(
val items: List<SavedItemWithLabelsAndHighlights>,
) : LibraryUiState
data object Error : LibraryUiState
}
enum class SavedItemAction {
Delete, Archive, Unarchive, EditLabels, EditInfo, MarkRead, MarkUnread
}

View File

@ -13,10 +13,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import app.omnivore.omnivore.utils.DatastoreKeys
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.core.analytics.EventTracker
import app.omnivore.omnivore.R
import app.omnivore.omnivore.core.analytics.EventTracker
import app.omnivore.omnivore.core.data.DataService
import app.omnivore.omnivore.core.data.archiveSavedItem
import app.omnivore.omnivore.core.data.createWebHighlight
@ -26,16 +24,19 @@ import app.omnivore.omnivore.core.data.mergeWebHighlights
import app.omnivore.omnivore.core.data.unarchiveSavedItem
import app.omnivore.omnivore.core.data.updateWebHighlight
import app.omnivore.omnivore.core.data.updateWebReadingProgress
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
import app.omnivore.omnivore.core.database.dao.SavedItemDao
import app.omnivore.omnivore.core.database.entities.SavedItem
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.core.network.Networker
import app.omnivore.omnivore.core.network.createNewLabel
import app.omnivore.omnivore.core.network.saveUrl
import app.omnivore.omnivore.core.network.savedItem
import app.omnivore.omnivore.core.database.entities.SavedItem
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.feature.components.HighlightColor
import app.omnivore.omnivore.feature.library.SavedItemAction
import app.omnivore.omnivore.feature.setSavedItemLabels
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
import app.omnivore.omnivore.utils.DatastoreKeys
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
@ -80,6 +81,7 @@ class WebReaderViewModel @Inject constructor(
private val dataService: DataService,
private val networker: Networker,
private val eventTracker: EventTracker,
private val savedItemDao: SavedItemDao // TODO - Use repo
) : ViewModel() {
var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID()
var javascriptDispatchQueue: MutableList<String> = mutableListOf()
@ -333,31 +335,11 @@ class WebReaderViewModel @Inject constructor(
}
SavedItemAction.MarkRead -> {
viewModelScope.launch {
dataService.updateWebReadingProgress(
jsonString = Gson().toJson(
mapOf(
"id" to itemID,
"readingProgressPercent" to 100.0,
"readingProgressAnchorIndex" to 0
)
)
)
}
// TODO
}
SavedItemAction.MarkUnread -> {
viewModelScope.launch {
dataService.updateWebReadingProgress(
jsonString = Gson().toJson(
mapOf(
"id" to itemID,
"readingProgressPercent" to 0,
"readingProgressAnchorIndex" to 0
)
)
)
}
// TODO
}
}
}
@ -381,14 +363,8 @@ class WebReaderViewModel @Inject constructor(
}
}
// fun setHighlightColor(color: HighlightColor) {
// CoroutineScope(Dispatchers.Main).launch {
// highlightColor.postValue(color)
// }
// }
fun handleIncomingWebMessage(actionID: String, jsonString: String) {
Log.d("sync", "incoming change: ${actionID}: ${jsonString}")
Log.d("sync", "incoming change: ${actionID}: $jsonString")
when (actionID) {
"createHighlight" -> {
viewModelScope.launch {
@ -410,7 +386,7 @@ class WebReaderViewModel @Inject constructor(
"articleReadingProgress" -> {
viewModelScope.launch {
dataService.updateWebReadingProgress(jsonString)
dataService.updateWebReadingProgress(jsonString, savedItemDao)
}
}

View File

@ -8,25 +8,23 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import app.omnivore.omnivore.navigation.Routes
import app.omnivore.omnivore.feature.auth.LoginViewModel
import app.omnivore.omnivore.feature.auth.WelcomeScreen
import app.omnivore.omnivore.feature.components.LabelsViewModel
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
import app.omnivore.omnivore.feature.library.LibraryView
import app.omnivore.omnivore.feature.library.LibraryViewModel
import app.omnivore.omnivore.feature.library.SearchView
import app.omnivore.omnivore.feature.library.SearchViewModel
import app.omnivore.omnivore.feature.save.SaveViewModel
import app.omnivore.omnivore.feature.settings.PolicyWebView
import app.omnivore.omnivore.feature.settings.SettingsView
import app.omnivore.omnivore.feature.settings.SettingsViewModel
import app.omnivore.omnivore.navigation.Routes
@Composable
fun RootView(
loginViewModel: LoginViewModel,
searchViewModel: SearchViewModel,
libraryViewModel: LibraryViewModel,
settingsViewModel: SettingsViewModel,
labelsViewModel: LabelsViewModel,
saveViewModel: SaveViewModel,
@ -39,7 +37,6 @@ fun RootView(
PrimaryNavigator(
loginViewModel = loginViewModel,
searchViewModel = searchViewModel,
libraryViewModel = libraryViewModel,
settingsViewModel = settingsViewModel,
labelsViewModel = labelsViewModel,
saveViewModel = saveViewModel,
@ -61,7 +58,6 @@ fun RootView(
@Composable
fun PrimaryNavigator(
loginViewModel: LoginViewModel,
libraryViewModel: LibraryViewModel,
searchViewModel: SearchViewModel,
settingsViewModel: SettingsViewModel,
labelsViewModel: LabelsViewModel,
@ -73,7 +69,6 @@ fun PrimaryNavigator(
NavHost(navController = navController, startDestination = Routes.Library.route) {
composable(Routes.Library.route) {
LibraryView(
libraryViewModel = libraryViewModel,
navController = navController,
labelsViewModel = labelsViewModel,
saveViewModel = saveViewModel,

View File

@ -160,36 +160,6 @@ fun readingProgress(item: SavedItemWithLabelsAndHighlights): String {
return ""
}
//var highlightsText: String {
// item.hig ?.let {
// if let highlights = item.highlights, highlights.count > 0 {
// let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count)
// if item.wordsCount > 0 {
// return " • \(fmted)"
// }
// return fmted
// }
// return ""
//}
//
//var notesText: String {
// let notes = item.highlights?.filter { item in
// if let highlight = item as? Highlight {
// return !(highlight.annotation ?? "").isEmpty
// }
// return false
// }
//
// if let notes = notes, notes.count > 0 {
// let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count)
// if item.wordsCount > 0 {
// return " • \(fmted)"
// }
// return fmted
// }
// return ""
//}
enum class FlairIcon(
val rawValue: String, val sortOrder: Int

View File

@ -9,6 +9,7 @@ androidxComposeCompiler = "1.5.9"
androidxCore = "1.12.0"
androidxDataStore = "1.0.0"
androidxEspresso = "3.5.1"
androidxHiltNavigationCompose = "1.1.0"
androidxLifecycle = "2.7.0"
androidxNavigation = "2.7.7"
androidxSecurity = "1.0.0"
@ -48,9 +49,11 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" }
androidx-dataStore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDataStore" }
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" }
androidx-lifecycle-viewModelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewmodelSavedstate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "androidxLifecycle" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "androidxSecurity" }