diff --git a/app/build.gradle b/app/build.gradle index 8f2426d15..a7b1b16a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -173,6 +173,9 @@ dependencies { implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material:a0954aba63') implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:a0954aba63') + // Language picker and Theme chooser + implementation 'androidx.appcompat:appcompat:1.6.1' + // Local model for language identification playImplementation 'com.google.mlkit:language-id:17.0.4' diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 43124fee9..3ea15b1a8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.RelaySetupInfo +import com.vitorpamplona.amethyst.model.Settings import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.model.ContactListEvent @@ -67,6 +68,11 @@ private object PrefKeys { const val WARN_ABOUT_REPORTS = "warn_about_reports" const val FILTER_SPAM_FROM_STRANGERS = "filter_spam_from_strangers" const val LAST_READ_PER_ROUTE = "last_read_route_per_route" + const val AUTOMATICALLY_SHOW_IMAGES = "automatically_show_images" + const val AUTOMATICALLY_START_PLAYBACK = "automatically_start_playback" + const val THEME = "theme" + const val PREFERRED_LANGUAGE = "preferred_Language" + const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview" val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } } @@ -236,6 +242,47 @@ object LocalPreferences { putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!) } }.apply() + + val globalPrefs = encryptedPreferences() + globalPrefs.edit().apply { + if (account.settings.automaticallyShowImages == null) { + remove(PrefKeys.AUTOMATICALLY_SHOW_IMAGES) + } else { + putBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, account.settings.automaticallyShowImages!!) + } + + if (account.settings.automaticallyStartPlayback == null) { + remove(PrefKeys.AUTOMATICALLY_START_PLAYBACK) + } else { + putBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, account.settings.automaticallyStartPlayback!!) + } + if (account.settings.automaticallyShowUrlPreview == null) { + remove(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW) + } else { + putBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, account.settings.automaticallyShowUrlPreview!!) + } + putString(PrefKeys.PREFERRED_LANGUAGE, account.settings.preferredLanguage ?: "") + }.apply() + } + + fun updateTheme(theme: Int) { + encryptedPreferences().edit().apply { + putInt(PrefKeys.THEME, theme) + }.apply() + } + + fun getTheme(): Int { + encryptedPreferences().apply { + return getInt(PrefKeys.THEME, 0) + } + } + + fun getPreferredLanguage(): String { + var language = "" + encryptedPreferences().apply { + language = getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: "" + } + return language } fun loadFromEncryptedStorage(): Account? { @@ -340,6 +387,28 @@ object LocalPreferences { mapOf() } + val settings = Settings() + encryptedPreferences().apply { + settings.automaticallyShowImages = if (contains(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)) { + getBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, false) + } else { + null + } + + settings.automaticallyStartPlayback = if (contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) { + getBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, false) + } else { + null + } + settings.automaticallyShowUrlPreview = if (contains(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)) { + getBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, false) + } else { + null + } + + settings.preferredLanguage = getString(PrefKeys.PREFERRED_LANGUAGE, "") + } + val a = Account( loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()), followingChannels = followingChannels, @@ -366,7 +435,8 @@ object LocalPreferences { showSensitiveContent = showSensitiveContent, warnAboutPostsWithReports = warnAboutReports, filterSpamFromStrangers = filterSpam, - lastReadPerRoute = lastReadPerRoute + lastReadPerRoute = lastReadPerRoute, + settings = settings ) return a diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 70d71fbd1..6171f8f72 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -73,7 +73,8 @@ class Account( var showSensitiveContent: Boolean? = null, var warnAboutPostsWithReports: Boolean = true, var filterSpamFromStrangers: Boolean = true, - var lastReadPerRoute: Map = mapOf() + var lastReadPerRoute: Map = mapOf(), + var settings: Settings = Settings() ) { var transientHiddenUsers: Set = setOf() @@ -84,6 +85,29 @@ class Account( val saveable: AccountLiveData = AccountLiveData(this) var userProfileCache: User? = null + fun updateAutomaticallyStartPlayback( + automaticallyStartPlayback: Boolean? + ) { + settings.automaticallyStartPlayback = automaticallyStartPlayback + live.invalidateData() + saveable.invalidateData() + } + + fun updateAutomaticallyShowUrlPreview( + automaticallyShowUrlPreview: Boolean? + ) { + settings.automaticallyShowUrlPreview = automaticallyShowUrlPreview + live.invalidateData() + saveable.invalidateData() + } + + fun updateAutomaticallyShowImages( + automaticallyShowImages: Boolean? + ) { + settings.automaticallyShowImages = automaticallyShowImages + live.invalidateData() + saveable.invalidateData() + } fun updateOptOutOptions(warnReports: Boolean, filterSpam: Boolean) { warnAboutPostsWithReports = warnReports diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt new file mode 100644 index 000000000..f4fec29d6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt @@ -0,0 +1,11 @@ +package com.vitorpamplona.amethyst.model + +import androidx.compose.runtime.Stable + +@Stable +class Settings( + var automaticallyShowImages: Boolean? = null, + var automaticallyStartPlayback: Boolean? = null, + var preferredLanguage: String? = null, + var automaticallyShowUrlPreview: Boolean? = null +) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/connectivitystatus/ConnectivityStatus.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/connectivitystatus/ConnectivityStatus.kt new file mode 100644 index 000000000..c119001bb --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/connectivitystatus/ConnectivityStatus.kt @@ -0,0 +1,17 @@ +package com.vitorpamplona.amethyst.service.connectivitystatus + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf + +object ConnectivityStatus { + private val onMobileData = mutableStateOf(false) + val isOnMobileData: MutableState = onMobileData + + private val onWifi = mutableStateOf(false) + val isOnWifi: MutableState = onWifi + + fun updateConnectivityStatus(isOnMobileData: Boolean, isOnWifi: Boolean) { + onMobileData.value = isOnMobileData + onWifi.value = isOnWifi + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 52c21b937..7a5b04b8f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -10,15 +10,18 @@ import android.os.Bundle import android.util.Log import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.ui.Modifier -import androidx.fragment.app.FragmentActivity +import androidx.core.os.LocaleListCompat import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager +import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent @@ -32,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.navigation.debugState import com.vitorpamplona.amethyst.ui.note.Nip47 import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -40,8 +44,7 @@ import kotlinx.coroutines.launch import java.net.URLEncoder import java.nio.charset.StandardCharsets -class MainActivity : FragmentActivity() { - +class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -50,16 +53,22 @@ class MainActivity : FragmentActivity() { val startingPage = uriToRoute(uri) LocalPreferences.migrateSingleUserPrefs() - + val language = LocalPreferences.getPreferredLanguage() + if (language.isNotBlank()) { + val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(language) + AppCompatDelegate.setApplicationLocales(appLocale) + } setContent { - AmethystTheme { + val themeViewModel: ThemeViewModel = viewModel() + themeViewModel.onChange(LocalPreferences.getTheme()) + AmethystTheme(themeViewModel) { // A surface container using the 'background' color from the theme Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { val accountStateViewModel: AccountStateViewModel = viewModel { AccountStateViewModel(this@MainActivity) } - AccountScreen(accountStateViewModel, startingPage) + AccountScreen(accountStateViewModel, themeViewModel, startingPage) } } } @@ -130,6 +139,14 @@ class MainActivity : FragmentActivity() { networkCapabilities: NetworkCapabilities ) { super.onCapabilitiesChanged(network, networkCapabilities) + val hasMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData") + Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi") + ConnectivityStatus.updateConnectivityStatus( + networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR), + networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + ) } // lost network connection diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index dd3750337..ff3fa7e63 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -6,11 +6,22 @@ import android.os.Build import android.util.Size import android.widget.Toast import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -29,8 +40,7 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.components.* +import com.vitorpamplona.amethyst.ui.components.VideoView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -107,7 +117,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac .fillMaxWidth() .verticalScroll(scroolState) ) { - ImageVideoPost(postViewModel, account) + ImageVideoPost(postViewModel, accountViewModel) } } } @@ -123,7 +133,7 @@ fun isNIP94Server(selectedServer: ServersAvailable?): Boolean { } @Composable -fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) { +fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewModel) { val fileServers = listOf( // Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)), Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)), @@ -180,7 +190,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) { } } else { postViewModel.galleryUri?.let { - VideoView(it.toString()) + VideoView(it.toString(), accountViewModel = accountViewModel) } } } @@ -191,7 +201,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) { ) { TextSpinner( label = stringResource(id = R.string.file_server), - placeholder = fileServers.firstOrNull { it.first == acc.defaultFileServer }?.second ?: fileServers[0].second, + placeholder = fileServers.firstOrNull { it.first == accountViewModel.account.defaultFileServer }?.second ?: fileServers[0].second, options = fileServerOptions, explainers = fileServerExplainers, onSelect = { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 89bb3d5a5..97b7263e8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -272,7 +272,8 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n scope.launch { postViewModel.imageUploadingError.emit(it) } - } + }, + accountViewModel = accountViewModel ) } } @@ -330,9 +331,9 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n ) ) } else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) { - VideoView(myUrlPreview) + VideoView(myUrlPreview, accountViewModel = accountViewModel) } else { - UrlPreview(myUrlPreview, myUrlPreview) + UrlPreview(myUrlPreview, myUrlPreview, accountViewModel) } } else if (startsWithNIP19Scheme(myUrlPreview)) { val bgColor = MaterialTheme.colors.background @@ -348,7 +349,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n nav ) } else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) { - UrlPreview("https://$myUrlPreview", myUrlPreview) + UrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel) } } } @@ -836,7 +837,8 @@ fun ImageVideoDescription( defaultServer: ServersAvailable, onAdd: (String, ServersAvailable, Boolean) -> Unit, onCancel: () -> Unit, - onError: (String) -> Unit + onError: (String) -> Unit, + accountViewModel: AccountViewModel ) { val resolver = LocalContext.current.contentResolver val mediaType = resolver.getType(uri) ?: "" @@ -966,7 +968,7 @@ fun ImageVideoDescription( ) } } else { - VideoView(uri.toString()) + VideoView(uri.toString(), accountViewModel = accountViewModel) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index a1b02d928..953bc2deb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -238,8 +238,8 @@ private fun RenderWordWithPreview( nav: (String) -> Unit ) { when (word) { - is ImageSegment -> ZoomableContentView(word.segmentText, state) - is LinkSegment -> UrlPreview(word.segmentText, word.segmentText) + is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel) + is LinkSegment -> UrlPreview(word.segmentText, word.segmentText, accountViewModel) is EmojiSegment -> RenderCustomEmoji(word.segmentText, state) is InvoiceSegment -> MayBeInvoicePreview(word.segmentText) is WithdrawSegment -> MayBeWithdrawal(word.segmentText) @@ -256,9 +256,13 @@ private fun RenderWordWithPreview( } @Composable -private fun ZoomableContentView(word: String, state: RichTextViewerState) { +private fun ZoomableContentView( + word: String, + state: RichTextViewerState, + accountViewModel: AccountViewModel +) { state.imagesForPager[word]?.let { - ZoomableContentView(it, state.imageList) + ZoomableContentView(it, state.imageList, accountViewModel) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt index 27641a31d..85b09ef1b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreview.kt @@ -9,37 +9,56 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.model.UrlCachedPreviewer +import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable -fun UrlPreview(url: String, urlText: String) { - var urlPreviewState by remember(url) { - mutableStateOf( - UrlCachedPreviewer.cache.get(url)?.let { it } ?: UrlPreviewState.Loading - ) +fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) { + val settings = accountViewModel.account.settings + val isMobile = ConnectivityStatus.isOnMobileData.value + + val automaticallyShowUrlPreview = when (settings.automaticallyShowUrlPreview) { + true -> !isMobile + false -> false + else -> true } - // Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created). - if (urlPreviewState == UrlPreviewState.Loading) { - LaunchedEffect(url) { - launch(Dispatchers.IO) { - UrlCachedPreviewer.previewInfo(url) { - launch(Dispatchers.Main) { - urlPreviewState = it + if (!automaticallyShowUrlPreview) { + ClickableUrl(urlText, url) + } else { + var urlPreviewState by remember(url) { + mutableStateOf( + UrlCachedPreviewer.cache.get(url)?.let { it } ?: UrlPreviewState.Loading + ) + } + + // Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created). + if (urlPreviewState == UrlPreviewState.Loading) { + LaunchedEffect(url) { + launch(Dispatchers.IO) { + UrlCachedPreviewer.previewInfo(url) { + launch(Dispatchers.Main) { + urlPreviewState = it + } } } } } - } - Crossfade(targetState = urlPreviewState, animationSpec = tween(durationMillis = 100)) { state -> - when (state) { - is UrlPreviewState.Loaded -> { - UrlPreviewCard(url, state.previewInfo) - } - else -> { - ClickableUrl(urlText, url) + Crossfade( + targetState = urlPreviewState, + animationSpec = tween(durationMillis = 100) + ) { state -> + when (state) { + is UrlPreviewState.Loaded -> { + UrlPreviewCard(url, state.previewInfo, accountViewModel) + } + + else -> { + ClickableUrl(urlText, url) + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt index 29edd4a3e..47b8da1cb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt @@ -19,66 +19,82 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.previews.UrlInfoItem +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.subtleBorder @Composable fun UrlPreviewCard( url: String, - previewInfo: UrlInfoItem + previewInfo: UrlInfoItem, + accountViewModel: AccountViewModel ) { - val uri = LocalUriHandler.current + val settings = accountViewModel.account.settings + val isMobile = ConnectivityStatus.isOnMobileData.value - Row( - modifier = Modifier - .clickable { runCatching { uri.openUri(url) } } - .clip(shape = QuoteBorder) - .border( - 1.dp, - MaterialTheme.colors.subtleBorder, - QuoteBorder - ) - ) { - Column { - AsyncImage( - model = previewInfo.imageUrlFullPath, - contentDescription = stringResource(R.string.preview_card_image_for, previewInfo.url), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth() - ) + val automaticallyShowUrlPreview = when (settings.automaticallyShowUrlPreview) { + true -> !isMobile + false -> false + else -> true + } - Text( - text = previewInfo.verifiedUrl?.host ?: previewInfo.url, - style = MaterialTheme.typography.caption, - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, top = 10.dp), - color = Color.Gray, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + if (!automaticallyShowUrlPreview) { + ClickableUrl(url, url) + } else { + val uri = LocalUriHandler.current - Text( - text = previewInfo.title, - style = MaterialTheme.typography.body2, - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + Row( + modifier = Modifier + .clickable { runCatching { uri.openUri(url) } } + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colors.subtleBorder, + QuoteBorder + ) + ) { + Column { + AsyncImage( + model = previewInfo.imageUrlFullPath, + contentDescription = stringResource(R.string.preview_card_image_for, previewInfo.url), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) - Text( - text = previewInfo.description, - style = MaterialTheme.typography.caption, - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) + Text( + text = previewInfo.verifiedUrl?.host ?: previewInfo.url, + style = MaterialTheme.typography.caption, + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, top = 10.dp), + color = Color.Gray, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = previewInfo.title, + style = MaterialTheme.typography.body2, + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = previewInfo.description, + style = MaterialTheme.typography.caption, + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index 8859a32b9..7fb068148 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -60,6 +61,8 @@ import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.upstream.DataSource import com.vitorpamplona.amethyst.VideoCache import com.vitorpamplona.amethyst.service.HttpClient +import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.time.ExperimentalTime @@ -68,7 +71,13 @@ import kotlin.time.measureTimedValue public var DefaultMutedSetting = mutableStateOf(true) @Composable -fun LoadThumbAndThenVideoView(videoUri: String, description: String? = null, thumbUri: String, onDialog: ((Boolean) -> Unit)? = null) { +fun LoadThumbAndThenVideoView( + videoUri: String, + description: String? = null, + thumbUri: String, + accountViewModel: AccountViewModel, + onDialog: ((Boolean) -> Unit)? = null +) { var loadingFinished by remember { mutableStateOf>(Pair(false, null)) } val context = LocalContext.current @@ -92,9 +101,9 @@ fun LoadThumbAndThenVideoView(videoUri: String, description: String? = null, thu if (loadingFinished.first) { if (loadingFinished.second != null) { - VideoView(videoUri, description, VideoThumb(loadingFinished.second), onDialog) + VideoView(videoUri, description, VideoThumb(loadingFinished.second), accountViewModel, onDialog) } else { - VideoView(videoUri, description, null, onDialog) + VideoView(videoUri, description, null, accountViewModel, onDialog) } } } @@ -105,10 +114,11 @@ fun VideoView( videoUri: String, description: String? = null, thumb: VideoThumb? = null, + accountViewModel: AccountViewModel, onDialog: ((Boolean) -> Unit)? = null ) { val (value, elapsed) = measureTimedValue { - VideoView1(videoUri, description, thumb, onDialog) + VideoView1(videoUri, description, thumb, onDialog, accountViewModel) } Log.d("Rendering Metrics", "VideoView $elapsed $videoUri") } @@ -118,7 +128,8 @@ fun VideoView1( videoUri: String, description: String? = null, thumb: VideoThumb? = null, - onDialog: ((Boolean) -> Unit)? = null + onDialog: ((Boolean) -> Unit)? = null, + accountViewModel: AccountViewModel ) { var exoPlayerData by remember { mutableStateOf(null) } val defaultToStart by remember { mutableStateOf(DefaultMutedSetting.value) } @@ -133,7 +144,7 @@ fun VideoView1( } exoPlayerData?.let { - VideoView(videoUri, description, it, defaultToStart, thumb, onDialog) + VideoView(videoUri, description, it, defaultToStart, thumb, onDialog, accountViewModel) } DisposableEffect(Unit) { @@ -151,10 +162,11 @@ fun VideoView( exoPlayerData: VideoPlayer, defaultToStart: Boolean = false, thumb: VideoThumb? = null, - onDialog: ((Boolean) -> Unit)? = null + onDialog: ((Boolean) -> Unit)? = null, + accountViewModel: AccountViewModel ) { - val (value, elapsed) = measureTimedValue { - VideoView1(videoUri, description, exoPlayerData, defaultToStart, thumb, onDialog) + val (_, elapsed) = measureTimedValue { + VideoView1(videoUri, description, exoPlayerData, defaultToStart, thumb, onDialog, accountViewModel) } Log.d("Rendering Metrics", "VideoView $elapsed $videoUri") } @@ -166,12 +178,26 @@ fun VideoView1( exoPlayerData: VideoPlayer, defaultToStart: Boolean = false, thumb: VideoThumb? = null, - onDialog: ((Boolean) -> Unit)? = null + onDialog: ((Boolean) -> Unit)? = null, + accountViewModel: AccountViewModel ) { val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current) val media = remember { MediaItem.Builder().setUri(videoUri).build() } + val settings = accountViewModel.account.settings + val isMobile = ConnectivityStatus.isOnMobileData.value + + val automaticallyStartPlayback = remember { + mutableStateOf( + when (settings.automaticallyStartPlayback) { + true -> !isMobile + false -> false + else -> true + } + ) + } + exoPlayerData.exoPlayer.apply { repeatMode = Player.REPEAT_MODE_ALL videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT @@ -196,7 +222,7 @@ fun VideoView1( prepare() } - RenderVideoPlayer(exoPlayerData, thumb, onDialog) + RenderVideoPlayer(exoPlayerData, thumb, automaticallyStartPlayback, onDialog) DisposableEffect(Unit) { val observer = LifecycleEventObserver { _, event -> @@ -230,6 +256,7 @@ data class VideoThumb( private fun RenderVideoPlayer( playerData: VideoPlayer, thumbData: VideoThumb?, + automaticallyStartPlayback: MutableState, onDialog: ((Boolean) -> Unit)? ) { val context = LocalContext.current @@ -241,7 +268,12 @@ private fun RenderVideoPlayer( .defaultMinSize(minHeight = 70.dp) .align(Alignment.Center) .onVisibilityChanges { visible -> - if (visible && !playerData.exoPlayer.isPlaying) { + if (!automaticallyStartPlayback.value) { + playerData.exoPlayer.stop() + } + if (!automaticallyStartPlayback.value && visible && !playerData.exoPlayer.isPlaying) { + playerData.exoPlayer.pause() + } else if (visible && !playerData.exoPlayer.isPlaying) { playerData.exoPlayer.play() } else if (!visible && playerData.exoPlayer.isPlaying) { playerData.exoPlayer.pause() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 9af8a664a..21affa985 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -5,7 +5,6 @@ import android.os.Build import android.util.Log import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -34,6 +33,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DownloadForOffline import androidx.compose.material.icons.filled.Report import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -70,10 +70,12 @@ import coil.imageLoader import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.BlurHashRequester +import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation import com.vitorpamplona.amethyst.ui.actions.SaveToGallery import com.vitorpamplona.amethyst.ui.note.BlankNote +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Font17SP import com.vitorpamplona.amethyst.ui.theme.imageModifier import kotlinx.collections.immutable.ImmutableList @@ -167,7 +169,11 @@ fun figureOutMimeType(fullUrl: String): ZoomableContent { @Composable @OptIn(ExperimentalFoundationApi::class) -fun ZoomableContentView(content: ZoomableContent, images: ImmutableList = listOf(content).toImmutableList()) { +fun ZoomableContentView( + content: ZoomableContent, + images: ImmutableList = listOf(content).toImmutableList(), + accountViewModel: AccountViewModel +) { val clipboardManager = LocalClipboardManager.current // store the dialog open or close state @@ -194,27 +200,41 @@ fun ZoomableContentView(content: ZoomableContent, images: ImmutableList UrlImageView(content, mainImageModifier) - is ZoomableUrlVideo -> VideoView(content.url, content.description) { dialogOpen = true } - is ZoomableLocalImage -> LocalImageView(content, mainImageModifier) + is ZoomableUrlImage -> UrlImageView(content, mainImageModifier, accountViewModel) + is ZoomableUrlVideo -> VideoView(content.url, content.description, accountViewModel = accountViewModel) { dialogOpen = true } + is ZoomableLocalImage -> LocalImageView(content, mainImageModifier, accountViewModel) is ZoomableLocalVideo -> content.localFile?.let { - VideoView(it.toUri().toString(), content.description) { dialogOpen = true } + VideoView(it.toUri().toString(), content.description, accountViewModel = accountViewModel) { dialogOpen = true } } } if (dialogOpen) { - ZoomableImageDialog(content, images, onDismiss = { dialogOpen = false }) + ZoomableImageDialog(content, images, onDismiss = { dialogOpen = false }, accountViewModel) } } @Composable private fun LocalImageView( content: ZoomableLocalImage, - mainImageModifier: Modifier + mainImageModifier: Modifier, + accountViewModel: AccountViewModel? ) { if (content.localFile != null && content.localFile.exists()) { BoxWithConstraints(contentAlignment = Alignment.Center) { + val settings = accountViewModel?.account?.settings + val isMobile = ConnectivityStatus.isOnMobileData.value + + val showImage = remember { + mutableStateOf( + when (settings?.automaticallyShowImages) { + true -> !isMobile + false -> false + else -> true + } + ) + } + val myModifier = remember { mainImageModifier .widthIn(max = maxWidth) @@ -236,17 +256,19 @@ private fun LocalImageView( mutableStateOf(null) } - AsyncImage( - model = content.localFile, - contentDescription = content.description, - contentScale = contentScale, - modifier = myModifier, - onState = { - painterState.value = it - } - ) + if (showImage.value) { + AsyncImage( + model = content.localFile, + contentDescription = content.description, + contentScale = contentScale, + modifier = myModifier, + onState = { + painterState.value = it + } + ) + } - AddedImageFeatures(painterState, content, contentScale, myModifier, verifierModifier) + AddedImageFeatures(painterState, content, contentScale, myModifier, verifierModifier, showImage) } } else { BlankNote() @@ -256,9 +278,23 @@ private fun LocalImageView( @Composable private fun UrlImageView( content: ZoomableUrlImage, - mainImageModifier: Modifier + mainImageModifier: Modifier, + accountViewModel: AccountViewModel? ) { BoxWithConstraints(contentAlignment = Alignment.Center) { + val settings = accountViewModel?.account?.settings + val isMobile = ConnectivityStatus.isOnMobileData.value + + val showImage = remember { + mutableStateOf( + when (settings?.automaticallyShowImages) { + true -> !isMobile + false -> false + else -> true + } + ) + } + val myModifier = remember { mainImageModifier .widthIn(max = maxWidth) @@ -280,17 +316,38 @@ private fun UrlImageView( mutableStateOf(null) } - AsyncImage( - model = content.url, - contentDescription = content.description, - contentScale = contentScale, - modifier = myModifier, - onState = { - painterState.value = it - } - ) + if (showImage.value) { + AsyncImage( + model = content.url, + contentDescription = content.description, + contentScale = contentScale, + modifier = myModifier, + onState = { + painterState.value = it + } + ) + } - AddedImageFeatures(painterState, content, contentScale, myModifier, verifierModifier) + AddedImageFeatures(painterState, content, contentScale, myModifier, verifierModifier, showImage) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ImageUrlWithDownloadButton(url: String, showImage: MutableState) { + FlowRow() { + ClickableUrl(urlText = url, url = url) + IconButton( + modifier = Modifier.size(20.dp), + onClick = { showImage.value = true } + ) { + Icon( + imageVector = Icons.Default.DownloadForOffline, + null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colors.primary + ) + } } } @@ -301,30 +358,35 @@ private fun AddedImageFeatures( content: ZoomableLocalImage, contentScale: ContentScale, myModifier: Modifier, - verifiedModifier: Modifier + verifiedModifier: Modifier, + showImage: MutableState ) { - when (painter.value) { - null, is AsyncImagePainter.State.Loading -> { - if (content.blurhash != null) { - DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier) - } else { - FlowRow() { - DisplayUrlWithLoadingSymbol(content) + if (!showImage.value) { + ImageUrlWithDownloadButton(content.uri, showImage) + } else { + when (painter.value) { + null, is AsyncImagePainter.State.Loading -> { + if (content.blurhash != null) { + DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier) + } else { + FlowRow() { + DisplayUrlWithLoadingSymbol(content) + } } } - } - is AsyncImagePainter.State.Error -> { - BlankNote() - } - - is AsyncImagePainter.State.Success -> { - if (content.isVerified != null) { - HashVerificationSymbol(content.isVerified, verifiedModifier) + is AsyncImagePainter.State.Error -> { + BlankNote() } - } - else -> { + is AsyncImagePainter.State.Success -> { + if (content.isVerified != null) { + HashVerificationSymbol(content.isVerified, verifiedModifier) + } + } + + else -> { + } } } } @@ -336,46 +398,51 @@ private fun AddedImageFeatures( content: ZoomableUrlImage, contentScale: ContentScale, myModifier: Modifier, - verifiedModifier: Modifier + verifiedModifier: Modifier, + showImage: MutableState ) { - var verifiedHash by remember { - mutableStateOf(null) - } - - when (painter.value) { - null, is AsyncImagePainter.State.Loading -> { - if (content.blurhash != null) { - DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier) - } else { - FlowRow() { - DisplayUrlWithLoadingSymbol(content) - } - } + if (!showImage.value) { + ImageUrlWithDownloadButton(content.url, showImage) + } else { + var verifiedHash by remember { + mutableStateOf(null) } - is AsyncImagePainter.State.Error -> { - ClickableUrl(urlText = "${content.url} ", url = content.url) - } - - is AsyncImagePainter.State.Success -> { - if (content.hash != null) { - val context = LocalContext.current - LaunchedEffect(key1 = content.url) { - launch(Dispatchers.IO) { - val newVerifiedHash = verifyHash(content, context) - if (newVerifiedHash != verifiedHash) { - verifiedHash = newVerifiedHash - } + when (painter.value) { + null, is AsyncImagePainter.State.Loading -> { + if (content.blurhash != null) { + DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier) + } else { + FlowRow() { + DisplayUrlWithLoadingSymbol(content) } } } - verifiedHash?.let { - HashVerificationSymbol(it, verifiedModifier) + is AsyncImagePainter.State.Error -> { + ClickableUrl(urlText = "${content.url} ", url = content.url) } - } - else -> { + is AsyncImagePainter.State.Success -> { + if (content.hash != null) { + val context = LocalContext.current + LaunchedEffect(key1 = content.url) { + launch(Dispatchers.IO) { + val newVerifiedHash = verifyHash(content, context) + if (newVerifiedHash != verifiedHash) { + verifiedHash = newVerifiedHash + } + } + } + } + + verifiedHash?.let { + HashVerificationSymbol(it, verifiedModifier) + } + } + + else -> { + } } } } @@ -473,7 +540,7 @@ private fun DisplayBlurHash( @OptIn(ExperimentalFoundationApi::class) @Composable -fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList = listOf(imageUrl).toImmutableList(), onDismiss: () -> Unit) { +fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList = listOf(imageUrl).toImmutableList(), onDismiss: () -> Unit, accountViewModel: AccountViewModel) { val view = LocalView.current DisposableEffect(key1 = Unit) { @@ -515,11 +582,11 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList - RenderImageOrVideo(allImages[index]) + RenderImageOrVideo(allImages[index], accountViewModel) } ) } else { - RenderImageOrVideo(imageUrl) + RenderImageOrVideo(imageUrl, accountViewModel) } Row( @@ -544,23 +611,23 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList(nextPage) } @@ -219,6 +222,15 @@ fun AppNavigation( ) }) } + + Route.Settings.let { route -> + composable(route.route, route.arguments, content = { + SettingsScreen( + accountViewModel = accountViewModel, + themeViewModel + ) + }) + } } actionableNextPage?.let { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index b115e5b56..c6ead6f6d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.Size16dp import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -299,20 +300,19 @@ fun ListContent( ) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = remember(accountState) { accountState?.account } ?: return - val coroutineScope = rememberCoroutineScope() - var backupDialogOpen by remember { mutableStateOf(false) } - var checked by remember { mutableStateOf(account.proxy != null) } - var disconnectTorDialog by remember { mutableStateOf(false) } - var conectOrbotDialogOpen by remember { mutableStateOf(false) } - var proxyPort = remember { mutableStateOf(account.proxyPort.toString()) } - val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() } - var wantsToEditRelays by remember { mutableStateOf(false) } + var backupDialogOpen by remember { mutableStateOf(false) } + var checked by remember { mutableStateOf(accountViewModel.account.proxy != null) } + var disconnectTorDialog by remember { mutableStateOf(false) } + var conectOrbotDialogOpen by remember { mutableStateOf(false) } + val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) } + val context = LocalContext.current + Column( modifier = modifier .fillMaxHeight() @@ -368,7 +368,6 @@ fun ListContent( ) val textTorProxy = if (checked) stringResource(R.string.disconnect_from_your_orbot_setup) else stringResource(R.string.connect_via_tor_short) - IconRow( title = textTorProxy, icon = R.drawable.ic_tor, @@ -391,6 +390,15 @@ fun ListContent( } ) + NavigationRow( + title = stringResource(R.string.settings), + icon = Route.Settings.icon, + tint = MaterialTheme.colors.onBackground, + nav = nav, + scaffoldState = scaffoldState, + route = Route.Settings.route + ) + Spacer(modifier = Modifier.weight(1f)) IconRow( @@ -401,12 +409,12 @@ fun ListContent( ) } - if (backupDialogOpen) { - AccountBackupDialog(account, onClose = { backupDialogOpen = false }) + if (wantsToEditRelays) { + NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav) + } + if (backupDialogOpen) { + AccountBackupDialog(accountViewModel.account, onClose = { backupDialogOpen = false }) } - - val context = LocalContext.current - if (conectOrbotDialogOpen) { ConnectOrbotDialog( onClose = { conectOrbotDialogOpen = false }, @@ -414,7 +422,7 @@ fun ListContent( conectOrbotDialogOpen = false disconnectTorDialog = false checked = true - enableTor(account, true, proxyPort, context = context) + enableTor(accountViewModel.account, true, proxyPort, context, coroutineScope) }, proxyPort ) @@ -436,7 +444,7 @@ fun ListContent( onClick = { disconnectTorDialog = false checked = false - enableTor(account, false, proxyPort, context) + enableTor(accountViewModel.account, false, proxyPort, context, coroutineScope) } ) { Text(text = stringResource(R.string.yes)) @@ -453,23 +461,22 @@ fun ListContent( } ) } - - if (wantsToEditRelays) { - NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav) - } } private fun enableTor( account: Account, checked: Boolean, portNumber: MutableState, - context: Context + context: Context, + scope: CoroutineScope ) { account.proxyPort = portNumber.value.toInt() account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort) - LocalPreferences.saveToEncryptedStorage(account) - ServiceManager.pause() - ServiceManager.start(context) + scope.launch(Dispatchers.IO) { + LocalPreferences.saveToEncryptedStorage(account) + ServiceManager.pause() + ServiceManager.start(context) + } } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index eec35833e..c2f197b19 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -128,6 +128,11 @@ sealed class Route( icon = R.drawable.ic_moments, arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList() ) + + object Settings : Route( + route = "Settings", + icon = R.drawable.ic_settings + ) } // ** diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index f3be731b7..145760686 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -355,7 +355,11 @@ private fun RenderNoteRow( } @Composable -fun RenderLiveActivityThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun RenderLiveActivityThumb( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return val eventUpdates by baseNote.live().metadata.observeAsState() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 932bcda55..32da2d7f8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserMetadata import com.vitorpamplona.amethyst.service.OnlineChecker +import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.model.ATag import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent import com.vitorpamplona.amethyst.service.model.AudioTrackEvent @@ -1303,7 +1304,7 @@ fun RenderAppDefinition( ) if (zoomImageDialogOpen) { - ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false }) + ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel) } } else { Image( @@ -1354,7 +1355,7 @@ fun RenderAppDefinition( } if (zoomImageDialogOpen) { - ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false }) + ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel) } Spacer(Modifier.weight(1f)) @@ -3086,7 +3087,7 @@ fun FileHeaderDisplay(note: Note, accountViewModel: AccountViewModel) { Crossfade(targetState = content) { if (it != null) { SensitivityWarning(note = note, accountViewModel = accountViewModel) { - ZoomableContentView(content = it) + ZoomableContentView(content = it, accountViewModel = accountViewModel) } } } @@ -3172,7 +3173,7 @@ private fun RenderNIP95( Crossfade(targetState = content) { if (it != null) { SensitivityWarning(note = header, accountViewModel = accountViewModel) { - ZoomableContentView(content = it) + ZoomableContentView(content = it, accountViewModel = accountViewModel) } } } @@ -3242,12 +3243,14 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, accountViewModel: AccountViewMo LoadThumbAndThenVideoView( videoUri = media, description = noteEvent.subject(), - thumbUri = cover + thumbUri = cover, + accountViewModel = accountViewModel ) } ?: VideoView( videoUri = media, - noteEvent.subject() + description = noteEvent.subject(), + accountViewModel = accountViewModel ) } } @@ -3371,7 +3374,8 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo ) { VideoView( videoUri = media, - description = subject + description = subject, + accountViewModel = accountViewModel ) } } else { @@ -3421,17 +3425,27 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView ) ) { Column { - image?.let { - AsyncImage( - model = it, - contentDescription = stringResource( - R.string.preview_card_image_for, - it - ), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth() - ) - } ?: CreateImageHeader(note, accountViewModel) + val settings = accountViewModel.account.settings + val isMobile = ConnectivityStatus.isOnMobileData.value + + val automaticallyShowUrlPreview = when (settings.automaticallyShowUrlPreview) { + true -> !isMobile + false -> false + else -> true + } + if (automaticallyShowUrlPreview) { + image?.let { + AsyncImage( + model = it, + contentDescription = stringResource( + R.string.preview_card_image_for, + it + ), + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + } ?: CreateImageHeader(note, accountViewModel) + } title?.let { Text( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index f3c923a68..82b872d7e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -12,7 +12,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage @Composable -fun AccountScreen(accountStateViewModel: AccountStateViewModel, startingPage: String?) { +fun AccountScreen(accountStateViewModel: AccountStateViewModel, themeViewModel: ThemeViewModel, startingPage: String?) { val accountState by accountStateViewModel.accountContent.collectAsState() Column() { @@ -27,7 +27,7 @@ fun AccountScreen(accountStateViewModel: AccountStateViewModel, startingPage: St factory = AccountViewModel.Factory(state.account) ) - MainScreen(accountViewModel, accountStateViewModel, startingPage) + MainScreen(accountViewModel, accountStateViewModel, themeViewModel, startingPage) } is AccountState.LoggedInViewOnly -> { val accountViewModel: AccountViewModel = viewModel( @@ -35,7 +35,7 @@ fun AccountScreen(accountStateViewModel: AccountStateViewModel, startingPage: St factory = AccountViewModel.Factory(state.account) ) - MainScreen(accountViewModel, accountStateViewModel, startingPage) + MainScreen(accountViewModel, accountStateViewModel, themeViewModel, startingPage) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThemeViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThemeViewModel.kt new file mode 100644 index 000000000..661d8672d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThemeViewModel.kt @@ -0,0 +1,14 @@ +package com.vitorpamplona.amethyst.ui.screen + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class ThemeViewModel : ViewModel() { + private val _theme = MutableLiveData(0) + val theme: LiveData = _theme + + fun onChange(newValue: Int) { + _theme.value = newValue + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 54bda3dda..adf04c6fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -78,19 +78,6 @@ import com.vitorpamplona.amethyst.service.model.RelaySetEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.note.* -import com.vitorpamplona.amethyst.ui.note.BadgeDisplay -import com.vitorpamplona.amethyst.ui.note.BlankNote -import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost -import com.vitorpamplona.amethyst.ui.note.DisplayPoW -import com.vitorpamplona.amethyst.ui.note.DisplayReward -import com.vitorpamplona.amethyst.ui.note.HiddenNote -import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture -import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu -import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu -import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay -import com.vitorpamplona.amethyst.ui.note.ReactionsRow -import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader import com.vitorpamplona.amethyst.ui.theme.SmallBorder @@ -372,7 +359,13 @@ fun NoteMaster( ) { Column() { if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && note.channelHex() != null) { - ChannelHeader(channelHex = note.channelHex()!!, showVideo = true, showBottomDiviser = false, accountViewModel = accountViewModel, nav = nav) + ChannelHeader( + channelHex = note.channelHex()!!, + showVideo = true, + showBottomDiviser = false, + accountViewModel = accountViewModel, + nav = nav + ) } else if (noteEvent is FileHeaderEvent) { FileHeaderDisplay(baseNote, accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 877b4edc3..806851e6e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -40,6 +40,24 @@ class AccountViewModel(val account: Account) : ViewModel() { val userFollows: LiveData = account.userProfile().live().follows.map { it } val userRelays: LiveData = account.userProfile().live().relays.map { it } + fun updateAutomaticallyStartPlayback( + automaticallyStartPlayback: Boolean? + ) { + account.updateAutomaticallyStartPlayback(automaticallyStartPlayback) + } + + fun updateAutomaticallyShowUrlPreview( + automaticallyShowUrlPreview: Boolean? + ) { + account.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview) + } + + fun updateAutomaticallyShowImages( + automaticallyShowImages: Boolean? + ) { + account.updateAutomaticallyShowImages(automaticallyShowImages) + } + fun isWriteable(): Boolean { return account.isWriteable() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index eac21a8a3..9785ce4a4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -632,7 +632,8 @@ private fun ShowVideoStreaming( } ZoomableContentView( - content = zoomableUrlVideo + content = zoomableUrlVideo, + accountViewModel = accountViewModel ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 1bffcfed1..b3fef99eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -51,11 +51,17 @@ import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel +import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable -fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) { +fun MainScreen( + accountViewModel: AccountViewModel, + accountStateViewModel: AccountStateViewModel, + themeViewModel: ThemeViewModel, + startingPage: String? = null +) { val scope = rememberCoroutineScope() val navController = rememberNavController() val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) @@ -211,6 +217,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun userReactionsStatsModel = userReactionsStatsModel, navController = navController, accountViewModel = accountViewModel, + themeViewModel = themeViewModel, nextPage = startingPage ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 5218ceb52..e04b8c39b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -599,7 +599,7 @@ private fun ProfileHeader( var zoomImageDialogOpen by remember { mutableStateOf(false) } Box { - DrawBanner(baseUser) + DrawBanner(baseUser, accountViewModel) Box( modifier = Modifier @@ -688,7 +688,7 @@ private fun ProfileHeader( val profilePic = baseUser.profilePicture() if (zoomImageDialogOpen && profilePic != null) { - ZoomableImageDialog(figureOutMimeType(profilePic), onDismiss = { zoomImageDialogOpen = false }) + ZoomableImageDialog(figureOutMimeType(profilePic), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel) } } @@ -1260,7 +1260,7 @@ private fun WatchAndRenderBadgeImage( @OptIn(ExperimentalFoundationApi::class) @Composable -public fun DrawBanner(baseUser: User) { +fun DrawBanner(baseUser: User, accountViewModel: AccountViewModel) { val userState by baseUser.live().metadata.observeAsState() val banner = remember(userState) { userState?.user?.info?.banner } @@ -1284,7 +1284,7 @@ public fun DrawBanner(baseUser: User) { ) if (zoomImageDialogOpen) { - ZoomableImageDialog(imageUrl = figureOutMimeType(banner), onDismiss = { zoomImageDialogOpen = false }) + ZoomableImageDialog(imageUrl = figureOutMimeType(banner), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel) } } else { Image( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt new file mode 100644 index 000000000..69b18e5ef --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt @@ -0,0 +1,314 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import android.content.Context +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ExposedDropdownMenuBox +import androidx.compose.material.ExposedDropdownMenuDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +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 +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.os.LocaleListCompat +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.StdPadding +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException + +fun Context.getLocaleListFromXml(): LocaleListCompat { + val tagsList = mutableListOf() + try { + val xpp: XmlPullParser = resources.getXml(R.xml.locales_config) + while (xpp.eventType != XmlPullParser.END_DOCUMENT) { + if (xpp.eventType == XmlPullParser.START_TAG) { + if (xpp.name == "locale") { + tagsList.add(xpp.getAttributeValue(0)) + } + } + xpp.next() + } + } catch (e: XmlPullParserException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + + return LocaleListCompat.forLanguageTags(tagsList.joinToString(",")) +} + +fun Context.getLangPreferenceDropdownEntries(): Map { + val localeList = getLocaleListFromXml() + val map = mutableMapOf() + + for (a in 0 until localeList.size()) { + localeList[a].let { + map.put(it!!.getDisplayName(it).replaceFirstChar { char -> char.uppercase() }, it.toLanguageTag()) + } + } + return map +} + +fun getLanguageIndex(languageEntries: Map): Int { + var languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag()) + if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.language) + if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf("en") + return languageIndex +} + +@Composable +fun SettingsScreen( + accountViewModel: AccountViewModel, + themeViewModel: ThemeViewModel +) { + val scope = rememberCoroutineScope() + val selectedItens = persistentListOf( + stringResource(R.string.always), + stringResource(R.string.wifi_only), + stringResource(R.string.never).replaceFirstChar { + it.uppercase() + } + ) + val settings = accountViewModel.account.settings + val index = if (settings.automaticallyShowImages == null) { 0 } else { + if (settings.automaticallyShowImages == true) 1 else 2 + } + val videoIndex = if (settings.automaticallyStartPlayback == null) { 0 } else { + if (settings.automaticallyShowImages == true) 1 else 2 + } + val linkIndex = if (settings.automaticallyShowUrlPreview == null) { 0 } else { + if (settings.automaticallyShowUrlPreview == true) 1 else 2 + } + + val themeItens = persistentListOf( + stringResource(R.string.system), + stringResource(R.string.light), + stringResource(R.string.dark) + ) + val themeIndex = themeViewModel.theme.value ?: 0 + + val context = LocalContext.current + + val languageEntries = context.getLangPreferenceDropdownEntries() + val languageList = languageEntries.keys.toImmutableList() + val languageIndex = getLanguageIndex(languageEntries) + + Column( + StdPadding + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Section(stringResource(R.string.application_preferences)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + TextSpinner( + label = stringResource(R.string.language), + placeholder = languageList[languageIndex], + options = languageList, + onSelect = { + scope.launch(Dispatchers.IO) { + val locale = languageEntries[languageList[it]] + accountViewModel.account.settings.preferredLanguage = locale + LocalPreferences.saveToEncryptedStorage(accountViewModel.account) + val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]]) + AppCompatDelegate.setApplicationLocales(appLocale) + } + }, + modifier = Modifier + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .weight(1f) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + TextSpinner( + label = stringResource(R.string.theme), + placeholder = themeItens[themeIndex], + options = themeItens, + onSelect = { + themeViewModel.onChange(it) + scope.launch(Dispatchers.IO) { + LocalPreferences.updateTheme(it) + } + }, + modifier = Modifier + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .weight(1f) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + TextSpinner( + label = stringResource(R.string.automatically_load_images_gifs), + placeholder = selectedItens[index], + options = selectedItens, + onSelect = { + val automaticallyShowImages = when (it) { + 1 -> true + 2 -> false + else -> null + } + + scope.launch(Dispatchers.IO) { + accountViewModel.updateAutomaticallyShowImages(automaticallyShowImages) + LocalPreferences.saveToEncryptedStorage(accountViewModel.account) + } + }, + modifier = Modifier + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .weight(1f) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + TextSpinner( + label = stringResource(R.string.automatically_play_videos), + placeholder = selectedItens[videoIndex], + options = selectedItens, + onSelect = { + val automaticallyStartPlayback = when (it) { + 1 -> true + 2 -> false + else -> null + } + + scope.launch(Dispatchers.IO) { + accountViewModel.updateAutomaticallyStartPlayback(automaticallyStartPlayback) + LocalPreferences.saveToEncryptedStorage(accountViewModel.account) + } + }, + modifier = Modifier + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .weight(1f) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + TextSpinner( + label = stringResource(R.string.automatically_show_url_preview), + placeholder = selectedItens[linkIndex], + options = selectedItens, + onSelect = { + val automaticallyShowUrlPreview = when (it) { + 1 -> true + 2 -> false + else -> null + } + + scope.launch(Dispatchers.IO) { + accountViewModel.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview) + LocalPreferences.saveToEncryptedStorage(accountViewModel.account) + } + }, + modifier = Modifier + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .weight(1f) + ) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun DropDownSettings( + selectedItem: MutableState, + listItems: Array, + title: String +) { + var expanded by remember { + mutableStateOf(false) + } + ExposedDropdownMenuBox( + modifier = Modifier.padding(8.dp), + expanded = expanded, + onExpandedChange = { + expanded = !expanded + } + ) { + TextField( + modifier = Modifier.fillMaxWidth(), + value = selectedItem.value, + onValueChange = {}, + readOnly = true, + label = { Text(text = title) }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded + ) + }, + colors = ExposedDropdownMenuDefaults.textFieldColors() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + listItems.forEach { selectedOption -> + DropdownMenuItem( + onClick = { + selectedItem.value = selectedOption + expanded = false + } + ) { + Text(text = selectedOption) + } + } + } + } +} + +@Composable +fun Section(text: String) { + Spacer(modifier = DoubleVertSpacer) + Text( + text = text, + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ) + Spacer(modifier = DoubleVertSpacer) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 46471728e..cb4f37191 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -14,6 +14,7 @@ import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -27,6 +28,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.resolveDefaults +import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel private val DarkColorPalette = darkColors( primary = Purple200, @@ -304,12 +306,14 @@ val Colors.innerPostModifier: Modifier get() = if (isLight) LightInnerPostBorderModifier else DarkInnerPostBorderModifier @Composable -fun AmethystTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { - val colors = if (darkTheme) { - DarkColorPalette - } else { - LightColorPalette +fun AmethystTheme(themeViewModel: ThemeViewModel, content: @Composable () -> Unit) { + val theme = themeViewModel.theme.observeAsState() + val darkTheme = when (theme.value) { + 2 -> true + 1 -> false + else -> isSystemInDarkTheme() } + val colors = if (darkTheme) DarkColorPalette else LightColorPalette MaterialTheme( colors = colors, @@ -319,10 +323,14 @@ fun AmethystTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composab ) val view = LocalView.current - if (!view.isInEditMode && darkTheme) { + if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - window.statusBarColor = colors.background.toArgb() + if (darkTheme) { + window.statusBarColor = colors.background.toArgb() + } else { + window.statusBarColor = colors.primary.toArgb() + } } } } diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 000000000..f783dd69b --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 437a0e623..196cb2c3a 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ - diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 837070953..7c490ffe7 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -7,17 +7,16 @@ - - + - + - - + +