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.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 {

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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) { enum class ServerSyncStatus(val rawValue: Int) {
IS_SYNCED(0), 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.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
} }

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 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 {

View File

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

View File

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

View File

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

View File

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

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.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(

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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