separate following screen viewmodel

This commit is contained in:
Stefano Sansone
2024-04-18 00:03:06 +02:00
parent 9cf57b4916
commit 7eb294af5f
9 changed files with 664 additions and 93 deletions

View File

@ -0,0 +1,153 @@
package app.omnivore.omnivore.feature.following
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.feature.components.LabelsViewModel
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
import app.omnivore.omnivore.feature.library.AddLinkBottomSheet
import app.omnivore.omnivore.feature.library.EditBottomSheet
import app.omnivore.omnivore.feature.library.LabelBottomSheet
import app.omnivore.omnivore.feature.library.LibraryBottomSheetState
import app.omnivore.omnivore.feature.library.LibraryNavigationBar
import app.omnivore.omnivore.feature.library.LibraryViewContent
import app.omnivore.omnivore.feature.save.SaveViewModel
import app.omnivore.omnivore.navigation.Routes
import app.omnivore.omnivore.navigation.TopLevelDestination
import kotlinx.coroutines.launch
@Composable
internal fun FollowingScreen(
labelsViewModel: LabelsViewModel,
saveViewModel: SaveViewModel,
editInfoViewModel: EditInfoViewModel,
navController: NavHostController,
viewModel: FollowingViewModel = hiltViewModel()
) {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val showBottomSheet: LibraryBottomSheetState by viewModel.bottomSheetState.observeAsState(
LibraryBottomSheetState.HIDDEN
)
viewModel.snackbarMessage?.let {
coroutineScope.launch {
snackbarHostState.showSnackbar(it)
viewModel.clearSnackbarMessage()
}
}
val labels by viewModel.labelsState.collectAsStateWithLifecycle()
val activeLabels by viewModel.activeLabels.collectAsStateWithLifecycle()
when (showBottomSheet) {
LibraryBottomSheetState.ADD_LINK -> {
AddLinkBottomSheet(saveViewModel) {
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
}
}
LibraryBottomSheetState.LABEL -> {
LabelBottomSheet(
deleteCurrentItem = { viewModel.currentItem.value = null },
labels = labels,
currentSavedItemData = viewModel.currentSavedItemUnderEdit(),
labelsViewModel,
{ viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN },
{ labelName, hexColorValue ->
viewModel.createNewSavedItemLabel(labelName, hexColorValue)
},
{ savedItemId, labels ->
viewModel.updateSavedItemLabels(savedItemId, labels)
},
activeLabels,
{ viewModel.updateAppliedLabels(it) }
)
}
LibraryBottomSheetState.EDIT -> {
EditBottomSheet(
editInfoViewModel,
deleteCurrentItem = { viewModel.currentItem.value = null },
{ viewModel.refresh() },
viewModel.currentSavedItemUnderEdit()
) {
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
}
}
LibraryBottomSheetState.HIDDEN -> {
}
}
val currentTopLevelDestination = TopLevelDestination.entries.find { it.route == navController.currentDestination?.route }
val selectedItem: SavedItemWithLabelsAndHighlights? by viewModel.actionsMenuItemLiveData.observeAsState()
Scaffold(
topBar = {
LibraryNavigationBar(
currentDestination = currentTopLevelDestination,
savedItemViewModel = viewModel,
onSearchClicked = { navController.navigate(Routes.Search.route) },
onAddLinkClicked = {
viewModel.bottomSheetState.value = LibraryBottomSheetState.ADD_LINK
}
)
},
) { paddingValues ->
when (uiState) {
is FollowingUiState.Success -> {
LibraryViewContent(
isFollowingScreen = currentTopLevelDestination == TopLevelDestination.FOLLOWING,
{ viewModel.actionsMenuItemLiveData.postValue(null) },
savedItemViewModel = viewModel,
refresh = { viewModel.refresh() },
onUnarchive = { viewModel.unarchiveSavedItem(it) },
onArchive = { viewModel.archiveSavedItem(it) },
onDelete = { viewModel.deleteSavedItem(it) },
paddingValues = paddingValues,
items = (uiState as FollowingUiState.Success).items,
selectedItem = selectedItem,
onSavedItemAction = { id, action ->
viewModel.handleSavedItemAction(id, action)
},
{ viewModel.loadUsingSearchAPI() },
{ viewModel.initialLoad() }
)
}
is FollowingUiState.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(strokeCap = StrokeCap.Round)
}
}
else -> {
// TODO
}
}
}
}

View File

@ -0,0 +1,390 @@
package app.omnivore.omnivore.feature.following
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.omnivore.omnivore.R
import app.omnivore.omnivore.core.data.model.LibraryQuery
import app.omnivore.omnivore.core.data.repository.LibraryRepository
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.feature.library.LibraryBottomSheetState
import app.omnivore.omnivore.feature.library.SavedItemAction
import app.omnivore.omnivore.feature.library.SavedItemFilter
import app.omnivore.omnivore.feature.library.SavedItemSortFilter
import app.omnivore.omnivore.feature.library.SavedItemViewModel
import app.omnivore.omnivore.utils.DatastoreKeys
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.time.Instant
import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
class FollowingViewModel @Inject constructor(
private val datastoreRepo: DatastoreRepository,
private val libraryRepository: LibraryRepository,
@ApplicationContext private val applicationContext: Context
) : ViewModel(), SavedItemViewModel {
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
private var librarySearchCursor: String? = null
var snackbarMessage by mutableStateOf<String?>(null)
private set
private val _libraryQuery = MutableStateFlow(
LibraryQuery(
allowedArchiveStates = listOf(0),
sortKey = "newest",
requiredLabels = listOf(),
excludedLabels = listOf(),
allowedContentReaders = listOf("WEB", "PDF", "EPUB")
)
)
val uiState: StateFlow<FollowingUiState> = _libraryQuery.flatMapLatest { query ->
libraryRepository.getSavedItems(query)
}.map(FollowingUiState::Success).stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = FollowingUiState.Loading
)
val appliedFilterLiveData = MutableLiveData<SavedItemFilter>(
SavedItemFilter.FOLLOWING
)
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
val bottomSheetState = MutableLiveData(LibraryBottomSheetState.HIDDEN)
val currentItem = mutableStateOf<String?>(null)
val labelsState = libraryRepository.getSavedItemsLabels().stateIn(
scope = viewModelScope, started = SharingStarted.Lazily, initialValue = listOf()
)
val activeLabels = MutableStateFlow<List<SavedItemLabel>>(listOf())
override val actionsMenuItemLiveData = MutableLiveData<SavedItemWithLabelsAndHighlights?>(null)
private fun loadInitialFilterValues() {
syncLabels()
viewModelScope.launch {
handleFilterChanges()
for (slug in contentRequestChannel) {
libraryRepository.fetchSavedItemContent(slug)
}
}
updateSavedItemFilter(appliedFilterLiveData.value ?: SavedItemFilter.INBOX)
}
private fun syncLabels() {
viewModelScope.launch {
val labels = libraryRepository.getLabels()
libraryRepository.insertAllLabels(labels)
}
}
fun clearSnackbarMessage() {
snackbarMessage = null
}
fun refresh() {
librarySearchCursor = null
load()
}
private fun getLastSyncTime(): Instant? = runBlocking {
datastoreRepo.getString(DatastoreKeys.libraryLastSyncTimestamp)?.let {
try {
return@let Instant.parse(it)
} catch (e: Exception) {
return@let null
}
}
}
fun initialLoad() {
if (getLastSyncTime() == null) {
librarySearchCursor = null
}
load()
}
fun load() {
loadInitialFilterValues()
viewModelScope.launch {
syncItems()
loadUsingSearchAPI()
}
}
fun loadUsingSearchAPI() {
viewModelScope.launch {
val result = libraryRepository.librarySearch(
cursor = librarySearchCursor, query = searchQueryString()
)
result.cursor?.let {
librarySearchCursor = it
}
result.savedItems.map {
val isSavedInDB = libraryRepository.isSavedItemContentStoredInDB(it.savedItem.slug)
if (!isSavedInDB) {
delay(2000)
contentRequestChannel.send(it.savedItem.slug)
}
}
}
}
fun updateSavedItemFilter(filter: SavedItemFilter) {
viewModelScope.launch {
datastoreRepo.putString(DatastoreKeys.lastUsedSavedItemFilter, filter.rawValue)
appliedFilterLiveData.value = filter
handleFilterChanges()
}
}
fun updateSavedItemSortFilter(filter: SavedItemSortFilter) {
viewModelScope.launch {
datastoreRepo.putString(DatastoreKeys.lastUsedSavedItemSortFilter, filter.rawValue)
appliedSortFilterLiveData.value = filter
handleFilterChanges()
}
}
fun updateAppliedLabels(labels: List<SavedItemLabel>) {
viewModelScope.launch {
activeLabels.value = labels
handleFilterChanges()
}
}
private fun handleFilterChanges() {
librarySearchCursor = null
if (appliedSortFilterLiveData.value != null && appliedFilterLiveData.value != null) {
val sortKey = when (appliedSortFilterLiveData.value) {
SavedItemSortFilter.NEWEST -> "newest"
SavedItemSortFilter.OLDEST -> "oldest"
SavedItemSortFilter.RECENTLY_READ -> "recentlyRead"
SavedItemSortFilter.RECENTLY_PUBLISHED -> "recentlyPublished"
else -> "newest"
}
val allowedArchiveStates = when (appliedFilterLiveData.value) {
SavedItemFilter.ALL -> listOf(0, 1)
SavedItemFilter.ARCHIVED -> listOf(1)
else -> listOf(0)
}
val allowedContentReaders = when (appliedFilterLiveData.value) {
SavedItemFilter.FILES -> listOf("PDF", "EPUB")
else -> listOf("WEB", "PDF", "EPUB")
}
var requiredLabels = when (appliedFilterLiveData.value) {
SavedItemFilter.FOLLOWING -> listOf("Newsletter", "RSS")
SavedItemFilter.NEWSLETTERS -> listOf("Newsletter")
SavedItemFilter.FEEDS -> listOf("RSS")
else -> listOf("Newsletter", "RSS")//activeLabels.value.map { it.name }
}
activeLabels.value.let { it ->
requiredLabels = requiredLabels + it.map { it.name }
}
val excludeLabels = when (appliedFilterLiveData.value) {
SavedItemFilter.READ_LATER -> listOf("Newsletter", "RSS")
else -> listOf()
}
_libraryQuery.value = LibraryQuery(
allowedArchiveStates = allowedArchiveStates,
sortKey = sortKey,
requiredLabels = requiredLabels,
excludedLabels = excludeLabels,
allowedContentReaders = allowedContentReaders
)
}
}
private suspend fun syncItems() {
val syncStart = Instant.now()
val lastSyncDate = getLastSyncTime() ?: Instant.MIN
withContext(Dispatchers.IO) {
performItemSync(
cursor = null,
since = lastSyncDate.toString(),
count = 0,
startTime = syncStart.toString()
)
}
}
private suspend fun performItemSync(
cursor: String?,
since: String,
count: Int,
startTime: String,
isInitialBatch: Boolean = true
) {
libraryRepository.syncOfflineItemsWithServerIfNeeded()
val result = libraryRepository.sync(since = since, cursor = cursor, limit = 20)
// Fetch content for the initial batch only
if (isInitialBatch) {
for (slug in result.savedItemSlugs) {
delay(250)
contentRequestChannel.send(slug)
}
}
val totalCount = count + result.count
if (!result.hasError && result.hasMoreItems && result.cursor != null) {
performItemSync(
cursor = result.cursor,
since = since,
count = totalCount,
startTime = startTime,
isInitialBatch = false
)
} else {
datastoreRepo.putString(DatastoreKeys.libraryLastSyncTimestamp, startTime)
}
}
override fun handleSavedItemAction(itemId: String, action: SavedItemAction) {
when (action) {
SavedItemAction.Delete -> {
deleteSavedItem(itemId)
}
SavedItemAction.Archive -> {
archiveSavedItem(itemId)
}
SavedItemAction.Unarchive -> {
unarchiveSavedItem(itemId)
}
SavedItemAction.EditLabels -> {
currentItem.value = itemId
bottomSheetState.value = LibraryBottomSheetState.LABEL
}
SavedItemAction.EditInfo -> {
currentItem.value = itemId
bottomSheetState.value = LibraryBottomSheetState.EDIT
}
SavedItemAction.MarkRead -> {
viewModelScope.launch {
libraryRepository.updateReadingProgress(itemId, 100.0, 0)
}
}
SavedItemAction.MarkUnread -> {
viewModelScope.launch {
libraryRepository.updateReadingProgress(itemId, 0.0, 0)
}
}
}
actionsMenuItemLiveData.postValue(null)
}
fun deleteSavedItem(itemID: String) {
viewModelScope.launch {
libraryRepository.deleteSavedItem(itemID)
}
}
fun archiveSavedItem(itemID: String) {
viewModelScope.launch {
libraryRepository.archiveSavedItem(itemID)
}
}
fun unarchiveSavedItem(itemID: String) {
viewModelScope.launch {
libraryRepository.unarchiveSavedItem(itemID)
}
}
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
viewModelScope.launch {
val result = libraryRepository.setSavedItemLabels(
itemId = savedItemID, labels = labels
)
snackbarMessage = if (result) {
applicationContext.getString(R.string.library_view_model_snackbar_success)
} else {
applicationContext.getString(R.string.library_view_model_snackbar_error)
}
handleFilterChanges()
}
}
fun createNewSavedItemLabel(labelName: String, hexColorValue: String) {
viewModelScope.launch {
libraryRepository.createNewSavedItemLabel(labelName, hexColorValue)
}
}
fun currentSavedItemUnderEdit(): SavedItemWithLabelsAndHighlights? {
currentItem.value?.let { itemID ->
return (uiState.value as FollowingUiState.Success).items.first { it.savedItem.savedItemId == itemID }
}
return null
}
private fun searchQueryString(): String {
var query =
"${appliedFilterLiveData.value?.queryString} ${appliedSortFilterLiveData.value?.queryString}"
activeLabels.value.let {
if (it.isNotEmpty()) {
query += " label:"
query += it.joinToString { label -> label.name }
}
}
return query
}
}
sealed interface FollowingUiState {
data object Loading : FollowingUiState
data class Success(
val items: List<SavedItemWithLabelsAndHighlights>,
) : FollowingUiState
data object Error : FollowingUiState
}

View File

@ -27,6 +27,7 @@ import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.omnivore.omnivore.R
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.feature.components.LabelChipColors
@ -41,7 +42,7 @@ fun LibraryFilterBar(
val activeSavedItemFilter: SavedItemFilter by viewModel.appliedFilterLiveData.observeAsState(
if (isFollowingScreen) SavedItemFilter.FOLLOWING else SavedItemFilter.INBOX
)
val activeLabels: List<SavedItemLabel> by viewModel.activeLabelsLiveData.observeAsState(listOf())
val activeLabels: List<SavedItemLabel> by viewModel.activeLabels.collectAsStateWithLifecycle()
var isSavedItemSortFilterMenuExpanded by remember { mutableStateOf(false) }
val activeSavedItemSortFilter: SavedItemSortFilter by viewModel.appliedSortFilterLiveData.observeAsState(
@ -97,7 +98,7 @@ fun LibraryFilterBar(
val chipColors = LabelChipColors.fromHex(label.color)
AssistChip(onClick = {
viewModel.updateAppliedLabels((viewModel.activeLabelsLiveData.value
viewModel.updateAppliedLabels((viewModel.activeLabels.value
?: listOf()).filter { it.savedItemLabelId != label.savedItemLabelId })
},
label = { Text(label.name) },

View File

@ -21,14 +21,12 @@ import androidx.compose.material.DismissState
import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.ScaffoldState
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Unarchive
import androidx.compose.material.rememberDismissState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
@ -36,6 +34,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.rememberModalBottomSheetState
@ -60,6 +59,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.core.database.entities.SavedItemWithLabelsAndHighlights
import app.omnivore.omnivore.feature.components.AddLinkSheetContent
import app.omnivore.omnivore.feature.components.LabelsSelectionSheetContent
@ -85,8 +85,7 @@ internal fun LibraryView(
navController: NavHostController,
viewModel: LibraryViewModel = hiltViewModel()
) {
val scaffoldState: ScaffoldState = rememberScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@ -97,11 +96,14 @@ internal fun LibraryView(
viewModel.snackbarMessage?.let {
coroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(it)
snackbarHostState.showSnackbar(it)
viewModel.clearSnackbarMessage()
}
}
val labels by viewModel.labelsState.collectAsStateWithLifecycle()
val activeLabels by viewModel.activeLabels.collectAsStateWithLifecycle()
when (showBottomSheet) {
LibraryBottomSheetState.ADD_LINK -> {
AddLinkBottomSheet(saveViewModel) {
@ -111,17 +113,28 @@ internal fun LibraryView(
LibraryBottomSheetState.LABEL -> {
LabelBottomSheet(
viewModel,
labelsViewModel
) {
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
}
deleteCurrentItem = { viewModel.currentItem.value = null },
labels = labels,
currentSavedItemData = viewModel.currentSavedItemUnderEdit(),
labelsViewModel,
{ viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN },
{ labelName, hexColorValue ->
viewModel.createNewSavedItemLabel(labelName, hexColorValue)
},
{ savedItemId, labels ->
viewModel.updateSavedItemLabels(savedItemId, labels)
},
activeLabels,
{ viewModel.updateAppliedLabels(it) }
)
}
LibraryBottomSheetState.EDIT -> {
EditBottomSheet(
editInfoViewModel,
viewModel
deleteCurrentItem = { viewModel.currentItem.value = null },
{ viewModel.refresh() },
viewModel.currentSavedItemUnderEdit()
) {
viewModel.bottomSheetState.value = LibraryBottomSheetState.HIDDEN
}
@ -132,6 +145,7 @@ internal fun LibraryView(
}
val currentTopLevelDestination = TopLevelDestination.entries.find { it.route == navController.currentDestination?.route }
val selectedItem: SavedItemWithLabelsAndHighlights? by viewModel.actionsMenuItemLiveData.observeAsState()
Scaffold(
topBar = {
@ -139,7 +153,9 @@ internal fun LibraryView(
currentDestination = currentTopLevelDestination,
savedItemViewModel = viewModel,
onSearchClicked = { navController.navigate(Routes.Search.route) },
onAddLinkClicked = { showAddLinkBottomSheet(viewModel) }
onAddLinkClicked = {
viewModel.bottomSheetState.value = LibraryBottomSheetState.ADD_LINK
}
)
},
) { paddingValues ->
@ -147,9 +163,20 @@ internal fun LibraryView(
is LibraryUiState.Success -> {
LibraryViewContent(
isFollowingScreen = currentTopLevelDestination == TopLevelDestination.FOLLOWING,
viewModel,
{ viewModel.actionsMenuItemLiveData.postValue(null) },
savedItemViewModel = viewModel,
refresh = { viewModel.refresh() },
onUnarchive = { viewModel.unarchiveSavedItem(it) },
onArchive = { viewModel.archiveSavedItem(it) },
onDelete = { viewModel.deleteSavedItem(it) },
paddingValues = paddingValues,
uiState = uiState
items = (uiState as LibraryUiState.Success).items,
selectedItem = selectedItem,
onSavedItemAction = { id, action ->
viewModel.handleSavedItemAction(id, action)
},
{ viewModel.loadUsingSearchAPI() },
{ viewModel.initialLoad() }
)
}
is LibraryUiState.Loading -> {
@ -169,16 +196,18 @@ internal fun LibraryView(
}
}
fun showAddLinkBottomSheet(libraryViewModel: LibraryViewModel) {
libraryViewModel.bottomSheetState.value = LibraryBottomSheetState.ADD_LINK
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LabelBottomSheet(
libraryViewModel: LibraryViewModel,
deleteCurrentItem: () -> Unit,
labels: List<SavedItemLabel>,
currentSavedItemData: SavedItemWithLabelsAndHighlights?,
labelsViewModel: LabelsViewModel,
onDismiss: () -> Unit = {}
onDismiss: () -> Unit = {},
createNewSavedItemLabel: (String, String) -> Unit,
updateSavedItemLabels: (String, List<SavedItemLabel>) -> Unit,
activeLabels: List<SavedItemLabel>,
updateAppliedLabels: (List<SavedItemLabel>) -> Unit
) {
ModalBottomSheet(
onDismissRequest = { onDismiss() },
@ -188,48 +217,41 @@ fun LabelBottomSheet(
),
) {
val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit()
val labels by libraryViewModel.labelsState.collectAsStateWithLifecycle()
if (currentSavedItemData != null) {
LabelsSelectionSheetContent(
labels = labels,
labelsViewModel = labelsViewModel,
initialSelectedLabels = currentSavedItemData.labels,
onCancel = {
libraryViewModel.currentItem.value = null
deleteCurrentItem()
onDismiss()
},
isLibraryMode = false,
onSave = {
if (it != labels) {
libraryViewModel.updateSavedItemLabels(
savedItemID = currentSavedItemData.savedItem.savedItemId,
labels = it
)
updateSavedItemLabels(currentSavedItemData.savedItem.savedItemId, it)
}
libraryViewModel.currentItem.value = null
deleteCurrentItem()
onDismiss()
},
onCreateLabel = { newLabelName, labelHexValue ->
libraryViewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
createNewSavedItemLabel(newLabelName, labelHexValue)
}
)
} else { // Is used in library mode
LabelsSelectionSheetContent(
labels = labels,
labelsViewModel = labelsViewModel,
initialSelectedLabels = libraryViewModel.activeLabelsLiveData.value ?: listOf(),
initialSelectedLabels = activeLabels,
onCancel = { onDismiss() },
isLibraryMode = true,
onSave = {
libraryViewModel.updateAppliedLabels(it)
libraryViewModel.currentItem.value = null
updateAppliedLabels(it)
deleteCurrentItem()
onDismiss()
},
onCreateLabel = { newLabelName, labelHexValue ->
libraryViewModel.createNewSavedItemLabel(newLabelName, labelHexValue)
createNewSavedItemLabel(newLabelName, labelHexValue)
}
)
}
@ -268,7 +290,9 @@ fun AddLinkBottomSheet(
@Composable
fun EditBottomSheet(
editInfoViewModel: EditInfoViewModel,
libraryViewModel: LibraryViewModel,
deleteCurrentItem: () -> Unit,
refresh: () -> Unit,
currentSavedItemUnderEdit: SavedItemWithLabelsAndHighlights?,
onDismiss: () -> Unit = {}
) {
ModalBottomSheet(
@ -278,20 +302,19 @@ fun EditBottomSheet(
skipPartiallyExpanded = true
),
) {
val currentSavedItemData = libraryViewModel.currentSavedItemUnderEdit()
EditInfoSheetContent(
savedItemId = currentSavedItemData?.savedItem?.savedItemId,
title = currentSavedItemData?.savedItem?.title,
author = currentSavedItemData?.savedItem?.author,
description = currentSavedItemData?.savedItem?.descriptionText,
savedItemId = currentSavedItemUnderEdit?.savedItem?.savedItemId,
title = currentSavedItemUnderEdit?.savedItem?.title,
author = currentSavedItemUnderEdit?.savedItem?.author,
description = currentSavedItemUnderEdit?.savedItem?.descriptionText,
viewModel = editInfoViewModel,
onCancel = {
libraryViewModel.currentItem.value = null
deleteCurrentItem()
onDismiss()
},
onUpdated = {
libraryViewModel.currentItem.value = null
libraryViewModel.refresh()
deleteCurrentItem()
refresh()
onDismiss()
}
)
@ -303,9 +326,18 @@ fun EditBottomSheet(
@Composable
fun LibraryViewContent(
isFollowingScreen: Boolean,
libraryViewModel: LibraryViewModel,
selectItem: () -> Unit,
savedItemViewModel: SavedItemViewModel,
refresh: () -> Unit,
onUnarchive: (String) -> Unit,
onArchive: (String) -> Unit,
onDelete: (String) -> Unit,
paddingValues: PaddingValues,
uiState: LibraryUiState
items: List<SavedItemWithLabelsAndHighlights>,
selectedItem: SavedItemWithLabelsAndHighlights?,
onSavedItemAction: (String, SavedItemAction) -> Unit,
loadUsingSearchAPI: () -> Unit,
initialLoad: () -> Unit
) {
val context = LocalContext.current
val listState = rememberLazyListState()
@ -315,13 +347,11 @@ fun LibraryViewContent(
LaunchedEffect(true) {
// fetch something
delay(1500)
libraryViewModel.refresh()
refresh()
pullToRefreshState.endRefresh()
}
}
val selectedItem: SavedItemWithLabelsAndHighlights? by libraryViewModel.actionsMenuItemLiveData.observeAsState()
Box(
modifier = Modifier
.padding(top = paddingValues.calculateTopPadding())
@ -337,11 +367,10 @@ fun LibraryViewContent(
horizontalAlignment = Alignment.CenterHorizontally
) {
items(
items = (uiState as LibraryUiState.Success).items,
items = items,
key = { item -> item.savedItem.savedItemId }
) { cardDataWithLabels ->
val swipeThreshold = 0.45f
val currentThresholdFraction = remember { mutableStateOf(0f) }
val currentItem by rememberUpdatedState(cardDataWithLabels.savedItem)
val swipeState = rememberDismissState(
@ -364,12 +393,12 @@ fun LibraryViewContent(
if (it == DismissValue.DismissedToEnd) { // Archiving/UnArchiving.
if (currentItem.isArchived) {
libraryViewModel.unarchiveSavedItem(currentItem.savedItemId)
onUnarchive(currentItem.savedItemId)
} else {
libraryViewModel.archiveSavedItem(currentItem.savedItemId)
onArchive(currentItem.savedItemId)
}
} else if (it == DismissValue.DismissedToStart) { // Deleting.
libraryViewModel.deleteSavedItem(currentItem.savedItemId)
onDelete(currentItem.savedItemId)
}
true
@ -426,10 +455,10 @@ fun LibraryViewContent(
)
SavedItemCard(
selected = selected,
savedItemViewModel = libraryViewModel,
savedItemViewModel = savedItemViewModel,
savedItem = savedItem,
onClickHandler = {
libraryViewModel.actionsMenuItemLiveData.postValue(null)
selectItem()
val activityClass =
if (currentItem.contentReader == "PDF") PDFReaderActivity::class.java else WebReaderLoadingContainerActivity::class.java
val intent = Intent(context, activityClass)
@ -437,7 +466,7 @@ fun LibraryViewContent(
context.startActivity(intent)
},
actionHandler = {
libraryViewModel.handleSavedItemAction(
onSavedItemAction(
currentItem.savedItemId,
it
)
@ -454,12 +483,12 @@ fun LibraryViewContent(
}
InfiniteListHandler(listState = listState) {
if ((uiState as LibraryUiState.Success).items.isEmpty()) {
if (items.isEmpty()) {
Log.d("sync", "loading with load func")
libraryViewModel.initialLoad()
initialLoad()
} else {
Log.d("sync", "loading with search api")
libraryViewModel.loadUsingSearchAPI()
loadUsingSearchAPI()
}
}
@ -467,8 +496,6 @@ fun LibraryViewContent(
modifier = Modifier.align(Alignment.TopCenter),
state = pullToRefreshState,
)
// LabelsSelectionSheet(viewModel = libraryViewModel)
}
}

View File

@ -45,7 +45,7 @@ class LibraryViewModel @Inject constructor(
var snackbarMessage by mutableStateOf<String?>(null)
private set
private val _libraryQuery = MutableStateFlow(
LibraryQuery(
allowedArchiveStates = listOf(0),
@ -59,10 +59,10 @@ class LibraryViewModel @Inject constructor(
val uiState: StateFlow<LibraryUiState> = _libraryQuery.flatMapLatest { query ->
libraryRepository.getSavedItems(query)
}.map(LibraryUiState::Success).stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = LibraryUiState.Loading
)
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = LibraryUiState.Loading
)
val appliedFilterLiveData = MutableLiveData<SavedItemFilter>()
val appliedSortFilterLiveData = MutableLiveData(SavedItemSortFilter.NEWEST)
@ -73,7 +73,7 @@ class LibraryViewModel @Inject constructor(
scope = viewModelScope, started = SharingStarted.Lazily, initialValue = listOf()
)
val activeLabelsLiveData = MutableLiveData<List<SavedItemLabel>>(listOf())
val activeLabels = MutableStateFlow<List<SavedItemLabel>>(listOf())
override val actionsMenuItemLiveData = MutableLiveData<SavedItemWithLabelsAndHighlights?>(null)
@ -169,7 +169,7 @@ class LibraryViewModel @Inject constructor(
fun updateAppliedLabels(labels: List<SavedItemLabel>) {
viewModelScope.launch {
activeLabelsLiveData.value = labels
activeLabels.value = labels
handleFilterChanges()
}
}
@ -201,10 +201,10 @@ class LibraryViewModel @Inject constructor(
SavedItemFilter.FOLLOWING -> listOf("Newsletter", "RSS")
SavedItemFilter.NEWSLETTERS -> listOf("Newsletter")
SavedItemFilter.FEEDS -> listOf("RSS")
else -> (activeLabelsLiveData.value ?: listOf()).map { it.name }
else -> activeLabels.value.map { it.name }
}
activeLabelsLiveData.value?.let { it ->
activeLabels.value.let { it ->
requiredLabels = requiredLabels + it.map { it.name }
}
@ -271,39 +271,39 @@ class LibraryViewModel @Inject constructor(
}
}
override fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
override fun handleSavedItemAction(itemId: String, action: SavedItemAction) {
when (action) {
SavedItemAction.Delete -> {
deleteSavedItem(itemID)
deleteSavedItem(itemId)
}
SavedItemAction.Archive -> {
archiveSavedItem(itemID)
archiveSavedItem(itemId)
}
SavedItemAction.Unarchive -> {
unarchiveSavedItem(itemID)
unarchiveSavedItem(itemId)
}
SavedItemAction.EditLabels -> {
currentItem.value = itemID
currentItem.value = itemId
bottomSheetState.value = LibraryBottomSheetState.LABEL
}
SavedItemAction.EditInfo -> {
currentItem.value = itemID
currentItem.value = itemId
bottomSheetState.value = LibraryBottomSheetState.EDIT
}
SavedItemAction.MarkRead -> {
viewModelScope.launch {
libraryRepository.updateReadingProgress(itemID, 100.0, 0)
libraryRepository.updateReadingProgress(itemId, 100.0, 0)
}
}
SavedItemAction.MarkUnread -> {
viewModelScope.launch {
libraryRepository.updateReadingProgress(itemID, 0.0, 0)
libraryRepository.updateReadingProgress(itemId, 0.0, 0)
}
}
}
@ -328,10 +328,10 @@ class LibraryViewModel @Inject constructor(
}
}
fun updateSavedItemLabels(savedItemID: String, labels: List<SavedItemLabel>) {
fun updateSavedItemLabels(savedItemId: String, labels: List<SavedItemLabel>) {
viewModelScope.launch {
val result = libraryRepository.setSavedItemLabels(
itemId = savedItemID, labels = labels
itemId = savedItemId, labels = labels
)
snackbarMessage = if (result) {
applicationContext.getString(R.string.library_view_model_snackbar_success)
@ -352,7 +352,6 @@ class LibraryViewModel @Inject constructor(
currentItem.value?.let { itemID ->
return (uiState.value as LibraryUiState.Success).items.first { it.savedItem.savedItemId == itemID }
}
return null
}
@ -360,7 +359,7 @@ class LibraryViewModel @Inject constructor(
var query =
"${appliedFilterLiveData.value?.queryString} ${appliedSortFilterLiveData.value?.queryString}"
activeLabelsLiveData.value?.let {
activeLabels.value.let {
if (it.isNotEmpty()) {
query += " label:"
query += it.joinToString { label -> label.name }

View File

@ -8,5 +8,5 @@ interface SavedItemViewModel {
val actionsMenuItemLiveData: MutableLiveData<SavedItemWithLabelsAndHighlights?>
get() = MutableLiveData<SavedItemWithLabelsAndHighlights?>(null)
fun handleSavedItemAction(itemID: String, action: SavedItemAction)
fun handleSavedItemAction(itemId: String, action: SavedItemAction)
}

View File

@ -153,23 +153,23 @@ class SearchViewModel @Inject constructor(
isRefreshing.postValue(false)
}
override fun handleSavedItemAction(itemID: String, action: SavedItemAction) {
override fun handleSavedItemAction(itemId: String, action: SavedItemAction) {
when (action) {
SavedItemAction.Delete -> {
viewModelScope.launch {
dataService.deleteSavedItem(itemID)
dataService.deleteSavedItem(itemId)
}
}
SavedItemAction.Archive -> {
viewModelScope.launch {
dataService.archiveSavedItem(itemID)
dataService.archiveSavedItem(itemId)
}
}
SavedItemAction.Unarchive -> {
viewModelScope.launch {
dataService.unarchiveSavedItem(itemID)
dataService.unarchiveSavedItem(itemId)
}
}

View File

@ -39,6 +39,7 @@ import app.omnivore.omnivore.feature.auth.LoginViewModel
import app.omnivore.omnivore.feature.auth.WelcomeScreen
import app.omnivore.omnivore.feature.components.LabelsViewModel
import app.omnivore.omnivore.feature.editinfo.EditInfoViewModel
import app.omnivore.omnivore.feature.following.FollowingScreen
import app.omnivore.omnivore.feature.library.LibraryView
import app.omnivore.omnivore.feature.library.SearchView
import app.omnivore.omnivore.feature.library.SearchViewModel
@ -134,7 +135,7 @@ fun PrimaryNavigator(
}
composable(Routes.Following.route) {
LibraryView(
FollowingScreen(
navController = navController,
labelsViewModel = labelsViewModel,
saveViewModel = saveViewModel,

View File

@ -1,6 +1,6 @@
#Wed Feb 14 01:21:10 GMT 2024
#Wed Apr 17 23:59:51 CEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists