mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Convert AppearanceSettingsFragment to compose.
This commit is contained in:
committed by
Jeffrey Starke
parent
92d31ee6ff
commit
2db87c36a3
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user