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,15 @@ 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.rememberCoroutineScope
62
68
import androidx.compose.ui.Alignment
63
69
import androidx.compose.ui.Modifier
64
70
import androidx.compose.ui.draw.clip
65
71
import androidx.compose.ui.graphics.Color
66
72
import androidx.compose.ui.layout.ContentScale
73
+ import androidx.compose.ui.platform.LocalConfiguration
67
74
import androidx.compose.ui.res.painterResource
68
75
import androidx.compose.ui.res.stringResource
69
76
import androidx.compose.ui.text.style.TextOverflow
@@ -75,20 +82,21 @@ import coil.compose.AsyncImage
75
82
import com.example.jetcaster.R
76
83
import com.example.jetcaster.core.data.database.model.Category
77
84
import com.example.jetcaster.core.data.database.model.EpisodeToPodcast
85
+ import com.example.jetcaster.core.data.database.model.Podcast
78
86
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
79
87
import com.example.jetcaster.core.data.model.FilterableCategoriesModel
80
88
import com.example.jetcaster.core.data.model.PodcastCategoryFilterResult
81
- import com.example.jetcaster.designsystem.theme.Keyline1
82
89
import com.example.jetcaster.ui.home.discover.discoverItems
83
90
import com.example.jetcaster.ui.home.library.libraryItems
84
91
import com.example.jetcaster.ui.theme.JetcasterTheme
85
92
import com.example.jetcaster.util.ToggleFollowPodcastIconButton
86
93
import com.example.jetcaster.util.quantityStringResource
87
94
import com.example.jetcaster.util.verticalGradientScrim
95
+ import kotlinx.collections.immutable.PersistentList
96
+ import kotlinx.coroutines.launch
88
97
import java.time.Duration
89
98
import java.time.LocalDateTime
90
99
import java.time.OffsetDateTime
91
- import kotlinx.collections.immutable.PersistentList
92
100
93
101
@Composable
94
102
fun Home (
@@ -110,6 +118,7 @@ fun Home(
110
118
onPodcastUnfollowed = viewModel::onPodcastUnfollowed,
111
119
navigateToPlayer = navigateToPlayer,
112
120
onTogglePodcastFollowed = viewModel::onTogglePodcastFollowed,
121
+ onLibraryPodcastSelected = viewModel::onLibraryPodcastSelected,
113
122
modifier = Modifier .fillMaxSize()
114
123
)
115
124
}
@@ -174,7 +183,15 @@ fun Home(
174
183
onCategorySelected : (Category ) -> Unit ,
175
184
navigateToPlayer : (String ) -> Unit ,
176
185
onTogglePodcastFollowed : (String ) -> Unit ,
186
+ onLibraryPodcastSelected : (Podcast ? ) -> Unit
177
187
) {
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
+
178
195
Column (
179
196
modifier = modifier.windowInsetsPadding(
180
197
WindowInsets .systemBars.only(WindowInsetsSides .Horizontal )
@@ -221,7 +238,8 @@ fun Home(
221
238
onHomeCategorySelected = onHomeCategorySelected,
222
239
onCategorySelected = onCategorySelected,
223
240
navigateToPlayer = navigateToPlayer,
224
- onTogglePodcastFollowed = onTogglePodcastFollowed
241
+ onTogglePodcastFollowed = onTogglePodcastFollowed,
242
+ onLibraryPodcastSelected = onLibraryPodcastSelected
225
243
)
226
244
}
227
245
}
@@ -243,11 +261,18 @@ private fun HomeContent(
243
261
onCategorySelected : (Category ) -> Unit ,
244
262
navigateToPlayer : (String ) -> Unit ,
245
263
onTogglePodcastFollowed : (String ) -> Unit ,
264
+ onLibraryPodcastSelected : (Podcast ? ) -> Unit
246
265
) {
266
+ val pagerState = rememberPagerState { featuredPodcasts.size }
267
+ LaunchedEffect (pagerState.currentPage, featuredPodcasts) {
268
+ val podcast = featuredPodcasts.getOrNull(pagerState.currentPage)
269
+ onLibraryPodcastSelected(podcast?.podcast)
270
+ }
247
271
LazyColumn (modifier = modifier.fillMaxSize()) {
248
272
if (featuredPodcasts.isNotEmpty()) {
249
273
item {
250
274
FollowedPodcastItem (
275
+ pagerState = pagerState,
251
276
items = featuredPodcasts,
252
277
onPodcastUnfollowed = onPodcastUnfollowed,
253
278
modifier = Modifier
@@ -265,7 +290,7 @@ private fun HomeContent(
265
290
// TODO show a progress indicator or similar
266
291
}
267
292
268
- if (homeCategories.isNotEmpty()) {
293
+ if (featuredPodcasts.isNotEmpty() && homeCategories.isNotEmpty()) {
269
294
stickyHeader {
270
295
HomeCategoryTabs (
271
296
categories = homeCategories,
@@ -298,6 +323,7 @@ private fun HomeContent(
298
323
299
324
@Composable
300
325
private fun FollowedPodcastItem (
326
+ pagerState : PagerState ,
301
327
items : PersistentList <PodcastWithExtraInfo >,
302
328
onPodcastUnfollowed : (String ) -> Unit ,
303
329
modifier : Modifier = Modifier ,
@@ -306,11 +332,10 @@ private fun FollowedPodcastItem(
306
332
Spacer (Modifier .height(16 .dp))
307
333
308
334
FollowedPodcasts (
335
+ pagerState = pagerState,
309
336
items = items,
310
337
onPodcastUnfollowed = onPodcastUnfollowed,
311
- modifier = Modifier
312
- .fillMaxWidth()
313
- .height(200 .dp)
338
+ modifier = Modifier .fillMaxWidth()
314
339
)
315
340
316
341
Spacer (Modifier .height(16 .dp))
@@ -367,34 +392,43 @@ fun HomeCategoryTabIndicator(
367
392
)
368
393
}
369
394
395
+ private val FEATURED_PODCAST_IMAGE_WIDTH = 160 .dp
396
+ private val FEATURED_PODCAST_IMAGE_HEIGHT = 180 .dp
397
+
370
398
@Composable
371
399
fun FollowedPodcasts (
400
+ pagerState : PagerState ,
372
401
items : PersistentList <PodcastWithExtraInfo >,
373
402
modifier : Modifier = Modifier ,
374
403
onPodcastUnfollowed : (String ) -> Unit ,
375
404
) {
376
405
// 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,
379
411
modifier = modifier,
380
412
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
+ }
384
431
)
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
432
}
399
433
}
400
434
@@ -409,9 +443,9 @@ private fun FollowedPodcastCarouselItem(
409
443
Column (modifier) {
410
444
Box (
411
445
Modifier
412
- .weight(1f )
446
+ .height(FEATURED_PODCAST_IMAGE_HEIGHT )
447
+ .width(FEATURED_PODCAST_IMAGE_WIDTH )
413
448
.align(Alignment .CenterHorizontally )
414
- .aspectRatio(1f )
415
449
) {
416
450
if (podcastImageUrl != null ) {
417
451
AsyncImage (
@@ -484,7 +518,8 @@ fun PreviewHomeContent() {
484
518
onPodcastUnfollowed = {},
485
519
navigateToPlayer = {},
486
520
onHomeCategorySelected = {},
487
- onTogglePodcastFollowed = {}
521
+ onTogglePodcastFollowed = {},
522
+ onLibraryPodcastSelected = {}
488
523
)
489
524
}
490
525
}
0 commit comments