diff --git a/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/TalkingKotlinEpisodeScreen.kt b/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/TalkingKotlinEpisodeScreen.kt index 5076c847..c7bc6446 100644 --- a/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/TalkingKotlinEpisodeScreen.kt +++ b/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/TalkingKotlinEpisodeScreen.kt @@ -21,7 +21,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -64,22 +64,23 @@ public fun TalkingKotlinEpisodeScreen( modifier: Modifier = Modifier, ) { val viewModel = viewModel() - LaunchedEffect(id) { + DisposableEffect(Unit) { viewModel.loadTalkingKotlinEpisode(id) + onDispose { + viewModel.reset() + } } val uiState = viewModel.uiState.collectAsStateWithLifecycle().value val context = LocalContext.current TalkingKotlinEpisodeScreen( - title = "", onNavigateUp = onNavigateUp, onSaveButtonClick = { /* TODO */ }, onShareButtonClick = { title, url -> context.openShareSheet(title, url) }, - onPlayPauseButtonClick = { viewModel.togglePlayPause() }, - onOpenLink = { - context.openCustomTab(it) - }, + onPlayPauseButtonClick = viewModel::togglePlayPause, + onOpenLink = context::openCustomTab, + onSaveStartPosition = viewModel::saveStartPosition, uiState = uiState, modifier = modifier, ) @@ -87,12 +88,12 @@ public fun TalkingKotlinEpisodeScreen( @Composable internal fun TalkingKotlinEpisodeScreen( - title: String, onNavigateUp: () -> Unit, onShareButtonClick: (title: String, url: String) -> Unit, onSaveButtonClick: () -> Unit, onPlayPauseButtonClick: () -> Unit, onOpenLink: (url: String) -> Unit, + onSaveStartPosition: (startPositionMillis: Long) -> Unit, uiState: TalkingKotlinEpisodeUiState, modifier: Modifier = Modifier, ) { @@ -102,7 +103,7 @@ internal fun TalkingKotlinEpisodeScreen( .background(KSTheme.colorScheme.background), ) { TopNavBar( - title = title, + title = "", modifier = Modifier.zIndex(1f), contentPadding = WindowInsets.statusBars.asPaddingValues(), navigationIcon = { @@ -153,6 +154,7 @@ internal fun TalkingKotlinEpisodeScreen( isPlaying = uiState.isPlaying, onPlayPauseButtonClick = onPlayPauseButtonClick, onOpenLink = onOpenLink, + onSaveStartPosition = onSaveStartPosition, ) } } @@ -166,6 +168,7 @@ private fun ContentUi( isPlaying: Boolean, onPlayPauseButtonClick: () -> Unit, onOpenLink: (url: String) -> Unit, + onSaveStartPosition: (startPositionMillis: Long) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -271,6 +274,7 @@ private fun ContentUi( episode = episode, isPlaying = isPlaying, onPlayPauseButtonClick = onPlayPauseButtonClick, + onSaveStartPosition = onSaveStartPosition, contentPadding = WindowInsets.navigationBars.asPaddingValues(), ) } diff --git a/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/TalkingKotlinEpisodeViewModel.kt b/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/TalkingKotlinEpisodeViewModel.kt index 17fbb2da..4d3048f0 100644 --- a/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/TalkingKotlinEpisodeViewModel.kt +++ b/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/TalkingKotlinEpisodeViewModel.kt @@ -21,5 +21,11 @@ internal class TalkingKotlinEpisodeViewModel @Inject constructor( presenter.loadTalkingKotlinEpisode(id) } + fun saveStartPosition(startPositionMillis: Long) = viewModelScope.launch { + presenter.saveStartPosition(startPositionMillis) + } + fun togglePlayPause() = presenter.togglePlayPause() + + fun reset() = presenter.reset() } diff --git a/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/component/PodcastPlayer.kt b/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/component/PodcastPlayer.kt index cc299c69..c46ea66a 100644 --- a/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/component/PodcastPlayer.kt +++ b/android/feature/talking-kotlin-episode/src/main/java/io/github/reactivecircus/kstreamlined/android/feature/talkingkotlinepisode/component/PodcastPlayer.kt @@ -62,6 +62,7 @@ internal fun PodcastPlayer( episode: TalkingKotlinEpisode, isPlaying: Boolean, onPlayPauseButtonClick: () -> Unit, + onSaveStartPosition: (Long) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), ) { @@ -107,9 +108,9 @@ internal fun PodcastPlayer( } DisposableEffect(Unit) { - @Suppress("MagicNumber") - player.seekTo(1200_000) + player.seekTo(episode.startPositionMillis) onDispose { + onSaveStartPosition(playerPositionMillis.toLong()) player.release() } } @@ -257,6 +258,7 @@ private fun PreviewPodcastPlayerUi_paused() { thumbnailUrl = "podcast-logo-url", summary = "summary", duration = "35min.", + startPositionMillis = 0, ), isPlaying = false, onPlayPauseButtonClick = {}, @@ -285,6 +287,7 @@ private fun PreviewPodcastPlayer_playing() { thumbnailUrl = "podcast-logo-url", summary = "summary", duration = "35min.", + startPositionMillis = 0, ), isPlaying = true, onPlayPauseButtonClick = {}, diff --git a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedRepository.kt b/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedRepository.kt index 722a0784..0acf4cee 100644 --- a/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedRepository.kt +++ b/kmp/data/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/data/feed/FeedRepository.kt @@ -35,6 +35,20 @@ public class FeedRepository( TODO() } + @Suppress("MaxLineLength") + private var fakeTalkingKotlinEpisode = FeedItem.TalkingKotlin( + id = "tag:soundcloud,2010:tracks/1689535512", + title = "Coil Goes Multiplatform with Colin White", + publishTime = "2023-11-29T22:00:00Z".toInstant(), + contentUrl = "https://soundcloud.com/user-38099918/coil-goes-multiplatform-with-colin-white", + savedForLater = false, + audioUrl = "https://feeds.soundcloud.com/stream/1689535512-user-38099918-network-resilient-applications-with-store5-talking-kotlin-128.mp3", + thumbnailUrl = "https://talkingkotlin.com/images/kotlin_talking_logo.png", + summary = "Welcome to another engaging episode of Talking Kotlin! In this edition, we dive into the dynamic world of Android development with Colin White, the creator of the widely acclaimed Coil library. Join us as we discuss the latest developments, insights, and the exciting roadmap for Coil. \uD83D\uDE80 Highlights from this Episode: Learn about Colin's journey in developing the Coil library. Discover the pivotal role Coil plays in simplifying image loading for Android developers. Get an exclusive sneak peek into the upcoming Coil 3.0, featuring multi-platform support and seamless integration with Jetpack Compose. \uD83D\uDD17 Helpful Links: Coil Library GitHub: github.com/coilkt/coil Follow Colin White on Twitter: @colinwhi \uD83C\uDF10 Connect with the Kotlin Community: https://kotlinlang.org/community/ Kotlin Foundation: https://kotlinfoundation.org/", + duration = "42min.", + startPositionMillis = 0, + ) + public suspend fun loadFeedItemById(id: String): FeedItem? { // TODO load from DB return when { @@ -48,24 +62,17 @@ public class FeedRepository( issueNumber = 386, ) } - id.contains("tag:soundcloud") -> { - @Suppress("MaxLineLength") - FeedItem.TalkingKotlin( - id = id, - title = "Coil Goes Multiplatform with Colin White", - publishTime = "2023-11-29T22:00:00Z".toInstant(), - contentUrl = "https://soundcloud.com/user-38099918/coil-goes-multiplatform-with-colin-white", - savedForLater = false, - audioUrl = "https://feeds.soundcloud.com/stream/1689535512-user-38099918-network-resilient-applications-with-store5-talking-kotlin-128.mp3", - thumbnailUrl = "https://talkingkotlin.com/images/kotlin_talking_logo.png", - summary = "Welcome to another engaging episode of Talking Kotlin! In this edition, we dive into the dynamic world of Android development with Colin White, the creator of the widely acclaimed Coil library. Join us as we discuss the latest developments, insights, and the exciting roadmap for Coil. \uD83D\uDE80 Highlights from this Episode: Learn about Colin's journey in developing the Coil library. Discover the pivotal role Coil plays in simplifying image loading for Android developers. Get an exclusive sneak peek into the upcoming Coil 3.0, featuring multi-platform support and seamless integration with Jetpack Compose. \uD83D\uDD17 Helpful Links: Coil Library GitHub: github.com/coilkt/coil Follow Colin White on Twitter: @colinwhi \uD83C\uDF10 Connect with the Kotlin Community: https://kotlinlang.org/community/ Kotlin Foundation: https://kotlinfoundation.org/", - duration = "42min.", - ) - } + id.contains("tag:soundcloud") -> fakeTalkingKotlinEpisode else -> null } } + public suspend fun saveTalkingKotlinEpisodeStartPosition(id: String, positionMillis: Long) { + // TODO persist to DB + require(id == fakeTalkingKotlinEpisode.id) + fakeTalkingKotlinEpisode = fakeTalkingKotlinEpisode.copy(startPositionMillis = positionMillis) + } + public suspend fun loadSavedFeedItems(): List { TODO() } diff --git a/kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedItem.kt b/kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedItem.kt index 97ecda9c..ceca9a9d 100644 --- a/kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedItem.kt +++ b/kmp/model/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/model/feed/FeedItem.kt @@ -38,6 +38,7 @@ public sealed interface FeedItem { val thumbnailUrl: String, val summary: String, val duration: String, + val startPositionMillis: Long, ) : FeedItem public data class KotlinWeekly( diff --git a/kmp/presentation/home/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/home/HomeUiState.kt b/kmp/presentation/home/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/home/HomeUiState.kt index 7ea2de21..b3143a88 100644 --- a/kmp/presentation/home/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/home/HomeUiState.kt +++ b/kmp/presentation/home/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/home/HomeUiState.kt @@ -74,6 +74,7 @@ public val FakeHomeFeedItems: List = listOf( thumbnailUrl = "https://i1.sndcdn.com/artworks-uP9Cxy5KSYNzGebf-3q1MsQ-t3000x3000.jpg", summary = "Dive into the intricate world of microservices with the latest episode of Talking Kotlin, hosted by Sebastian and Hadi.", duration = "56min.", + startPositionMillis = 0, ), FeedItem.KotlinBlog( id = "https://blog.jetbrains.com/?post_type=blog&p=404245", @@ -134,5 +135,6 @@ public val FakeHomeFeedItems: List = listOf( thumbnailUrl = "https://i1.sndcdn.com/avatars-000289370353-di6ese-original.jpg", summary = "In this episode, we are talking to engineers from @instaboxglobal who use Compose Multiplatform in Production.", duration = "55min.", + startPositionMillis = 0, ), ).toHomeFeedItems() diff --git a/kmp/presentation/home/src/commonTest/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/home/HomeFeedItemMapperTest.kt b/kmp/presentation/home/src/commonTest/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/home/HomeFeedItemMapperTest.kt index ed62f239..f23a2120 100644 --- a/kmp/presentation/home/src/commonTest/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/home/HomeFeedItemMapperTest.kt +++ b/kmp/presentation/home/src/commonTest/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/home/HomeFeedItemMapperTest.kt @@ -61,6 +61,7 @@ class HomeFeedItemMapperTest { thumbnailUrl = "podcast-logo-url", summary = "summary", duration = "35min.", + startPositionMillis = 0, ), // 5 days ago FeedItem.KotlinBlog( @@ -148,6 +149,7 @@ class HomeFeedItemMapperTest { thumbnailUrl = "podcast-logo-url", summary = "summary", duration = "35min.", + startPositionMillis = 0, ), displayablePublishTime = "Yesterday", ) diff --git a/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisode.kt b/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisode.kt index 525b4398..d6716a93 100644 --- a/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisode.kt +++ b/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisode.kt @@ -10,4 +10,5 @@ public data class TalkingKotlinEpisode( val thumbnailUrl: String, val summary: String, val duration: String, + val startPositionMillis: Long, ) diff --git a/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodeMapper.kt b/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodeMapper.kt index 88fb7d83..bb3a6417 100644 --- a/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodeMapper.kt +++ b/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodeMapper.kt @@ -16,6 +16,7 @@ internal fun FeedItem.TalkingKotlin.asPresentationModel( audioUrl = audioUrl, thumbnailUrl = thumbnailUrl, summary = summary, - duration = duration + duration = duration, + startPositionMillis = startPositionMillis, ) } diff --git a/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodePresenter.kt b/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodePresenter.kt index e1f2b0d9..e11c5a72 100644 --- a/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodePresenter.kt +++ b/kmp/presentation/talking-kotlin-episode/src/commonMain/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodePresenter.kt @@ -24,6 +24,11 @@ public class TalkingKotlinEpisodePresenter( ) } + public suspend fun saveStartPosition(startPositionMillis: Long) { + val id = (uiState.value as TalkingKotlinEpisodeUiState.Content).episode.id + feedRepository.saveTalkingKotlinEpisodeStartPosition(id, startPositionMillis) + } + public fun togglePlayPause() { _uiState.update { if (it is TalkingKotlinEpisodeUiState.Content) { @@ -33,4 +38,9 @@ public class TalkingKotlinEpisodePresenter( } } } + + // TODO remove once ViewModel is scoped properly + public fun reset() { + _uiState.value = TalkingKotlinEpisodeUiState.Initializing + } } diff --git a/kmp/presentation/talking-kotlin-episode/src/commonTest/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodeMapperTest.kt b/kmp/presentation/talking-kotlin-episode/src/commonTest/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodeMapperTest.kt index c8d9fa96..44c6a314 100644 --- a/kmp/presentation/talking-kotlin-episode/src/commonTest/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodeMapperTest.kt +++ b/kmp/presentation/talking-kotlin-episode/src/commonTest/kotlin/io/github/reactivecircus/kstreamlined/kmp/presentation/talkingkotlinepisode/TalkingKotlinEpisodeMapperTest.kt @@ -22,6 +22,7 @@ class TalkingKotlinEpisodeMapperTest { thumbnailUrl = "podcast-logo-url", summary = "summary", duration = "35min.", + startPositionMillis = 3000, ) val expectedTalkingKotlinEpisode = TalkingKotlinEpisode( @@ -34,6 +35,7 @@ class TalkingKotlinEpisodeMapperTest { thumbnailUrl = "podcast-logo-url", summary = "summary", duration = "35min.", + startPositionMillis = 3000, ) assertEquals(expectedTalkingKotlinEpisode, feedItem.asPresentationModel(timeZone))