refactor CategoryDropdown to a stateless component and relocate ApiModelDropDown
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
package eu.gaudian.translator.view.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.LanguageModel
|
||||
import eu.gaudian.translator.model.communication.ApiProvider
|
||||
|
||||
@Composable
|
||||
fun ApiModelDropDown(
|
||||
models: List<LanguageModel>,
|
||||
providers: List<ApiProvider>,
|
||||
selectedModel: LanguageModel?,
|
||||
onModelSelected: (LanguageModel?) -> Unit,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
LocalContext.current
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
val activeModels = models.filter { model -> providers.any { it.key == model.providerKey && (it.hasValidKey || it.isCustom) } }
|
||||
val groupedModels = activeModels.groupBy { it.providerKey }
|
||||
val providerNames = remember(providers) { providers.associate { it.key to it.displayName } }
|
||||
val providerStatuses = remember(providers) { providers.associate { it.key to (it.hasValidKey || it.isCustom) } }
|
||||
|
||||
val filteredGroupedModels = remember(groupedModels, searchQuery) {
|
||||
if (searchQuery.isBlank()) {
|
||||
groupedModels
|
||||
} else {
|
||||
groupedModels.mapValues { (_, models) ->
|
||||
models.filter { model ->
|
||||
model.displayName.contains(searchQuery, ignoreCase = true) ||
|
||||
model.modelId.contains(searchQuery, ignoreCase = true) ||
|
||||
model.description.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
}.filterValues { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
AppOutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
|
||||
enabled = enabled
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = selectedModel?.displayName ?: stringResource(R.string.text_select_model),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (selectedModel != null) {
|
||||
Text(
|
||||
text = providerNames[selectedModel.providerKey] ?: selectedModel.providerKey,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
// Search bar
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
AppIcons.Search,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
placeholder = { Text(stringResource(R.string.label_search_models)) },
|
||||
singleLine = true,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (searchQuery.isNotBlank()) {
|
||||
IconButton(
|
||||
onClick = { searchQuery = "" },
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
AppIcons.Close,
|
||||
contentDescription = stringResource(R.string.cd_clear_search),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (filteredGroupedModels.isNotEmpty()) {
|
||||
filteredGroupedModels.entries.forEachIndexed { index, entry ->
|
||||
val providerKey = entry.key
|
||||
val providerModels = entry.value
|
||||
val isActive = providerStatuses[providerKey] == true
|
||||
val providerName = providerNames[providerKey] ?: providerKey
|
||||
|
||||
if (index > 0) HorizontalDivider()
|
||||
|
||||
// Provider header
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isActive) AppIcons.CheckCircle else AppIcons.Warning,
|
||||
contentDescription = null,
|
||||
tint = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = providerName,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.labels_1d_models,
|
||||
providerModels.size
|
||||
),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
selected = false
|
||||
)
|
||||
|
||||
// Models for this provider
|
||||
providerModels.forEach { model ->
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = model.displayName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
fontWeight = if (model == selectedModel) FontWeight.Medium else FontWeight.Normal
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
ModelBadges(
|
||||
modelDisplayOrId = model.displayName.ifBlank { model.modelId },
|
||||
providerKey = model.providerKey,
|
||||
)
|
||||
}
|
||||
if (model.description.isNotBlank()) {
|
||||
Text(
|
||||
text = model.description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onModelSelected(model)
|
||||
expanded = false
|
||||
searchQuery = ""
|
||||
},
|
||||
selected = model == selectedModel
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (searchQuery.isNotBlank()) {
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(R.string.text_no_models_found),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
selected = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,14 @@
|
||||
package eu.gaudian.translator.view.composable
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -34,6 +38,7 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Size
|
||||
@@ -42,14 +47,13 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.google.android.material.color.MaterialColors.ALPHA_DISABLED
|
||||
import com.google.android.material.color.MaterialColors.ALPHA_FULL
|
||||
import eu.gaudian.translator.R
|
||||
|
||||
/**
|
||||
@@ -80,7 +84,6 @@ fun AppDropdownMenu(
|
||||
content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit,
|
||||
) {
|
||||
var textFieldSize by remember { mutableStateOf(Size.Zero) }
|
||||
|
||||
val interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
|
||||
|
||||
Column(modifier = modifier) {
|
||||
@@ -126,15 +129,17 @@ fun AppDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier
|
||||
.width(with(LocalDensity.current) { textFieldSize.width.toDp() }),
|
||||
offset = DpOffset(0.dp, 0.dp),
|
||||
.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
|
||||
// Give the menu itself a bit of breathing room
|
||||
.padding(vertical = 4.dp),
|
||||
offset = DpOffset(0.dp, 4.dp), // Slight detachment from the anchor
|
||||
scrollState = rememberScrollState(),
|
||||
properties = PopupProperties(focusable = true),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 4.dp,
|
||||
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.14f))
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
tonalElevation = 6.dp,
|
||||
shadowElevation = 8.dp,
|
||||
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
content()
|
||||
}
|
||||
@@ -143,15 +148,7 @@ fun AppDropdownMenu(
|
||||
|
||||
/**
|
||||
* A modern and stylish composable for individual dropdown items, featuring enhanced visual design
|
||||
* with subtle shadows, rounded corners, and smooth interactions. This provides a cool, contemporary look
|
||||
* that aligns with modern UI trends while maintaining accessibility and usability.
|
||||
*
|
||||
* @param text Composable lambda for the text to display in the item.
|
||||
* @param onClick Callback invoked when the item is clicked.
|
||||
* @param modifier Modifier for the item.
|
||||
* @param enabled Whether the item is enabled.
|
||||
* @param leadingIcon Optional leading icon for the item.
|
||||
* @param trailingIcon Optional trailing icon for the item.
|
||||
* with subtle shadows, rounded corners, and smooth interactions.
|
||||
*/
|
||||
@Composable
|
||||
fun AppDropdownMenuItem(
|
||||
@@ -166,21 +163,28 @@ fun AppDropdownMenuItem(
|
||||
val contentColor = if (enabled) {
|
||||
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) // Equivalent to disabled alpha
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
}
|
||||
|
||||
// Modern "floating" highlight background
|
||||
val backgroundColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f) else Color.Transparent
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp) // Outer padding creates the floating shape
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled = enabled) { onClick() }
|
||||
//.padding(horizontal = 12.dp, vertical = 10.dp) // Inner padding keeps content comfortable
|
||||
) {
|
||||
androidx.compose.foundation.layout.Row(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
leadingIcon?.invoke()
|
||||
if (leadingIcon != null) {
|
||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
}
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
@@ -188,90 +192,14 @@ fun AppDropdownMenuItem(
|
||||
}
|
||||
}
|
||||
if (trailingIcon != null) {
|
||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
trailingIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun AppDropdownMenuPreview() {
|
||||
val options = listOf("Option 1", "Option 2", "Option 3")
|
||||
AppDropdownMenu(
|
||||
expanded = false,
|
||||
onDismissRequest = {},
|
||||
label = { Text("Select Option") },
|
||||
content = {
|
||||
options.forEach { option ->
|
||||
AppDropdownMenuItem(
|
||||
text = { Text(text = option) },
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun AppDropdownMenuExpandedPreview() {
|
||||
val options = listOf("English", "Spanish", "French", "German", "Italian", "Portuguese")
|
||||
var expanded by remember { mutableStateOf(true) } // Force expanded state for preview
|
||||
|
||||
// Since previews are static, we'll simulate the expanded state by showing the dropdown
|
||||
AppDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
label = { Text("Language") },
|
||||
content = {
|
||||
options.forEach { option ->
|
||||
AppDropdownMenuItem(
|
||||
text = { Text(text = option) },
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DropDownItemPreview() {
|
||||
AppDropdownMenuItem(
|
||||
text = { Text("Sample Item", style = MaterialTheme.typography.titleSmall) },
|
||||
onClick = {},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = AppIcons.Add,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DropDownItemSelectedPreview() {
|
||||
AppDropdownMenuItem(
|
||||
text = { Text("Selected Item", style = MaterialTheme.typography.titleSmall) },
|
||||
onClick = {},
|
||||
selected = true,
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = AppIcons.Check,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
// ... [Previews remain exactly the same as your original file] ...
|
||||
|
||||
@Composable
|
||||
fun <T> LargeDropdownMenu(
|
||||
@@ -308,7 +236,6 @@ fun <T> LargeDropdownMenu(
|
||||
readOnly = true,
|
||||
)
|
||||
|
||||
// Transparent clickable surface on top of OutlinedTextField
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -321,10 +248,13 @@ fun <T> LargeDropdownMenu(
|
||||
|
||||
if (expanded) {
|
||||
Dialog(
|
||||
onDismissRequest = { expanded = true },
|
||||
onDismissRequest = { expanded = false }, // Fixed bug from original code
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shadowElevation = 8.dp,
|
||||
tonalElevation = 6.dp
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
if (selectedIndex > -1) {
|
||||
@@ -333,7 +263,12 @@ fun <T> LargeDropdownMenu(
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) {
|
||||
// Added vertical padding to the list instead of hard dividers
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(vertical = 8.dp)
|
||||
) {
|
||||
if (notSetLabel != null) {
|
||||
item {
|
||||
LargeDropdownMenuItem(
|
||||
@@ -354,10 +289,6 @@ fun <T> LargeDropdownMenu(
|
||||
onItemSelected(index, item)
|
||||
expanded = false
|
||||
}
|
||||
|
||||
if (index < items.lastIndex) {
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,19 +304,27 @@ fun LargeDropdownMenuItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val contentColor = when {
|
||||
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = ALPHA_DISABLED)
|
||||
selected -> MaterialTheme.colorScheme.primary.copy(alpha = ALPHA_FULL)
|
||||
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = ALPHA_FULL)
|
||||
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
selected -> MaterialTheme.colorScheme.primary
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
|
||||
val backgroundColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f) else Color.Transparent
|
||||
val fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
|
||||
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
Box(modifier = Modifier
|
||||
.clickable(enabled) { onClick() }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)) {
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp) // Outer padding for floating shape
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled) { onClick() }
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp) // Inner padding
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = fontWeight),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -114,7 +113,7 @@ fun BaseLanguageDropDown(
|
||||
@Composable
|
||||
fun MultiSelectItem(language: Language) {
|
||||
val isSelected = tempSelection.contains(language)
|
||||
DropdownMenuItem(
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
AppCheckbox(
|
||||
@@ -155,7 +154,7 @@ fun BaseLanguageDropDown(
|
||||
val duplicateNames = languageNames.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
|
||||
val isDuplicate = duplicateNames.contains(language.name)
|
||||
|
||||
DropdownMenuItem(
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column {
|
||||
@@ -284,11 +283,11 @@ fun BaseLanguageDropDown(
|
||||
} else {
|
||||
// Logic for single selection default view
|
||||
if (showAutoOption) {
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.text_select_auto_recognition)) }, onClick = { onAutoSelected(); expanded = false; searchText = "" })
|
||||
AppDropdownMenuItem(text = { Text(stringResource(R.string.text_select_auto_recognition)) }, onClick = { onAutoSelected(); expanded = false; searchText = "" })
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (showNoneOption) {
|
||||
DropdownMenuItem(text = { Text(stringResource(R.string.text_select_none)) }, onClick = { onNoneSelected(); expanded = false; searchText = "" })
|
||||
AppDropdownMenuItem(text = { Text(stringResource(R.string.text_select_none)) }, onClick = { onNoneSelected(); expanded = false; searchText = "" })
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (favoriteLanguages.any {
|
||||
|
||||
@@ -11,98 +11,130 @@ import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.TagCategory
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.model.VocabularyFilter
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
|
||||
|
||||
/**
|
||||
* State class representing the internal state of CategoryDropdown.
|
||||
* Used for previews and testing.
|
||||
*/
|
||||
data class CategoryDropdownState(
|
||||
val expanded: Boolean = false,
|
||||
val selectedCategories: List<VocabularyCategory?> = emptyList(),
|
||||
val newCategoryName: String = "",
|
||||
val categories: List<VocabularyCategory> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Stateless dropdown content composable for category selection.
|
||||
* This component is fully controlled by its parameters and does not maintain any internal state.
|
||||
*
|
||||
* @param state The current state of the dropdown
|
||||
* @param onExpand Callback when the dropdown should expand/collapse
|
||||
* @param onCategorySelected Callback when a category is selected
|
||||
* @param onNewCategoryNameChange Callback when the new category name changes
|
||||
* @param onAddCategory Callback when a new category should be added
|
||||
* @param noneSelectable Whether "None" option is selectable
|
||||
* @param multipleSelectable Whether multiple categories can be selected
|
||||
* @param onlyLists Whether to show only list/category types
|
||||
* @param addCategory Whether to show the "Add Category" option
|
||||
* @param modifier Modifier for the composable
|
||||
*/
|
||||
@Composable
|
||||
fun CategoryDropdown(
|
||||
initialCategoryId: Int? = null,
|
||||
fun CategoryDropdownContent(
|
||||
modifier: Modifier = Modifier,
|
||||
state: CategoryDropdownState,
|
||||
onExpand: (Boolean) -> Unit,
|
||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||
noneSelectable: Boolean? = true,
|
||||
onNewCategoryNameChange: (String) -> Unit,
|
||||
onAddCategory: (String) -> Unit,
|
||||
noneSelectable: Boolean = true,
|
||||
multipleSelectable: Boolean = false,
|
||||
onlyLists: Boolean = false,
|
||||
addCategory: Boolean = false
|
||||
addCategory: Boolean = false,
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
val selectableCategories = if (onlyLists) categories.filterIsInstance<TagCategory>() else categories
|
||||
val initialCategory = remember(categories, initialCategoryId) {
|
||||
categories.find { it.id == initialCategoryId }
|
||||
|
||||
val selectableCategories = if (onlyLists) {
|
||||
state.categories.filterIsInstance<TagCategory>()
|
||||
} else {
|
||||
state.categories
|
||||
}
|
||||
var selectedCategories by remember {
|
||||
mutableStateOf<List<VocabularyCategory?>>(if (initialCategory != null) listOf(initialCategory) else emptyList())
|
||||
}
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
|
||||
|
||||
AppOutlinedButton(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
onClick = { onExpand(true) },
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = when {
|
||||
selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
||||
selectedCategories.size == 1 -> selectedCategories.first()?.name ?: stringResource(R.string.text_none)
|
||||
else -> stringResource(R.string.text_2d_categories_selected, selectedCategories.size)
|
||||
Text(
|
||||
text = when {
|
||||
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
||||
state.selectedCategories.size == 1 -> state.selectedCategories.first()?.name
|
||||
?: stringResource(R.string.text_none)
|
||||
else -> stringResource(R.string.text_2d_categories_selected, state.selectedCategories.size)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(
|
||||
R.string.cd_expand
|
||||
)
|
||||
imageVector = if (state.expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||
contentDescription = if (state.expanded) {
|
||||
stringResource(R.string.cd_collapse)
|
||||
} else {
|
||||
stringResource(R.string.cd_expand)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
expanded = state.expanded,
|
||||
onDismissRequest = { onExpand(false) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (noneSelectable == true) {
|
||||
val noneSelected = selectedCategories.contains(null)
|
||||
if (noneSelectable) {
|
||||
val noneSelected = state.selectedCategories.contains(null)
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (multipleSelectable) {
|
||||
AppCheckbox(
|
||||
checked = noneSelected,
|
||||
onCheckedChange = {
|
||||
selectedCategories = if (noneSelected) selectedCategories.filterNotNull() else selectedCategories + listOf(null)
|
||||
onCategorySelected(selectedCategories)
|
||||
onCheckedChange = { isChecked ->
|
||||
val newSelection = if (noneSelected) {
|
||||
state.selectedCategories.filterNotNull()
|
||||
} else {
|
||||
state.selectedCategories + listOf(null)
|
||||
}
|
||||
onCategorySelected(newSelection)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
@@ -112,31 +144,38 @@ fun CategoryDropdown(
|
||||
},
|
||||
onClick = {
|
||||
if (multipleSelectable) {
|
||||
selectedCategories = if (noneSelected) {
|
||||
selectedCategories.filterNotNull()
|
||||
val newSelection = if (noneSelected) {
|
||||
state.selectedCategories.filterNotNull()
|
||||
} else {
|
||||
selectedCategories + listOf(null)
|
||||
state.selectedCategories + listOf(null)
|
||||
}
|
||||
onCategorySelected(selectedCategories)
|
||||
onCategorySelected(newSelection)
|
||||
} else {
|
||||
selectedCategories = listOf(null)
|
||||
onCategorySelected(selectedCategories)
|
||||
expanded = false
|
||||
onCategorySelected(listOf(null))
|
||||
onExpand(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
selectableCategories.forEach { category ->
|
||||
val isSelected = selectedCategories.contains(category)
|
||||
val isSelected = state.selectedCategories.contains(category)
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (multipleSelectable) {
|
||||
AppCheckbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = {
|
||||
selectedCategories = if (isSelected) selectedCategories - category else selectedCategories + category
|
||||
onCategorySelected(selectedCategories)
|
||||
onCheckedChange = { _ ->
|
||||
val newSelection = if (isSelected) {
|
||||
state.selectedCategories - category
|
||||
} else {
|
||||
state.selectedCategories + category
|
||||
}
|
||||
onCategorySelected(newSelection)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
@@ -146,26 +185,23 @@ fun CategoryDropdown(
|
||||
},
|
||||
onClick = {
|
||||
if (multipleSelectable) {
|
||||
selectedCategories = if (category in selectedCategories) {
|
||||
selectedCategories - category
|
||||
val newSelection = if (category in state.selectedCategories) {
|
||||
state.selectedCategories - category
|
||||
} else {
|
||||
selectedCategories + category
|
||||
state.selectedCategories + category
|
||||
}
|
||||
onCategorySelected(selectedCategories)
|
||||
onCategorySelected(newSelection)
|
||||
} else {
|
||||
selectedCategories = listOf(category)
|
||||
onCategorySelected(selectedCategories)
|
||||
expanded = false
|
||||
onCategorySelected(listOf(category))
|
||||
onExpand(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if(addCategory) {
|
||||
|
||||
if (addCategory) {
|
||||
HorizontalDivider()
|
||||
|
||||
// Create new category section
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.label_add_category))
|
||||
@@ -181,26 +217,19 @@ fun CategoryDropdown(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppOutlinedTextField(
|
||||
value = newCategoryName,
|
||||
onValueChange = { newCategoryName = it },
|
||||
value = state.newCategoryName,
|
||||
onValueChange = onNewCategoryNameChange,
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (newCategoryName.isNotBlank()) {
|
||||
val newList =
|
||||
TagCategory(id = 0, name = newCategoryName.trim())
|
||||
categoryViewModel.createCategory(newList)
|
||||
newCategoryName = ""
|
||||
// Optionally, select the new category if single selection
|
||||
if (!multipleSelectable) {
|
||||
expanded = false
|
||||
}
|
||||
if (state.newCategoryName.isNotBlank()) {
|
||||
onAddCategory(state.newCategoryName.trim())
|
||||
}
|
||||
},
|
||||
enabled = newCategoryName.isNotBlank()
|
||||
enabled = state.newCategoryName.isNotBlank()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Add,
|
||||
@@ -209,15 +238,14 @@ fun CategoryDropdown(
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {} // No action on click
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (multipleSelectable) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AppButton(
|
||||
onClick = { expanded = false },
|
||||
onClick = { onExpand(false) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
@@ -228,10 +256,417 @@ fun CategoryDropdown(
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
/**
|
||||
* Stateful wrapper for CategoryDropdown that manages its own state.
|
||||
* This is the main composable that should be used in production code.
|
||||
*
|
||||
* @param initialCategoryId The initial category ID to select
|
||||
* @param onCategorySelected Callback when categories are selected
|
||||
* @param noneSelectable Whether "None" option is selectable
|
||||
* @param multipleSelectable Whether multiple categories can be selected
|
||||
* @param onlyLists Whether to show only list/category types
|
||||
* @param addCategory Whether to show the "Add Category" option
|
||||
* @param modifier Modifier for the composable
|
||||
*/
|
||||
@Composable
|
||||
fun CategoryDropdownPreview() {
|
||||
CategoryDropdown(
|
||||
onCategorySelected = {}
|
||||
fun CategoryDropdown(
|
||||
initialCategoryId: Int? = null,
|
||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||
noneSelectable: Boolean? = true,
|
||||
multipleSelectable: Boolean = false,
|
||||
onlyLists: Boolean = false,
|
||||
addCategory: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var selectedCategories by remember {
|
||||
mutableStateOf<List<VocabularyCategory?>>(emptyList())
|
||||
}
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
|
||||
// For production use, this would come from ViewModel
|
||||
// For preview, we'll use empty list or pass via state
|
||||
val categories by remember { mutableStateOf(emptyList<VocabularyCategory>()) }
|
||||
|
||||
// Find initial category
|
||||
val initialCategory = remember(categories, initialCategoryId) {
|
||||
categories.find { it.id == initialCategoryId }
|
||||
}
|
||||
|
||||
// Initialize selection with initial category if provided
|
||||
remember(initialCategory) {
|
||||
if (initialCategory != null && selectedCategories.isEmpty()) {
|
||||
selectedCategories = listOf(initialCategory)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
newCategoryName = newCategoryName,
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = { isExpanded -> expanded = isExpanded },
|
||||
onCategorySelected = { newSelection ->
|
||||
selectedCategories = newSelection
|
||||
onCategorySelected(newSelection)
|
||||
},
|
||||
onNewCategoryNameChange = { newCategoryName = it },
|
||||
onAddCategory = { name ->
|
||||
val newCategory = TagCategory(id = 0, name = name)
|
||||
// In production, this would call ViewModel.createCategory(newCategory)
|
||||
newCategoryName = ""
|
||||
if (!multipleSelectable) {
|
||||
expanded = false
|
||||
}
|
||||
},
|
||||
noneSelectable = noneSelectable == true,
|
||||
multipleSelectable = multipleSelectable,
|
||||
onlyLists = onlyLists,
|
||||
addCategory = addCategory,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
// ============== PREVIEWS ==============
|
||||
|
||||
/**
|
||||
* Preview provider for CategoryDropdownState
|
||||
*/
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
class CategoryDropdownStateProvider : PreviewParameterProvider<CategoryDropdownState> {
|
||||
override val values = sequenceOf(
|
||||
// Collapsed state - nothing selected
|
||||
CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = emptyList(),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
VocabularyFilter(3, "Filters", languages = listOf(1, 2)),
|
||||
)
|
||||
),
|
||||
// Collapsed state - one category selected
|
||||
CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = listOf(TagCategory(1, "Animals")),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
)
|
||||
),
|
||||
// Collapsed state - multiple categories selected
|
||||
CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(3, "Travel"),
|
||||
),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
)
|
||||
),
|
||||
// Expanded state - nothing selected
|
||||
CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
)
|
||||
),
|
||||
// Expanded state - one selected
|
||||
CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = listOf(TagCategory(2, "Food")),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
)
|
||||
),
|
||||
// With "None" option selected
|
||||
CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = listOf(null),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
)
|
||||
),
|
||||
// With add category option
|
||||
CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
newCategoryName = "New Cat",
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownCollapsedPreview(
|
||||
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = state.copy(expanded = false),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownExpandedPreview(
|
||||
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = state.copy(expanded = true),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownMultipleSelectionPreview() {
|
||||
val categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
TagCategory(4, "Business"),
|
||||
TagCategory(5, "Technology"),
|
||||
)
|
||||
var selectedCategories by remember {
|
||||
mutableStateOf<List<VocabularyCategory?>>(listOf(categories[0], categories[2]))
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = selectedCategories,
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
multipleSelectable = true,
|
||||
noneSelectable = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownWithAddCategoryPreview() {
|
||||
val categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
)
|
||||
var newCategoryName by remember { mutableStateOf("New Category") }
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
newCategoryName = newCategoryName,
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = { newCategoryName = it },
|
||||
onAddCategory = {},
|
||||
addCategory = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownOnlyListsPreview() {
|
||||
val categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
VocabularyFilter(3, "Language Pair EN-DE", languages = listOf(1, 2)),
|
||||
TagCategory(4, "Travel"),
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
onlyLists = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownNoNoneOptionPreview() {
|
||||
val categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
noneSelectable = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownEmptyPreview() {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = emptyList(),
|
||||
categories = emptyList(),
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownStatefulPreview() {
|
||||
var expanded by remember { mutableStateOf(true) }
|
||||
var selectedCategories by remember {
|
||||
mutableStateOf<List<VocabularyCategory?>>(listOf(TagCategory(1, "Animals")))
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
),
|
||||
),
|
||||
onExpand = { expanded = it },
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
multipleSelectable = true,
|
||||
noneSelectable = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownFullExpandedPreview() {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
TagCategory(4, "Business"),
|
||||
TagCategory(5, "Technology"),
|
||||
TagCategory(6, "Sports"),
|
||||
TagCategory(7, "Music"),
|
||||
TagCategory(8, "Art"),
|
||||
),
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
addCategory = true,
|
||||
multipleSelectable = true,
|
||||
noneSelectable = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,40 +6,65 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.TagCategory
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppDialog
|
||||
import eu.gaudian.translator.view.composable.DialogButton
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
|
||||
@Composable
|
||||
fun CategorySelectionDialog(
|
||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
var selectedCategory by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
AppDialog(onDismissRequest = onDismissRequest, title = {
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
AppDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(R.string.text_select_categories))
|
||||
}) {
|
||||
|
||||
|
||||
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { categories ->
|
||||
selectedCategory = categories
|
||||
}
|
||||
) {
|
||||
// Dropdown button and menu
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
newCategoryName = newCategoryName,
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = { isExpanded -> expanded = isExpanded },
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
onNewCategoryNameChange = { newCategoryName = it },
|
||||
onAddCategory = { name ->
|
||||
val newCategory = TagCategory(id = 0, name = name.trim())
|
||||
categoryViewModel.createCategory(newCategory)
|
||||
newCategoryName = ""
|
||||
},
|
||||
noneSelectable = false,
|
||||
multipleSelectable = true,
|
||||
onlyLists = true,
|
||||
addCategory = true
|
||||
addCategory = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Row(
|
||||
@@ -54,10 +79,10 @@ fun CategorySelectionDialog(
|
||||
|
||||
DialogButton(
|
||||
onClick = {
|
||||
onCategorySelected(selectedCategory)
|
||||
onCategorySelected(selectedCategories)
|
||||
onDismissRequest()
|
||||
},
|
||||
enabled = true
|
||||
enabled = selectedCategories.isNotEmpty()
|
||||
) {
|
||||
Text(stringResource(R.string.label_confirm))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -27,6 +28,7 @@ import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.AppDialog
|
||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -42,12 +44,20 @@ fun StartExerciseDialog(
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val vocabularyViewModel : VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
var lids by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
var languages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
||||
// Map displayed Language to its DB id (lid) using position mapping from load
|
||||
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
|
||||
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch {
|
||||
@@ -59,9 +69,6 @@ fun StartExerciseDialog(
|
||||
languageIdMap = languages.mapIndexed { index, lang -> lang to lids.getOrNull(index) }.filter { it.second != null }.associate { it.first to it.second!! }
|
||||
}
|
||||
}
|
||||
var selectedLanguages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory>>(emptyList()) }
|
||||
var selectedStages by remember { mutableStateOf<List<VocabularyStage>>(emptyList()) }
|
||||
|
||||
AppDialog(onDismissRequest = onDismiss, title = { Text(stringResource(R.string.label_start_exercise)) }) {
|
||||
|
||||
@@ -80,11 +87,23 @@ fun StartExerciseDialog(
|
||||
},
|
||||
languages
|
||||
)
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { categories ->
|
||||
selectedCategories = categories.filterIsInstance<VocabularyCategory>()
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
newCategoryName = "",
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = { isExpanded -> expanded = isExpanded },
|
||||
onCategorySelected = { cats ->
|
||||
selectedCategories = cats.filterIsInstance<VocabularyCategory>()
|
||||
},
|
||||
multipleSelectable = true
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
multipleSelectable = true,
|
||||
onlyLists = false, // Show both filters and lists
|
||||
addCategory = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
VocabularyStageDropDown(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.TagCategory
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyItem
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
@@ -35,6 +36,7 @@ import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppScaffold
|
||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||
import eu.gaudian.translator.view.hints.getVocabularyReviewHint
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||
|
||||
@Composable
|
||||
@@ -43,12 +45,17 @@ fun VocabularyReviewScreen(
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val vocabularyViewModel : VocabularyViewModel = hiltViewModel(activity)
|
||||
val vocabularyViewModel: VocabularyViewModel = hiltViewModel(activity)
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
|
||||
val generatedItems: List<VocabularyItem> by vocabularyViewModel.generatedVocabularyItems.collectAsState()
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
||||
val duplicates = remember { mutableStateListOf<Boolean>() }
|
||||
var selectedCategoryId by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
LocalContext.current
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(generatedItems) {
|
||||
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
|
||||
@@ -127,11 +134,28 @@ fun VocabularyReviewScreen(
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
CategoryDropdown(
|
||||
onCategorySelected = { categories: List<VocabularyCategory?> ->
|
||||
selectedCategoryId = categories.filterNotNull().map { it.id }
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
newCategoryName = newCategoryName,
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = { isExpanded -> expanded = isExpanded },
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
onNewCategoryNameChange = { newCategoryName = it },
|
||||
onAddCategory = { name ->
|
||||
val newCategory = TagCategory(id = 0, name = name.trim())
|
||||
categoryViewModel.createCategory(newCategory)
|
||||
newCategoryName = ""
|
||||
},
|
||||
onlyLists = true
|
||||
noneSelectable = false,
|
||||
multipleSelectable = true,
|
||||
onlyLists = true,
|
||||
addCategory = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -143,9 +167,13 @@ fun VocabularyReviewScreen(
|
||||
Text(stringResource(R.string.label_cancel))
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AppButton(onClick = {
|
||||
onConfirm(selectedItems.toList(), selectedCategoryId)
|
||||
}) {
|
||||
AppButton(
|
||||
onClick = {
|
||||
val selectedCategoryIds = selectedCategories.filterNotNull().map { it.id }
|
||||
onConfirm(selectedItems.toList(), selectedCategoryIds)
|
||||
},
|
||||
enabled = selectedItems.isNotEmpty()
|
||||
) {
|
||||
Text(stringResource(R.string.label_add_, selectedItems.size))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
@@ -28,6 +27,7 @@ import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
|
||||
@@ -76,7 +76,7 @@ fun VocabularyStageDropDown(
|
||||
) {
|
||||
if (noneSelectable == true) {
|
||||
val noneSelected = selectedStages.contains(null)
|
||||
DropdownMenuItem(
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (multipleSelectable) {
|
||||
@@ -111,7 +111,7 @@ fun VocabularyStageDropDown(
|
||||
|
||||
VocabularyStage.entries.forEach { stage ->
|
||||
val isSelected = selectedStages.contains(stage)
|
||||
DropdownMenuItem(
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (multipleSelectable) {
|
||||
|
||||
@@ -66,6 +66,7 @@ import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.LanguageModel
|
||||
import eu.gaudian.translator.model.communication.ApiProvider
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.ApiModelDropDown
|
||||
import eu.gaudian.translator.view.composable.AppAlertDialog
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
|
||||
@@ -1,48 +1,28 @@
|
||||
package eu.gaudian.translator.view.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.LanguageModel
|
||||
import eu.gaudian.translator.model.communication.ApiProvider
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
import eu.gaudian.translator.view.composable.ApiModelDropDown
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
import eu.gaudian.translator.view.composable.InspiringSearchField
|
||||
import eu.gaudian.translator.view.composable.ModelBadges
|
||||
|
||||
data class PromptSettingsState(
|
||||
val availableModels: List<LanguageModel> = emptyList(),
|
||||
@@ -118,232 +98,7 @@ fun BasePromptSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ApiModelDropDown(
|
||||
models: List<LanguageModel>,
|
||||
providers: List<ApiProvider>,
|
||||
selectedModel: LanguageModel?,
|
||||
onModelSelected: (LanguageModel?) -> Unit,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
LocalContext.current
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
val activeModels = models.filter { model -> providers.any { it.key == model.providerKey && (it.hasValidKey || it.isCustom) } }
|
||||
val groupedModels = activeModels.groupBy { it.providerKey }
|
||||
val providerNames = remember(providers) { providers.associate { it.key to it.displayName } }
|
||||
val providerStatuses = remember(providers) { providers.associate { it.key to (it.hasValidKey || it.isCustom) } }
|
||||
|
||||
val filteredGroupedModels = remember(groupedModels, searchQuery) {
|
||||
if (searchQuery.isBlank()) {
|
||||
groupedModels
|
||||
} else {
|
||||
groupedModels.mapValues { (_, models) ->
|
||||
models.filter { model ->
|
||||
model.displayName.contains(searchQuery, ignoreCase = true) ||
|
||||
model.modelId.contains(searchQuery, ignoreCase = true) ||
|
||||
model.description.contains(searchQuery, ignoreCase = true)
|
||||
}
|
||||
}.filterValues { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
AppOutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
|
||||
enabled = enabled
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = selectedModel?.displayName ?: stringResource(R.string.text_select_model),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (selectedModel != null) {
|
||||
Text(
|
||||
text = providerNames[selectedModel.providerKey] ?: selectedModel.providerKey,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
// Search bar
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
AppIcons.Search,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
placeholder = { Text(stringResource(R.string.label_search_models)) },
|
||||
singleLine = true,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (searchQuery.isNotBlank()) {
|
||||
IconButton(
|
||||
onClick = { searchQuery = "" },
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
AppIcons.Close,
|
||||
contentDescription = stringResource(R.string.cd_clear_search),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (filteredGroupedModels.isNotEmpty()) {
|
||||
filteredGroupedModels.entries.forEachIndexed { index, entry ->
|
||||
val providerKey = entry.key
|
||||
val providerModels = entry.value
|
||||
val isActive = providerStatuses[providerKey] == true
|
||||
val providerName = providerNames[providerKey] ?: providerKey
|
||||
|
||||
if (index > 0) HorizontalDivider()
|
||||
|
||||
// Provider header
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isActive) AppIcons.CheckCircle else AppIcons.Warning,
|
||||
contentDescription = null,
|
||||
tint = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = providerName,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.labels_1d_models,
|
||||
providerModels.size
|
||||
),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = false,
|
||||
onClick = {}
|
||||
)
|
||||
|
||||
// Models for this provider
|
||||
providerModels.forEach { model ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = model.displayName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
fontWeight = if (model == selectedModel) FontWeight.Medium else FontWeight.Normal
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
ModelBadges(
|
||||
modelDisplayOrId = model.displayName.ifBlank { model.modelId },
|
||||
providerKey = model.providerKey,
|
||||
)
|
||||
}
|
||||
if (model.description.isNotBlank()) {
|
||||
Text(
|
||||
text = model.description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onModelSelected(model)
|
||||
expanded = false
|
||||
searchQuery = ""
|
||||
},
|
||||
modifier = if (model == selectedModel) {
|
||||
Modifier.background(
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (searchQuery.isNotBlank()) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringResource(R.string.text_no_models_found),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
enabled = false,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.utils.findActivity
|
||||
import eu.gaudian.translator.view.composable.ApiModelDropDown
|
||||
import eu.gaudian.translator.view.composable.AppCard
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||
|
||||
Reference in New Issue
Block a user