diff --git a/design/api/current.api b/design/api/current.api index 0e9da44d..996de2e5 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,3 +755,38 @@ package com.urlaunched.android.design.ui.tutorial.utils { } +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 long getProgressColor(); + method public long getTrackColor(); + property public final long progressColor; + property public final long trackColor; + } + + 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 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 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/design/build.gradle b/design/build.gradle index 38319e3a..e06fbc7e 100644 --- a/design/build.gradle +++ b/design/build.gradle @@ -75,6 +75,10 @@ dependencies { debugImplementation libs.composeDependencies.composeUiTooling debugImplementation libs.composeDependencies.composeUiTestManifest + // Media3 + implementation libs.media3Dependencies.core + implementation libs.media3Dependencies.ui + // Paging implementation libs.pagingDependencies.core implementation libs.pagingDependencies.compose @@ -91,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/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 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 new file mode 100644 index 00000000..53470191 --- /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.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/VideoPlayer.kt b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoPlayer.kt new file mode 100644 index 00000000..166eccce --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoPlayer.kt @@ -0,0 +1,45 @@ +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.graphics.Color +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 + +@OptIn(UnstableApi::class) +@Composable +internal fun VideoPlayer(videoPlayer: Player?, videoResizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_ZOOM) { + if (LocalInspectionMode.current) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF4CAF50)) + ) + } else { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + PlayerView(context).apply { + player = videoPlayer + useController = false + resizeMode = videoResizeMode + + 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..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 new file mode 100644 index 00000000..e043c521 --- /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.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +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 + ) + ) + + Box( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .clickable( + interactionSource = null, + indication = null, + onClick = onNextVideo + ) + ) + } +} \ No newline at end of file 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..714adda3 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/videotutorial/ui/VideoTutorialContainer.kt @@ -0,0 +1,183 @@ +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.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, + videoUrls: 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 = videoUrls.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(videoUrls) + + onDispose { + playerState.release() + lifecycleOwner.lifecycle.removeObserver(playerState) + } + } +} + +@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( + 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", + "https://www.w3schools.com/html/mov_bbb.mp4" + ), + onTutorialFinish = {}, + 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 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 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; } 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..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 @@ -11,7 +11,9 @@ class NoOpSinglePlayerStateImpl : SinglePlayerState { PlayerUiState( audioState = AudioState.PAUSE, currentMediaItemId = "", - audioDuration = 0L + audioDuration = 0L, + currentMediaIndex = 0, + endReached = false ) ) override val currentPlayingPosition: Long = 0 @@ -29,6 +31,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..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 @@ -23,6 +23,7 @@ interface SinglePlayerState : DefaultLifecycleObserver { fun release() fun seekTo(millis: Long) fun seekFor(millis: Long) + fun playUrls(urls: List) } @Composable @@ -30,14 +31,15 @@ interface SinglePlayerState : DefaultLifecycleObserver { fun rememberPlayerState( context: Context = LocalContext.current, coroutineScope: CoroutineScope = rememberCoroutineScope(), + seekToStartOnEnd: Boolean = true, notificationData: NotificationPlayerHelper.NotificationData? = null, - notificationChannelName: String? + notificationChannelName: String? = null ): SinglePlayerState = if (LocalInspectionMode.current) { remember { 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 15b6b99b..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 @@ -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? @@ -35,7 +36,9 @@ class SinglePlayerStateImpl( PlayerUiState( audioState = AudioState.PAUSE, currentMediaItemId = "-1", - audioDuration = 0 + audioDuration = 0, + currentMediaIndex = 0, + endReached = false ) ) @@ -94,6 +97,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) { @@ -189,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() @@ -200,7 +219,9 @@ 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, + endReached = player?.playbackState == Player.STATE_ENDED ) } } @@ -213,8 +234,10 @@ class SinglePlayerStateImpl( } Player.STATE_ENDED -> { - player?.seekTo(0) - player?.pause() + if (seekToStartOnEnd) { + player?.seekTo(0) + player?.pause() + } actualState = AudioState.PAUSE actualState 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..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 @@ -6,5 +6,7 @@ data class PlayerUiState( val audioState: AudioState, val currentMediaItemId: String, val audioDuration: Long, - val isWaitingForAutoplay: Boolean = false + val isWaitingForAutoplay: Boolean = false, + val currentMediaIndex: Int, + val endReached: Boolean ) \ No newline at end of file