From 63b20b88d0da4b2a4b48973e3bfc5d4f71677d96 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 13:54:18 +0000 Subject: [PATCH] refactor(translation): split UI from orchestration, dedupe boilerplate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure-readability refactor — no behaviour change, all 410 unit tests still pass. TranslatableRichTextViewer.kt (358 → 192 lines) - Extract the in-line LaunchedEffect block (cache check + ML Kit await + cancellation bridge + result validation + caching) into a private `suspend translateAndCache` function. The effect body is now four lines: try/catch around one call. - Add a small `ResultOrError.toTranslationConfig(content)` extension that returns a TranslationConfig only when an actual translation took place, replacing the five-condition inline if/else inside the effect. - Move TranslationMessage / LangSettingsDropdown / CheckmarkRow out to a sibling file (TranslationStatusBar.kt). They render the "Translated from X to Y" footer and don't belong in the orchestrator file. TranslationStatusBar.kt (new) - Renamed the public composable to `TranslationStatusBar` to make its role obvious. - Split the status text and the dropdown into separate private composables so each fits on screen at a glance. - Add a tiny `LangMenuItem(checked, label, onClick)` to dedupe the four `DropdownMenuItem { text = { CheckmarkRow(...) }, onClick = ... }` blocks. - Hoist `rememberDeviceLocales()` out of the dropdown body for clarity. - Cache `settings.preferenceBetween(source, target)` once per dropdown render instead of calling it twice with identical args. LanguageTranslatorService.kt - Extract the in-flight cache plumbing into a `private inline fun dedupe(key, factory)` helper. `autoTranslate` is now three lines that read top-to-bottom: pre-filter, dedupe, identifyLanguage → translateOrSkip. - Promote the inline `when` deciding whether to translate (matches translateTo, is "und", is in dontTranslateFrom) into a named `translateOrSkip` function so the policy is greppable. TranslationDictionary.kt - Add a `private inline fun Pattern.forEachMatch(text, block)` extension. The four near-identical `val matcher = …; while (matcher.find()) addUnique(matcher.group())` loops collapse to three one-liners; the URL detector loop stays explicit because it has its own filter. `inline` on dedupe and forEachMatch keeps the lambda allocations gone, so this is a zero-cost refactor at runtime. https://claude.ai/code/session_0153e2sVbAijKxinQYa6cNx5 --- .../service/lang/LanguageTranslatorService.kt | 36 ++- .../service/lang/TranslationDictionary.kt | 19 +- .../components/TranslatableRichTextViewer.kt | 241 +++--------------- .../ui/components/TranslationStatusBar.kt | 217 ++++++++++++++++ 4 files changed, 290 insertions(+), 223 deletions(-) create mode 100644 amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslationStatusBar.kt diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt index df20633ee..9f5d9f008 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt @@ -128,22 +128,34 @@ object LanguageTranslatorService { translateTo: String, ): Task { if (!TranslationDictionary.isWorthTranslating(text)) return Tasks.forCanceled() - - val key = InFlightKey(text, translateTo, dontTranslateFrom) - inFlight[key]?.let { return it } - - val task = + return dedupe(InFlightKey(text, translateTo, dontTranslateFrom)) { identifyLanguage(text).onSuccessTask(executorService) { detected -> - when { - detected == "und" -> Tasks.forCanceled() - detected.equals(translateTo, ignoreCase = true) -> Tasks.forCanceled() - detected in dontTranslateFrom -> Tasks.forCanceled() - else -> translate(text, detected, translateTo) - } + translateOrSkip(text, detected, dontTranslateFrom, translateTo) } + } + } + private fun translateOrSkip( + text: String, + detected: String, + dontTranslateFrom: Set, + translateTo: String, + ): Task = + when { + detected == "und" -> Tasks.forCanceled() + detected.equals(translateTo, ignoreCase = true) -> Tasks.forCanceled() + detected in dontTranslateFrom -> Tasks.forCanceled() + else -> translate(text, detected, translateTo) + } + + private inline fun dedupe( + key: InFlightKey, + factory: () -> Task, + ): Task { + inFlight[key]?.let { return it } + val candidate = factory() // putIfAbsent guards against a racing caller: keep the winner, drop the loser. - val winner = inFlight.putIfAbsent(key, task) ?: task + val winner = inFlight.putIfAbsent(key, candidate) ?: candidate winner.addOnCompleteListener(executorService) { inFlight.remove(key, winner) } return winner } diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/TranslationDictionary.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/TranslationDictionary.kt index 61b659ed5..5a504ebbe 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/TranslationDictionary.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/TranslationDictionary.kt @@ -70,14 +70,9 @@ internal object TranslationDictionary { dict[placeholder(counter++)] = value } - val lnMatcher = lnRegex.matcher(text) - while (lnMatcher.find()) addUnique(lnMatcher.group()) - - val tagMatcher = tagRegex.matcher(text) - while (tagMatcher.find()) addUnique(tagMatcher.group()) - - val nip08Matcher = nip08RefRegex.matcher(text) - while (nip08Matcher.find()) addUnique(nip08Matcher.group()) + lnRegex.forEachMatch(text, ::addUnique) + tagRegex.forEachMatch(text, ::addUnique) + nip08RefRegex.forEachMatch(text, ::addUnique) for (url in UrlDetector(text).detect()) { val original = url.originalUrl @@ -89,6 +84,14 @@ internal object TranslationDictionary { return dict } + private inline fun Pattern.forEachMatch( + text: String, + block: (String) -> Unit, + ) { + val matcher = matcher(text) + while (matcher.find()) block(matcher.group()) + } + fun encode( text: String, dict: Map, diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index b916543f6..004d1bdd7 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -20,17 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.components -import android.content.res.Resources import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -38,31 +28,20 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.core.os.ConfigurationCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService +import com.vitorpamplona.amethyst.service.lang.ResultOrError import com.vitorpamplona.amethyst.service.lang.TranslationsCache import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp -import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ensureActive import kotlinx.coroutines.tasks.await -import java.util.Locale +import kotlin.coroutines.coroutineContext @Composable fun TranslatableRichTextViewer( @@ -119,51 +98,13 @@ fun TranslatableRichTextViewer( } LaunchedEffect(content, translateTo, dontTranslateFrom) { - TranslationsCache.get(content, translateTo, dontTranslateFrom)?.let { - translatedTextState.value = it - return@LaunchedEffect - } - - val noOp = TranslationConfig(content, null, null) try { - val task = LanguageTranslatorService.autoTranslate(content, dontTranslateFrom, translateTo) - // ML Kit cancels the task to signal "no translation needed" (same language, "und", - // blocklisted). await() bridges that into a CancellationException; cache the no-op so - // we don't re-run language identification next time the same text scrolls into view. - val raw = - try { - task.await() - } catch (e: CancellationException) { - coroutineContext.ensureActive() - TranslationsCache.set(content, translateTo, dontTranslateFrom, noOp) - translatedTextState.value = noOp - return@LaunchedEffect - } - - coroutineContext.ensureActive() - - val translated = raw.result - val source = raw.sourceLang - val target = raw.targetLang - val newConfig = - if ( - translated != null && - source != null && - target != null && - source != target && - translated != content - ) { - TranslationConfig(translated, source, target) - } else { - noOp - } - TranslationsCache.set(content, translateTo, dontTranslateFrom, newConfig) - translatedTextState.value = newConfig + translatedTextState.value = translateAndCache(content, translateTo, dontTranslateFrom) } catch (e: CancellationException) { throw e } catch (_: Exception) { - // Network / model download / translator failure — keep showing the original. Do not - // cache: a transient failure shouldn't block future attempts on the same text. + // Transient ML Kit / network failure — keep showing the original. Do not cache: a + // one-off failure shouldn't block future attempts on the same text. } } @@ -202,7 +143,7 @@ private fun RenderTextWithTranslateOptions( displayText(toBeViewed) if (translationOccurred) { - TranslationMessage( + TranslationStatusBar( source = source, target = target, modifier = translationMessageModifier, @@ -212,146 +153,40 @@ private fun RenderTextWithTranslateOptions( } } -@Composable -private fun TranslationMessage( - source: String, - target: String, - modifier: Modifier = MaxWidthPaddingTop5dp, - accountViewModel: AccountViewModel, - onChangeWhatToShow: (Boolean) -> Unit, -) { - var langSettingsPopupExpanded by remember { mutableStateOf(false) } +/** + * Returns the translation for [content] under the current language settings, hitting the cache + * first and falling back to ML Kit. ML Kit's "no translation needed" cancellation (same language, + * undetected, blocklisted) is bridged into a no-op [TranslationConfig] that is itself cached, so + * the same text scrolling back into view doesn't re-run language identification. + */ +private suspend fun translateAndCache( + content: String, + translateTo: String, + dontTranslateFrom: Set, +): TranslationConfig { + TranslationsCache.get(content, translateTo, dontTranslateFrom)?.let { return it } - val sourceDisplay = remember(source) { Locale.forLanguageTag(source).displayName } - val targetDisplay = remember(target) { Locale.forLanguageTag(target).displayName } - val autoLabel = stringRes(R.string.translations_auto) - val translatedFromLabel = stringRes(R.string.translations_translated_from) - val toLabel = stringRes(R.string.translations_to) - - Row(modifier = modifier) { - val textColor = MaterialTheme.colorScheme.lessImportantLink - - Text( - text = - buildAnnotatedString { - appendLink(autoLabel, textColor) { langSettingsPopupExpanded = !langSettingsPopupExpanded } - append(" $translatedFromLabel ") - appendLink(sourceDisplay, textColor) { onChangeWhatToShow(true) } - append(" $toLabel ") - appendLink(targetDisplay, textColor) { onChangeWhatToShow(false) } - }, - style = - LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f), - fontSize = Font14SP, - ), - overflow = TextOverflow.Visible, - maxLines = 3, - ) - - if (langSettingsPopupExpanded) { - LangSettingsDropdown( - expanded = true, - source = source, - target = target, - sourceDisplay = sourceDisplay, - targetDisplay = targetDisplay, - accountViewModel = accountViewModel, - onDismiss = { langSettingsPopupExpanded = false }, - ) + val noOp = TranslationConfig(content, null, null) + val raw = + try { + LanguageTranslatorService.autoTranslate(content, dontTranslateFrom, translateTo).await() + } catch (e: CancellationException) { + // If our coroutine is the cancelled one, propagate; otherwise it's ML Kit signalling + // "no translation needed" — cache the no-op and return it. + coroutineContext.ensureActive() + return noOp.also { TranslationsCache.set(content, translateTo, dontTranslateFrom, it) } } - } + coroutineContext.ensureActive() + + val config = raw.toTranslationConfig(content) ?: noOp + TranslationsCache.set(content, translateTo, dontTranslateFrom, config) + return config } -@Composable -private fun LangSettingsDropdown( - expanded: Boolean, - source: String, - target: String, - sourceDisplay: String, - targetDisplay: String, - accountViewModel: AccountViewModel, - onDismiss: () -> Unit, -) { - val deviceLocales = - remember { - val list = ConfigurationCompat.getLocales(Resources.getSystem().configuration) - (0 until list.size()).mapNotNull { list.get(it) } - } - - DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { - DropdownMenuItem( - text = { - CheckmarkRow( - checked = source in accountViewModel.dontTranslateFrom(), - label = stringRes(R.string.translations_never_translate_from_lang, sourceDisplay), - ) - }, - onClick = { - accountViewModel.toggleDontTranslateFrom(source) - onDismiss() - }, - ) - HorizontalDivider(thickness = DividerThickness) - DropdownMenuItem( - text = { - CheckmarkRow( - checked = accountViewModel.account.settings.preferenceBetween(source, target) == source, - label = stringRes(R.string.translations_show_in_lang_first, sourceDisplay), - ) - }, - onClick = { - accountViewModel.prefer(source, target, source) - onDismiss() - }, - ) - DropdownMenuItem( - text = { - CheckmarkRow( - checked = accountViewModel.account.settings.preferenceBetween(source, target) == target, - label = stringRes(R.string.translations_show_in_lang_first, targetDisplay), - ) - }, - onClick = { - accountViewModel.prefer(source, target, target) - onDismiss() - }, - ) - HorizontalDivider(thickness = DividerThickness) - - for (lang in deviceLocales) { - DropdownMenuItem( - text = { - CheckmarkRow( - checked = accountViewModel.account.settings.translateToContains(lang.language), - label = stringRes(R.string.translations_always_translate_to_lang, lang.displayName), - ) - }, - onClick = { - onDismiss() - accountViewModel.updateTranslateTo(lang.language) - }, - ) - } - } -} - -@Composable -private fun CheckmarkRow( - checked: Boolean, - label: String, -) { - Row(verticalAlignment = Alignment.CenterVertically) { - if (checked) { - Icon( - symbol = MaterialSymbols.Check, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - } else { - Spacer(modifier = Modifier.size(24.dp)) - } - Spacer(modifier = Modifier.size(10.dp)) - Text(label) - } +private fun ResultOrError.toTranslationConfig(content: String): TranslationConfig? { + val translated = result ?: return null + val source = sourceLang ?: return null + val target = targetLang ?: return null + if (source == target || translated == content) return null + return TranslationConfig(translated, source, target) } diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslationStatusBar.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslationStatusBar.kt new file mode 100644 index 000000000..880c28d97 --- /dev/null +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslationStatusBar.kt @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.components + +import android.content.res.Resources +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.core.os.ConfigurationCompat +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.MaxWidthPaddingTop5dp +import com.vitorpamplona.amethyst.ui.theme.lessImportantLink +import java.util.Locale + +/** + * The "Auto-translated from X to Y" footer shown beneath translated rich text. Tapping the source + * or target labels toggles which version is displayed; tapping "Auto-translated" opens the + * per-language preferences dropdown. + */ +@Composable +internal fun TranslationStatusBar( + source: String, + target: String, + modifier: Modifier = MaxWidthPaddingTop5dp, + accountViewModel: AccountViewModel, + onShowOriginalChange: (Boolean) -> Unit, +) { + var dropdownExpanded by remember { mutableStateOf(false) } + + val sourceDisplay = remember(source) { Locale.forLanguageTag(source).displayName } + val targetDisplay = remember(target) { Locale.forLanguageTag(target).displayName } + + Row(modifier = modifier) { + TranslationStatusText( + sourceDisplay = sourceDisplay, + targetDisplay = targetDisplay, + onAutoLabelClick = { dropdownExpanded = !dropdownExpanded }, + onSourceLabelClick = { onShowOriginalChange(true) }, + onTargetLabelClick = { onShowOriginalChange(false) }, + ) + + if (dropdownExpanded) { + LangSettingsDropdown( + source = source, + target = target, + sourceDisplay = sourceDisplay, + targetDisplay = targetDisplay, + accountViewModel = accountViewModel, + onDismiss = { dropdownExpanded = false }, + ) + } + } +} + +@Composable +private fun TranslationStatusText( + sourceDisplay: String, + targetDisplay: String, + onAutoLabelClick: () -> Unit, + onSourceLabelClick: () -> Unit, + onTargetLabelClick: () -> Unit, +) { + val textColor = MaterialTheme.colorScheme.lessImportantLink + val autoLabel = stringRes(R.string.translations_auto) + val translatedFromLabel = stringRes(R.string.translations_translated_from) + val toLabel = stringRes(R.string.translations_to) + + Text( + text = + buildAnnotatedString { + appendLink(autoLabel, textColor, onAutoLabelClick) + append(" $translatedFromLabel ") + appendLink(sourceDisplay, textColor, onSourceLabelClick) + append(" $toLabel ") + appendLink(targetDisplay, textColor, onTargetLabelClick) + }, + style = + LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.32f), + fontSize = Font14SP, + ), + overflow = TextOverflow.Visible, + maxLines = 3, + ) +} + +@Composable +private fun LangSettingsDropdown( + source: String, + target: String, + sourceDisplay: String, + targetDisplay: String, + accountViewModel: AccountViewModel, + onDismiss: () -> Unit, +) { + val deviceLocales = rememberDeviceLocales() + val settings = accountViewModel.account.settings + val preferenceForPair = settings.preferenceBetween(source, target) + + DropdownMenu(expanded = true, onDismissRequest = onDismiss) { + LangMenuItem( + checked = source in accountViewModel.dontTranslateFrom(), + label = stringRes(R.string.translations_never_translate_from_lang, sourceDisplay), + onClick = { + accountViewModel.toggleDontTranslateFrom(source) + onDismiss() + }, + ) + HorizontalDivider(thickness = DividerThickness) + LangMenuItem( + checked = preferenceForPair == source, + label = stringRes(R.string.translations_show_in_lang_first, sourceDisplay), + onClick = { + accountViewModel.prefer(source, target, source) + onDismiss() + }, + ) + LangMenuItem( + checked = preferenceForPair == target, + label = stringRes(R.string.translations_show_in_lang_first, targetDisplay), + onClick = { + accountViewModel.prefer(source, target, target) + onDismiss() + }, + ) + HorizontalDivider(thickness = DividerThickness) + for (lang in deviceLocales) { + LangMenuItem( + checked = settings.translateToContains(lang.language), + label = stringRes(R.string.translations_always_translate_to_lang, lang.displayName), + onClick = { + onDismiss() + accountViewModel.updateTranslateTo(lang.language) + }, + ) + } + } +} + +@Composable +private fun rememberDeviceLocales(): List = + remember { + val list = ConfigurationCompat.getLocales(Resources.getSystem().configuration) + (0 until list.size()).mapNotNull { list.get(it) } + } + +@Composable +private fun LangMenuItem( + checked: Boolean, + label: String, + onClick: () -> Unit, +) { + DropdownMenuItem( + text = { CheckmarkRow(checked, label) }, + onClick = onClick, + ) +} + +@Composable +private fun CheckmarkRow( + checked: Boolean, + label: String, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (checked) { + Icon( + symbol = MaterialSymbols.Check, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } else { + Spacer(modifier = Modifier.size(24.dp)) + } + Spacer(modifier = Modifier.size(10.dp)) + Text(label) + } +}