diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index c6a6bb951..8ee4e293f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -113,6 +113,10 @@ class AccountSyncedSettings( security.warnAboutPostsWithReports = syncedSettingsInternal.security.warnAboutPostsWithReports } } + + fun dontTranslateFromFilteredBySpokenLanguages(): Set { + return languages.dontTranslateFrom - getLanguagesSpokenByUser() + } } @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index aee8a9f70..48276a4c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -84,6 +84,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SecurityFiltersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UserSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog @@ -123,6 +124,7 @@ fun AppNavigation( composableFromEnd { BookmarkListScreen(accountViewModel, nav) } composableFromEnd { DraftListScreen(accountViewModel, nav) } composableFromEnd { SettingsScreen(sharedPreferencesViewModel, accountViewModel, nav) } + composableFromEnd { UserSettingsScreen(accountViewModel, nav) } composableFromBottomArgs { NIP47SetupScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs { AllRelayListScreen(it.toAdd, accountViewModel, nav) } @@ -251,9 +253,6 @@ private fun NavigateIfIntentRequested( } nav.newStack(Route.NewPost(message = message, attachment = media.toString())) - - media = null - message = null } else { var newAccount by remember { mutableStateOf(null) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 687b012bb..f52aaed5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -51,6 +51,7 @@ import androidx.compose.material.icons.outlined.CloudUpload import androidx.compose.material.icons.outlined.Drafts import androidx.compose.material.icons.outlined.GroupAdd import androidx.compose.material.icons.outlined.Key +import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.HorizontalDivider @@ -66,7 +67,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -74,7 +74,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle @@ -427,8 +426,6 @@ fun ListContent( var backupDialogOpen by remember { mutableStateOf(false) } - val context = LocalContext.current - Column(modifier) { NavigationRow( title = R.string.profile, @@ -508,6 +505,14 @@ fun ListContent( route = Route.Settings, ) + NavigationRow( + title = R.string.user_preferences, + icons = listOf(Icons.Outlined.Person, Icons.Outlined.Settings), + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.UserSettings, + ) + Spacer(modifier = Modifier.weight(1f)) IconRow( @@ -584,11 +589,28 @@ fun NavigationRow( tint: Color, nav: INav, route: Route, +) { + NavigationRow( + title = title, + icons = listOf(icon), + tint = tint, + nav = nav, + route = route, + ) +} + +@Composable +fun NavigationRow( + title: Int, + icons: List, + tint: Color, + nav: INav, + route: Route, ) { IconRow( - title, - icon, - tint, + title = title, + icons = icons, + tint = tint, onClick = { nav.closeDrawer() nav.nav(route) @@ -636,6 +658,21 @@ fun IconRow( icon: ImageVector, tint: Color, onClick: () -> Unit, +) { + IconRow( + title = title, + icons = listOf(icon), + tint = tint, + onClick = onClick, + ) +} + +@Composable +fun IconRow( + title: Int, + icons: List, + tint: Color, + onClick: () -> Unit, ) { Row( modifier = @@ -650,12 +687,15 @@ fun IconRow( modifier = IconRowModifier, verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = icon, - contentDescription = stringRes(title), - modifier = Size22Modifier, - tint = tint, - ) + icons.forEach { icon -> + Icon( + imageVector = icon, + contentDescription = stringRes(title), + modifier = Size22Modifier.padding(end = 4.dp), + tint = tint, + ) + } + Text( modifier = IconRowTextModifier, text = stringRes(title), @@ -709,8 +749,6 @@ fun BottomContent( accountViewModel: AccountViewModel, nav: INav, ) { - val coroutineScope = rememberCoroutineScope() - // store the dialog open or close state var dialogOpen by remember { mutableStateOf(false) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index eced5ad9c..7be1576e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -62,6 +62,8 @@ sealed class Route { @Serializable object Settings : Route() + @Serializable object UserSettings : Route() + @Serializable object EditProfile : Route() @Serializable data class EditRelays( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 6fc5e8653..f4d592aac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -898,6 +898,10 @@ class AccountViewModel( fun dontTranslateFrom() = account.settings.syncedSettings.languages.dontTranslateFrom + fun dontTranslateFromFilteredBySpokenLanguages() = account.settings.syncedSettings.dontTranslateFromFilteredBySpokenLanguages() + + fun toggleDontTranslateFrom(languageCode: String) = account.toggleDontTranslateFrom(languageCode) + fun translateTo() = account.settings.syncedSettings.languages.translateTo fun defaultZapType() = account.settings.syncedSettings.zaps.defaultZapType.value diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 131d971bd..5eb38c0aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -119,12 +119,13 @@ fun getLanguageIndex( sharedPreferencesViewModel: SharedPreferencesViewModel, ): Int { val language = sharedPreferencesViewModel.sharedPrefs.language - var languageIndex = -1 - if (language != null) { - languageIndex = languageEntries.values.toTypedArray().indexOf(language) - } else { - languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag()) - } + var languageIndex: Int + languageIndex = + if (language != null) { + languageEntries.values.toTypedArray().indexOf(language) + } else { + languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag()) + } if (languageIndex == -1) { languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.language) } @@ -365,7 +366,7 @@ fun SettingsRow( text = stringRes(description), style = MaterialTheme.typography.bodySmall, color = Color.Gray, - maxLines = 2, + maxLines = 3, overflow = TextOverflow.Ellipsis, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt new file mode 100644 index 000000000..9f57b1af5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -0,0 +1,154 @@ +/** + * 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.screen.loggedIn.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedTextField +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.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold +import com.vitorpamplona.amethyst.ui.navigation.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.Locale as JavaLocale + +@Preview(device = "spec:width=2160px,height=2340px,dpi=440") +@Composable +fun UserSettingsScreenPreview() { + val accountViewModel = mockAccountViewModel() + val nav = EmptyNav + ThemeComparisonRow { + UserSettingsScreen(accountViewModel, nav) + } +} + +@Composable +fun UserSettingsScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + DisappearingScaffold( + isInvertedLayout = false, + topBar = { + TopBarWithBackButton(stringRes(id = R.string.user_preferences), nav::popBack) + }, + accountViewModel = accountViewModel, + ) { + Column(Modifier.padding(it)) { + Column( + Modifier + .fillMaxSize() + .padding(top = Size10dp, start = Size20dp, end = Size20dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + DontTranslateFromSetting(accountViewModel) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { + var expanded by remember { mutableStateOf(false) } + val selectedLanguages = accountViewModel.dontTranslateFromFilteredBySpokenLanguages().toMutableSet() + + Column { + SettingsRow( + name = R.string.dont_translate_from, + description = R.string.dont_translate_from_description, + ) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + ) { + OutlinedTextField( + value = stringRes(R.string.quick_action_select), + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable), + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + selectedLanguages.forEach { languageCode -> + DropdownMenuItem( + text = { Text(text = JavaLocale.forLanguageTag(languageCode).displayName) }, + onClick = { + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + accountViewModel.toggleDontTranslateFrom( + languageCode, + ) + } + selectedLanguages.remove(languageCode) + expanded = false + }, + trailingIcon = { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Remove $languageCode", + tint = Color.Red, + modifier = Modifier.size(16.dp), + ) + }, + ) + } + } + } + } + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index cbcfc0fb4..2d9b6cdb2 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -675,6 +675,7 @@ Adds sensitive content warning before showing this content App Preferences + User Preferences Settings Always @@ -1178,4 +1179,7 @@ Share image… Search hashtag: #%1$s + + Don\'t Translate From + Languages shown here will not be translated. Select a language to remove it and have it translated again.