add dao and repository modules
This commit is contained in:
@ -1,38 +1,31 @@
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.database.AppDatabase
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import kotlinx.coroutines.*
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class DataService @Inject constructor(
|
||||
context: Context,
|
||||
val networker: Networker
|
||||
val networker: Networker,
|
||||
appDatabase: AppDatabase
|
||||
) {
|
||||
val savedItemSyncChannel = Channel<SavedItem>(capacity = Channel.UNLIMITED)
|
||||
val highlightSyncChannel = Channel<Highlight>(capacity = Channel.UNLIMITED)
|
||||
val savedItemSyncChannel = Channel<SavedItem>(capacity = Channel.UNLIMITED)
|
||||
|
||||
val db = Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java, "omnivore-database"
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
val db = appDatabase
|
||||
|
||||
init {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
startSyncChannels()
|
||||
init {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
startSyncChannels()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearDatabase() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
db.clearAllTables()
|
||||
fun clearDatabase() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
db.clearAllTables()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +1,34 @@
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||
import app.omnivore.omnivore.core.network.updateReadingProgress
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun DataService.updateWebReadingProgress(jsonString: String) {
|
||||
suspend fun DataService.updateWebReadingProgress(
|
||||
jsonString: String,
|
||||
savedItemDao: SavedItemDao
|
||||
) {
|
||||
val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
|
||||
val savedItemId = readingProgressParams.id ?: return
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val savedItem = db.savedItemDao().findById(savedItemId) ?: return@withContext
|
||||
savedItem.readingProgress = readingProgressParams.readingProgressPercent ?: 0.0
|
||||
savedItem.readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
val savedItem = savedItemDao.findById(savedItemId) ?: return@withContext
|
||||
val updatedItem = savedItem.copy(
|
||||
readingProgress = readingProgressParams.readingProgressPercent ?: 0.0,
|
||||
readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0,
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
)
|
||||
savedItemDao.update(updatedItem)
|
||||
|
||||
val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
savedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
db.savedItemDao().update(savedItem)
|
||||
updatedItem.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
savedItemDao.update(updatedItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
package app.omnivore.omnivore.core.data
|
||||
|
||||
import android.util.Log
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.highlightChangeToHighlight
|
||||
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||
import app.omnivore.omnivore.core.network.createHighlight
|
||||
import app.omnivore.omnivore.core.network.deleteHighlights
|
||||
@ -13,10 +17,6 @@ import app.omnivore.omnivore.graphql.generated.type.CreateHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.HighlightType
|
||||
import app.omnivore.omnivore.graphql.generated.type.MergeHighlightInput
|
||||
import app.omnivore.omnivore.graphql.generated.type.UpdateHighlightInput
|
||||
import app.omnivore.omnivore.core.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 com.apollographql.apollo3.api.Optional
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@ -50,7 +50,7 @@ suspend fun DataService.syncOfflineItemsWithServerIfNeeded() {
|
||||
}
|
||||
|
||||
private suspend fun DataService.syncSavedItem(item: SavedItem) {
|
||||
fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
suspend fun updateSyncStatus(status: ServerSyncStatus) {
|
||||
item.serverSyncStatus = status.rawValue
|
||||
db.savedItemDao().update(item)
|
||||
}
|
||||
|
||||
@ -1,21 +1,16 @@
|
||||
package app.omnivore.omnivore.core.data.repository
|
||||
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.data.model.LibraryQuery
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class LibraryRepository @Inject constructor(
|
||||
private val dataService: DataService,
|
||||
) {
|
||||
fun getSavedItems(query: LibraryQuery): Flow<List<SavedItemWithLabelsAndHighlights>> =
|
||||
dataService.db.savedItemDao().filteredLibraryData(
|
||||
query.allowedArchiveStates,
|
||||
query.sortKey,
|
||||
query.requiredLabels,
|
||||
query.excludedLabels,
|
||||
query.allowedContentReaders
|
||||
)
|
||||
interface LibraryRepository {
|
||||
|
||||
fun getSavedItems(query: LibraryQuery): Flow<List<SavedItemWithLabelsAndHighlights>>
|
||||
|
||||
suspend fun updateReadingProgress(
|
||||
itemId: String,
|
||||
readingProgressPercentage: Double,
|
||||
readingProgressAnchorIndex: Int
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
package app.omnivore.omnivore.core.data.repository.impl
|
||||
|
||||
import app.omnivore.omnivore.core.data.model.LibraryQuery
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
import app.omnivore.omnivore.core.data.repository.LibraryRepository
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.ReadingProgressParams
|
||||
import app.omnivore.omnivore.core.network.updateReadingProgress
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class LibraryRepositoryImpl @Inject constructor(
|
||||
private val savedItemDao: SavedItemDao,
|
||||
private val networker: Networker
|
||||
): LibraryRepository {
|
||||
|
||||
override fun getSavedItems(query: LibraryQuery): Flow<List<SavedItemWithLabelsAndHighlights>> =
|
||||
savedItemDao.filteredLibraryData(
|
||||
query.allowedArchiveStates,
|
||||
query.sortKey,
|
||||
hasRequiredLabels = query.requiredLabels.size,
|
||||
hasExcludedLabels = query.excludedLabels.size,
|
||||
query.requiredLabels,
|
||||
query.excludedLabels,
|
||||
query.allowedContentReaders
|
||||
)
|
||||
|
||||
override suspend fun updateReadingProgress(
|
||||
itemId: String,
|
||||
readingProgressPercentage: Double,
|
||||
readingProgressAnchorIndex: Int
|
||||
) {
|
||||
|
||||
val jsonString = Gson().toJson(
|
||||
mapOf(
|
||||
"id" to itemId,
|
||||
"readingProgressPercent" to readingProgressPercentage,
|
||||
"readingProgressAnchorIndex" to readingProgressAnchorIndex,
|
||||
"force" to true
|
||||
)
|
||||
)
|
||||
|
||||
val readingProgressParams = Gson().fromJson(jsonString, ReadingProgressParams::class.java)
|
||||
val savedItemId = readingProgressParams.id ?: return
|
||||
|
||||
|
||||
val savedItem = savedItemDao.findById(savedItemId)
|
||||
val updatedItem = savedItem?.copy(
|
||||
readingProgress = readingProgressParams.readingProgressPercent ?: 0.0,
|
||||
readingProgressAnchor = readingProgressParams.readingProgressAnchorIndex ?: 0,
|
||||
serverSyncStatus = ServerSyncStatus.NEEDS_UPDATE.rawValue
|
||||
)
|
||||
|
||||
updatedItem?.let { savedItemDao.update(updatedItem) }
|
||||
|
||||
val isUpdatedOnServer = networker.updateReadingProgress(readingProgressParams)
|
||||
|
||||
if (isUpdatedOnServer) {
|
||||
updatedItem?.serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue
|
||||
updatedItem?.let { savedItemDao.update(updatedItem) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ package app.omnivore.omnivore.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import app.omnivore.omnivore.core.database.entities.Highlight
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChange
|
||||
import app.omnivore.omnivore.core.database.entities.HighlightChangesDao
|
||||
@ -11,7 +12,6 @@ import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRe
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndHighlightCrossRefDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRef
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemAndSavedItemLabelCrossRefDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabelDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlightsDao
|
||||
@ -28,7 +28,8 @@ import app.omnivore.omnivore.core.database.entities.ViewerDao
|
||||
SavedItemAndSavedItemLabelCrossRef::class,
|
||||
SavedItemAndHighlightCrossRef::class
|
||||
],
|
||||
version = 24
|
||||
version = 24,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun viewerDao(): ViewerDao
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
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 {
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM savedItem")
|
||||
fun getAll(): Flow<List<SavedItem>>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
|
||||
suspend fun findById(itemID: String): SavedItem?
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0")
|
||||
fun getUnSynced(): List<SavedItem>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE slug = :slug")
|
||||
fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights?
|
||||
|
||||
@Query("DELETE FROM savedItem WHERE savedItemId = :itemID")
|
||||
fun deleteById(itemID: String)
|
||||
|
||||
@Query("DELETE FROM savedItem WHERE savedItemId in (:itemIDs)")
|
||||
fun deleteByIds(itemIDs: List<String>)
|
||||
|
||||
@Update
|
||||
suspend fun update(savedItem: SavedItem)
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.savedItemId = :savedItemId " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId "
|
||||
)
|
||||
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.savedItemId = :savedItemId " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId "
|
||||
)
|
||||
suspend fun getById(savedItemId: String): SavedItemWithLabelsAndHighlights?
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.serverSyncStatus != 2 " +
|
||||
"AND SavedItem.isArchived IN (:allowedArchiveStates) " +
|
||||
"AND SavedItem.contentReader IN (:allowedContentReaders) " +
|
||||
"AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " +
|
||||
"AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId " +
|
||||
|
||||
"ORDER BY \n" +
|
||||
"CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" +
|
||||
"CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" +
|
||||
|
||||
"CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" +
|
||||
"CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC"
|
||||
)
|
||||
fun filteredLibraryData(
|
||||
allowedArchiveStates: List<Int>,
|
||||
sortKey: String,
|
||||
hasRequiredLabels: Int,
|
||||
hasExcludedLabels: Int,
|
||||
requiredLabels: List<String>,
|
||||
excludedLabels: List<String>,
|
||||
allowedContentReaders: List<String>
|
||||
): Flow<List<SavedItemWithLabelsAndHighlights>>
|
||||
}
|
||||
@ -1,9 +1,13 @@
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Transaction
|
||||
|
||||
@Entity
|
||||
data class SavedItem(
|
||||
@ -128,114 +132,7 @@ abstract class SavedItemWithLabelsAndHighlightsDao {
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface SavedItemDao {
|
||||
@Query("SELECT * FROM savedItem")
|
||||
fun getAll(): List<SavedItem>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE savedItemId = :itemID")
|
||||
fun findById(itemID: String): SavedItem?
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE serverSyncStatus != 0")
|
||||
fun getUnSynced(): List<SavedItem>
|
||||
|
||||
@Query("SELECT * FROM savedItem WHERE slug = :slug")
|
||||
fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights?
|
||||
|
||||
@Query("DELETE FROM savedItem WHERE savedItemId = :itemID")
|
||||
fun deleteById(itemID: String)
|
||||
|
||||
@Query("DELETE FROM savedItem WHERE savedItemId in (:itemIDs)")
|
||||
fun deleteByIds(itemIDs: List<String>)
|
||||
|
||||
@Update
|
||||
fun update(savedItem: SavedItem)
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.savedItemId = :savedItemId " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId "
|
||||
)
|
||||
fun getLibraryItemById(savedItemId: String): LiveData<SavedItemWithLabelsAndHighlights>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.savedItemId = :savedItemId " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId "
|
||||
)
|
||||
suspend fun getById(savedItemId: String): SavedItemWithLabelsAndHighlights?
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
"FROM SavedItem " +
|
||||
"LEFT OUTER JOIN SavedItemAndSavedItemLabelCrossRef on SavedItem.savedItemId = SavedItemAndSavedItemLabelCrossRef.savedItemId " +
|
||||
"LEFT OUTER JOIN SavedItemAndHighlightCrossRef on SavedItem.savedItemId = SavedItemAndHighlightCrossRef.savedItemId " +
|
||||
|
||||
"LEFT OUTER JOIN SavedItemLabel on SavedItemLabel.savedItemLabelId = SavedItemAndSavedItemLabelCrossRef.savedItemLabelId " +
|
||||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.serverSyncStatus != 2 " +
|
||||
"AND SavedItem.isArchived IN (:allowedArchiveStates) " +
|
||||
"AND SavedItem.contentReader IN (:allowedContentReaders) " +
|
||||
"AND CASE WHEN :hasRequiredLabels THEN SavedItemLabel.name in (:requiredLabels) ELSE 1 END " +
|
||||
"AND CASE WHEN :hasExcludedLabels THEN SavedItemLabel.name is NULL OR SavedItemLabel.name not in (:excludedLabels) ELSE 1 END " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId " +
|
||||
|
||||
"ORDER BY \n" +
|
||||
"CASE WHEN :sortKey = 'newest' THEN SavedItem.savedAt END DESC,\n" +
|
||||
"CASE WHEN :sortKey = 'oldest' THEN SavedItem.savedAt END ASC,\n" +
|
||||
|
||||
"CASE WHEN :sortKey = 'recentlyRead' THEN SavedItem.readAt END DESC,\n" +
|
||||
"CASE WHEN :sortKey = 'recentlyPublished' THEN SavedItem.publishDate END DESC"
|
||||
)
|
||||
fun _filteredLibraryData(
|
||||
allowedArchiveStates: List<Int>,
|
||||
sortKey: String,
|
||||
hasRequiredLabels: Int,
|
||||
hasExcludedLabels: Int,
|
||||
requiredLabels: List<String>,
|
||||
excludedLabels: List<String>,
|
||||
allowedContentReaders: List<String>
|
||||
): Flow<List<SavedItemWithLabelsAndHighlights>>
|
||||
|
||||
fun filteredLibraryData(
|
||||
allowedArchiveStates: List<Int>,
|
||||
sortKey: String,
|
||||
requiredLabels: List<String>,
|
||||
excludedLabels: List<String>,
|
||||
allowedContentReaders: List<String>
|
||||
): Flow<List<SavedItemWithLabelsAndHighlights>> {
|
||||
return _filteredLibraryData(
|
||||
allowedArchiveStates = allowedArchiveStates,
|
||||
sortKey = sortKey,
|
||||
hasRequiredLabels = requiredLabels.size,
|
||||
hasExcludedLabels = excludedLabels.size,
|
||||
requiredLabels = requiredLabels,
|
||||
excludedLabels = excludedLabels,
|
||||
allowedContentReaders = allowedContentReaders
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object SavedItemQueryConstants {
|
||||
|
||||
@ -1,66 +1,70 @@
|
||||
package app.omnivore.omnivore.core.database.entities
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
|
||||
|
||||
@Entity
|
||||
data class SavedItemLabel(
|
||||
@PrimaryKey val savedItemLabelId: String,
|
||||
val name: String,
|
||||
val color: String,
|
||||
val createdAt: String?,
|
||||
val labelDescription: String?,
|
||||
val serverSyncStatus: Int = 0
|
||||
@PrimaryKey val savedItemLabelId: String,
|
||||
val name: String,
|
||||
val color: String,
|
||||
val createdAt: String?,
|
||||
val labelDescription: String?,
|
||||
val serverSyncStatus: Int = 0
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface SavedItemLabelDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemLabel>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemLabel>)
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
|
||||
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
|
||||
@Transaction
|
||||
@Query("SELECT * FROM SavedItemLabel WHERE serverSyncStatus != 2 ORDER BY name ASC")
|
||||
fun getSavedItemLabelsLiveData(): LiveData<List<SavedItemLabel>>
|
||||
|
||||
@Transaction
|
||||
@Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId")
|
||||
fun updateTempLabel(tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED)
|
||||
@Transaction
|
||||
@Query("UPDATE SavedItemLabel set savedItemLabelId = :permanentId, serverSyncStatus = :status WHERE savedItemLabelId = :tempId")
|
||||
fun updateTempLabel(
|
||||
tempId: String, permanentId: String, status: ServerSyncStatus = ServerSyncStatus.IS_SYNCED
|
||||
)
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC")
|
||||
fun namedLabels(names: List<String>): List<SavedItemLabel>
|
||||
@Transaction
|
||||
@Query("SELECT * FROM SavedItemLabel WHERE name in (:names) ORDER BY name ASC")
|
||||
fun namedLabels(names: List<String>): List<SavedItemLabel>
|
||||
}
|
||||
|
||||
@Entity(
|
||||
primaryKeys = ["savedItemLabelId", "savedItemId"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SavedItem::class,
|
||||
parentColumns = arrayOf("savedItemId"),
|
||||
childColumns = arrayOf("savedItemId"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = SavedItemLabel::class,
|
||||
parentColumns = arrayOf("savedItemLabelId"),
|
||||
childColumns = arrayOf("savedItemLabelId")
|
||||
)
|
||||
]
|
||||
primaryKeys = ["savedItemLabelId", "savedItemId"], foreignKeys = [ForeignKey(
|
||||
entity = SavedItem::class,
|
||||
parentColumns = arrayOf("savedItemId"),
|
||||
childColumns = arrayOf("savedItemId"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
), ForeignKey(
|
||||
entity = SavedItemLabel::class,
|
||||
parentColumns = arrayOf("savedItemLabelId"),
|
||||
childColumns = arrayOf("savedItemLabelId")
|
||||
)]
|
||||
)
|
||||
data class SavedItemAndSavedItemLabelCrossRef(
|
||||
val savedItemLabelId: String,
|
||||
val savedItemId: String
|
||||
val savedItemLabelId: String, val savedItemId: String
|
||||
)
|
||||
|
||||
|
||||
@Dao
|
||||
interface SavedItemAndSavedItemLabelCrossRefDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||
|
||||
@Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId")
|
||||
fun deleteRefsBySavedItemId(savedItemId: String)
|
||||
@Query("DELETE FROM savedItemAndSavedItemLabelCrossRef WHERE savedItemId = :savedItemId")
|
||||
fun deleteRefsBySavedItemId(savedItemId: String)
|
||||
}
|
||||
|
||||
// has many highlights
|
||||
|
||||
@ -1,25 +1,20 @@
|
||||
package app.omnivore.omnivore.core.network
|
||||
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.utils.Constants
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import com.apollographql.apollo3.ApolloClient
|
||||
import javax.inject.Inject
|
||||
|
||||
class Networker @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
private val datastoreRepo: DatastoreRepository
|
||||
) {
|
||||
suspend fun baseUrl() = datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL
|
||||
suspend fun baseUrl() =
|
||||
datastoreRepo.getString(DatastoreKeys.omnivoreSelfHostedAPIServer) ?: Constants.apiURL
|
||||
|
||||
suspend fun serverUrl() = "${baseUrl()}/api/graphql"
|
||||
private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: ""
|
||||
private suspend fun serverUrl() = "${baseUrl()}/api/graphql"
|
||||
private suspend fun authToken() = datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) ?: ""
|
||||
|
||||
suspend fun publicApolloClient() = ApolloClient.Builder()
|
||||
.serverUrl(serverUrl())
|
||||
.build()
|
||||
|
||||
suspend fun authenticatedApolloClient() = ApolloClient.Builder()
|
||||
.serverUrl(serverUrl())
|
||||
.addHttpHeader("Authorization", value = authToken())
|
||||
.build()
|
||||
suspend fun authenticatedApolloClient() = ApolloClient.Builder().serverUrl(serverUrl())
|
||||
.addHttpHeader("Authorization", value = authToken()).build()
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package app.omnivore.omnivore.di
|
||||
import android.content.Context
|
||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.database.AppDatabase
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.datastore.OmnivoreDatastore
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
@ -34,8 +35,8 @@ object AppModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideDataService(
|
||||
@ApplicationContext app: Context,
|
||||
networker: Networker
|
||||
) = DataService(app, networker)
|
||||
networker: Networker,
|
||||
appDatabase: AppDatabase
|
||||
) = DataService(networker, appDatabase)
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
package app.omnivore.omnivore.di
|
||||
|
||||
import app.omnivore.omnivore.core.database.AppDatabase
|
||||
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 providesTopicsDao(
|
||||
database: AppDatabase,
|
||||
): SavedItemDao = database.savedItemDao()
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package app.omnivore.omnivore.di
|
||||
|
||||
import app.omnivore.omnivore.core.data.repository.LibraryRepository
|
||||
import app.omnivore.omnivore.core.data.repository.impl.LibraryRepositoryImpl
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface DataModule {
|
||||
|
||||
@Binds
|
||||
fun bindsLibraryRepository(
|
||||
libraryRepository: LibraryRepositoryImpl,
|
||||
): LibraryRepository
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package app.omnivore.omnivore.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import app.omnivore.omnivore.core.database.AppDatabase
|
||||
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,
|
||||
): AppDatabase = Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
"omnivore-database",
|
||||
).build()
|
||||
}
|
||||
@ -1,15 +1,25 @@
|
||||
package app.omnivore.omnivore.feature.library
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SuggestionChipDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -23,98 +33,92 @@ import app.omnivore.omnivore.feature.components.LabelChipColors
|
||||
|
||||
@Composable
|
||||
fun LibraryFilterBar(
|
||||
viewModel: LibraryViewModel = hiltViewModel()
|
||||
viewModel: LibraryViewModel = hiltViewModel()
|
||||
) {
|
||||
var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) }
|
||||
val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(SavedItemFilter.INBOX)
|
||||
val activeLabels: List<SavedItemLabel> by viewModel.activeLabelsLiveData.observeAsState(listOf())
|
||||
var isSavedItemFilterMenuExpanded by remember { mutableStateOf(false) }
|
||||
val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(
|
||||
SavedItemFilter.INBOX
|
||||
)
|
||||
val activeLabels: List<SavedItemLabel> by viewModel.activeLabelsLiveData.observeAsState(listOf())
|
||||
|
||||
var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) }
|
||||
val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(SavedItemSortFilter.NEWEST)
|
||||
val listState = rememberLazyListState()
|
||||
var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) }
|
||||
val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(
|
||||
SavedItemSortFilter.NEWEST
|
||||
)
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
Column {
|
||||
LazyRow(
|
||||
state = listState,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(start = 6.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
item {
|
||||
AssistChip(
|
||||
onClick = { isSavedItemFilterMenuExpanded = true },
|
||||
label = { Text(activeSavedItemFilter.displayText) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = "drop down button to change primary library filter"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
AssistChip(
|
||||
onClick = { isSavedItemSortFilterMenuExpanded = true },
|
||||
label = { Text(activeSavedItemSortFilter.displayText) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = "drop down button to change library sort order"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
AssistChip(
|
||||
onClick = { viewModel.bottomSheetState.value = LibraryBottomSheetState.LABEL },
|
||||
label = { Text(stringResource(R.string.library_filter_bar_label_labels)) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = "drop down button to open label selection sheet"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
}
|
||||
items(activeLabels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label ->
|
||||
val chipColors = LabelChipColors.fromHex(label.color)
|
||||
Column {
|
||||
LazyRow(
|
||||
state = listState,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(start = 6.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
item {
|
||||
AssistChip(onClick = { isSavedItemFilterMenuExpanded = true },
|
||||
label = { Text(activeSavedItemFilter.displayText) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = "drop down button to change primary library filter"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
AssistChip(onClick = { isSavedItemSortFilterMenuExpanded = true },
|
||||
label = { Text(activeSavedItemSortFilter.displayText) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = "drop down button to change library sort order"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
AssistChip(
|
||||
onClick = { viewModel.bottomSheetState.value = LibraryBottomSheetState.LABEL },
|
||||
label = { Text(stringResource(R.string.library_filter_bar_label_labels)) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = "drop down button to open label selection sheet"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
}
|
||||
items(activeLabels.sortedWith(compareBy { it.name.toLowerCase(Locale.current) })) { label ->
|
||||
val chipColors = LabelChipColors.fromHex(label.color)
|
||||
|
||||
AssistChip(
|
||||
onClick = {
|
||||
viewModel.updateAppliedLabels(
|
||||
(viewModel.activeLabelsLiveData.value ?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId }
|
||||
)
|
||||
},
|
||||
label = { Text(label.name) },
|
||||
border = null,
|
||||
colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
|
||||
containerColor = chipColors.containerColor,
|
||||
labelColor = chipColors.textColor,
|
||||
iconContentColor = chipColors.textColor
|
||||
),
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "close icon to remove label"
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
AssistChip(onClick = {
|
||||
viewModel.updateAppliedLabels((viewModel.activeLabelsLiveData.value
|
||||
?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId })
|
||||
},
|
||||
label = { Text(label.name) },
|
||||
border = null,
|
||||
colors = SuggestionChipDefaults.elevatedSuggestionChipColors(
|
||||
containerColor = chipColors.containerColor,
|
||||
labelColor = chipColors.textColor,
|
||||
iconContentColor = chipColors.textColor
|
||||
),
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Close, contentDescription = "close icon to remove label"
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemFilterContextMenu(isExpanded = isSavedItemFilterMenuExpanded,
|
||||
onDismiss = { isSavedItemFilterMenuExpanded = false },
|
||||
actionHandler = { viewModel.updateSavedItemFilter(it) })
|
||||
|
||||
SavedItemSortFilterContextMenu(isExpanded = isSavedItemSortFilterMenuExpanded,
|
||||
onDismiss = { isSavedItemSortFilterMenuExpanded = false },
|
||||
actionHandler = { viewModel.updateSavedItemSortFilter(it) })
|
||||
}
|
||||
|
||||
SavedItemFilterContextMenu(
|
||||
isExpanded = isSavedItemFilterMenuExpanded,
|
||||
onDismiss = { isSavedItemFilterMenuExpanded = false },
|
||||
actionHandler = { viewModel.updateSavedItemFilter(it) }
|
||||
)
|
||||
|
||||
SavedItemSortFilterContextMenu(
|
||||
isExpanded = isSavedItemSortFilterMenuExpanded,
|
||||
onDismiss = { isSavedItemSortFilterMenuExpanded = false },
|
||||
actionHandler = { viewModel.updateSavedItemSortFilter(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material.rememberDismissState
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
@ -44,12 +45,12 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@ -72,52 +73,52 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun LibraryView(
|
||||
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
||||
internal fun LibraryView(
|
||||
labelsViewModel: LabelsViewModel,
|
||||
saveViewModel: SaveViewModel,
|
||||
editInfoViewModel: EditInfoViewModel,
|
||||
navController: NavHostController
|
||||
navController: NavHostController,
|
||||
viewModel: LibraryViewModel = hiltViewModel()
|
||||
) {
|
||||
val scaffoldState: ScaffoldState = rememberScaffoldState()
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val uiState by libraryViewModel.uiState.collectAsStateWithLifecycle()
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
val showBottomSheet: LibraryBottomSheetState by libraryViewModel.bottomSheetState.observeAsState(
|
||||
val showBottomSheet: LibraryBottomSheetState by viewModel.bottomSheetState.observeAsState(
|
||||
LibraryBottomSheetState.HIDDEN
|
||||
)
|
||||
|
||||
libraryViewModel.snackbarMessage?.let {
|
||||
viewModel.snackbarMessage?.let {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.snackbarHostState.showSnackbar(it)
|
||||
libraryViewModel.clearSnackbarMessage()
|
||||
viewModel.clearSnackbarMessage()
|
||||
}
|
||||
}
|
||||
|
||||
when (showBottomSheet) {
|
||||
LibraryBottomSheetState.ADD_LINK -> {
|
||||
AddLinkBottomSheet(saveViewModel) {
|
||||
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
||||
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
LibraryBottomSheetState.LABEL -> {
|
||||
LabelBottomSheet(
|
||||
libraryViewModel,
|
||||
viewModel,
|
||||
labelsViewModel
|
||||
) {
|
||||
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
||||
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
LibraryBottomSheetState.EDIT -> {
|
||||
EditBottomSheet(
|
||||
editInfoViewModel,
|
||||
libraryViewModel
|
||||
viewModel
|
||||
) {
|
||||
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
||||
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,9 +130,9 @@ fun LibraryView(
|
||||
scaffoldState = scaffoldState,
|
||||
topBar = {
|
||||
LibraryNavigationBar(
|
||||
savedItemViewModel = libraryViewModel,
|
||||
savedItemViewModel = viewModel,
|
||||
onSearchClicked = { navController.navigate(Routes.Search.route) },
|
||||
onAddLinkClicked = { showAddLinkBottomSheet(libraryViewModel) },
|
||||
onAddLinkClicked = { showAddLinkBottomSheet(viewModel) },
|
||||
onSettingsIconClick = { navController.navigate(Routes.Settings.route) }
|
||||
)
|
||||
},
|
||||
@ -139,13 +140,25 @@ fun LibraryView(
|
||||
when (uiState) {
|
||||
is LibraryUiState.Success -> {
|
||||
LibraryViewContent(
|
||||
libraryViewModel,
|
||||
viewModel,
|
||||
modifier = Modifier
|
||||
.padding(top = paddingValues.calculateTopPadding()),
|
||||
cardsData = (uiState as LibraryUiState.Success).items
|
||||
uiState = uiState
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
is LibraryUiState.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(strokeCap = StrokeCap.Round)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -285,7 +298,7 @@ fun EditBottomSheet(
|
||||
fun LibraryViewContent(
|
||||
libraryViewModel: LibraryViewModel,
|
||||
modifier: Modifier,
|
||||
cardsData: List<SavedItemWithLabelsAndHighlights>
|
||||
uiState: LibraryUiState
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val listState = rememberLazyListState()
|
||||
@ -296,9 +309,6 @@ fun LibraryViewContent(
|
||||
)
|
||||
|
||||
val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState()
|
||||
/* val cardsData: List<SavedItemWithLabelsAndHighlights> by libraryViewModel.itemsLiveData.observeAsState(
|
||||
listOf()
|
||||
)*/
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -318,13 +328,14 @@ fun LibraryViewContent(
|
||||
LibraryFilterBar(libraryViewModel)
|
||||
}
|
||||
items(
|
||||
items = cardsData,
|
||||
items = (uiState as LibraryUiState.Success).items,
|
||||
key = { item -> item.savedItem.savedItemId }
|
||||
) { cardDataWithLabels ->
|
||||
val swipeThreshold = 0.45f
|
||||
|
||||
val currentThresholdFraction = remember { mutableStateOf(0f) }
|
||||
val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem)
|
||||
//val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem)
|
||||
val currentItem = cardDataWithLabels.savedItem
|
||||
val swipeState = rememberDismissState(
|
||||
confirmStateChange = {
|
||||
when(it) {
|
||||
@ -430,7 +441,7 @@ fun LibraryViewContent(
|
||||
}
|
||||
|
||||
InfiniteListHandler(listState = listState) {
|
||||
if (cardsData.isEmpty()) {
|
||||
if ((uiState as LibraryUiState.Success).items.isEmpty()) {
|
||||
Log.d("sync", "loading with load func")
|
||||
libraryViewModel.initialLoad()
|
||||
} else {
|
||||
|
||||
@ -20,7 +20,6 @@ import app.omnivore.omnivore.core.data.sync
|
||||
import app.omnivore.omnivore.core.data.syncLabels
|
||||
import app.omnivore.omnivore.core.data.syncOfflineItemsWithServerIfNeeded
|
||||
import app.omnivore.omnivore.core.data.unarchiveSavedItem
|
||||
import app.omnivore.omnivore.core.data.updateWebReadingProgress
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
@ -31,7 +30,6 @@ import app.omnivore.omnivore.feature.setSavedItemLabels
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -79,12 +77,15 @@ class LibraryViewModel @Inject constructor(
|
||||
|
||||
val uiState: StateFlow<LibraryUiState> = _libraryQuery.flatMapLatest { query ->
|
||||
libraryRepository.getSavedItems(query)
|
||||
}.map(LibraryUiState::Success).stateIn(
|
||||
}
|
||||
.map(LibraryUiState::Success)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000), // Adjust as needed
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = LibraryUiState.Loading
|
||||
)
|
||||
|
||||
|
||||
private val itemsLiveData = MediatorLiveData<List<SavedItemWithLabelsAndHighlights>>()
|
||||
val appliedFilterLiveData = MutableLiveData(SavedItemFilter.INBOX)
|
||||
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
|
||||
@ -99,6 +100,7 @@ class LibraryViewModel @Inject constructor(
|
||||
private var hasLoadedInitialFilters = false
|
||||
|
||||
private fun loadInitialFilterValues() {
|
||||
|
||||
if (hasLoadedInitialFilters) {
|
||||
return
|
||||
}
|
||||
@ -336,31 +338,13 @@ class LibraryViewModel @Inject constructor(
|
||||
|
||||
SavedItemAction.MarkRead -> {
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebReadingProgress(
|
||||
jsonString = Gson().toJson(
|
||||
mapOf(
|
||||
"id" to itemID,
|
||||
"readingProgressPercent" to 100.0,
|
||||
"readingProgressAnchorIndex" to 0,
|
||||
"force" to true
|
||||
)
|
||||
)
|
||||
)
|
||||
libraryRepository.updateReadingProgress(itemID, 100.0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
SavedItemAction.MarkUnread -> {
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebReadingProgress(
|
||||
jsonString = Gson().toJson(
|
||||
mapOf(
|
||||
"id" to itemID,
|
||||
"readingProgressPercent" to 0,
|
||||
"readingProgressAnchorIndex" to 0,
|
||||
"force" to true
|
||||
)
|
||||
)
|
||||
)
|
||||
libraryRepository.updateReadingProgress(itemID, 0.0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,10 +13,8 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.core.analytics.EventTracker
|
||||
import app.omnivore.omnivore.core.data.DataService
|
||||
import app.omnivore.omnivore.core.data.archiveSavedItem
|
||||
import app.omnivore.omnivore.core.data.createWebHighlight
|
||||
@ -26,16 +24,19 @@ import app.omnivore.omnivore.core.data.mergeWebHighlights
|
||||
import app.omnivore.omnivore.core.data.unarchiveSavedItem
|
||||
import app.omnivore.omnivore.core.data.updateWebHighlight
|
||||
import app.omnivore.omnivore.core.data.updateWebReadingProgress
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.core.database.dao.SavedItemDao
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.core.datastore.DatastoreRepository
|
||||
import app.omnivore.omnivore.core.network.Networker
|
||||
import app.omnivore.omnivore.core.network.createNewLabel
|
||||
import app.omnivore.omnivore.core.network.saveUrl
|
||||
import app.omnivore.omnivore.core.network.savedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItem
|
||||
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.feature.components.HighlightColor
|
||||
import app.omnivore.omnivore.feature.library.SavedItemAction
|
||||
import app.omnivore.omnivore.feature.setSavedItemLabels
|
||||
import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
|
||||
import app.omnivore.omnivore.utils.DatastoreKeys
|
||||
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@ -80,6 +81,7 @@ class WebReaderViewModel @Inject constructor(
|
||||
private val dataService: DataService,
|
||||
private val networker: Networker,
|
||||
private val eventTracker: EventTracker,
|
||||
private val savedItemDao: SavedItemDao // TODO - Use repo
|
||||
) : ViewModel() {
|
||||
var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID()
|
||||
var javascriptDispatchQueue: MutableList<String> = mutableListOf()
|
||||
@ -384,7 +386,7 @@ class WebReaderViewModel @Inject constructor(
|
||||
|
||||
"articleReadingProgress" -> {
|
||||
viewModelScope.launch {
|
||||
dataService.updateWebReadingProgress(jsonString)
|
||||
dataService.updateWebReadingProgress(jsonString, savedItemDao)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -160,36 +160,6 @@ fun readingProgress(item: SavedItemWithLabelsAndHighlights): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
//var highlightsText: String {
|
||||
// item.hig ?.let {
|
||||
// if let highlights = item.highlights, highlights.count > 0 {
|
||||
// let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count)
|
||||
// if item.wordsCount > 0 {
|
||||
// return " • \(fmted)"
|
||||
// }
|
||||
// return fmted
|
||||
// }
|
||||
// return ""
|
||||
//}
|
||||
//
|
||||
//var notesText: String {
|
||||
// let notes = item.highlights?.filter { item in
|
||||
// if let highlight = item as? Highlight {
|
||||
// return !(highlight.annotation ?? "").isEmpty
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// if let notes = notes, notes.count > 0 {
|
||||
// let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count)
|
||||
// if item.wordsCount > 0 {
|
||||
// return " • \(fmted)"
|
||||
// }
|
||||
// return fmted
|
||||
// }
|
||||
// return ""
|
||||
//}
|
||||
|
||||
|
||||
enum class FlairIcon(
|
||||
val rawValue: String, val sortOrder: Int
|
||||
|
||||
Reference in New Issue
Block a user