Step 1 in unifying dropdowns
This commit is contained in:
@@ -1,22 +1,19 @@
|
||||
package eu.gaudian.translator.view.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -24,8 +21,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -42,7 +37,6 @@ fun ApiModelDropDown(
|
||||
onModelSelected: (LanguageModel?) -> Unit,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
LocalContext.current
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
@@ -65,13 +59,8 @@ fun ApiModelDropDown(
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
AppOutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
|
||||
enabled = enabled
|
||||
) {
|
||||
// Custom button content showing selected model and provider
|
||||
val buttonContent: @Composable () -> Unit = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@@ -101,56 +90,27 @@ fun ApiModelDropDown(
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
AppDropdownContainer(
|
||||
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 {
|
||||
Row(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.heightIn(max = 400.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Icon(
|
||||
AppIcons.Search,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
placeholder = { Text(stringResource(R.string.label_search_models)) },
|
||||
singleLine = true,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (searchQuery.isNotBlank()) {
|
||||
IconButton(
|
||||
onClick = { searchQuery = "" },
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Icon(
|
||||
AppIcons.Close,
|
||||
contentDescription = stringResource(R.string.cd_clear_search),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (filteredGroupedModels.isNotEmpty()) {
|
||||
filteredGroupedModels.entries.forEachIndexed { index, entry ->
|
||||
val providerKey = entry.key
|
||||
@@ -158,7 +118,7 @@ fun ApiModelDropDown(
|
||||
val isActive = providerStatuses[providerKey] == true
|
||||
val providerName = providerNames[providerKey] ?: providerKey
|
||||
|
||||
if (index > 0) HorizontalDivider()
|
||||
if (index > 0) HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
// Provider header
|
||||
AppDropdownMenuItem(
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
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.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
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.CompositionLocalProvider
|
||||
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.res.stringResource
|
||||
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.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -56,100 +69,362 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import eu.gaudian.translator.R
|
||||
|
||||
/**
|
||||
* A modern, custom dropdown menu composable that provides a styled text field with a dropdown list of options.
|
||||
* 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() }
|
||||
// =========================================
|
||||
// UNIFIED DROPDOWN STYLES & CONSTANTS
|
||||
// =========================================
|
||||
|
||||
Column(modifier = modifier) {
|
||||
OutlinedTextField(
|
||||
value = selectedText,
|
||||
onValueChange = {},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onGloballyPositioned { coordinates ->
|
||||
textFieldSize = coordinates.size.toSize()
|
||||
object DropdownDefaults {
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
val itemPaddingHorizontal = 8.dp
|
||||
val itemPaddingVertical = 2.dp
|
||||
|
||||
@Composable
|
||||
fun containerColor(): Color = MaterialTheme.colorScheme.surface
|
||||
|
||||
@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(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = Modifier
|
||||
.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
|
||||
// Give the menu itself a bit of breathing room
|
||||
.padding(vertical = 4.dp),
|
||||
offset = DpOffset(0.dp, 4.dp), // Slight detachment from the anchor
|
||||
scrollState = rememberScrollState(),
|
||||
properties = PopupProperties(focusable = true),
|
||||
shape = RoundedCornerShape(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()
|
||||
@Composable
|
||||
fun itemContentColor(selected: Boolean, enabled: Boolean): Color {
|
||||
return when {
|
||||
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
selected -> MaterialTheme.colorScheme.primary
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A modern and stylish composable for individual dropdown items, featuring enhanced visual design
|
||||
* with subtle shadows, rounded corners, and smooth interactions.
|
||||
* A drop-in replacement for [androidx.compose.material3.DropdownMenu] that opens
|
||||
* 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
|
||||
fun AppDropdownMenuItem(
|
||||
text: @Composable () -> Unit,
|
||||
@@ -160,23 +435,25 @@ fun AppDropdownMenuItem(
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
selected: Boolean = false,
|
||||
) {
|
||||
val contentColor = if (enabled) {
|
||||
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
}
|
||||
|
||||
// Modern "floating" highlight background
|
||||
val backgroundColor = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f) else Color.Transparent
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = DropdownDefaults.itemContentColor(selected, enabled),
|
||||
label = "contentColor"
|
||||
)
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = DropdownDefaults.itemBackground(selected),
|
||||
label = "backgroundColor"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp) // Outer padding creates the floating shape
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.padding(
|
||||
horizontal = DropdownDefaults.itemPaddingHorizontal,
|
||||
vertical = DropdownDefaults.itemPaddingVertical
|
||||
)
|
||||
.clip(DropdownDefaults.shape)
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled = enabled) { onClick() }
|
||||
//.padding(horizontal = 12.dp, vertical = 10.dp) // Inner padding keeps content comfortable
|
||||
) {
|
||||
Row(
|
||||
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
|
||||
fun <T> LargeDropdownMenu(
|
||||
@@ -210,12 +592,12 @@ fun <T> LargeDropdownMenu(
|
||||
items: List<T>,
|
||||
selectedIndex: Int = -1,
|
||||
onItemSelected: (index: Int, item: T) -> Unit,
|
||||
selectedItemToString: (T) -> String = { it.toString() },
|
||||
drawItem: @Composable (T, Boolean, Boolean, () -> Unit) -> Unit = { item, selected, itemEnabled, onClick ->
|
||||
selectedItemToString: (T) -> String = { item: T -> item.toString() },
|
||||
drawItem: @Composable (T, Boolean, Boolean, () -> Unit) -> Unit = { item: T, selected: Boolean, _: Boolean, onClick: () -> Unit ->
|
||||
LargeDropdownMenuItem(
|
||||
text = item.toString(),
|
||||
selected = selected,
|
||||
enabled = itemEnabled,
|
||||
enabled = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
},
|
||||
@@ -247,13 +629,10 @@ fun <T> LargeDropdownMenu(
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
Dialog(
|
||||
onDismissRequest = { expanded = false }, // Fixed bug from original code
|
||||
) {
|
||||
Dialog(onDismissRequest = { expanded = false }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shadowElevation = 8.dp,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 6.dp
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
@@ -263,7 +642,6 @@ fun <T> LargeDropdownMenu(
|
||||
}
|
||||
}
|
||||
|
||||
// Added vertical padding to the list instead of hard dividers
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState,
|
||||
@@ -279,7 +657,7 @@ fun <T> LargeDropdownMenu(
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(items) { index, item ->
|
||||
itemsIndexed(items) { index: Int, item: T ->
|
||||
val selectedItem = index == selectedIndex
|
||||
drawItem(
|
||||
item,
|
||||
@@ -296,39 +674,7 @@ fun <T> LargeDropdownMenu(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ============== PREVIEWS ==============
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@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")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
@@ -365,53 +735,8 @@ fun LargeDropdownMenuPreview() {
|
||||
label = "Select Option",
|
||||
items = options,
|
||||
selectedIndex = selectedIndex,
|
||||
onItemSelected = { index, _ ->
|
||||
onItemSelected = { index: Int, _: String ->
|
||||
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
|
||||
|
||||
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.Spacer
|
||||
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.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
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.collectAsState
|
||||
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.font.FontFamily
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -82,7 +83,10 @@ fun BaseLanguageDropDown(
|
||||
else -> stringResource(R.string.label_language_none)
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
AppOutlinedButton(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
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
|
||||
searchText = ""
|
||||
tempSelection = emptyList() // Also reset temp selection on dismiss
|
||||
}) {
|
||||
// Helper composable for a single language row in multiple selection mode
|
||||
tempSelection = emptyList()
|
||||
},
|
||||
sheetState = sheetState
|
||||
) {
|
||||
@Composable
|
||||
fun MultiSelectItem(language: Language) {
|
||||
val isSelected = tempSelection.contains(language)
|
||||
@@ -120,7 +129,6 @@ fun BaseLanguageDropDown(
|
||||
checked = isSelected,
|
||||
onCheckedChange = {
|
||||
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
selectedLanguagesCount = tempSelection.size
|
||||
onLanguagesSelected(tempSelection)
|
||||
}
|
||||
@@ -141,13 +149,11 @@ fun BaseLanguageDropDown(
|
||||
},
|
||||
onClick = {
|
||||
tempSelection = if (isSelected) tempSelection - language else tempSelection + language
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
selectedLanguagesCount = tempSelection.size
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Helper composable for a single language row in single selection mode
|
||||
@Composable
|
||||
fun SingleSelectItem(language: Language) {
|
||||
val languageNames = languages.map { it.name }
|
||||
@@ -197,43 +203,22 @@ fun BaseLanguageDropDown(
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// --- Main Dropdown Content ---
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.heightIn(max = 900.dp) // Constrain the height
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Search bar with a back arrow
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = { expanded = false; searchText = "" }) {
|
||||
Icon(AppIcons.ArrowBack, contentDescription = stringResource(R.string.label_close))
|
||||
}
|
||||
TextField(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
singleLine = true,
|
||||
DropdownSearchField(
|
||||
searchQuery = searchText,
|
||||
onSearchQueryChange = { searchText = it },
|
||||
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()
|
||||
|
||||
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
// Replaced height(max = 900.dp) with standard weight logic to allow proper scrolling bounds
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val isSearching = searchText.isNotBlank()
|
||||
|
||||
if (isSearching) {
|
||||
@@ -255,80 +240,91 @@ fun BaseLanguageDropDown(
|
||||
} else if (alternateLanguages.isNotEmpty()) {
|
||||
val sortedAlternate = alternateLanguages.sortedBy { it.name }
|
||||
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) }
|
||||
} 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) }
|
||||
}
|
||||
|
||||
} else {
|
||||
if (enableMultipleSelection) {
|
||||
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) }
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
val recentHistoryFiltered = languageHistory.filter { it !in favoriteLanguages }.takeLast(5)
|
||||
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) }
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
val remainingLanguages = languages.sortedBy { it.name }
|
||||
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) }
|
||||
}
|
||||
} else {
|
||||
// Logic for single selection default view
|
||||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
if (favoriteLanguages.any {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
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 {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
it.code != "none" && it.code != "auto"
|
||||
}.forEach { language -> SingleSelectItem(language) }
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
val recentHistoryFiltered = languageHistory.filter {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
it !in favoriteLanguages && it.code != "none" && it.code != "auto"
|
||||
}.takeLast(5)
|
||||
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) }
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
val remainingLanguages = languages.filter {
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
it !in favoriteLanguages && it !in recentHistoryFiltered && it.code != "none" && it.code != "auto"
|
||||
}.sortedBy { it.name }
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Done button for multiple selection mode
|
||||
if (enableMultipleSelection) {
|
||||
HorizontalDivider()
|
||||
AppButton(
|
||||
onClick = {
|
||||
onLanguagesSelected(tempSelection)
|
||||
@Suppress("AssignedValueIsNeverRead")
|
||||
selectedLanguagesCount = tempSelection.size
|
||||
expanded = false
|
||||
searchText = ""
|
||||
@@ -340,6 +336,10 @@ fun BaseLanguageDropDown(
|
||||
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.LanguageViewModel
|
||||
|
||||
|
||||
enum class DialogCategoryType { TAG, FILTER }
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
@file:Suppress("AssignedValueIsNeverRead", "HardCodedStringLiteral")
|
||||
|
||||
package eu.gaudian.translator.view.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -24,24 +23,20 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.TagCategory
|
||||
import eu.gaudian.translator.model.VocabularyCategory
|
||||
import eu.gaudian.translator.model.VocabularyFilter
|
||||
import eu.gaudian.translator.ui.theme.ThemePreviews
|
||||
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.AppDropdownContainer
|
||||
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedTextField
|
||||
import eu.gaudian.translator.view.composable.DropdownHeader
|
||||
import eu.gaudian.translator.viewmodel.CategoryViewModel
|
||||
|
||||
|
||||
@@ -54,22 +49,12 @@ data class CategoryDropdownState(
|
||||
val selectedCategories: List<VocabularyCategory?> = emptyList(),
|
||||
val newCategoryName: String = "",
|
||||
val categories: List<VocabularyCategory> = emptyList(),
|
||||
val searchQuery: String = "",
|
||||
)
|
||||
|
||||
/**
|
||||
* Stateless dropdown content composable for category selection.
|
||||
* This component is fully controlled by its parameters and does not maintain any internal state.
|
||||
*
|
||||
* @param state The current state of the dropdown
|
||||
* @param onExpand Callback when the dropdown should expand/collapse
|
||||
* @param onCategorySelected Callback when a category is selected
|
||||
* @param onNewCategoryNameChange Callback when the new category name changes
|
||||
* @param onAddCategory Callback when a new category should be added
|
||||
* @param noneSelectable Whether "None" option is selectable
|
||||
* @param multipleSelectable Whether multiple categories can be selected
|
||||
* @param onlyLists Whether to show only list/category types
|
||||
* @param addCategory Whether to show the "Add Category" option
|
||||
* @param modifier Modifier for the composable
|
||||
*/
|
||||
@Composable
|
||||
fun CategoryDropdownContent(
|
||||
@@ -79,10 +64,12 @@ fun CategoryDropdownContent(
|
||||
onCategorySelected: (List<VocabularyCategory?>) -> Unit,
|
||||
onNewCategoryNameChange: (String) -> Unit,
|
||||
onAddCategory: (String) -> Unit,
|
||||
onSearchQueryChange: (String) -> Unit = {},
|
||||
noneSelectable: Boolean = true,
|
||||
multipleSelectable: Boolean = false,
|
||||
onlyLists: Boolean = false,
|
||||
addCategory: Boolean = false,
|
||||
enableSearch: Boolean = false,
|
||||
) {
|
||||
val selectableCategories = if (onlyLists) {
|
||||
state.categories.filterIsInstance<TagCategory>()
|
||||
@@ -90,37 +77,34 @@ fun CategoryDropdownContent(
|
||||
state.categories
|
||||
}
|
||||
|
||||
AppOutlinedButton(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
onClick = { onExpand(true) },
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = when {
|
||||
// Filter categories by search query if search is enabled
|
||||
val filteredCategories = if (enableSearch && state.searchQuery.isNotBlank()) {
|
||||
selectableCategories.filter { category ->
|
||||
category.name.contains(state.searchQuery, ignoreCase = true)
|
||||
}
|
||||
} else {
|
||||
selectableCategories
|
||||
}
|
||||
|
||||
val buttonText = when {
|
||||
state.selectedCategories.isEmpty() -> stringResource(R.string.text_select_category)
|
||||
state.selectedCategories.size == 1 -> state.selectedCategories.first()?.name
|
||||
?: stringResource(R.string.label_no_category)
|
||||
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,
|
||||
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) {
|
||||
val noneSelected = state.selectedCategories.contains(null)
|
||||
@@ -133,7 +117,7 @@ fun CategoryDropdownContent(
|
||||
if (multipleSelectable) {
|
||||
AppCheckbox(
|
||||
checked = noneSelected,
|
||||
onCheckedChange = { isChecked ->
|
||||
onCheckedChange = { _ ->
|
||||
val newSelection = if (noneSelected) {
|
||||
state.selectedCategories.filterNotNull()
|
||||
} else {
|
||||
@@ -144,7 +128,10 @@ fun CategoryDropdownContent(
|
||||
)
|
||||
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 = {
|
||||
@@ -163,7 +150,7 @@ fun CategoryDropdownContent(
|
||||
)
|
||||
}
|
||||
|
||||
selectableCategories.forEach { category ->
|
||||
filteredCategories.forEach { category ->
|
||||
val isSelected = state.selectedCategories.contains(category)
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
@@ -185,7 +172,10 @@ fun CategoryDropdownContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Text(category.name)
|
||||
Text(
|
||||
text = category.name,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
@@ -204,16 +194,24 @@ fun CategoryDropdownContent(
|
||||
)
|
||||
}
|
||||
|
||||
if (addCategory) {
|
||||
HorizontalDivider()
|
||||
|
||||
if (enableSearch && state.searchQuery.isNotBlank() && filteredCategories.isEmpty()) {
|
||||
AppDropdownMenuItem(
|
||||
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 = {},
|
||||
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(
|
||||
text = {
|
||||
@@ -227,7 +225,7 @@ fun CategoryDropdownContent(
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (state.newCategoryName.isNotBlank()) {
|
||||
@@ -246,32 +244,11 @@ fun CategoryDropdownContent(
|
||||
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.
|
||||
* This is the main composable that should be used in production code.
|
||||
*
|
||||
* @param initialCategoryId The initial category ID to select
|
||||
* @param onCategorySelected Callback when categories are selected
|
||||
* @param noneSelectable Whether "None" option is selectable
|
||||
* @param multipleSelectable Whether multiple categories can be selected
|
||||
* @param onlyLists Whether to show only list/category types
|
||||
* @param addCategory Whether to show the "Add Category" option
|
||||
* @param modifier Modifier for the composable
|
||||
*/
|
||||
@Composable
|
||||
fun CategoryDropdown(
|
||||
@@ -282,26 +259,23 @@ fun CategoryDropdown(
|
||||
multipleSelectable: Boolean = false,
|
||||
onlyLists: Boolean = false,
|
||||
addCategory: Boolean = false,
|
||||
enableSearch: Boolean = false,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var selectedCategories by remember {
|
||||
mutableStateOf<List<VocabularyCategory?>>(emptyList())
|
||||
}
|
||||
var newCategoryName by remember { mutableStateOf("") }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
val activity = LocalContext.current.findActivity()
|
||||
val categoryViewModel: CategoryViewModel = hiltViewModel(viewModelStoreOwner = activity)
|
||||
val categories by categoryViewModel.categories.collectAsState(initial = emptyList())
|
||||
|
||||
|
||||
|
||||
|
||||
// Find initial category
|
||||
val initialCategory = remember(categories, initialCategoryId) {
|
||||
categories.find { it.id == initialCategoryId }
|
||||
}
|
||||
|
||||
// Initialize selection with initial category if provided
|
||||
remember(initialCategory) {
|
||||
if (initialCategory != null && selectedCategories.isEmpty()) {
|
||||
selectedCategories = listOf(initialCategory)
|
||||
@@ -315,6 +289,7 @@ fun CategoryDropdown(
|
||||
selectedCategories = selectedCategories,
|
||||
newCategoryName = newCategoryName,
|
||||
categories = categories,
|
||||
searchQuery = searchQuery,
|
||||
),
|
||||
onExpand = { isExpanded -> expanded = isExpanded },
|
||||
onCategorySelected = { newSelection ->
|
||||
@@ -324,115 +299,35 @@ fun CategoryDropdown(
|
||||
onNewCategoryNameChange = { newCategoryName = it },
|
||||
onAddCategory = { name ->
|
||||
val newCategory = TagCategory(id = 0, name = name)
|
||||
// In production, this would call ViewModel.createCategory(newCategory)
|
||||
newCategoryName = ""
|
||||
categoryViewModel.createCategory(newCategory)
|
||||
//selectedCategories = selectedCategories + newCategory
|
||||
if (!multipleSelectable) {
|
||||
expanded = false
|
||||
}
|
||||
},
|
||||
onSearchQueryChange = { searchQuery = it },
|
||||
noneSelectable = noneSelectable == true,
|
||||
multipleSelectable = multipleSelectable,
|
||||
onlyLists = onlyLists,
|
||||
addCategory = addCategory,
|
||||
enableSearch = enableSearch,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
// ============== PREVIEWS ==============
|
||||
|
||||
/**
|
||||
* Preview provider for CategoryDropdownState
|
||||
*/
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
class CategoryDropdownStateProvider : PreviewParameterProvider<CategoryDropdownState> {
|
||||
override val values = sequenceOf(
|
||||
// Collapsed state - nothing selected
|
||||
CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = emptyList(),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
VocabularyFilter(3, "Filters", languages = listOf(1, 2)),
|
||||
)
|
||||
),
|
||||
// Collapsed state - one category selected
|
||||
CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = listOf(TagCategory(1, "Animals")),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
)
|
||||
),
|
||||
// Collapsed state - multiple categories selected
|
||||
CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(3, "Travel"),
|
||||
),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
)
|
||||
),
|
||||
// Expanded state - nothing selected
|
||||
CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
)
|
||||
),
|
||||
// Expanded state - one selected
|
||||
CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = listOf(TagCategory(2, "Food")),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
)
|
||||
),
|
||||
// With "None" option selected
|
||||
CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = listOf(null),
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
)
|
||||
),
|
||||
// With add category option
|
||||
CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
newCategoryName = "New Cat",
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownCollapsedPreview(
|
||||
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
fun CategoryDropdownCollapsedPreview() {
|
||||
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||
CategoryDropdownContent(
|
||||
state = state.copy(expanded = false),
|
||||
state = CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = emptyList(),
|
||||
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food")),
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
@@ -444,15 +339,14 @@ fun CategoryDropdownCollapsedPreview(
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownExpandedPreview(
|
||||
@PreviewParameter(CategoryDropdownStateProvider::class) state: CategoryDropdownState
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
fun CategoryDropdownExpandedPreview() {
|
||||
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||
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 = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
@@ -466,21 +360,10 @@ fun CategoryDropdownExpandedPreview(
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownMultipleSelectionPreview() {
|
||||
val categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
TagCategory(4, "Business"),
|
||||
TagCategory(5, "Technology"),
|
||||
)
|
||||
var selectedCategories by remember {
|
||||
mutableStateOf<List<VocabularyCategory?>>(listOf(categories[0], categories[2]))
|
||||
}
|
||||
val categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food"), TagCategory(3, "Travel"))
|
||||
var selectedCategories by remember { mutableStateOf<List<VocabularyCategory?>>(listOf(categories[0], categories[2])) }
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
@@ -502,26 +385,17 @@ fun CategoryDropdownMultipleSelectionPreview() {
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownWithAddCategoryPreview() {
|
||||
val categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
)
|
||||
var newCategoryName by remember { mutableStateOf("New Category") }
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
newCategoryName = newCategoryName,
|
||||
categories = categories,
|
||||
newCategoryName = "New Cat",
|
||||
categories = listOf(TagCategory(1, "Animals"), TagCategory(2, "Food")),
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = { newCategoryName = it },
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
addCategory = true,
|
||||
)
|
||||
@@ -532,127 +406,8 @@ fun CategoryDropdownWithAddCategoryPreview() {
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownOnlyListsPreview() {
|
||||
val categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
VocabularyFilter(3, "Language Pair EN-DE", languages = listOf(1, 2)),
|
||||
TagCategory(4, "Travel"),
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
onlyLists = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownNoNoneOptionPreview() {
|
||||
val categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
selectedCategories = emptyList(),
|
||||
categories = categories,
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
noneSelectable = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownEmptyPreview() {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = false,
|
||||
selectedCategories = emptyList(),
|
||||
categories = emptyList(),
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownStatefulPreview() {
|
||||
var expanded by remember { mutableStateOf(true) }
|
||||
var selectedCategories by remember {
|
||||
mutableStateOf<List<VocabularyCategory?>>(listOf(TagCategory(1, "Animals")))
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = expanded,
|
||||
selectedCategories = selectedCategories,
|
||||
categories = listOf(
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
),
|
||||
),
|
||||
onExpand = { expanded = it },
|
||||
onCategorySelected = { selectedCategories = it },
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
multipleSelectable = true,
|
||||
noneSelectable = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
@ThemePreviews
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CategoryDropdownFullExpandedPreview() {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
fun CategoryDropdownWithSearchPreview() {
|
||||
Surface(modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.background) {
|
||||
CategoryDropdownContent(
|
||||
state = CategoryDropdownState(
|
||||
expanded = true,
|
||||
@@ -661,20 +416,16 @@ fun CategoryDropdownFullExpandedPreview() {
|
||||
TagCategory(1, "Animals"),
|
||||
TagCategory(2, "Food"),
|
||||
TagCategory(3, "Travel"),
|
||||
TagCategory(4, "Business"),
|
||||
TagCategory(5, "Technology"),
|
||||
TagCategory(6, "Sports"),
|
||||
TagCategory(7, "Music"),
|
||||
TagCategory(8, "Art"),
|
||||
TagCategory(4, "Technology"),
|
||||
TagCategory(5, "Sports")
|
||||
),
|
||||
searchQuery = "",
|
||||
),
|
||||
onExpand = {},
|
||||
onCategorySelected = {},
|
||||
onNewCategoryNameChange = {},
|
||||
onAddCategory = {},
|
||||
addCategory = true,
|
||||
multipleSelectable = true,
|
||||
noneSelectable = true,
|
||||
enableSearch = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
@file:Suppress("AssignedValueIsNeverRead")
|
||||
|
||||
package eu.gaudian.translator.view.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.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.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -20,16 +16,13 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.gaudian.translator.R
|
||||
import eu.gaudian.translator.model.VocabularyStage
|
||||
import eu.gaudian.translator.view.composable.AppButton
|
||||
import eu.gaudian.translator.view.composable.AppCheckbox
|
||||
import eu.gaudian.translator.view.composable.AppDropdownContainer
|
||||
import eu.gaudian.translator.view.composable.AppDropdownMenuItem
|
||||
import eu.gaudian.translator.view.composable.AppIcons
|
||||
import eu.gaudian.translator.view.composable.AppOutlinedButton
|
||||
|
||||
|
||||
@Composable
|
||||
@@ -44,46 +37,38 @@ fun VocabularyStageDropDown(
|
||||
var selectedStages by remember { mutableStateOf(preselectedStages) }
|
||||
val context = LocalContext.current
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
AppOutlinedButton(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = when {
|
||||
val buttonText = when {
|
||||
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)
|
||||
},
|
||||
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,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
onExpandRequest = { expanded = true },
|
||||
buttonText = buttonText,
|
||||
modifier = modifier,
|
||||
showDoneButton = multipleSelectable,
|
||||
onDoneClick = { expanded = false }
|
||||
) {
|
||||
if (noneSelectable == true) {
|
||||
val noneSelected = selectedStages.contains(null)
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (multipleSelectable) {
|
||||
AppCheckbox(
|
||||
checked = noneSelected,
|
||||
onCheckedChange = {
|
||||
selectedStages = if (noneSelected) selectedStages.filterNotNull() else selectedStages + listOf(null)
|
||||
selectedStages = if (noneSelected) {
|
||||
selectedStages.filterNotNull()
|
||||
} else {
|
||||
selectedStages + listOf(null)
|
||||
}
|
||||
onStageSelected(selectedStages)
|
||||
}
|
||||
)
|
||||
@@ -113,12 +98,19 @@ fun VocabularyStageDropDown(
|
||||
val isSelected = selectedStages.contains(stage)
|
||||
AppDropdownMenuItem(
|
||||
text = {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (multipleSelectable) {
|
||||
AppCheckbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = {
|
||||
selectedStages = if (isSelected) selectedStages - stage else selectedStages + stage
|
||||
selectedStages = if (isSelected) {
|
||||
selectedStages - stage
|
||||
} else {
|
||||
selectedStages + stage
|
||||
}
|
||||
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.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
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.AppSwitch
|
||||
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.LanguageViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -415,6 +417,7 @@ fun CorrectionScreenContent(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ToneDropdown(
|
||||
selectedTone: CorrectionViewModel.Tone,
|
||||
@@ -447,20 +450,33 @@ private fun ToneDropdown(
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
if (expanded) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
|
||||
sheetState = sheetState,
|
||||
containerColor = DropdownDefaults.containerColor()
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.text_none)) },
|
||||
Column(
|
||||
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 = {
|
||||
onToneSelected(CorrectionViewModel.Tone.NONE)
|
||||
expanded = false
|
||||
},
|
||||
enabled = enabled
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
val options = listOf(
|
||||
CorrectionViewModel.Tone.FORMAL,
|
||||
CorrectionViewModel.Tone.CASUAL,
|
||||
@@ -471,16 +487,23 @@ private fun ToneDropdown(
|
||||
CorrectionViewModel.Tone.ACADEMIC,
|
||||
CorrectionViewModel.Tone.CREATIVE
|
||||
)
|
||||
|
||||
options.forEach { tone ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = labelFor(tone)) },
|
||||
// Replaced with LargeDropdownMenuItem
|
||||
LargeDropdownMenuItem(
|
||||
text = labelFor(tone),
|
||||
selected = selectedTone == tone,
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
onToneSelected(tone)
|
||||
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_clear_search">Suche löschen</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="title_single">Einzeln</string>
|
||||
<string name="title_widget_streak">Streak</string>
|
||||
@@ -210,7 +209,7 @@
|
||||
<string name="text_favorites">Favoriten</string>
|
||||
<string name="text_recent_history">Verlauf</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_select_all_languages">Alle Sprachen auswählen</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_clear_search">Limpar pesquisa</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="title_single">Único</string>
|
||||
<string name="title_widget_streak">Sequência</string>
|
||||
@@ -207,7 +206,7 @@
|
||||
<string name="text_favorites">Favoritos</string>
|
||||
<string name="text_recent_history">Histórico</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_select_all_languages">Selecionar todos os idiomas</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_error_2d">Erro: %1$s</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_more">Mais</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_pronoun">Pronoun</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_raw_data_2d">Raw Data:</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_languages">Select Languages</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_translations_to_add">Select Translations to Add</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_translate_how_it_works">How translation works</string>
|
||||
<string name="label_no_category">None</string>
|
||||
<string name="text_select">Select</string>
|
||||
<string name="text_search">Search</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user