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 isMultiSelectionMode by modelsManagementViewModel.isMultiSelectionMode.collectAsState()
|
||||||
val selectedModels by modelsManagementViewModel.selectedModels.collectAsState()
|
val selectedModels by modelsManagementViewModel.selectedModels.collectAsState()
|
||||||
val showSortMenu by modelsManagementViewModel.showSortMenu.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()
|
val showImportModelMenu by modelsManagementViewModel.showImportModelMenu.collectAsState()
|
||||||
|
|
||||||
// Create file launcher for importing local models
|
// Create file launcher for importing local models
|
||||||
|
|
@ -246,7 +248,12 @@ fun AppContent(
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
filtering = BottomBarConfig.ModelsManagement.FilteringConfig(
|
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(
|
selection = BottomBarConfig.ModelsManagement.SelectionConfig(
|
||||||
isActive = isMultiSelectionMode,
|
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.ClearAll
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
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.FilterAlt
|
||||||
import androidx.compose.material.icons.filled.FolderOpen
|
import androidx.compose.material.icons.filled.FolderOpen
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.SearchOff
|
import androidx.compose.material.icons.filled.SearchOff
|
||||||
import androidx.compose.material.icons.filled.SelectAll
|
import androidx.compose.material.icons.filled.SelectAll
|
||||||
|
import androidx.compose.material.icons.outlined.DeleteSweep
|
||||||
import androidx.compose.material.icons.outlined.FilterAlt
|
import androidx.compose.material.icons.outlined.FilterAlt
|
||||||
import androidx.compose.material3.BottomAppBar
|
import androidx.compose.material3.BottomAppBar
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
|
|
@ -101,7 +101,12 @@ sealed class BottomBarConfig {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class FilteringConfig(
|
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(
|
data class SelectionConfig(
|
||||||
|
|
@ -271,20 +276,6 @@ fun ModelsManagementBottomBar(
|
||||||
actions = {
|
actions = {
|
||||||
if (selection.isActive) {
|
if (selection.isActive) {
|
||||||
/* Multi-selection mode actions */
|
/* 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(
|
IconButton(
|
||||||
onClick = selection.deleteSelected,
|
onClick = selection.deleteSelected,
|
||||||
enabled = selection.selectedModels.isNotEmpty()
|
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 {
|
} else {
|
||||||
/* Default mode actions */
|
/* Default mode actions */
|
||||||
|
|
||||||
|
// Multi-selection action
|
||||||
|
IconButton(onClick = { selection.toggleMode(true) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.DeleteSweep,
|
||||||
|
contentDescription = "Delete models"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Sorting action
|
// Sorting action
|
||||||
IconButton(onClick = { sorting.toggleMenu(true) }) {
|
IconButton(onClick = { sorting.toggleMenu(true) }) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -339,20 +351,47 @@ fun ModelsManagementBottomBar(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtering action
|
// Filtering action
|
||||||
IconButton(
|
IconButton(onClick = { filtering.toggleMenu(true) }) {
|
||||||
onClick = filtering.onClick
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.FilterAlt,
|
imageVector =
|
||||||
|
if (filtering.isActive) Icons.Default.FilterAlt
|
||||||
|
else Icons.Outlined.FilterAlt,
|
||||||
contentDescription = "Filter models"
|
contentDescription = "Filter models"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selection action
|
// Filter dropdown menu
|
||||||
IconButton(onClick = { selection.toggleMode(true) }) {
|
DropdownMenu(
|
||||||
Icon(
|
expanded = filtering.isMenuVisible,
|
||||||
imageVector = Icons.Default.DeleteSweep,
|
onDismissRequest = { filtering.toggleMenu(false) }
|
||||||
contentDescription = "Delete models"
|
) {
|
||||||
|
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: ModelsManagementViewModel,
|
||||||
) {
|
) {
|
||||||
// ViewModel states
|
// ViewModel states
|
||||||
val sortedModels by viewModel.sortedModels.collectAsState()
|
val filteredModels by viewModel.filteredModels.collectAsState()
|
||||||
val managementState by viewModel.managementState.collectAsState()
|
val managementState by viewModel.managementState.collectAsState()
|
||||||
|
|
||||||
// Selection state from ViewModel
|
// Selection state from ViewModel
|
||||||
|
|
@ -81,7 +81,7 @@ fun ModelsManagementScreen(
|
||||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.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
|
val isSelected = if (isMultiSelectionMode) selectedModels.contains(model.id) else null
|
||||||
|
|
||||||
ModelCardFullExpandable(
|
ModelCardFullExpandable(
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class ModelSelectionViewModel @Inject constructor(
|
||||||
_showSortMenu.value = visible
|
_showSortMenu.value = visible
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI state: filters
|
// UI state: filter menu
|
||||||
private val _activeFilters = MutableStateFlow<Map<ModelFilter, Boolean>>(
|
private val _activeFilters = MutableStateFlow<Map<ModelFilter, Boolean>>(
|
||||||
ModelFilter.ALL_FILTERS.associateWith { false }
|
ModelFilter.ALL_FILTERS.associateWith { false }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.ModelInfo
|
||||||
import com.example.llama.revamp.data.model.ModelSortOrder
|
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.InsufficientStorageException
|
||||||
import com.example.llama.revamp.data.repository.ModelRepository
|
import com.example.llama.revamp.data.repository.ModelRepository
|
||||||
import com.example.llama.revamp.util.getFileNameFromUri
|
import com.example.llama.revamp.util.getFileNameFromUri
|
||||||
|
|
@ -17,15 +20,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.set
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ModelsManagementViewModel @Inject constructor(
|
class ModelsManagementViewModel @Inject constructor(
|
||||||
|
|
@ -33,16 +36,9 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
private val modelRepository: ModelRepository
|
private val modelRepository: ModelRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// Data: available models
|
// Data: models
|
||||||
private val _availableModels: StateFlow<List<ModelInfo>> = modelRepository.getModels()
|
private val _filteredModels = MutableStateFlow<List<ModelInfo>>(emptyList())
|
||||||
.stateIn(
|
val filteredModels: StateFlow<List<ModelInfo>> = _filteredModels.asStateFlow()
|
||||||
scope = viewModelScope,
|
|
||||||
started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT_MS),
|
|
||||||
initialValue = emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _sortedModels = MutableStateFlow<List<ModelInfo>>(emptyList())
|
|
||||||
val sortedModels: StateFlow<List<ModelInfo>> = _sortedModels.asStateFlow()
|
|
||||||
|
|
||||||
// UI state: multi-selection mode
|
// UI state: multi-selection mode
|
||||||
private val _isMultiSelectionMode = MutableStateFlow(false)
|
private val _isMultiSelectionMode = MutableStateFlow(false)
|
||||||
|
|
@ -61,7 +57,7 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
|
|
||||||
fun toggleModelSelectionById(modelId: String) {
|
fun toggleModelSelectionById(modelId: String) {
|
||||||
val current = _selectedModels.value.toMutableMap()
|
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 (model != null) {
|
||||||
if (current.containsKey(modelId)) {
|
if (current.containsKey(modelId)) {
|
||||||
|
|
@ -75,7 +71,7 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
|
|
||||||
fun toggleAllSelection(selectAll: Boolean) {
|
fun toggleAllSelection(selectAll: Boolean) {
|
||||||
if (selectAll) {
|
if (selectAll) {
|
||||||
_selectedModels.value = _sortedModels.value.associateBy { it.id }
|
_selectedModels.value = _filteredModels.value.associateBy { it.id }
|
||||||
} else {
|
} else {
|
||||||
_selectedModels.value = emptyMap()
|
_selectedModels.value = emptyMap()
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +92,33 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
_showSortMenu.value = show
|
_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
|
// UI state: import menu
|
||||||
private val _showImportModelMenu = MutableStateFlow(false)
|
private val _showImportModelMenu = MutableStateFlow(false)
|
||||||
val showImportModelMenu: StateFlow<Boolean> = _showImportModelMenu.asStateFlow()
|
val showImportModelMenu: StateFlow<Boolean> = _showImportModelMenu.asStateFlow()
|
||||||
|
|
@ -106,20 +129,18 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(_availableModels, _sortOrder, ::sortModels)
|
combine(
|
||||||
.collect { _sortedModels.value = it }
|
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
|
// Internal state
|
||||||
private val _managementState = MutableStateFlow<ModelManagementState>(ModelManagementState.Idle)
|
private val _managementState = MutableStateFlow<ModelManagementState>(ModelManagementState.Idle)
|
||||||
val managementState: StateFlow<ModelManagementState> = _managementState.asStateFlow()
|
val managementState: StateFlow<ModelManagementState> = _managementState.asStateFlow()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue