diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt index 209d3f922e..f248524cbc 100644 --- a/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt @@ -135,14 +135,18 @@ internal fun TranslationsDialog( onSettingClicked: () -> Unit, onLearnMoreClicked: () -> Unit, onTranslateButtonClick: () -> Unit, + onNotNowButtonClick: () -> Unit, ) { TranslationsDialogBottomSheet( learnMoreUrl = learnMoreUrl, showFirstTimeTranslation = showFirstTimeTranslation, translationError = translationError, + translateFromLanguages = getTranslateFromLanguageList(), + translateToLanguages = getTranslateToLanguageList(), onSettingClicked = onSettingClicked, onLearnMoreClicked = onLearnMoreClicked, - onTranslateButtonClick = onTranslateButtonClick, + onTranslateButtonClicked = onTranslateButtonClick, + onNotNowButtonClicked = onNotNowButtonClick, ) } diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt index 83c0a71b8c..ce75ba7016 100644 --- a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.translations import android.content.res.Configuration +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,6 +14,7 @@ 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.requiredSizeIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize @@ -30,19 +32,27 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import mozilla.components.concept.engine.translate.Language import mozilla.components.concept.engine.translate.TranslationError import org.mozilla.fenix.R import org.mozilla.fenix.compose.BetaLabel +import org.mozilla.fenix.compose.ContextualMenu import org.mozilla.fenix.compose.LinkText import org.mozilla.fenix.compose.LinkTextState +import org.mozilla.fenix.compose.MenuItem import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.button.PrimaryButton import org.mozilla.fenix.compose.button.TertiaryButton @@ -50,18 +60,35 @@ import org.mozilla.fenix.compose.button.TextButton import org.mozilla.fenix.shopping.ui.ReviewQualityCheckInfoCard import org.mozilla.fenix.shopping.ui.ReviewQualityCheckInfoType import org.mozilla.fenix.theme.FirefoxTheme +import java.util.Locale + +private val ICON_SIZE = 24.dp /** * Firefox Translations bottom sheet dialog. + * + * @param learnMoreUrl The learn more link for translations website. + * @param showFirstTimeTranslation Whether translations first flow should be shown. + * @param translationError The type of translation errors that can occur. + * @param translateFromLanguages Translation menu items to be shown in the translate from dropdown. + * @param translateToLanguages Translation menu items are to be shown in the translate to dropdown. + * @param onSettingClicked Invoked when the user clicks on the settings button. + * @param onLearnMoreClicked Invoked when the user clicks on the "Learn More" button. + * @param onTranslateButtonClicked Invoked when the user clicks on the "Translate" button. + * @param onNotNowButtonClicked Invoked when the user clicks on the "Not Now" button. */ @Composable +@Suppress("LongParameterList") fun TranslationsDialogBottomSheet( learnMoreUrl: String, showFirstTimeTranslation: Boolean, translationError: TranslationError? = null, + translateFromLanguages: List, + translateToLanguages: List, onSettingClicked: () -> Unit, onLearnMoreClicked: () -> Unit, - onTranslateButtonClick: () -> Unit, + onTranslateButtonClicked: () -> Unit, + onNotNowButtonClicked: () -> Unit, ) { var orientation by remember { mutableIntStateOf(Configuration.ORIENTATION_PORTRAIT) } @@ -80,12 +107,18 @@ fun TranslationsDialogBottomSheet( .clearAndSetSemantics {}, ) - TranslationsDialogHeader(onSettingClicked, showFirstTimeTranslation) + TranslationsDialogHeader( + showFirstTimeTranslation = showFirstTimeTranslation, + onSettingClicked = onSettingClicked, + ) Spacer(modifier = Modifier.height(8.dp)) if (showFirstTimeTranslation) { - TranslationsDialogInfoMessage(onLearnMoreClicked, learnMoreUrl) + TranslationsDialogInfoMessage( + learnMoreUrl = learnMoreUrl, + onLearnMoreClicked = onLearnMoreClicked, + ) } translationError?.let { @@ -97,11 +130,17 @@ fun TranslationsDialogBottomSheet( if (translationError !is TranslationError.CouldNotLoadLanguagesError) { when (orientation) { Configuration.ORIENTATION_LANDSCAPE -> { - TranslationsDropdownsInLandscapeMode() + TranslationsDialogContentInLandscapeMode( + translateFromLanguages = translateFromLanguages, + translateToLanguages = translateToLanguages, + ) } else -> { - TranslationsDropdownsInPortraitMode() + TranslationsDialogContentInPortraitMode( + translateFromLanguages = translateFromLanguages, + translateToLanguages = translateToLanguages, + ) } } @@ -109,40 +148,56 @@ fun TranslationsDialogBottomSheet( } TranslationsDialogActionButtons( - onTranslateButtonClick = onTranslateButtonClick, translationError = translationError, + onTranslateButtonClicked = onTranslateButtonClicked, + onNotNowButtonClicked = onNotNowButtonClicked, ) } } @Composable -private fun TranslationsDropdownsInPortraitMode() { +private fun TranslationsDialogContentInPortraitMode( + translateFromLanguages: List, + translateToLanguages: List, +) { Column { TranslationsDropdown( header = stringResource(id = R.string.translations_bottom_sheet_translate_from), + modifier = Modifier.fillMaxWidth(), + translateLanguages = translateFromLanguages, ) Spacer(modifier = Modifier.height(16.dp)) TranslationsDropdown( header = stringResource(id = R.string.translations_bottom_sheet_translate_to), + modifier = Modifier.fillMaxWidth(), + translateLanguages = translateToLanguages, ) } } @Composable -private fun TranslationsDropdownsInLandscapeMode() { +private fun TranslationsDialogContentInLandscapeMode( + translateFromLanguages: List, + translateToLanguages: List, +) { Column { Row { TranslationsDropdown( - modifier = Modifier.weight(1f), header = stringResource(id = R.string.translations_bottom_sheet_translate_from), + modifier = Modifier.weight(1f), + isInLandscapeMode = true, + translateLanguages = translateFromLanguages, ) + Spacer(modifier = Modifier.width(16.dp)) TranslationsDropdown( - modifier = Modifier.weight(1f), header = stringResource(id = R.string.translations_bottom_sheet_translate_to), + modifier = Modifier.weight(1f), + isInLandscapeMode = true, + translateLanguages = translateToLanguages, ) } } @@ -150,8 +205,8 @@ private fun TranslationsDropdownsInLandscapeMode() { @Composable private fun TranslationsDialogHeader( - onSettingClicked: () -> Unit, showFirstTimeTranslation: Boolean, + onSettingClicked: () -> Unit, ) { val title: String = if (showFirstTimeTranslation) { stringResource( @@ -236,11 +291,12 @@ private fun TranslationErrorWarning(translationError: TranslationError) { @Composable private fun TranslationsDialogInfoMessage( - onLearnMoreClicked: () -> Unit, learnMoreUrl: String, + onLearnMoreClicked: () -> Unit, ) { val learnMoreText = stringResource(id = R.string.translations_bottom_sheet_info_message_learn_more) + val learnMoreState = LinkTextState( text = learnMoreText, url = learnMoreUrl, @@ -265,40 +321,119 @@ private fun TranslationsDialogInfoMessage( @Composable private fun TranslationsDropdown( header: String, + translateLanguages: List, modifier: Modifier = Modifier, + isInLandscapeMode: Boolean = false, ) { - Column(modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { + val density = LocalDensity.current + + var expanded by remember { mutableStateOf(false) } + + var selectedLanguage by remember { + mutableStateOf( + translateLanguages.last().localizedDisplayName, + ) + } + + var contextMenuWidthDp by remember { + mutableStateOf(0.dp) + } + + Column( + modifier = modifier + .clickable { + expanded = true + } + .semantics { role = Role.DropdownList }, + ) { Text( text = header, + modifier = Modifier.wrapContentSize(), color = FirefoxTheme.colors.textPrimary, style = FirefoxTheme.typography.caption, ) + Spacer(modifier = Modifier.height(4.dp)) + Row { - Text( - text = "English", - modifier = Modifier.weight(1f), - color = FirefoxTheme.colors.textPrimary, - style = FirefoxTheme.typography.subtitle1, - ) + selectedLanguage?.let { + Text( + text = it, + modifier = Modifier.weight(1f), + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.subtitle1, + ) + } Spacer(modifier = Modifier.width(10.dp)) - Icon( - painter = painterResource(id = R.drawable.mozac_ic_dropdown_arrow), - contentDescription = null, - tint = FirefoxTheme.colors.iconPrimary, - ) + Box { + Icon( + painter = painterResource(id = R.drawable.mozac_ic_dropdown_arrow), + contentDescription = null, + tint = FirefoxTheme.colors.iconPrimary, + ) + + ContextualMenu( + showMenu = expanded, + onDismissRequest = { + expanded = false + }, + menuItems = getContextMenuItems(translateLanguages = translateLanguages) { + expanded = false + selectedLanguage = it.localizedDisplayName + }, + modifier = Modifier + .onGloballyPositioned { coordinates -> + contextMenuWidthDp = with(density) { + coordinates.size.width.toDp() + } + } + .requiredSizeIn(maxHeight = 200.dp), + offset = if (isInLandscapeMode) { + DpOffset( + -contextMenuWidthDp + ICON_SIZE, + -ICON_SIZE, + ) + } else { + DpOffset( + 0.dp, + -ICON_SIZE, + ) + }, + ) + } } Divider(color = FirefoxTheme.colors.formDefault) } } +private fun getContextMenuItems( + translateLanguages: List, + onClickItem: (Language) -> Unit, +): List { + val menuItems = mutableListOf() + translateLanguages.map { item -> + item.localizedDisplayName?.let { + menuItems.add( + MenuItem( + title = it, + onClick = { + onClickItem(item) + }, + ), + ) + } + } + return menuItems +} + @Composable private fun TranslationsDialogActionButtons( - onTranslateButtonClick: () -> Unit, translationError: TranslationError? = null, + onTranslateButtonClicked: () -> Unit, + onNotNowButtonClicked: () -> Unit, ) { val isTranslationInProgress = remember { mutableStateOf(false) } @@ -317,8 +452,9 @@ private fun TranslationsDialogActionButtons( TextButton( text = negativeButtonTitle, modifier = Modifier, - onClick = {}, + onClick = onNotNowButtonClicked, ) + Spacer(modifier = Modifier.width(10.dp)) if (isTranslationInProgress.value) { @@ -344,7 +480,7 @@ private fun TranslationsDialogActionButtons( modifier = Modifier.wrapContentSize(), ) { isTranslationInProgress.value = true - onTranslateButtonClick() + onTranslateButtonClicked() } } else { PrimaryButton( @@ -352,7 +488,7 @@ private fun TranslationsDialogActionButtons( modifier = Modifier.wrapContentSize(), ) { isTranslationInProgress.value = true - onTranslateButtonClick() + onTranslateButtonClicked() } } } @@ -367,9 +503,78 @@ private fun TranslationsDialogBottomSheetPreview() { learnMoreUrl = "", showFirstTimeTranslation = true, translationError = TranslationError.LanguageNotSupportedError(null), + translateFromLanguages = getTranslateFromLanguageList(), + translateToLanguages = getTranslateToLanguageList(), onSettingClicked = {}, onLearnMoreClicked = {}, - onTranslateButtonClick = {}, + onTranslateButtonClicked = {}, + onNotNowButtonClicked = {}, + ) + } +} + +@Composable +internal fun getTranslateFromLanguageList(): List { + return mutableListOf().apply { + add( + Language( + code = Locale.CHINA.toLanguageTag(), + localizedDisplayName = Locale.CHINA.displayLanguage, + ), + ) + add( + Language( + code = Locale.ENGLISH.toLanguageTag(), + localizedDisplayName = Locale.ENGLISH.displayLanguage, + ), + ) + add( + Language( + code = Locale.GERMAN.toLanguageTag(), + localizedDisplayName = Locale.GERMAN.displayLanguage, + ), + ) + add( + Language( + code = Locale.JAPANESE.toLanguageTag(), + localizedDisplayName = Locale.JAPANESE.displayLanguage, + ), + ) + } +} + +@Composable +internal fun getTranslateToLanguageList(): List { + return mutableListOf().apply { + add( + Language( + code = Locale.KOREAN.toLanguageTag(), + localizedDisplayName = Locale.KOREAN.displayLanguage, + ), + ) + add( + Language( + code = Locale.CANADA.toLanguageTag(), + localizedDisplayName = Locale.CANADA.displayLanguage, + ), + ) + add( + Language( + code = Locale.FRENCH.toLanguageTag(), + localizedDisplayName = Locale.FRENCH.displayLanguage, + ), + ) + add( + Language( + code = Locale.ITALY.toLanguageTag(), + localizedDisplayName = Locale.ITALY.displayLanguage, + ), + ) + add( + Language( + code = Locale.GERMAN.toLanguageTag(), + localizedDisplayName = Locale.GERMAN.displayLanguage, + ), ) } } diff --git a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt index 52a706d52d..b8ebd0c872 100644 --- a/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt @@ -116,6 +116,7 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { ) }, onTranslateButtonClick = {}, + onNotNowButtonClick = { dismiss() }, ) } }