Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snackbar, animation, avatar #29

Merged
merged 5 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class MockProfileClientImpl : ProfileClient {

override suspend fun getFollowers(
uuid: String,
query: String,
page: Int,
pageSize: Int
): UserFollowerResponse {
Expand All @@ -92,6 +93,7 @@ class MockProfileClientImpl : ProfileClient {

override suspend fun getFollowing(
uuid: String,
query: String,
page: Int,
pageSize: Int
): UserFollowerResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ interface ProfileClient {

suspend fun getFollowers(
uuid: String,
query: String,
page: Int,
pageSize: Int
): UserFollowerResponse

suspend fun getFollowing(
uuid: String,
query: String,
page: Int,
pageSize: Int
): UserFollowerResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,27 @@ class ProfileClientImpl(

override suspend fun getFollowers(
uuid: String,
query: String,
page: Int,
pageSize: Int
): UserFollowerResponse = client.request {
get("$HOST/followers") {
parameter("uuid", uuid)
parameter("query", query)
parameter("page", page)
parameter("page_size", pageSize)
}.body()
}

override suspend fun getFollowing(
uuid: String,
query: String,
page: Int,
pageSize: Int
): UserFollowerResponse = client.request {
get("$HOST/following") {
parameter("uuid", uuid)
parameter("query", query)
parameter("page", page)
parameter("page_size", pageSize)
}.body()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import com.stslex.core.network.clients.film.client.FilmClient
import com.stslex.core.network.clients.film.client.MockFilmClientImpl
import com.stslex.core.network.clients.profile.client.ProfileClient
import com.stslex.core.network.clients.profile.client.ProfileClientImpl
import com.stslex.core.network.utils.PagingWorker
import com.stslex.core.network.utils.PagingWorkerImpl
import com.stslex.core.network.utils.token.AuthController
import com.stslex.core.network.utils.token.AuthControllerImpl
import org.koin.dsl.module
Expand Down Expand Up @@ -72,4 +74,8 @@ val coreNetworkModule = module {
userStore = get(),
)
}

factory<PagingWorker> {
PagingWorkerImpl()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stslex.core.network.utils

fun interface PagingWorker {

suspend operator fun invoke(
request: suspend () -> Unit
)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.stslex.core.network.utils

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.coroutines.coroutineContext

class PagingWorkerImpl : PagingWorker {
private var job: Job? = null
private var nextPageJob: Job? = null
private var lastRequestTime = 0L

override suspend fun invoke(
request: suspend () -> Unit
) {
if (lastRequestTime + REQUEST_DELAY > currentTimeMs) {
nextPageJob = startRequest(
request = request,
start = CoroutineStart.LAZY
)
}
startRequest(request = request)
}

private suspend fun startRequest(
request: suspend () -> Unit,
start: CoroutineStart = CoroutineStart.DEFAULT,
): Job = CoroutineScope(coroutineContext)
.launch(
start = start
) {
job = nextPageJob
nextPageJob = null
request()
}.apply {
invokeOnCompletion {
nextPageJob?.start()
}
}

companion object {
private const val REQUEST_DELAY = 500L
}
}
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,
}
Loading
Loading