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
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -34,6 +38,7 @@ import androidx.compose.runtime.mutableIntStateOf
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Size
|
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.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.toSize
|
import androidx.compose.ui.unit.toSize
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.PopupProperties
|
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
|
import eu.gaudian.translator.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +84,6 @@ fun AppDropdownMenu(
|
|||||||
content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit,
|
content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
var textFieldSize by remember { mutableStateOf(Size.Zero) }
|
var textFieldSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
|
||||||
val interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
|
val interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
@@ -126,15 +129,17 @@ fun AppDropdownMenu(
|
|||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(with(LocalDensity.current) { textFieldSize.width.toDp() }),
|
.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
|
||||||
offset = DpOffset(0.dp, 0.dp),
|
// 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(),
|
scrollState = rememberScrollState(),
|
||||||
properties = PopupProperties(focusable = true),
|
properties = PopupProperties(focusable = true),
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
tonalElevation = 0.dp,
|
tonalElevation = 6.dp,
|
||||||
shadowElevation = 4.dp,
|
shadowElevation = 8.dp,
|
||||||
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.14f))
|
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@@ -143,15 +148,7 @@ fun AppDropdownMenu(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A modern and stylish composable for individual dropdown items, featuring enhanced visual design
|
* 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
|
* with subtle shadows, rounded corners, and smooth interactions.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AppDropdownMenuItem(
|
fun AppDropdownMenuItem(
|
||||||
@@ -166,21 +163,28 @@ fun AppDropdownMenuItem(
|
|||||||
val contentColor = if (enabled) {
|
val contentColor = if (enabled) {
|
||||||
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||||
} else {
|
} 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(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 2.dp) // Outer padding creates the floating shape
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(backgroundColor)
|
||||||
.clickable(enabled = enabled) { onClick() }
|
.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(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
leadingIcon?.invoke()
|
leadingIcon?.invoke()
|
||||||
if (leadingIcon != null) {
|
if (leadingIcon != null) {
|
||||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
}
|
}
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||||
@@ -188,90 +192,14 @@ fun AppDropdownMenuItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trailingIcon != null) {
|
if (trailingIcon != null) {
|
||||||
androidx.compose.foundation.layout.Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
trailingIcon()
|
trailingIcon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
// ... [Previews remain exactly the same as your original file] ...
|
||||||
@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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> LargeDropdownMenu(
|
fun <T> LargeDropdownMenu(
|
||||||
@@ -308,7 +236,6 @@ fun <T> LargeDropdownMenu(
|
|||||||
readOnly = true,
|
readOnly = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Transparent clickable surface on top of OutlinedTextField
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -321,10 +248,13 @@ fun <T> LargeDropdownMenu(
|
|||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = { expanded = true },
|
onDismissRequest = { expanded = false }, // Fixed bug from original code
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
shadowElevation = 8.dp,
|
||||||
|
tonalElevation = 6.dp
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
if (selectedIndex > -1) {
|
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) {
|
if (notSetLabel != null) {
|
||||||
item {
|
item {
|
||||||
LargeDropdownMenuItem(
|
LargeDropdownMenuItem(
|
||||||
@@ -354,10 +289,6 @@ fun <T> LargeDropdownMenu(
|
|||||||
onItemSelected(index, item)
|
onItemSelected(index, item)
|
||||||
expanded = false
|
expanded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < items.lastIndex) {
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,19 +304,27 @@ fun LargeDropdownMenuItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val contentColor = when {
|
val contentColor = when {
|
||||||
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = ALPHA_DISABLED)
|
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
selected -> MaterialTheme.colorScheme.primary.copy(alpha = ALPHA_FULL)
|
selected -> MaterialTheme.colorScheme.primary
|
||||||
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = ALPHA_FULL)
|
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) {
|
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||||
Box(modifier = Modifier
|
Box(
|
||||||
.clickable(enabled) { onClick() }
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.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 = 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.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -114,7 +113,7 @@ fun BaseLanguageDropDown(
|
|||||||
@Composable
|
@Composable
|
||||||
fun MultiSelectItem(language: Language) {
|
fun MultiSelectItem(language: Language) {
|
||||||
val isSelected = tempSelection.contains(language)
|
val isSelected = tempSelection.contains(language)
|
||||||
DropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
AppCheckbox(
|
AppCheckbox(
|
||||||
@@ -155,7 +154,7 @@ fun BaseLanguageDropDown(
|
|||||||
val duplicateNames = languageNames.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
|
val duplicateNames = languageNames.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
|
||||||
val isDuplicate = duplicateNames.contains(language.name)
|
val isDuplicate = duplicateNames.contains(language.name)
|
||||||
|
|
||||||
DropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Column {
|
Column {
|
||||||
@@ -284,11 +283,11 @@ fun BaseLanguageDropDown(
|
|||||||
} else {
|
} else {
|
||||||
// Logic for single selection default view
|
// Logic for single selection default view
|
||||||
if (showAutoOption) {
|
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()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
if (showNoneOption) {
|
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()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
if (favoriteLanguages.any {
|
if (favoriteLanguages.any {
|
||||||
|
|||||||
@@ -11,98 +11,130 @@ import androidx.compose.material3.DropdownMenu
|
|||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.TagCategory
|
import eu.gaudian.translator.model.TagCategory
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
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.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||||
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
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
|
@Composable
|
||||||
fun CategoryDropdown(
|
fun CategoryDropdownContent(
|
||||||
initialCategoryId: Int? = null,
|
modifier: Modifier = Modifier,
|
||||||
|
state: CategoryDropdownState,
|
||||||
|
onExpand: (Boolean) -> Unit,
|
||||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||||
noneSelectable: Boolean? = true,
|
onNewCategoryNameChange: (String) -> Unit,
|
||||||
|
onAddCategory: (String) -> Unit,
|
||||||
|
noneSelectable: Boolean = true,
|
||||||
multipleSelectable: Boolean = false,
|
multipleSelectable: Boolean = false,
|
||||||
onlyLists: Boolean = false,
|
onlyLists: Boolean = false,
|
||||||
addCategory: Boolean = false
|
addCategory: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val activity = LocalContext.current.findActivity()
|
val selectableCategories = if (onlyLists) {
|
||||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
state.categories.filterIsInstance<TagCategory>()
|
||||||
var expanded by remember { mutableStateOf(false) }
|
} else {
|
||||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
state.categories
|
||||||
val selectableCategories = if (onlyLists) categories.filterIsInstance<TagCategory>() else categories
|
|
||||||
val initialCategory = remember(categories, initialCategoryId) {
|
|
||||||
categories.find { it.id == initialCategoryId }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
var selectedCategories by remember {
|
|
||||||
mutableStateOf<List<VocabularyCategory?>>(if (initialCategory != null) listOf(initialCategory) else emptyList())
|
|
||||||
}
|
|
||||||
var newCategoryName by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
|
|
||||||
AppOutlinedButton(
|
AppOutlinedButton(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
onClick = { expanded = true },
|
onClick = { onExpand(true) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(text = when {
|
Text(
|
||||||
selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
text = when {
|
||||||
selectedCategories.size == 1 -> selectedCategories.first()?.name ?: stringResource(R.string.text_none)
|
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
||||||
else -> stringResource(R.string.text_2d_categories_selected, selectedCategories.size)
|
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),
|
modifier = Modifier.weight(1f),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
imageVector = if (state.expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||||
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(
|
contentDescription = if (state.expanded) {
|
||||||
R.string.cd_expand
|
stringResource(R.string.cd_collapse)
|
||||||
)
|
} else {
|
||||||
|
stringResource(R.string.cd_expand)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = state.expanded,
|
||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { onExpand(false) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
if (noneSelectable == true) {
|
if (noneSelectable) {
|
||||||
val noneSelected = selectedCategories.contains(null)
|
val noneSelected = state.selectedCategories.contains(null)
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
AppCheckbox(
|
AppCheckbox(
|
||||||
checked = noneSelected,
|
checked = noneSelected,
|
||||||
onCheckedChange = {
|
onCheckedChange = { isChecked ->
|
||||||
selectedCategories = if (noneSelected) selectedCategories.filterNotNull() else selectedCategories + listOf(null)
|
val newSelection = if (noneSelected) {
|
||||||
onCategorySelected(selectedCategories)
|
state.selectedCategories.filterNotNull()
|
||||||
|
} else {
|
||||||
|
state.selectedCategories + listOf(null)
|
||||||
|
}
|
||||||
|
onCategorySelected(newSelection)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
@@ -112,31 +144,38 @@ fun CategoryDropdown(
|
|||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
selectedCategories = if (noneSelected) {
|
val newSelection = if (noneSelected) {
|
||||||
selectedCategories.filterNotNull()
|
state.selectedCategories.filterNotNull()
|
||||||
} else {
|
} else {
|
||||||
selectedCategories + listOf(null)
|
state.selectedCategories + listOf(null)
|
||||||
}
|
}
|
||||||
onCategorySelected(selectedCategories)
|
onCategorySelected(newSelection)
|
||||||
} else {
|
} else {
|
||||||
selectedCategories = listOf(null)
|
onCategorySelected(listOf(null))
|
||||||
onCategorySelected(selectedCategories)
|
onExpand(false)
|
||||||
expanded = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectableCategories.forEach { category ->
|
selectableCategories.forEach { category ->
|
||||||
val isSelected = selectedCategories.contains(category)
|
val isSelected = state.selectedCategories.contains(category)
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
AppCheckbox(
|
AppCheckbox(
|
||||||
checked = isSelected,
|
checked = isSelected,
|
||||||
onCheckedChange = {
|
onCheckedChange = { _ ->
|
||||||
selectedCategories = if (isSelected) selectedCategories - category else selectedCategories + category
|
val newSelection = if (isSelected) {
|
||||||
onCategorySelected(selectedCategories)
|
state.selectedCategories - category
|
||||||
|
} else {
|
||||||
|
state.selectedCategories + category
|
||||||
|
}
|
||||||
|
onCategorySelected(newSelection)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
@@ -146,26 +185,23 @@ fun CategoryDropdown(
|
|||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
selectedCategories = if (category in selectedCategories) {
|
val newSelection = if (category in state.selectedCategories) {
|
||||||
selectedCategories - category
|
state.selectedCategories - category
|
||||||
} else {
|
} else {
|
||||||
selectedCategories + category
|
state.selectedCategories + category
|
||||||
}
|
}
|
||||||
onCategorySelected(selectedCategories)
|
onCategorySelected(newSelection)
|
||||||
} else {
|
} else {
|
||||||
selectedCategories = listOf(category)
|
onCategorySelected(listOf(category))
|
||||||
onCategorySelected(selectedCategories)
|
onExpand(false)
|
||||||
expanded = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(addCategory) {
|
if (addCategory) {
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
// Create new category section
|
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(stringResource(R.string.label_add_category))
|
Text(stringResource(R.string.label_add_category))
|
||||||
@@ -181,26 +217,19 @@ fun CategoryDropdown(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
AppOutlinedTextField(
|
AppOutlinedTextField(
|
||||||
value = newCategoryName,
|
value = state.newCategoryName,
|
||||||
onValueChange = { newCategoryName = it },
|
onValueChange = onNewCategoryNameChange,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (newCategoryName.isNotBlank()) {
|
if (state.newCategoryName.isNotBlank()) {
|
||||||
val newList =
|
onAddCategory(state.newCategoryName.trim())
|
||||||
TagCategory(id = 0, name = newCategoryName.trim())
|
|
||||||
categoryViewModel.createCategory(newList)
|
|
||||||
newCategoryName = ""
|
|
||||||
// Optionally, select the new category if single selection
|
|
||||||
if (!multipleSelectable) {
|
|
||||||
expanded = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = newCategoryName.isNotBlank()
|
enabled = state.newCategoryName.isNotBlank()
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = AppIcons.Add,
|
imageVector = AppIcons.Add,
|
||||||
@@ -209,15 +238,14 @@ fun CategoryDropdown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {} // No action on click
|
onClick = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
AppButton(
|
AppButton(
|
||||||
onClick = { expanded = false },
|
onClick = { onExpand(false) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp)
|
.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
|
@Composable
|
||||||
fun CategoryDropdownPreview() {
|
fun CategoryDropdown(
|
||||||
CategoryDropdown(
|
initialCategoryId: Int? = null,
|
||||||
onCategorySelected = {}
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
|
import eu.gaudian.translator.model.TagCategory
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
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.AppDialog
|
||||||
import eu.gaudian.translator.view.composable.DialogButton
|
import eu.gaudian.translator.view.composable.DialogButton
|
||||||
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategorySelectionDialog(
|
fun CategorySelectionDialog(
|
||||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||||
onDismissRequest: () -> 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))
|
Text(text = stringResource(R.string.text_select_categories))
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
|
// Dropdown button and menu
|
||||||
|
CategoryDropdownContent(
|
||||||
CategoryDropdown(
|
state = CategoryDropdownState(
|
||||||
onCategorySelected = { categories ->
|
expanded = expanded,
|
||||||
selectedCategory = categories
|
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,
|
noneSelectable = false,
|
||||||
multipleSelectable = true,
|
multipleSelectable = true,
|
||||||
onlyLists = true,
|
onlyLists = true,
|
||||||
addCategory = true
|
addCategory = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -54,10 +79,10 @@ fun CategorySelectionDialog(
|
|||||||
|
|
||||||
DialogButton(
|
DialogButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
onCategorySelected(selectedCategory)
|
onCategorySelected(selectedCategories)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
enabled = true
|
enabled = selectedCategories.isNotEmpty()
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.label_confirm))
|
Text(stringResource(R.string.label_confirm))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.utils.findActivity
|
||||||
import eu.gaudian.translator.view.composable.AppDialog
|
import eu.gaudian.translator.view.composable.AppDialog
|
||||||
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
import eu.gaudian.translator.view.composable.MultipleLanguageDropdown
|
||||||
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -42,12 +44,20 @@ fun StartExerciseDialog(
|
|||||||
) {
|
) {
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
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 coroutineScope = rememberCoroutineScope()
|
||||||
|
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
var lids by remember { mutableStateOf<List<Int>>(emptyList()) }
|
var lids by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||||
var languages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
var languages by remember { mutableStateOf<List<Language>>(emptyList()) }
|
||||||
// Map displayed Language to its DB id (lid) using position mapping from load
|
// Map displayed Language to its DB id (lid) using position mapping from load
|
||||||
var languageIdMap by remember { mutableStateOf<Map<Language, Int>>(emptyMap()) }
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
coroutineScope.launch {
|
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!! }
|
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)) }) {
|
AppDialog(onDismissRequest = onDismiss, title = { Text(stringResource(R.string.label_start_exercise)) }) {
|
||||||
|
|
||||||
@@ -80,11 +87,23 @@ fun StartExerciseDialog(
|
|||||||
},
|
},
|
||||||
languages
|
languages
|
||||||
)
|
)
|
||||||
CategoryDropdown(
|
CategoryDropdownContent(
|
||||||
onCategorySelected = { categories ->
|
state = CategoryDropdownState(
|
||||||
selectedCategories = categories.filterIsInstance<VocabularyCategory>()
|
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(
|
VocabularyStageDropDown(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
|
import eu.gaudian.translator.model.TagCategory
|
||||||
import eu.gaudian.translator.model.VocabularyCategory
|
import eu.gaudian.translator.model.VocabularyCategory
|
||||||
import eu.gaudian.translator.model.VocabularyItem
|
import eu.gaudian.translator.model.VocabularyItem
|
||||||
import eu.gaudian.translator.utils.findActivity
|
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.AppScaffold
|
||||||
import eu.gaudian.translator.view.composable.AppTopAppBar
|
import eu.gaudian.translator.view.composable.AppTopAppBar
|
||||||
import eu.gaudian.translator.view.hints.getVocabularyReviewHint
|
import eu.gaudian.translator.view.hints.getVocabularyReviewHint
|
||||||
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
import eu.gaudian.translator.viewmodel.VocabularyViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -43,12 +45,17 @@ fun VocabularyReviewScreen(
|
|||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
val activity = LocalContext.current.findActivity()
|
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 generatedItems: List<VocabularyItem> by vocabularyViewModel.generatedVocabularyItems.collectAsState()
|
||||||
|
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
val selectedItems = remember { mutableStateListOf<VocabularyItem>() }
|
||||||
val duplicates = remember { mutableStateListOf<Boolean>() }
|
val duplicates = remember { mutableStateListOf<Boolean>() }
|
||||||
var selectedCategoryId by remember { mutableStateOf<List<Int>>(emptyList()) }
|
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(emptyList()) }
|
||||||
LocalContext.current
|
var newCategoryName by remember { mutableStateOf("") }
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(generatedItems) {
|
LaunchedEffect(generatedItems) {
|
||||||
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
|
val duplicateResults = vocabularyViewModel.findDuplicates(generatedItems)
|
||||||
@@ -127,11 +134,28 @@ fun VocabularyReviewScreen(
|
|||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
modifier = Modifier.padding(8.dp)
|
modifier = Modifier.padding(8.dp)
|
||||||
)
|
)
|
||||||
CategoryDropdown(
|
CategoryDropdownContent(
|
||||||
onCategorySelected = { categories: List<VocabularyCategory?> ->
|
state = CategoryDropdownState(
|
||||||
selectedCategoryId = categories.filterNotNull().map { it.id }
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -143,9 +167,13 @@ fun VocabularyReviewScreen(
|
|||||||
Text(stringResource(R.string.label_cancel))
|
Text(stringResource(R.string.label_cancel))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
AppButton(onClick = {
|
AppButton(
|
||||||
onConfirm(selectedItems.toList(), selectedCategoryId)
|
onClick = {
|
||||||
}) {
|
val selectedCategoryIds = selectedCategories.filterNotNull().map { it.id }
|
||||||
|
onConfirm(selectedItems.toList(), selectedCategoryIds)
|
||||||
|
},
|
||||||
|
enabled = selectedItems.isNotEmpty()
|
||||||
|
) {
|
||||||
Text(stringResource(R.string.label_add_, selectedItems.size))
|
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.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -28,6 +27,7 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.model.VocabularyStage
|
import eu.gaudian.translator.model.VocabularyStage
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
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.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ fun VocabularyStageDropDown(
|
|||||||
) {
|
) {
|
||||||
if (noneSelectable == true) {
|
if (noneSelectable == true) {
|
||||||
val noneSelected = selectedStages.contains(null)
|
val noneSelected = selectedStages.contains(null)
|
||||||
DropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
@@ -111,7 +111,7 @@ fun VocabularyStageDropDown(
|
|||||||
|
|
||||||
VocabularyStage.entries.forEach { stage ->
|
VocabularyStage.entries.forEach { stage ->
|
||||||
val isSelected = selectedStages.contains(stage)
|
val isSelected = selectedStages.contains(stage)
|
||||||
DropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import eu.gaudian.translator.R
|
|||||||
import eu.gaudian.translator.model.LanguageModel
|
import eu.gaudian.translator.model.LanguageModel
|
||||||
import eu.gaudian.translator.model.communication.ApiProvider
|
import eu.gaudian.translator.model.communication.ApiProvider
|
||||||
import eu.gaudian.translator.utils.findActivity
|
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.AppAlertDialog
|
||||||
import eu.gaudian.translator.view.composable.AppButton
|
import eu.gaudian.translator.view.composable.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
import eu.gaudian.translator.view.composable.AppCard
|
||||||
|
|||||||
@@ -1,48 +1,28 @@
|
|||||||
package eu.gaudian.translator.view.settings
|
package eu.gaudian.translator.view.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.model.LanguageModel
|
import eu.gaudian.translator.model.LanguageModel
|
||||||
import eu.gaudian.translator.model.communication.ApiProvider
|
import eu.gaudian.translator.model.communication.ApiProvider
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
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.AppButton
|
||||||
import eu.gaudian.translator.view.composable.AppCard
|
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.InspiringSearchField
|
||||||
import eu.gaudian.translator.view.composable.ModelBadges
|
|
||||||
|
|
||||||
data class PromptSettingsState(
|
data class PromptSettingsState(
|
||||||
val availableModels: List<LanguageModel> = emptyList(),
|
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 androidx.navigation.NavController
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.utils.findActivity
|
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.AppCard
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
import eu.gaudian.translator.view.composable.AppIcons
|
||||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
|
|||||||
Reference in New Issue
Block a user