for-android/app/src/main/java/chat/stoat/sheets/AddServerSheet.kt

494 lines
23 KiB
Kotlin

package chat.stoat.sheets
import android.content.Intent
import android.util.Log
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import chat.stoat.R
import chat.stoat.activities.InviteActivity
import chat.stoat.api.routes.server.createServer
import chat.stoat.callbacks.Action
import chat.stoat.callbacks.ActionChannel
import chat.stoat.composables.sheets.SheetSelection
import chat.stoat.composables.vectorassets.CreateServer
import chat.stoat.composables.vectorassets.JoinServer
import chat.stoat.composables.vectorassets.NewServer
import chat.stoat.core.model.data.STOAT_WEB_APP
import chat.stoat.material.EasingTokens
import chat.stoat.screens.chat.ChatRouterDestination
import chat.stoat.ui.theme.FragmentMono
import kotlinx.coroutines.launch
import logcat.asLog
import logcat.logcat
enum class AddServerSheetStep(val animationValue: Int) {
Initial(0),
JoinFromInvite(1),
CreateServer(1)
}
@Composable
fun AddServerSheet(onDismiss: () -> Unit) {
var currentStep by remember { mutableStateOf(AddServerSheetStep.Initial) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
) {
AnimatedContent(
currentStep,
transitionSpec = {
if (targetState.animationValue > initialState.animationValue) {
(slideInHorizontally(
animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate),
initialOffsetX = { fullWidth -> fullWidth }
) + fadeIn(
animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate)
)) togetherWith (slideOutHorizontally(
animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate),
targetOffsetX = { fullWidth -> -fullWidth }
) + fadeOut(
animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate)
))
} else {
(slideInHorizontally(
animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate),
initialOffsetX = { fullWidth -> -fullWidth }
) + fadeIn(
animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate)
)) togetherWith (slideOutHorizontally(
animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate),
targetOffsetX = { fullWidth -> fullWidth }
) + fadeOut(
animationSpec = tween(300, 0, EasingTokens.EmphasizedDecelerate)
))
}
},
contentAlignment = Alignment.BottomCenter,
modifier = Modifier.animateContentSize(
animationSpec = tween(150, 0, EasingTokens.EmphasizedDecelerate)
)
) { step ->
Box(
modifier = Modifier.padding(16.dp)
) {
if (step == AddServerSheetStep.Initial) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
imageVector = NewServer,
contentDescription = null,
modifier = Modifier
.fillMaxWidth(0.5f)
)
Text(
text = stringResource(id = R.string.add_server_sheet_step_0_title),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = stringResource(id = R.string.add_server_sheet_step_0_description),
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.7f),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
SheetSelection(
icon = {
Image(
imageVector = JoinServer,
contentDescription = null,
modifier = Modifier
.size(48.dp)
)
},
title = {
Text(
text = stringResource(id = R.string.add_server_sheet_step_0_join)
)
},
description = {
Text(
text = stringResource(id = R.string.add_server_sheet_step_0_join_description)
)
},
) {
currentStep = AddServerSheetStep.JoinFromInvite
}
SheetSelection(
icon = {
Image(
imageVector = CreateServer,
contentDescription = null,
modifier = Modifier
.size(48.dp)
)
},
title = {
Text(
text = stringResource(id = R.string.add_server_sheet_step_0_create)
)
},
description = {
Text(
text = stringResource(id = R.string.add_server_sheet_step_0_create_description)
)
},
) {
currentStep = AddServerSheetStep.CreateServer
}
}
} else if (step == AddServerSheetStep.JoinFromInvite) {
val inviteState = rememberTextFieldState()
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(id = R.string.add_server_sheet_step_1j_title),
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
IconButton(
onClick = {
currentStep = AddServerSheetStep.Initial
}
) {
Icon(
painter = painterResource(R.drawable.ic_arrow_back_24dp),
contentDescription = stringResource(id = R.string.back),
)
}
}
Text(
text = stringResource(id = R.string.add_server_sheet_step_1j_description),
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.7f),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = stringResource(id = R.string.add_server_sheet_step_1j_examples_heading),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.add_server_sheet_step_1j_example_1),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
fontFamily = FragmentMono,
modifier = Modifier
.clip(
MaterialTheme.shapes.small.copy(
topStart = MaterialTheme.shapes.large.topStart,
topEnd = MaterialTheme.shapes.large.topEnd,
)
)
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
.padding(16.dp)
.fillMaxWidth()
)
Text(
text = stringResource(id = R.string.add_server_sheet_step_1j_example_2),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
fontFamily = FragmentMono,
modifier = Modifier
.clip(
MaterialTheme.shapes.small
)
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
.padding(16.dp)
.fillMaxWidth()
)
Text(
text = stringResource(id = R.string.add_server_sheet_step_1j_example_3),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
fontFamily = FragmentMono,
modifier = Modifier
.clip(
MaterialTheme.shapes.small.copy(
bottomStart = MaterialTheme.shapes.large.bottomStart,
bottomEnd = MaterialTheme.shapes.large.bottomEnd,
)
)
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
.padding(16.dp)
.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(8.dp))
TextField(
state = inviteState,
label = {
Text(stringResource(R.string.add_server_sheet_step_1j_label))
},
lineLimits = TextFieldLineLimits.SingleLine,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 8.dp)
)
Button(
onClick = {
val intent = Intent(context, InviteActivity::class.java)
intent.data = if (inviteState.text.startsWith("https://")) {
try {
inviteState.text.toString().toUri()
} catch (e: Exception) {
Log.e(
"AddServerSheet",
"Invalid URL: ${inviteState.text}",
e
)
return@Button
}
} else {
"https://$STOAT_WEB_APP/invite/${inviteState.text}".toUri()
}
context.startActivity(intent)
onDismiss()
},
modifier = Modifier
.fillMaxWidth()
) {
Text(
text = stringResource(id = R.string.add_server_sheet_step_1j_join)
)
}
}
} else if (step == AddServerSheetStep.CreateServer) {
val serverNameState = rememberTextFieldState()
val serverNameIsBlank = remember(serverNameState.text) {
serverNameState.text.isBlank()
}
var serverNameRangeError by remember { mutableStateOf(false) }
var serverCreationError by remember { mutableStateOf(false) }
LaunchedEffect(serverNameState.text) {
if (serverNameState.text.length > 32) {
serverNameRangeError = true
} else {
serverNameRangeError = false
}
}
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(id = R.string.add_server_sheet_step_1c_title),
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
IconButton(
onClick = {
currentStep = AddServerSheetStep.Initial
}
) {
Icon(
painter = painterResource(R.drawable.ic_arrow_back_24dp),
contentDescription = stringResource(id = R.string.back),
)
}
}
Text(
text = stringResource(id = R.string.add_server_sheet_step_1c_description),
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.7f),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
val outline = MaterialTheme.colorScheme.outline
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Canvas(Modifier.size(80.dp)) {
val stroke = Stroke(
width = 4.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(
floatArrayOf(30f, 40f),
0f
)
)
drawCircle(
color = outline,
style = stroke,
radius = size.minDimension / 2 - stroke.width / 2
)
}
Text(
text = when {
serverNameIsBlank -> stringResource(R.string.add_server_sheet_step_1c_name_placeholder)
else -> serverNameState.text.toString()
},
style = MaterialTheme.typography.bodyLarge,
color = LocalContentColor.current.copy(
alpha = when {
serverNameIsBlank -> 0.5f
else -> 1f
}
),
fontWeight = when {
serverNameIsBlank -> FontWeight.Medium
else -> FontWeight.Bold
},
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
)
}
TextField(
state = serverNameState,
label = {
Text(stringResource(R.string.add_server_sheet_step_1c_name))
},
lineLimits = TextFieldLineLimits.SingleLine,
supportingText = {
Text(
when {
serverCreationError -> stringResource(R.string.add_server_sheet_step_1c_error)
serverNameRangeError -> stringResource(
R.string.add_server_sheet_step_1c_name_error_range,
32
)
else -> ""
}
)
},
isError = serverNameRangeError || serverCreationError,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 8.dp)
)
Button(
onClick = {
serverCreationError = false
scope.launch {
try {
val server = createServer(serverNameState.text.toString())
// Backend should've already created a channel for us to go to
server.channels?.first()?.id?.let {
ActionChannel.send(
Action.ChatNavigate(
ChatRouterDestination.Channel(it)
)
)
}
onDismiss()
} catch (e: Exception) {
serverCreationError = true
logcat { "Error creating server: ${e.asLog()}" }
}
}
},
enabled = !serverNameIsBlank && !serverNameRangeError,
modifier = Modifier
.fillMaxWidth()
) {
Text(
text = stringResource(id = R.string.add_server_sheet_step_1c_create)
)
}
}
}
}
}
}
}