feature: support filtering in Model Management screen
This commit is contained in:
parent
d97e28a6d8
commit
dd0367b970
|
|
@ -228,6 +228,8 @@ fun AppContent(
|
|||
val isMultiSelectionMode by modelsManagementViewModel.isMultiSelectionMode.collectAsState()
|
||||
val selectedModels by modelsManagementViewModel.selectedModels.collectAsState()
|
||||
val showSortMenu by modelsManagementViewModel.showSortMenu.collectAsState()
|
||||
val activeFilters by modelsManagementViewModel.activeFilters.collectAsState()
|
||||
val showFilterMenu by modelsManagementViewModel.showFilterMenu.collectAsState()
|
||||
val showImportModelMenu by modelsManagementViewModel.showImportModelMenu.collectAsState()
|
||||
|
||||
// Create file launcher for importing local models
|
||||
|
|
@ -246,7 +248,12 @@ fun AppContent(
|
|||
}
|
||||
),
|
||||
filtering = BottomBarConfig.ModelsManagement.FilteringConfig(
|
||||
onClick = { /* TODO: implement filtering */ },
|
||||
isActive = activeFilters.any { it.value },
|
||||
filters = activeFilters,
|
||||
onToggleFilter = modelsManagementViewModel::toggleFilter,
|
||||
onClearFilters = modelsManagementViewModel::clearFilters,
|
||||
isMenuVisible = showFilterMenu,
|
||||
toggleMenu = modelsManagementViewModel::toggleFilterMenu
|
||||
),
|
||||
selection = BottomBarConfig.ModelsManagement.SelectionConfig(
|
||||
isActive = isMultiSelectionMode,
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ import androidx.compose.material.icons.filled.Check
|
|||
import androidx.compose.material.icons.filled.ClearAll
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DeleteSweep
|
||||
import androidx.compose.material.icons.filled.FilterAlt
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.SearchOff
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material.icons.outlined.DeleteSweep
|
||||
import androidx.compose.material.icons.outlined.FilterAlt
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Checkbox
|
||||
|
|
@ -101,7 +101,12 @@ sealed class BottomBarConfig {
|
|||
)
|
||||
|
||||
data class FilteringConfig(
|
||||
val onClick: () -> Unit
|
||||
val isActive: Boolean,
|
||||
val filters: Map<ModelFilter, Boolean>,
|
||||
val onToggleFilter: (ModelFilter, Boolean) -> Unit,
|
||||
val onClearFilters: () -> Unit,
|
||||
val isMenuVisible: Boolean,
|
||||
val toggleMenu: (Boolean) -> Unit
|
||||
)
|
||||
|
||||
data class SelectionConfig(
|
||||
|
|
@ -271,20 +276,6 @@ fun ModelsManagementBottomBar(
|
|||
actions = {
|
||||
if (selection.isActive) {
|
||||
/* Multi-selection mode actions */
|
||||
IconButton(onClick = { selection.toggleAllSelection(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = "Select all"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { selection.toggleAllSelection(false) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ClearAll,
|
||||
contentDescription = "Deselect all"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = selection.deleteSelected,
|
||||
enabled = selection.selectedModels.isNotEmpty()
|
||||
|
|
@ -299,9 +290,30 @@ fun ModelsManagementBottomBar(
|
|||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { selection.toggleAllSelection(false) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ClearAll,
|
||||
contentDescription = "Deselect all"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { selection.toggleAllSelection(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SelectAll,
|
||||
contentDescription = "Select all"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
/* Default mode actions */
|
||||
|
||||
// Multi-selection action
|
||||
IconButton(onClick = { selection.toggleMode(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.DeleteSweep,
|
||||
contentDescription = "Delete models"
|
||||
)
|
||||
}
|
||||
|
||||
// Sorting action
|
||||
IconButton(onClick = { sorting.toggleMenu(true) }) {
|
||||
Icon(
|
||||
|
|
@ -339,20 +351,47 @@ fun ModelsManagementBottomBar(
|
|||
}
|
||||
|
||||
// Filtering action
|
||||
IconButton(
|
||||
onClick = filtering.onClick
|
||||
) {
|
||||
IconButton(onClick = { filtering.toggleMenu(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FilterAlt,
|
||||
imageVector =
|
||||
if (filtering.isActive) Icons.Default.FilterAlt
|
||||
else Icons.Outlined.FilterAlt,
|
||||
contentDescription = "Filter models"
|
||||
)
|
||||
}
|
||||
|
||||
// Selection action
|
||||
IconButton(onClick = { selection.toggleMode(true) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DeleteSweep,
|
||||
contentDescription = "Delete models"
|
||||
// Filter dropdown menu
|
||||
DropdownMenu(
|
||||
expanded = filtering.isMenuVisible,
|
||||
onDismissRequest = { filtering.toggleMenu(false) }
|
||||
) {
|
||||
Text(
|
||||
text = "Filter by",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
filtering.filters.forEach { (filter, isEnabled) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(filter.displayName) },
|
||||
leadingIcon = {
|
||||
Checkbox(
|
||||
checked = isEnabled,
|
||||
onCheckedChange = null
|
||||
)
|
||||
},
|
||||
onClick = { filtering.onToggleFilter(filter, !isEnabled) }
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text("Clear filters") },
|
||||
onClick = {
|
||||
filtering.onClearFilters()
|
||||
filtering.toggleMenu(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ fun ModelsManagementScreen(
|
|||
viewModel: ModelsManagementViewModel,
|
||||
) {
|
||||
// ViewModel states
|
||||
val sortedModels by viewModel.sortedModels.collectAsState()
|
||||
val filteredModels by viewModel.filteredModels.collectAsState()
|
||||
val managementState by viewModel.managementState.collectAsState()
|
||||
|
||||
// Selection state from ViewModel
|
||||
|
|
@ -81,7 +81,7 @@ fun ModelsManagementScreen(
|
|||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(items = sortedModels, key = { it.id }) { model ->
|
||||
items(items = filteredModels, key = { it.id }) { model ->
|
||||
val isSelected = if (isMultiSelectionMode) selectedModels.contains(model.id) else null
|
||||
|
||||
ModelCardFullExpandable(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class ModelSelectionViewModel @Inject constructor(
|
|||
_showSortMenu.value = visible
|
||||
}
|
||||
|
||||
// UI state: filters
|
||||
// UI state: filter menu
|
||||
private val _activeFilters = MutableStateFlow<Map<ModelFilter, Boolean>>(
|
||||
ModelFilter.ALL_FILTERS.associateWith { false }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import android.net.Uri
|
|||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.llama.revamp.data.model.ModelFilter
|
||||
import com.example.llama.revamp.data.model.ModelInfo
|
||||
import com.example.llama.revamp.data.model.ModelSortOrder
|
||||
import com.example.llama.revamp.data.model.filterBy
|
||||
import com.example.llama.revamp.data.model.sortByOrder
|
||||
import com.example.llama.revamp.data.repository.InsufficientStorageException
|
||||
import com.example.llama.revamp.data.repository.ModelRepository
|
||||
import com.example.llama.revamp.util.getFileNameFromUri
|
||||
|
|
@ -17,15 +20,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileNotFoundException
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.set
|
||||
|
||||
@HiltViewModel
|
||||
class ModelsManagementViewModel @Inject constructor(
|
||||
|
|
@ -33,16 +36,9 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
private val modelRepository: ModelRepository
|
||||
) : ViewModel() {
|
||||
|
||||
// Data: available models
|
||||
private val _availableModels: StateFlow<List<ModelInfo>> = modelRepository.getModels()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT_MS),
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
private val _sortedModels = MutableStateFlow<List<ModelInfo>>(emptyList())
|
||||
val sortedModels: StateFlow<List<ModelInfo>> = _sortedModels.asStateFlow()
|
||||
// Data: models
|
||||
private val _filteredModels = MutableStateFlow<List<ModelInfo>>(emptyList())
|
||||
val filteredModels: StateFlow<List<ModelInfo>> = _filteredModels.asStateFlow()
|
||||
|
||||
// UI state: multi-selection mode
|
||||
private val _isMultiSelectionMode = MutableStateFlow(false)
|
||||
|
|
@ -61,7 +57,7 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
|
||||
fun toggleModelSelectionById(modelId: String) {
|
||||
val current = _selectedModels.value.toMutableMap()
|
||||
val model = _sortedModels.value.find { it.id == modelId }
|
||||
val model = _filteredModels.value.find { it.id == modelId }
|
||||
|
||||
if (model != null) {
|
||||
if (current.containsKey(modelId)) {
|
||||
|
|
@ -75,7 +71,7 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
|
||||
fun toggleAllSelection(selectAll: Boolean) {
|
||||
if (selectAll) {
|
||||
_selectedModels.value = _sortedModels.value.associateBy { it.id }
|
||||
_selectedModels.value = _filteredModels.value.associateBy { it.id }
|
||||
} else {
|
||||
_selectedModels.value = emptyMap()
|
||||
}
|
||||
|
|
@ -96,6 +92,33 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
_showSortMenu.value = show
|
||||
}
|
||||
|
||||
// UI state: filters
|
||||
private val _activeFilters = MutableStateFlow<Map<ModelFilter, Boolean>>(
|
||||
ModelFilter.ALL_FILTERS.associateWith { false }
|
||||
)
|
||||
val activeFilters: StateFlow<Map<ModelFilter, Boolean>> = _activeFilters.asStateFlow()
|
||||
|
||||
fun toggleFilter(filter: ModelFilter, enabled: Boolean) {
|
||||
_activeFilters.update { current ->
|
||||
current.toMutableMap().apply {
|
||||
this[filter] = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearFilters() {
|
||||
_activeFilters.update { current ->
|
||||
current.mapValues { false }
|
||||
}
|
||||
}
|
||||
|
||||
private val _showFilterMenu = MutableStateFlow(false)
|
||||
val showFilterMenu: StateFlow<Boolean> = _showFilterMenu.asStateFlow()
|
||||
|
||||
fun toggleFilterMenu(visible: Boolean) {
|
||||
_showFilterMenu.value = visible
|
||||
}
|
||||
|
||||
// UI state: import menu
|
||||
private val _showImportModelMenu = MutableStateFlow(false)
|
||||
val showImportModelMenu: StateFlow<Boolean> = _showImportModelMenu.asStateFlow()
|
||||
|
|
@ -106,20 +129,18 @@ class ModelsManagementViewModel @Inject constructor(
|
|||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
combine(_availableModels, _sortOrder, ::sortModels)
|
||||
.collect { _sortedModels.value = it }
|
||||
combine(
|
||||
modelRepository.getModels(),
|
||||
_activeFilters,
|
||||
_sortOrder,
|
||||
) { models, filters, sortOrder ->
|
||||
models.filterBy(filters).sortByOrder(sortOrder)
|
||||
}.collectLatest {
|
||||
_filteredModels.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortModels(models: List<ModelInfo>, order: ModelSortOrder) =
|
||||
when (order) {
|
||||
ModelSortOrder.NAME_ASC -> models.sortedBy { it.name }
|
||||
ModelSortOrder.NAME_DESC -> models.sortedByDescending { it.name }
|
||||
ModelSortOrder.SIZE_ASC -> models.sortedBy { it.sizeInBytes }
|
||||
ModelSortOrder.SIZE_DESC -> models.sortedByDescending { it.sizeInBytes }
|
||||
ModelSortOrder.LAST_USED -> models.sortedByDescending { it.dateLastUsed ?: 0 }
|
||||
}
|
||||
|
||||
// Internal state
|
||||
private val _managementState = MutableStateFlow<ModelManagementState>(ModelManagementState.Idle)
|
||||
val managementState: StateFlow<ModelManagementState> = _managementState.asStateFlow()
|
||||
|
|
|
|||
Loading…
Reference in New Issue