UI: add model loading in progress view; polish the empty model info view
This commit is contained in:
parent
a4881cb87b
commit
f23b74c730
|
|
@ -293,6 +293,10 @@ fun AppContent(
|
||||||
|
|
||||||
ModelScreenUiMode.MANAGING ->
|
ModelScreenUiMode.MANAGING ->
|
||||||
BottomBarConfig.Models.Management(
|
BottomBarConfig.Models.Management(
|
||||||
|
isDeletionEnabled = filteredModels?.isNotEmpty() == true,
|
||||||
|
onToggleDeleting = {
|
||||||
|
modelsViewModel.toggleMode(ModelScreenUiMode.DELETING)
|
||||||
|
},
|
||||||
sorting = BottomBarConfig.Models.Management.SortingConfig(
|
sorting = BottomBarConfig.Models.Management.SortingConfig(
|
||||||
currentOrder = sortOrder,
|
currentOrder = sortOrder,
|
||||||
isMenuVisible = showSortMenu,
|
isMenuVisible = showSortMenu,
|
||||||
|
|
@ -322,9 +326,6 @@ fun AppContent(
|
||||||
modelsManagementViewModel.toggleImportMenu(false)
|
modelsManagementViewModel.toggleImportMenu(false)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
onToggleDeleting = {
|
|
||||||
modelsViewModel.toggleMode(ModelScreenUiMode.DELETING)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ModelScreenUiMode.DELETING ->
|
ModelScreenUiMode.DELETING ->
|
||||||
|
|
@ -334,10 +335,12 @@ fun AppContent(
|
||||||
},
|
},
|
||||||
selectedModels = selectedModelsToDelete,
|
selectedModels = selectedModelsToDelete,
|
||||||
selectAllFilteredModels = {
|
selectAllFilteredModels = {
|
||||||
modelsManagementViewModel.selectAllFilteredModelsToDelete(filteredModels)
|
filteredModels?.let {
|
||||||
|
modelsManagementViewModel.selectModelsToDelete(it)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clearAllSelectedModels = {
|
clearAllSelectedModels = {
|
||||||
modelsManagementViewModel.clearAllSelectedModelsToDelete()
|
modelsManagementViewModel.clearSelectedModelsToDelete()
|
||||||
},
|
},
|
||||||
deleteSelected = {
|
deleteSelected = {
|
||||||
selectedModelsToDelete.let {
|
selectedModelsToDelete.let {
|
||||||
|
|
|
||||||
|
|
@ -31,24 +31,44 @@ fun InfoView(
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
action: InfoAction? = null
|
action: InfoAction? = null
|
||||||
|
) {
|
||||||
|
InfoView(
|
||||||
|
modifier = modifier,
|
||||||
|
title = title,
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
message = message,
|
||||||
|
action = action
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoView(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String,
|
||||||
|
icon: @Composable () -> Unit,
|
||||||
|
message: String? = null,
|
||||||
|
action: InfoAction? = null
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.padding(16.dp),
|
modifier = modifier.padding(16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
icon()
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(64.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.headlineSmall
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
||||||
message?.let {
|
message?.let {
|
||||||
|
|
|
||||||
|
|
@ -125,10 +125,11 @@ fun AppScaffold(
|
||||||
|
|
||||||
is BottomBarConfig.Models.Management -> {
|
is BottomBarConfig.Models.Management -> {
|
||||||
ModelsManagementBottomBar(
|
ModelsManagementBottomBar(
|
||||||
|
isDeletionEnabled = config.isDeletionEnabled,
|
||||||
|
onToggleDeleting = config.onToggleDeleting,
|
||||||
sortingConfig = config.sorting,
|
sortingConfig = config.sorting,
|
||||||
filteringConfig = config.filtering,
|
filteringConfig = config.filtering,
|
||||||
importingConfig = config.importing,
|
importingConfig = config.importing
|
||||||
onToggleDeleting = config.onToggleDeleting
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,11 @@ sealed class BottomBarConfig {
|
||||||
) : BottomBarConfig()
|
) : BottomBarConfig()
|
||||||
|
|
||||||
data class Management(
|
data class Management(
|
||||||
|
val isDeletionEnabled: Boolean,
|
||||||
|
val onToggleDeleting: () -> Unit,
|
||||||
val sorting: SortingConfig,
|
val sorting: SortingConfig,
|
||||||
val filtering: FilteringConfig,
|
val filtering: FilteringConfig,
|
||||||
val importing: ImportConfig,
|
val importing: ImportConfig,
|
||||||
val onToggleDeleting: () -> Unit,
|
|
||||||
) : BottomBarConfig() {
|
) : BottomBarConfig() {
|
||||||
data class SortingConfig(
|
data class SortingConfig(
|
||||||
val currentOrder: ModelSortOrder,
|
val currentOrder: ModelSortOrder,
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,10 @@ fun ModelsBrowsingBottomBar(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (filteringConfig.isActive) Icons.Default.FilterAlt
|
if (filteringConfig.isActive) Icons.Default.FilterAlt
|
||||||
else Icons.Outlined.FilterAlt,
|
else Icons.Outlined.FilterAlt,
|
||||||
contentDescription = "Filter models"
|
contentDescription = "Filter models",
|
||||||
|
tint =
|
||||||
|
if (filteringConfig.isActive) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,19 +31,19 @@ import com.example.llama.data.model.ModelSortOrder
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ModelsManagementBottomBar(
|
fun ModelsManagementBottomBar(
|
||||||
|
isDeletionEnabled: Boolean,
|
||||||
|
onToggleDeleting: () -> Unit,
|
||||||
sortingConfig: BottomBarConfig.Models.Management.SortingConfig,
|
sortingConfig: BottomBarConfig.Models.Management.SortingConfig,
|
||||||
filteringConfig: BottomBarConfig.Models.Management.FilteringConfig,
|
filteringConfig: BottomBarConfig.Models.Management.FilteringConfig,
|
||||||
importingConfig: BottomBarConfig.Models.Management.ImportConfig,
|
importingConfig: BottomBarConfig.Models.Management.ImportConfig,
|
||||||
onToggleDeleting: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
BottomAppBar(
|
BottomAppBar(
|
||||||
actions = {
|
actions = {
|
||||||
// Batch-deletion action
|
// Batch-deletion action
|
||||||
IconButton(onClick = onToggleDeleting) {
|
IconButton(enabled = isDeletionEnabled, onClick = onToggleDeleting) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.DeleteSweep,
|
imageVector = Icons.Outlined.DeleteSweep,
|
||||||
contentDescription = "Delete models"
|
contentDescription = "Delete models",)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorting action
|
// Sorting action
|
||||||
|
|
@ -104,7 +104,10 @@ fun ModelsManagementBottomBar(
|
||||||
imageVector =
|
imageVector =
|
||||||
if (filteringConfig.isActive) Icons.Default.FilterAlt
|
if (filteringConfig.isActive) Icons.Default.FilterAlt
|
||||||
else Icons.Outlined.FilterAlt,
|
else Icons.Outlined.FilterAlt,
|
||||||
contentDescription = "Filter models"
|
contentDescription = "Filter models",
|
||||||
|
tint =
|
||||||
|
if (filteringConfig.isActive) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,15 @@ import com.example.llama.viewmodel.PreselectedModelToRun
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ModelsBrowsingScreen(
|
fun ModelsBrowsingScreen(
|
||||||
filteredModels: List<ModelInfo>,
|
filteredModels: List<ModelInfo>?,
|
||||||
activeFiltersCount: Int,
|
activeFiltersCount: Int,
|
||||||
preselection: PreselectedModelToRun?,
|
preselection: PreselectedModelToRun?,
|
||||||
onManageModelsClicked: () -> Unit,
|
onManageModelsClicked: () -> Unit,
|
||||||
viewModel: ModelsViewModel,
|
viewModel: ModelsViewModel,
|
||||||
) {
|
) {
|
||||||
if (filteredModels.isEmpty()) {
|
if (filteredModels == null) {
|
||||||
|
ModelsLoadingInProgressView()
|
||||||
|
} else if (filteredModels.isEmpty()) {
|
||||||
// Empty model prompt
|
// Empty model prompt
|
||||||
EmptyModelsView(activeFiltersCount, onManageModelsClicked)
|
EmptyModelsView(activeFiltersCount, onManageModelsClicked)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.example.llama.ui.screens
|
package com.example.llama.ui.screens
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.basicMarquee
|
import androidx.compose.foundation.basicMarquee
|
||||||
|
|
@ -18,6 +19,7 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Help
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ContactSupport
|
import androidx.compose.material.icons.automirrored.outlined.ContactSupport
|
||||||
import androidx.compose.material.icons.filled.Attribution
|
import androidx.compose.material.icons.filled.Attribution
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
|
@ -78,13 +80,15 @@ import java.util.Locale
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ModelsManagementAndDeletingScreen(
|
fun ModelsManagementAndDeletingScreen(
|
||||||
filteredModels: List<ModelInfo>,
|
filteredModels: List<ModelInfo>?,
|
||||||
activeFiltersCount: Int,
|
activeFiltersCount: Int,
|
||||||
isDeleting: Boolean,
|
isDeleting: Boolean,
|
||||||
onScaffoldEvent: (ScaffoldEvent) -> Unit,
|
onScaffoldEvent: (ScaffoldEvent) -> Unit,
|
||||||
modelsViewModel: ModelsViewModel,
|
modelsViewModel: ModelsViewModel,
|
||||||
managementViewModel: ModelsManagementViewModel,
|
managementViewModel: ModelsManagementViewModel,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
// Selection state
|
// Selection state
|
||||||
val selectedModels by managementViewModel.selectedModelsToDelete.collectAsState()
|
val selectedModels by managementViewModel.selectedModelsToDelete.collectAsState()
|
||||||
|
|
||||||
|
|
@ -102,18 +106,29 @@ fun ModelsManagementAndDeletingScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
if (filteredModels.isEmpty()) {
|
if (filteredModels == null) {
|
||||||
|
ModelsLoadingInProgressView()
|
||||||
|
} else if (filteredModels.isEmpty()) {
|
||||||
// Import model prompt
|
// Import model prompt
|
||||||
val message = when (activeFiltersCount) {
|
val message = when (activeFiltersCount) {
|
||||||
0 -> "Tap the \"+\" button to import a model!"
|
0 -> "Tap the \"+\" button\n to import a model"
|
||||||
1 -> "No models match the selected filter"
|
1 -> "No models match\n the selected filter"
|
||||||
else -> "No models match the selected filters"
|
else -> "No models match\n the selected filters"
|
||||||
}
|
}
|
||||||
InfoView(
|
InfoView(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(0.8f).align(Alignment.Center),
|
||||||
title = "No Models Available",
|
title = message,
|
||||||
icon = Icons.Default.FolderOpen,
|
icon = Icons.Default.FolderOpen,
|
||||||
message = message,
|
message = "Import a local GGUF model file, or download directly from HuggingFace!",
|
||||||
|
action = InfoAction(
|
||||||
|
label = "Learn more",
|
||||||
|
icon = Icons.AutoMirrored.Default.Help,
|
||||||
|
onAction = {
|
||||||
|
val url = "https://huggingface.co/docs/hub/en/gguf"
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Model cards
|
// Model cards
|
||||||
|
|
@ -178,6 +193,7 @@ fun ModelsManagementAndDeletingScreen(
|
||||||
|
|
||||||
is Importation.Error -> {
|
is Importation.Error -> {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
|
context = context,
|
||||||
title = "Import Failed",
|
title = "Import Failed",
|
||||||
message = state.message,
|
message = state.message,
|
||||||
learnMoreUrl = state.learnMoreUrl,
|
learnMoreUrl = state.learnMoreUrl,
|
||||||
|
|
@ -241,6 +257,7 @@ fun ModelsManagementAndDeletingScreen(
|
||||||
|
|
||||||
is Download.Error -> {
|
is Download.Error -> {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
|
context = context,
|
||||||
title = "Download Failed",
|
title = "Download Failed",
|
||||||
message = state.message,
|
message = state.message,
|
||||||
onDismiss = { managementViewModel.resetManagementState() }
|
onDismiss = { managementViewModel.resetManagementState() }
|
||||||
|
|
@ -267,6 +284,7 @@ fun ModelsManagementAndDeletingScreen(
|
||||||
|
|
||||||
is Deletion.Error -> {
|
is Deletion.Error -> {
|
||||||
ErrorDialog(
|
ErrorDialog(
|
||||||
|
context = context,
|
||||||
title = "Deletion Failed",
|
title = "Deletion Failed",
|
||||||
message = state.message,
|
message = state.message,
|
||||||
onDismiss = { managementViewModel.resetManagementState() }
|
onDismiss = { managementViewModel.resetManagementState() }
|
||||||
|
|
@ -660,13 +678,12 @@ private fun BatchDeleteConfirmationDialog(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ErrorDialog(
|
private fun ErrorDialog(
|
||||||
|
context: Context,
|
||||||
title: String,
|
title: String,
|
||||||
message: String,
|
message: String,
|
||||||
learnMoreUrl: String? = null,
|
learnMoreUrl: String? = null,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val action = learnMoreUrl?.let { url ->
|
val action = learnMoreUrl?.let { url ->
|
||||||
InfoAction(
|
InfoAction(
|
||||||
label = "Learn More",
|
label = "Learn More",
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Warning
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -17,6 +20,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.llama.data.model.ModelInfo
|
import com.example.llama.data.model.ModelInfo
|
||||||
import com.example.llama.ui.components.InfoView
|
import com.example.llama.ui.components.InfoView
|
||||||
import com.example.llama.ui.scaffold.ScaffoldEvent
|
import com.example.llama.ui.scaffold.ScaffoldEvent
|
||||||
|
|
@ -63,7 +67,7 @@ fun ModelsScreen(
|
||||||
modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING)
|
modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING)
|
||||||
}
|
}
|
||||||
ModelScreenUiMode.DELETING -> {
|
ModelScreenUiMode.DELETING -> {
|
||||||
managementViewModel.clearAllSelectedModelsToDelete()
|
managementViewModel.clearSelectedModelsToDelete()
|
||||||
modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING)
|
modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +81,10 @@ fun ModelsScreen(
|
||||||
ModelsBrowsingScreen(
|
ModelsBrowsingScreen(
|
||||||
filteredModels = filteredModels,
|
filteredModels = filteredModels,
|
||||||
preselection = preselection,
|
preselection = preselection,
|
||||||
onManageModelsClicked = { /* TODO-han.yin */ },
|
onManageModelsClicked = {
|
||||||
|
managementViewModel.toggleImportMenu(true)
|
||||||
|
modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING)
|
||||||
|
},
|
||||||
activeFiltersCount = activeFiltersCount,
|
activeFiltersCount = activeFiltersCount,
|
||||||
viewModel = modelsViewModel,
|
viewModel = modelsViewModel,
|
||||||
)
|
)
|
||||||
|
|
@ -112,6 +119,21 @@ fun ModelsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModelsLoadingInProgressView() {
|
||||||
|
InfoView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
title = "Loading...",
|
||||||
|
icon = {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(64.dp),
|
||||||
|
strokeWidth = ProgressIndicatorDefaults.CircularStrokeWidth * 1.5f
|
||||||
|
)
|
||||||
|
},
|
||||||
|
message = "Searching for installed models on your device...",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RamErrorDialog(
|
private fun RamErrorDialog(
|
||||||
ramError: RamWarning,
|
ramError: RamWarning,
|
||||||
|
|
|
||||||
|
|
@ -101,43 +101,45 @@ fun ModelsSearchingScreen(
|
||||||
expanded = true,
|
expanded = true,
|
||||||
onExpandedChange = handleExpanded
|
onExpandedChange = handleExpanded
|
||||||
) {
|
) {
|
||||||
if (queryResults.isEmpty()) {
|
queryResults?.let { results ->
|
||||||
if (searchQuery.isNotBlank()) {
|
if (results.isEmpty()) {
|
||||||
// If no results under current query, show "no results" message
|
if (searchQuery.isNotBlank()) {
|
||||||
EmptySearchResultsView(
|
// If no results under current query, show "no results" message
|
||||||
onClearSearch = {
|
EmptySearchResultsView(
|
||||||
textFieldState.clearText()
|
onClearSearch = {
|
||||||
toggleSearchFocusAndIme(true)
|
textFieldState.clearText()
|
||||||
}
|
toggleSearchFocusAndIme(true)
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp),
|
|
||||||
) {
|
|
||||||
items(items = queryResults, key = { it.id }) { model ->
|
|
||||||
ModelCardFullExpandable(
|
|
||||||
model = model,
|
|
||||||
isSelected = if (model == preselection?.modelInfo) true else null,
|
|
||||||
onSelected = { selected ->
|
|
||||||
if (selected) {
|
|
||||||
toggleSearchFocusAndIme(false)
|
|
||||||
} else {
|
|
||||||
viewModel.resetPreselection()
|
|
||||||
toggleSearchFocusAndIme(true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isExpanded = model == preselection?.modelInfo,
|
|
||||||
onExpanded = { expanded ->
|
|
||||||
viewModel.preselectModel(model, expanded)
|
|
||||||
toggleSearchFocusAndIme(!expanded)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
items(items = results, key = { it.id }) { model ->
|
||||||
|
ModelCardFullExpandable(
|
||||||
|
model = model,
|
||||||
|
isSelected = if (model == preselection?.modelInfo) true else null,
|
||||||
|
onSelected = { selected ->
|
||||||
|
if (selected) {
|
||||||
|
toggleSearchFocusAndIme(false)
|
||||||
|
} else {
|
||||||
|
viewModel.resetPreselection()
|
||||||
|
toggleSearchFocusAndIme(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isExpanded = model == preselection?.modelInfo,
|
||||||
|
onExpanded = { expanded ->
|
||||||
|
viewModel.preselectModel(model, expanded)
|
||||||
|
toggleSearchFocusAndIme(!expanded)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} ?: ModelsLoadingInProgressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,11 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectAllFilteredModelsToDelete(filteredModels: List<ModelInfo>) {
|
fun selectModelsToDelete(models: List<ModelInfo>) {
|
||||||
_selectedModelsToDelete.value = filteredModels.associateBy { it.id }
|
_selectedModelsToDelete.value = models.associateBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearAllSelectedModelsToDelete() {
|
fun clearSelectedModelsToDelete() {
|
||||||
_selectedModelsToDelete.value = emptyMap()
|
_selectedModelsToDelete.value = emptyMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +100,7 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
huggingFaceQueryJob?.let {
|
huggingFaceQueryJob?.let {
|
||||||
if (it.isActive) { it.cancel() }
|
if (it.isActive) { it.cancel() }
|
||||||
}
|
}
|
||||||
clearAllSelectedModelsToDelete()
|
clearSelectedModelsToDelete()
|
||||||
_managementState.value = ModelManagementState.Idle
|
_managementState.value = ModelManagementState.Idle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,7 +288,7 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
_managementState.value = Deletion.Deleting(deleted.toFloat() / total, modelsToDelete)
|
_managementState.value = Deletion.Deleting(deleted.toFloat() / total, modelsToDelete)
|
||||||
}
|
}
|
||||||
_managementState.value = Deletion.Success(modelsToDelete.values.toList())
|
_managementState.value = Deletion.Success(modelsToDelete.values.toList())
|
||||||
clearAllSelectedModelsToDelete()
|
clearSelectedModelsToDelete()
|
||||||
|
|
||||||
// Reset state after a delay
|
// Reset state after a delay
|
||||||
delay(DELETE_SUCCESS_RESET_TIMEOUT_MS)
|
delay(DELETE_SUCCESS_RESET_TIMEOUT_MS)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ class ModelsViewModel @Inject constructor(
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// UI state: model management mode
|
// UI state: model management mode
|
||||||
|
private val _allModels = MutableStateFlow<List<ModelInfo>?>(null)
|
||||||
|
val allModels = _allModels.asStateFlow()
|
||||||
|
|
||||||
private val _modelScreenUiMode = MutableStateFlow(ModelScreenUiMode.BROWSING)
|
private val _modelScreenUiMode = MutableStateFlow(ModelScreenUiMode.BROWSING)
|
||||||
val modelScreenUiMode = _modelScreenUiMode.asStateFlow()
|
val modelScreenUiMode = _modelScreenUiMode.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -128,11 +131,11 @@ class ModelsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data: filtered & sorted models
|
// Data: filtered & sorted models
|
||||||
private val _filteredModels = MutableStateFlow<List<ModelInfo>>(emptyList())
|
private val _filteredModels = MutableStateFlow<List<ModelInfo>?>(null)
|
||||||
val filteredModels = _filteredModels.asStateFlow()
|
val filteredModels = _filteredModels.asStateFlow()
|
||||||
|
|
||||||
// Data: queried models
|
// Data: queried models
|
||||||
private val _queryResults = MutableStateFlow<List<ModelInfo>>(emptyList())
|
private val _queryResults = MutableStateFlow<List<ModelInfo>?>(null)
|
||||||
val queryResults = _queryResults.asStateFlow()
|
val queryResults = _queryResults.asStateFlow()
|
||||||
|
|
||||||
// Data: pre-selected model in expansion mode
|
// Data: pre-selected model in expansion mode
|
||||||
|
|
@ -161,29 +164,39 @@ class ModelsViewModel @Inject constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(
|
launch {
|
||||||
modelRepository.getModels(),
|
modelRepository.getModels().collectLatest {
|
||||||
_activeFilters,
|
_allModels.value = it
|
||||||
_sortOrder,
|
}
|
||||||
) { models, filters, sortOrder ->
|
}
|
||||||
models.filterBy(filters).sortByOrder(sortOrder)
|
|
||||||
}.collectLatest {
|
launch {
|
||||||
_filteredModels.value = it
|
combine(
|
||||||
}
|
_allModels,
|
||||||
}
|
_activeFilters,
|
||||||
|
_sortOrder,
|
||||||
viewModelScope.launch {
|
) { models, filters, sortOrder ->
|
||||||
combine(
|
models?.filterBy(filters)?.sortByOrder(sortOrder)
|
||||||
modelRepository.getModels(),
|
}.collectLatest {
|
||||||
snapshotFlow { searchFieldState.text }.debounce(QUERY_DEBOUNCE_TIMEOUT_MS)
|
_filteredModels.value = it
|
||||||
) { models, query ->
|
}
|
||||||
if (query.isBlank()) {
|
}
|
||||||
emptyList()
|
|
||||||
} else {
|
launch {
|
||||||
models.queryBy(query.toString()).sortedBy { it.dateLastUsed ?: it.dateAdded }
|
combine(
|
||||||
|
_allModels,
|
||||||
|
snapshotFlow { searchFieldState.text }.debounce(QUERY_DEBOUNCE_TIMEOUT_MS)
|
||||||
|
) { models, query ->
|
||||||
|
if (query.isBlank()) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
models?.queryBy(query.toString())?.sortedBy {
|
||||||
|
it.dateLastUsed ?: it.dateAdded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.collectLatest {
|
||||||
|
_queryResults.value = it
|
||||||
}
|
}
|
||||||
}.collectLatest {
|
|
||||||
_queryResults.value = it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue