Skip to content

Commit

Permalink
Migrate Podcast Bottom Header to Jetpack Compose (#3513)
Browse files Browse the repository at this point in the history
  • Loading branch information
mebarbosa authored Jan 31, 2025
1 parent f2fbcd3 commit 2f43ca4
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
Expand All @@ -18,13 +17,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.FragmentManager
import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground
import au.com.shiftyjelly.pocketcasts.compose.buttons.RowOutlinedButton
import au.com.shiftyjelly.pocketcasts.compose.components.TextH30
import au.com.shiftyjelly.pocketcasts.compose.components.TextP40
import au.com.shiftyjelly.pocketcasts.compose.preview.ThemePreviewParameterProvider
import au.com.shiftyjelly.pocketcasts.compose.theme
Expand All @@ -43,6 +43,7 @@ import au.com.shiftyjelly.pocketcasts.localization.R as LR
fun StarRatingView(
fragmentManager: FragmentManager,
viewModel: PodcastRatingsViewModel,
modifier: Modifier = Modifier,
) {
val state by viewModel.stateFlow.collectAsState()

Expand All @@ -51,6 +52,7 @@ fun StarRatingView(
val loadedState = state as RatingState.Loaded
Content(
state = loadedState,
modifier = modifier,
onClick = { source ->
viewModel.onRatingStarsTapped(
podcastUuid = loadedState.podcastUuid,
Expand All @@ -70,26 +72,23 @@ fun StarRatingView(
@Composable
private fun Content(
state: RatingState.Loaded,
modifier: Modifier = Modifier,
onClick: (RatingTappedSource) -> Unit,
) {
val starsContentDescription = stringResource(LR.string.podcast_star_rating_content_description)

Row(
modifier = Modifier.padding(
start = 8.dp,
end = 4.dp,
top = 8.dp,
),
modifier = modifier.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { onClick(RatingTappedSource.STARS) }
.padding(8.dp)
.semantics {
this.contentDescription = starsContentDescription
},
}
.padding(8.dp),
) {
Stars(
stars = state.stars,
Expand All @@ -112,20 +111,16 @@ private fun Content(

Spacer(modifier = Modifier.weight(1f))

RowOutlinedButton(
TextH30(
text = stringResource(R.string.rate_button),
onClick = { onClick(RatingTappedSource.BUTTON) },
includePadding = false,
fontSize = 16.sp,
textPadding = 0.dp,
fontWeight = FontWeight.W500,
border = null,
fullWidth = false,
colors = ButtonDefaults.outlinedButtonColors(
backgroundColor = Color.Transparent,
contentColor = MaterialTheme.theme.colors.primaryText01,
),
modifier = Modifier.padding(top = 2.dp, bottom = 2.dp),
textAlign = TextAlign.Center,
fontSize = 16.sp,
modifier = Modifier
.clickable {
onClick(RatingTappedSource.BUTTON)
}
.padding(8.dp),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.text.Html
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
Expand All @@ -18,11 +17,14 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
Expand All @@ -32,6 +34,7 @@ import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import au.com.shiftyjelly.pocketcasts.analytics.SourceView
import au.com.shiftyjelly.pocketcasts.compose.AppTheme
import au.com.shiftyjelly.pocketcasts.compose.text.toAnnotatedString
import au.com.shiftyjelly.pocketcasts.models.entity.BaseEpisode
import au.com.shiftyjelly.pocketcasts.models.entity.Bookmark
import au.com.shiftyjelly.pocketcasts.models.entity.Podcast
Expand All @@ -43,7 +46,6 @@ import au.com.shiftyjelly.pocketcasts.podcasts.R
import au.com.shiftyjelly.pocketcasts.podcasts.databinding.AdapterEpisodeBinding
import au.com.shiftyjelly.pocketcasts.podcasts.databinding.AdapterEpisodeHeaderBinding
import au.com.shiftyjelly.pocketcasts.podcasts.databinding.AdapterPodcastHeaderBinding
import au.com.shiftyjelly.pocketcasts.podcasts.helper.readMore
import au.com.shiftyjelly.pocketcasts.podcasts.view.components.PlayButton
import au.com.shiftyjelly.pocketcasts.podcasts.view.components.ratings.StarRatingView
import au.com.shiftyjelly.pocketcasts.podcasts.view.podcast.adapter.BookmarkHeaderViewHolder
Expand All @@ -54,7 +56,6 @@ import au.com.shiftyjelly.pocketcasts.podcasts.view.podcast.adapter.TabsViewHold
import au.com.shiftyjelly.pocketcasts.podcasts.viewmodel.PodcastRatingsViewModel
import au.com.shiftyjelly.pocketcasts.podcasts.viewmodel.PodcastViewModel.PodcastTab
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.preferences.model.ArtworkConfiguration
import au.com.shiftyjelly.pocketcasts.preferences.model.ArtworkConfiguration.Element
import au.com.shiftyjelly.pocketcasts.repositories.download.DownloadManager
import au.com.shiftyjelly.pocketcasts.repositories.images.PocketCastsImageRequestFactory
Expand All @@ -70,7 +71,6 @@ import au.com.shiftyjelly.pocketcasts.utils.featureflag.FeatureFlag
import au.com.shiftyjelly.pocketcasts.views.extensions.hide
import au.com.shiftyjelly.pocketcasts.views.extensions.show
import au.com.shiftyjelly.pocketcasts.views.extensions.toggleVisibility
import au.com.shiftyjelly.pocketcasts.views.extensions.trimPadding
import au.com.shiftyjelly.pocketcasts.views.helper.AnimatorUtil
import au.com.shiftyjelly.pocketcasts.views.helper.SwipeButtonLayoutFactory
import au.com.shiftyjelly.pocketcasts.views.helper.toCircle
Expand Down Expand Up @@ -232,7 +232,7 @@ class PodcastAdapter(
VIEW_TYPE_NO_BOOKMARK -> NoBookmarkViewHolder(ComposeView(parent.context), theme, onHeadsetSettingsClicked)
else -> EpisodeViewHolder(
binding = AdapterEpisodeBinding.inflate(inflater, parent, false),
viewMode = if (settings.artworkConfiguration.value.useEpisodeArtwork(ArtworkConfiguration.Element.Podcasts)) {
viewMode = if (settings.artworkConfiguration.value.useEpisodeArtwork(Element.Podcasts)) {
EpisodeViewHolder.ViewMode.Artwork
} else {
EpisodeViewHolder.ViewMode.NoArtwork
Expand All @@ -243,7 +243,7 @@ class PodcastAdapter(
imageRequestFactory = imageRequestFactory.smallSize(),
settings = settings,
swipeButtonLayoutFactory = swipeButtonLayoutFactory,
artworkContext = ArtworkConfiguration.Element.Podcasts,
artworkContext = Element.Podcasts,
)
}
}
Expand Down Expand Up @@ -273,28 +273,6 @@ class PodcastAdapter(
bindHeaderBottom(holder)
bindHeaderTop(holder)

holder.binding.bottom.ratings.setContent {
AppTheme(theme.activeTheme) {
StarRatingView(fragmentManager, ratingsViewModel)
}
}

holder.binding.bottom.podcastInfoPanel.setContent {
AppTheme(theme.activeTheme) {
PodcastInfoView(
PodcastInfoState(
author = podcast.author,
link = podcast.getShortUrl(),
schedule = podcast.displayableFrequency(context.resources),
next = podcast.displayableNextEpisodeDate(context),
),
onWebsiteLinkClicked = {
onWebsiteLinkClicked(context)
},
)
}
}

val imageView = holder.binding.top.artwork
// stopping the artwork flickering when the image is reloaded
if (imageView.drawable == null || holder.lastImagePodcastUuid == null || holder.lastImagePodcastUuid != podcast.uuid) {
Expand All @@ -314,26 +292,45 @@ class PodcastAdapter(

private fun bindHeaderBottom(holder: PodcastViewHolder) {
holder.binding.bottom.root.isVisible = headerExpanded
val tintColor = ThemeColor.podcastText02(theme.activeTheme, tintColor)
holder.binding.bottom.title.text = podcast.title
holder.binding.bottom.title.readMore(3)
with(holder.binding.bottom.category) {
text = podcast.getFirstCategory(context.resources)
}
holder.binding.bottom.description.text =
if (FeatureFlag.isEnabled(Feature.PODCAST_HTML_DESCRIPTION) && podcast.podcastHtmlDescription.isNotEmpty()) {
// keep the extra line break from paragraphs as it looks better
Html.fromHtml(
podcast.podcastHtmlDescription,
Html.FROM_HTML_MODE_COMPACT and
Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH.inv(),
).trimPadding()
} else {
podcast.podcastDescription

val isHtmlDescription = FeatureFlag.isEnabled(Feature.PODCAST_HTML_DESCRIPTION) && podcast.podcastHtmlDescription.isNotEmpty()
val descriptionLinkColor: Int = ThemeColor.podcastText02(theme.activeTheme, tintColor)

val description = if (isHtmlDescription) {
HtmlCompat.fromHtml(
podcast.podcastHtmlDescription,
HtmlCompat.FROM_HTML_MODE_COMPACT and
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH.inv(), // keep the extra line break from paragraphs as it looks better
).toAnnotatedString(urlColor = descriptionLinkColor)
} else {
AnnotatedString(podcast.podcastDescription)
}

holder.binding.bottom.podcastHeaderBottom.setContent {
AppTheme(theme.activeTheme) {
PodcastHeaderBottom(
title = podcast.title,
category = podcast.getFirstCategory(context.resources),
description = description,
podcastInfoContent = {
PodcastInfoView(
PodcastInfoState(
author = podcast.author,
link = podcast.getShortUrl(),
schedule = podcast.displayableFrequency(context.resources),
next = podcast.displayableNextEpisodeDate(context),
),
onWebsiteLinkClicked = {
onWebsiteLinkClicked(context)
},
)
},
ratingsContent = {
StarRatingView(fragmentManager, ratingsViewModel, Modifier.padding(top = 8.dp))
},
onDescriptionClicked = onPodcastDescriptionClicked,
)
}
holder.binding.bottom.description.setLinkTextColor(tintColor)
holder.binding.bottom.description.readMore(3) {
onPodcastDescriptionClicked()
}
}

Expand Down Expand Up @@ -750,9 +747,6 @@ class PodcastAdapter(
binding.top.settings.setOnClickListener {
adapter.onSettingsClicked()
}
binding.bottom.ratings.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,
)
}

private fun unsubscribe() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package au.com.shiftyjelly.pocketcasts.podcasts.view.podcast

import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
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.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import au.com.shiftyjelly.pocketcasts.compose.components.TextH20
import au.com.shiftyjelly.pocketcasts.compose.components.TextP40
import au.com.shiftyjelly.pocketcasts.compose.theme

private const val collapsedLines: Int = 3

@Composable
fun PodcastHeaderBottom(
title: String,
category: String,
description: AnnotatedString,
onDescriptionClicked: () -> Unit,
modifier: Modifier = Modifier,
ratingsContent: @Composable () -> Unit = {},
podcastInfoContent: @Composable () -> Unit = {},
) {
var isTitleExpanded by remember { mutableStateOf(false) }
var isDescriptionExpanded by remember { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }

Column(
modifier = modifier
.fillMaxWidth()
.clipToBounds()
.padding(bottom = 8.dp)
.padding(horizontal = 16.dp)
.background(MaterialTheme.theme.colors.primaryUi02),
) {
TextH20(
text = title,
maxLines = if (isTitleExpanded) Int.MAX_VALUE else collapsedLines,
overflow = if (isTitleExpanded) TextOverflow.Clip else TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp)
.animateContentSize(
animationSpec = tween(
durationMillis = 200,
easing = FastOutSlowInEasing,
),
)
.clickable(
indication = null,
interactionSource = interactionSource,
onClick = {
isTitleExpanded = !isTitleExpanded
},
),
)

TextP40(
text = category,
fontSize = 15.sp,
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
color = MaterialTheme.theme.colors.primaryText02,
)

ratingsContent()

Divider(
color = MaterialTheme.theme.colors.primaryUi05,
thickness = 1.dp,
)

Text(
text = description,
maxLines = if (isDescriptionExpanded) Int.MAX_VALUE else collapsedLines,
overflow = if (isDescriptionExpanded) TextOverflow.Clip else TextOverflow.Ellipsis,
lineHeight = 21.sp,
fontSize = 16.sp,
color = MaterialTheme.theme.colors.primaryText01,
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp, bottom = 16.dp)
.animateContentSize(
animationSpec = tween(
durationMillis = 200,
easing = FastOutSlowInEasing,
),
)
.clickable(
indication = null,
interactionSource = interactionSource,
onClick = {
isDescriptionExpanded = !isDescriptionExpanded
onDescriptionClicked.invoke()
},
),
)

podcastInfoContent()
}
}
Loading

0 comments on commit 2f43ca4

Please sign in to comment.