session-android/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt

277 lines
8.9 KiB
Kotlin

package org.thoughtcrime.securesms.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ScrollState
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.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Card
import androidx.compose.material.Colors
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.RadioButton
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.runIf
import org.thoughtcrime.securesms.components.ProfilePictureView
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCard
import kotlin.math.min
interface Callbacks<in T> {
fun onSetClick(): Any?
fun setValue(value: T)
}
object NoOpCallbacks: Callbacks<Any> {
override fun onSetClick() {}
override fun setValue(value: Any) {}
}
data class RadioOption<T>(
val value: T,
val title: GetString,
val subtitle: GetString? = null,
val contentDescription: GetString = title,
val selected: Boolean = false,
val enabled: Boolean = true,
)
@Composable
fun <T> OptionsCard(card: OptionsCard<T>, callbacks: Callbacks<T>) {
Text(text = card.title())
CellNoMargin {
LazyColumn(
modifier = Modifier.heightIn(max = 5000.dp)
) {
itemsIndexed(card.options) { i, it ->
if (i != 0) Divider()
TitledRadioButton(it) { callbacks.setValue(it.value) }
}
}
}
}
@Composable
fun ItemButton(
text: String,
@DrawableRes icon: Int,
colors: ButtonColors = transparentButtonColors(),
contentDescription: String = text,
onClick: () -> Unit
) {
TextButton(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
colors = colors,
onClick = onClick,
shape = RectangleShape,
) {
Box(modifier = Modifier
.width(80.dp)
.fillMaxHeight()) {
Icon(
painter = painterResource(id = icon),
contentDescription = contentDescription,
modifier = Modifier.align(Alignment.Center)
)
}
Text(text, modifier = Modifier.fillMaxWidth())
}
}
@Composable
fun Cell(content: @Composable () -> Unit) {
CellWithPaddingAndMargin(padding = 0.dp) { content() }
}
@Composable
fun CellNoMargin(content: @Composable () -> Unit) {
CellWithPaddingAndMargin(padding = 0.dp, margin = 0.dp) { content() }
}
@Composable
fun CellWithPaddingAndMargin(
padding: Dp = 24.dp,
margin: Dp = 32.dp,
content: @Composable () -> Unit
) {
Card(
backgroundColor = MaterialTheme.colors.cellColor,
shape = RoundedCornerShape(16.dp),
elevation = 0.dp,
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
.padding(horizontal = margin),
) {
Box(Modifier.padding(padding)) { content() }
}
}
@Composable
fun <T> TitledRadioButton(option: RadioOption<T>, onClick: () -> Unit) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.runIf(option.enabled) { clickable { if (!option.selected) onClick() } }
.heightIn(min = 60.dp)
.padding(horizontal = 32.dp)
.contentDescription(option.contentDescription)
) {
Column(modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)) {
Column {
Text(
text = option.title(),
fontSize = 16.sp,
modifier = Modifier.alpha(if (option.enabled) 1f else 0.5f)
)
option.subtitle?.let {
Text(
text = it(),
fontSize = 11.sp,
modifier = Modifier.alpha(if (option.enabled) 1f else 0.5f)
)
}
}
}
RadioButton(
selected = option.selected,
onClick = null,
enabled = option.enabled,
modifier = Modifier
.height(26.dp)
.align(Alignment.CenterVertically)
)
}
}
@Composable
fun Modifier.contentDescription(text: GetString?): Modifier {
val context = LocalContext.current
return text?.let { semantics { contentDescription = it(context) } } ?: this
}
@Composable
fun OutlineButton(text: GetString, contentDescription: GetString? = text, modifier: Modifier = Modifier, onClick: () -> Unit) {
OutlinedButton(
modifier = modifier.size(108.dp, 34.dp)
.contentDescription(contentDescription),
onClick = onClick,
border = BorderStroke(1.dp, LocalExtraColors.current.prominentButtonColor),
shape = RoundedCornerShape(50), // = 50% percent
colors = ButtonDefaults.outlinedButtonColors(
contentColor = LocalExtraColors.current.prominentButtonColor,
backgroundColor = MaterialTheme.colors.background
)
){
Text(text = text())
}
}
private val Colors.cellColor: Color
@Composable
get() = LocalExtraColors.current.settingsBackground
fun Modifier.fadingEdges(
scrollState: ScrollState,
topEdgeHeight: Dp = 0.dp,
bottomEdgeHeight: Dp = 20.dp
): Modifier = this.then(
Modifier
// adding layer fixes issue with blending gradient and content
.graphicsLayer { alpha = 0.99F }
.drawWithContent {
drawContent()
val topColors = listOf(Color.Transparent, Color.Black)
val topStartY = scrollState.value.toFloat()
val topGradientHeight = min(topEdgeHeight.toPx(), topStartY)
if (topGradientHeight > 0f) drawRect(
brush = Brush.verticalGradient(
colors = topColors,
startY = topStartY,
endY = topStartY + topGradientHeight
),
blendMode = BlendMode.DstIn
)
val bottomColors = listOf(Color.Black, Color.Transparent)
val bottomEndY = size.height - scrollState.maxValue + scrollState.value
val bottomGradientHeight =
min(bottomEdgeHeight.toPx(), scrollState.maxValue.toFloat() - scrollState.value)
if (bottomGradientHeight > 0f) drawRect(
brush = Brush.verticalGradient(
colors = bottomColors,
startY = bottomEndY - bottomGradientHeight,
endY = bottomEndY
),
blendMode = BlendMode.DstIn
)
}
)
@Composable
fun Divider() {
androidx.compose.material.Divider(
modifier = Modifier.padding(horizontal = 16.dp),
)
}
@Composable
fun RowScope.Avatar(recipient: Recipient) {
Box(
modifier = Modifier
.width(60.dp)
.align(Alignment.CenterVertically)
) {
AndroidView(
factory = {
ProfilePictureView(it).apply { update(recipient) }
},
modifier = Modifier
.width(46.dp)
.height(46.dp)
)
}
}