Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,15 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
alias(libs.plugins.kotlin.parcelize)
}
android {
namespace = "net.thunderbird.core.ui.compose.preference"
resourcePrefix = "core_ui_preference_"
}
dependencies {
implementation(projects.core.ui.compose.designsystem)
testImplementation(projects.core.ui.compose.testing)
}

View file

@ -0,0 +1,20 @@
package net.thunderbird.core.ui.compose.preference.ui
import androidx.compose.runtime.Composable
import app.k9mail.core.ui.compose.common.annotation.PreviewDevicesWithBackground
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
import net.thunderbird.core.ui.compose.preference.ui.fake.FakePreferenceData
@Composable
@PreviewDevicesWithBackground
fun PreferenceViewPreview() {
PreviewWithTheme {
PreferenceView(
title = "Title",
subtitle = "Subtitle",
preferences = FakePreferenceData.preferences,
onPreferenceChange = {},
onBack = {},
)
}
}

View file

@ -0,0 +1,29 @@
package net.thunderbird.core.ui.compose.preference.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
@Composable
@Preview(showBackground = true)
internal fun PreferenceTopBarPreview() {
PreviewWithThemes {
PreferenceTopBar(
title = "Title",
subtitle = null,
onBack = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun PreferenceTopBarWithSubtitlePreview() {
PreviewWithThemes {
PreferenceTopBar(
title = "Title",
subtitle = "Subtitle",
onBack = {},
)
}
}

View file

@ -0,0 +1,28 @@
package net.thunderbird.core.ui.compose.preference.ui.components.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
@Composable
@Preview(showBackground = true)
internal fun ColorViewPreview() {
PreviewWithThemes {
ColorView(
color = 0xFFFF0000.toInt(),
onClick = null,
)
}
}
@Composable
@Preview(showBackground = true)
internal fun ColorViewWithSelectionPreview() {
PreviewWithThemes {
ColorView(
color = 0xFFFF0000.toInt(),
onClick = null,
isSelected = true,
)
}
}

View file

@ -0,0 +1,19 @@
package net.thunderbird.core.ui.compose.preference.ui.components.dialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
import net.thunderbird.core.ui.compose.preference.ui.fake.FakePreferenceData
@Composable
@Preview(showBackground = true)
internal fun PreferenceDialogColorViewPreview() {
PreviewWithTheme {
PreferenceDialogColorView(
preference = FakePreferenceData.colorPreference,
onConfirmClick = {},
onDismissClick = {},
onDismissRequest = {},
)
}
}

View file

@ -0,0 +1,22 @@
package net.thunderbird.core.ui.compose.preference.ui.components.dialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
@Composable
@Preview(showBackground = true)
internal fun PreferenceDialogLayoutPreview() {
PreviewWithTheme {
PreferenceDialogLayout(
title = "Dialog",
icon = null,
onConfirmClick = {},
onDismissClick = {},
onDismissRequest = {},
) {
TextBodyMedium("PreferenceDialogLayoutContent")
}
}
}

View file

@ -0,0 +1,19 @@
package net.thunderbird.core.ui.compose.preference.ui.components.dialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
import net.thunderbird.core.ui.compose.preference.ui.fake.FakePreferenceData
@Composable
@Preview(showBackground = true)
internal fun PreferenceDialogSingleChoiceCompactViewPreview() {
PreviewWithTheme {
PreferenceDialogSingleChoiceCompactView(
preference = FakePreferenceData.singleChoiceCompactPreference,
onConfirmClick = {},
onDismissClick = {},
onDismissRequest = {},
)
}
}

View file

@ -0,0 +1,19 @@
package net.thunderbird.core.ui.compose.preference.ui.components.dialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithTheme
import net.thunderbird.core.ui.compose.preference.ui.fake.FakePreferenceData
@Composable
@Preview(showBackground = true)
internal fun PreferenceDialogTextViewPreview() {
PreviewWithTheme {
PreferenceDialogTextView(
preference = FakePreferenceData.textPreference,
onConfirmClick = {},
onDismissClick = {},
onDismissRequest = {},
)
}
}

View file

@ -0,0 +1,17 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.core.ui.compose.preference.ui.fake.FakePreferenceData
@Composable
@Preview(showBackground = true)
internal fun PreferenceItemColorViewPreview() {
PreviewWithThemes {
PreferenceItemColorView(
preference = FakePreferenceData.colorPreference,
onClick = {},
)
}
}

View file

@ -0,0 +1,37 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleLarge
@Composable
@Preview(showBackground = true)
internal fun PreferenceItemLayoutPreview() {
PreviewWithThemes {
PreferenceItemLayout(
onClick = {},
icon = null,
modifier = Modifier.fillMaxWidth(),
) {
TextTitleLarge(text = "PreferenceItemLayoutContent")
}
}
}
@Composable
@Preview(showBackground = true)
internal fun PreferenceItemLayoutWithIconPreview() {
PreviewWithThemes {
PreferenceItemLayout(
onClick = {},
icon = Icons.Outlined.Info,
modifier = Modifier.fillMaxWidth(),
) {
TextTitleLarge(text = "PreferenceItemLayoutContent")
}
}
}

View file

@ -0,0 +1,17 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.core.ui.compose.preference.ui.fake.FakePreferenceData
@Composable
@Preview(showBackground = true)
internal fun PreferenceItemSingleChoiceCompactViewPreview() {
PreviewWithThemes {
PreferenceItemSingleChoiceCompactView(
preference = FakePreferenceData.singleChoiceCompactPreference,
onClick = {},
)
}
}

View file

@ -0,0 +1,28 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.core.ui.compose.preference.ui.fake.FakePreferenceData
@Composable
@Preview(showBackground = true)
internal fun PreferenceItemSingleChoiceViewPreview() {
PreviewWithThemes {
PreferenceItemSingleChoiceView(
preference = FakePreferenceData.singleChoicePreference.copy(description = { null }),
onPreferenceChange = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun PreferenceItemSingleChoiceViewWithDescriptionPreview() {
PreviewWithThemes {
PreferenceItemSingleChoiceView(
preference = FakePreferenceData.singleChoicePreference,
onPreferenceChange = {},
)
}
}

View file

@ -0,0 +1,50 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.core.ui.compose.preference.ui.fake.FakePreferenceData
@Composable
@Preview(showBackground = true)
internal fun Preview_Switch_On_Enabled() {
PreviewWithThemes {
PreferenceItemSwitchView(
preference = FakePreferenceData.switchPreference,
onPreferenceChange = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun Preview_Switch_Off_Enabled() {
PreviewWithThemes {
PreferenceItemSwitchView(
preference = FakePreferenceData.switchPreference.copy(value = false),
onPreferenceChange = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun Preview_Switch_On_Disabled() {
PreviewWithThemes {
PreferenceItemSwitchView(
preference = FakePreferenceData.switchPreference.copy(enabled = false),
onPreferenceChange = {},
)
}
}
@Composable
@Preview(showBackground = true)
internal fun Preview_Switch_Off_Disabled() {
PreviewWithThemes {
PreferenceItemSwitchView(
preference = FakePreferenceData.switchPreference.copy(value = false, enabled = false),
onPreferenceChange = {},
)
}
}

View file

@ -0,0 +1,17 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemes
import net.thunderbird.core.ui.compose.preference.ui.fake.FakePreferenceData
@Composable
@Preview(showBackground = true)
internal fun PreferenceItemTextViewPreview() {
PreviewWithThemes {
PreferenceItemTextView(
preference = FakePreferenceData.textPreference,
onClick = {},
)
}
}

View file

@ -0,0 +1,78 @@
package net.thunderbird.core.ui.compose.preference.ui.fake
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import kotlinx.collections.immutable.persistentListOf
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting.SingleChoice.Choice
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting.SingleChoiceCompact.CompactChoice
internal object FakePreferenceData {
val textPreference = PreferenceSetting.Text(
id = "text",
icon = { Icons.Outlined.Delete },
title = { "Title" },
description = { "Description" },
value = "Value",
)
val colorPreference = PreferenceSetting.Color(
id = "color",
icon = { Icons.Outlined.Delete },
title = { "Title" },
description = { "Description" },
value = 0xFFFF0000.toInt(),
colors = persistentListOf(
0xFFFF0000.toInt(),
0xFF00FF00.toInt(),
0xFF0000FF.toInt(),
),
)
private val choices = persistentListOf<Choice>(
Choice("1") { "Choice 1" },
Choice("2") { "Choice 2" },
Choice("3") { "Choice 3" },
)
val singleChoicePreference = PreferenceSetting.SingleChoice(
id = "single_choice",
title = { "Title" },
description = { "Description" },
value = choices[1],
options = choices,
)
private val compactChoices = persistentListOf<CompactChoice>(
CompactChoice("1") { "Compact Choice 1" },
CompactChoice("2") { "Compact Choice 2" },
CompactChoice("3") { "Compact Choice 3" },
CompactChoice("1") { "Compact Choice 4" },
CompactChoice("2") { "Compact Choice 5" },
CompactChoice("3") { "Compact Choice 6" },
)
val singleChoiceCompactPreference = PreferenceSetting.SingleChoiceCompact(
id = "single_choice_compact",
title = { "Title" },
icon = { Icons.Outlined.Info },
description = { "Description" },
value = compactChoices[1],
options = compactChoices,
)
val switchPreference = PreferenceSetting.Switch(
id = "switch",
title = { "Title" },
description = { "Description" },
enabled = true,
value = true,
)
val preferences = persistentListOf(
textPreference,
colorPreference,
switchPreference,
singleChoicePreference,
)
}

View file

@ -0,0 +1,125 @@
package net.thunderbird.core.ui.compose.preference.api
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import kotlinx.collections.immutable.ImmutableList
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting.SingleChoice.Choice
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting.SingleChoiceCompact.CompactChoice
/**
* A preference that can be displayed in a preference screen.
*/
sealed interface Preference : Parcelable {
val id: String
}
/**
* A preference that holds a value of type [T].
*/
sealed interface PreferenceSetting<T> : Preference {
val value: T
val requiresEditView: Boolean
@Parcelize
data class Text(
override val id: String,
val title: () -> String,
val description: () -> String? = { null },
val icon: () -> ImageVector? = { null },
override val value: String,
) : PreferenceSetting<String> {
@IgnoredOnParcel
override val requiresEditView: Boolean = true
}
@Parcelize
data class Color(
override val id: String,
val title: () -> String,
val description: () -> String? = { null },
val icon: () -> ImageVector? = { null },
override val value: Int,
val colors: ImmutableList<Int>,
) : PreferenceSetting<Int> {
@IgnoredOnParcel
override val requiresEditView: Boolean = true
}
@Parcelize
data class SingleChoice(
override val id: String,
val title: () -> String,
val description: () -> String? = { null },
override val value: Choice,
val options: ImmutableList<Choice>,
) : PreferenceSetting<Choice> {
@IgnoredOnParcel
override val requiresEditView: Boolean = false
@Parcelize
data class Choice(
val id: String,
val title: () -> String,
) : Parcelable
}
@Parcelize
data class SingleChoiceCompact(
override val id: String,
val title: () -> String,
val description: () -> String? = { null },
val icon: () -> ImageVector? = { null },
override val value: CompactChoice,
val options: ImmutableList<CompactChoice>,
) : PreferenceSetting<CompactChoice> {
@IgnoredOnParcel
override val requiresEditView: Boolean = true
@Parcelize
data class CompactChoice(
val id: String,
val title: () -> String,
) : Parcelable
}
@Parcelize
data class Switch(
override val id: String,
val title: () -> String,
val description: () -> String? = { null },
val enabled: Boolean,
override val value: Boolean,
) : PreferenceSetting<Boolean> {
@IgnoredOnParcel
override val requiresEditView: Boolean = false
}
}
/**
* A preference that does not hold a value. It is used to display a section, a divider or custom UI.
*/
sealed interface PreferenceDisplay : Preference {
@Parcelize
data class Custom(
override val id: String,
val customUi: @Composable (Modifier) -> Unit,
) : PreferenceDisplay
@Parcelize
data class SectionHeader(
override val id: String,
val title: () -> String,
val color: () -> Color = { Color.Unspecified },
) : PreferenceDisplay
@Parcelize
data class SectionDivider(
override val id: String,
) : PreferenceDisplay
}

View file

@ -0,0 +1,36 @@
package net.thunderbird.core.ui.compose.preference.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import kotlinx.collections.immutable.ImmutableList
import net.thunderbird.core.ui.compose.preference.api.Preference
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
/**
* A view that displays a list of preferences.
*
* @param title The title of the view.
* @param subtitle The subtitle of the view (optional).
* @param preferences The list of preferences to display.
* @param onPreferenceChange The callback to be invoked when a preference is changed.
* @param onBack The callback to be invoked when the back button is clicked.
* @param modifier The modifier to be applied to the view.
*/
@Composable
fun PreferenceView(
title: String,
preferences: ImmutableList<Preference>,
onPreferenceChange: (PreferenceSetting<*>) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
subtitle: String? = null,
) {
PreferenceViewWithDialog(
title = title,
subtitle = subtitle,
preferences = preferences,
onPreferenceChange = onPreferenceChange,
onBack = onBack,
modifier = modifier,
)
}

View file

@ -0,0 +1,70 @@
package net.thunderbird.core.ui.compose.preference.ui
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
import app.k9mail.core.ui.compose.designsystem.template.Scaffold
import kotlinx.collections.immutable.ImmutableList
import net.thunderbird.core.ui.compose.preference.api.Preference
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
import net.thunderbird.core.ui.compose.preference.ui.components.PreferenceTopBar
import net.thunderbird.core.ui.compose.preference.ui.components.dialog.PreferenceDialog
import net.thunderbird.core.ui.compose.preference.ui.components.list.PreferenceList
@Composable
internal fun PreferenceViewWithDialog(
title: String,
preferences: ImmutableList<Preference>,
onPreferenceChange: (PreferenceSetting<*>) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
subtitle: String? = null,
) {
var showDialog by rememberSaveable { mutableStateOf(false) }
var selectedIndex by rememberSaveable { mutableIntStateOf(0) }
Scaffold(
topBar = {
PreferenceTopBar(
title = title,
subtitle = subtitle,
onBack = onBack,
)
},
modifier = modifier,
) { innerPadding ->
ResponsiveWidthContainer { contentPadding ->
PreferenceList(
preferences = preferences,
onItemClick = { index, _ ->
selectedIndex = index
showDialog = true
},
onPreferenceChange = onPreferenceChange,
modifier = Modifier
.padding(innerPadding)
.padding(contentPadding),
)
}
}
if (showDialog) {
val preference = preferences[selectedIndex]
PreferenceDialog(
preference = preference,
onConfirmClick = { preference ->
onPreferenceChange(preference)
showDialog = false
},
onDismissClick = { showDialog = false },
onDismissRequest = { showDialog = false },
)
}
}

View file

@ -0,0 +1,25 @@
package net.thunderbird.core.ui.compose.preference.ui.components
import androidx.compose.runtime.Composable
import app.k9mail.core.ui.compose.designsystem.organism.SubtitleTopAppBarWithBackButton
import app.k9mail.core.ui.compose.designsystem.organism.TopAppBarWithBackButton
@Composable
internal fun PreferenceTopBar(
title: String,
subtitle: String?,
onBack: () -> Unit,
) {
if (subtitle != null) {
SubtitleTopAppBarWithBackButton(
title = title,
subtitle = subtitle,
onBackClick = onBack,
)
} else {
TopAppBarWithBackButton(
title = title,
onBackClick = onBack,
)
}
}

View file

@ -0,0 +1,46 @@
package net.thunderbird.core.ui.compose.preference.ui.components.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icons
import app.k9mail.core.ui.compose.theme2.MainTheme
@Composable
internal fun ColorView(
color: Int,
onClick: ((Int) -> Unit)?,
modifier: Modifier = Modifier,
isSelected: Boolean = false,
size: Dp = MainTheme.sizes.icon,
) {
Surface(
color = Color(color),
modifier = modifier
.size(size)
.clip(CircleShape)
.let {
if (onClick != null) {
it.clickable(onClick = { onClick(color) })
} else {
it
}
},
) {
if (isSelected) {
Icon(
tint = MainTheme.colors.onSecondary,
imageVector = Icons.Outlined.Check,
modifier = Modifier.padding(MainTheme.spacings.default),
)
}
}
}

View file

@ -0,0 +1,54 @@
package net.thunderbird.core.ui.compose.preference.ui.components.dialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import net.thunderbird.core.ui.compose.preference.api.Preference
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
@Composable
internal fun PreferenceDialog(
preference: Preference,
onConfirmClick: (PreferenceSetting<*>) -> Unit,
onDismissClick: () -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
) {
require(preference is PreferenceSetting<*>) {
"Unsupported preference type: ${preference::class.java.simpleName}"
}
when (preference) {
is PreferenceSetting.Text -> {
PreferenceDialogTextView(
preference = preference,
onConfirmClick = onConfirmClick,
onDismissClick = onDismissClick,
onDismissRequest = onDismissRequest,
modifier = modifier,
)
}
is PreferenceSetting.Color -> {
PreferenceDialogColorView(
preference = preference,
onConfirmClick = onConfirmClick,
onDismissClick = onDismissClick,
onDismissRequest = onDismissRequest,
modifier = modifier,
)
}
is PreferenceSetting.SingleChoiceCompact -> {
PreferenceDialogSingleChoiceCompactView(
preference = preference,
onConfirmClick = onConfirmClick,
onDismissClick = onDismissClick,
onDismissRequest = onDismissRequest,
modifier = modifier,
)
}
// No dialog needed
is PreferenceSetting.SingleChoice, is PreferenceSetting.Switch -> Unit
}
}

View file

@ -0,0 +1,62 @@
package net.thunderbird.core.ui.compose.preference.ui.components.dialog
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
import net.thunderbird.core.ui.compose.preference.ui.components.common.ColorView
@Composable
internal fun PreferenceDialogColorView(
preference: PreferenceSetting.Color,
onConfirmClick: (PreferenceSetting<*>) -> Unit,
onDismissClick: () -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
) {
val currentColor = rememberSaveable { mutableIntStateOf(preference.value) }
val gridState = rememberLazyGridState()
PreferenceDialogLayout(
title = preference.title(),
icon = preference.icon(),
onConfirmClick = { onConfirmClick(preference.copy(value = currentColor.intValue)) },
onDismissClick = onDismissClick,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
preference.description()?.let {
TextBodyMedium(text = it)
Spacer(modifier = Modifier.height(MainTheme.spacings.double))
}
LazyVerticalGrid(
state = gridState,
columns = GridCells.Adaptive(minSize = MainTheme.sizes.iconAvatar),
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
) {
items(preference.colors) { color ->
ColorView(
color = color,
onClick = { newColor ->
currentColor.intValue = newColor
},
isSelected = color == currentColor.intValue,
modifier = Modifier.size(MainTheme.sizes.iconAvatar),
)
}
}
}
}

View file

@ -0,0 +1,42 @@
package net.thunderbird.core.ui.compose.preference.ui.components.dialog
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import app.k9mail.core.ui.compose.designsystem.organism.AlertDialog
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.preference.R
@Composable
internal fun PreferenceDialogLayout(
title: String,
icon: ImageVector?,
onConfirmClick: () -> Unit,
onDismissClick: () -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
AlertDialog(
title = title,
icon = icon,
confirmText = stringResource(id = R.string.core_ui_preference_dialog_button_accept),
onConfirmClick = onConfirmClick,
dismissText = stringResource(id = R.string.core_ui_preference_dialog_button_cancel),
onDismissClick = onDismissClick,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
Column(
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.half),
modifier = Modifier.fillMaxWidth(),
) {
content()
}
}
}

View file

@ -0,0 +1,50 @@
package net.thunderbird.core.ui.compose.preference.ui.components.dialog
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.RadioGroup
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
@Composable
internal fun PreferenceDialogSingleChoiceCompactView(
preference: PreferenceSetting.SingleChoiceCompact,
onConfirmClick: (PreferenceSetting<*>) -> Unit,
onDismissClick: () -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
) {
val options by remember { mutableStateOf(preference.options) }
var selectedOption by remember { mutableStateOf(preference.value) }
PreferenceDialogLayout(
title = preference.title(),
icon = preference.icon(),
onConfirmClick = {
onConfirmClick(preference.copy(value = selectedOption))
},
onDismissClick = onDismissClick,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
preference.description()?.let {
TextBodyMedium(text = it)
Spacer(modifier = Modifier.height(MainTheme.spacings.default))
}
RadioGroup(
onClick = { selectedOption = it },
options = options,
optionTitle = { it.title() },
selectedOption = selectedOption,
)
}
}

View file

@ -0,0 +1,76 @@
package net.thunderbird.core.ui.compose.preference.ui.components.dialog
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.designsystem.molecule.input.AdvancedTextInput
import app.k9mail.core.ui.compose.theme2.MainTheme
import kotlinx.coroutines.delay
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
// This a workaround for a bug in Compose, preventing the keyboard been show when requesting focus on a dialog,
// see: https://issuetracker.google.com/issues/204502668
private const val EDIT_TEXT_FOCUS_DELAY = 200L
@Composable
internal fun PreferenceDialogTextView(
preference: PreferenceSetting.Text,
onConfirmClick: (PreferenceSetting<*>) -> Unit,
onDismissClick: () -> Unit,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusRequester = remember { FocusRequester() }
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(
TextFieldValue(
text = preference.value,
selection = TextRange(preference.value.length),
),
)
}
LaunchedEffect(Unit) {
delay(EDIT_TEXT_FOCUS_DELAY)
focusRequester.requestFocus()
}
PreferenceDialogLayout(
title = preference.title(),
icon = preference.icon(),
onConfirmClick = {
onConfirmClick(preference.copy(value = textFieldValue.text))
},
onDismissClick = onDismissClick,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
preference.description()?.let {
TextBodyMedium(text = it)
Spacer(modifier = Modifier.height(MainTheme.spacings.default))
}
AdvancedTextInput(
text = textFieldValue,
contentPadding = PaddingValues(),
onTextChange = { changedText ->
textFieldValue = changedText
},
modifier = Modifier.focusRequester(focusRequester),
)
}
}

View file

@ -0,0 +1,77 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import net.thunderbird.core.ui.compose.preference.api.Preference
import net.thunderbird.core.ui.compose.preference.api.PreferenceDisplay
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
@Composable
internal fun PreferenceItem(
preference: Preference,
onClick: () -> Unit,
onPreferenceChange: (PreferenceSetting<*>) -> Unit,
modifier: Modifier = Modifier,
) {
when (preference) {
// PreferenceSetting
is PreferenceSetting.Text -> {
PreferenceItemTextView(
preference = preference,
onClick = onClick,
modifier = modifier,
)
}
is PreferenceSetting.Color -> {
PreferenceItemColorView(
preference = preference,
onClick = onClick,
modifier = modifier,
)
}
is PreferenceSetting.SingleChoice -> {
PreferenceItemSingleChoiceView(
preference = preference,
onPreferenceChange = onPreferenceChange,
modifier = modifier,
)
}
is PreferenceSetting.Switch -> {
PreferenceItemSwitchView(
preference = preference,
onPreferenceChange = onPreferenceChange,
modifier = modifier,
)
}
is PreferenceSetting.SingleChoiceCompact -> PreferenceItemSingleChoiceCompactView(
preference = preference,
onClick = onClick,
modifier = modifier,
)
// PreferenceDisplay
is PreferenceDisplay.Custom -> {
PreferenceItemCustomView(
preference = preference,
modifier = modifier,
)
}
is PreferenceDisplay.SectionHeader -> {
PreferenceItemSectionHeaderView(
preference = preference,
modifier = modifier,
)
}
is PreferenceDisplay.SectionDivider -> {
PreferenceItemSectionDividerView(
modifier = modifier,
)
}
}
}

View file

@ -0,0 +1,44 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
import net.thunderbird.core.ui.compose.preference.ui.components.common.ColorView
@Composable
internal fun PreferenceItemColorView(
preference: PreferenceSetting.Color,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceItemLayout(
onClick = onClick,
icon = preference.icon(),
modifier = modifier,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Column(
Modifier.weight(1f),
) {
TextTitleMedium(text = preference.title())
preference.description()?.let {
TextBodyMedium(text = it)
}
}
ColorView(
color = preference.value,
onClick = null,
modifier = Modifier.padding(start = MainTheme.spacings.default),
)
}
}
}

View file

@ -0,0 +1,13 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import net.thunderbird.core.ui.compose.preference.api.PreferenceDisplay
@Composable
internal fun PreferenceItemCustomView(
preference: PreferenceDisplay.Custom,
modifier: Modifier = Modifier,
) {
preference.customUi(modifier)
}

View file

@ -0,0 +1,49 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
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.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import app.k9mail.core.ui.compose.designsystem.atom.icon.Icon
import app.k9mail.core.ui.compose.theme2.MainTheme
@Composable
internal fun PreferenceItemLayout(
onClick: () -> Unit,
icon: ImageVector?,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
Box(
modifier = modifier
.clickable(onClick = onClick),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.padding(MainTheme.spacings.double),
) {
Icon(
imageVector = it,
)
}
}
Column(
modifier = Modifier.padding(MainTheme.spacings.double),
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.half),
) {
content()
}
}
}
}

View file

@ -0,0 +1,14 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.DividerHorizontal
@Composable
internal fun PreferenceItemSectionDividerView(
modifier: Modifier = Modifier,
) {
DividerHorizontal(
modifier = modifier,
)
}

View file

@ -0,0 +1,22 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.preference.api.PreferenceDisplay
@Composable
internal fun PreferenceItemSectionHeaderView(
preference: PreferenceDisplay.SectionHeader,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.padding(MainTheme.spacings.double)) {
TextTitleMedium(
text = preference.title(),
color = preference.color(),
)
}
}

View file

@ -0,0 +1,36 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
@Composable
internal fun PreferenceItemSingleChoiceCompactView(
preference: PreferenceSetting.SingleChoiceCompact,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceItemLayout(
onClick = onClick,
icon = preference.icon(),
modifier = modifier,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Column(
Modifier.weight(1f),
) {
TextTitleMedium(text = preference.value.title())
preference.description()?.let {
TextBodyMedium(text = it)
}
}
}
}
}

View file

@ -0,0 +1,43 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.button.ButtonSegmentedSingleChoice
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
@Composable
internal fun PreferenceItemSingleChoiceView(
preference: PreferenceSetting.SingleChoice,
onPreferenceChange: (PreferenceSetting<*>) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.padding(MainTheme.spacings.double),
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.half),
) {
TextTitleMedium(text = preference.title())
ButtonSegmentedSingleChoice(
onClick = {
onPreferenceChange(preference.copy(value = it))
},
options = preference.options,
optionTitle = { it.title() },
selectedOption = preference.value,
)
preference.description()?.let {
TextBodyMedium(
modifier = Modifier.padding(start = MainTheme.spacings.oneHalf),
color = MainTheme.colors.onSurfaceVariant,
text = it,
)
}
}
}

View file

@ -0,0 +1,41 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.Switch
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
import app.k9mail.core.ui.compose.theme2.MainTheme
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
@Composable
internal fun PreferenceItemSwitchView(
preference: PreferenceSetting.Switch,
modifier: Modifier = Modifier,
onPreferenceChange: (PreferenceSetting<*>) -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.padding(MainTheme.spacings.double),
) {
Column(
Modifier.weight(1f),
) {
TextTitleMedium(text = preference.title())
preference.description()?.let {
TextBodyMedium(text = it)
}
}
Switch(
checked = preference.value,
onCheckedChange = {
onPreferenceChange(preference.copy(value = it))
},
enabled = preference.enabled,
)
}
}

View file

@ -0,0 +1,23 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium
import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
@Composable
internal fun PreferenceItemTextView(
preference: PreferenceSetting.Text,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceItemLayout(
onClick = onClick,
icon = preference.icon(),
modifier = modifier,
) {
TextTitleMedium(text = preference.title())
TextBodyMedium(text = preference.value)
}
}

View file

@ -0,0 +1,33 @@
package net.thunderbird.core.ui.compose.preference.ui.components.list
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import kotlinx.collections.immutable.ImmutableList
import net.thunderbird.core.ui.compose.preference.api.Preference
import net.thunderbird.core.ui.compose.preference.api.PreferenceSetting
@Composable
internal fun PreferenceList(
preferences: ImmutableList<Preference>,
onItemClick: (index: Int, item: Preference) -> Unit,
onPreferenceChange: (PreferenceSetting<*>) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
) {
itemsIndexed(preferences) { index, item ->
PreferenceItem(
preference = item,
onClick = {
onItemClick(index, item)
},
onPreferenceChange = onPreferenceChange,
modifier = Modifier.fillMaxWidth(),
)
}
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="core_ui_preference_dialog_button_accept">Accept</string>
<string name="core_ui_preference_dialog_button_cancel">Cancel</string>
</resources>