diff --git a/Jetcaster/README.md b/Jetcaster/README.md
index 364c4165b2..0fbbe55c11 100644
--- a/Jetcaster/README.md
+++ b/Jetcaster/README.md
@@ -3,7 +3,7 @@
# Jetcaster sample 🎙️
Jetcaster is a sample podcast app, built with [Jetpack Compose][compose]. The goal of the sample is to
-showcase dynamic theming and full featured architecture.
+showcase building with Compose across multiple form factors and full featured architecture.
To try out this sample app, use the latest stable version
of [Android Studio](https://developer.android.com/studio).
@@ -14,7 +14,7 @@ project from Android Studio following the steps
### Status: 🚧 In progress 🚧
Jetcaster is still in the early stages of development, and as such only one screen has been created so far. However,
-most of the app's architecture has been implemented, as well as the data layer, and early stages of dynamic theming.
+most of the app's architecture has been implemented as well as the data layer.
## Screenshots
@@ -36,29 +36,6 @@ The player screen layout is adapting to different form factors, including a tabl
-### Dynamic theming
-The home screen currently implements dynamic theming, using the artwork of the currently selected podcast from the carousel to update the `primary` and `onPrimary` [colors](https://developer.android.com/reference/kotlin/androidx/compose/material/Colors). You can see it in action in the screenshots above: as the carousel item is changed, the background gradient is updated to match the artwork.
-
-This is implemented in [`DynamicTheming.kt`](app/src/main/java/com/example/jetcaster/util/DynamicTheming.kt), which provides the `DynamicThemePrimaryColorsFromImage` composable, to automatically animate the theme colors based on the provided image URL, like so:
-
-``` kotlin
-val dominantColorState: DominantColorState = rememberDominantColorState()
-
-DynamicThemePrimaryColorsFromImage(dominantColorState) {
- var imageUrl = remember { mutableStateOf("") }
-
- // When the image url changes, call updateColorsFromImageUrl()
- launchInComposition(imageUrl) {
- dominantColorState.updateColorsFromImageUrl(imageUrl)
- }
-
- // Content which will be dynamically themed....
-}
-```
-
-Underneath, [`DominantColorState`](app/src/main/java/com/example/jetcaster/util/DynamicTheming.kt) uses the [Coil][coil] library to fetch the artwork image 🖼️, and then [Palette][palette] to extract the dominant colors from the image 🎨.
-
-
### Others
Some other notable things which are implemented:
diff --git a/Jetcaster/app/build.gradle.kts b/Jetcaster/app/build.gradle.kts
index 46d0d4c8b4..e4fe8c86e8 100644
--- a/Jetcaster/app/build.gradle.kts
+++ b/Jetcaster/app/build.gradle.kts
@@ -100,9 +100,8 @@ dependencies {
implementation(libs.androidx.constraintlayout.compose)
implementation(libs.androidx.compose.foundation)
- implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.materialWindow)
+ implementation(libs.androidx.compose.material3.window)
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt
index 4a49efdf09..838e9eb71b 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt
+++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt
@@ -16,9 +16,9 @@
package com.example.jetcaster.ui
-import androidx.compose.material.AlertDialog
-import androidx.compose.material.Text
-import androidx.compose.material.TextButton
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/MainActivity.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/MainActivity.kt
index 3c18739094..8218625f3b 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/MainActivity.kt
+++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/MainActivity.kt
@@ -16,10 +16,8 @@
package com.example.jetcaster.ui
-import android.graphics.Color
import android.os.Bundle
import androidx.activity.ComponentActivity
-import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
@@ -32,10 +30,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- enableEdgeToEdge(
- // This app is only ever in dark mode, so hard code detectDarkMode to true.
- SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT, detectDarkMode = { true })
- )
+ enableEdgeToEdge()
setContent {
val windowSizeClass = calculateWindowSizeClass(this)
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 b7db0741ad..d081d5cedc 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
@@ -21,6 +21,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
@@ -35,31 +36,28 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.PagerState
-import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.LocalContentAlpha
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Tab
-import androidx.compose.material.TabPosition
-import androidx.compose.material.TabRow
-import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
-import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabPosition
+import androidx.compose.material3.TabRow
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -78,18 +76,14 @@ import com.example.jetcaster.R
import com.example.jetcaster.core.data.database.model.Category
import com.example.jetcaster.core.data.database.model.EpisodeToPodcast
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
+import com.example.jetcaster.designsystem.theme.Keyline1
import com.example.jetcaster.ui.home.category.PodcastCategoryViewState
import com.example.jetcaster.ui.home.discover.DiscoverViewState
import com.example.jetcaster.ui.home.discover.discoverItems
import com.example.jetcaster.ui.home.library.libraryItems
import com.example.jetcaster.ui.theme.JetcasterTheme
-import com.example.jetcaster.ui.theme.Keyline1
-import com.example.jetcaster.ui.theme.MinContrastOfPrimaryVsSurface
-import com.example.jetcaster.util.DynamicThemePrimaryColorsFromImage
import com.example.jetcaster.util.ToggleFollowPodcastIconButton
-import com.example.jetcaster.util.contrastAgainst
import com.example.jetcaster.util.quantityStringResource
-import com.example.jetcaster.util.rememberDominantColorState
import com.example.jetcaster.util.verticalGradientScrim
import java.time.Duration
import java.time.LocalDateTime
@@ -121,6 +115,7 @@ fun Home(
}
}
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeAppBar(
backgroundColor: Color,
@@ -142,28 +137,25 @@ fun HomeAppBar(
)
}
},
- backgroundColor = backgroundColor,
actions = {
- CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
- IconButton(
- onClick = { /* TODO: Open search */ }
- ) {
- Icon(
- imageVector = Icons.Filled.Search,
- contentDescription = stringResource(R.string.cd_search)
- )
- }
- IconButton(
- onClick = { /* TODO: Open account? */ }
- ) {
- Icon(
- imageVector = Icons.Default.AccountCircle,
- contentDescription = stringResource(R.string.cd_account)
- )
- }
+ IconButton(
+ onClick = { /* TODO: Open search */ }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.Search,
+ contentDescription = stringResource(R.string.cd_search)
+ )
+ }
+ IconButton(
+ onClick = { /* TODO: Open account? */ }
+ ) {
+ Icon(
+ imageVector = Icons.Default.AccountCircle,
+ contentDescription = stringResource(R.string.cd_account)
+ )
}
},
- modifier = modifier
+ modifier = modifier.background(backgroundColor)
)
}
@@ -192,67 +184,46 @@ fun Home(
// We dynamically theme this sub-section of the layout to match the selected
// 'top podcast'
- val surfaceColor = MaterialTheme.colors.surface
+ val surfaceColor = MaterialTheme.colorScheme.surface
val appBarColor = surfaceColor.copy(alpha = 0.87f)
- val dominantColorState = rememberDominantColorState { color ->
- // We want a color which has sufficient contrast against the surface color
- color.contrastAgainst(surfaceColor) >= MinContrastOfPrimaryVsSurface
- }
- DynamicThemePrimaryColorsFromImage(dominantColorState) {
- val pagerState = rememberPagerState { featuredPodcasts.size }
+ val scrimColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
- val selectedImageUrl = featuredPodcasts.getOrNull(pagerState.currentPage)
- ?.podcast?.imageUrl
-
- // When the selected image url changes, call updateColorsFromImageUrl() or reset()
- LaunchedEffect(selectedImageUrl) {
- if (selectedImageUrl != null) {
- dominantColorState.updateColorsFromImageUrl(selectedImageUrl)
- } else {
- dominantColorState.reset()
- }
- }
-
- val scrimColor = MaterialTheme.colors.primary.copy(alpha = 0.38f)
-
- // Top Bar
- Column(
- modifier = Modifier
+ // Top Bar
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(color = scrimColor)
+ ) {
+ // Draw a scrim over the status bar which matches the app bar
+ Spacer(
+ Modifier
+ .background(appBarColor)
.fillMaxWidth()
- .background(color = scrimColor)
- ) {
- // Draw a scrim over the status bar which matches the app bar
- Spacer(
- Modifier
- .background(appBarColor)
- .fillMaxWidth()
- .windowInsetsTopHeight(WindowInsets.statusBars)
- )
- HomeAppBar(
- backgroundColor = appBarColor,
- modifier = Modifier.fillMaxWidth()
- )
- }
-
- // Main Content
- HomeContent(
- featuredPodcasts = featuredPodcasts,
- isRefreshing = isRefreshing,
- selectedHomeCategory = selectedHomeCategory,
- homeCategories = homeCategories,
- discoverViewState = discoverViewState,
- podcastCategoryViewState = podcastCategoryViewState,
- libraryEpisodes = libraryEpisodes,
- scrimColor = scrimColor,
- pagerState = pagerState,
- onPodcastUnfollowed = onPodcastUnfollowed,
- onHomeCategorySelected = onHomeCategorySelected,
- onCategorySelected = onCategorySelected,
- navigateToPlayer = navigateToPlayer,
- onTogglePodcastFollowed = onTogglePodcastFollowed
+ .windowInsetsTopHeight(WindowInsets.statusBars)
+ )
+ HomeAppBar(
+ backgroundColor = appBarColor,
+ modifier = Modifier.fillMaxWidth()
)
}
+
+ // Main Content
+ HomeContent(
+ featuredPodcasts = featuredPodcasts,
+ isRefreshing = isRefreshing,
+ selectedHomeCategory = selectedHomeCategory,
+ homeCategories = homeCategories,
+ discoverViewState = discoverViewState,
+ podcastCategoryViewState = podcastCategoryViewState,
+ libraryEpisodes = libraryEpisodes,
+ scrimColor = scrimColor,
+ onPodcastUnfollowed = onPodcastUnfollowed,
+ onHomeCategorySelected = onHomeCategorySelected,
+ onCategorySelected = onCategorySelected,
+ navigateToPlayer = navigateToPlayer,
+ onTogglePodcastFollowed = onTogglePodcastFollowed
+ )
}
}
@@ -267,7 +238,6 @@ private fun HomeContent(
podcastCategoryViewState: PodcastCategoryViewState,
libraryEpisodes: List,
scrimColor: Color,
- pagerState: PagerState,
modifier: Modifier = Modifier,
onPodcastUnfollowed: (String) -> Unit,
onHomeCategorySelected: (HomeCategory) -> Unit,
@@ -280,7 +250,6 @@ private fun HomeContent(
item {
FollowedPodcastItem(
items = featuredPodcasts,
- pagerState = pagerState,
onPodcastUnfollowed = onPodcastUnfollowed,
modifier = Modifier
.fillMaxWidth()
@@ -328,11 +297,9 @@ private fun HomeContent(
}
}
-@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun FollowedPodcastItem(
items: PersistentList,
- pagerState: PagerState,
onPodcastUnfollowed: (String) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -341,10 +308,8 @@ private fun FollowedPodcastItem(
FollowedPodcasts(
items = items,
- pagerState = pagerState,
onPodcastUnfollowed = onPodcastUnfollowed,
modifier = Modifier
- .padding(start = Keyline1, top = 16.dp, end = Keyline1)
.fillMaxWidth()
.height(200.dp)
)
@@ -382,7 +347,7 @@ private fun HomeCategoryTabs(
HomeCategory.Library -> stringResource(R.string.home_library)
HomeCategory.Discover -> stringResource(R.string.home_discover)
},
- style = MaterialTheme.typography.body2
+ style = MaterialTheme.typography.bodyMedium
)
}
)
@@ -393,7 +358,7 @@ private fun HomeCategoryTabs(
@Composable
fun HomeCategoryTabIndicator(
modifier: Modifier = Modifier,
- color: Color = MaterialTheme.colors.onSurface
+ color: Color = MaterialTheme.colorScheme.onSurface
) {
Spacer(
modifier
@@ -403,28 +368,34 @@ fun HomeCategoryTabIndicator(
)
}
-@OptIn(ExperimentalFoundationApi::class)
@Composable
fun FollowedPodcasts(
items: PersistentList,
- pagerState: PagerState,
modifier: Modifier = Modifier,
onPodcastUnfollowed: (String) -> Unit,
) {
- HorizontalPager(
- state = pagerState,
- modifier = modifier
- ) { page ->
- val (podcast, lastEpisodeDate) = items[page]
- FollowedPodcastCarouselItem(
- podcastImageUrl = podcast.imageUrl,
- podcastTitle = podcast.title,
- onUnfollowedClick = { onPodcastUnfollowed(podcast.uri) },
- lastEpisodeDateText = lastEpisodeDate?.let { lastUpdated(it) },
- modifier = Modifier
- .padding(4.dp)
- .fillMaxSize()
+ // TODO: Update this component to a carousel once better support is available
+ val lastIndex = items.size - 1
+ LazyRow(
+ modifier = modifier,
+ contentPadding = PaddingValues(
+ start = Keyline1,
+ top = 16.dp,
+ end = Keyline1,
)
+ ) {
+ itemsIndexed(items) { index: Int,
+ (podcast, lastEpisodeDate): PodcastWithExtraInfo ->
+ FollowedPodcastCarouselItem(
+ podcastImageUrl = podcast.imageUrl,
+ podcastTitle = podcast.title,
+ onUnfollowedClick = { onPodcastUnfollowed(podcast.uri) },
+ lastEpisodeDateText = lastEpisodeDate?.let { lastUpdated(it) },
+ modifier = Modifier.padding(4.dp)
+ )
+
+ if (index < lastIndex) Spacer(Modifier.width(24.dp))
+ }
}
}
@@ -436,9 +407,7 @@ private fun FollowedPodcastCarouselItem(
lastEpisodeDateText: String? = null,
onUnfollowedClick: () -> Unit,
) {
- Column(
- modifier.padding(horizontal = 12.dp, vertical = 8.dp)
- ) {
+ Column(modifier) {
Box(
Modifier
.weight(1f)
@@ -464,17 +433,15 @@ private fun FollowedPodcastCarouselItem(
}
if (lastEpisodeDateText != null) {
- CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
- Text(
- text = lastEpisodeDateText,
- style = MaterialTheme.typography.caption,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier
- .padding(top = 8.dp)
- .align(Alignment.CenterHorizontally)
- )
- }
+ Text(
+ text = lastEpisodeDateText,
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .align(Alignment.CenterHorizontally)
+ )
}
}
}
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 321d72e3f8..4deab17146 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
@@ -33,21 +33,17 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.Divider
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.LocalContentAlpha
-import androidx.compose.material.LocalContentColor
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
import androidx.compose.material.icons.filled.MoreVert
-import androidx.compose.material.icons.filled.PlaylistAdd
import androidx.compose.material.icons.rounded.PlayCircleFilled
import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -72,10 +68,10 @@ import com.example.jetcaster.core.data.database.model.Episode
import com.example.jetcaster.core.data.database.model.EpisodeToPodcast
import com.example.jetcaster.core.data.database.model.Podcast
import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo
+import com.example.jetcaster.designsystem.theme.Keyline1
import com.example.jetcaster.ui.home.PreviewEpisodes
import com.example.jetcaster.ui.home.PreviewPodcasts
import com.example.jetcaster.ui.theme.JetcasterTheme
-import com.example.jetcaster.ui.theme.Keyline1
import com.example.jetcaster.util.ToggleFollowPodcastIconButton
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@@ -84,6 +80,7 @@ data class PodcastCategoryViewState(
val topPodcasts: List = emptyList(),
val episodes: List = emptyList()
)
+
fun LazyListScope.podcastCategory(
topPodcasts: List,
episodes: List,
@@ -121,7 +118,8 @@ fun EpisodeListItem(
episode: Episode,
podcast: Podcast,
onClick: (String) -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ showDivider: Boolean = true,
) {
ConstraintLayout(modifier = modifier.clickable { onClick(episode.uri) }) {
val (
@@ -129,14 +127,15 @@ fun EpisodeListItem(
date, addPlaylist, overflow
) = createRefs()
- Divider(
- Modifier.constrainAs(divider) {
- top.linkTo(parent.top)
- centerHorizontallyTo(parent)
-
- width = fillToConstraints
- }
- )
+ if (showDivider) {
+ HorizontalDivider(
+ Modifier.constrainAs(divider) {
+ top.linkTo(parent.top)
+ centerHorizontallyTo(parent)
+ width = fillToConstraints
+ }
+ )
+ }
// If we have an image Url, we can show it using Coil
AsyncImage(
@@ -159,7 +158,7 @@ fun EpisodeListItem(
text = episode.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.subtitle1,
+ style = MaterialTheme.typography.titleMedium,
modifier = Modifier.constrainAs(episodeTitle) {
linkTo(
start = parent.start,
@@ -176,32 +175,30 @@ fun EpisodeListItem(
val titleImageBarrier = createBottomBarrier(podcastTitle, image)
- CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
- Text(
- text = podcast.title,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.subtitle2,
- modifier = Modifier.constrainAs(podcastTitle) {
- linkTo(
- start = parent.start,
- end = image.start,
- startMargin = Keyline1,
- endMargin = 16.dp,
- bias = 0f
- )
- top.linkTo(episodeTitle.bottom, 6.dp)
- height = preferredWrapContent
- width = preferredWrapContent
- }
- )
- }
+ Text(
+ text = podcast.title,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleSmall,
+ modifier = Modifier.constrainAs(podcastTitle) {
+ linkTo(
+ start = parent.start,
+ end = image.start,
+ startMargin = Keyline1,
+ endMargin = 16.dp,
+ bias = 0f
+ )
+ top.linkTo(episodeTitle.bottom, 6.dp)
+ height = preferredWrapContent
+ width = preferredWrapContent
+ }
+ )
Image(
imageVector = Icons.Rounded.PlayCircleFilled,
contentDescription = stringResource(R.string.cd_play),
contentScale = ContentScale.Fit,
- colorFilter = ColorFilter.tint(LocalContentColor.current),
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
@@ -217,63 +214,63 @@ fun EpisodeListItem(
}
)
- CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
- val duration = episode.duration
- Text(
- text = when {
- duration != null -> {
- // If we have the duration, we combine the date/duration via a
- // formatted string
- stringResource(
- R.string.episode_date_duration,
- MediumDateFormatter.format(episode.published),
- duration.toMinutes().toInt()
- )
- }
- // Otherwise we just use the date
- else -> MediumDateFormatter.format(episode.published)
- },
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.caption,
- modifier = Modifier.constrainAs(date) {
- centerVerticallyTo(playIcon)
- linkTo(
- start = playIcon.end,
- startMargin = 12.dp,
- end = addPlaylist.start,
- endMargin = 16.dp,
- bias = 0f // float this towards the start
+ val duration = episode.duration
+ Text(
+ text = when {
+ duration != null -> {
+ // If we have the duration, we combine the date/duration via a
+ // formatted string
+ stringResource(
+ R.string.episode_date_duration,
+ MediumDateFormatter.format(episode.published),
+ duration.toMinutes().toInt()
)
- width = preferredWrapContent
}
- )
-
- IconButton(
- onClick = { /* TODO */ },
- modifier = Modifier.constrainAs(addPlaylist) {
- end.linkTo(overflow.start)
- centerVerticallyTo(playIcon)
- }
- ) {
- Icon(
- imageVector = Icons.Default.PlaylistAdd,
- contentDescription = stringResource(R.string.cd_add)
+ // Otherwise we just use the date
+ else -> MediumDateFormatter.format(episode.published)
+ },
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.constrainAs(date) {
+ centerVerticallyTo(playIcon)
+ linkTo(
+ start = playIcon.end,
+ startMargin = 12.dp,
+ end = addPlaylist.start,
+ endMargin = 16.dp,
+ bias = 0f // float this towards the start
)
+ width = preferredWrapContent
}
+ )
- IconButton(
- onClick = { /* TODO */ },
- modifier = Modifier.constrainAs(overflow) {
- end.linkTo(parent.end, 8.dp)
- centerVerticallyTo(playIcon)
- }
- ) {
- Icon(
- imageVector = Icons.Default.MoreVert,
- contentDescription = stringResource(R.string.cd_more)
- )
+ IconButton(
+ onClick = { /* TODO */ },
+ modifier = Modifier.constrainAs(addPlaylist) {
+ end.linkTo(overflow.start)
+ centerVerticallyTo(playIcon)
}
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
+ contentDescription = stringResource(R.string.cd_add),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ IconButton(
+ onClick = { /* TODO */ },
+ modifier = Modifier.constrainAs(overflow) {
+ end.linkTo(parent.end, 8.dp)
+ centerVerticallyTo(playIcon)
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Default.MoreVert,
+ contentDescription = stringResource(R.string.cd_more),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
}
}
}
@@ -344,7 +341,7 @@ private fun TopPodcastRowItem(
Text(
text = podcastTitle,
- style = MaterialTheme.typography.body2,
+ style = MaterialTheme.typography.bodyMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt
index 817d620b70..d8665309b1 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt
+++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt
@@ -16,24 +16,31 @@
package com.example.jetcaster.ui.home.discover
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.ScrollableTabRow
-import androidx.compose.material.Surface
-import androidx.compose.material.Tab
-import androidx.compose.material.TabPosition
-import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabPosition
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import com.example.jetcaster.R
import com.example.jetcaster.core.data.database.model.Category
+import com.example.jetcaster.designsystem.theme.Keyline1
import com.example.jetcaster.ui.home.category.PodcastCategoryViewState
import com.example.jetcaster.ui.home.category.podcastCategory
-import com.example.jetcaster.ui.theme.Keyline1
data class DiscoverViewState(
val categories: List = emptyList(),
@@ -113,20 +120,37 @@ private fun ChoiceChipContent(
) {
Surface(
color = when {
- selected -> MaterialTheme.colors.primary.copy(alpha = 0.08f)
- else -> MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
+ selected -> MaterialTheme.colorScheme.secondaryContainer
+ else -> MaterialTheme.colorScheme.surfaceContainer
},
contentColor = when {
- selected -> MaterialTheme.colors.primary
- else -> MaterialTheme.colors.onSurface
+ selected -> MaterialTheme.colorScheme.onSecondaryContainer
+ else -> MaterialTheme.colorScheme.onSurfaceVariant
},
- shape = MaterialTheme.shapes.small,
+ shape = MaterialTheme.shapes.medium,
modifier = modifier
) {
- Text(
- text = text,
- style = MaterialTheme.typography.body2,
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
- )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(
+ horizontal = when {
+ selected -> 8.dp
+ else -> 16.dp
+ },
+ vertical = 8.dp
+ )
+ ) {
+ if (selected) {
+ Icon(
+ imageVector = Icons.Default.Check,
+ contentDescription = stringResource(id = R.string.cd_selected_category),
+ modifier = Modifier.height(18.dp).padding(end = 8.dp)
+ )
+ }
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
}
}
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt
index 490ea28c9b..4d505051bb 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt
+++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/library/Library.kt
@@ -16,10 +16,17 @@
package com.example.jetcaster.ui.home.library
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.example.jetcaster.R
import com.example.jetcaster.core.data.database.model.EpisodeToPodcast
+import com.example.jetcaster.designsystem.theme.Keyline1
import com.example.jetcaster.ui.home.category.EpisodeListItem
fun LazyListScope.libraryItems(
@@ -31,12 +38,27 @@ fun LazyListScope.libraryItems(
return
}
- items(episodes, key = { it.episode.uri }) { item ->
+ item {
+ Text(
+ text = stringResource(id = R.string.latest_episodes),
+ modifier = Modifier.padding(
+ start = Keyline1,
+ top = 16.dp,
+ ),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+
+ itemsIndexed(
+ episodes,
+ key = { _, item -> item.episode.uri }
+ ) { index, item ->
EpisodeListItem(
episode = item.episode,
podcast = item.podcast,
onClick = navigateToPlayer,
- modifier = Modifier.fillParentMaxWidth()
+ modifier = Modifier.fillParentMaxWidth(),
+ showDivider = index != 0
)
}
}
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt
index f5a3ae922b..27d73dd8bd 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt
+++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt
@@ -41,31 +41,27 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.LocalContentAlpha
-import androidx.compose.material.LocalContentColor
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Slider
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
import androidx.compose.material.icons.filled.Forward30
import androidx.compose.material.icons.filled.MoreVert
-import androidx.compose.material.icons.filled.PlaylistAdd
import androidx.compose.material.icons.filled.Replay10
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material.icons.rounded.PlayCircleFilled
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -89,13 +85,9 @@ import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.example.jetcaster.R
import com.example.jetcaster.ui.theme.JetcasterTheme
-import com.example.jetcaster.ui.theme.MinContrastOfPrimaryVsSurface
-import com.example.jetcaster.util.DynamicThemePrimaryColorsFromImage
-import com.example.jetcaster.util.contrastAgainst
import com.example.jetcaster.util.isBookPosture
import com.example.jetcaster.util.isSeparatingPosture
import com.example.jetcaster.util.isTableTopPosture
-import com.example.jetcaster.util.rememberDominantColorState
import com.example.jetcaster.util.verticalGradientScrim
import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy
import com.google.accompanist.adaptive.TwoPane
@@ -144,67 +136,65 @@ fun PlayerContent(
onBackPress: () -> Unit,
modifier: Modifier = Modifier
) {
- PlayerDynamicTheme(uiState.podcastImageUrl) {
- val foldingFeature = displayFeatures.filterIsInstance().firstOrNull()
+ val foldingFeature = displayFeatures.filterIsInstance().firstOrNull()
- // Use a two pane layout if there is a fold impacting layout (meaning it is separating
- // or non-flat) or if we have a large enough width to show both.
- if (
- windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded ||
- isBookPosture(foldingFeature) ||
+ // Use a two pane layout if there is a fold impacting layout (meaning it is separating
+ // or non-flat) or if we have a large enough width to show both.
+ if (
+ windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded ||
+ isBookPosture(foldingFeature) ||
+ isTableTopPosture(foldingFeature) ||
+ isSeparatingPosture(foldingFeature)
+ ) {
+ // Determine if we are going to be using a vertical strategy (as if laying out
+ // both sides in a column). We want to do so if we are in a tabletop posture,
+ // or we have an impactful horizontal fold. Otherwise, we'll use a horizontal strategy.
+ val usingVerticalStrategy =
isTableTopPosture(foldingFeature) ||
- isSeparatingPosture(foldingFeature)
- ) {
- // Determine if we are going to be using a vertical strategy (as if laying out
- // both sides in a column). We want to do so if we are in a tabletop posture,
- // or we have an impactful horizontal fold. Otherwise, we'll use a horizontal strategy.
- val usingVerticalStrategy =
- isTableTopPosture(foldingFeature) ||
- (
- isSeparatingPosture(foldingFeature) &&
- foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
- )
+ (
+ isSeparatingPosture(foldingFeature) &&
+ foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
+ )
- if (usingVerticalStrategy) {
+ if (usingVerticalStrategy) {
+ TwoPane(
+ first = {
+ PlayerContentTableTopTop(uiState = uiState)
+ },
+ second = {
+ PlayerContentTableTopBottom(uiState = uiState, onBackPress = onBackPress)
+ },
+ strategy = VerticalTwoPaneStrategy(splitFraction = 0.5f),
+ displayFeatures = displayFeatures,
+ modifier = modifier,
+ )
+ } else {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalGradientScrim(
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f),
+ startYPercentage = 1f,
+ endYPercentage = 0f
+ )
+ .systemBarsPadding()
+ .padding(horizontal = 8.dp)
+ ) {
+ TopAppBar(onBackPress = onBackPress)
TwoPane(
first = {
- PlayerContentTableTopTop(uiState = uiState)
+ PlayerContentBookStart(uiState = uiState)
},
second = {
- PlayerContentTableTopBottom(uiState = uiState, onBackPress = onBackPress)
+ PlayerContentBookEnd(uiState = uiState)
},
- strategy = VerticalTwoPaneStrategy(splitFraction = 0.5f),
- displayFeatures = displayFeatures,
- modifier = modifier,
+ strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f),
+ displayFeatures = displayFeatures
)
- } else {
- Column(
- modifier = modifier
- .fillMaxSize()
- .verticalGradientScrim(
- color = MaterialTheme.colors.primary.copy(alpha = 0.50f),
- startYPercentage = 1f,
- endYPercentage = 0f
- )
- .systemBarsPadding()
- .padding(horizontal = 8.dp)
- ) {
- TopAppBar(onBackPress = onBackPress)
- TwoPane(
- first = {
- PlayerContentBookStart(uiState = uiState)
- },
- second = {
- PlayerContentBookEnd(uiState = uiState)
- },
- strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f),
- displayFeatures = displayFeatures
- )
- }
}
- } else {
- PlayerContentRegular(uiState, onBackPress, modifier)
}
+ } else {
+ PlayerContentRegular(uiState, onBackPress, modifier)
}
}
@@ -221,7 +211,7 @@ private fun PlayerContentRegular(
modifier = modifier
.fillMaxSize()
.verticalGradientScrim(
- color = MaterialTheme.colors.primary.copy(alpha = 0.50f),
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f),
startYPercentage = 1f,
endYPercentage = 0f
)
@@ -266,7 +256,7 @@ private fun PlayerContentTableTopTop(
modifier = modifier
.fillMaxWidth()
.verticalGradientScrim(
- color = MaterialTheme.colors.primary.copy(alpha = 0.50f),
+ color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f),
startYPercentage = 1f,
endYPercentage = 0f
)
@@ -306,7 +296,7 @@ private fun PlayerContentTableTopBottom(
PodcastDescription(
title = uiState.title,
podcastName = uiState.podcastName,
- titleTextStyle = MaterialTheme.typography.h6
+ titleTextStyle = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.weight(0.5f))
Column(
@@ -379,14 +369,14 @@ private fun TopAppBar(onBackPress: () -> Unit) {
Row(Modifier.fillMaxWidth()) {
IconButton(onClick = onBackPress) {
Icon(
- imageVector = Icons.Default.ArrowBack,
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.cd_back)
)
}
Spacer(Modifier.weight(1f))
IconButton(onClick = { /* TODO */ }) {
Icon(
- imageVector = Icons.Default.PlaylistAdd,
+ imageVector = Icons.AutoMirrored.Filled.PlaylistAdd,
contentDescription = stringResource(R.string.cd_add)
)
}
@@ -423,7 +413,7 @@ private fun PlayerImage(
private fun PodcastDescription(
title: String,
podcastName: String,
- titleTextStyle: TextStyle = MaterialTheme.typography.h5
+ titleTextStyle: TextStyle = MaterialTheme.typography.headlineSmall
) {
Text(
text = title,
@@ -431,13 +421,11 @@ private fun PodcastDescription(
maxLines = 1,
modifier = Modifier.basicMarquee()
)
- CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
- Text(
- text = podcastName,
- style = MaterialTheme.typography.body2,
- maxLines = 1
- )
- }
+ Text(
+ text = podcastName,
+ style = MaterialTheme.typography.bodyMedium,
+ maxLines = 1
+ )
}
@Composable
@@ -445,8 +433,8 @@ private fun PodcastInformation(
title: String,
name: String,
summary: String,
- titleTextStyle: TextStyle = MaterialTheme.typography.h5,
- nameTextStyle: TextStyle = MaterialTheme.typography.h3,
+ titleTextStyle: TextStyle = MaterialTheme.typography.headlineSmall,
+ nameTextStyle: TextStyle = MaterialTheme.typography.displaySmall,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -466,12 +454,10 @@ private fun PodcastInformation(
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(32.dp))
- CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
- Text(
- text = summary,
- style = MaterialTheme.typography.body2,
- )
- }
+ Text(
+ text = summary,
+ style = MaterialTheme.typography.bodyMedium,
+ )
Spacer(modifier = Modifier.weight(1f))
}
}
@@ -545,34 +531,6 @@ private fun PlayerButtons(
}
}
-/**
- * Theme that updates the colors dynamically depending on the podcast image URL
- */
-@Composable
-private fun PlayerDynamicTheme(
- podcastImageUrl: String,
- content: @Composable () -> Unit
-) {
- val surfaceColor = MaterialTheme.colors.surface
- val dominantColorState = rememberDominantColorState(
- defaultColor = MaterialTheme.colors.surface
- ) { color ->
- // We want a color which has sufficient contrast against the surface color
- color.contrastAgainst(surfaceColor) >= MinContrastOfPrimaryVsSurface
- }
- DynamicThemePrimaryColorsFromImage(dominantColorState) {
- // Update the dominantColorState with colors coming from the podcast image URL
- LaunchedEffect(podcastImageUrl) {
- if (podcastImageUrl.isNotEmpty()) {
- dominantColorState.updateColorsFromImageUrl(podcastImageUrl)
- } else {
- dominantColorState.reset()
- }
- }
- content()
- }
-}
-
/**
* Full screen circular progress indicator
*/
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Color.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Color.kt
index 03254e8269..5193851599 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Color.kt
+++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Color.kt
@@ -16,37 +16,9 @@
package com.example.jetcaster.ui.theme
-import androidx.compose.material.Colors
-import androidx.compose.material.darkColors
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.compositeOver
-
/**
* This is the minimum amount of calculated contrast for a color to be used on top of the
* surface color. These values are defined within the WCAG AA guidelines, and we use a value of
* 3:1 which is the minimum for user-interface components.
*/
const val MinContrastOfPrimaryVsSurface = 3f
-
-/**
- * Return the fully opaque color that results from compositing [onSurface] atop [surface] with the
- * given [alpha]. Useful for situations where semi-transparent colors are undesirable.
- */
-@Composable
-fun Colors.compositedOnSurface(alpha: Float): Color {
- return onSurface.copy(alpha = alpha).compositeOver(surface)
-}
-
-val Yellow800 = Color(0xFFF29F05)
-val Red300 = Color(0xFFEA6D7E)
-
-val JetcasterColors = darkColors(
- primary = Yellow800,
- onPrimary = Color.Black,
- primaryVariant = Yellow800,
- secondary = Yellow800,
- onSecondary = Color.Black,
- error = Red300,
- onError = Color.Black
-)
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Theme.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Theme.kt
index a6c728a5eb..d22c5c1f63 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Theme.kt
+++ b/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Theme.kt
@@ -19,7 +19,7 @@ package com.example.jetcaster.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material.MaterialTheme
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
@@ -32,6 +32,8 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
+import com.example.jetcaster.designsystem.theme.JetcasterShapes
+import com.example.jetcaster.designsystem.theme.JetcasterTypography
import com.example.jetcaster.designsystem.theme.backgroundDark
import com.example.jetcaster.designsystem.theme.backgroundDarkHighContrast
import com.example.jetcaster.designsystem.theme.backgroundDarkMediumContrast
@@ -243,18 +245,6 @@ import com.example.jetcaster.designsystem.theme.tertiaryLight
import com.example.jetcaster.designsystem.theme.tertiaryLightHighContrast
import com.example.jetcaster.designsystem.theme.tertiaryLightMediumContrast
-@Composable
-fun JetcasterTheme(
- content: @Composable () -> Unit
-) {
- MaterialTheme(
- colors = JetcasterColors,
- typography = JetcasterTypography,
- shapes = JetcasterShapes,
- content = content
- )
-}
-
private val lightScheme = lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
@@ -496,10 +486,10 @@ val unspecified_scheme = ColorFamily(
)
@Composable
-fun JetcasterThemeM3(
+fun JetcasterTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
- dynamicColor: Boolean = true,
+ dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
@@ -512,17 +502,19 @@ fun JetcasterThemeM3(
else -> lightScheme
}
val view = LocalView.current
+ val statusBarColor = colorScheme.surface
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
- window.statusBarColor = colorScheme.primary.toArgb()
- WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ window.statusBarColor = statusBarColor.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
- androidx.compose.material3.MaterialTheme(
+ MaterialTheme(
colorScheme = colorScheme,
- typography = JetcasterTypographyM3,
+ shapes = JetcasterShapes,
+ typography = JetcasterTypography,
content = content
)
}
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/util/Buttons.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/util/Buttons.kt
index 2fe99a0c1a..c90ffc9d82 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/util/Buttons.kt
+++ b/Jetcaster/app/src/main/java/com/example/jetcaster/util/Buttons.kt
@@ -20,18 +20,16 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.LocalContentColor
-import androidx.compose.material.MaterialTheme
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
@@ -63,8 +61,8 @@ fun ToggleFollowPodcastIconButton(
},
tint = animateColorAsState(
when {
- isFollowed -> LocalContentColor.current
- else -> Color.Black.copy(alpha = ContentAlpha.high)
+ isFollowed -> MaterialTheme.colorScheme.onPrimary
+ else -> MaterialTheme.colorScheme.primary
}
).value,
modifier = Modifier
@@ -75,11 +73,11 @@ fun ToggleFollowPodcastIconButton(
.background(
color = animateColorAsState(
when {
- isFollowed -> MaterialTheme.colors.surface.copy(0.38f)
- else -> Color.White
+ isFollowed -> MaterialTheme.colorScheme.primary
+ else -> MaterialTheme.colorScheme.surfaceContainerHighest
}
).value,
- shape = MaterialTheme.shapes.small
+ shape = CircleShape
)
.padding(4.dp)
)
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/util/DynamicTheming.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/util/DynamicTheming.kt
deleted file mode 100644
index 4cead93b60..0000000000
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/util/DynamicTheming.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright 2020 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.util
-
-import android.content.Context
-import androidx.collection.LruCache
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.spring
-import androidx.compose.material.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.core.graphics.drawable.toBitmap
-import androidx.palette.graphics.Palette
-import coil.imageLoader
-import coil.request.ImageRequest
-import coil.request.SuccessResult
-import coil.size.Scale
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-@Composable
-fun rememberDominantColorState(
- context: Context = LocalContext.current,
- defaultColor: Color = MaterialTheme.colors.primary,
- defaultOnColor: Color = MaterialTheme.colors.onPrimary,
- cacheSize: Int = 12,
- isColorValid: (Color) -> Boolean = { true }
-): DominantColorState = remember {
- DominantColorState(context, defaultColor, defaultOnColor, cacheSize, isColorValid)
-}
-
-/**
- * A composable which allows dynamic theming of the [androidx.compose.material.Colors.primary]
- * color from an image.
- */
-@Composable
-fun DynamicThemePrimaryColorsFromImage(
- dominantColorState: DominantColorState = rememberDominantColorState(),
- content: @Composable () -> Unit
-) {
- val colors = MaterialTheme.colors.copy(
- primary = animateColorAsState(
- dominantColorState.color,
- spring(stiffness = Spring.StiffnessLow)
- ).value,
- onPrimary = animateColorAsState(
- dominantColorState.onColor,
- spring(stiffness = Spring.StiffnessLow)
- ).value
- )
- MaterialTheme(colors = colors, content = content)
-}
-
-/**
- * A class which stores and caches the result of any calculated dominant colors
- * from images.
- *
- * @param context Android context
- * @param defaultColor The default color, which will be used if [calculateDominantColor] fails to
- * calculate a dominant color
- * @param defaultOnColor The default foreground 'on color' for [defaultColor].
- * @param cacheSize The size of the [LruCache] used to store recent results. Pass `0` to
- * disable the cache.
- * @param isColorValid A lambda which allows filtering of the calculated image colors.
- */
-@Stable
-class DominantColorState(
- private val context: Context,
- private val defaultColor: Color,
- private val defaultOnColor: Color,
- cacheSize: Int = 12,
- private val isColorValid: (Color) -> Boolean = { true }
-) {
- var color by mutableStateOf(defaultColor)
- private set
- var onColor by mutableStateOf(defaultOnColor)
- private set
-
- private val cache = when {
- cacheSize > 0 -> LruCache(cacheSize)
- else -> null
- }
-
- suspend fun updateColorsFromImageUrl(url: String) {
- val result = calculateDominantColor(url)
- color = result?.color ?: defaultColor
- onColor = result?.onColor ?: defaultOnColor
- }
-
- private suspend fun calculateDominantColor(url: String): DominantColors? {
- val cached = cache?.get(url)
- if (cached != null) {
- // If we already have the result cached, return early now...
- return cached
- }
-
- // Otherwise we calculate the swatches in the image, and return the first valid color
- return calculateSwatchesInImage(context, url)
- // First we want to sort the list by the color's population
- .sortedByDescending { swatch -> swatch.population }
- // Then we want to find the first valid color
- .firstOrNull { swatch -> isColorValid(Color(swatch.rgb)) }
- // If we found a valid swatch, wrap it in a [DominantColors]
- ?.let { swatch ->
- DominantColors(
- color = Color(swatch.rgb),
- onColor = Color(swatch.bodyTextColor).copy(alpha = 1f)
- )
- }
- // Cache the resulting [DominantColors]
- ?.also { result -> cache?.put(url, result) }
- }
-
- /**
- * Reset the color values to [defaultColor].
- */
- fun reset() {
- color = defaultColor
- onColor = defaultColor
- }
-}
-
-@Immutable
-private data class DominantColors(val color: Color, val onColor: Color)
-
-/**
- * Fetches the given [imageUrl] with Coil, then uses [Palette] to calculate the dominant color.
- */
-private suspend fun calculateSwatchesInImage(
- context: Context,
- imageUrl: String
-): List {
- val request = ImageRequest.Builder(context)
- .data(imageUrl)
- // We scale the image to cover 128px x 128px (i.e. min dimension == 128px)
- .size(128).scale(Scale.FILL)
- // Disable hardware bitmaps, since Palette uses Bitmap.getPixels()
- .allowHardware(false)
- // Set a custom memory cache key to avoid overwriting the displayed image in the cache
- .memoryCacheKey("$imageUrl.palette")
- .build()
-
- val bitmap = when (val result = context.imageLoader.execute(request)) {
- is SuccessResult -> result.drawable.toBitmap()
- else -> null
- }
-
- return bitmap?.let {
- withContext(Dispatchers.Default) {
- val palette = Palette.Builder(bitmap)
- // Disable any bitmap resizing in Palette. We've already loaded an appropriately
- // sized bitmap through Coil
- .resizeBitmapArea(0)
- // Clear any built-in filters. We want the unfiltered dominant color
- .clearFilters()
- // We reduce the maximum color count down to 8
- .maximumColorCount(8)
- .generate()
-
- palette.swatches
- }
- } ?: emptyList()
-}
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/util/GradientScrim.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/util/GradientScrim.kt
index 5c6a996361..fb2b8250df 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/util/GradientScrim.kt
+++ b/Jetcaster/app/src/main/java/com/example/jetcaster/util/GradientScrim.kt
@@ -17,7 +17,6 @@
package com.example.jetcaster.util
import androidx.annotation.FloatRange
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -113,7 +112,6 @@ private data class VerticalGradientElement(
}
}
-@OptIn(ExperimentalComposeUiApi::class)
private class VerticalGradientModifier(
var onDraw: DrawScope.() -> Unit
) : Modifier.Node(), DrawModifierNode {
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/util/Pager.kt b/Jetcaster/app/src/main/java/com/example/jetcaster/util/Pager.kt
deleted file mode 100644
index 1556286ea6..0000000000
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/util/Pager.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright 2020 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.util
-
-/**
- * Pager is now a library! https://google.github.io/accompanist/pager/
- */
diff --git a/Jetcaster/app/src/main/res/values/strings.xml b/Jetcaster/app/src/main/res/values/strings.xml
index c2cd845503..08bb67ca9c 100644
--- a/Jetcaster/app/src/main/res/values/strings.xml
+++ b/Jetcaster/app/src/main/res/values/strings.xml
@@ -54,5 +54,6 @@
Follow
Following
Not following
+ Selected category
diff --git a/Jetcaster/designsystem/build.gradle.kts b/Jetcaster/designsystem/build.gradle.kts
index 566a565b39..ce86e815dc 100644
--- a/Jetcaster/designsystem/build.gradle.kts
+++ b/Jetcaster/designsystem/build.gradle.kts
@@ -30,6 +30,8 @@ android {
dependencies {
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
+ implementation(libs.androidx.compose.foundation)
+ implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.text)
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Keylines.kt b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Keylines.kt
similarity index 93%
rename from Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Keylines.kt
rename to Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Keylines.kt
index 5242395c1c..e097575da7 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Keylines.kt
+++ b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Keylines.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.example.jetcaster.ui.theme
+package com.example.jetcaster.designsystem.theme
import androidx.compose.ui.unit.dp
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Shape.kt b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt
similarity index 90%
rename from Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Shape.kt
rename to Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt
index 0e7b2e1148..78e51c6dd6 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Shape.kt
+++ b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package com.example.jetcaster.ui.theme
+package com.example.jetcaster.designsystem.theme
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Shapes
+import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
val JetcasterShapes = Shapes(
diff --git a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Type.kt b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt
similarity index 57%
rename from Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Type.kt
rename to Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt
index 0fcabdd969..b9d6eb171e 100644
--- a/Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Type.kt
+++ b/Jetcaster/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt
@@ -14,105 +14,13 @@
* limitations under the License.
*/
-package com.example.jetcaster.ui.theme
+package com.example.jetcaster.designsystem.theme
-import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
-import com.example.jetcaster.designsystem.theme.Montserrat
-val JetcasterTypography = Typography(
- h1 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 96.sp,
- fontWeight = FontWeight.Light,
- lineHeight = 117.sp,
- letterSpacing = (-1.5).sp
- ),
- h2 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 60.sp,
- fontWeight = FontWeight.Light,
- lineHeight = 73.sp,
- letterSpacing = (-0.5).sp
- ),
- h3 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 48.sp,
- fontWeight = FontWeight.Normal,
- lineHeight = 59.sp
- ),
- h4 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 30.sp,
- fontWeight = FontWeight.SemiBold,
- lineHeight = 37.sp
- ),
- h5 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 24.sp,
- fontWeight = FontWeight.SemiBold,
- lineHeight = 29.sp
- ),
- h6 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 20.sp,
- fontWeight = FontWeight.SemiBold,
- lineHeight = 24.sp
- ),
- subtitle1 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 16.sp,
- fontWeight = FontWeight.SemiBold,
- lineHeight = 20.sp,
- letterSpacing = 0.5.sp
- ),
- subtitle2 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 14.sp,
- fontWeight = FontWeight.Medium,
- lineHeight = 17.sp,
- letterSpacing = 0.1.sp
- ),
- body1 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 16.sp,
- fontWeight = FontWeight.Medium,
- lineHeight = 20.sp,
- letterSpacing = 0.15.sp
- ),
- body2 = TextStyle(
- fontFamily = Montserrat,
- fontSize = 14.sp,
- fontWeight = FontWeight.SemiBold,
- lineHeight = 20.sp,
- letterSpacing = 0.25.sp
- ),
- button = TextStyle(
- fontFamily = Montserrat,
- fontSize = 14.sp,
- fontWeight = FontWeight.SemiBold,
- lineHeight = 16.sp,
- letterSpacing = 1.25.sp
- ),
- caption = TextStyle(
- fontFamily = Montserrat,
- fontSize = 12.sp,
- fontWeight = FontWeight.SemiBold,
- lineHeight = 16.sp,
- letterSpacing = 0.sp
- ),
- overline = TextStyle(
- fontFamily = Montserrat,
- fontSize = 12.sp,
- fontWeight = FontWeight.SemiBold,
- lineHeight = 16.sp,
- letterSpacing = 1.sp
- )
-)
-
-val JetcasterTypographyM3 = androidx.compose.material3.Typography(
+val JetcasterTypography = androidx.compose.material3.Typography(
displayLarge = TextStyle(
fontFamily = Montserrat,
fontSize = 57.sp,
diff --git a/Jetcaster/gradle/libs.versions.toml b/Jetcaster/gradle/libs.versions.toml
index 402d8fdfb2..5a8dd0fea8 100644
--- a/Jetcaster/gradle/libs.versions.toml
+++ b/Jetcaster/gradle/libs.versions.toml
@@ -72,7 +72,7 @@ androidx-compose-foundation-layout = { module = "androidx.compose.foundation:fou
androidx-compose-material = { module = "androidx.compose.material:material" }
androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
-androidx-compose-materialWindow = { module = "androidx.compose.material3:material3-window-size-class" }
+androidx-compose-material3-window = { module = "androidx.compose.material3:material3-window-size-class" }
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }