Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ internal interface ItemDao {
itemType: VaultItemType? = null,
): Flow<List<LightweightItemSearchResult>>

@Query("UPDATE item SET pinned = :pinned WHERE id = :id")
suspend fun setPinned(id: ItemId, pinned: Boolean)
@Query("UPDATE item SET pinned = :pinned WHERE id IN (:ids)")
suspend fun setPinned(ids: Collection<ItemId>, pinned: Boolean)

@Query("SELECT i.id, i.name, i.item_type as itemType, i.pinned FROM item i WHERE (:vaultId IS NULL OR vault_id = :vaultId)")
fun observeLiteItems(vaultId: VaultId? = null): Flow<List<LightweightItem>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ internal class ItemRepositoryImpl(
itemDao.searchItem(query, Tag.normalize(query), itemType)
.map { results -> results.map(LightweightItemSearchResult::toDomain) }

override suspend fun setPinned(itemId: ItemId, pinned: Boolean) =
itemDao.setPinned(itemId, pinned)
override suspend fun setPinned(itemIds: Set<ItemId>, pinned: Boolean) =
itemDao.setPinned(itemIds, pinned)

override fun observeAllTags(): Flow<List<Tag>> =
tagDao.observeAllTags().map { it.map(TagEntity::toDomain) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface ItemRepository {
itemType: VaultItemType? = null
): Flow<List<LiteItemSearchResult>>

suspend fun setPinned(itemId: ItemId, pinned: Boolean)
suspend fun setPinned(itemIds: Set<ItemId>, pinned: Boolean)

fun observeAllTags(): Flow<List<Tag>>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,13 @@ class FakeItemRepository(
}
}

override suspend fun setPinned(itemId: ItemId, pinned: Boolean) = Unit
override suspend fun setPinned(itemIds: Set<ItemId>, pinned: Boolean) {
allStores.update { store ->
store.mapValues { (id, item) ->
if (id in itemIds) item.copy(pinned = pinned) else item
}
}
}

override fun observeLiteVaultItems(vaultId: VaultId?): Flow<List<LiteItem>> =
allStores.map { store ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ internal class ViewCreditCardViewModel(

ViewCreditCardUiEvent.OnPinClick -> _itemId.value?.let { id ->
viewModelScope.launch {
itemRepository.setPinned(id, !state.value.pinned)
itemRepository.setPinned(setOf(id), !state.value.pinned)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ internal class ViewLoginViewModel(
ViewLoginUiEvent.OnPinClick -> {
_itemId.value?.let { id ->
viewModelScope.launch {
itemRepository.setPinned(id, !state.value.pinned)
itemRepository.setPinned(setOf(id), !state.value.pinned)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ fun ItemListScreen(
onClearSelection = viewModel::onClearSelection,
onSelectAll = viewModel::onSelectAll,
onDeleteSelectedRequest = viewModel::onDeleteSelectedRequest,
onPinSelectedRequest = viewModel::onPinSelectedRequest,
onDismissDeleteConfirmation = viewModel::onDismissDeleteConfirmation,
onConfirmDeleteSelected = viewModel::onConfirmDeleteSelected,
scrollBehavior = scrollBehavior,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import de.davis.keygo.feature.list_screen.presentation.mapper.toBottomSheetState
import de.davis.keygo.feature.list_screen.presentation.model.Event
import de.davis.keygo.feature.list_screen.presentation.model.FilterAction
import de.davis.keygo.feature.list_screen.presentation.model.FilterBottomSheetState
import de.davis.keygo.feature.list_screen.presentation.model.ItemSelection
import de.davis.keygo.feature.list_screen.presentation.model.ListItemState
import de.davis.keygo.feature.vault.domain.usecase.ObserveVaultsAndSelectionUseCase
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -100,7 +101,7 @@ internal class ItemListViewModel(
filterUseCase(filter, items, scores, tagIds)
}.distinctUntilChanged()

private val selectedItemIds = MutableStateFlow(emptySet<ItemId>())
private val selection = MutableStateFlow(ItemSelection())
private val highlightedId = MutableStateFlow<ItemId?>(null)
private val _isVaultFlowVisible = MutableStateFlow(false)
private val _isDeleteConfirmationVisible = MutableStateFlow(false)
Expand All @@ -122,17 +123,17 @@ internal class ItemListViewModel(
vaultsAndSelection,
filteredItems,
searchResults,
selectedItemIds,
selection,
submittedSearchQuery,
highlightedId,
_isVaultFlowVisible,
_isDeleteConfirmationVisible,
) { vaultsAndSel, items, searchResults, selectedIds, submittedSearchQuery, highlightedId, isVaultFlowVisible, isDeleteConfirmationVisible ->
) { vaultsAndSel, items, searchResults, selection, submittedSearchQuery, highlightedId, isVaultFlowVisible, isDeleteConfirmationVisible ->
ListItemState(
items = items,
searchResults = searchResults,
hasSearchQuery = submittedSearchQuery.isNotBlank(),
selectedItemIds = selectedIds,
selection = selection,
highlightedId = highlightedId,
isVaultFlowVisible = isVaultFlowVisible,
isDeleteConfirmationVisible = isDeleteConfirmationVisible,
Expand Down Expand Up @@ -235,15 +236,25 @@ internal class ItemListViewModel(
fun onSelectAll() {
if (!enableSelection) return

selectedItemIds.update { listItemState.value.items.mapTo(mutableSetOf()) { it.id } }
selection.update { ItemSelection.of(listItemState.value.items) }
}

fun onClearSelection() {
selectedItemIds.update { emptySet() }
selection.update { ItemSelection() }
}

fun onDeleteSelectedRequest() {
if (selectedItemIds.value.isNotEmpty()) _isDeleteConfirmationVisible.update { true }
if (selection.value.isActive) _isDeleteConfirmationVisible.update { true }
}

fun onPinSelectedRequest() {
val current = selection.value
if (!current.isActive) return

val pinned = !current.allPinned
selection.update { it.withAllPinned(pinned) }

viewModelScope.launch { itemRepository.setPinned(current.ids, pinned) }
}

fun onDismissDeleteConfirmation() {
Expand All @@ -253,7 +264,7 @@ internal class ItemListViewModel(
fun onConfirmDeleteSelected() {
_isDeleteConfirmationVisible.update { false }

val deleted = selectedItemIds.getAndUpdate { emptySet() }
val deleted = selection.getAndUpdate { ItemSelection() }.ids
if (deleted.isEmpty()) return

// Read off the list still on screen: after the delete lands the flow has already dropped
Expand All @@ -270,8 +281,8 @@ internal class ItemListViewModel(


fun onItemClick(itemId: ItemId, forceSkipSelection: Boolean = false) {
if (enableSelection && !forceSkipSelection && selectedItemIds.value.isNotEmpty()) {
val isSelected = itemId in selectedItemIds.value
if (enableSelection && !forceSkipSelection && selection.value.isActive) {
val isSelected = itemId in selection.value.ids
updateItemSelectionState(itemId, selected = !isSelected)
} else {
highlightedId.update { itemId }
Expand All @@ -288,9 +299,12 @@ internal class ItemListViewModel(


private fun updateItemSelectionState(id: ItemId, selected: Boolean) {
selectedItemIds.update { currentSelectedIds ->
if (selected) currentSelectedIds + id
else currentSelectedIds - id
// The pinned flag is read off the row being selected: the selection carries it from here
// on, so the top bar knows whether it can offer an unpin without asking the list again.
val pinned = listItemState.value.items.any { it.id == id && it.pinned }
selection.update { currentSelection ->
if (selected) currentSelection.select(id, pinned)
else currentSelection.deselect(id)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ internal fun ItemListContent(
onClearSelection: () -> Unit,
onSelectAll: () -> Unit,
onDeleteSelectedRequest: () -> Unit,
onPinSelectedRequest: () -> Unit,
onDismissDeleteConfirmation: () -> Unit,
onConfirmDeleteSelected: () -> Unit,
onVaultSelectorClick: () -> Unit,
Expand Down Expand Up @@ -127,9 +128,11 @@ internal fun ItemListContent(
SelectionTopBar(
selectedCount = uiState.selectedItemIds.size,
canDelete = enableDeletion,
allPinned = uiState.allSelectedPinned,
onClearSelection = onClearSelection,
onSelectAll = onSelectAll,
onDeleteSelected = onDeleteSelectedRequest,
onPinSelected = onPinSelectedRequest,
)
else
AppBarWithSearch(
Expand Down Expand Up @@ -275,7 +278,6 @@ private fun ItemListContentPreview() {
searchResults = listOf(sampleItem),
hasSearchQuery = false,
highlightedId = null,
selectedItemIds = emptySet(),
)
}
val searchTextFieldState = rememberTextFieldState()
Expand Down Expand Up @@ -314,6 +316,7 @@ private fun ItemListContentPreview() {
onClearSelection = {},
onSelectAll = {},
onDeleteSelectedRequest = {},
onPinSelectedRequest = {},
onDismissDeleteConfirmation = {},
onConfirmDeleteSelected = {},
onVaultSelectorClick = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package de.davis.keygo.feature.list_screen.presentation.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -21,9 +23,11 @@ import de.davis.keygo.feature.list_screen.R
internal fun SelectionTopBar(
selectedCount: Int,
canDelete: Boolean,
allPinned: Boolean,
onClearSelection: () -> Unit,
onSelectAll: () -> Unit,
onDeleteSelected: () -> Unit,
onPinSelected: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
Expand All @@ -47,6 +51,15 @@ internal fun SelectionTopBar(
)
}

IconButton(onClick = onPinSelected) {
Icon(
imageVector = if (allPinned) Icons.Default.PushPin else Icons.Outlined.PushPin,
contentDescription = stringResource(
if (allPinned) R.string.unpin_all else R.string.pin_all,
),
)
}

if (canDelete)
IconButton(onClick = onDeleteSelected) {
Icon(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package de.davis.keygo.feature.list_screen.presentation.model

import androidx.compose.runtime.Immutable
import de.davis.keygo.core.item.domain.alias.ItemId
import de.davis.keygo.core.item.domain.model.lite.LiteItem

@Immutable
internal data class ItemSelection(val pinnedById: Map<ItemId, Boolean> = emptyMap()) {

val ids: Set<ItemId> get() = pinnedById.keys

val isActive: Boolean get() = pinnedById.isNotEmpty()

val allPinned: Boolean get() = isActive && pinnedById.values.all { it }

fun select(itemId: ItemId, pinned: Boolean): ItemSelection =
ItemSelection(pinnedById + (itemId to pinned))

fun deselect(itemId: ItemId): ItemSelection = ItemSelection(pinnedById - itemId)

fun withAllPinned(pinned: Boolean): ItemSelection =
ItemSelection(pinnedById.mapValues { pinned })

companion object {
fun of(items: List<LiteItem>): ItemSelection =
ItemSelection(items.associate { it.id to it.pinned })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ internal data class ListItemState(
val items: List<LiteItem> = emptyList(),
val searchResults: List<LiteItem> = emptyList(),
val hasSearchQuery: Boolean = false,
val selectedItemIds: Set<ItemId> = emptySet(),
val selection: ItemSelection = ItemSelection(),
val highlightedId: ItemId? = null,
val isVaultFlowVisible: Boolean = false,
val isDeleteConfirmationVisible: Boolean = false,
val vaults: List<VaultMetadata> = emptyList(),
val vaultContext: VaultContext = VaultContext.NoSpecific,
) {
val isSelectionActive: Boolean get() = selectedItemIds.isNotEmpty()
val selectedItemIds: Set<ItemId> get() = selection.ids
val isSelectionActive: Boolean get() = selection.isActive
val allSelectedPinned: Boolean get() = selection.allPinned
}
2 changes: 2 additions & 0 deletions feature/list_screen/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<string name="selected_count">%d selected</string>
<string name="clear_selection">Clear selection</string>
<string name="select_all">Select all</string>
<string name="pin_all">Pin all</string>
<string name="unpin_all">Unpin all</string>
<string name="delete">Delete</string>
<string name="cancel">Cancel</string>

Expand Down
Loading
Loading