Skip to content

Commit 59462de

Browse files
committed
[Jetcaster]: Handle empty library state.
1 parent ccdb03d commit 59462de

File tree

2 files changed

+82
-33
lines changed

2 files changed

+82
-33
lines changed

Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,21 @@
1414
* limitations under the License.
1515
*/
1616

17+
@file:OptIn(ExperimentalFoundationApi::class)
18+
1719
package com.example.jetcaster.ui.home
1820

1921
import androidx.compose.foundation.ExperimentalFoundationApi
2022
import androidx.compose.foundation.Image
2123
import androidx.compose.foundation.background
24+
import androidx.compose.foundation.clickable
2225
import androidx.compose.foundation.layout.Box
2326
import androidx.compose.foundation.layout.Column
2427
import androidx.compose.foundation.layout.PaddingValues
2528
import androidx.compose.foundation.layout.Row
2629
import androidx.compose.foundation.layout.Spacer
2730
import androidx.compose.foundation.layout.WindowInsets
2831
import androidx.compose.foundation.layout.WindowInsetsSides
29-
import androidx.compose.foundation.layout.aspectRatio
3032
import androidx.compose.foundation.layout.fillMaxSize
3133
import androidx.compose.foundation.layout.fillMaxWidth
3234
import androidx.compose.foundation.layout.height
@@ -40,8 +42,10 @@ import androidx.compose.foundation.layout.width
4042
import androidx.compose.foundation.layout.windowInsetsPadding
4143
import androidx.compose.foundation.layout.windowInsetsTopHeight
4244
import androidx.compose.foundation.lazy.LazyColumn
43-
import androidx.compose.foundation.lazy.LazyRow
44-
import androidx.compose.foundation.lazy.itemsIndexed
45+
import androidx.compose.foundation.pager.HorizontalPager
46+
import androidx.compose.foundation.pager.PageSize
47+
import androidx.compose.foundation.pager.PagerState
48+
import androidx.compose.foundation.pager.rememberPagerState
4549
import androidx.compose.foundation.shape.RoundedCornerShape
4650
import androidx.compose.material.icons.Icons
4751
import androidx.compose.material.icons.filled.AccountCircle
@@ -58,12 +62,15 @@ import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
5862
import androidx.compose.material3.Text
5963
import androidx.compose.material3.TopAppBar
6064
import androidx.compose.runtime.Composable
65+
import androidx.compose.runtime.LaunchedEffect
6166
import androidx.compose.runtime.getValue
67+
import androidx.compose.runtime.rememberCoroutineScope
6268
import androidx.compose.ui.Alignment
6369
import androidx.compose.ui.Modifier
6470
import androidx.compose.ui.draw.clip
6571
import androidx.compose.ui.graphics.Color
6672
import androidx.compose.ui.layout.ContentScale
73+
import androidx.compose.ui.platform.LocalConfiguration
6774
import androidx.compose.ui.res.painterResource
6875
import androidx.compose.ui.res.stringResource
6976
import androidx.compose.ui.text.style.TextOverflow
@@ -75,20 +82,21 @@ import coil.compose.AsyncImage
7582
import com.example.jetcaster.R
7683
import com.example.jetcaster.core.data.database.model.Category
7784
import com.example.jetcaster.core.data.database.model.EpisodeToPodcast
85+
import com.example.jetcaster.core.data.database.model.Podcast
7886
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
7987
import com.example.jetcaster.core.data.model.FilterableCategoriesModel
8088
import com.example.jetcaster.core.data.model.PodcastCategoryFilterResult
81-
import com.example.jetcaster.designsystem.theme.Keyline1
8289
import com.example.jetcaster.ui.home.discover.discoverItems
8390
import com.example.jetcaster.ui.home.library.libraryItems
8491
import com.example.jetcaster.ui.theme.JetcasterTheme
8592
import com.example.jetcaster.util.ToggleFollowPodcastIconButton
8693
import com.example.jetcaster.util.quantityStringResource
8794
import com.example.jetcaster.util.verticalGradientScrim
95+
import kotlinx.collections.immutable.PersistentList
96+
import kotlinx.coroutines.launch
8897
import java.time.Duration
8998
import java.time.LocalDateTime
9099
import java.time.OffsetDateTime
91-
import kotlinx.collections.immutable.PersistentList
92100

93101
@Composable
94102
fun Home(
@@ -110,6 +118,7 @@ fun Home(
110118
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
111119
navigateToPlayer = navigateToPlayer,
112120
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
121+
onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
113122
modifier = Modifier.fillMaxSize()
114123
)
115124
}
@@ -174,7 +183,15 @@ fun Home(
174183
onCategorySelected: (Category) -> Unit,
175184
navigateToPlayer: (String) -> Unit,
176185
onTogglePodcastFollowed: (String) -> Unit,
186+
onLibraryPodcastSelected: (Podcast?) -> Unit
177187
) {
188+
// Effect that changes the home category selection when there are no subscribed podcasts
189+
LaunchedEffect(key1 = featuredPodcasts) {
190+
if (featuredPodcasts.isEmpty()) {
191+
onHomeCategorySelected(HomeCategory.Discover)
192+
}
193+
}
194+
178195
Column(
179196
modifier = modifier.windowInsetsPadding(
180197
WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
@@ -221,7 +238,8 @@ fun Home(
221238
onHomeCategorySelected = onHomeCategorySelected,
222239
onCategorySelected = onCategorySelected,
223240
navigateToPlayer = navigateToPlayer,
224-
onTogglePodcastFollowed = onTogglePodcastFollowed
241+
onTogglePodcastFollowed = onTogglePodcastFollowed,
242+
onLibraryPodcastSelected = onLibraryPodcastSelected
225243
)
226244
}
227245
}
@@ -243,11 +261,18 @@ private fun HomeContent(
243261
onCategorySelected: (Category) -> Unit,
244262
navigateToPlayer: (String) -> Unit,
245263
onTogglePodcastFollowed: (String) -> Unit,
264+
onLibraryPodcastSelected: (Podcast?) -> Unit
246265
) {
266+
val pagerState = rememberPagerState { featuredPodcasts.size }
267+
LaunchedEffect(pagerState.currentPage, featuredPodcasts) {
268+
val podcast = featuredPodcasts.getOrNull(pagerState.currentPage)
269+
onLibraryPodcastSelected(podcast?.podcast)
270+
}
247271
LazyColumn(modifier = modifier.fillMaxSize()) {
248272
if (featuredPodcasts.isNotEmpty()) {
249273
item {
250274
FollowedPodcastItem(
275+
pagerState = pagerState,
251276
items = featuredPodcasts,
252277
onPodcastUnfollowed = onPodcastUnfollowed,
253278
modifier = Modifier
@@ -265,7 +290,7 @@ private fun HomeContent(
265290
// TODO show a progress indicator or similar
266291
}
267292

268-
if (homeCategories.isNotEmpty()) {
293+
if (featuredPodcasts.isNotEmpty() && homeCategories.isNotEmpty()) {
269294
stickyHeader {
270295
HomeCategoryTabs(
271296
categories = homeCategories,
@@ -298,6 +323,7 @@ private fun HomeContent(
298323

299324
@Composable
300325
private fun FollowedPodcastItem(
326+
pagerState: PagerState,
301327
items: PersistentList<PodcastWithExtraInfo>,
302328
onPodcastUnfollowed: (String) -> Unit,
303329
modifier: Modifier = Modifier,
@@ -306,11 +332,10 @@ private fun FollowedPodcastItem(
306332
Spacer(Modifier.height(16.dp))
307333

308334
FollowedPodcasts(
335+
pagerState = pagerState,
309336
items = items,
310337
onPodcastUnfollowed = onPodcastUnfollowed,
311-
modifier = Modifier
312-
.fillMaxWidth()
313-
.height(200.dp)
338+
modifier = Modifier.fillMaxWidth()
314339
)
315340

316341
Spacer(Modifier.height(16.dp))
@@ -367,34 +392,43 @@ fun HomeCategoryTabIndicator(
367392
)
368393
}
369394

395+
private val FEATURED_PODCAST_IMAGE_WIDTH = 160.dp
396+
private val FEATURED_PODCAST_IMAGE_HEIGHT = 180.dp
397+
370398
@Composable
371399
fun FollowedPodcasts(
400+
pagerState: PagerState,
372401
items: PersistentList<PodcastWithExtraInfo>,
373402
modifier: Modifier = Modifier,
374403
onPodcastUnfollowed: (String) -> Unit,
375404
) {
376405
// TODO: Update this component to a carousel once better support is available
377-
val lastIndex = items.size - 1
378-
LazyRow(
406+
val horizontalPadding =
407+
(LocalConfiguration.current.screenWidthDp.dp - FEATURED_PODCAST_IMAGE_WIDTH) / 2
408+
val coroutineScope = rememberCoroutineScope()
409+
HorizontalPager(
410+
state = pagerState,
379411
modifier = modifier,
380412
contentPadding = PaddingValues(
381-
start = Keyline1,
382-
top = 16.dp,
383-
end = Keyline1,
413+
horizontal = horizontalPadding,
414+
vertical = 16.dp,
415+
),
416+
pageSize = PageSize.Fixed(180.dp)
417+
) { page ->
418+
val (podcast, lastEpisodeDate) = items[page]
419+
FollowedPodcastCarouselItem(
420+
podcastImageUrl = podcast.imageUrl,
421+
podcastTitle = podcast.title,
422+
onUnfollowedClick = { onPodcastUnfollowed(podcast.uri) },
423+
lastEpisodeDateText = lastEpisodeDate?.let { lastUpdated(it) },
424+
modifier = Modifier
425+
.fillMaxSize()
426+
.clickable {
427+
coroutineScope.launch {
428+
pagerState.animateScrollToPage(page)
429+
}
430+
}
384431
)
385-
) {
386-
itemsIndexed(items) { index: Int,
387-
(podcast, lastEpisodeDate): PodcastWithExtraInfo ->
388-
FollowedPodcastCarouselItem(
389-
podcastImageUrl = podcast.imageUrl,
390-
podcastTitle = podcast.title,
391-
onUnfollowedClick = { onPodcastUnfollowed(podcast.uri) },
392-
lastEpisodeDateText = lastEpisodeDate?.let { lastUpdated(it) },
393-
modifier = Modifier.padding(4.dp)
394-
)
395-
396-
if (index < lastIndex) Spacer(Modifier.width(24.dp))
397-
}
398432
}
399433
}
400434

@@ -409,9 +443,9 @@ private fun FollowedPodcastCarouselItem(
409443
Column(modifier) {
410444
Box(
411445
Modifier
412-
.weight(1f)
446+
.height(FEATURED_PODCAST_IMAGE_HEIGHT)
447+
.width(FEATURED_PODCAST_IMAGE_WIDTH)
413448
.align(Alignment.CenterHorizontally)
414-
.aspectRatio(1f)
415449
) {
416450
if (podcastImageUrl != null) {
417451
AsyncImage(
@@ -484,7 +518,8 @@ fun PreviewHomeContent() {
484518
onPodcastUnfollowed = {},
485519
navigateToPlayer = {},
486520
onHomeCategorySelected = {},
487-
onTogglePodcastFollowed = {}
521+
onTogglePodcastFollowed = {},
522+
onLibraryPodcastSelected = {}
488523
)
489524
}
490525
}

Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import androidx.lifecycle.ViewModel
2020
import androidx.lifecycle.viewModelScope
2121
import com.example.jetcaster.core.data.database.model.Category
2222
import com.example.jetcaster.core.data.database.model.EpisodeToPodcast
23+
import com.example.jetcaster.core.data.database.model.Podcast
2324
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
2425
import com.example.jetcaster.core.data.di.Graph
2526
import com.example.jetcaster.core.data.domain.FilterableCategoriesUseCase
2627
import com.example.jetcaster.core.data.domain.GetLatestFollowedEpisodesUseCase
2728
import com.example.jetcaster.core.data.domain.PodcastCategoryFilterUseCase
2829
import com.example.jetcaster.core.data.model.FilterableCategoriesModel
2930
import com.example.jetcaster.core.data.model.PodcastCategoryFilterResult
31+
import com.example.jetcaster.core.data.repository.EpisodeStore
3032
import com.example.jetcaster.core.data.repository.PodcastStore
3133
import com.example.jetcaster.core.data.repository.PodcastsRepository
3234
import com.example.jetcaster.util.combine
@@ -44,13 +46,16 @@ import kotlinx.coroutines.launch
4446
class HomeViewModel(
4547
private val podcastsRepository: PodcastsRepository = Graph.podcastRepository,
4648
private val podcastStore: PodcastStore = Graph.podcastStore,
49+
private val episodeStore: EpisodeStore = Graph.episodeStore,
4750
private val getLatestFollowedEpisodesUseCase: GetLatestFollowedEpisodesUseCase =
4851
Graph.getLatestFollowedEpisodesUseCase,
4952
private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase =
5053
Graph.podcastCategoryFilterUseCase,
5154
private val filterableCategoriesUseCase: FilterableCategoriesUseCase =
5255
Graph.filterableCategoriesUseCase
5356
) : ViewModel() {
57+
// Holds our currently selected podcast in the library
58+
private val selectedLibraryPodcast = MutableStateFlow<Podcast?>(null)
5459
// Holds our currently selected home category
5560
private val selectedHomeCategory = MutableStateFlow(HomeCategory.Discover)
5661
// Holds the currently available home categories
@@ -72,15 +77,20 @@ class HomeViewModel(
7277
combine(
7378
homeCategories,
7479
selectedHomeCategory,
75-
podcastStore.followedPodcastsSortedByLastEpisode(limit = 20),
80+
podcastStore.followedPodcastsSortedByLastEpisode(limit = 10),
7681
refreshing,
7782
_selectedCategory.flatMapLatest { selectedCategory ->
7883
filterableCategoriesUseCase(selectedCategory)
7984
},
8085
_selectedCategory.flatMapLatest {
8186
podcastCategoryFilterUseCase(it)
8287
},
83-
getLatestFollowedEpisodesUseCase()
88+
selectedLibraryPodcast.flatMapLatest {
89+
episodeStore.episodesInPodcast(
90+
podcastUri = it?.uri ?: "",
91+
limit = 20
92+
)
93+
}
8494
) { homeCategories,
8595
selectedHomeCategory,
8696
podcasts,
@@ -143,6 +153,10 @@ class HomeViewModel(
143153
podcastStore.togglePodcastFollowed(podcastUri)
144154
}
145155
}
156+
157+
fun onLibraryPodcastSelected(podcast: Podcast?) {
158+
selectedLibraryPodcast.value = podcast
159+
}
146160
}
147161

148162
enum class HomeCategory {

0 commit comments

Comments
 (0)