Step 1 in unifying dropdowns
This commit is contained in:
@@ -1,22 +1,19 @@
|
|||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
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.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
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.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
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.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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -24,8 +21,6 @@ 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.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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -42,7 +37,6 @@ fun ApiModelDropDown(
|
|||||||
onModelSelected: (LanguageModel?) -> Unit,
|
onModelSelected: (LanguageModel?) -> Unit,
|
||||||
enabled: Boolean = true
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
LocalContext.current
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -65,13 +59,8 @@ fun ApiModelDropDown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
// Custom button content showing selected model and provider
|
||||||
AppOutlinedButton(
|
val buttonContent: @Composable () -> Unit = {
|
||||||
onClick = { expanded = true },
|
|
||||||
modifier = Modifier.align(Alignment.Center),
|
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
|
|
||||||
enabled = enabled
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
@@ -101,56 +90,27 @@ fun ApiModelDropDown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
AppDropdownContainer(
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false }
|
onDismissRequest = {
|
||||||
|
expanded = false
|
||||||
|
searchQuery = ""
|
||||||
|
},
|
||||||
|
onExpandRequest = { expanded = true },
|
||||||
|
buttonText = "", // Not used with custom button content
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = enabled,
|
||||||
|
showSearch = true,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
onSearchQueryChange = { searchQuery = it },
|
||||||
|
searchPlaceholder = stringResource(R.string.label_search_models),
|
||||||
|
buttonContent = buttonContent
|
||||||
) {
|
) {
|
||||||
// Search bar
|
Column(
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.heightIn(max = 400.dp)
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
.verticalScroll(rememberScrollState())
|
||||||
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()) {
|
if (filteredGroupedModels.isNotEmpty()) {
|
||||||
filteredGroupedModels.entries.forEachIndexed { index, entry ->
|
filteredGroupedModels.entries.forEachIndexed { index, entry ->
|
||||||
val providerKey = entry.key
|
val providerKey = entry.key
|
||||||
@@ -158,7 +118,7 @@ fun ApiModelDropDown(
|
|||||||
val isActive = providerStatuses[providerKey] == true
|
val isActive = providerStatuses[providerKey] == true
|
||||||
val providerName = providerNames[providerKey] ?: providerKey
|
val providerName = providerNames[providerKey] ?: providerKey
|
||||||
|
|
||||||
if (index > 0) HorizontalDivider()
|
if (index > 0) HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
||||||
// Provider header
|
// Provider header
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
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.ColumnScope
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -14,22 +17,30 @@ 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
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
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.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
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.material3.DropdownMenu
|
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.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -48,6 +59,8 @@ 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.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
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
|
||||||
@@ -56,100 +69,362 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
|
|
||||||
/**
|
// =========================================
|
||||||
* A modern, custom dropdown menu composable that provides a styled text field with a dropdown list of options.
|
// UNIFIED DROPDOWN STYLES & CONSTANTS
|
||||||
* This implementation uses a custom dropdown for a more tailored look compared to the stock menu, behaving like a normal ExposedDropdownMenu.
|
// =========================================
|
||||||
* Allows managing selection and expansion, making it a convenient wrapper for dropdowns.
|
|
||||||
*
|
|
||||||
* @param expanded Whether the dropdown menu is expanded.
|
|
||||||
* @param onDismissRequest Callback invoked when the dropdown menu should be dismissed.
|
|
||||||
* @param modifier Modifier for the composable.
|
|
||||||
* @param label Composable for the label displayed in the text field.
|
|
||||||
* @param enabled Whether the dropdown is enabled.
|
|
||||||
* @param placeholder Optional placeholder text when no option is selected.
|
|
||||||
* @param selectedText The text to display in the text field for the selected option.
|
|
||||||
* @param onExpandRequest Callback invoked when the dropdown should expand.
|
|
||||||
* @param content Composable content for the dropdown items, typically using AppDropdownMenuItem.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun AppDropdownMenu(
|
|
||||||
expanded: Boolean,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
label: @Composable (() -> Unit)? = null,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
placeholder: @Composable (() -> Unit)? = null,
|
|
||||||
selectedText: String = "",
|
|
||||||
onExpandRequest: () -> Unit = {},
|
|
||||||
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) {
|
object DropdownDefaults {
|
||||||
OutlinedTextField(
|
val shape = RoundedCornerShape(8.dp)
|
||||||
value = selectedText,
|
val itemPaddingHorizontal = 8.dp
|
||||||
onValueChange = {},
|
val itemPaddingVertical = 2.dp
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
@Composable
|
||||||
.onGloballyPositioned { coordinates ->
|
fun containerColor(): Color = MaterialTheme.colorScheme.surface
|
||||||
textFieldSize = coordinates.size.toSize()
|
|
||||||
|
@Composable
|
||||||
|
fun itemBackground(selected: Boolean): Color {
|
||||||
|
return if (selected) {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)
|
||||||
|
} else {
|
||||||
|
Color.Transparent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.clickable(
|
|
||||||
enabled = enabled,
|
|
||||||
onClick = onExpandRequest,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = null
|
|
||||||
),
|
|
||||||
readOnly = true,
|
|
||||||
label = label,
|
|
||||||
placeholder = placeholder,
|
|
||||||
trailingIcon = {
|
|
||||||
val icon = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
},
|
|
||||||
shape = ComponentDefaults.DefaultShape,
|
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
|
||||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
|
||||||
unfocusedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = ComponentDefaults.ALPHA_LOW),
|
|
||||||
focusedLabelColor = MaterialTheme.colorScheme.primary,
|
|
||||||
cursorColor = MaterialTheme.colorScheme.primary,
|
|
||||||
disabledBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = ComponentDefaults.ALPHA_LOW),
|
|
||||||
disabledLabelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = ComponentDefaults.ALPHA_MEDIUM)
|
|
||||||
),
|
|
||||||
enabled = enabled,
|
|
||||||
interactionSource = interactionSource
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
@Composable
|
||||||
expanded = expanded,
|
fun itemContentColor(selected: Boolean, enabled: Boolean): Color {
|
||||||
onDismissRequest = onDismissRequest,
|
return when {
|
||||||
modifier = Modifier
|
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
|
selected -> MaterialTheme.colorScheme.primary
|
||||||
// Give the menu itself a bit of breathing room
|
else -> MaterialTheme.colorScheme.onSurface
|
||||||
.padding(vertical = 4.dp),
|
|
||||||
offset = DpOffset(0.dp, 4.dp), // Slight detachment from the anchor
|
|
||||||
scrollState = rememberScrollState(),
|
|
||||||
properties = PopupProperties(focusable = true),
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A modern and stylish composable for individual dropdown items, featuring enhanced visual design
|
* A drop-in replacement for [androidx.compose.material3.DropdownMenu] that opens
|
||||||
* with subtle shadows, rounded corners, and smooth interactions.
|
* as a BottomSheet. Compatible with the standard M3 signature.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@Suppress("unused", "HardCodedStringLiteral")
|
||||||
|
@Composable
|
||||||
|
fun AppDropDownMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
offset: DpOffset = DpOffset(0.dp, 0.dp), // Retained for signature compatibility
|
||||||
|
scrollState: ScrollState = rememberScrollState(),
|
||||||
|
properties: PopupProperties = PopupProperties(focusable = true), // Retained for signature compatibility
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
if (expanded) {
|
||||||
|
// skipPartiallyExpanded = true ensures it behaves more like a menu
|
||||||
|
// (fully open or completely closed) rather than a peekable sheet.
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
sheetState = sheetState,
|
||||||
|
// Container color, shape, etc., can be linked to your DropdownDefaults here if needed.
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
// Execute standard DropdownMenuItems here
|
||||||
|
content()
|
||||||
|
|
||||||
|
// Extra padding to ensure the last item isn't hidden behind the system navigation bar
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding().height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// UNIFIED DROPDOWN CONTAINER
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A unified dropdown container that provides consistent styling and behavior
|
||||||
|
* for all dropdown menus in the app.
|
||||||
|
*
|
||||||
|
* @param expanded Whether the dropdown is currently expanded
|
||||||
|
* @param onDismissRequest Callback when the dropdown should be dismissed
|
||||||
|
* @param onExpandRequest Callback when the dropdown should expand (click on button)
|
||||||
|
* @param buttonText The text to display on the dropdown button
|
||||||
|
* @param modifier Modifier for the container
|
||||||
|
* @param enabled Whether the dropdown is enabled
|
||||||
|
* @param showSearch Whether to show the search field at the top of the dropdown
|
||||||
|
* @param searchQuery Current search query (only used if showSearch is true)
|
||||||
|
* @param onSearchQueryChange Callback when search query changes (only used if showSearch is true)
|
||||||
|
* @param searchPlaceholder Placeholder text for search field
|
||||||
|
* @param showDoneButton Whether to show a "Done" button at the bottom (for multi-select)
|
||||||
|
* @param onDoneClick Callback when Done button is clicked
|
||||||
|
* @param buttonContent Custom content for the button (if null, uses default text-based button)
|
||||||
|
* @param dropdownContent Content to display inside the dropdown menu
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppDropdownContainer(
|
||||||
|
expanded: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onExpandRequest: () -> Unit,
|
||||||
|
buttonText: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
showSearch: Boolean = false,
|
||||||
|
searchQuery: String = "",
|
||||||
|
onSearchQueryChange: ((String) -> Unit)? = null,
|
||||||
|
searchPlaceholder: String? = null,
|
||||||
|
showDoneButton: Boolean = false,
|
||||||
|
onDoneClick: (() -> Unit)? = null,
|
||||||
|
buttonContent: @Composable (() -> Unit)? = null,
|
||||||
|
dropdownContent: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
// Dropdown Button
|
||||||
|
if (buttonContent != null) {
|
||||||
|
AppOutlinedButton(
|
||||||
|
onClick = onExpandRequest,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = enabled
|
||||||
|
) {
|
||||||
|
buttonContent()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AppOutlinedButton(
|
||||||
|
shape = DropdownDefaults.shape,
|
||||||
|
onClick = onExpandRequest,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = enabled
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = buttonText,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||||
|
contentDescription = if (expanded)
|
||||||
|
stringResource(R.string.cd_collapse)
|
||||||
|
else
|
||||||
|
stringResource(R.string.cd_expand)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom Sheet "Dropdown" Menu
|
||||||
|
if (expanded) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
sheetState = sheetState,
|
||||||
|
containerColor = DropdownDefaults.containerColor()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// Pinned Search field (optional)
|
||||||
|
if (showSearch && onSearchQueryChange != null) {
|
||||||
|
DropdownSearchField(
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
onSearchQueryChange = onSearchQueryChange,
|
||||||
|
placeholder = {
|
||||||
|
Text(searchPlaceholder ?: stringResource(R.string.text_search))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrollable Content
|
||||||
|
// Weight ensures this takes up available space without pushing
|
||||||
|
// the done button off-screen if the list is very long.
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f, fill = false)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
dropdownContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinned Done button (optional, for multi-select)
|
||||||
|
if (showDoneButton && onDoneClick != null) {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
AppButton(
|
||||||
|
onClick = {
|
||||||
|
onDoneClick()
|
||||||
|
onDismissRequest() // Often expected to close on 'Done'
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.label_done))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra padding for the system navigation bar so the bottom
|
||||||
|
// item/button isn't cut off by gesture hints or software keys.
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding().height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// UNIFIED DROPDOWN SEARCH FIELD
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A standardized search field for dropdown menus.
|
||||||
|
* Provides consistent styling across all dropdowns with search functionality.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DropdownSearchField(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
searchQuery: String,
|
||||||
|
onSearchQueryChange: (String) -> Unit,
|
||||||
|
placeholder: @Composable () -> Unit = { Text(stringResource(R.string.text_search)) },
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = 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 = onSearchQueryChange,
|
||||||
|
placeholder = placeholder,
|
||||||
|
singleLine = true,
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
disabledContainerColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
if (searchQuery.isNotBlank()) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { onSearchQueryChange("") },
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = AppIcons.Close,
|
||||||
|
contentDescription = stringResource(R.string.cd_clear_search),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true, name = "Search Field - Empty")
|
||||||
|
@Composable
|
||||||
|
fun DropdownSearchFieldEmptyPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
Surface {
|
||||||
|
DropdownSearchField(
|
||||||
|
searchQuery = "",
|
||||||
|
onSearchQueryChange = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true, name = "Search Field - Filled")
|
||||||
|
@Composable
|
||||||
|
fun DropdownSearchFieldFilledPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
Surface {
|
||||||
|
DropdownSearchField(
|
||||||
|
searchQuery = "English",
|
||||||
|
onSearchQueryChange = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true, name = "Search Field - With Close Button")
|
||||||
|
@Composable
|
||||||
|
fun DropdownSearchFieldWithClosePreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
Surface {
|
||||||
|
DropdownSearchField(
|
||||||
|
searchQuery = "German",
|
||||||
|
onSearchQueryChange = {}
|
||||||
|
// Providing this triggers the right-most close icon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true, name = "Search Field - Interactive")
|
||||||
|
@Composable
|
||||||
|
fun DropdownSearchFieldInteractivePreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
Surface {
|
||||||
|
var query by remember { mutableStateOf("") }
|
||||||
|
DropdownSearchField(
|
||||||
|
searchQuery = query,
|
||||||
|
onSearchQueryChange = { query = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// UNIFIED DROPDOWN HEADER
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A standardized header for dropdown sections.
|
||||||
|
* Provides consistent styling for section headers like "Favorites", "Recent", etc.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DropdownHeader(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.Bold),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
|
||||||
|
modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// UNIFIED DROPDOWN ITEM COMPONENT
|
||||||
|
// =========================================
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppDropdownMenuItem(
|
fun AppDropdownMenuItem(
|
||||||
text: @Composable () -> Unit,
|
text: @Composable () -> Unit,
|
||||||
@@ -160,23 +435,25 @@ fun AppDropdownMenuItem(
|
|||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val contentColor = if (enabled) {
|
val contentColor by animateColorAsState(
|
||||||
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
targetValue = DropdownDefaults.itemContentColor(selected, enabled),
|
||||||
} else {
|
label = "contentColor"
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
)
|
||||||
}
|
val backgroundColor by animateColorAsState(
|
||||||
|
targetValue = DropdownDefaults.itemBackground(selected),
|
||||||
// Modern "floating" highlight background
|
label = "backgroundColor"
|
||||||
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
|
.padding(
|
||||||
.clip(RoundedCornerShape(8.dp))
|
horizontal = DropdownDefaults.itemPaddingHorizontal,
|
||||||
|
vertical = DropdownDefaults.itemPaddingVertical
|
||||||
|
)
|
||||||
|
.clip(DropdownDefaults.shape)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.clickable(enabled = enabled) { onClick() }
|
.clickable(enabled = enabled) { onClick() }
|
||||||
//.padding(horizontal = 12.dp, vertical = 10.dp) // Inner padding keeps content comfortable
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -199,7 +476,112 @@ fun AppDropdownMenuItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... [Previews remain exactly the same as your original file] ...
|
/**
|
||||||
|
* A lightweight, modern dropdown menu composable with a clean text field and dropdown list.
|
||||||
|
*/
|
||||||
|
@Suppress("unused", "HardCodedStringLiteral")
|
||||||
|
@Composable
|
||||||
|
fun AppDropdownMenu(
|
||||||
|
expanded: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
label: @Composable (() -> Unit)? = null,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
placeholder: @Composable (() -> Unit)? = null,
|
||||||
|
selectedText: String = "",
|
||||||
|
onExpandRequest: () -> Unit = {},
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
var textFieldSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedText,
|
||||||
|
onValueChange = {},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
textFieldSize = coordinates.size.toSize()
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = onExpandRequest,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null
|
||||||
|
),
|
||||||
|
readOnly = true,
|
||||||
|
label = label,
|
||||||
|
placeholder = placeholder,
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
||||||
|
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = DropdownDefaults.shape,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.6f),
|
||||||
|
focusedLabelColor = MaterialTheme.colorScheme.primary,
|
||||||
|
cursorColor = MaterialTheme.colorScheme.primary,
|
||||||
|
disabledBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||||
|
disabledLabelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
|
),
|
||||||
|
enabled = enabled,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = Modifier.width(with(LocalDensity.current) { textFieldSize.width.toDp() }),
|
||||||
|
offset = DpOffset(0.dp, 2.dp),
|
||||||
|
properties = PopupProperties(focusable = true),
|
||||||
|
shape = DropdownDefaults.shape,
|
||||||
|
containerColor = DropdownDefaults.containerColor()
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// LEGACY COMPONENTS (UPDATED TO USE UNIFIED STYLES)
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LargeDropdownMenuItem(
|
||||||
|
text: String,
|
||||||
|
selected: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val contentColor = DropdownDefaults.itemContentColor(selected, enabled)
|
||||||
|
val backgroundColor = DropdownDefaults.itemBackground(selected)
|
||||||
|
val fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
horizontal = DropdownDefaults.itemPaddingHorizontal,
|
||||||
|
vertical = DropdownDefaults.itemPaddingVertical
|
||||||
|
)
|
||||||
|
.clip(DropdownDefaults.shape)
|
||||||
|
.background(backgroundColor)
|
||||||
|
.clickable(enabled) { onClick() }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleSmall.copy(fontWeight = fontWeight),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> LargeDropdownMenu(
|
fun <T> LargeDropdownMenu(
|
||||||
@@ -210,12 +592,12 @@ fun <T> LargeDropdownMenu(
|
|||||||
items: List<T>,
|
items: List<T>,
|
||||||
selectedIndex: Int = -1,
|
selectedIndex: Int = -1,
|
||||||
onItemSelected: (index: Int, item: T) -> Unit,
|
onItemSelected: (index: Int, item: T) -> Unit,
|
||||||
selectedItemToString: (T) -> String = { it.toString() },
|
selectedItemToString: (T) -> String = { item: T -> item.toString() },
|
||||||
drawItem: @Composable (T, Boolean, Boolean, () -> Unit) -> Unit = { item, selected, itemEnabled, onClick ->
|
drawItem: @Composable (T, Boolean, Boolean, () -> Unit) -> Unit = { item: T, selected: Boolean, _: Boolean, onClick: () -> Unit ->
|
||||||
LargeDropdownMenuItem(
|
LargeDropdownMenuItem(
|
||||||
text = item.toString(),
|
text = item.toString(),
|
||||||
selected = selected,
|
selected = selected,
|
||||||
enabled = itemEnabled,
|
enabled = true,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -247,13 +629,10 @@ fun <T> LargeDropdownMenu(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
Dialog(
|
Dialog(onDismissRequest = { expanded = false }) {
|
||||||
onDismissRequest = { expanded = false }, // Fixed bug from original code
|
|
||||||
) {
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
shadowElevation = 8.dp,
|
|
||||||
tonalElevation = 6.dp
|
tonalElevation = 6.dp
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@@ -263,7 +642,6 @@ fun <T> LargeDropdownMenu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Added vertical padding to the list instead of hard dividers
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
state = listState,
|
state = listState,
|
||||||
@@ -279,7 +657,7 @@ fun <T> LargeDropdownMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemsIndexed(items) { index, item ->
|
itemsIndexed(items) { index: Int, item: T ->
|
||||||
val selectedItem = index == selectedIndex
|
val selectedItem = index == selectedIndex
|
||||||
drawItem(
|
drawItem(
|
||||||
item,
|
item,
|
||||||
@@ -296,39 +674,7 @@ fun <T> LargeDropdownMenu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
// ============== PREVIEWS ==============
|
||||||
fun LargeDropdownMenuItem(
|
|
||||||
text: String,
|
|
||||||
selected: Boolean,
|
|
||||||
enabled: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
val contentColor = when {
|
|
||||||
!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
|
|
||||||
.fillMaxWidth()
|
|
||||||
.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.copy(fontWeight = fontWeight),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@@ -354,6 +700,30 @@ fun LargeDropdownMenuItemSelectedPreview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun AppDropdownMenuItemPreview() {
|
||||||
|
AppDropdownMenuItem(
|
||||||
|
text = { Text("Sample Item") },
|
||||||
|
onClick = {},
|
||||||
|
selected = false,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("HardCodedStringLiteral")
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun AppDropdownMenuItemSelectedPreview() {
|
||||||
|
AppDropdownMenuItem(
|
||||||
|
text = { Text("Selected Item") },
|
||||||
|
onClick = {},
|
||||||
|
selected = true,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
@Suppress("HardCodedStringLiteral")
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -365,53 +735,8 @@ fun LargeDropdownMenuPreview() {
|
|||||||
label = "Select Option",
|
label = "Select Option",
|
||||||
items = options,
|
items = options,
|
||||||
selectedIndex = selectedIndex,
|
selectedIndex = selectedIndex,
|
||||||
onItemSelected = { index, _ ->
|
onItemSelected = { index: Int, _: String ->
|
||||||
selectedIndex = index
|
selectedIndex = index
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun LargeDropdownMenuExpandedPreview() {
|
|
||||||
val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5", "Option 6")
|
|
||||||
var selectedIndex by remember { mutableIntStateOf(2) }
|
|
||||||
|
|
||||||
// Simulate expanded state by showing the dropdown and the dialog content
|
|
||||||
Column {
|
|
||||||
LargeDropdownMenu(
|
|
||||||
label = "Select Option",
|
|
||||||
items = options,
|
|
||||||
selectedIndex = selectedIndex,
|
|
||||||
onItemSelected = { index, _ ->
|
|
||||||
selectedIndex = index
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Manually show the expanded dialog content for preview
|
|
||||||
Dialog(onDismissRequest = {}) {
|
|
||||||
Surface(shape = RoundedCornerShape(12.dp)) {
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
LaunchedEffect("ScrollToSelected") {
|
|
||||||
listState.scrollToItem(index = selectedIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) {
|
|
||||||
itemsIndexed(options) { index, item ->
|
|
||||||
LargeDropdownMenuItem(
|
|
||||||
text = item,
|
|
||||||
selected = index == selectedIndex,
|
|
||||||
enabled = true,
|
|
||||||
onClick = { selectedIndex = index }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (index < options.lastIndex) {
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("AssignedValueIsNeverRead", "HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.composable
|
package eu.gaudian.translator.view.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -6,19 +8,19 @@ 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.heightIn
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
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.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.ModalBottomSheet
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -33,7 +35,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -82,7 +83,10 @@ fun BaseLanguageDropDown(
|
|||||||
else -> stringResource(R.string.label_language_none)
|
else -> stringResource(R.string.label_language_none)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
AppOutlinedButton(
|
AppOutlinedButton(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
onClick = { expanded = true },
|
onClick = { expanded = true },
|
||||||
@@ -104,12 +108,17 @@ fun BaseLanguageDropDown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(modifier = modifier.fillMaxWidth(), expanded = expanded, onDismissRequest = {
|
if (expanded) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = {
|
||||||
expanded = false
|
expanded = false
|
||||||
searchText = ""
|
searchText = ""
|
||||||
tempSelection = emptyList() // Also reset temp selection on dismiss
|
tempSelection = emptyList()
|
||||||
}) {
|
},
|
||||||
// Helper composable for a single language row in multiple selection mode
|
sheetState = sheetState
|
||||||
|
) {
|
||||||
@Composable
|
@Composable
|
||||||
fun MultiSelectItem(language: Language) {
|
fun MultiSelectItem(language: Language) {
|
||||||
val isSelected = tempSelection.contains(language)
|
val isSelected = tempSelection.contains(language)
|
||||||
@@ -120,7 +129,6 @@ fun BaseLanguageDropDown(
|
|||||||
checked = isSelected,
|
checked = isSelected,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
|
||||||
selectedLanguagesCount = tempSelection.size
|
selectedLanguagesCount = tempSelection.size
|
||||||
onLanguagesSelected(tempSelection)
|
onLanguagesSelected(tempSelection)
|
||||||
}
|
}
|
||||||
@@ -141,13 +149,11 @@ fun BaseLanguageDropDown(
|
|||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
|
||||||
selectedLanguagesCount = tempSelection.size
|
selectedLanguagesCount = tempSelection.size
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper composable for a single language row in single selection mode
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SingleSelectItem(language: Language) {
|
fun SingleSelectItem(language: Language) {
|
||||||
val languageNames = languages.map { it.name }
|
val languageNames = languages.map { it.name }
|
||||||
@@ -197,43 +203,22 @@ fun BaseLanguageDropDown(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Main Dropdown Content ---
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth()
|
||||||
.heightIn(max = 900.dp) // Constrain the height
|
|
||||||
) {
|
) {
|
||||||
// Search bar with a back arrow
|
DropdownSearchField(
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
searchQuery = searchText,
|
||||||
IconButton(onClick = { expanded = false; searchText = "" }) {
|
onSearchQueryChange = { searchText = it },
|
||||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.label_close))
|
|
||||||
}
|
|
||||||
TextField(
|
|
||||||
value = searchText,
|
|
||||||
onValueChange = { searchText = it },
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = { Text(stringResource(R.string.text_search_3d)) },
|
placeholder = { Text(stringResource(R.string.text_search_3d)) },
|
||||||
trailingIcon = {
|
|
||||||
if (searchText.isNotBlank()) {
|
|
||||||
IconButton(onClick = { searchText = "" }) {
|
|
||||||
Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.cd_clear_search))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = Color.Transparent,
|
|
||||||
unfocusedContainerColor = Color.Transparent,
|
|
||||||
disabledContainerColor = Color.Transparent,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
|
||||||
),
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// Replaced height(max = 900.dp) with standard weight logic to allow proper scrolling bounds
|
||||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f, fill = false)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
val isSearching = searchText.isNotBlank()
|
val isSearching = searchText.isNotBlank()
|
||||||
|
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
@@ -255,80 +240,91 @@ fun BaseLanguageDropDown(
|
|||||||
} else if (alternateLanguages.isNotEmpty()) {
|
} else if (alternateLanguages.isNotEmpty()) {
|
||||||
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
||||||
if (enableMultipleSelection) {
|
if (enableMultipleSelection) {
|
||||||
Text(stringResource(R.string.text_all_languages), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||||
sortedAlternate.forEach { language -> MultiSelectItem(language) }
|
sortedAlternate.forEach { language -> MultiSelectItem(language) }
|
||||||
} else {
|
} else {
|
||||||
Text(stringResource(R.string.text_all_languages), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||||
sortedAlternate.forEach { language -> SingleSelectItem(language) }
|
sortedAlternate.forEach { language -> SingleSelectItem(language) }
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (enableMultipleSelection) {
|
if (enableMultipleSelection) {
|
||||||
if (favoriteLanguages.isNotEmpty()) {
|
if (favoriteLanguages.isNotEmpty()) {
|
||||||
Text(stringResource(R.string.text_favorites), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
DropdownHeader(text = stringResource(R.string.text_favorites))
|
||||||
favoriteLanguages.forEach { language -> MultiSelectItem(language) }
|
favoriteLanguages.forEach { language -> MultiSelectItem(language) }
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
val recentHistoryFiltered = languageHistory.filter { it !in favoriteLanguages }.takeLast(5)
|
val recentHistoryFiltered = languageHistory.filter { it !in favoriteLanguages }.takeLast(5)
|
||||||
if (recentHistoryFiltered.isNotEmpty() && alternateLanguages.isEmpty()) {
|
if (recentHistoryFiltered.isNotEmpty() && alternateLanguages.isEmpty()) {
|
||||||
Text(stringResource(R.string.text_recent_history), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
DropdownHeader(text = stringResource(R.string.text_recent_history))
|
||||||
recentHistoryFiltered.forEach { language -> MultiSelectItem(language) }
|
recentHistoryFiltered.forEach { language -> MultiSelectItem(language) }
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
val remainingLanguages = languages.sortedBy { it.name }
|
val remainingLanguages = languages.sortedBy { it.name }
|
||||||
if (remainingLanguages.isNotEmpty()) {
|
if (remainingLanguages.isNotEmpty()) {
|
||||||
Text(stringResource(R.string.text_all_languages), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||||
remainingLanguages.forEach { language -> MultiSelectItem(language) }
|
remainingLanguages.forEach { language -> MultiSelectItem(language) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Logic for single selection default view
|
|
||||||
if (showAutoOption) {
|
if (showAutoOption) {
|
||||||
AppDropdownMenuItem(text = { Text(stringResource(R.string.text_select_auto_recognition)) }, onClick = { onAutoSelected(); expanded = false; searchText = "" })
|
LargeDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.text_select_auto_recognition),
|
||||||
|
selected = false, // Set to true if you want to highlight it when active
|
||||||
|
enabled = true,
|
||||||
|
onClick = {
|
||||||
|
onAutoSelected()
|
||||||
|
expanded = false
|
||||||
|
searchText = ""
|
||||||
|
}
|
||||||
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
if (showNoneOption) {
|
if (showNoneOption) {
|
||||||
AppDropdownMenuItem(text = { Text(stringResource(R.string.text_select_none)) }, onClick = { onNoneSelected(); expanded = false; searchText = "" })
|
LargeDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.text_select_no_language),
|
||||||
|
selected = false, // Set to true if you want to highlight it when active
|
||||||
|
enabled = true,
|
||||||
|
onClick = {
|
||||||
|
onNoneSelected()
|
||||||
|
expanded = false
|
||||||
|
searchText = ""
|
||||||
|
}
|
||||||
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
if (favoriteLanguages.any {
|
if (favoriteLanguages.any {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
it.code != "none" && it.code != "auto"
|
it.code != "none" && it.code != "auto"
|
||||||
}) {
|
}) {
|
||||||
Text(stringResource(R.string.text_favorites), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
DropdownHeader(text = stringResource(R.string.text_favorites))
|
||||||
favoriteLanguages.filter {
|
favoriteLanguages.filter {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
it.code != "none" && it.code != "auto"
|
it.code != "none" && it.code != "auto"
|
||||||
}.forEach { language -> SingleSelectItem(language) }
|
}.forEach { language -> SingleSelectItem(language) }
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
val recentHistoryFiltered = languageHistory.filter {
|
val recentHistoryFiltered = languageHistory.filter {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
it !in favoriteLanguages && it.code != "none" && it.code != "auto"
|
it !in favoriteLanguages && it.code != "none" && it.code != "auto"
|
||||||
}.takeLast(5)
|
}.takeLast(5)
|
||||||
if (recentHistoryFiltered.isNotEmpty()) {
|
if (recentHistoryFiltered.isNotEmpty()) {
|
||||||
Text(stringResource(R.string.text_recent_history), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
DropdownHeader(text = stringResource(R.string.text_recent_history))
|
||||||
recentHistoryFiltered.forEach { language -> SingleSelectItem(language) }
|
recentHistoryFiltered.forEach { language -> SingleSelectItem(language) }
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
val remainingLanguages = languages.filter {
|
val remainingLanguages = languages.filter {
|
||||||
@Suppress("HardCodedStringLiteral")
|
|
||||||
it !in favoriteLanguages && it !in recentHistoryFiltered && it.code != "none" && it.code != "auto"
|
it !in favoriteLanguages && it !in recentHistoryFiltered && it.code != "none" && it.code != "auto"
|
||||||
}.sortedBy { it.name }
|
}.sortedBy { it.name }
|
||||||
if (remainingLanguages.isNotEmpty()) {
|
if (remainingLanguages.isNotEmpty()) {
|
||||||
Text(stringResource(R.string.text_all_languages), style = TextStyle(fontWeight = FontWeight.Bold), modifier = Modifier.padding(8.dp))
|
DropdownHeader(text = stringResource(R.string.text_all_languages))
|
||||||
remainingLanguages.forEach { language -> SingleSelectItem(language) }
|
remainingLanguages.forEach { language -> SingleSelectItem(language) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done button for multiple selection mode
|
|
||||||
if (enableMultipleSelection) {
|
if (enableMultipleSelection) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
AppButton(
|
AppButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
onLanguagesSelected(tempSelection)
|
onLanguagesSelected(tempSelection)
|
||||||
@Suppress("AssignedValueIsNeverRead")
|
|
||||||
selectedLanguagesCount = tempSelection.size
|
selectedLanguagesCount = tempSelection.size
|
||||||
expanded = false
|
expanded = false
|
||||||
searchText = ""
|
searchText = ""
|
||||||
@@ -340,6 +336,10 @@ fun BaseLanguageDropDown(
|
|||||||
Text(stringResource(R.string.label_done))
|
Text(stringResource(R.string.label_done))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provides breathing room for system gestures at bottom of sheet
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding().height(16.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import eu.gaudian.translator.view.hints.CategoryHint
|
|||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
|
|
||||||
|
|
||||||
enum class DialogCategoryType { TAG, FILTER }
|
enum class DialogCategoryType { TAG, FILTER }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
|
@file:Suppress("AssignedValueIsNeverRead", "HardCodedStringLiteral")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.dialogs
|
package eu.gaudian.translator.view.dialogs
|
||||||
|
|
||||||
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.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
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.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
|
||||||
@@ -24,24 +23,20 @@ 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.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.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 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.model.VocabularyFilter
|
|
||||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||||
import eu.gaudian.translator.utils.findActivity
|
import eu.gaudian.translator.utils.findActivity
|
||||||
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.AppDropdownContainer
|
||||||
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.AppOutlinedTextField
|
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||||
|
import eu.gaudian.translator.view.composable.DropdownHeader
|
||||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||||
|
|
||||||
|
|
||||||
@@ -54,22 +49,12 @@ data class CategoryDropdownState(
|
|||||||
val selectedCategories: List<VocabularyCategory?> = emptyList(),
|
val selectedCategories: List<VocabularyCategory?> = emptyList(),
|
||||||
val newCategoryName: String = "",
|
val newCategoryName: String = "",
|
||||||
val categories: List<VocabularyCategory> = emptyList(),
|
val categories: List<VocabularyCategory> = emptyList(),
|
||||||
|
val searchQuery: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stateless dropdown content composable for category selection.
|
* Stateless dropdown content composable for category selection.
|
||||||
* This component is fully controlled by its parameters and does not maintain any internal state.
|
* 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 CategoryDropdownContent(
|
fun CategoryDropdownContent(
|
||||||
@@ -79,10 +64,12 @@ fun CategoryDropdownContent(
|
|||||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||||
onNewCategoryNameChange: (String) -> Unit,
|
onNewCategoryNameChange: (String) -> Unit,
|
||||||
onAddCategory: (String) -> Unit,
|
onAddCategory: (String) -> Unit,
|
||||||
|
onSearchQueryChange: (String) -> Unit = {},
|
||||||
noneSelectable: Boolean = true,
|
noneSelectable: Boolean = true,
|
||||||
multipleSelectable: Boolean = false,
|
multipleSelectable: Boolean = false,
|
||||||
onlyLists: Boolean = false,
|
onlyLists: Boolean = false,
|
||||||
addCategory: Boolean = false,
|
addCategory: Boolean = false,
|
||||||
|
enableSearch: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val selectableCategories = if (onlyLists) {
|
val selectableCategories = if (onlyLists) {
|
||||||
state.categories.filterIsInstance<TagCategory>()
|
state.categories.filterIsInstance<TagCategory>()
|
||||||
@@ -90,37 +77,34 @@ fun CategoryDropdownContent(
|
|||||||
state.categories
|
state.categories
|
||||||
}
|
}
|
||||||
|
|
||||||
AppOutlinedButton(
|
// Filter categories by search query if search is enabled
|
||||||
shape = RoundedCornerShape(8.dp),
|
val filteredCategories = if (enableSearch && state.searchQuery.isNotBlank()) {
|
||||||
onClick = { onExpand(true) },
|
selectableCategories.filter { category ->
|
||||||
modifier = modifier.fillMaxWidth(),
|
category.name.contains(state.searchQuery, ignoreCase = true)
|
||||||
) {
|
}
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
} else {
|
||||||
Text(
|
selectableCategories
|
||||||
text = when {
|
}
|
||||||
|
|
||||||
|
val buttonText = when {
|
||||||
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
||||||
state.selectedCategories.size == 1 -> state.selectedCategories.first()?.name
|
state.selectedCategories.size == 1 -> state.selectedCategories.first()?.name
|
||||||
?: stringResource(R.string.label_no_category)
|
?: stringResource(R.string.label_no_category)
|
||||||
else -> stringResource(R.string.text_2d_categories_selected, state.selectedCategories.size)
|
else -> stringResource(R.string.text_2d_categories_selected, state.selectedCategories.size)
|
||||||
},
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
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(
|
AppDropdownContainer(
|
||||||
expanded = state.expanded,
|
expanded = state.expanded,
|
||||||
onDismissRequest = { onExpand(false) },
|
onDismissRequest = { onExpand(false) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
onExpandRequest = { onExpand(true) },
|
||||||
|
buttonText = buttonText,
|
||||||
|
modifier = modifier,
|
||||||
|
showSearch = enableSearch,
|
||||||
|
searchQuery = state.searchQuery,
|
||||||
|
onSearchQueryChange = onSearchQueryChange,
|
||||||
|
searchPlaceholder = stringResource(R.string.text_search),
|
||||||
|
showDoneButton = multipleSelectable,
|
||||||
|
onDoneClick = { onExpand(false) }
|
||||||
) {
|
) {
|
||||||
if (noneSelectable) {
|
if (noneSelectable) {
|
||||||
val noneSelected = state.selectedCategories.contains(null)
|
val noneSelected = state.selectedCategories.contains(null)
|
||||||
@@ -133,7 +117,7 @@ fun CategoryDropdownContent(
|
|||||||
if (multipleSelectable) {
|
if (multipleSelectable) {
|
||||||
AppCheckbox(
|
AppCheckbox(
|
||||||
checked = noneSelected,
|
checked = noneSelected,
|
||||||
onCheckedChange = { isChecked ->
|
onCheckedChange = { _ ->
|
||||||
val newSelection = if (noneSelected) {
|
val newSelection = if (noneSelected) {
|
||||||
state.selectedCategories.filterNotNull()
|
state.selectedCategories.filterNotNull()
|
||||||
} else {
|
} else {
|
||||||
@@ -144,7 +128,10 @@ fun CategoryDropdownContent(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
Text(stringResource(R.string.label_no_category))
|
Text(
|
||||||
|
text = stringResource(R.string.label_no_category),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -163,7 +150,7 @@ fun CategoryDropdownContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectableCategories.forEach { category ->
|
filteredCategories.forEach { category ->
|
||||||
val isSelected = state.selectedCategories.contains(category)
|
val isSelected = state.selectedCategories.contains(category)
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
@@ -185,7 +172,10 @@ fun CategoryDropdownContent(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
Text(category.name)
|
Text(
|
||||||
|
text = category.name,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -204,16 +194,24 @@ fun CategoryDropdownContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addCategory) {
|
if (enableSearch && state.searchQuery.isNotBlank() && filteredCategories.isEmpty()) {
|
||||||
HorizontalDivider()
|
|
||||||
|
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(stringResource(R.string.label_add_category))
|
Text(
|
||||||
|
text = stringResource(R.string.text_no_models_found),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onClick = {},
|
onClick = {},
|
||||||
modifier = Modifier.padding(4.dp)
|
enabled = false
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addCategory) {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
||||||
|
DropdownHeader(text = stringResource(R.string.label_add_category))
|
||||||
|
|
||||||
AppDropdownMenuItem(
|
AppDropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
@@ -227,7 +225,7 @@ fun CategoryDropdownContent(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (state.newCategoryName.isNotBlank()) {
|
if (state.newCategoryName.isNotBlank()) {
|
||||||
@@ -246,32 +244,11 @@ fun CategoryDropdownContent(
|
|||||||
onClick = {}
|
onClick = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multipleSelectable) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
AppButton(
|
|
||||||
onClick = { onExpand(false) },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.label_done))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stateful wrapper for CategoryDropdown that manages its own state.
|
* 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 CategoryDropdown(
|
fun CategoryDropdown(
|
||||||
@@ -282,26 +259,23 @@ fun CategoryDropdown(
|
|||||||
multipleSelectable: Boolean = false,
|
multipleSelectable: Boolean = false,
|
||||||
onlyLists: Boolean = false,
|
onlyLists: Boolean = false,
|
||||||
addCategory: Boolean = false,
|
addCategory: Boolean = false,
|
||||||
|
enableSearch: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
var selectedCategories by remember {
|
var selectedCategories by remember {
|
||||||
mutableStateOf<List<VocabularyCategory?>>(emptyList())
|
mutableStateOf<List<VocabularyCategory?>>(emptyList())
|
||||||
}
|
}
|
||||||
var newCategoryName by remember { mutableStateOf("") }
|
var newCategoryName by remember { mutableStateOf("") }
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val activity = LocalContext.current.findActivity()
|
val activity = LocalContext.current.findActivity()
|
||||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Find initial category
|
|
||||||
val initialCategory = remember(categories, initialCategoryId) {
|
val initialCategory = remember(categories, initialCategoryId) {
|
||||||
categories.find { it.id == initialCategoryId }
|
categories.find { it.id == initialCategoryId }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize selection with initial category if provided
|
|
||||||
remember(initialCategory) {
|
remember(initialCategory) {
|
||||||
if (initialCategory != null && selectedCategories.isEmpty()) {
|
if (initialCategory != null && selectedCategories.isEmpty()) {
|
||||||
selectedCategories = listOf(initialCategory)
|
selectedCategories = listOf(initialCategory)
|
||||||
@@ -315,6 +289,7 @@ fun CategoryDropdown(
|
|||||||
selectedCategories = selectedCategories,
|
selectedCategories = selectedCategories,
|
||||||
newCategoryName = newCategoryName,
|
newCategoryName = newCategoryName,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
|
searchQuery = searchQuery,
|
||||||
),
|
),
|
||||||
onExpand = { isExpanded -> expanded = isExpanded },
|
onExpand = { isExpanded -> expanded = isExpanded },
|
||||||
onCategorySelected = { newSelection ->
|
onCategorySelected = { newSelection ->
|
||||||
@@ -324,115 +299,35 @@ fun CategoryDropdown(
|
|||||||
onNewCategoryNameChange = { newCategoryName = it },
|
onNewCategoryNameChange = { newCategoryName = it },
|
||||||
onAddCategory = { name ->
|
onAddCategory = { name ->
|
||||||
val newCategory = TagCategory(id = 0, name = name)
|
val newCategory = TagCategory(id = 0, name = name)
|
||||||
// In production, this would call ViewModel.createCategory(newCategory)
|
|
||||||
newCategoryName = ""
|
newCategoryName = ""
|
||||||
|
categoryViewModel.createCategory(newCategory)
|
||||||
|
//selectedCategories = selectedCategories + newCategory
|
||||||
if (!multipleSelectable) {
|
if (!multipleSelectable) {
|
||||||
expanded = false
|
expanded = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSearchQueryChange = { searchQuery = it },
|
||||||
noneSelectable = noneSelectable == true,
|
noneSelectable = noneSelectable == true,
|
||||||
multipleSelectable = multipleSelectable,
|
multipleSelectable = multipleSelectable,
|
||||||
onlyLists = onlyLists,
|
onlyLists = onlyLists,
|
||||||
addCategory = addCategory,
|
addCategory = addCategory,
|
||||||
|
enableSearch = enableSearch,
|
||||||
modifier = modifier,
|
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
|
@ThemePreviews
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownCollapsedPreview(
|
fun CategoryDropdownCollapsedPreview() {
|
||||||
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
CategoryDropdownContent(
|
||||||
state = state.copy(expanded = false),
|
state = CategoryDropdownState(
|
||||||
|
expanded = false,
|
||||||
|
selectedCategories = emptyList(),
|
||||||
|
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food")),
|
||||||
|
),
|
||||||
onExpand = {},
|
onExpand = {},
|
||||||
onCategorySelected = {},
|
onCategorySelected = {},
|
||||||
onNewCategoryNameChange = {},
|
onNewCategoryNameChange = {},
|
||||||
@@ -444,15 +339,14 @@ fun CategoryDropdownCollapsedPreview(
|
|||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownExpandedPreview(
|
fun CategoryDropdownExpandedPreview() {
|
||||||
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
CategoryDropdownContent(
|
||||||
state = state.copy(expanded = true),
|
state = CategoryDropdownState(
|
||||||
|
expanded = true,
|
||||||
|
selectedCategories = listOf(TagCategory(1, "Animals")),
|
||||||
|
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food"), TagCategory(3, "Travel")),
|
||||||
|
),
|
||||||
onExpand = {},
|
onExpand = {},
|
||||||
onCategorySelected = {},
|
onCategorySelected = {},
|
||||||
onNewCategoryNameChange = {},
|
onNewCategoryNameChange = {},
|
||||||
@@ -466,21 +360,10 @@ fun CategoryDropdownExpandedPreview(
|
|||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownMultipleSelectionPreview() {
|
fun CategoryDropdownMultipleSelectionPreview() {
|
||||||
val categories = listOf(
|
val categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food"), TagCategory(3, "Travel"))
|
||||||
TagCategory(1, "Animals"),
|
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(listOf(categories[0], categories[2])) }
|
||||||
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(
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
CategoryDropdownContent(
|
||||||
state = CategoryDropdownState(
|
state = CategoryDropdownState(
|
||||||
expanded = true,
|
expanded = true,
|
||||||
@@ -502,26 +385,17 @@ fun CategoryDropdownMultipleSelectionPreview() {
|
|||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownWithAddCategoryPreview() {
|
fun CategoryDropdownWithAddCategoryPreview() {
|
||||||
val categories = listOf(
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
TagCategory(1, "Animals"),
|
|
||||||
TagCategory(2, "Food"),
|
|
||||||
)
|
|
||||||
var newCategoryName by remember { mutableStateOf("New Category") }
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
color = MaterialTheme.colorScheme.background
|
|
||||||
) {
|
|
||||||
CategoryDropdownContent(
|
CategoryDropdownContent(
|
||||||
state = CategoryDropdownState(
|
state = CategoryDropdownState(
|
||||||
expanded = true,
|
expanded = true,
|
||||||
selectedCategories = emptyList(),
|
selectedCategories = emptyList(),
|
||||||
newCategoryName = newCategoryName,
|
newCategoryName = "New Cat",
|
||||||
categories = categories,
|
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food")),
|
||||||
),
|
),
|
||||||
onExpand = {},
|
onExpand = {},
|
||||||
onCategorySelected = {},
|
onCategorySelected = {},
|
||||||
onNewCategoryNameChange = { newCategoryName = it },
|
onNewCategoryNameChange = {},
|
||||||
onAddCategory = {},
|
onAddCategory = {},
|
||||||
addCategory = true,
|
addCategory = true,
|
||||||
)
|
)
|
||||||
@@ -532,127 +406,8 @@ fun CategoryDropdownWithAddCategoryPreview() {
|
|||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryDropdownOnlyListsPreview() {
|
fun CategoryDropdownWithSearchPreview() {
|
||||||
val categories = listOf(
|
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||||
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(
|
CategoryDropdownContent(
|
||||||
state = CategoryDropdownState(
|
state = CategoryDropdownState(
|
||||||
expanded = true,
|
expanded = true,
|
||||||
@@ -661,20 +416,16 @@ fun CategoryDropdownFullExpandedPreview() {
|
|||||||
TagCategory(1, "Animals"),
|
TagCategory(1, "Animals"),
|
||||||
TagCategory(2, "Food"),
|
TagCategory(2, "Food"),
|
||||||
TagCategory(3, "Travel"),
|
TagCategory(3, "Travel"),
|
||||||
TagCategory(4, "Business"),
|
TagCategory(4, "Technology"),
|
||||||
TagCategory(5, "Technology"),
|
TagCategory(5, "Sports")
|
||||||
TagCategory(6, "Sports"),
|
|
||||||
TagCategory(7, "Music"),
|
|
||||||
TagCategory(8, "Art"),
|
|
||||||
),
|
),
|
||||||
|
searchQuery = "",
|
||||||
),
|
),
|
||||||
onExpand = {},
|
onExpand = {},
|
||||||
onCategorySelected = {},
|
onCategorySelected = {},
|
||||||
onNewCategoryNameChange = {},
|
onNewCategoryNameChange = {},
|
||||||
onAddCategory = {},
|
onAddCategory = {},
|
||||||
addCategory = true,
|
enableSearch = true,
|
||||||
multipleSelectable = true,
|
|
||||||
noneSelectable = true,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
|
@file:Suppress("AssignedValueIsNeverRead")
|
||||||
|
|
||||||
package eu.gaudian.translator.view.dialogs
|
package eu.gaudian.translator.view.dialogs
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
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.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -20,16 +16,13 @@ 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.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.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
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.VocabularyStage
|
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.AppCheckbox
|
||||||
|
import eu.gaudian.translator.view.composable.AppDropdownContainer
|
||||||
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.AppOutlinedButton
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -44,46 +37,38 @@ fun VocabularyStageDropDown(
|
|||||||
var selectedStages by remember { mutableStateOf(preselectedStages) }
|
var selectedStages by remember { mutableStateOf(preselectedStages) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
Box(
|
val buttonText = when {
|
||||||
modifier = modifier,
|
|
||||||
contentAlignment = Alignment.CenterEnd
|
|
||||||
) {
|
|
||||||
AppOutlinedButton(
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
onClick = { expanded = true },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Text(text = when {
|
|
||||||
selectedStages.isEmpty() -> stringResource(R.string.label_select_stage)
|
selectedStages.isEmpty() -> stringResource(R.string.label_select_stage)
|
||||||
selectedStages.size == 1 -> selectedStages.first()?.toString(context)?:stringResource(R.string.text_none)
|
selectedStages.size == 1 -> selectedStages.first()?.toString(context) ?: stringResource(R.string.text_none)
|
||||||
else -> stringResource(R.string.stages_selected, selectedStages.size)
|
else -> stringResource(R.string.stages_selected, selectedStages.size)
|
||||||
},
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
imageVector = if (expanded) AppIcons.ArrowDropUp else AppIcons.ArrowDropDown,
|
|
||||||
contentDescription = if (expanded) stringResource(R.string.cd_collapse) else stringResource(R.string.cd_expand)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
AppDropdownContainer(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
modifier = Modifier.fillMaxWidth()
|
onExpandRequest = { expanded = true },
|
||||||
|
buttonText = buttonText,
|
||||||
|
modifier = modifier,
|
||||||
|
showDoneButton = multipleSelectable,
|
||||||
|
onDoneClick = { expanded = false }
|
||||||
) {
|
) {
|
||||||
if (noneSelectable == true) {
|
if (noneSelectable == true) {
|
||||||
val noneSelected = selectedStages.contains(null)
|
val noneSelected = selectedStages.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 = {
|
||||||
selectedStages = if (noneSelected) selectedStages.filterNotNull() else selectedStages + listOf(null)
|
selectedStages = if (noneSelected) {
|
||||||
|
selectedStages.filterNotNull()
|
||||||
|
} else {
|
||||||
|
selectedStages + listOf(null)
|
||||||
|
}
|
||||||
onStageSelected(selectedStages)
|
onStageSelected(selectedStages)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -113,12 +98,19 @@ fun VocabularyStageDropDown(
|
|||||||
val isSelected = selectedStages.contains(stage)
|
val isSelected = selectedStages.contains(stage)
|
||||||
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 = {
|
||||||
selectedStages = if (isSelected) selectedStages - stage else selectedStages + stage
|
selectedStages = if (isSelected) {
|
||||||
|
selectedStages - stage
|
||||||
|
} else {
|
||||||
|
selectedStages + stage
|
||||||
|
}
|
||||||
onStageSelected(selectedStages)
|
onStageSelected(selectedStages)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -143,19 +135,6 @@ fun VocabularyStageDropDown(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multipleSelectable) {
|
|
||||||
HorizontalDivider()
|
|
||||||
AppButton(
|
|
||||||
onClick = { expanded = false },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.label_done))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.compose.animation.expandVertically
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
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
|
||||||
@@ -20,6 +19,7 @@ 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
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@@ -32,14 +32,14 @@ import androidx.compose.material.icons.filled.ContentPaste
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
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
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -67,6 +67,8 @@ 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.AppSwitch
|
import eu.gaudian.translator.view.composable.AppSwitch
|
||||||
import eu.gaudian.translator.view.composable.DictionaryLanguageDropDown
|
import eu.gaudian.translator.view.composable.DictionaryLanguageDropDown
|
||||||
|
import eu.gaudian.translator.view.composable.DropdownDefaults
|
||||||
|
import eu.gaudian.translator.view.composable.LargeDropdownMenuItem
|
||||||
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
import eu.gaudian.translator.viewmodel.CorrectionViewModel
|
||||||
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
import eu.gaudian.translator.viewmodel.LanguageViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -415,6 +417,7 @@ fun CorrectionScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ToneDropdown(
|
private fun ToneDropdown(
|
||||||
selectedTone: CorrectionViewModel.Tone,
|
selectedTone: CorrectionViewModel.Tone,
|
||||||
@@ -447,20 +450,33 @@ private fun ToneDropdown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
if (expanded) {
|
||||||
expanded = expanded,
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
onDismissRequest = { expanded = false },
|
onDismissRequest = { expanded = false },
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
|
sheetState = sheetState,
|
||||||
|
containerColor = DropdownDefaults.containerColor()
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
Column(
|
||||||
text = { Text(text = stringResource(R.string.text_none)) },
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 8.dp) // Gives breathing room at top/bottom of list
|
||||||
|
) {
|
||||||
|
// Replaced with LargeDropdownMenuItem
|
||||||
|
LargeDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.text_none),
|
||||||
|
selected = selectedTone == CorrectionViewModel.Tone.NONE,
|
||||||
|
enabled = enabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
onToneSelected(CorrectionViewModel.Tone.NONE)
|
onToneSelected(CorrectionViewModel.Tone.NONE)
|
||||||
expanded = false
|
expanded = false
|
||||||
},
|
}
|
||||||
enabled = enabled
|
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
||||||
val options = listOf(
|
val options = listOf(
|
||||||
CorrectionViewModel.Tone.FORMAL,
|
CorrectionViewModel.Tone.FORMAL,
|
||||||
CorrectionViewModel.Tone.CASUAL,
|
CorrectionViewModel.Tone.CASUAL,
|
||||||
@@ -471,16 +487,23 @@ private fun ToneDropdown(
|
|||||||
CorrectionViewModel.Tone.ACADEMIC,
|
CorrectionViewModel.Tone.ACADEMIC,
|
||||||
CorrectionViewModel.Tone.CREATIVE
|
CorrectionViewModel.Tone.CREATIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
options.forEach { tone ->
|
options.forEach { tone ->
|
||||||
DropdownMenuItem(
|
// Replaced with LargeDropdownMenuItem
|
||||||
text = { Text(text = labelFor(tone)) },
|
LargeDropdownMenuItem(
|
||||||
|
text = labelFor(tone),
|
||||||
|
selected = selectedTone == tone,
|
||||||
|
enabled = enabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
onToneSelected(tone)
|
onToneSelected(tone)
|
||||||
expanded = false
|
expanded = false
|
||||||
},
|
}
|
||||||
enabled = enabled
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding().height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
<string name="cd_re_generate_definition">Definition neu erstellen</string>
|
<string name="cd_re_generate_definition">Definition neu erstellen</string>
|
||||||
<string name="cd_clear_search">Suche löschen</string>
|
<string name="cd_clear_search">Suche löschen</string>
|
||||||
<string name="cd_translation_history">Übersetzungsverlauf</string>
|
<string name="cd_translation_history">Übersetzungsverlauf</string>
|
||||||
<string name="label_quit_app">App beenden?</string>
|
|
||||||
<string name="label_reload">Neu laden</string>
|
<string name="label_reload">Neu laden</string>
|
||||||
<string name="title_single">Einzeln</string>
|
<string name="title_single">Einzeln</string>
|
||||||
<string name="title_widget_streak">Streak</string>
|
<string name="title_widget_streak">Streak</string>
|
||||||
@@ -210,7 +209,7 @@
|
|||||||
<string name="text_favorites">Favoriten</string>
|
<string name="text_favorites">Favoriten</string>
|
||||||
<string name="text_recent_history">Verlauf</string>
|
<string name="text_recent_history">Verlauf</string>
|
||||||
<string name="text_select_auto_recognition">Automatische Erkennung auswählen</string>
|
<string name="text_select_auto_recognition">Automatische Erkennung auswählen</string>
|
||||||
<string name="text_select_none">Keine auswählen</string>
|
<string name="text_select_no_language">Keine auswählen</string>
|
||||||
<string name="text_language_options">Sprachoptionen</string>
|
<string name="text_language_options">Sprachoptionen</string>
|
||||||
<string name="text_select_all_languages">Alle Sprachen auswählen</string>
|
<string name="text_select_all_languages">Alle Sprachen auswählen</string>
|
||||||
<string name="text_delete_custom_language">Eigene Sprache löschen</string>
|
<string name="text_delete_custom_language">Eigene Sprache löschen</string>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
<string name="cd_re_generate_definition">Gerar Definição Novamente</string>
|
<string name="cd_re_generate_definition">Gerar Definição Novamente</string>
|
||||||
<string name="cd_clear_search">Limpar pesquisa</string>
|
<string name="cd_clear_search">Limpar pesquisa</string>
|
||||||
<string name="cd_translation_history">Histórico de Tradução</string>
|
<string name="cd_translation_history">Histórico de Tradução</string>
|
||||||
<string name="label_quit_app">Fechar o aplicativo?</string>
|
|
||||||
<string name="label_reload">Recarregar</string>
|
<string name="label_reload">Recarregar</string>
|
||||||
<string name="title_single">Único</string>
|
<string name="title_single">Único</string>
|
||||||
<string name="title_widget_streak">Sequência</string>
|
<string name="title_widget_streak">Sequência</string>
|
||||||
@@ -207,7 +206,7 @@
|
|||||||
<string name="text_favorites">Favoritos</string>
|
<string name="text_favorites">Favoritos</string>
|
||||||
<string name="text_recent_history">Histórico</string>
|
<string name="text_recent_history">Histórico</string>
|
||||||
<string name="text_select_auto_recognition">Selecionar Reconhecimento Automático</string>
|
<string name="text_select_auto_recognition">Selecionar Reconhecimento Automático</string>
|
||||||
<string name="text_select_none">Não selecionar nenhum</string>
|
<string name="text_select_no_language">Não selecionar nenhum</string>
|
||||||
<string name="text_language_options">Opções de Idioma</string>
|
<string name="text_language_options">Opções de Idioma</string>
|
||||||
<string name="text_select_all_languages">Selecionar todos os idiomas</string>
|
<string name="text_select_all_languages">Selecionar todos os idiomas</string>
|
||||||
<string name="text_delete_custom_language">Excluir idioma personalizado</string>
|
<string name="text_delete_custom_language">Excluir idioma personalizado</string>
|
||||||
@@ -629,7 +628,7 @@
|
|||||||
<string name="text_paste_or_open_a_">Cole ou abra um link do YouTube para ver as legendas aqui.</string>
|
<string name="text_paste_or_open_a_">Cole ou abra um link do YouTube para ver as legendas aqui.</string>
|
||||||
<string name="text_error_2d">Erro: %1$s</string>
|
<string name="text_error_2d">Erro: %1$s</string>
|
||||||
<string name="text_repeat_wrong_guesses">Repetir Respostas Erradas</string>
|
<string name="text_repeat_wrong_guesses">Repetir Respostas Erradas</string>
|
||||||
<string name="label_language_none">Nenhuma</string>
|
<string name="label_language_none">Nenhum</string>
|
||||||
<string name="label_grammar_inflections">Flexões</string>
|
<string name="label_grammar_inflections">Flexões</string>
|
||||||
<string name="label_more">Mais</string>
|
<string name="label_more">Mais</string>
|
||||||
<string name="label_translations">Traduções</string>
|
<string name="label_translations">Traduções</string>
|
||||||
|
|||||||
@@ -316,7 +316,7 @@
|
|||||||
<string name="label_preview_second">Preview (first 5) for second column: %1$s</string>
|
<string name="label_preview_second">Preview (first 5) for second column: %1$s</string>
|
||||||
<string name="label_pronoun">Pronoun</string>
|
<string name="label_pronoun">Pronoun</string>
|
||||||
<string name="label_providers">Providers</string>
|
<string name="label_providers">Providers</string>
|
||||||
<string name="label_quit_app">Quit app?</string>
|
<string name="label_quit_app">Quit App</string>
|
||||||
<string name="label_quit_exercise_qm">Quit Exercise?</string>
|
<string name="label_quit_exercise_qm">Quit Exercise?</string>
|
||||||
<string name="label_raw_data_2d">Raw Data:</string>
|
<string name="label_raw_data_2d">Raw Data:</string>
|
||||||
<string name="label_related_words">Related Words</string>
|
<string name="label_related_words">Related Words</string>
|
||||||
@@ -884,7 +884,7 @@
|
|||||||
<string name="text_select_category">Select Category</string>
|
<string name="text_select_category">Select Category</string>
|
||||||
<string name="text_select_languages">Select Languages</string>
|
<string name="text_select_languages">Select Languages</string>
|
||||||
<string name="text_select_model">Select Model</string>
|
<string name="text_select_model">Select Model</string>
|
||||||
<string name="text_select_none">Select None</string>
|
<string name="text_select_no_language">Select None</string>
|
||||||
<string name="text_select_the_content_dictionary">Select the content to be generated for a dictionary entry.</string>
|
<string name="text_select_the_content_dictionary">Select the content to be generated for a dictionary entry.</string>
|
||||||
<string name="text_select_translations_to_add">Select Translations to Add</string>
|
<string name="text_select_translations_to_add">Select Translations to Add</string>
|
||||||
<string name="text_selected">Selected</string>
|
<string name="text_selected">Selected</string>
|
||||||
@@ -1040,4 +1040,6 @@
|
|||||||
<string name="hint_scan_hint_title">Finding the right AI model</string>
|
<string name="hint_scan_hint_title">Finding the right AI model</string>
|
||||||
<string name="hint_translate_how_it_works">How translation works</string>
|
<string name="hint_translate_how_it_works">How translation works</string>
|
||||||
<string name="label_no_category">None</string>
|
<string name="label_no_category">None</string>
|
||||||
|
<string name="text_select">Select</string>
|
||||||
|
<string name="text_search">Search</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user