Milan Jurkulak 2 سال پیش
والد
کامیت
85f6d1411e

+ 62 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/DimensionSubcomposeLayout.kt

@@ -0,0 +1,62 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpSize
+
+@Composable
+internal fun DimensionSubComposeLayout(
+    modifier: Modifier = Modifier,
+    density: Density = LocalDensity.current,
+    mainContent: @Composable () -> Unit,
+    dependentContent: @Composable (DpSize) -> Unit
+) = SubcomposeLayout(
+    modifier = modifier
+) { constraints: Constraints ->
+    val mainPlaceable = subcompose(
+        SlotsEnum.Main,
+        mainContent
+    ).map { measurable ->
+        measurable.measure(constraints.copy(
+            minWidth = 0,
+            minHeight = 0
+        ))
+    }.first()
+    val dependentPlaceable: Placeable = subcompose(
+        SlotsEnum.Dependent
+    ) {
+        dependentContent(
+            DpSize(
+                density.run { mainPlaceable.width.toDp() },
+                density.run { mainPlaceable.height.toDp() }
+            )
+        )
+    }.map { measurable: Measurable ->
+        measurable.measure(constraints)
+    }.first()
+    layout(
+        mainPlaceable.width,
+        mainPlaceable.height
+    ) {
+        dependentPlaceable.placeRelative(0, 0)
+    }
+}
+
+/**
+ * Enum class for SubcomposeLayouts with main and dependent Composables
+ */
+enum class SlotsEnum { Main, Dependent }

+ 53 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/animatePlacement.kt

@@ -0,0 +1,53 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector2D
+import androidx.compose.animation.core.Spring.StiffnessMediumLow
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.layout.offset
+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.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.round
+import kotlinx.coroutines.launch
+
+@Suppress("unused")
+fun Modifier.animatePlacement(): Modifier = composed {
+    val scope = rememberCoroutineScope()
+    var targetOffset by remember { mutableStateOf(IntOffset.Zero) }
+    var animatable by remember {
+        mutableStateOf<Animatable<IntOffset, AnimationVector2D>?>(null)
+    }
+    onPlaced {
+        // Calculate the position in the parent layout
+        targetOffset = it.positionInParent().round()
+    }.offset {
+        // Animate to the new target offset when alignment changes.
+        val anim = animatable ?: Animatable(targetOffset, IntOffset.VectorConverter)
+            .also { animatable = it }
+        if (anim.targetValue != targetOffset) {
+            scope.launch {
+                anim.animateTo(targetOffset, spring(stiffness = StiffnessMediumLow))
+            }
+        }
+        // Offset the child in the opposite direction to the targetOffset, and slowly catch
+        // up to zero offset via an animation to achieve an overall animated movement.
+        animatable?.let { it.value - targetOffset } ?: IntOffset.Zero
+    }
+}

+ 12 - 9
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt

@@ -13,12 +13,13 @@ package org.mjdev.tvlib.ui.components.media
 import android.annotation.SuppressLint
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ModifierExt.conditional
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.ModifierExt.swipeGestures
 import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMediaPlayerState
@@ -29,6 +30,7 @@ import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMe
 fun MediaPlayerContainer(
     modifier: Modifier = Modifier,
     visible: Boolean = true,
+    enableSwipeGestures: Boolean = true,
     state: MediaPlayerState = rememberMediaPlayerState(),
 ) {
     val nextItem: () -> Unit = { state.player.seekToNext() }
@@ -36,14 +38,15 @@ fun MediaPlayerContainer(
     if (visible) {
         Box(
             modifier = modifier
-                .fillMaxSize()
-                .swipeGestures(
-                    onSwipeLeft = { nextItem() },
-                    onSwipeRight = { prevItem() },
-                    onSwipeUp = { nextItem() },
-                    onSwipeDown = { prevItem() },
-                )
-                .background(Color.Black)
+                .conditional(enableSwipeGestures) {
+                    swipeGestures(
+                        onSwipeLeft = { nextItem() },
+                        onSwipeRight = { prevItem() },
+                        onSwipeUp = { nextItem() },
+                        onSwipeDown = { prevItem() },
+                    )
+                }
+                .background(Color.Black, RectangleShape)
                 .recomposeHighlighter()
         ) {
             state.player.GetPlayerView()

+ 14 - 16
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/AppUpdateAvailableMessage.kt

@@ -10,8 +10,8 @@ package org.mjdev.tvlib.ui.components.tv
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
 import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.tooling.preview.Preview
@@ -32,24 +32,22 @@ fun AppUpdateAvailableMessage(
     backgroundColor: Color = Color(0, 132, 0, 255),
     buttonText: Any? = R.string.bt_update,
     dismissState: MutableState<Boolean> = mutableStateOf(false),
-    isDebug:Boolean = BuildConfig.DEBUG,
+    isDebug: Boolean = BuildConfig.DEBUG,
     appUpdater: AppUpdater = rememberAppUpdater(
         githubUser = githubUser,
         githubRepository = githubRepository,
         isDebug = isDebug
     ),
+    isEdit: Boolean = isEditMode(),
+    updateAvailable: State<Boolean> = appUpdater.isUpdateAvailable.collectAsState(false),
     onButtonClick: () -> Unit = { appUpdater.updateApp() },
-) {
-    val updateAvailable by appUpdater.isUpdateAvailable.collectAsState(false)
-    if (isEditMode() || updateAvailable) {
-        ErrorMessage(
-            title = title,
-            dismissText = buttonText,
-            error = message.asException(),
-            backgroundColor = backgroundColor,
-            dismissible = true,
-            dismissState = dismissState,
-            dismissOnClick = onButtonClick
-        )
-    }
-}
+) = ErrorMessage(
+    visible = isEdit || updateAvailable.value,
+    title = title,
+    dismissText = buttonText,
+    error = message.asException(),
+    backgroundColor = backgroundColor,
+    dismissible = true,
+    dismissState = dismissState,
+    dismissOnClick = onButtonClick
+)

+ 54 - 44
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/ErrorMessage.kt

@@ -8,6 +8,9 @@
 
 package org.mjdev.tvlib.ui.components.tv
 
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.BoxWithConstraints
@@ -36,6 +39,7 @@ import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.ui.components.button.Button
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.ui.components.text.TextAny
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -58,56 +62,62 @@ fun ErrorMessage(
     borderColor: Color = Color.DarkGray,
     backgroundShape: Shape = RoundedCornerShape(roundSize),
     dismissible: Boolean = true,
-    dismissState: MutableState<Boolean> = mutableStateOf(false),
-    dismissOnClick: () -> Unit = {}
+    visible: Boolean = isEditMode(),
+    dismissState: MutableState<Boolean> = mutableStateOf(!visible),
+    enter: EnterTransition = EnterTransition.None,
+    exit: ExitTransition = ExitTransition.None,
+    dismissOnClick: () -> Unit = {},
+) = AnimatedVisibility(
+    visible = !dismissState.value,
+    enter = enter,
+    exit = exit
 ) {
-    if (!dismissState.value) {
-        BoxWithConstraints {
-            Row(
+    BoxWithConstraints {
+        Row(
+            modifier = Modifier
+                .width(constraints.maxWidth.dp)
+                .padding(paddingContent)
+                .border(borderSize, borderColor, backgroundShape)
+                .background(backgroundColor, backgroundShape)
+                .recomposeHighlighter(),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            TextAny(
                 modifier = Modifier
-                    .width(constraints.maxWidth.dp)
-                    .padding(paddingContent)
-                    .border(borderSize, borderColor, backgroundShape)
-                    .background(backgroundColor, backgroundShape)
+                    .padding(paddingTitle)
                     .recomposeHighlighter(),
-                verticalAlignment = Alignment.CenterVertically,
-            ) {
-                TextAny(
-                    modifier = Modifier
-                        .padding(paddingTitle)
-                        .recomposeHighlighter(),
-                    text = title ?: stringResource(id = R.string.title_error),
-                    fontWeight = fontWeight,
-                    fontSize = fontSizeTitle,
-                    color = color
-                )
-                TextAny(
+                text = title ?: stringResource(id = R.string.title_error),
+                fontWeight = fontWeight,
+                fontSize = fontSizeTitle,
+                color = color
+            )
+            TextAny(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .weight(1f)
+                    .recomposeHighlighter(),
+                color = color,
+                fontSize = fontSizeMessage,
+                fontWeight = fontWeight,
+                textAlign = TextAlign.Left,
+                maxLines = 4,
+                softWrap = true,
+                text = error?.message ?: (stringResource(id = R.string.error_unknown))
+            )
+            if (dismissible && (dismissText != null)) {
+                Button(
                     modifier = Modifier
-                        .fillMaxWidth()
-                        .weight(1f, false)
+                        .padding(4.dp)
                         .recomposeHighlighter(),
-                    color = color,
-                    fontSize = fontSizeMessage,
-                    fontWeight = fontWeight,
-                    textAlign = TextAlign.Left,
-                    softWrap = true,
-                    text = error?.message ?: (stringResource(id = R.string.error_unknown))
+                    text = dismissText,
+                    fontSize = fontSizeButton,
+                    contentPadding = PaddingValues(8.dp, 0.dp),
+                    containerColor = Color.Black.copy(alpha = 0.2f),
+                    onClick = {
+                        dismissState.value = true
+                        dismissOnClick.invoke()
+                    }
                 )
-                if (dismissible && (dismissText != null)) {
-                    Button(
-                        modifier = Modifier
-                            .padding(4.dp)
-                            .recomposeHighlighter(),
-                        text = dismissText,
-                        fontSize = fontSizeButton,
-                        contentPadding = PaddingValues(8.dp, 0.dp),
-                        containerColor = Color.Black.copy(alpha = 0.2f),
-                        onClick = {
-                            dismissState.value = true
-                            dismissOnClick.invoke()
-                        }
-                    )
-                }
             }
         }
     }

+ 12 - 12
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/NetworkErrorMessage.kt

@@ -8,6 +8,8 @@
 
 package org.mjdev.tvlib.ui.components.tv
 
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.collectAsState
@@ -28,15 +30,13 @@ fun NetworkErrorMessage(
     dismissible: Boolean = false,
     backgroundColor: Color = Color.Black,
     networkService: NetworkConnectivityService = rememberNetworkService(),
-    networkState: State<NetworkStatus?> = networkService.networkStatus
-        .collectAsState(NetworkStatus.Connected(null)),
-) {
-    val isEdit = isEditMode()
-    if (isEdit || networkState.isNotConnected) {
-        ErrorMessage(
-            error = message.asException(),
-            backgroundColor = backgroundColor,
-            dismissible = dismissible
-        )
-    }
-}
+    networkState: State<NetworkStatus?> = networkService.networkStatus.collectAsState(null),
+    isEdit: Boolean = isEditMode(),
+    enter: EnterTransition = EnterTransition.None,
+    exit: ExitTransition = ExitTransition.None
+) = ErrorMessage(
+    visible = isEdit || networkState.isNotConnected,
+    error = message.asException(),
+    backgroundColor = backgroundColor,
+    dismissible = dismissible
+)