mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #2174 from omnivore-app/fix/android-loading
Improvements to Android library loading
This commit is contained in:
commit
f8fc1306df
25 changed files with 833 additions and 848 deletions
|
|
@ -17,8 +17,8 @@ android {
|
|||
applicationId "app.omnivore.omnivore"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 61
|
||||
versionName "0.0.61"
|
||||
versionCode 70
|
||||
versionName "0.0.70"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import javax.inject.Inject
|
|||
|
||||
interface DatastoreRepository {
|
||||
val hasAuthTokenFlow: Flow<Boolean>
|
||||
val themeKeyFlow: Flow<String>
|
||||
|
||||
suspend fun clear()
|
||||
suspend fun putString(key: String, value: String)
|
||||
suspend fun putInt(key: String, value: Int)
|
||||
|
|
@ -67,4 +69,10 @@ class OmnivoreDatastore @Inject constructor(
|
|||
val token = preferences[key]
|
||||
token != null
|
||||
}
|
||||
|
||||
override val themeKeyFlow: Flow<String> = context
|
||||
.dataStore.data.map { preferences ->
|
||||
val key = stringPreferencesKey(DatastoreKeys.preferredTheme)
|
||||
preferences[key] ?: "System"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
package app.omnivore.omnivore.dataService
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.ui.library.SavedItemFilter
|
||||
import app.omnivore.omnivore.ui.library.SavedItemSortFilter
|
||||
|
||||
fun DataService.libraryLiveData(
|
||||
primaryFilter: SavedItemFilter,
|
||||
sortFilter: SavedItemSortFilter,
|
||||
labels: List<SavedItemLabel>
|
||||
): LiveData<List<SavedItemCardDataWithLabels>> {
|
||||
val mediatorLiveData = MediatorLiveData<List<SavedItemCardDataWithLabels>>()
|
||||
val queryParams = LibraryLiveDataQueryParams.make(primaryFilter)
|
||||
|
||||
val libraryLiveData = when (sortFilter) {
|
||||
SavedItemSortFilter.NEWEST -> db.savedItemDao().getLibraryLiveData(
|
||||
archiveFilter = queryParams.archiveFilter
|
||||
)
|
||||
SavedItemSortFilter.OLDEST -> db.savedItemDao().getLibraryLiveDataSortedByOldest(
|
||||
archiveFilter = queryParams.archiveFilter
|
||||
)
|
||||
SavedItemSortFilter.RECENTLY_READ -> db.savedItemDao().getLibraryLiveDataSortedByRecentlyRead(
|
||||
archiveFilter = queryParams.archiveFilter
|
||||
)
|
||||
SavedItemSortFilter.RECENTLY_PUBLISHED -> db.savedItemDao().getLibraryLiveDataSortedByRecentlyPublished(
|
||||
archiveFilter = queryParams.archiveFilter
|
||||
)
|
||||
}
|
||||
|
||||
mediatorLiveData.addSource(libraryLiveData) { result ->
|
||||
when (primaryFilter) {
|
||||
SavedItemFilter.INBOX -> {
|
||||
mediatorLiveData.value = result
|
||||
}
|
||||
SavedItemFilter.READ_LATER -> {
|
||||
mediatorLiveData.value = result.filter { item ->
|
||||
!item.labels.any { it.name.lowercase() == "newsletter" }
|
||||
}
|
||||
}
|
||||
SavedItemFilter.NEWSLETTERS -> {
|
||||
mediatorLiveData.value = result.filter { item ->
|
||||
item.labels.any { it.name.lowercase() == "newsletter" }
|
||||
}
|
||||
}
|
||||
SavedItemFilter.RECOMMENDED -> {
|
||||
mediatorLiveData.value = result // TODO: "recommendations.@count > 0"
|
||||
}
|
||||
SavedItemFilter.ALL -> {
|
||||
mediatorLiveData.value = result
|
||||
}
|
||||
SavedItemFilter.ARCHIVED -> {
|
||||
mediatorLiveData.value = result
|
||||
}
|
||||
SavedItemFilter.HAS_HIGHLIGHTS -> {
|
||||
mediatorLiveData.value = result // TODO: "highlights.@count > 0"
|
||||
}
|
||||
SavedItemFilter.FILES -> {
|
||||
mediatorLiveData.value = result.filter { item ->
|
||||
item.cardData.contentReader == "PDF"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (labels.isNotEmpty()) {
|
||||
mediatorLiveData.value = (mediatorLiveData.value ?: listOf()).filter {
|
||||
it.labels.intersect(labels.toSet()).any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mediatorLiveData
|
||||
}
|
||||
|
||||
private data class LibraryLiveDataQueryParams(
|
||||
val archiveFilter: Int
|
||||
) {
|
||||
companion object {
|
||||
fun make(savedItemFilter: SavedItemFilter): LibraryLiveDataQueryParams {
|
||||
return when (savedItemFilter) {
|
||||
SavedItemFilter.INBOX -> {
|
||||
LibraryLiveDataQueryParams(
|
||||
archiveFilter = 1, // Filter out items marked as archive
|
||||
)
|
||||
}
|
||||
SavedItemFilter.READ_LATER -> {
|
||||
LibraryLiveDataQueryParams(
|
||||
archiveFilter = 1, // Filter out items marked as archive
|
||||
)
|
||||
}
|
||||
SavedItemFilter.NEWSLETTERS -> {
|
||||
LibraryLiveDataQueryParams(
|
||||
archiveFilter = 1, // Filter out items marked as archive
|
||||
)
|
||||
}
|
||||
SavedItemFilter.RECOMMENDED -> {
|
||||
LibraryLiveDataQueryParams(
|
||||
archiveFilter = 1, // Filter out items marked as archive
|
||||
)
|
||||
}
|
||||
SavedItemFilter.ALL -> {
|
||||
LibraryLiveDataQueryParams(
|
||||
archiveFilter = 2, // Don't filter anything out (2 will not match anything)
|
||||
)
|
||||
}
|
||||
SavedItemFilter.ARCHIVED -> {
|
||||
LibraryLiveDataQueryParams(
|
||||
archiveFilter = 0, // Filter out items not marked as archived
|
||||
)
|
||||
}
|
||||
SavedItemFilter.HAS_HIGHLIGHTS -> {
|
||||
LibraryLiveDataQueryParams(
|
||||
archiveFilter = 1, // Filter out items marked as archive
|
||||
)
|
||||
}
|
||||
SavedItemFilter.FILES -> {
|
||||
LibraryLiveDataQueryParams(
|
||||
archiveFilter = 1, // Filter out items marked as archive
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,24 +17,7 @@ suspend fun DataService.librarySearch(cursor: String?, query: String): SearchRes
|
|||
)
|
||||
}
|
||||
|
||||
db.savedItemDao().insertAll(savedItems.map { it.savedItem })
|
||||
|
||||
val labels: MutableList<SavedItemLabel> = mutableListOf()
|
||||
val crossRefs: MutableList<SavedItemAndSavedItemLabelCrossRef> = mutableListOf()
|
||||
|
||||
// save labels
|
||||
for (searchItem in searchResult.items) {
|
||||
labels.addAll(searchItem.labels)
|
||||
|
||||
val newCrossRefs = searchItem.labels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = searchItem.item.savedItemId)
|
||||
}
|
||||
|
||||
crossRefs.addAll(newCrossRefs)
|
||||
}
|
||||
|
||||
db.savedItemLabelDao().insertAll(labels)
|
||||
db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
|
||||
db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems)
|
||||
|
||||
Log.d("sync", "found ${searchResult.items.size} items with search api. Query: $query cursor: $cursor")
|
||||
|
||||
|
|
@ -52,7 +35,7 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
|
|||
?: return SavedItemSyncResult.errorResult
|
||||
|
||||
val savedItems = syncResult.items.map {
|
||||
SavedItem(
|
||||
val savedItem = SavedItem(
|
||||
savedItemId = it.id,
|
||||
title = it.title,
|
||||
createdAt = it.createdAt as String,
|
||||
|
|
@ -74,71 +57,40 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
|
|||
content = null,
|
||||
wordsCount = it.wordsCount
|
||||
)
|
||||
}
|
||||
|
||||
db.savedItemDao().insertAll(savedItems)
|
||||
|
||||
val labels: MutableList<SavedItemLabel> = mutableListOf()
|
||||
val crossRefs: MutableList<SavedItemAndSavedItemLabelCrossRef> = mutableListOf()
|
||||
|
||||
// save labels
|
||||
for (item in syncResult.items) {
|
||||
val itemLabels = (item.labels ?: listOf()).map {
|
||||
val labels = it.labels?.map { label ->
|
||||
SavedItemLabel(
|
||||
savedItemLabelId = it.labelFields.id,
|
||||
name = it.labelFields.name,
|
||||
color = it.labelFields.color,
|
||||
savedItemLabelId = label.labelFields.id,
|
||||
name = label.labelFields.name,
|
||||
color = label.labelFields.color,
|
||||
createdAt = null,
|
||||
labelDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
labels.addAll(itemLabels)
|
||||
|
||||
val newCrossRefs = itemLabels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(
|
||||
savedItemLabelId = it.savedItemLabelId,
|
||||
savedItemId = item.id
|
||||
} ?: listOf()
|
||||
val highlights = it.highlights?.map { highlight ->
|
||||
Highlight(
|
||||
type = highlight.highlightFields.type.toString(),
|
||||
highlightId = highlight.highlightFields.id,
|
||||
annotation = highlight.highlightFields.annotation,
|
||||
createdByMe = highlight.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
patch = highlight.highlightFields.patch,
|
||||
prefix = highlight.highlightFields.prefix,
|
||||
quote = highlight.highlightFields.quote,
|
||||
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
shortId = highlight.highlightFields.shortId,
|
||||
suffix = highlight.highlightFields.suffix,
|
||||
createdAt = null,
|
||||
updatedAt = highlight.highlightFields.updatedAt as String?,
|
||||
)
|
||||
}
|
||||
|
||||
crossRefs.addAll(newCrossRefs)
|
||||
}
|
||||
|
||||
db.savedItemLabelDao().insertAll(labels)
|
||||
db.savedItemAndSavedItemLabelCrossRefDao().insertAll(crossRefs)
|
||||
|
||||
// Persist Highlights
|
||||
db.highlightDao().insertAll(syncResult.items.flatMap {
|
||||
it.highlights ?: listOf()
|
||||
}.map {
|
||||
Highlight(
|
||||
type = it.highlightFields.type.toString(),
|
||||
highlightId = it.highlightFields.id,
|
||||
annotation = it.highlightFields.annotation,
|
||||
createdByMe = it.highlightFields.createdByMe,
|
||||
markedForDeletion = false,
|
||||
patch = it.highlightFields.patch,
|
||||
prefix = it.highlightFields.prefix,
|
||||
quote = it.highlightFields.quote,
|
||||
serverSyncStatus = ServerSyncStatus.IS_SYNCED.rawValue,
|
||||
shortId = it.highlightFields.shortId,
|
||||
suffix = it.highlightFields.suffix,
|
||||
createdAt = null,
|
||||
updatedAt = it.highlightFields.updatedAt as String?,
|
||||
} ?: listOf()
|
||||
SavedItemWithLabelsAndHighlights(
|
||||
savedItem = savedItem,
|
||||
labels = labels,
|
||||
highlights = highlights
|
||||
)
|
||||
})
|
||||
|
||||
val highlightCrossRefs = syncResult.items.flatMap {
|
||||
val savedItem = it
|
||||
(savedItem.highlights ?: listOf()).map {
|
||||
Pair(it, savedItem.id)
|
||||
}
|
||||
}.map {
|
||||
SavedItemAndHighlightCrossRef(highlightId = it.first.highlightFields.id, savedItemId = it.second)
|
||||
}
|
||||
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs)
|
||||
db.savedItemWithLabelsAndHighlightsDao().insertAll(savedItems)
|
||||
|
||||
Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since")
|
||||
|
||||
|
|
@ -159,28 +111,16 @@ fun DataService.isSavedItemContentStoredInDB(slug: String): Boolean {
|
|||
|
||||
suspend fun DataService.fetchSavedItemContent(slug: String) {
|
||||
val syncResult = networker.savedItem(slug)
|
||||
val isSuccess = syncResult.item != null
|
||||
|
||||
val savedItem = syncResult.item ?: return
|
||||
db.savedItemDao().insert(savedItem)
|
||||
|
||||
// Persist Labels
|
||||
db.savedItemLabelDao().insertAll(syncResult.labels)
|
||||
|
||||
val labelCrossRefs = syncResult.labels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = savedItem.savedItemId)
|
||||
val savedItem = syncResult.item
|
||||
savedItem?.let {
|
||||
val item = SavedItemWithLabelsAndHighlights(
|
||||
savedItem = savedItem,
|
||||
labels = syncResult.labels,
|
||||
highlights = syncResult.highlights
|
||||
)
|
||||
db.savedItemWithLabelsAndHighlightsDao().insertAll(listOf(item))
|
||||
}
|
||||
|
||||
db.savedItemAndSavedItemLabelCrossRefDao().insertAll(labelCrossRefs)
|
||||
|
||||
// Persist Highlights
|
||||
db.highlightDao().insertAll(syncResult.highlights)
|
||||
|
||||
val highlightCrossRefs = syncResult.highlights.map {
|
||||
SavedItemAndHighlightCrossRef(highlightId = it.highlightId, savedItemId = savedItem.savedItemId)
|
||||
}
|
||||
|
||||
db.savedItemAndHighlightCrossRefDao().insertAll(highlightCrossRefs)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
package app.omnivore.omnivore.networking
|
||||
|
||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||
import app.omnivore.omnivore.graphql.generated.TypeaheadSearchQuery
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItem
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.TypeaheadCardData
|
||||
import com.apollographql.apollo3.api.Optional
|
||||
|
||||
data class SearchQueryResponse(
|
||||
val cursor: String?,
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@ import app.omnivore.omnivore.persistence.entities.*
|
|||
SavedItemAndSavedItemLabelCrossRef::class,
|
||||
SavedItemAndHighlightCrossRef::class
|
||||
],
|
||||
version = 6
|
||||
version = 7
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun viewerDao(): ViewerDao
|
||||
abstract fun savedItemDao(): SavedItemDao
|
||||
abstract fun highlightDao(): HighlightDao
|
||||
abstract fun savedItemLabelDao(): SavedItemLabelDao
|
||||
abstract fun savedItemWithLabelsAndHighlightsDao(): SavedItemWithLabelsAndHighlightsDao
|
||||
abstract fun savedItemAndSavedItemLabelCrossRefDao(): SavedItemAndSavedItemLabelCrossRefDao
|
||||
abstract fun savedItemAndHighlightCrossRefDao(): SavedItemAndHighlightCrossRefDao
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ data class Highlight(
|
|||
entity = Highlight::class,
|
||||
parentColumns = arrayOf("highlightId"),
|
||||
childColumns = arrayOf("highlightId"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = SavedItem::class,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package app.omnivore.omnivore.persistence.entities
|
||||
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.*
|
||||
import app.omnivore.omnivore.BuildConfig
|
||||
import app.omnivore.omnivore.graphql.generated.SearchQuery
|
||||
import app.omnivore.omnivore.models.ServerSyncStatus
|
||||
import app.omnivore.omnivore.ui.library.SavedItemSortFilter
|
||||
import java.util.*
|
||||
|
||||
@Entity
|
||||
|
|
@ -69,30 +66,6 @@ data class SavedItem(
|
|||
}
|
||||
}
|
||||
|
||||
data class SavedItemCardData(
|
||||
val savedItemId: String,
|
||||
val slug: String,
|
||||
val publisherURLString: String?,
|
||||
val title: String,
|
||||
val author: String?,
|
||||
val imageURLString: String?,
|
||||
val isArchived: Boolean,
|
||||
val pageURLString: String,
|
||||
val contentReader: String?,
|
||||
val savedAt: String,
|
||||
val readingProgress: Double,
|
||||
val wordsCount: Int?
|
||||
) {
|
||||
fun publisherDisplayName(): String? {
|
||||
return publisherURLString?.toUri()?.host
|
||||
}
|
||||
|
||||
fun isPDF(): Boolean {
|
||||
val hasPDFSuffix = pageURLString.endsWith("pdf")
|
||||
return contentReader == "PDF" || hasPDFSuffix
|
||||
}
|
||||
}
|
||||
|
||||
data class TypeaheadCardData(
|
||||
val savedItemId: String,
|
||||
val slug: String,
|
||||
|
|
@ -100,6 +73,58 @@ data class TypeaheadCardData(
|
|||
val isArchived: Boolean,
|
||||
)
|
||||
|
||||
@Dao
|
||||
abstract class SavedItemWithLabelsAndHighlightsDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertSavedItems(items: List<SavedItem>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertLabelCrossRefs(items: List<SavedItemAndSavedItemLabelCrossRef>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertLabels(items: List<SavedItemLabel>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertHighlights(items: List<Highlight>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun insertHighlightCrossRefs(items: List<SavedItemAndHighlightCrossRef>)
|
||||
|
||||
@Transaction
|
||||
open fun insertAll(savedItems: List<SavedItemWithLabelsAndHighlights>) {
|
||||
insertSavedItems(savedItems.map { it.savedItem })
|
||||
|
||||
val labels: MutableList<SavedItemLabel> = mutableListOf()
|
||||
val highlights: MutableList<Highlight> = mutableListOf()
|
||||
|
||||
val labelCrossRefs: MutableList<SavedItemAndSavedItemLabelCrossRef> = mutableListOf()
|
||||
val highlightCrossRefs: MutableList<SavedItemAndHighlightCrossRef> = mutableListOf()
|
||||
|
||||
for (searchItem in savedItems) {
|
||||
labels.addAll(searchItem.labels)
|
||||
highlights.addAll(searchItem.highlights)
|
||||
|
||||
val newLabelCrossRefs = searchItem.labels.map {
|
||||
SavedItemAndSavedItemLabelCrossRef(savedItemLabelId = it.savedItemLabelId, savedItemId = searchItem.savedItem.savedItemId)
|
||||
}
|
||||
|
||||
val newHighlightCrossRefs = searchItem.highlights.map {
|
||||
SavedItemAndHighlightCrossRef(highlightId = it.highlightId, savedItemId = searchItem.savedItem.savedItemId)
|
||||
}
|
||||
|
||||
labelCrossRefs.addAll(newLabelCrossRefs)
|
||||
highlightCrossRefs.addAll(newHighlightCrossRefs)
|
||||
}
|
||||
|
||||
insertLabels(labels)
|
||||
insertLabelCrossRefs(labelCrossRefs)
|
||||
|
||||
insertHighlights(highlights)
|
||||
insertHighlightCrossRefs(highlightCrossRefs)
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface SavedItemDao {
|
||||
@Query("SELECT * FROM savedItem")
|
||||
|
|
@ -114,54 +139,12 @@ interface SavedItemDao {
|
|||
@Query("SELECT * FROM savedItem WHERE slug = :slug")
|
||||
fun getSavedItemWithLabelsAndHighlights(slug: String): SavedItemWithLabelsAndHighlights?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(items: List<SavedItem>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(item: SavedItem)
|
||||
|
||||
@Query("DELETE FROM savedItem WHERE savedItemId = :itemID")
|
||||
fun deleteById(itemID: String)
|
||||
|
||||
@Update
|
||||
fun update(savedItem: SavedItem)
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.columns} " +
|
||||
"FROM SavedItem " +
|
||||
"WHERE serverSyncStatus != 2 AND isArchived != :archiveFilter " +
|
||||
"ORDER BY savedAt DESC"
|
||||
)
|
||||
fun getLibraryLiveData(archiveFilter: Int): LiveData<List<SavedItemCardDataWithLabels>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.columns} " +
|
||||
"FROM SavedItem " +
|
||||
"WHERE serverSyncStatus != 2 AND isArchived != :archiveFilter " +
|
||||
"ORDER BY savedAt ASC"
|
||||
)
|
||||
fun getLibraryLiveDataSortedByOldest(archiveFilter: Int): LiveData<List<SavedItemCardDataWithLabels>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.columns} " +
|
||||
"FROM SavedItem " +
|
||||
"WHERE serverSyncStatus != 2 AND isArchived != :archiveFilter " +
|
||||
"ORDER BY readAt DESC, savedAt DESC"
|
||||
)
|
||||
fun getLibraryLiveDataSortedByRecentlyRead(archiveFilter: Int): LiveData<List<SavedItemCardDataWithLabels>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.columns} " +
|
||||
"FROM SavedItem " +
|
||||
"WHERE serverSyncStatus != 2 AND isArchived != :archiveFilter " +
|
||||
"ORDER BY publishDate DESC"
|
||||
)
|
||||
fun getLibraryLiveDataSortedByRecentlyPublished(archiveFilter: Int): LiveData<List<SavedItemCardDataWithLabels>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT ${SavedItemQueryConstants.libraryColumns} " +
|
||||
|
|
@ -173,8 +156,6 @@ interface SavedItemDao {
|
|||
"LEFT OUTER JOIN Highlight on highlight.highlightId = SavedItemAndHighlightCrossRef.highlightId " +
|
||||
|
||||
"WHERE SavedItem.savedItemId = :savedItemId " +
|
||||
"AND SavedItem.serverSyncStatus != 2 " +
|
||||
"AND Highlight.serverSyncStatus != 2 " +
|
||||
|
||||
"GROUP BY SavedItem.savedItemId "
|
||||
)
|
||||
|
|
@ -208,7 +189,7 @@ interface SavedItemDao {
|
|||
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(
|
||||
val result = _filteredLibraryData(
|
||||
allowedArchiveStates = allowedArchiveStates,
|
||||
sortKey = sortKey,
|
||||
hasRequiredLabels = requiredLabels.size,
|
||||
|
|
@ -217,6 +198,7 @@ interface SavedItemDao {
|
|||
excludedLabels = excludedLabels,
|
||||
allowedContentReaders = allowedContentReaders
|
||||
)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,25 +44,6 @@ data class SavedItemAndSavedItemLabelCrossRef(
|
|||
val savedItemId: String
|
||||
)
|
||||
|
||||
data class SavedItemWithLabels(
|
||||
@Embedded val savedItem: SavedItem,
|
||||
@Relation(
|
||||
parentColumn = "savedItemId",
|
||||
entityColumn = "savedItemLabelId",
|
||||
associateBy = Junction(SavedItemAndSavedItemLabelCrossRef::class)
|
||||
)
|
||||
val labels: List<SavedItemLabel>
|
||||
)
|
||||
|
||||
data class SavedItemCardDataWithLabels(
|
||||
@Embedded val cardData: SavedItemCardData,
|
||||
@Relation(
|
||||
parentColumn = "savedItemId",
|
||||
entityColumn = "savedItemLabelId",
|
||||
associateBy = Junction(SavedItemAndSavedItemLabelCrossRef::class)
|
||||
)
|
||||
val labels: List<SavedItemLabel>
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface SavedItemAndSavedItemLabelCrossRefDao {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ fun WebReaderLabelsSelectionSheet(viewModel: WebReaderViewModel) {
|
|||
|
||||
val modalBottomSheetState = rememberModalBottomSheetState(
|
||||
ModalBottomSheetValue.HalfExpanded,
|
||||
confirmStateChange = { it != ModalBottomSheetValue.Hidden }
|
||||
)
|
||||
|
||||
if (isActive) {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@ package app.omnivore.omnivore.ui.library
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
|
|
@ -19,14 +17,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.ui.components.LabelsSelectionSheet
|
||||
import app.omnivore.omnivore.ui.savedItemViews.SavedItemCard
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ class LibraryViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun refresh() {
|
||||
cursor = null
|
||||
librarySearchCursor = null
|
||||
isRefreshing = true
|
||||
load(true)
|
||||
}
|
||||
|
|
@ -164,8 +166,9 @@ class LibraryViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
fun handleFilterChanges() {
|
||||
librarySearchCursor = null
|
||||
|
||||
if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) {
|
||||
val applied = appliedFilterLiveData.value
|
||||
val sortKey = when (appliedSortFilterLiveData.value) {
|
||||
SavedItemSortFilter.NEWEST -> "newest"
|
||||
SavedItemSortFilter.OLDEST -> "oldest"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package app.omnivore.omnivore.ui.library
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
|
||||
interface SavedItemViewModel {
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardDataWithLabels
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.ui.reader.WebReaderLoadingContainerActivity
|
||||
import app.omnivore.omnivore.persistence.entities.TypeaheadCardData
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ package app.omnivore.omnivore.ui.notebook
|
|||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -37,126 +32,114 @@ import androidx.compose.ui.res.colorResource
|
|||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.omnivore.omnivore.MainActivity
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.ui.library.*
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
import kotlinx.coroutines.launch
|
||||
import app.omnivore.omnivore.persistence.entities.Highlight
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotebookActivity: ComponentActivity() {
|
||||
val viewModel: NotebookViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val savedItemId = intent.getStringExtra("SAVED_ITEM_ID")
|
||||
fun notebookMD(notes: List<Highlight>, highlights: List<Highlight>): String {
|
||||
var result = ""
|
||||
|
||||
setContent {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val useDarkIcons = !isSystemInDarkTheme()
|
||||
if (notes.isNotEmpty()) {
|
||||
result += "## Notes\n"
|
||||
notes.forEach {
|
||||
result += it.annotation + "\n"
|
||||
}
|
||||
result += "\n"
|
||||
}
|
||||
|
||||
DisposableEffect(systemUiController, useDarkIcons) {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Black,
|
||||
darkIcons = false
|
||||
)
|
||||
|
||||
onDispose {}
|
||||
}
|
||||
|
||||
OmnivoreTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
// .background(color = Color.Black)
|
||||
) {
|
||||
savedItemId?.let {
|
||||
NotebookView(
|
||||
savedItemId = savedItemId,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (highlights.isNotEmpty()) {
|
||||
result += "## Highlights\n"
|
||||
highlights.forEach {
|
||||
result += "> ${it.quote}\n"
|
||||
if ((it.annotation?: "").isNotEmpty()) {
|
||||
result += it.annotation + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// // animate the view up when keyboard appears
|
||||
// WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
// val rootView = findViewById<View>(android.R.id.content).rootView
|
||||
// ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
|
||||
// val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
// rootView.setPadding(0, 0, 0, imeHeight)
|
||||
// insets
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun startMainActivity() {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
this.startActivity(intent)
|
||||
result += "\n"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun NotebookView(savedItemId: String, viewModel: NotebookViewModel) {
|
||||
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
var isMenuOpen by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val savedItem = viewModel.getLibraryItemById(savedItemId).observeAsState()
|
||||
val scrollState = rememberScrollState()
|
||||
val modalBottomSheetState = rememberModalBottomSheetState(
|
||||
ModalBottomSheetValue.Hidden,
|
||||
)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
val clipboard: ClipboardManager? =
|
||||
LocalContext.current.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
|
||||
|
||||
val notes = savedItem.value?.highlights?.filter { it.type == "NOTE" } ?: listOf()
|
||||
val highlights = savedItem.value?.highlights?.filter { it.type == "HIGHLIGHT" } ?: listOf()
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
sheetBackgroundColor = Color.Transparent,
|
||||
sheetState = modalBottomSheetState,
|
||||
sheetContent = {
|
||||
EditNoteModal()
|
||||
// EditNoteModal()
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Notebook") },
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
actions = {
|
||||
Box {
|
||||
IconButton(onClick = {
|
||||
isMenuOpen = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
if (isMenuOpen) {
|
||||
DropdownMenu(
|
||||
expanded = isMenuOpen,
|
||||
onDismissRequest = { isMenuOpen = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Copy") },
|
||||
onClick = {
|
||||
val clip = ClipData.newPlainText("notebook", notebookMD(notes, highlights))
|
||||
clipboard?.let {
|
||||
it
|
||||
clipboard?.setPrimaryClip(clip)
|
||||
} ?: run {
|
||||
coroutineScope.launch {
|
||||
snackBarHostState
|
||||
.showSnackbar("Notebook copied")
|
||||
}
|
||||
}
|
||||
isMenuOpen = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// actions = {
|
||||
// IconButton(onClick = {
|
||||
//
|
||||
// }) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Default.MoreVert,
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(scrollState)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
|
|
@ -166,7 +149,6 @@ fun NotebookView(savedItemId: String, viewModel: NotebookViewModel) {
|
|||
}
|
||||
HighlightsList(it)
|
||||
}
|
||||
Spacer(Modifier.weight(100f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -8,6 +9,10 @@ import android.view.ViewGroup
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -22,7 +27,11 @@ import androidx.compose.ui.platform.ComposeView
|
|||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import app.omnivore.omnivore.ui.notebook.ArticleNotes
|
||||
import app.omnivore.omnivore.ui.notebook.HighlightsList
|
||||
import app.omnivore.omnivore.ui.notebook.notebookMD
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AnnotationEditFragment : DialogFragment() {
|
||||
private var onSave: (String) -> Unit = {}
|
||||
|
|
@ -54,7 +63,7 @@ class AnnotationEditFragment : DialogFragment() {
|
|||
initialAnnotation,
|
||||
onSave,
|
||||
onCancel,
|
||||
dismissAction = { dismiss() }
|
||||
// dismissAction = { dismiss() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -73,62 +82,96 @@ fun AnnotationEditView(
|
|||
initialAnnotation: String,
|
||||
onSave: (String) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
dismissAction: () -> Unit = {}
|
||||
) {
|
||||
val annotation = remember { mutableStateOf(initialAnnotation) }
|
||||
val focusRequester = FocusRequester()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row {
|
||||
TextButton(
|
||||
onClick = {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Notebook") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
onCancel()
|
||||
dismissAction()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back",
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
|
||||
Text(text = "Note")
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
onSave(annotation.value)
|
||||
dismissAction()
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onSave(annotation.value)
|
||||
}
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
TextField(
|
||||
value = annotation.value,
|
||||
onValueChange = { annotation.value = it },
|
||||
modifier = Modifier
|
||||
.width(IntrinsicSize.Max)
|
||||
.height(IntrinsicSize.Max)
|
||||
.weight(1.0F)
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
// Column(
|
||||
// modifier = Modifier.padding(16.dp),
|
||||
// horizontalAlignment = Alignment.CenterHorizontally,
|
||||
// ) {
|
||||
// Row {
|
||||
// TextButton(
|
||||
// onClick = {
|
||||
// onCancel()
|
||||
// dismissAction()
|
||||
// }
|
||||
// ) {
|
||||
// Text("Cancel")
|
||||
// }
|
||||
//
|
||||
// Spacer(modifier = Modifier.weight(1.0F))
|
||||
//
|
||||
// Text(text = "Note")
|
||||
//
|
||||
// Spacer(modifier = Modifier.weight(1.0F))
|
||||
//
|
||||
// TextButton(
|
||||
// onClick = {
|
||||
// onSave(annotation.value)
|
||||
// dismissAction()
|
||||
// }
|
||||
// ) {
|
||||
// Text("Save")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
//
|
||||
//
|
||||
//
|
||||
// Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
//
|
||||
// LaunchedEffect(Unit) {
|
||||
// focusRequester.requestFocus()
|
||||
// }
|
||||
|
||||
// Row {
|
||||
// Spacer(modifier = Modifier.weight(0.1F))
|
||||
|
|
@ -142,8 +185,7 @@ fun AnnotationEditView(
|
|||
// )
|
||||
// Spacer(modifier = Modifier.weight(0.1F))
|
||||
// }
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
// }
|
||||
//
|
||||
// // }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
|
||||
@Composable
|
||||
fun ReaderPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val currentWebPreferences = webReaderViewModel.storedWebPreferences(isDark)
|
||||
val isFontListExpanded = remember { mutableStateOf(false) }
|
||||
val highContrastTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersHighContrastText) }
|
||||
|
||||
val justifyTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersJustifyText) }
|
||||
|
||||
val selectedWebFontName = remember { mutableStateOf(currentWebPreferences.fontFamily.displayText) }
|
||||
|
||||
|
||||
var fontSizeSliderValue by remember { mutableStateOf(currentWebPreferences.textFontSize.toFloat()) }
|
||||
var marginSliderValue by remember { mutableStateOf(currentWebPreferences.maxWidthPercentage.toFloat()) }
|
||||
var lineSpacingSliderValue by remember { mutableStateOf(currentWebPreferences.lineHeight.toFloat()) }
|
||||
|
||||
val themeState = remember { mutableStateOf(currentWebPreferences.storedThemePreference) }
|
||||
|
||||
val themeListState = rememberLazyListState()
|
||||
|
||||
OmnivoreTheme() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 15.dp)
|
||||
.padding(vertical = 35.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 15.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("Font", style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
))
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Box {
|
||||
OutlinedButton(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
onClick = { isFontListExpanded.value = true },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
contentColor = Color(red = 137, green = 137, blue = 137),
|
||||
// containerColor = Color.Transparent,
|
||||
),
|
||||
) {
|
||||
Text(selectedWebFontName.value)
|
||||
}
|
||||
if (isFontListExpanded.value) {
|
||||
DropdownMenu(
|
||||
expanded = isFontListExpanded.value,
|
||||
onDismissRequest = { isFontListExpanded.value = false },
|
||||
) {
|
||||
WebFont.values().forEach {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(it.displayText, style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
))
|
||||
},
|
||||
onClick = {
|
||||
webReaderViewModel.applyWebFont(it)
|
||||
selectedWebFontName.value = it.displayText
|
||||
isFontListExpanded.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("Font Size:", style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
))
|
||||
Slider(
|
||||
value = fontSizeSliderValue,
|
||||
onValueChange = {
|
||||
fontSizeSliderValue = it
|
||||
webReaderViewModel.setFontSize(it.toInt())
|
||||
},
|
||||
steps = 10,
|
||||
valueRange = 8f..28f,
|
||||
)
|
||||
|
||||
Text("Margin", style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
))
|
||||
Slider(
|
||||
value = marginSliderValue,
|
||||
onValueChange = {
|
||||
marginSliderValue = it
|
||||
webReaderViewModel.setMaxWidthPercentage(it.toInt())
|
||||
},
|
||||
steps = 4,
|
||||
valueRange = 60f..100f,
|
||||
)
|
||||
|
||||
Text("Line Spacing", style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
))
|
||||
Slider(
|
||||
value = lineSpacingSliderValue,
|
||||
onValueChange = {
|
||||
lineSpacingSliderValue = it
|
||||
webReaderViewModel.setLineHeight(it.toInt())
|
||||
},
|
||||
steps = 8,
|
||||
valueRange = 100f..300f,
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text("Theme:", style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
))
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Text("Auto", style = TextStyle(
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
))
|
||||
Checkbox(
|
||||
checked = themeState.value == "System",
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
themeState.value = "System"
|
||||
webReaderViewModel.updateStoredThemePreference("System", isDark)
|
||||
} else {
|
||||
val newThemeKey = if (isDark) "Black" else "Light"
|
||||
themeState.value = newThemeKey
|
||||
webReaderViewModel.updateStoredThemePreference(newThemeKey, isDark)
|
||||
}
|
||||
})
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
for(theme in Themes.values()) {
|
||||
if (theme.themeKey != "System") {
|
||||
val isSelected = theme.themeKey == themeState.value
|
||||
Button(
|
||||
onClick = {
|
||||
themeState.value = theme.themeKey
|
||||
webReaderViewModel.updateStoredThemePreference(theme.themeKey, isDark)
|
||||
},
|
||||
shape = CircleShape,
|
||||
border = BorderStroke(3.dp, if (isSelected) colorResource(R.color.cta_yellow) else Color.Transparent),
|
||||
modifier = Modifier.size(35.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
|
||||
containerColor = Color(theme.backgroundColor)
|
||||
)
|
||||
) {
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(0.1F))
|
||||
}
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(2.0F))
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("High Contrast Text",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137)
|
||||
))
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Switch(
|
||||
checked = highContrastTextSwitchState.value,
|
||||
onCheckedChange = {
|
||||
highContrastTextSwitchState.value = it
|
||||
webReaderViewModel.updateHighContrastTextPreference(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Justify Text",
|
||||
style = TextStyle(
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(red = 137, green = 137, blue = 137))
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Switch(
|
||||
checked = justifyTextSwitchState.value,
|
||||
onCheckedChange = {
|
||||
justifyTextSwitchState.value = it
|
||||
webReaderViewModel.updateJustifyText(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Stepper(label: String, onIncrease: () -> Unit, onDecrease: () -> Unit) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 6.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
|
||||
IconButton(onClick = { onDecrease() }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.minus),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
Divider(
|
||||
color = Color.Black,
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.width(1.dp)
|
||||
)
|
||||
|
||||
IconButton(onClick = { onIncrease() }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.plus),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class WebPreferences(
|
||||
val textFontSize: Int,
|
||||
val lineHeight: Int,
|
||||
val maxWidthPercentage: Int,
|
||||
val themeKey: String,
|
||||
val storedThemePreference: String,
|
||||
val fontFamily: WebFont,
|
||||
val prefersHighContrastText: Boolean,
|
||||
val prefersJustifyText: Boolean
|
||||
)
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
package app.omnivore.omnivore.ui.reader
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.ui.components.SegmentedControl
|
||||
|
||||
@Composable
|
||||
fun WebPreferencesDialog(onDismiss: () -> Unit, webReaderViewModel: WebReaderViewModel) {
|
||||
Dialog(onDismissRequest = { onDismiss() }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.height(350.dp)
|
||||
) {
|
||||
WebPreferencesView(webReaderViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WebPreferencesView(webReaderViewModel: WebReaderViewModel) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val currentWebPreferences = webReaderViewModel.storedWebPreferences(isDark)
|
||||
val isFontListExpanded = remember { mutableStateOf(false) }
|
||||
val highContrastTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersHighContrastText) }
|
||||
|
||||
val justifyTextSwitchState = remember { mutableStateOf(currentWebPreferences.prefersJustifyText) }
|
||||
|
||||
val selectedWebFontRawValue = remember { mutableStateOf(currentWebPreferences.fontFamily.rawValue) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp, bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text("Web Preferences")
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Stepper(
|
||||
label = "Font Size:",
|
||||
onIncrease = { webReaderViewModel.updateFontSize(isIncrease = true) },
|
||||
onDecrease = { webReaderViewModel.updateFontSize(isIncrease = false) }
|
||||
)
|
||||
|
||||
Stepper(
|
||||
label = "Margin:",
|
||||
onIncrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = false) },
|
||||
onDecrease = { webReaderViewModel.updateMaxWidthPercentage(isIncrease = true) }
|
||||
)
|
||||
|
||||
Stepper(
|
||||
label = "Line Spacing:",
|
||||
onIncrease = { webReaderViewModel.updateLineSpacing(isIncrease = true) },
|
||||
onDecrease = { webReaderViewModel.updateLineSpacing(isIncrease = false) }
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("High Contrast Text")
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Switch(
|
||||
checked = highContrastTextSwitchState.value,
|
||||
onCheckedChange = {
|
||||
highContrastTextSwitchState.value = it
|
||||
webReaderViewModel.updateHighContrastTextPreference(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Justify Text")
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Switch(
|
||||
checked = justifyTextSwitchState.value,
|
||||
onCheckedChange = {
|
||||
justifyTextSwitchState.value = it
|
||||
webReaderViewModel.updateJustifyText(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text("Theme:")
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
SegmentedControl(
|
||||
items = webReaderViewModel.systemThemeKeys,
|
||||
initialSelectedItemIndex = webReaderViewModel.systemThemeKeys.indexOf(currentWebPreferences.storedThemePreference)
|
||||
) {
|
||||
webReaderViewModel.updateStoredThemePreference(it, isDark)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { isFontListExpanded.value = !isFontListExpanded.value })
|
||||
) {
|
||||
Text("Font Family")
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
Icon(
|
||||
imageVector =
|
||||
if (isFontListExpanded.value)
|
||||
Icons.Filled.KeyboardArrowDown
|
||||
else
|
||||
Icons.Filled.KeyboardArrowRight,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
if (isFontListExpanded.value) {
|
||||
WebFont.values().forEach {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = {
|
||||
webReaderViewModel.applyWebFont(it)
|
||||
selectedWebFontRawValue.value = it.rawValue
|
||||
})
|
||||
) {
|
||||
Text(
|
||||
it.displayText,
|
||||
modifier = Modifier
|
||||
.padding(top = 6.dp, start = 6.dp, end = 6.dp, bottom = 6.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
if (it.rawValue == selectedWebFontRawValue.value) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Stepper(label: String, onIncrease: () -> Unit, onDecrease: () -> Unit) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 6.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
|
||||
IconButton(onClick = { onDecrease() }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.minus),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
Divider(
|
||||
color = Color.Black,
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.width(1.dp)
|
||||
)
|
||||
|
||||
IconButton(onClick = { onIncrease() }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.plus),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class WebPreferences(
|
||||
val textFontSize: Int,
|
||||
val lineHeight: Int,
|
||||
val maxWidthPercentage: Int,
|
||||
val themeKey: String,
|
||||
val storedThemePreference: String,
|
||||
val fontFamily: WebFont,
|
||||
val prefersHighContrastText: Boolean,
|
||||
val prefersJustifyText: Boolean
|
||||
)
|
||||
|
|
@ -30,7 +30,6 @@ import java.util.*
|
|||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun WebReader(
|
||||
preferences: WebPreferences,
|
||||
styledContent: String,
|
||||
webReaderViewModel: WebReaderViewModel
|
||||
) {
|
||||
|
|
@ -115,14 +114,6 @@ fun WebReader(
|
|||
for (script in webReaderViewModel.javascriptDispatchQueue) {
|
||||
Log.d("js", "executing script: $script")
|
||||
it.evaluateJavascript(script, null)
|
||||
|
||||
if (script.contains("event.isDark")) {
|
||||
if (script.contains("event.isDark = 'true'")) {
|
||||
it.setBackgroundColor(Color.Transparent.hashCode())
|
||||
} else {
|
||||
it.setBackgroundColor(Color.White.hashCode())
|
||||
}
|
||||
}
|
||||
}
|
||||
webReaderViewModel.resetJavascriptDispatchQueue()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package app.omnivore.omnivore.ui.reader
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
|
|
@ -11,45 +10,45 @@ import androidx.activity.viewModels
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import app.omnivore.omnivore.MainActivity
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.ui.components.LabelsSelectionSheetContent
|
||||
import app.omnivore.omnivore.ui.components.WebReaderLabelsSelectionSheet
|
||||
import app.omnivore.omnivore.ui.notebook.NotebookView
|
||||
import app.omnivore.omnivore.ui.notebook.NotebookViewModel
|
||||
import app.omnivore.omnivore.ui.savedItemViews.SavedItemContextMenu
|
||||
import app.omnivore.omnivore.ui.theme.OmnivoreTheme
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import app.omnivore.omnivore.Routes
|
||||
import app.omnivore.omnivore.ui.notebook.NotebookActivity
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WebReaderLoadingContainerActivity: ComponentActivity() {
|
||||
val viewModel: WebReaderViewModel by viewModels()
|
||||
val notebookViewModel: NotebookViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -83,6 +82,7 @@ class WebReaderLoadingContainerActivity: ComponentActivity() {
|
|||
slug = slug,
|
||||
onLibraryIconTap = if (requestID != null) { { startMainActivity() } } else null,
|
||||
webReaderViewModel = viewModel,
|
||||
notebookViewModel = notebookViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -105,24 +105,43 @@ class WebReaderLoadingContainerActivity: ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
enum class BottomSheetState(
|
||||
) {
|
||||
NONE(),
|
||||
PREFERENCES(),
|
||||
NOTEBOOK(),
|
||||
HIGHLIGHTNOTE(),
|
||||
LABELS(),
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, onLibraryIconTap: (() -> Unit)? = null, webReaderViewModel: WebReaderViewModel) {
|
||||
fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null,
|
||||
onLibraryIconTap: (() -> Unit)? = null,
|
||||
webReaderViewModel: WebReaderViewModel,
|
||||
notebookViewModel: NotebookViewModel) {
|
||||
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
var showWebPreferencesDialog by remember { mutableStateOf(false ) }
|
||||
var bottomSheetState by remember { mutableStateOf(BottomSheetState.NONE) }
|
||||
|
||||
val isDarkMode = isSystemInDarkTheme()
|
||||
val currentThemeKey = webReaderViewModel.currentThemeKey.observeAsState()
|
||||
val currentTheme = Themes.values().find { it.themeKey == currentThemeKey.value }
|
||||
|
||||
val webReaderParams: WebReaderParams? by webReaderViewModel.webReaderParamsLiveData.observeAsState(null)
|
||||
val annotation: String? by webReaderViewModel.annotationLiveData.observeAsState(null)
|
||||
val shouldPopView: Boolean by webReaderViewModel.shouldPopViewLiveData.observeAsState(false)
|
||||
val toolbarHeightPx: Float by webReaderViewModel.currentToolbarHeightLiveData.observeAsState(0.0f)
|
||||
|
||||
val labels: List<SavedItemLabel> by webReaderViewModel.savedItemLabelsLiveData.observeAsState(listOf())
|
||||
|
||||
val maxToolbarHeight = 48.dp
|
||||
val backgroundColor = if (isSystemInDarkTheme()) Color.Black else Color.White
|
||||
webReaderViewModel.maxToolbarHeightPx = with(LocalDensity.current) { maxToolbarHeight.roundToPx().toFloat() }
|
||||
webReaderViewModel.loadItem(slug = slug, requestID = requestID)
|
||||
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val styledContent = webReaderParams?.let {
|
||||
val webReaderContent = WebReaderContent(
|
||||
|
|
@ -133,111 +152,240 @@ fun WebReaderLoadingContainer(slug: String? = null, requestID: String? = null, o
|
|||
webReaderContent.styledContent()
|
||||
} ?: null
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.background(color = backgroundColor)
|
||||
) {
|
||||
if (styledContent != null) {
|
||||
WebReader(
|
||||
preferences = webReaderViewModel.storedWebPreferences(isSystemInDarkTheme()),
|
||||
styledContent = styledContent,
|
||||
webReaderViewModel = webReaderViewModel
|
||||
)
|
||||
val modalBottomSheetState = rememberModalBottomSheetState(
|
||||
initialValue = ModalBottomSheetValue.Hidden,
|
||||
)
|
||||
|
||||
TopAppBar(
|
||||
modifier = Modifier
|
||||
.height(height = with(LocalDensity.current) {
|
||||
toolbarHeightPx.roundToInt().toDp()
|
||||
}),
|
||||
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = androidx.compose.material.icons.Icons.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
val themeBackgroundColor = currentTheme?.let {
|
||||
if (it.themeKey == "System" && isDarkMode) {
|
||||
Color(0xFF000000)
|
||||
} else if (it.themeKey == "System" ) {
|
||||
Color(0xFFFFFFFF)
|
||||
} else {
|
||||
Color(it.backgroundColor ?: 0xFFFFFFFF)
|
||||
}
|
||||
} ?: Color(0xFFFFFFFF)
|
||||
val themeTintColor = currentTheme?.let {
|
||||
if (it.themeKey == "System" && isDarkMode) {
|
||||
Color(0xFFFFFFFF)
|
||||
} else if (it.themeKey == "System" ) {
|
||||
Color(0xFF000000)
|
||||
} else {
|
||||
Color(it.foregroundColor ?: 0xFF000000)
|
||||
}
|
||||
} ?: Color(0xFF000000)
|
||||
|
||||
annotation?.let {
|
||||
bottomSheetState = BottomSheetState.HIGHLIGHTNOTE
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
|
||||
}
|
||||
}
|
||||
|
||||
val showLabelsSelector: Boolean by webReaderViewModel.showLabelsSelectionSheetLiveData.observeAsState(false)
|
||||
|
||||
if (showLabelsSelector) {
|
||||
bottomSheetState = BottomSheetState.LABELS
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding(),
|
||||
sheetBackgroundColor = Color.Transparent,
|
||||
sheetState = modalBottomSheetState,
|
||||
sheetContent = {
|
||||
when (bottomSheetState) {
|
||||
BottomSheetState.PREFERENCES -> {
|
||||
BottomSheetUI("Reader Preferences") {
|
||||
ReaderPreferencesView(webReaderViewModel)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (onLibraryIconTap != null) {
|
||||
IconButton(onClick = { onLibraryIconTap() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Home,
|
||||
contentDescription = null
|
||||
}
|
||||
BottomSheetState.NOTEBOOK -> {
|
||||
webReaderParams?.let { params ->
|
||||
BottomSheetUI(title = "Notebook") {
|
||||
NotebookView(savedItemId = params.item.savedItemId, viewModel = notebookViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
BottomSheetState.HIGHLIGHTNOTE -> {
|
||||
annotation?.let { annotation ->
|
||||
BottomSheetUI(title = "Note") {
|
||||
AnnotationEditView(
|
||||
initialAnnotation = annotation,
|
||||
onSave = {
|
||||
webReaderViewModel.saveAnnotation(it)
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.hide()
|
||||
bottomSheetState = BottomSheetState.NONE
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
webReaderViewModel.cancelAnnotationEdit()
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.hide()
|
||||
bottomSheetState = BottomSheetState.NONE
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
webReaderParams?.let {
|
||||
}
|
||||
app.omnivore.omnivore.ui.reader.BottomSheetState.LABELS -> {
|
||||
BottomSheetUI(title = "Notebook") {
|
||||
LabelsSelectionSheetContent(
|
||||
labels = labels,
|
||||
initialSelectedLabels = webReaderParams?.labels ?: listOf(),
|
||||
onCancel = {
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.hide()
|
||||
bottomSheetState = BottomSheetState.NONE
|
||||
}
|
||||
},
|
||||
isLibraryMode = false,
|
||||
onSave = {
|
||||
if (it != labels) {
|
||||
webReaderViewModel.updateSavedItemLabels(
|
||||
savedItemID = webReaderParams?.item?.savedItemId ?: "", labels = it
|
||||
)
|
||||
}
|
||||
coroutineScope.launch {
|
||||
modalBottomSheetState.hide()
|
||||
bottomSheetState = BottomSheetState.NONE
|
||||
}
|
||||
},
|
||||
onCreateLabel = { newLabelName, labelHexValue ->
|
||||
webReaderViewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
BottomSheetState.NONE -> {
|
||||
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1.0F))
|
||||
}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier
|
||||
.height(height = with(LocalDensity.current) {
|
||||
toolbarHeightPx.roundToInt().toDp()
|
||||
}),
|
||||
backgroundColor = themeBackgroundColor,
|
||||
elevation = 0.dp,
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
val intent = Intent(context, NotebookActivity::class.java)
|
||||
intent.putExtra("SAVED_ITEM_ID", it.item.savedItemId)
|
||||
context.startActivity(intent)
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.notebook),
|
||||
contentDescription = null
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
modifier = Modifier,
|
||||
contentDescription = "Back",
|
||||
tint = themeTintColor
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { showWebPreferencesDialog = true }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.format_letter_case),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { isMenuExpanded = true }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dots_horizontal),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
SavedItemContextMenu(
|
||||
isExpanded = isMenuExpanded,
|
||||
isArchived = webReaderParams!!.item.isArchived,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
actionHandler = {
|
||||
webReaderViewModel.handleSavedItemAction(
|
||||
webReaderParams!!.item.savedItemId,
|
||||
it
|
||||
},
|
||||
actions = {
|
||||
if (onLibraryIconTap != null) {
|
||||
IconButton(onClick = { onLibraryIconTap() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Home,
|
||||
contentDescription = null,
|
||||
tint = themeTintColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
webReaderParams?.let {
|
||||
IconButton(onClick = {
|
||||
coroutineScope.launch {
|
||||
bottomSheetState = BottomSheetState.NOTEBOOK
|
||||
modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.notebook),
|
||||
contentDescription = null,
|
||||
tint = themeTintColor
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
coroutineScope.launch {
|
||||
bottomSheetState = BottomSheetState.PREFERENCES
|
||||
modalBottomSheetState.animateTo(ModalBottomSheetValue.HalfExpanded)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.format_letter_case),
|
||||
contentDescription = null,
|
||||
tint = themeTintColor
|
||||
)
|
||||
}
|
||||
)
|
||||
IconButton(onClick = { isMenuExpanded = true }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dots_horizontal),
|
||||
contentDescription = null,
|
||||
tint = themeTintColor
|
||||
)
|
||||
if (isMenuExpanded) {
|
||||
webReaderParams?.let { params ->
|
||||
SavedItemContextMenu(
|
||||
isExpanded = isMenuExpanded,
|
||||
isArchived = params.item.isArchived,
|
||||
onDismiss = { isMenuExpanded = false },
|
||||
actionHandler = {
|
||||
webReaderViewModel.handleSavedItemAction(
|
||||
params.item.savedItemId,
|
||||
it
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (styledContent != null) {
|
||||
WebReader(
|
||||
styledContent = styledContent,
|
||||
webReaderViewModel = webReaderViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (showWebPreferencesDialog) {
|
||||
WebPreferencesDialog(
|
||||
onDismiss = {
|
||||
showWebPreferencesDialog = false
|
||||
},
|
||||
webReaderViewModel = webReaderViewModel
|
||||
)
|
||||
LaunchedEffect(shouldPopView) {
|
||||
if (shouldPopView) {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
if (annotation != null) {
|
||||
AnnotationEditView(
|
||||
initialAnnotation = annotation!!,
|
||||
onSave = {
|
||||
webReaderViewModel.saveAnnotation(it)
|
||||
},
|
||||
onCancel = {
|
||||
webReaderViewModel.cancelAnnotationEdit()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
WebReaderLabelsSelectionSheet(webReaderViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldPopView) {
|
||||
if (shouldPopView) {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun BottomSheetUI(title: String?, content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp))
|
||||
.background(Color.White)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
Scaffold(
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier
|
||||
.fillMaxSize()) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.*
|
||||
import app.omnivore.omnivore.DatastoreKeys
|
||||
import app.omnivore.omnivore.DatastoreRepository
|
||||
import app.omnivore.omnivore.dataService.*
|
||||
|
|
@ -24,6 +22,7 @@ import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
|
|||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -37,6 +36,15 @@ data class AnnotationWebViewMessage(
|
|||
val annotation: String?
|
||||
)
|
||||
|
||||
enum class Themes(val themeKey: String, val backgroundColor: Long, val foregroundColor: Long) {
|
||||
SYSTEM("System", 0xFFFFFFFF, 0xFF000000),
|
||||
LIGHT("Light", 0xFFFFFFFF, 0xFF000000),
|
||||
SEPIA("Sepia", 0xFFFBF0D9, 0xFF000000),
|
||||
DARK("Dark", 0xFF2F3030, 0xFFFFFFFF),
|
||||
APOLLO("Apollo", 0xFF6A6968, 0xFFFFFFFF),
|
||||
BLACK("Black", 0xFF000000, 0xFFFFFFFF),
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class WebReaderViewModel @Inject constructor(
|
||||
private val datastoreRepo: DatastoreRepository,
|
||||
|
|
@ -56,9 +64,6 @@ class WebReaderViewModel @Inject constructor(
|
|||
val showLabelsSelectionSheetLiveData = MutableLiveData(false)
|
||||
val savedItemLabelsLiveData = dataService.db.savedItemLabelDao().getSavedItemLabelsLiveData()
|
||||
|
||||
// "Sepia", "Apollo",
|
||||
val systemThemeKeys = listOf("Light", "Black", "System")
|
||||
|
||||
var hasTappedExistingHighlight = false
|
||||
var lastTapCoordinates: TapCoordinates? = null
|
||||
private var isLoading = false
|
||||
|
|
@ -256,6 +261,11 @@ class WebReaderViewModel @Inject constructor(
|
|||
javascriptActionLoopUUIDLiveData.value = UUID.randomUUID()
|
||||
}
|
||||
|
||||
val currentThemeKey: LiveData<String> = datastoreRepo
|
||||
.themeKeyFlow
|
||||
.distinctUntilChanged()
|
||||
.asLiveData()
|
||||
|
||||
fun storedWebPreferences(isDarkMode: Boolean): WebPreferences = runBlocking {
|
||||
val storedFontSize = datastoreRepo.getInt(DatastoreKeys.preferredWebFontSize)
|
||||
val storedLineHeight = datastoreRepo.getInt(DatastoreKeys.preferredWebLineHeight)
|
||||
|
|
@ -288,59 +298,37 @@ class WebReaderViewModel @Inject constructor(
|
|||
return storedThemePreference
|
||||
}
|
||||
|
||||
fun updateStoredThemePreference(index: Int, isDarkMode: Boolean) {
|
||||
val newThemeKey = themeKey(isDarkMode, systemThemeKeys[index])
|
||||
fun updateStoredThemePreference(newThemeKey: String, isDarkMode: Boolean) {
|
||||
Log.d("theme", "Setting theme key: ${newThemeKey}")
|
||||
|
||||
runBlocking {
|
||||
datastoreRepo.putString(DatastoreKeys.preferredTheme, systemThemeKeys[index])
|
||||
datastoreRepo.putString(DatastoreKeys.preferredTheme, newThemeKey)
|
||||
}
|
||||
|
||||
val script = "var event = new Event('updateTheme');event.themeName = '$newThemeKey';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun updateFontSize(isIncrease: Boolean) {
|
||||
val delta = if (isIncrease) 2 else -2
|
||||
var newFontSize: Int
|
||||
|
||||
fun setFontSize(newFontSize: Int) {
|
||||
runBlocking {
|
||||
val storedFontSize = datastoreRepo.getInt(DatastoreKeys.preferredWebFontSize)
|
||||
newFontSize = ((storedFontSize ?: 12) + delta).coerceIn(8, 28)
|
||||
datastoreRepo.putInt(DatastoreKeys.preferredWebFontSize, newFontSize)
|
||||
}
|
||||
|
||||
// Get value from data store and then update it
|
||||
val script = "var event = new Event('updateFontSize');event.fontSize = '$newFontSize';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun updateMaxWidthPercentage(isIncrease: Boolean) {
|
||||
val delta = if (isIncrease) 10 else -10
|
||||
var newMaxWidthPercentageValue: Int
|
||||
|
||||
fun setMaxWidthPercentage(newMaxWidthPercentageValue: Int) {
|
||||
runBlocking {
|
||||
val storedWidth = datastoreRepo.getInt(DatastoreKeys.preferredWebMaxWidthPercentage)
|
||||
newMaxWidthPercentageValue = ((storedWidth ?: 100) + delta).coerceIn(40, 100)
|
||||
datastoreRepo.putInt(DatastoreKeys.preferredWebMaxWidthPercentage, newMaxWidthPercentageValue)
|
||||
}
|
||||
|
||||
// Get value from data store and then update it
|
||||
val script = "var event = new Event('updateMaxWidthPercentage');event.maxWidthPercentage = '$newMaxWidthPercentageValue';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
||||
fun updateLineSpacing(isIncrease: Boolean) {
|
||||
val delta = if (isIncrease) 25 else -25
|
||||
var newLineHeight: Int
|
||||
|
||||
fun setLineHeight(newLineHeight: Int) {
|
||||
runBlocking {
|
||||
val storedHeight = datastoreRepo.getInt(DatastoreKeys.preferredWebLineHeight)
|
||||
newLineHeight = ((storedHeight ?: 150) + delta).coerceIn(100, 300)
|
||||
datastoreRepo.putInt(DatastoreKeys.preferredWebLineHeight, newLineHeight)
|
||||
}
|
||||
|
||||
// Get value from data store and then update it
|
||||
val script = "var event = new Event('updateLineHeight');event.lineHeight = '$newLineHeight';document.dispatchEvent(event);"
|
||||
enqueueScript(script)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package app.omnivore.omnivore.ui.savedItemViews
|
||||
|
||||
import LabelChip
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
|
|
@ -21,8 +22,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import app.omnivore.omnivore.R
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemWithLabelsAndHighlights
|
||||
import app.omnivore.omnivore.ui.components.LabelChipColors
|
||||
import app.omnivore.omnivore.ui.library.LibraryViewModel
|
||||
|
|
@ -36,8 +35,6 @@ import coil.compose.rememberAsyncImagePainter
|
|||
fun SavedItemCard(savedItemViewModel: SavedItemViewModel, savedItem: SavedItemWithLabelsAndHighlights, onClickHandler: () -> Unit, actionHandler: (SavedItemAction) -> Unit) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val actionsMenuItem: SavedItemWithLabelsAndHighlights? by savedItemViewModel.actionsMenuItemLiveData.observeAsState(null)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
|
|
|
|||
|
|
@ -24,19 +24,19 @@ fun SavedItemContextMenu(
|
|||
expanded = isExpanded,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Edit Labels") },
|
||||
onClick = {
|
||||
actionHandler(SavedItemAction.EditLabels)
|
||||
onDismiss()
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.tag),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
)
|
||||
// DropdownMenuItem(
|
||||
// text = { Text("Edit Labels") },
|
||||
// onClick = {
|
||||
// actionHandler(SavedItemAction.EditLabels)
|
||||
// onDismiss()
|
||||
// },
|
||||
// leadingIcon = {
|
||||
// Icon(
|
||||
// painter = painterResource(id = R.drawable.tag),
|
||||
// contentDescription = null
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (isArchived) "Unarchive" else "Archive") },
|
||||
onClick = {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ import androidx.compose.ui.text.TextStyle
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemCardData
|
||||
import app.omnivore.omnivore.persistence.entities.SavedItemLabel
|
||||
import app.omnivore.omnivore.persistence.entities.TypeaheadCardData
|
||||
import app.omnivore.omnivore.ui.components.LabelChipColors
|
||||
import app.omnivore.omnivore.ui.library.SavedItemAction
|
||||
|
|
|
|||
Loading…
Reference in a new issue