Skip to content

Commit

Permalink
add snackbar and refactor auth animation
Browse files Browse the repository at this point in the history
  • Loading branch information
stslex committed Feb 13, 2024
1 parent b8e378b commit bd4cb37
Show file tree
Hide file tree
Showing 13 changed files with 387 additions and 97 deletions.
Original file line number Diff line number Diff line change
@@ -1,83 +1,207 @@
package com.stslex.core.ui.components

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Snackbar
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.swipeable
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TextButton
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.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import com.stslex.core.ui.theme.AppDimension
import com.stslex.core.ui.theme.toPx
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlin.math.roundToInt

@Composable
fun BoxWithConstraintsScope.AppSnackbarHost(
snackbarHostState: SnackbarHostState,
modifier: Modifier = Modifier,
) {
val width = maxWidth
AppSnackbarHost(
snackbarHostState = snackbarHostState,
width = width,
modifier = modifier
)
}

@Composable
fun BoxScope.AppSnackbarHost(
snackbarHostState: SnackbarHostState,
width: Dp,
modifier: Modifier = Modifier,
) {
SnackbarHost(
modifier = modifier
.align(Alignment.BottomCenter),
hostState = snackbarHostState
) { snackbarData ->
val actionLabel = snackbarData.visuals.actionLabel ?: return@SnackbarHost
val action = SnackbarType.getByAction(actionLabel) ?: return@SnackbarHost
when (action) {
SnackbarType.ERROR -> ErrorSnackbar(
snackbarHostState = snackbarHostState,
message = snackbarData.visuals.message,
width = width,
)

SnackbarType.SUCCESS -> SuccessSnackbar(
snackbarHostState = snackbarHostState,
snackbarData.visuals.message,
width = width,
)

SnackbarType.INFO -> InfoSnackbar(
snackbarHostState = snackbarHostState,
snackbarData.visuals.message,
width = width,
)
}
}
}

@Composable
fun SuccessSnackbar(
snackbarHostState: SnackbarHostState,
message: String,
width: Dp,
modifier: Modifier = Modifier,
) {
AppSnackbar(
type = SnackbarType.SUCCESS,
message = message,
width = width,
snackbarHostState = snackbarHostState,
modifier = modifier
)
}

@Composable
fun InfoSnackbar(
snackbarHostState: SnackbarHostState,
message: String,
width: Dp,
modifier: Modifier = Modifier,
) {
AppSnackbar(
type = SnackbarType.INFO,
message = message,
width = width,
snackbarHostState = snackbarHostState,
modifier = modifier
)
}

@Composable
fun ErrorSnackbar(
snackbarHostState: SnackbarHostState,
message: String,
width: Dp,
modifier: Modifier = Modifier,
) {
AppSnackbar(
type = SnackbarType.ERROR,
message = message,
width = width,
snackbarHostState = snackbarHostState,
modifier = modifier
)
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AppSnackbar(
type: SnackbarType,
message: String,
width: Dp,
snackbarHostState: SnackbarHostState,
modifier: Modifier = Modifier,
) {
val swipeableState = rememberSwipeableState(SnackbarSwipeState.NONE)
var width by remember { mutableStateOf(0f) }
val swipeableState = rememberSwipeableState(SnackbarSwipeState.CENTER)
val widthPx = width.toPx

LaunchedEffect(swipeableState) {
snapshotFlow {
swipeableState.offset.value
}
.filter { value ->
value == widthPx || value == -widthPx
}
.distinctUntilChanged()
.collect {
snackbarHostState.currentSnackbarData?.dismiss()
}
}

Snackbar(
modifier = modifier
.swipeable(
state = swipeableState,
orientation = Orientation.Horizontal,
anchors = mapOf(
0f to SnackbarSwipeState.LEFT,
width * 0.5f to SnackbarSwipeState.RIGHT,
width to SnackbarSwipeState.RIGHT
-widthPx to SnackbarSwipeState.LEFT,
0f to SnackbarSwipeState.CENTER,
widthPx to SnackbarSwipeState.RIGHT
),
)
.onGloballyPositioned {
width = it.size.width.toFloat()
},
.offset {
IntOffset(
x = swipeableState.offset.value.roundToInt(),
y = 0
)
}
.padding(horizontal = AppDimension.Padding.medium),
action = {
Text(text = "OK")
TextButton(
onClick = {
snackbarHostState.currentSnackbarData?.performAction()
}
) {
// TODO repeat for errors???
Text(text = "OK")
}
}
) {
Row {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppDimension.Padding.small),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "info"
imageVector = type.imageVector,
contentDescription = type.contentDescription
)
Spacer(modifier = Modifier.padding(AppDimension.Padding.medium))
Text(
modifier = Modifier,
text = type.message,
color = MaterialTheme.colorScheme.onBackground
text = message,
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Start,
overflow = TextOverflow.Ellipsis,
maxLines = 2
)
}
}
}

enum class SnackbarSwipeState {
LEFT,
RIGHT,
NONE
}

sealed class SnackbarType(
val message: String
) {
data class Error(
private val mes: String
) : SnackbarType(mes)

data class Success(
private val mes: String
) : SnackbarType(mes)

data class Info(
private val mes: String
) : SnackbarType(mes)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.stslex.core.ui.components

internal enum class SnackbarSwipeState {
LEFT,
CENTER,
RIGHT,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.stslex.core.ui.components

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Warning
import androidx.compose.ui.graphics.vector.ImageVector

enum class SnackbarType(
val label: String,
val imageVector: ImageVector,
val contentDescription: String
) {
ERROR(
label = "error",
imageVector = Icons.Default.Warning,
contentDescription = "Error"
),
SUCCESS(
label = "success",
imageVector = Icons.Default.Done,
contentDescription = "Success"
),
INFO(
label = "info",
imageVector = Icons.Default.Info,
contentDescription = "Info"
);

companion object {

fun getByAction(
actionLabel: String?
): SnackbarType? = entries.firstOrNull { type ->
type.label == actionLabel
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ abstract class BaseStore<S : State, E : Event, A : Action, N : Navigation>(

protected fun <T> launch(
onError: suspend (Throwable) -> Unit = {},
onSuccess: (T) -> Unit = {},
onSuccess: suspend CoroutineScope.(T) -> Unit = {},
action: suspend CoroutineScope.() -> T,
): Job = screenModelScope.launch(
context = exceptionHandler(onError) + appDispatcher.default,
Expand Down
52 changes: 51 additions & 1 deletion core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/Store.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,60 @@
package com.stslex.core.ui.mvi

import androidx.compose.material3.SnackbarDuration
import androidx.compose.runtime.Stable
import com.stslex.core.ui.components.SnackbarType

interface Store {

interface State

interface Event
interface Event {

@Stable
sealed class Snackbar(
open val message: String,
open val duration: SnackbarDuration,
open val withDismissAction: Boolean,
val action: String,
) : Event {

@Stable
data class Error(
override val message: String,
override val duration: SnackbarDuration = SnackbarDuration.Short,
override val withDismissAction: Boolean = false,
) : Snackbar(
message = message,
action = SnackbarType.ERROR.label,
duration = duration,
withDismissAction = withDismissAction
)

@Stable
data class Success(
override val message: String,
override val duration: SnackbarDuration = SnackbarDuration.Short,
override val withDismissAction: Boolean = false,
) : Snackbar(
message = message,
action = SnackbarType.SUCCESS.label,
duration = duration,
withDismissAction = withDismissAction
)

@Stable
data class Info(
override val message: String,
override val duration: SnackbarDuration = SnackbarDuration.Short,
override val withDismissAction: Boolean = false,
) : Snackbar(
message = message,
action = SnackbarType.INFO.label,
duration = duration,
withDismissAction = withDismissAction
)
}
}

interface Navigation

Expand Down
Loading

0 comments on commit bd4cb37

Please sign in to comment.