From 74e635ef2294129e911b17a75c1b44a6f70717ac Mon Sep 17 00:00:00 2001 From: Boonapart Date: Mon, 23 Jun 2025 15:12:46 +0300 Subject: [PATCH 01/15] Moved reusable VideoTutorial UI to core design module --- design/api/current.api | 72 +++++++++++++++++++ design/build.gradle | 3 + .../videoTutorial/model/VideoContentColors.kt | 7 ++ .../model/VideoProgressColors.kt | 8 +++ .../model/VideoProgressPadding.kt | 13 ++++ .../ui/videoTutorial/ui/VideoContent.kt | 51 +++++++++++++ .../ui/videoTutorial/ui/VideoProgress.kt | 61 ++++++++++++++++ .../videoTutorial/ui/VideoProgressModifier.kt | 26 +++++++ .../ui/videoTutorial/ui/VideoTapNavigator.kt | 35 +++++++++ .../videoTutorial/ui/VideoTutorialWrapper.kt | 47 ++++++++++++ .../ui/videoTutorial/utils/ShadowConstants.kt | 5 ++ .../ui/videoTutorial/utils/ShadowDimens.kt | 9 +++ gradle/libs.versions.toml | 2 + 13 files changed, 339 insertions(+) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoContentColors.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressColors.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressPadding.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoContent.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgress.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgressModifier.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTapNavigator.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTutorialWrapper.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowConstants.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowDimens.kt diff --git a/design/api/current.api b/design/api/current.api index 0e9da44d..4244b764 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -730,3 +730,75 @@ package com.urlaunched.android.design.ui.tutorial.utils { } +package com.urlaunched.android.design.ui.videoTutorial.model { + + public final class VideoContentColors { + ctor public VideoContentColors(optional long backgroundColor); + method public long component1-0d7_KjU(); + method public com.urlaunched.android.design.ui.videoTutorial.model.VideoContentColors copy-8_81llA(long backgroundColor); + method public long getBackgroundColor(); + property public final long backgroundColor; + } + + public final class VideoProgressColors { + ctor public VideoProgressColors(optional long trackColor, optional long progressColor); + method public long component1-0d7_KjU(); + method public long component2-0d7_KjU(); + method public com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressColors copy--OWjLjI(long trackColor, long progressColor); + method public long getProgressColor(); + method public long getTrackColor(); + property public final long progressColor; + property public final long trackColor; + } + + public final class VideoProgressPadding { + ctor public VideoProgressPadding(optional float start, optional float top, optional float end, optional float bottom, optional float spaceBetweenProgress); + method public float component1-D9Ej5fM(); + method public float component2-D9Ej5fM(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public float component5-D9Ej5fM(); + method public com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressPadding copy-RyVG9vg(float start, float top, float end, float bottom, float spaceBetweenProgress); + method public float getBottom(); + method public float getEnd(); + method public float getSpaceBetweenProgress(); + method public float getStart(); + method public float getTop(); + property public final float bottom; + property public final float end; + property public final float spaceBetweenProgress; + property public final float start; + property public final float top; + } + +} + +package com.urlaunched.android.design.ui.videoTutorial.ui { + + public final class VideoProgressModifierKt { + method @androidx.compose.runtime.Composable public static androidx.compose.ui.Modifier dropShadow(androidx.compose.ui.Modifier, optional long color, optional float alpha, optional float cornersRadius, optional float shadowBlurRadius, optional long offset); + } + + public final class VideoTutorialWrapperKt { + method @androidx.compose.runtime.Composable public static void VideoTutorialWrapper(optional androidx.compose.ui.Modifier modifier, androidx.media3.common.Player? player, optional com.urlaunched.android.design.ui.videoTutorial.model.VideoContentColors videoContentColors, kotlin.jvm.functions.Function0 onPreviousVideo, kotlin.jvm.functions.Function0 onNextVideo, java.util.List mediaLink, int currentMediaIndex, float progressForCurrentMedia, optional com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressColors videoProgressDefaultColors, optional com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressPadding videoProgressPadding, optional kotlin.jvm.functions.Function0 closeButton); + } + +} + +package com.urlaunched.android.design.ui.videoTutorial.utils { + + public final class ShadowConstants { + field public static final float DROP_SHADOW_ALPHA = 0.15f; + field public static final com.urlaunched.android.design.ui.videoTutorial.utils.ShadowConstants INSTANCE; + } + + public final class ShadowDimens { + method public float getDropShadowBlurRadius(); + method public long getDropShadowOffset(); + property public final float dropShadowBlurRadius; + property public final long dropShadowOffset; + field public static final com.urlaunched.android.design.ui.videoTutorial.utils.ShadowDimens INSTANCE; + } + +} + diff --git a/design/build.gradle b/design/build.gradle index 38319e3a..cf8215ea 100644 --- a/design/build.gradle +++ b/design/build.gradle @@ -75,6 +75,9 @@ dependencies { debugImplementation libs.composeDependencies.composeUiTooling debugImplementation libs.composeDependencies.composeUiTestManifest + //Media 3 + implementation libs.composeMedia3.ui + // Paging implementation libs.pagingDependencies.core implementation libs.pagingDependencies.compose diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoContentColors.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoContentColors.kt new file mode 100644 index 00000000..21b9b691 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoContentColors.kt @@ -0,0 +1,7 @@ +package com.urlaunched.android.design.ui.videoTutorial.model + +import androidx.compose.ui.graphics.Color + +data class VideoContentColors( + val backgroundColor: Color = Color(0xFF4CAF50) +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressColors.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressColors.kt new file mode 100644 index 00000000..cdd52ba1 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressColors.kt @@ -0,0 +1,8 @@ +package com.urlaunched.android.design.ui.videoTutorial.model + +import androidx.compose.ui.graphics.Color + +data class VideoProgressColors( + val trackColor: Color = Color.White, + val progressColor: Color = Color.White +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressPadding.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressPadding.kt new file mode 100644 index 00000000..a63f3a2b --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressPadding.kt @@ -0,0 +1,13 @@ +package com.urlaunched.android.design.ui.videoTutorial.model + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.resources.dimens.Dimens + +data class VideoProgressPadding( + val start: Dp = Dimens.spacingNormal, + val top: Dp = Dimens.spacingNormalSpecial, + val end: Dp = Dimens.spacingNormal, + val bottom: Dp = 0.dp, + val spaceBetweenProgress: Dp = Dimens.spacingTiny +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoContent.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoContent.kt new file mode 100644 index 00000000..51c7ae04 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoContent.kt @@ -0,0 +1,51 @@ +package com.urlaunched.android.design.ui.videoTutorial.ui + +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.annotation.OptIn +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.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView + + +import com.urlaunched.android.design.ui.videoTutorial.model.VideoContentColors + +@OptIn(UnstableApi::class) +@Composable +internal fun VideoContent( + player: Player?, + videoContentColors: VideoContentColors = VideoContentColors() +) { + if (LocalInspectionMode.current) { + Box( + modifier = Modifier + .fillMaxSize() + .background(videoContentColors.backgroundColor) + ) + } else { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + PlayerView(context).apply { + this.player = player + useController = false + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM + + layoutParams = + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + } + ) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgress.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgress.kt new file mode 100644 index 00000000..c8a8159d --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgress.kt @@ -0,0 +1,61 @@ +package com.urlaunched.android.design.ui.videoTutorial.ui + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressColors +import com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressPadding + +@Composable +internal fun VideoProgress( + mediaLink: List, + currentMediaIndex: Int, + progressForCurrentMedia: Float, + videoProgressDefaultColors: VideoProgressColors = VideoProgressColors(), + videoProgressPadding: VideoProgressPadding = VideoProgressPadding(), + closeButton: @Composable () -> Unit = {}, +) { + Row( + modifier = Modifier + .statusBarsPadding() + .fillMaxWidth() + .padding( + top = videoProgressPadding.top, + start = videoProgressPadding.start, + end = videoProgressPadding.end + ), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(mediaLink.size) { index -> + LinearProgressIndicator( + modifier = Modifier + .dropShadow(cornersRadius = Dimens.cornerRadiusLarge) + .weight(1f) + .padding(end = if (index != mediaLink.lastIndex) videoProgressPadding.spaceBetweenProgress else Dimens.zeroDp), + strokeCap = StrokeCap.Round, + trackColor = videoProgressDefaultColors.trackColor, + color = videoProgressDefaultColors.progressColor, + gapSize = Dimens.zeroDp, + drawStopIndicator = { + // Do nothing + }, + progress = { + when { + currentMediaIndex > index -> 1f + currentMediaIndex == index -> progressForCurrentMedia + else -> 0f + } + } + ) + } + + closeButton() + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgressModifier.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgressModifier.kt new file mode 100644 index 00000000..80ee191a --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgressModifier.kt @@ -0,0 +1,26 @@ +package com.urlaunched.android.design.ui.videoTutorial.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.ui.shadow.shadow +import com.urlaunched.android.design.ui.videoTutorial.utils.ShadowConstants +import com.urlaunched.android.design.ui.videoTutorial.utils.ShadowDimens + +@Composable +fun Modifier.dropShadow( + color: Color = Color.Black, + alpha: Float = ShadowConstants.DROP_SHADOW_ALPHA, + cornersRadius: Dp = 0.dp, + shadowBlurRadius: Dp = ShadowDimens.dropShadowBlurRadius, + offset: DpOffset = ShadowDimens.dropShadowOffset +) = this.shadow( + alpha = alpha, + shadowBlurRadius = shadowBlurRadius, + offset = offset, + color = color, + cornersRadius = cornersRadius +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTapNavigator.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTapNavigator.kt new file mode 100644 index 00000000..ae81ab8e --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTapNavigator.kt @@ -0,0 +1,35 @@ +package com.urlaunched.android.design.ui.videoTutorial.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +internal fun VideoTapNavigator( + onPreviousVideo: () -> Unit, + onNextVideo: () -> Unit +) { + Row(Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .clickable( + onClick = onPreviousVideo + ) + ) + + Box( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .clickable( + onClick = onNextVideo + ) + ) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTutorialWrapper.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTutorialWrapper.kt new file mode 100644 index 00000000..3c01047c --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTutorialWrapper.kt @@ -0,0 +1,47 @@ +package com.urlaunched.android.design.ui.videoTutorial.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.media3.common.Player + +import com.urlaunched.android.design.ui.videoTutorial.model.VideoContentColors +import com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressColors +import com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressPadding + +@Composable +fun VideoTutorialWrapper( + modifier: Modifier = Modifier, + player: Player?, + videoContentColors: VideoContentColors = VideoContentColors(), + onPreviousVideo: () -> Unit, + onNextVideo: () -> Unit, + mediaLink: List, + currentMediaIndex: Int, + progressForCurrentMedia: Float, + videoProgressDefaultColors: VideoProgressColors = VideoProgressColors(), + videoProgressPadding: VideoProgressPadding = VideoProgressPadding(), + closeButton: @Composable () -> Unit = {}, +) { + Box(modifier = modifier.fillMaxSize()) { + VideoContent( + player = player, + videoContentColors = videoContentColors + ) + + VideoTapNavigator( + onPreviousVideo = onPreviousVideo, + onNextVideo = onNextVideo + ) + + VideoProgress( + mediaLink = mediaLink, + currentMediaIndex = currentMediaIndex, + progressForCurrentMedia = progressForCurrentMedia, + videoProgressDefaultColors = videoProgressDefaultColors, + videoProgressPadding = videoProgressPadding, + closeButton = closeButton, + ) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowConstants.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowConstants.kt new file mode 100644 index 00000000..6ce85187 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowConstants.kt @@ -0,0 +1,5 @@ +package com.urlaunched.android.design.ui.videoTutorial.utils + +object ShadowConstants { + const val DROP_SHADOW_ALPHA = 0.15f +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowDimens.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowDimens.kt new file mode 100644 index 00000000..b05759a0 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowDimens.kt @@ -0,0 +1,9 @@ +package com.urlaunched.android.design.ui.videoTutorial.utils + +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +object ShadowDimens { + val dropShadowBlurRadius = 4.dp + val dropShadowOffset = DpOffset(0.dp, 2.dp) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 502f8c34..894eead2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,6 +101,7 @@ aspectPluginVersion = "1.4.1" junit = "1.2.1" espressoCoreVersion = "3.6.1" materialVersion = "1.12.0" +media3UiVersion = "1.7.1" [libraries] androidCoreDependencies-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } @@ -168,6 +169,7 @@ roomDependencies-paging = { module = "androidx.room:room-paging", version.ref = media3Dependencies-core = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Version" } media3Dependencies-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Version" } media3Dependencies-session = { module = "androidx.media3:media3-session", version.ref = "media3Version" } +composeMedia3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3UiVersion" } androidXMediaDependencies-core = { module = "androidx.media:media", version.ref = "androidXMediaVersion" } From a181e436c04edb310f68680e58df0220fbcd83d6 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Thu, 2 Oct 2025 18:44:39 +0300 Subject: [PATCH 02/15] Create shadow style with overload --- .../urlaunched/android/design/ui/shadow/Shadow.kt | 9 +++++++++ .../android/design/ui/shadow/models/ShadowStyle.kt | 14 ++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt diff --git a/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt b/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt index 1a82bf7b..afb142e9 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt @@ -9,6 +9,15 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle + +fun Modifier.shadow(style: ShadowStyle) = this.shadow( + color = style.color, + alpha = style.alpha, + cornersRadius = style.cornersRadius, + shadowBlurRadius = style.blurRadius, + offset = style.offset +) fun Modifier.shadow( color: Color = Color.Black, diff --git a/design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt new file mode 100644 index 00000000..ed8979bb --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt @@ -0,0 +1,14 @@ +package com.urlaunched.android.design.ui.shadow.models + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +data class ShadowStyle( + val color: Color = Color.Black, + val alpha: Float = 1f, + val cornersRadius: Dp = 0.dp, + val blurRadius: Dp = 0.dp, + val offset: DpOffset = DpOffset.Zero +) \ No newline at end of file From 91f62c3e6ae6fb0de9912997b8110e162c43b070 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 09:32:59 +0300 Subject: [PATCH 03/15] Refactor video tutorial component --- .../videoTutorial/model/VideoContentColors.kt | 7 -- .../model/VideoProgressPadding.kt | 13 --- .../ui/videoTutorial/ui/VideoProgress.kt | 61 ------------ .../videoTutorial/ui/VideoProgressModifier.kt | 26 ----- .../videoTutorial/ui/VideoTutorialWrapper.kt | 47 --------- .../ui/videoTutorial/utils/ShadowConstants.kt | 5 - .../ui/videoTutorial/utils/ShadowDimens.kt | 9 -- .../constants/VideoProgressDefaults.kt | 23 +++++ .../model/VideoProgressColors.kt | 4 +- .../videotutorial/model/VideoProgressStyle.kt | 11 +++ .../ui/VideoPlayer.kt} | 26 ++--- .../ui/videotutorial/ui/VideoProgress.kt | 80 ++++++++++++++++ .../ui/VideoTapNavigator.kt | 14 +-- .../ui/VideoTutorialContainer.kt | 95 +++++++++++++++++++ 14 files changed, 228 insertions(+), 193 deletions(-) delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoContentColors.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressPadding.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgress.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgressModifier.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTutorialWrapper.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowConstants.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowDimens.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videotutorial/constants/VideoProgressDefaults.kt rename design/src/main/java/com/urlaunched/android/design/ui/{videoTutorial => videotutorial}/model/VideoProgressColors.kt (53%) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videotutorial/model/VideoProgressStyle.kt rename design/src/main/java/com/urlaunched/android/design/ui/{videoTutorial/ui/VideoContent.kt => videotutorial/ui/VideoPlayer.kt} (60%) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoProgress.kt rename design/src/main/java/com/urlaunched/android/design/ui/{videoTutorial => videotutorial}/ui/VideoTapNavigator.kt (65%) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoContentColors.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoContentColors.kt deleted file mode 100644 index 21b9b691..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoContentColors.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.urlaunched.android.design.ui.videoTutorial.model - -import androidx.compose.ui.graphics.Color - -data class VideoContentColors( - val backgroundColor: Color = Color(0xFF4CAF50) -) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressPadding.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressPadding.kt deleted file mode 100644 index a63f3a2b..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressPadding.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.urlaunched.android.design.ui.videoTutorial.model - -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.urlaunched.android.design.resources.dimens.Dimens - -data class VideoProgressPadding( - val start: Dp = Dimens.spacingNormal, - val top: Dp = Dimens.spacingNormalSpecial, - val end: Dp = Dimens.spacingNormal, - val bottom: Dp = 0.dp, - val spaceBetweenProgress: Dp = Dimens.spacingTiny -) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgress.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgress.kt deleted file mode 100644 index c8a8159d..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgress.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.urlaunched.android.design.ui.videoTutorial.ui - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.StrokeCap -import com.urlaunched.android.design.resources.dimens.Dimens -import com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressColors -import com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressPadding - -@Composable -internal fun VideoProgress( - mediaLink: List, - currentMediaIndex: Int, - progressForCurrentMedia: Float, - videoProgressDefaultColors: VideoProgressColors = VideoProgressColors(), - videoProgressPadding: VideoProgressPadding = VideoProgressPadding(), - closeButton: @Composable () -> Unit = {}, -) { - Row( - modifier = Modifier - .statusBarsPadding() - .fillMaxWidth() - .padding( - top = videoProgressPadding.top, - start = videoProgressPadding.start, - end = videoProgressPadding.end - ), - verticalAlignment = Alignment.CenterVertically - ) { - repeat(mediaLink.size) { index -> - LinearProgressIndicator( - modifier = Modifier - .dropShadow(cornersRadius = Dimens.cornerRadiusLarge) - .weight(1f) - .padding(end = if (index != mediaLink.lastIndex) videoProgressPadding.spaceBetweenProgress else Dimens.zeroDp), - strokeCap = StrokeCap.Round, - trackColor = videoProgressDefaultColors.trackColor, - color = videoProgressDefaultColors.progressColor, - gapSize = Dimens.zeroDp, - drawStopIndicator = { - // Do nothing - }, - progress = { - when { - currentMediaIndex > index -> 1f - currentMediaIndex == index -> progressForCurrentMedia - else -> 0f - } - } - ) - } - - closeButton() - } -} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgressModifier.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgressModifier.kt deleted file mode 100644 index 80ee191a..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoProgressModifier.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.urlaunched.android.design.ui.videoTutorial.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import com.urlaunched.android.design.ui.shadow.shadow -import com.urlaunched.android.design.ui.videoTutorial.utils.ShadowConstants -import com.urlaunched.android.design.ui.videoTutorial.utils.ShadowDimens - -@Composable -fun Modifier.dropShadow( - color: Color = Color.Black, - alpha: Float = ShadowConstants.DROP_SHADOW_ALPHA, - cornersRadius: Dp = 0.dp, - shadowBlurRadius: Dp = ShadowDimens.dropShadowBlurRadius, - offset: DpOffset = ShadowDimens.dropShadowOffset -) = this.shadow( - alpha = alpha, - shadowBlurRadius = shadowBlurRadius, - offset = offset, - color = color, - cornersRadius = cornersRadius -) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTutorialWrapper.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTutorialWrapper.kt deleted file mode 100644 index 3c01047c..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTutorialWrapper.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.urlaunched.android.design.ui.videoTutorial.ui - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.media3.common.Player - -import com.urlaunched.android.design.ui.videoTutorial.model.VideoContentColors -import com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressColors -import com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressPadding - -@Composable -fun VideoTutorialWrapper( - modifier: Modifier = Modifier, - player: Player?, - videoContentColors: VideoContentColors = VideoContentColors(), - onPreviousVideo: () -> Unit, - onNextVideo: () -> Unit, - mediaLink: List, - currentMediaIndex: Int, - progressForCurrentMedia: Float, - videoProgressDefaultColors: VideoProgressColors = VideoProgressColors(), - videoProgressPadding: VideoProgressPadding = VideoProgressPadding(), - closeButton: @Composable () -> Unit = {}, -) { - Box(modifier = modifier.fillMaxSize()) { - VideoContent( - player = player, - videoContentColors = videoContentColors - ) - - VideoTapNavigator( - onPreviousVideo = onPreviousVideo, - onNextVideo = onNextVideo - ) - - VideoProgress( - mediaLink = mediaLink, - currentMediaIndex = currentMediaIndex, - progressForCurrentMedia = progressForCurrentMedia, - videoProgressDefaultColors = videoProgressDefaultColors, - videoProgressPadding = videoProgressPadding, - closeButton = closeButton, - ) - } -} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowConstants.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowConstants.kt deleted file mode 100644 index 6ce85187..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowConstants.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.urlaunched.android.design.ui.videoTutorial.utils - -object ShadowConstants { - const val DROP_SHADOW_ALPHA = 0.15f -} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowDimens.kt b/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowDimens.kt deleted file mode 100644 index b05759a0..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/utils/ShadowDimens.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.urlaunched.android.design.ui.videoTutorial.utils - -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp - -object ShadowDimens { - val dropShadowBlurRadius = 4.dp - val dropShadowOffset = DpOffset(0.dp, 2.dp) -} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/constants/VideoProgressDefaults.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/constants/VideoProgressDefaults.kt new file mode 100644 index 00000000..76fc65f9 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/constants/VideoProgressDefaults.kt @@ -0,0 +1,23 @@ +package com.urlaunched.android.design.ui.videotutorial.constants + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle + +internal object VideoProgressDefaults { + + private const val SHADOW_ALPHA = 0.15f + + private val shadowBlurRadius = 4.dp + private val shadowOffset = DpOffset(0.dp, 2.dp) + + val DefaultProgressShadow = ShadowStyle( + color = Color.Black, + alpha = SHADOW_ALPHA, + cornersRadius = Dimens.cornerRadiusLarge, + blurRadius = shadowBlurRadius, + offset = shadowOffset + ) +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressColors.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/model/VideoProgressColors.kt similarity index 53% rename from design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressColors.kt rename to design/src/main/java/com/urlaunched/android/design/ui/videotutorial/model/VideoProgressColors.kt index cdd52ba1..53470191 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/model/VideoProgressColors.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/model/VideoProgressColors.kt @@ -1,8 +1,8 @@ -package com.urlaunched.android.design.ui.videoTutorial.model +package com.urlaunched.android.design.ui.videotutorial.model import androidx.compose.ui.graphics.Color data class VideoProgressColors( - val trackColor: Color = Color.White, + val trackColor: Color = Color.LightGray, val progressColor: Color = Color.White ) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/model/VideoProgressStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/model/VideoProgressStyle.kt new file mode 100644 index 00000000..33b74a5d --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/model/VideoProgressStyle.kt @@ -0,0 +1,11 @@ +package com.urlaunched.android.design.ui.videotutorial.model + +import androidx.compose.ui.unit.Dp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle +import com.urlaunched.android.design.ui.videotutorial.constants.VideoProgressDefaults + +data class VideoProgressStyle( + val shadow: ShadowStyle? = VideoProgressDefaults.DefaultProgressShadow, + val gapSize: Dp = Dimens.spacingTiny +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoContent.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoPlayer.kt similarity index 60% rename from design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoContent.kt rename to design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoPlayer.kt index 51c7ae04..166eccce 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoContent.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoPlayer.kt @@ -1,4 +1,4 @@ -package com.urlaunched.android.design.ui.videoTutorial.ui +package com.urlaunched.android.design.ui.videotutorial.ui import android.view.ViewGroup import android.widget.FrameLayout @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.Player @@ -15,35 +16,28 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView - -import com.urlaunched.android.design.ui.videoTutorial.model.VideoContentColors - @OptIn(UnstableApi::class) @Composable -internal fun VideoContent( - player: Player?, - videoContentColors: VideoContentColors = VideoContentColors() -) { +internal fun VideoPlayer(videoPlayer: Player?, videoResizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_ZOOM) { if (LocalInspectionMode.current) { Box( modifier = Modifier .fillMaxSize() - .background(videoContentColors.backgroundColor) + .background(Color(0xFF4CAF50)) ) } else { AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> PlayerView(context).apply { - this.player = player + player = videoPlayer useController = false - resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM + resizeMode = videoResizeMode - layoutParams = - FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) } } ) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoProgress.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoProgress.kt new file mode 100644 index 00000000..b4fb8a56 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoProgress.kt @@ -0,0 +1,80 @@ +package com.urlaunched.android.design.ui.videotutorial.ui + +import androidx.annotation.FloatRange +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.modifiers.ifNotNull +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle +import com.urlaunched.android.design.ui.shadow.shadow +import com.urlaunched.android.design.ui.videotutorial.constants.VideoProgressDefaults + +@Composable +internal fun VideoProgress( + modifier: Modifier = Modifier, + mediaCount: Int, + currentMediaIndex: Int, + @FloatRange(0.0, 1.0) + currentMediaProgress: Float, + trackColor: Color = Color.LightGray, + progressColor: Color = Color.White, + shadow: ShadowStyle? = VideoProgressDefaults.DefaultProgressShadow, + gapSize: Dp = Dimens.spacingTiny, + closeButton: @Composable RowScope.() -> Unit = {} +) { + Row( + modifier = modifier.statusBarsPadding(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(gapSize) + ) { + repeat(mediaCount) { index -> + LinearProgressIndicator( + modifier = Modifier + .ifNotNull(shadow) { Modifier.shadow(it) } + .weight(1f), + strokeCap = StrokeCap.Round, + trackColor = trackColor, + color = progressColor, + gapSize = Dimens.zeroDp, + drawStopIndicator = { + // Do nothing + }, + progress = { + when { + currentMediaIndex > index -> 1f + currentMediaIndex == index -> currentMediaProgress + else -> 0f + } + } + ) + } + + closeButton() + } +} + +@Preview +@Composable +private fun VideoProgressPreview() { + VideoProgress( + modifier = Modifier + .fillMaxWidth() + .padding(Dimens.spacingNormal), + progressColor = Color.Red, + mediaCount = 5, + currentMediaIndex = 3, + currentMediaProgress = 0.5f + ) +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTapNavigator.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTapNavigator.kt similarity index 65% rename from design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTapNavigator.kt rename to design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTapNavigator.kt index ae81ab8e..e043c521 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/videoTutorial/ui/VideoTapNavigator.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTapNavigator.kt @@ -1,24 +1,22 @@ -package com.urlaunched.android.design.ui.videoTutorial.ui +package com.urlaunched.android.design.ui.videotutorial.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @Composable -internal fun VideoTapNavigator( - onPreviousVideo: () -> Unit, - onNextVideo: () -> Unit -) { - Row(Modifier.fillMaxSize()) { +internal fun VideoTapNavigator(modifier: Modifier = Modifier, onPreviousVideo: () -> Unit, onNextVideo: () -> Unit) { + Row(modifier = modifier) { Box( modifier = Modifier .fillMaxHeight() .weight(1f) .clickable( + interactionSource = null, + indication = null, onClick = onPreviousVideo ) ) @@ -28,6 +26,8 @@ internal fun VideoTapNavigator( .fillMaxHeight() .weight(1f) .clickable( + interactionSource = null, + indication = null, onClick = onNextVideo ) ) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt new file mode 100644 index 00000000..805bdd18 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt @@ -0,0 +1,95 @@ +package com.urlaunched.android.design.ui.videotutorial.ui + +import androidx.annotation.FloatRange +import androidx.annotation.OptIn +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.AspectRatioFrameLayout +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.videotutorial.model.VideoProgressColors +import com.urlaunched.android.design.ui.videotutorial.model.VideoProgressStyle + +@OptIn(UnstableApi::class) +@Composable +fun VideoTutorialContainer( + modifier: Modifier = Modifier, + player: Player?, + mediaCount: Int, + currentMediaIndex: Int, + @FloatRange(0.0, 1.0) + currentMediaProgress: Float, + onPreviousVideo: () -> Unit, + onNextVideo: () -> Unit, + videoResizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_ZOOM, + progressBarPadding: PaddingValues = + PaddingValues(horizontal = Dimens.spacingNormal, vertical = Dimens.spacingNormalSpecial), + videoProgressColors: VideoProgressColors = VideoProgressColors(), + videoProgressStyle: VideoProgressStyle = VideoProgressStyle(), + closeButton: @Composable RowScope.() -> Unit = {} +) { + Box( + modifier = modifier + ) { + VideoPlayer( + videoPlayer = player, + videoResizeMode = videoResizeMode + ) + + VideoTapNavigator( + onPreviousVideo = onPreviousVideo, + onNextVideo = onNextVideo + ) + + VideoProgress( + modifier = Modifier.padding(progressBarPadding), + mediaCount = mediaCount, + currentMediaIndex = currentMediaIndex, + currentMediaProgress = currentMediaProgress, + progressColor = videoProgressColors.progressColor, + trackColor = videoProgressColors.trackColor, + closeButton = closeButton, + shadow = videoProgressStyle.shadow, + gapSize = videoProgressStyle.gapSize + ) + } +} + +@Preview +@Composable +private fun VideoTutorialContainerPreview() { + VideoTutorialContainer( + player = null, + mediaCount = 5, + currentMediaIndex = 2, + currentMediaProgress = 0.6f, + onPreviousVideo = {}, + onNextVideo = {}, + closeButton = { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = null, + tint = Color.White, + modifier = Modifier + .padding(start = Dimens.spacingNormal) + .clickable( + interactionSource = null, + indication = ripple(bounded = false), + onClick = {} + ) + ) + } + ) +} \ No newline at end of file From 1a650972291707df37a88671dfa7ff22b6ba279a Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 09:34:26 +0300 Subject: [PATCH 04/15] Update api --- design/api/current.api | 89 ++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/design/api/current.api b/design/api/current.api index 4244b764..eb2e75a5 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -428,11 +428,36 @@ package com.urlaunched.android.design.ui.scrollbar.controller { package com.urlaunched.android.design.ui.shadow { public final class ShadowKt { + method public static androidx.compose.ui.Modifier shadow(androidx.compose.ui.Modifier, com.urlaunched.android.design.ui.shadow.models.ShadowStyle style); method public static androidx.compose.ui.Modifier shadow(androidx.compose.ui.Modifier, optional long color, optional float alpha, optional float cornersRadius, optional float shadowBlurRadius, optional long offset); } } +package com.urlaunched.android.design.ui.shadow.models { + + public final class ShadowStyle { + ctor public ShadowStyle(optional long color, optional float alpha, optional float cornersRadius, optional float blurRadius, optional long offset); + method public long component1-0d7_KjU(); + method public float component2(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public long component5-RKDOV3M(); + method public com.urlaunched.android.design.ui.shadow.models.ShadowStyle copy-w1ByDHw(long color, float alpha, float cornersRadius, float blurRadius, long offset); + method public float getAlpha(); + method public float getBlurRadius(); + method public long getColor(); + method public float getCornersRadius(); + method public long getOffset(); + property public final float alpha; + property public final float blurRadius; + property public final long color; + property public final float cornersRadius; + property public final long offset; + } + +} + package com.urlaunched.android.design.ui.shimmer { public final class ShimmerKt { @@ -730,74 +755,36 @@ package com.urlaunched.android.design.ui.tutorial.utils { } -package com.urlaunched.android.design.ui.videoTutorial.model { - - public final class VideoContentColors { - ctor public VideoContentColors(optional long backgroundColor); - method public long component1-0d7_KjU(); - method public com.urlaunched.android.design.ui.videoTutorial.model.VideoContentColors copy-8_81llA(long backgroundColor); - method public long getBackgroundColor(); - property public final long backgroundColor; - } +package com.urlaunched.android.design.ui.videotutorial.model { public final class VideoProgressColors { ctor public VideoProgressColors(optional long trackColor, optional long progressColor); method public long component1-0d7_KjU(); method public long component2-0d7_KjU(); - method public com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressColors copy--OWjLjI(long trackColor, long progressColor); + method public com.urlaunched.android.design.ui.videotutorial.model.VideoProgressColors copy--OWjLjI(long trackColor, long progressColor); method public long getProgressColor(); method public long getTrackColor(); property public final long progressColor; property public final long trackColor; } - public final class VideoProgressPadding { - ctor public VideoProgressPadding(optional float start, optional float top, optional float end, optional float bottom, optional float spaceBetweenProgress); - method public float component1-D9Ej5fM(); + public final class VideoProgressStyle { + ctor public VideoProgressStyle(optional com.urlaunched.android.design.ui.shadow.models.ShadowStyle? shadow, optional float gapSize); + method public com.urlaunched.android.design.ui.shadow.models.ShadowStyle? component1(); method public float component2-D9Ej5fM(); - method public float component3-D9Ej5fM(); - method public float component4-D9Ej5fM(); - method public float component5-D9Ej5fM(); - method public com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressPadding copy-RyVG9vg(float start, float top, float end, float bottom, float spaceBetweenProgress); - method public float getBottom(); - method public float getEnd(); - method public float getSpaceBetweenProgress(); - method public float getStart(); - method public float getTop(); - property public final float bottom; - property public final float end; - property public final float spaceBetweenProgress; - property public final float start; - property public final float top; + method public com.urlaunched.android.design.ui.videotutorial.model.VideoProgressStyle copy-3ABfNKs(com.urlaunched.android.design.ui.shadow.models.ShadowStyle? shadow, float gapSize); + method public float getGapSize(); + method public com.urlaunched.android.design.ui.shadow.models.ShadowStyle? getShadow(); + property public final float gapSize; + property public final com.urlaunched.android.design.ui.shadow.models.ShadowStyle? shadow; } } -package com.urlaunched.android.design.ui.videoTutorial.ui { - - public final class VideoProgressModifierKt { - method @androidx.compose.runtime.Composable public static androidx.compose.ui.Modifier dropShadow(androidx.compose.ui.Modifier, optional long color, optional float alpha, optional float cornersRadius, optional float shadowBlurRadius, optional long offset); - } - - public final class VideoTutorialWrapperKt { - method @androidx.compose.runtime.Composable public static void VideoTutorialWrapper(optional androidx.compose.ui.Modifier modifier, androidx.media3.common.Player? player, optional com.urlaunched.android.design.ui.videoTutorial.model.VideoContentColors videoContentColors, kotlin.jvm.functions.Function0 onPreviousVideo, kotlin.jvm.functions.Function0 onNextVideo, java.util.List mediaLink, int currentMediaIndex, float progressForCurrentMedia, optional com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressColors videoProgressDefaultColors, optional com.urlaunched.android.design.ui.videoTutorial.model.VideoProgressPadding videoProgressPadding, optional kotlin.jvm.functions.Function0 closeButton); - } - -} - -package com.urlaunched.android.design.ui.videoTutorial.utils { - - public final class ShadowConstants { - field public static final float DROP_SHADOW_ALPHA = 0.15f; - field public static final com.urlaunched.android.design.ui.videoTutorial.utils.ShadowConstants INSTANCE; - } +package com.urlaunched.android.design.ui.videotutorial.ui { - public final class ShadowDimens { - method public float getDropShadowBlurRadius(); - method public long getDropShadowOffset(); - property public final float dropShadowBlurRadius; - property public final long dropShadowOffset; - field public static final com.urlaunched.android.design.ui.videoTutorial.utils.ShadowDimens INSTANCE; + public final class VideoTutorialContainerKt { + method @androidx.compose.runtime.Composable public static void VideoTutorialContainer(optional androidx.compose.ui.Modifier modifier, androidx.media3.common.Player? player, int mediaCount, int currentMediaIndex, @FloatRange(from=0.0, to=1.0) float currentMediaProgress, kotlin.jvm.functions.Function0 onPreviousVideo, kotlin.jvm.functions.Function0 onNextVideo, optional int videoResizeMode, optional androidx.compose.foundation.layout.PaddingValues progressBarPadding, optional com.urlaunched.android.design.ui.videotutorial.model.VideoProgressColors videoProgressColors, optional com.urlaunched.android.design.ui.videotutorial.model.VideoProgressStyle videoProgressStyle, optional kotlin.jvm.functions.Function1 closeButton); } } From 9a3d5cdd26d2ac0dea8c61d41692d9d0a66fdbb5 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 10:05:43 +0300 Subject: [PATCH 05/15] Remove media3 ui duplicate dependency --- design/build.gradle | 5 +++-- gradle/libs.versions.toml | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/design/build.gradle b/design/build.gradle index cf8215ea..b42c802c 100644 --- a/design/build.gradle +++ b/design/build.gradle @@ -75,8 +75,9 @@ dependencies { debugImplementation libs.composeDependencies.composeUiTooling debugImplementation libs.composeDependencies.composeUiTestManifest - //Media 3 - implementation libs.composeMedia3.ui + // Media3 + implementation libs.media3Dependencies.core + implementation libs.media3Dependencies.ui // Paging implementation libs.pagingDependencies.core diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 894eead2..502f8c34 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,7 +101,6 @@ aspectPluginVersion = "1.4.1" junit = "1.2.1" espressoCoreVersion = "3.6.1" materialVersion = "1.12.0" -media3UiVersion = "1.7.1" [libraries] androidCoreDependencies-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } @@ -169,7 +168,6 @@ roomDependencies-paging = { module = "androidx.room:room-paging", version.ref = media3Dependencies-core = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Version" } media3Dependencies-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Version" } media3Dependencies-session = { module = "androidx.media3:media3-session", version.ref = "media3Version" } -composeMedia3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3UiVersion" } androidXMediaDependencies-core = { module = "androidx.media:media", version.ref = "androidXMediaVersion" } From 27218cdefdda6fd8c9b988306c16272af2b46c99 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 10:15:56 +0300 Subject: [PATCH 06/15] Add player function to play a list of urls --- .../NoOpSinglePlayerStateImpl.kt | 4 ++++ .../player/signleplayerstate/SinglePlayerState.kt | 1 + .../signleplayerstate/SinglePlayerStateImpl.kt | 15 +++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt index 8c47db65..15a2c390 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt @@ -29,6 +29,10 @@ class NoOpSinglePlayerStateImpl : SinglePlayerState { // No-op } + override fun playUrls(urls: List) { + // No-op + } + override fun playFile(path: String, id: String) { // No-op } diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt index a5409570..2dfba17b 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt @@ -23,6 +23,7 @@ interface SinglePlayerState : DefaultLifecycleObserver { fun release() fun seekTo(millis: Long) fun seekFor(millis: Long) + fun playUrls(urls: List) } @Composable diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt index 15b6b99b..84b35b52 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt @@ -94,6 +94,21 @@ class SinglePlayerStateImpl( } } + override fun playUrls(urls: List) { + player?.run { + addMediaItems( + urls.map { url -> + MediaItem.Builder() + .setUri(url) + .setMediaId(url) + .build() + } + ) + prepare() + playWhenReady = true + } + } + override fun playFile(path: String, id: String) { player?.run { if (currentMediaItem?.mediaId != id) { From dc3089c97c76f2f3d445d4fd92631ab4aeb0723d Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 10:17:08 +0300 Subject: [PATCH 07/15] Make notificationChannelName null by default --- .../android/player/signleplayerstate/SinglePlayerState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt index 2dfba17b..5082663c 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt @@ -32,7 +32,7 @@ fun rememberPlayerState( context: Context = LocalContext.current, coroutineScope: CoroutineScope = rememberCoroutineScope(), notificationData: NotificationPlayerHelper.NotificationData? = null, - notificationChannelName: String? + notificationChannelName: String? = null ): SinglePlayerState = if (LocalInspectionMode.current) { remember { NoOpSinglePlayerStateImpl() From eba3b5f6a64ef8d8dbcce866df497595788656f1 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 10:25:32 +0300 Subject: [PATCH 08/15] Add seekToStartOnEnd flag to player --- .../android/player/signleplayerstate/SinglePlayerState.kt | 5 +++-- .../player/signleplayerstate/SinglePlayerStateImpl.kt | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt index 5082663c..1c7d9862 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerState.kt @@ -31,6 +31,7 @@ interface SinglePlayerState : DefaultLifecycleObserver { fun rememberPlayerState( context: Context = LocalContext.current, coroutineScope: CoroutineScope = rememberCoroutineScope(), + seekToStartOnEnd: Boolean = true, notificationData: NotificationPlayerHelper.NotificationData? = null, notificationChannelName: String? = null ): SinglePlayerState = if (LocalInspectionMode.current) { @@ -38,7 +39,7 @@ fun rememberPlayerState( NoOpSinglePlayerStateImpl() } } else { - remember(context, coroutineScope, notificationData) { - SinglePlayerStateImpl(context, coroutineScope, notificationData, notificationChannelName) + remember(context, coroutineScope, seekToStartOnEnd, notificationData) { + SinglePlayerStateImpl(context, seekToStartOnEnd, coroutineScope, notificationData, notificationChannelName) } } \ No newline at end of file diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt index 84b35b52..545c2da3 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt @@ -22,6 +22,7 @@ import java.io.File @UnstableApi class SinglePlayerStateImpl( context: Context, + private val seekToStartOnEnd: Boolean, private val coroutineScope: CoroutineScope, private val notificationData: NotificationPlayerHelper.NotificationData?, private val notificationChannelName: String? @@ -228,8 +229,10 @@ class SinglePlayerStateImpl( } Player.STATE_ENDED -> { - player?.seekTo(0) - player?.pause() + if (seekToStartOnEnd) { + player?.seekTo(0) + player?.pause() + } actualState = AudioState.PAUSE actualState From 2afa078dd725f9e4b8f212f4daa0f87d7d6b7ba1 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 10:37:28 +0300 Subject: [PATCH 09/15] Add currentMediaIndex property to player ui state --- .../player/signleplayerstate/NoOpSinglePlayerStateImpl.kt | 3 ++- .../player/signleplayerstate/SinglePlayerStateImpl.kt | 6 ++++-- .../android/player/signleplayerstate/model/PlayerUiState.kt | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt index 15a2c390..f22dbb7e 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt @@ -11,7 +11,8 @@ class NoOpSinglePlayerStateImpl : SinglePlayerState { PlayerUiState( audioState = AudioState.PAUSE, currentMediaItemId = "", - audioDuration = 0L + audioDuration = 0L, + currentMediaIndex = 0 ) ) override val currentPlayingPosition: Long = 0 diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt index 545c2da3..8d1fee22 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt @@ -36,7 +36,8 @@ class SinglePlayerStateImpl( PlayerUiState( audioState = AudioState.PAUSE, currentMediaItemId = "-1", - audioDuration = 0 + audioDuration = 0, + currentMediaIndex = 0 ) ) @@ -216,7 +217,8 @@ class SinglePlayerStateImpl( coroutineScope.launch { _playerUiState.value = _playerUiState.value.copy( audioState = player.state, - audioDuration = player?.duration.takeIf { it != C.TIME_UNSET } ?: 0 + audioDuration = player?.duration.takeIf { it != C.TIME_UNSET } ?: 0, + currentMediaIndex = player?.currentMediaItemIndex ?: 0 ) } } diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/model/PlayerUiState.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/model/PlayerUiState.kt index ac124893..3d053f60 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/model/PlayerUiState.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/model/PlayerUiState.kt @@ -6,5 +6,6 @@ data class PlayerUiState( val audioState: AudioState, val currentMediaItemId: String, val audioDuration: Long, - val isWaitingForAutoplay: Boolean = false + val isWaitingForAutoplay: Boolean = false, + val currentMediaIndex: Int ) \ No newline at end of file From 4b85bbbec823b474afed83807bacbb273a9f8ddb Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 10:39:15 +0300 Subject: [PATCH 10/15] Add endReached property to player ui state --- .../player/signleplayerstate/NoOpSinglePlayerStateImpl.kt | 3 ++- .../player/signleplayerstate/SinglePlayerStateImpl.kt | 6 ++++-- .../android/player/signleplayerstate/model/PlayerUiState.kt | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt index f22dbb7e..3cf4756a 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/NoOpSinglePlayerStateImpl.kt @@ -12,7 +12,8 @@ class NoOpSinglePlayerStateImpl : SinglePlayerState { audioState = AudioState.PAUSE, currentMediaItemId = "", audioDuration = 0L, - currentMediaIndex = 0 + currentMediaIndex = 0, + endReached = false ) ) override val currentPlayingPosition: Long = 0 diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt index 8d1fee22..799c0217 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt @@ -37,7 +37,8 @@ class SinglePlayerStateImpl( audioState = AudioState.PAUSE, currentMediaItemId = "-1", audioDuration = 0, - currentMediaIndex = 0 + currentMediaIndex = 0, + endReached = false ) ) @@ -218,7 +219,8 @@ class SinglePlayerStateImpl( _playerUiState.value = _playerUiState.value.copy( audioState = player.state, audioDuration = player?.duration.takeIf { it != C.TIME_UNSET } ?: 0, - currentMediaIndex = player?.currentMediaItemIndex ?: 0 + currentMediaIndex = player?.currentMediaItemIndex ?: 0, + endReached = player?.playbackState == Player.STATE_ENDED ) } } diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/model/PlayerUiState.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/model/PlayerUiState.kt index 3d053f60..219fc719 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/model/PlayerUiState.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/model/PlayerUiState.kt @@ -7,5 +7,6 @@ data class PlayerUiState( val currentMediaItemId: String, val audioDuration: Long, val isWaitingForAutoplay: Boolean = false, - val currentMediaIndex: Int + val currentMediaIndex: Int, + val endReached: Boolean ) \ No newline at end of file From 33b42e605474f51836444aaa56f536442f6e1b46 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 12:41:26 +0300 Subject: [PATCH 11/15] Add overload with build-in player --- design/build.gradle | 2 + .../ui/VideoTutorialContainer.kt | 100 ++++++++++++++++-- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/design/build.gradle b/design/build.gradle index b42c802c..e06fbc7e 100644 --- a/design/build.gradle +++ b/design/build.gradle @@ -95,6 +95,8 @@ dependencies { // Local modules implementation project(":cdn:models:presentation") + implementation project(":player") + implementation project(":common") // Bottom sheet implementation libs.bottomSheet diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt index 805bdd18..f22d5706 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt @@ -12,15 +12,102 @@ import androidx.compose.material.icons.rounded.Close import androidx.compose.material3.Icon import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout +import com.urlaunched.android.common.lifecycle.HandleLifecycleEvents import com.urlaunched.android.design.resources.dimens.Dimens import com.urlaunched.android.design.ui.videotutorial.model.VideoProgressColors import com.urlaunched.android.design.ui.videotutorial.model.VideoProgressStyle +import com.urlaunched.android.player.signleplayerstate.PlayerCollectingContainer +import com.urlaunched.android.player.signleplayerstate.model.PlayerUiState +import com.urlaunched.android.player.signleplayerstate.rememberPlayerState +import kotlin.time.Duration.Companion.milliseconds + +@OptIn(UnstableApi::class) +@Composable +fun VideoTutorialContainer( + modifier: Modifier = Modifier, + videoTutorial: List, + onTutorialFinish: () -> Unit, + videoResizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_ZOOM, + progressBarPadding: PaddingValues = + PaddingValues(horizontal = Dimens.spacingNormal, vertical = Dimens.spacingNormalSpecial), + videoProgressColors: VideoProgressColors = VideoProgressColors(), + videoProgressStyle: VideoProgressStyle = VideoProgressStyle(), + closeButton: @Composable RowScope.() -> Unit = {} +) { + val lifecycleOwner = LocalLifecycleOwner.current + val playerState = rememberPlayerState(seekToStartOnEnd = false) + val playerUiState by playerState.playerUiState.collectAsStateWithLifecycle() + var currentMediaProgress by remember { mutableFloatStateOf(0f) } + + VideoTutorialContainer( + modifier = modifier, + player = playerState.player, + mediaCount = videoTutorial.size, + currentMediaIndex = playerUiState.currentMediaIndex, + currentMediaProgress = currentMediaProgress, + videoResizeMode = videoResizeMode, + progressBarPadding = progressBarPadding, + videoProgressStyle = videoProgressStyle, + videoProgressColors = videoProgressColors, + onPreviousVideo = { + if (playerState.player?.hasPreviousMediaItem() == true) { + playerState.player?.seekToPreviousMediaItem() + } else { + playerState.player?.seekToPrevious() + } + }, + onNextVideo = { + playerState.player?.seekToNext() + }, + closeButton = closeButton + ) + + PlayerCollectingContainer( + playerUiState = playerUiState, + getCurrentPosition = { + playerState.currentPlayingPosition + }, + content = { _: PlayerUiState, _: Long, currentPlayingPosition: MutableState, duration: Long -> + currentMediaProgress = (currentPlayingPosition.value.milliseconds / duration.milliseconds).toFloat() + .takeIf { it.isFinite() } ?: 0f + } + ) + + LaunchedEffect(playerUiState.endReached) { + if (playerUiState.endReached) { + onTutorialFinish() + } + } + + HandleLifecycleEvents( + onStart = { playerState.player?.play() } + ) + + DisposableEffect(playerState) { + lifecycleOwner.lifecycle.addObserver(playerState) + playerState.playUrls(videoTutorial) + + onDispose { + playerState.release() + lifecycleOwner.lifecycle.removeObserver(playerState) + } + } +} @OptIn(UnstableApi::class) @Composable @@ -71,12 +158,13 @@ fun VideoTutorialContainer( @Composable private fun VideoTutorialContainerPreview() { VideoTutorialContainer( - player = null, - mediaCount = 5, - currentMediaIndex = 2, - currentMediaProgress = 0.6f, - onPreviousVideo = {}, - onNextVideo = {}, + videoTutorial = listOf( + "https://www.w3schools.com/html/mov_bbb.mp4", + "https://www.w3schools.com/html/mov_bbb.mp4", + "https://www.w3schools.com/html/mov_bbb.mp4", + "https://www.w3schools.com/html/mov_bbb.mp4" + ), + onTutorialFinish = {}, closeButton = { Icon( imageVector = Icons.Rounded.Close, From 6495d9968f8944b64e65f73c1253b5491c5befd7 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 17:38:17 +0300 Subject: [PATCH 12/15] Add tracks changed event to playback change reason --- .../android/player/signleplayerstate/SinglePlayerStateImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt index 799c0217..246f10a5 100644 --- a/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt +++ b/player/src/main/java/com/urlaunched/android/player/signleplayerstate/SinglePlayerStateImpl.kt @@ -207,7 +207,8 @@ class SinglePlayerStateImpl( private inline fun Player.Events.onPlaybackButtonChanged(changePlaybackState: () -> Unit) { if (containsAny( Player.EVENT_PLAYBACK_STATE_CHANGED, - Player.EVENT_PLAY_WHEN_READY_CHANGED + Player.EVENT_PLAY_WHEN_READY_CHANGED, + Player.EVENT_TRACKS_CHANGED ) ) { changePlaybackState() From 38f40f250d394a4e34f2562a03f2d45b80146574 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Wed, 8 Oct 2025 17:38:45 +0300 Subject: [PATCH 13/15] Update agp to 8.6.0 --- gradle/libs.versions.toml | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 502f8c34..037ae2c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -87,8 +87,8 @@ bottomSheetVersion = "1.33.0" # plugins # dependencies -androidApplicationPluginVersion = "8.4.2" -androidLibraryPluginVersion = "8.4.2" +androidApplicationPluginVersion = "8.6.0" +androidLibraryPluginVersion = "8.6.0" kotlinAndroidPluginVersion = "2.1.20" kotlinJvmPluginVersion = "2.1.20" kotlinSerializationPluginVersion = "2.1.20" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a..a4413138 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 8d230ed8a11a1636f8c7ba41e6abbc9dd658a117 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 13 Oct 2025 09:29:48 +0300 Subject: [PATCH 14/15] Rename videoTutorial parameter to videoUrls --- .../design/ui/videotutorial/ui/VideoTutorialContainer.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt index f22d5706..714adda3 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt @@ -40,7 +40,7 @@ import kotlin.time.Duration.Companion.milliseconds @Composable fun VideoTutorialContainer( modifier: Modifier = Modifier, - videoTutorial: List, + videoUrls: List, onTutorialFinish: () -> Unit, videoResizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_ZOOM, progressBarPadding: PaddingValues = @@ -57,7 +57,7 @@ fun VideoTutorialContainer( VideoTutorialContainer( modifier = modifier, player = playerState.player, - mediaCount = videoTutorial.size, + mediaCount = videoUrls.size, currentMediaIndex = playerUiState.currentMediaIndex, currentMediaProgress = currentMediaProgress, videoResizeMode = videoResizeMode, @@ -100,7 +100,7 @@ fun VideoTutorialContainer( DisposableEffect(playerState) { lifecycleOwner.lifecycle.addObserver(playerState) - playerState.playUrls(videoTutorial) + playerState.playUrls(videoUrls) onDispose { playerState.release() @@ -158,7 +158,7 @@ fun VideoTutorialContainer( @Composable private fun VideoTutorialContainerPreview() { VideoTutorialContainer( - videoTutorial = listOf( + videoUrls = listOf( "https://www.w3schools.com/html/mov_bbb.mp4", "https://www.w3schools.com/html/mov_bbb.mp4", "https://www.w3schools.com/html/mov_bbb.mp4", From 4975b4dd4df2c304f667ac0a4c4978d4da9f1662 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 13 Oct 2025 09:30:06 +0300 Subject: [PATCH 15/15] Update api --- design/api/current.api | 1 + player/api/current.api | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/design/api/current.api b/design/api/current.api index eb2e75a5..996de2e5 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -785,6 +785,7 @@ package com.urlaunched.android.design.ui.videotutorial.ui { public final class VideoTutorialContainerKt { method @androidx.compose.runtime.Composable public static void VideoTutorialContainer(optional androidx.compose.ui.Modifier modifier, androidx.media3.common.Player? player, int mediaCount, int currentMediaIndex, @FloatRange(from=0.0, to=1.0) float currentMediaProgress, kotlin.jvm.functions.Function0 onPreviousVideo, kotlin.jvm.functions.Function0 onNextVideo, optional int videoResizeMode, optional androidx.compose.foundation.layout.PaddingValues progressBarPadding, optional com.urlaunched.android.design.ui.videotutorial.model.VideoProgressColors videoProgressColors, optional com.urlaunched.android.design.ui.videotutorial.model.VideoProgressStyle videoProgressStyle, optional kotlin.jvm.functions.Function1 closeButton); + method @androidx.compose.runtime.Composable public static void VideoTutorialContainer(optional androidx.compose.ui.Modifier modifier, java.util.List videoUrls, kotlin.jvm.functions.Function0 onTutorialFinish, optional int videoResizeMode, optional androidx.compose.foundation.layout.PaddingValues progressBarPadding, optional com.urlaunched.android.design.ui.videotutorial.model.VideoProgressColors videoProgressColors, optional com.urlaunched.android.design.ui.videotutorial.model.VideoProgressStyle videoProgressStyle, optional kotlin.jvm.functions.Function1 closeButton); } } diff --git a/player/api/current.api b/player/api/current.api index 45a09377..94dbb668 100644 --- a/player/api/current.api +++ b/player/api/current.api @@ -116,6 +116,7 @@ package com.urlaunched.android.player.signleplayerstate { method public void pause(); method public void playFile(String path, String id); method public void playUrl(String url, String id); + method public void playUrls(java.util.List urls); method public void release(); method public void seekFor(long millis); method public void seekTo(long millis); @@ -155,6 +156,7 @@ package com.urlaunched.android.player.signleplayerstate { method public void pause(); method public void playFile(String path, String id); method public void playUrl(String url, String id); + method public void playUrls(java.util.List urls); method public void release(); method public void seekFor(long millis); method public void seekTo(long millis); @@ -164,13 +166,14 @@ package com.urlaunched.android.player.signleplayerstate { } @androidx.media3.common.util.UnstableApi public final class SinglePlayerStateImpl implements com.urlaunched.android.player.signleplayerstate.SinglePlayerState { - ctor public SinglePlayerStateImpl(android.content.Context context, kotlinx.coroutines.CoroutineScope coroutineScope, com.urlaunched.android.player.signleplayerstate.NotificationPlayerHelper.NotificationData? notificationData, String? notificationChannelName); + ctor public SinglePlayerStateImpl(android.content.Context context, boolean seekToStartOnEnd, kotlinx.coroutines.CoroutineScope coroutineScope, com.urlaunched.android.player.signleplayerstate.NotificationPlayerHelper.NotificationData? notificationData, String? notificationChannelName); method public long getCurrentPlayingPosition(); method public androidx.media3.common.Player? getPlayer(); method public kotlinx.coroutines.flow.StateFlow getPlayerUiState(); method public void pause(); method public void playFile(String path, String id); method public void playUrl(String url, String id); + method public void playUrls(java.util.List urls); method @androidx.media3.common.util.UnstableApi public void release(); method public void seekFor(long millis); method public void seekTo(long millis); @@ -181,7 +184,7 @@ package com.urlaunched.android.player.signleplayerstate { } public final class SinglePlayerStateKt { - method @androidx.compose.runtime.Composable @androidx.media3.common.util.UnstableApi public static com.urlaunched.android.player.signleplayerstate.SinglePlayerState rememberPlayerState(optional android.content.Context context, optional kotlinx.coroutines.CoroutineScope coroutineScope, optional com.urlaunched.android.player.signleplayerstate.NotificationPlayerHelper.NotificationData? notificationData, String? notificationChannelName); + method @androidx.compose.runtime.Composable @androidx.media3.common.util.UnstableApi public static com.urlaunched.android.player.signleplayerstate.SinglePlayerState rememberPlayerState(optional android.content.Context context, optional kotlinx.coroutines.CoroutineScope coroutineScope, optional boolean seekToStartOnEnd, optional com.urlaunched.android.player.signleplayerstate.NotificationPlayerHelper.NotificationData? notificationData, optional String? notificationChannelName); } } @@ -189,19 +192,25 @@ package com.urlaunched.android.player.signleplayerstate { package com.urlaunched.android.player.signleplayerstate.model { public final class PlayerUiState { - ctor public PlayerUiState(com.urlaunched.android.player.models.AudioState audioState, String currentMediaItemId, long audioDuration, optional boolean isWaitingForAutoplay); + ctor public PlayerUiState(com.urlaunched.android.player.models.AudioState audioState, String currentMediaItemId, long audioDuration, optional boolean isWaitingForAutoplay, int currentMediaIndex, boolean endReached); method public com.urlaunched.android.player.models.AudioState component1(); method public String component2(); method public long component3(); method public boolean component4(); - method public com.urlaunched.android.player.signleplayerstate.model.PlayerUiState copy(com.urlaunched.android.player.models.AudioState audioState, String currentMediaItemId, long audioDuration, boolean isWaitingForAutoplay); + method public int component5(); + method public boolean component6(); + method public com.urlaunched.android.player.signleplayerstate.model.PlayerUiState copy(com.urlaunched.android.player.models.AudioState audioState, String currentMediaItemId, long audioDuration, boolean isWaitingForAutoplay, int currentMediaIndex, boolean endReached); method public long getAudioDuration(); method public com.urlaunched.android.player.models.AudioState getAudioState(); + method public int getCurrentMediaIndex(); method public String getCurrentMediaItemId(); + method public boolean getEndReached(); method public boolean isWaitingForAutoplay(); property public final long audioDuration; property public final com.urlaunched.android.player.models.AudioState audioState; + property public final int currentMediaIndex; property public final String currentMediaItemId; + property public final boolean endReached; property public final boolean isWaitingForAutoplay; }