14
14
* limitations under the License.
15
15
*/
16
16
17
+ @file:OptIn(ExperimentalFoundationApi ::class )
18
+
17
19
package com.example.jetcaster.ui.home
18
20
19
21
import androidx.compose.foundation.ExperimentalFoundationApi
20
22
import androidx.compose.foundation.Image
21
23
import androidx.compose.foundation.background
24
+ import androidx.compose.foundation.clickable
22
25
import androidx.compose.foundation.layout.Box
23
26
import androidx.compose.foundation.layout.Column
24
27
import androidx.compose.foundation.layout.PaddingValues
25
28
import androidx.compose.foundation.layout.Row
26
29
import androidx.compose.foundation.layout.Spacer
27
30
import androidx.compose.foundation.layout.WindowInsets
28
31
import androidx.compose.foundation.layout.WindowInsetsSides
29
- import androidx.compose.foundation.layout.aspectRatio
30
32
import androidx.compose.foundation.layout.fillMaxSize
31
33
import androidx.compose.foundation.layout.fillMaxWidth
32
34
import androidx.compose.foundation.layout.height
@@ -40,8 +42,10 @@ import androidx.compose.foundation.layout.width
40
42
import androidx.compose.foundation.layout.windowInsetsPadding
41
43
import androidx.compose.foundation.layout.windowInsetsTopHeight
42
44
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
45
49
import androidx.compose.foundation.shape.RoundedCornerShape
46
50
import androidx.compose.material.icons.Icons
47
51
import androidx.compose.material.icons.filled.AccountCircle
@@ -58,12 +62,20 @@ import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
58
62
import androidx.compose.material3.Text
59
63
import androidx.compose.material3.TopAppBar
60
64
import androidx.compose.runtime.Composable
65
+ import androidx.compose.runtime.LaunchedEffect
61
66
import androidx.compose.runtime.getValue
67
+ import androidx.compose.runtime.mutableStateOf
68
+ import androidx.compose.runtime.remember
69
+ import androidx.compose.runtime.rememberCoroutineScope
70
+ import androidx.compose.runtime.setValue
62
71
import androidx.compose.ui.Alignment
63
72
import androidx.compose.ui.Modifier
64
73
import androidx.compose.ui.draw.clip
65
74
import androidx.compose.ui.graphics.Color
66
75
import androidx.compose.ui.layout.ContentScale
76
+ import androidx.compose.ui.layout.onSizeChanged
77
+ import androidx.compose.ui.platform.LocalConfiguration
78
+ import androidx.compose.ui.platform.LocalDensity
67
79
import androidx.compose.ui.res.painterResource
68
80
import androidx.compose.ui.res.stringResource
69
81
import androidx.compose.ui.text.style.TextOverflow
@@ -75,20 +87,21 @@ import coil.compose.AsyncImage
75
87
import com.example.jetcaster.R
76
88
import com.example.jetcaster.core.data.database.model.Category
77
89
import com.example.jetcaster.core.data.database.model.EpisodeToPodcast
90
+ import com.example.jetcaster.core.data.database.model.Podcast
78
91
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
79
92
import com.example.jetcaster.core.data.model.FilterableCategoriesModel
80
93
import com.example.jetcaster.core.data.model.PodcastCategoryFilterResult
81
- import com.example.jetcaster.designsystem.theme.Keyline1
82
94
import com.example.jetcaster.ui.home.discover.discoverItems
83
95
import com.example.jetcaster.ui.home.library.libraryItems
84
96
import com.example.jetcaster.ui.theme.JetcasterTheme
85
97
import com.example.jetcaster.util.ToggleFollowPodcastIconButton
86
98
import com.example.jetcaster.util.quantityStringResource
87
99
import com.example.jetcaster.util.verticalGradientScrim
100
+ import kotlinx.collections.immutable.PersistentList
101
+ import kotlinx.coroutines.launch
88
102
import java.time.Duration
89
103
import java.time.LocalDateTime
90
104
import java.time.OffsetDateTime
91
- import kotlinx.collections.immutable.PersistentList
92
105
93
106
@Composable
94
107
fun Home (
@@ -110,6 +123,7 @@ fun Home(
110
123
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
111
124
navigateToPlayer = navigateToPlayer,
112
125
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
126
+ onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
113
127
modifier = Modifier .fillMaxSize()
114
128
)
115
129
}
@@ -174,7 +188,15 @@ fun Home(
174
188
onCategorySelected : (Category ) -> Unit ,
175
189
navigateToPlayer : (String ) -> Unit ,
176
190
onTogglePodcastFollowed : (String ) -> Unit ,
191
+ onLibraryPodcastSelected : (Podcast ? ) -> Unit
177
192
) {
193
+ // Effect that changes the home category selection when there are no subscribed podcasts
194
+ LaunchedEffect (key1 = featuredPodcasts) {
195
+ if (featuredPodcasts.isEmpty()) {
196
+ onHomeCategorySelected(HomeCategory .Discover )
197
+ }
198
+ }
199
+
178
200
Column (
179
201
modifier = modifier.windowInsetsPadding(
180
202
WindowInsets .systemBars.only(WindowInsetsSides .Horizontal )
@@ -221,7 +243,8 @@ fun Home(
221
243
onHomeCategorySelected = onHomeCategorySelected,
222
244
onCategorySelected = onCategorySelected,
223
245
navigateToPlayer = navigateToPlayer,
224
- onTogglePodcastFollowed = onTogglePodcastFollowed
246
+ onTogglePodcastFollowed = onTogglePodcastFollowed,
247
+ onLibraryPodcastSelected = onLibraryPodcastSelected
225
248
)
226
249
}
227
250
}
@@ -243,11 +266,18 @@ private fun HomeContent(
243
266
onCategorySelected : (Category ) -> Unit ,
244
267
navigateToPlayer : (String ) -> Unit ,
245
268
onTogglePodcastFollowed : (String ) -> Unit ,
269
+ onLibraryPodcastSelected : (Podcast ? ) -> Unit
246
270
) {
271
+ val pagerState = rememberPagerState { featuredPodcasts.size }
272
+ LaunchedEffect (pagerState.currentPage, featuredPodcasts) {
273
+ val podcast = featuredPodcasts.getOrNull(pagerState.currentPage)
274
+ onLibraryPodcastSelected(podcast?.podcast)
275
+ }
247
276
LazyColumn (modifier = modifier.fillMaxSize()) {
248
277
if (featuredPodcasts.isNotEmpty()) {
249
278
item {
250
279
FollowedPodcastItem (
280
+ pagerState = pagerState,
251
281
items = featuredPodcasts,
252
282
onPodcastUnfollowed = onPodcastUnfollowed,
253
283
modifier = Modifier
@@ -265,7 +295,7 @@ private fun HomeContent(
265
295
// TODO show a progress indicator or similar
266
296
}
267
297
268
- if (homeCategories.isNotEmpty()) {
298
+ if (featuredPodcasts.isNotEmpty() && homeCategories.isNotEmpty()) {
269
299
stickyHeader {
270
300
HomeCategoryTabs (
271
301
categories = homeCategories,
@@ -298,6 +328,7 @@ private fun HomeContent(
298
328
299
329
@Composable
300
330
private fun FollowedPodcastItem (
331
+ pagerState : PagerState ,
301
332
items : PersistentList <PodcastWithExtraInfo >,
302
333
onPodcastUnfollowed : (String ) -> Unit ,
303
334
modifier : Modifier = Modifier ,
@@ -306,11 +337,10 @@ private fun FollowedPodcastItem(
306
337
Spacer (Modifier .height(16 .dp))
307
338
308
339
FollowedPodcasts (
340
+ pagerState = pagerState,
309
341
items = items,
310
342
onPodcastUnfollowed = onPodcastUnfollowed,
311
- modifier = Modifier
312
- .fillMaxWidth()
313
- .height(200 .dp)
343
+ modifier = Modifier .fillMaxWidth()
314
344
)
315
345
316
346
Spacer (Modifier .height(16 .dp))
@@ -367,34 +397,51 @@ fun HomeCategoryTabIndicator(
367
397
)
368
398
}
369
399
400
+ private val FEATURED_PODCAST_IMAGE_WIDTH_DP = 160 .dp
401
+ private val FEATURED_PODCAST_IMAGE_HEIGHT_DP = 180 .dp
402
+
403
+ @OptIn(ExperimentalFoundationApi ::class )
370
404
@Composable
371
405
fun FollowedPodcasts (
406
+ pagerState : PagerState ,
372
407
items : PersistentList <PodcastWithExtraInfo >,
373
408
modifier : Modifier = Modifier ,
374
409
onPodcastUnfollowed : (String ) -> Unit ,
375
410
) {
376
- // TODO: Update this component to a carousel once better support is available
377
- val lastIndex = items.size - 1
378
- LazyRow (
379
- modifier = modifier,
411
+ val coroutineScope = rememberCoroutineScope()
412
+
413
+ var horizontalPadding by remember { mutableStateOf(0 .dp) }
414
+ val density = LocalDensity .current
415
+ val screenWidth = LocalConfiguration .current.screenWidthDp.dp
416
+ HorizontalPager (
417
+ state = pagerState,
418
+ modifier = modifier.onSizeChanged {size ->
419
+ // TODO: this is not quite performant since it requires 2 passes to compute the content
420
+ // padding. This should be revisited once a carousel component is available
421
+ horizontalPadding = with (density) {
422
+ (size.width.toDp() - FEATURED_PODCAST_IMAGE_WIDTH_DP ) / 2
423
+ }
424
+ },
380
425
contentPadding = PaddingValues (
381
- start = Keyline1 ,
382
- top = 16 .dp,
383
- end = Keyline1 ,
426
+ horizontal = horizontalPadding,
427
+ vertical = 16 .dp,
428
+ ),
429
+ pageSize = PageSize .Fixed (180 .dp)
430
+ ) { page ->
431
+ val (podcast, lastEpisodeDate) = items[page]
432
+ FollowedPodcastCarouselItem (
433
+ podcastImageUrl = podcast.imageUrl,
434
+ podcastTitle = podcast.title,
435
+ onUnfollowedClick = { onPodcastUnfollowed(podcast.uri) },
436
+ lastEpisodeDateText = lastEpisodeDate?.let { lastUpdated(it) },
437
+ modifier = Modifier
438
+ .fillMaxSize()
439
+ .clickable {
440
+ coroutineScope.launch {
441
+ pagerState.animateScrollToPage(page)
442
+ }
443
+ }
384
444
)
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
- }
398
445
}
399
446
}
400
447
@@ -409,9 +456,9 @@ private fun FollowedPodcastCarouselItem(
409
456
Column (modifier) {
410
457
Box (
411
458
Modifier
412
- .weight(1f )
459
+ .height(FEATURED_PODCAST_IMAGE_HEIGHT_DP )
460
+ .width(FEATURED_PODCAST_IMAGE_WIDTH_DP )
413
461
.align(Alignment .CenterHorizontally )
414
- .aspectRatio(1f )
415
462
) {
416
463
if (podcastImageUrl != null ) {
417
464
AsyncImage (
@@ -484,7 +531,8 @@ fun PreviewHomeContent() {
484
531
onPodcastUnfollowed = {},
485
532
navigateToPlayer = {},
486
533
onHomeCategorySelected = {},
487
- onTogglePodcastFollowed = {}
534
+ onTogglePodcastFollowed = {},
535
+ onLibraryPodcastSelected = {}
488
536
)
489
537
}
490
538
}
0 commit comments