Convert AppearanceSettingsFragment to compose.

This commit is contained in:
Alex Hart
2025-08-20 15:11:19 -03:00
committed by Jeffrey Starke
parent 92d31ee6ff
commit 2db87c36a3
4 changed files with 321 additions and 89 deletions

View File

@@ -1,39 +1,40 @@
package org.thoughtcrime.securesms.components.settings.app.appearance
import android.os.Build
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import org.signal.core.util.concurrent.observe
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.integerArrayResource
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.appearance.navbar.ChooseNavigationBarStyleFragment
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.keyvalue.SettingsValues
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__appearance) {
/**
* Allows the user to change language, theme, etc. from application settings.
*/
class AppearanceSettingsFragment : ComposeFragment() {
private lateinit var viewModel: AppearanceSettingsViewModel
private val themeLabels by lazy { resources.getStringArray(R.array.pref_theme_entries) }
private val themeValues by lazy { resources.getStringArray(R.array.pref_theme_values) }
private val messageFontSizeLabels by lazy { resources.getStringArray(R.array.pref_message_font_size_entries) }
private val messageFontSizeValues by lazy { resources.getIntArray(R.array.pref_message_font_size_values) }
private val languageLabels by lazy { resources.getStringArray(R.array.language_entries) }
private val languageValues by lazy { resources.getStringArray(R.array.language_values) }
override fun bindAdapter(adapter: MappingAdapter) {
viewModel = ViewModelProvider(this)[AppearanceSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
private val viewModel: AppearanceSettingsViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
childFragmentManager.setFragmentResultListener(ChooseNavigationBarStyleFragment.REQUEST_KEY, viewLifecycleOwner) { key, bundle ->
if (bundle.getBoolean(key, false)) {
viewModel.refreshState()
@@ -41,64 +42,148 @@ class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__app
}
}
private fun getConfiguration(state: AppearanceSettingsState): DSLConfiguration {
return configure {
radioListPref(
title = DSLSettingsText.from(R.string.preferences__language),
listItems = languageLabels,
selected = languageValues.indexOf(state.language),
onSelected = {
viewModel.setLanguage(languageValues[it])
}
)
@Composable
override fun FragmentContent() {
val callbacks = remember { Callbacks() }
val state by viewModel.state.collectAsStateWithLifecycle()
radioListPref(
title = DSLSettingsText.from(R.string.preferences__theme),
listItems = themeLabels,
selected = themeValues.indexOf(state.theme.serialize()),
onSelected = {
viewModel.setTheme(activity, SettingsValues.Theme.deserialize(themeValues[it]))
}
)
AppearanceSettingsScreen(
state = state,
callbacks = callbacks
)
}
clickPref(
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_appearanceSettings_to_wallpaperActivity)
}
)
private inner class Callbacks : AppearanceSettingsCallbacks {
override fun onNavigationClick() {
requireActivity().onBackPressedDispatcher.onBackPressed()
}
if (Build.VERSION.SDK_INT >= 26) {
clickPref(
title = DSLSettingsText.from(R.string.preferences__app_icon),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_appearanceSettings_to_appIconActivity)
}
)
}
override fun onLanguageSelected(selection: String) {
viewModel.setLanguage(selection)
}
radioListPref(
title = DSLSettingsText.from(R.string.preferences_chats__message_text_size),
listItems = messageFontSizeLabels,
selected = messageFontSizeValues.indexOf(state.messageFontSize),
onSelected = {
viewModel.setMessageFontSize(messageFontSizeValues[it].toInt())
}
)
override fun onThemeSelected(selection: String) {
viewModel.setTheme(activity, SettingsValues.Theme.deserialize(selection))
}
clickPref(
title = DSLSettingsText.from(R.string.preferences_navigation_bar_size),
summary = DSLSettingsText.from(
if (state.isCompactNavigationBar) {
R.string.preferences_compact
} else {
R.string.preferences_normal
}
),
onClick = {
ChooseNavigationBarStyleFragment().show(childFragmentManager, null)
}
)
override fun onChatColorAndWallpaperClick() {
findNavController().safeNavigate(R.id.action_appearanceSettings_to_wallpaperActivity)
}
override fun onAppIconClick() {
findNavController().safeNavigate(R.id.action_appearanceSettings_to_appIconActivity)
}
override fun onMessageFontSizeSelected(selection: String) {
viewModel.setMessageFontSize(selection.toInt())
}
override fun onNavigationBarSizeClick() {
ChooseNavigationBarStyleFragment().show(childFragmentManager, null)
}
}
}
interface AppearanceSettingsCallbacks {
fun onNavigationClick() = Unit
fun onLanguageSelected(selection: String) = Unit
fun onThemeSelected(selection: String) = Unit
fun onChatColorAndWallpaperClick() = Unit
fun onAppIconClick() = Unit
fun onMessageFontSizeSelected(selection: String) = Unit
fun onNavigationBarSizeClick() = Unit
object Empty : AppearanceSettingsCallbacks
}
@Composable
private fun AppearanceSettingsScreen(
state: AppearanceSettingsState,
callbacks: AppearanceSettingsCallbacks
) {
Scaffolds.Settings(
title = stringResource(R.string.preferences__appearance),
onNavigationClick = callbacks::onNavigationClick,
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues)
) {
item {
Rows.RadioListRow(
text = stringResource(R.string.preferences__language),
labels = stringArrayResource(R.array.language_entries),
values = stringArrayResource(R.array.language_values),
selectedValue = state.language,
onSelected = callbacks::onLanguageSelected
)
}
item {
Rows.RadioListRow(
text = stringResource(R.string.preferences__theme),
labels = stringArrayResource(R.array.pref_theme_entries),
values = stringArrayResource(R.array.pref_theme_values),
selectedValue = state.theme.serialize(),
onSelected = callbacks::onThemeSelected
)
}
item {
Rows.TextRow(
text = stringResource(R.string.preferences__chat_color_and_wallpaper),
onClick = callbacks::onChatColorAndWallpaperClick
)
}
if (Build.VERSION.SDK_INT >= 26) {
item {
Rows.TextRow(
text = stringResource(R.string.preferences__app_icon),
onClick = callbacks::onAppIconClick
)
}
}
item {
Rows.RadioListRow(
text = stringResource(R.string.preferences_chats__message_text_size),
labels = stringArrayResource(R.array.pref_message_font_size_entries),
values = integerArrayResource(R.array.pref_message_font_size_values).map { it.toString() }.toTypedArray(),
selectedValue = state.messageFontSize.toString(),
onSelected = callbacks::onMessageFontSizeSelected
)
}
item {
val label = if (state.isCompactNavigationBar) {
R.string.preferences_compact
} else {
R.string.preferences_normal
}
Rows.TextRow(
text = stringResource(R.string.preferences_navigation_bar_size),
label = stringResource(label),
onClick = callbacks::onNavigationBarSizeClick
)
}
}
}
}
@SignalPreview
@Composable
private fun AppearanceSettingsScreenPreview() {
Previews.Preview {
AppearanceSettingsScreen(
state = AppearanceSettingsState(
theme = SettingsValues.Theme.SYSTEM,
messageFontSize = 0,
language = "en-US",
isCompactNavigationBar = false
),
callbacks = AppearanceSettingsCallbacks.Empty
)
}
}

View File

@@ -2,21 +2,17 @@ package org.thoughtcrime.securesms.components.settings.app.appearance
import android.app.Activity
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob
import org.thoughtcrime.securesms.keyvalue.SettingsValues.Theme
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.SplashScreenUtil
import org.thoughtcrime.securesms.util.rx.RxStore
class AppearanceSettingsViewModel : ViewModel() {
private val store = RxStore(getState())
val state: Flowable<AppearanceSettingsState> = store.stateFlowable
override fun onCleared() {
super.onCleared()
store.dispose()
}
private val store = MutableStateFlow(getState())
val state: StateFlow<AppearanceSettingsState> = store
fun refreshState() {
store.update { getState() }

View File

@@ -7,22 +7,28 @@ package org.signal.core.ui.compose
import android.R
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -218,7 +224,9 @@ object Dialogs {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().fillMaxHeight()
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Spacer(modifier = Modifier.size(24.dp))
CircularProgressIndicator()
@@ -342,6 +350,76 @@ object Dialogs {
}
}
@Composable
fun RadioListDialog(
onDismissRequest: () -> Unit,
properties: DialogProperties = DialogProperties(),
title: String,
labels: Array<String>,
values: Array<String>,
selectedIndex: Int,
onSelected: (Int) -> Unit
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = properties
) {
Surface(
modifier = Modifier
.padding(vertical = 100.dp)
.background(
color = SignalTheme.colors.colorSurface2,
shape = AlertDialogDefaults.shape
)
.clip(AlertDialogDefaults.shape)
) {
Column {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp).horizontalGutters()
)
LazyColumn(
modifier = Modifier.padding(top = 24.dp, bottom = 16.dp),
state = rememberLazyListState(
initialFirstVisibleItemIndex = selectedIndex
)
) {
items(
count = values.size,
key = { values[it] }
) { index ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 48.dp)
.clickable(
enabled = true,
onClick = {
onSelected(index)
onDismissRequest()
}
)
.horizontalGutters()
) {
RadioButton(
enabled = true,
selected = index == selectedIndex,
onClick = null,
modifier = Modifier.padding(end = 24.dp)
)
Text(text = labels[index])
}
}
}
}
}
}
}
/**
* Alert dialog that supports three options.
* If you only need two options (confirm/dismiss), use [SimpleAlertDialog] instead.
@@ -485,3 +563,19 @@ private fun IndeterminateProgressDialogCancellablePreview() {
Dialogs.IndeterminateProgressDialog("Completing...", "Do not close app", "Cancel") {}
}
}
@SignalPreview
@Composable
private fun RadioListDialogPreview() {
Previews.Preview {
Dialogs.RadioListDialog(
onDismissRequest = {},
title = "TestDialog",
properties = DialogProperties(),
labels = arrayOf(),
values = arrayOf(),
selectedIndex = -1,
onSelected = {}
)
}
}

View File

@@ -144,6 +144,45 @@ object Rows {
}
}
@Composable
fun RadioListRow(
text: String,
labels: Array<String>,
values: Array<String>,
selectedValue: String,
onSelected: (String) -> Unit
) {
val selectedIndex = values.indexOf(selectedValue)
val selectedLabel = if (selectedIndex in labels.indices) {
labels[selectedIndex]
} else {
null
}
var displayDialog by remember { mutableStateOf(false) }
TextRow(
text = text,
label = selectedLabel,
onClick = {
displayDialog = true
}
)
if (displayDialog) {
Dialogs.RadioListDialog(
onDismissRequest = { displayDialog = false },
labels = labels,
values = values,
selectedIndex = selectedIndex,
title = text,
onSelected = {
onSelected(values[it])
}
)
}
}
/**
* Row that positions [text] and optional [label] in a [TextAndLabel] to the side of a [Switch].
*
@@ -565,3 +604,21 @@ private fun TextAndLabelPreview() {
}
}
}
@SignalPreview
@Composable
private fun RadioListRowPreview() {
var selectedValue by remember { mutableStateOf("b") }
Previews.Preview {
Rows.RadioListRow(
text = "Radio List",
labels = arrayOf("A", "B", "C"),
values = arrayOf("a", "b", "c"),
selectedValue = selectedValue,
onSelected = {
selectedValue = it
}
)
}
}