diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt index 3ed91cdd64..d4997baa7e 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt @@ -90,7 +90,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices @@ -101,7 +100,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowWidthSizeClass -import coil.compose.AsyncImage import com.example.jetcaster.R import com.example.jetcaster.core.model.CategoryInfo import com.example.jetcaster.core.model.EpisodeInfo @@ -110,6 +108,7 @@ import com.example.jetcaster.core.model.LibraryInfo import com.example.jetcaster.core.model.PlayerEpisode import com.example.jetcaster.core.model.PodcastCategoryFilterResult import com.example.jetcaster.core.model.PodcastInfo +import com.example.jetcaster.designsystem.component.PodcastImage import com.example.jetcaster.ui.home.discover.discoverItems import com.example.jetcaster.ui.home.library.libraryItems import com.example.jetcaster.ui.podcast.PodcastDetailsScreen @@ -791,9 +790,9 @@ private fun FollowedPodcasts( @Composable private fun FollowedPodcastCarouselItem( + podcastTitle: String, + podcastImageUrl: String, modifier: Modifier = Modifier, - podcastImageUrl: String? = null, - podcastTitle: String? = null, lastEpisodeDateText: String? = null, onUnfollowedClick: () -> Unit, ) { @@ -803,16 +802,13 @@ private fun FollowedPodcastCarouselItem( .size(FEATURED_PODCAST_IMAGE_SIZE_DP) .align(Alignment.CenterHorizontally) ) { - if (podcastImageUrl != null) { - AsyncImage( - model = podcastImageUrl, - contentDescription = podcastTitle, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.medium), - ) - } + PodcastImage( + podcastImageUrl = podcastImageUrl, + contentDescription = podcastTitle, + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium), + ) ToggleFollowPodcastIconButton( onClick = onUnfollowedClick, @@ -943,6 +939,8 @@ private fun PreviewPodcastCard() { JetcasterTheme { FollowedPodcastCarouselItem( modifier = Modifier.size(128.dp), + podcastTitle = "", + podcastImageUrl = "", onUnfollowedClick = {} ) } diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt index 29c6e15115..2deb2d444b 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt @@ -38,18 +38,15 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.ImageRequest import com.example.jetcaster.core.model.EpisodeInfo import com.example.jetcaster.core.model.PlayerEpisode import com.example.jetcaster.core.model.PodcastCategoryFilterResult import com.example.jetcaster.core.model.PodcastInfo +import com.example.jetcaster.designsystem.component.PodcastImage import com.example.jetcaster.designsystem.theme.Keyline1 import com.example.jetcaster.ui.home.PreviewEpisodes import com.example.jetcaster.ui.home.PreviewPodcasts @@ -147,9 +144,11 @@ private fun CategoryPodcastRow( podcastImageUrl = podcast.imageUrl, isFollowed = podcast.isSubscribed ?: false, onToggleFollowClicked = { onTogglePodcastFollowed(podcast) }, - modifier = Modifier.width(128.dp).clickable { - navigateToPodcastDetails(podcast) - } + modifier = Modifier + .width(128.dp) + .clickable { + navigateToPodcastDetails(podcast) + } ) if (index < lastIndex) Spacer(Modifier.width(24.dp)) @@ -160,10 +159,10 @@ private fun CategoryPodcastRow( @Composable private fun TopPodcastRowItem( podcastTitle: String, + podcastImageUrl: String, isFollowed: Boolean, modifier: Modifier = Modifier, onToggleFollowClicked: () -> Unit, - podcastImageUrl: String? = null, ) { Column( modifier.semantics(mergeDescendants = true) {} @@ -174,19 +173,13 @@ private fun TopPodcastRowItem( .aspectRatio(1f) .align(Alignment.CenterHorizontally) ) { - if (podcastImageUrl != null) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(podcastImageUrl) - .crossfade(true) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.medium), - ) - } + PodcastImage( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium), + podcastImageUrl = podcastImageUrl, + contentDescription = podcastTitle + ) ToggleFollowPodcastIconButton( onClick = onToggleFollowClicked, diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt index f8db646b71..8cc1200881 100644 --- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt +++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt @@ -54,20 +54,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil.compose.AsyncImage -import coil.request.ImageRequest import com.example.jetcaster.R import com.example.jetcaster.core.model.EpisodeInfo import com.example.jetcaster.core.model.PlayerEpisode import com.example.jetcaster.core.model.PodcastInfo +import com.example.jetcaster.designsystem.component.PodcastImage import com.example.jetcaster.designsystem.theme.Keyline1 import com.example.jetcaster.ui.home.PreviewEpisodes import com.example.jetcaster.ui.home.PreviewPodcasts @@ -203,16 +200,12 @@ fun PodcastDetailsHeaderItem( verticalAlignment = Alignment.Bottom, modifier = Modifier.fillMaxWidth() ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(podcast.imageUrl) - .crossfade(true) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop, + PodcastImage( modifier = Modifier .size(148.dp) - .clip(MaterialTheme.shapes.large) + .clip(MaterialTheme.shapes.large), + podcastImageUrl = podcast.imageUrl, + contentDescription = podcast.title ) Column( modifier = Modifier.padding(start = 16.dp) diff --git a/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt new file mode 100644 index 0000000000..2896f72c01 --- /dev/null +++ b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetcaster.designsystem.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImagePainter +import coil.compose.rememberAsyncImagePainter +import coil.request.ImageRequest + +@Composable +fun PodcastImage( + podcastImageUrl: String, + contentDescription: String?, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Crop, +) { + var imagePainterState by remember { + mutableStateOf(AsyncImagePainter.State.Empty) + } + + val imageLoader = rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(podcastImageUrl) + .crossfade(true) + .build(), + contentScale = contentScale, + onState = { state -> imagePainterState = state } + ) + + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + when (imagePainterState) { + is AsyncImagePainter.State.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .size(48.dp) + .align(Alignment.Center) + ) + } + + else -> { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + ) + } + } + + Image( + painter = imageLoader, + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier, + ) + } +}