diff --git a/Jetcaster/.editorconfig b/Jetcaster/.editorconfig new file mode 100644 index 0000000000..e699cf2e28 --- /dev/null +++ b/Jetcaster/.editorconfig @@ -0,0 +1,26 @@ +# When authoring changes in .editorconfig, run ./gradlew spotlessApply --no-daemon +# Reference: https://github.com/diffplug/spotless/issues/1924 +[*.{kt,kts}] +ktlint_code_style = android_studio +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +max_line_length = 140 # ktlint official +ktlint_function_naming_ignore_when_annotated_with = Composable, Test +ktlint_standard_filename = disabled +ktlint_standard_package-name = disabled +ktlint_standard_property-naming = disabled +ktlint_standard_backing-property-naming = disabled +ktlint_standard_argument-list-wrapping = disabled +ktlint_standard_parameter-list-wrapping = disabled +ktlint_standard_double-colon-spacing = disabled +ktlint_standard_enum-entry-name-case = disabled +ktlint_standard_multiline-if-else = disabled +ktlint_standard_no-empty-first-line-in-method-block = disabled +ktlint_standard_package-name = disabled +ktlint_standard_trailing-comma = disabled +ktlint_standard_spacing-around-angle-brackets = disabled +ktlint_standard_spacing-between-declarations-with-annotations = disabled +ktlint_standard_spacing-between-declarations-with-comments = disabled +ktlint_standard_unary-op-spacing = disabled +ktlint_standard_function-expression-body = disabled +ktlint_standard_value-parameter-comment = disabled diff --git a/Jetcaster/build.gradle.kts b/Jetcaster/build.gradle.kts index bcd5617e23..39178a700d 100644 --- a/Jetcaster/build.gradle.kts +++ b/Jetcaster/build.gradle.kts @@ -32,45 +32,18 @@ apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") subprojects { apply(plugin = "com.diffplug.spotless") configure { - ratchetFrom = "origin/main" kotlin { target("**/*.kt") - targetExclude("**/build/**/*.kt") - ktlint().editorConfigOverride( - mapOf( - "ktlint_code_style" to "android_studio", - "ij_kotlin_allow_trailing_comma" to true, - "ktlint_function_naming_ignore_when_annotated_with" to "Composable", - // These rules were introduced in ktlint 0.46.0 and should not be - // enabled without further discussion. They are disabled for now. - // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 - "disabled_rules" to - "filename," + - "annotation,annotation-spacing," + - "argument-list-wrapping," + - "double-colon-spacing," + - "enum-entry-name-case," + - "multiline-if-else," + - "no-empty-first-line-in-method-block," + - "package-name," + - "trailing-comma," + - "spacing-around-angle-brackets," + - "spacing-between-declarations-with-annotations," + - "spacing-between-declarations-with-comments," + - "unary-op-spacing" - ) - ) + targetExclude("${layout.buildDirectory}/**/*.kt") + ktlint() licenseHeaderFile(rootProject.file("spotless/copyright.kt")) } - format("kts") { - target("**/*.kts") - targetExclude("**/build/**/*.kts") - // Look for the first line that doesn't have a block comment (assumed to be the license) - licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") - } kotlinGradle { target("*.gradle.kts") + targetExclude("${layout.buildDirectory}/**/*.kt") ktlint() + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") } } } diff --git a/Jetcaster/core/data-testing/build.gradle.kts b/Jetcaster/core/data-testing/build.gradle.kts index a8644e1c1b..40cc5d1cf0 100644 --- a/Jetcaster/core/data-testing/build.gradle.kts +++ b/Jetcaster/core/data-testing/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2025 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. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -5,10 +22,16 @@ plugins { android { namespace = "com.example.jetcaster.core.data.testing" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -19,7 +42,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestCategoryStore.kt b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestCategoryStore.kt index 60de97944d..8f8ac64dba 100644 --- a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestCategoryStore.kt +++ b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestCategoryStore.kt @@ -39,20 +39,14 @@ class TestCategoryStore : CategoryStore { private val episodesFromPodcasts = MutableStateFlow>>(emptyMap()) - override fun categoriesSortedByPodcastCount(limit: Int): Flow> = - categoryFlow + override fun categoriesSortedByPodcastCount(limit: Int): Flow> = categoryFlow - override fun podcastsInCategorySortedByPodcastCount( - categoryId: Long, - limit: Int - ): Flow> = podcastsInCategoryFlow.map { - it[categoryId]?.take(limit) ?: emptyList() - } + override fun podcastsInCategorySortedByPodcastCount(categoryId: Long, limit: Int): Flow> = + podcastsInCategoryFlow.map { + it[categoryId]?.take(limit) ?: emptyList() + } - override fun episodesFromPodcastsInCategory( - categoryId: Long, - limit: Int - ): Flow> = episodesFromPodcasts.map { + override fun episodesFromPodcastsInCategory(categoryId: Long, limit: Int): Flow> = episodesFromPodcasts.map { it[categoryId]?.take(limit) ?: emptyList() } diff --git a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestEpisodeStore.kt b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestEpisodeStore.kt index 9dd7c526ea..2afb76aa0f 100644 --- a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestEpisodeStore.kt +++ b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestEpisodeStore.kt @@ -29,52 +29,47 @@ import kotlinx.coroutines.flow.update class TestEpisodeStore : EpisodeStore { private val episodesFlow = MutableStateFlow>(listOf()) - override fun episodeWithUri(episodeUri: String): Flow = - episodesFlow.map { episodes -> - episodes.first { it.uri == episodeUri } + override fun episodeWithUri(episodeUri: String): Flow = episodesFlow.map { episodes -> + episodes.first { it.uri == episodeUri } + } + + override fun episodeAndPodcastWithUri(episodeUri: String): Flow = episodesFlow.map { episodes -> + val e = episodes.first { + it.uri == episodeUri + } + EpisodeToPodcast().apply { + episode = e + _podcasts = emptyList() } + } - override fun episodeAndPodcastWithUri(episodeUri: String): Flow = - episodesFlow.map { episodes -> - val e = episodes.first { - it.uri == episodeUri - } + override fun episodesInPodcast(podcastUri: String, limit: Int): Flow> = episodesFlow.map { episodes -> + episodes.filter { + it.podcastUri == podcastUri + }.map { e -> EpisodeToPodcast().apply { episode = e - _podcasts = emptyList() } } + } - override fun episodesInPodcast(podcastUri: String, limit: Int): Flow> = - episodesFlow.map { episodes -> - episodes.filter { - it.podcastUri == podcastUri - }.map { e -> - EpisodeToPodcast().apply { - episode = e - } + override fun episodesInPodcasts(podcastUris: List, limit: Int): Flow> = episodesFlow.map { episodes -> + episodes.filter { + podcastUris.contains(it.podcastUri) + }.map { ep -> + EpisodeToPodcast().apply { + episode = ep } } + } - override fun episodesInPodcasts( - podcastUris: List, - limit: Int - ): Flow> = - episodesFlow.map { episodes -> - episodes.filter { - podcastUris.contains(it.podcastUri) - }.map { ep -> - EpisodeToPodcast().apply { - episode = ep - } - } - } + override suspend fun addEpisodes(episodes: Collection) = episodesFlow.update { + it + episodes + } - override suspend fun addEpisodes(episodes: Collection) = - episodesFlow.update { - it + episodes - } + override suspend fun deleteEpisode(episode: Episode) = episodesFlow.update { + it - episode + } - override suspend fun isEmpty(): Boolean = - episodesFlow.first().isEmpty() + override suspend fun isEmpty(): Boolean = episodesFlow.first().isEmpty() } diff --git a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestPodcastStore.kt b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestPodcastStore.kt index 8a4808ecfd..24e6916325 100644 --- a/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestPodcastStore.kt +++ b/Jetcaster/core/data-testing/src/main/java/com/example/jetcaster/core/data/testing/repository/TestPodcastStore.kt @@ -31,71 +31,62 @@ class TestPodcastStore : PodcastStore { private val podcastFlow = MutableStateFlow>(listOf()) private val followedPodcasts = mutableSetOf() - override fun podcastWithUri(uri: String): Flow = - podcastFlow.map { podcasts -> - podcasts.first { it.uri == uri } - } + override fun podcastWithUri(uri: String): Flow = podcastFlow.map { podcasts -> + podcasts.first { it.uri == uri } + } - override fun podcastWithExtraInfo(podcastUri: String): Flow = - podcastFlow.map { podcasts -> - val podcast = podcasts.first { it.uri == podcastUri } - PodcastWithExtraInfo().apply { - this.podcast = podcast - } + override fun podcastWithExtraInfo(podcastUri: String): Flow = podcastFlow.map { podcasts -> + val podcast = podcasts.first { it.uri == podcastUri } + PodcastWithExtraInfo().apply { + this.podcast = podcast } + } - override fun podcastsSortedByLastEpisode(limit: Int): Flow> = - podcastFlow.map { podcasts -> - podcasts.map { p -> - PodcastWithExtraInfo().apply { - podcast = p - isFollowed = followedPodcasts.contains(p.uri) - } + override fun podcastsSortedByLastEpisode(limit: Int): Flow> = podcastFlow.map { podcasts -> + podcasts.map { p -> + PodcastWithExtraInfo().apply { + podcast = p + isFollowed = followedPodcasts.contains(p.uri) } } + } - override fun followedPodcastsSortedByLastEpisode(limit: Int): Flow> = - podcastFlow.map { podcasts -> - podcasts.filter { - followedPodcasts.contains(it.uri) - }.map { p -> - PodcastWithExtraInfo().apply { - podcast = p - isFollowed = true - } + override fun followedPodcastsSortedByLastEpisode(limit: Int): Flow> = podcastFlow.map { podcasts -> + podcasts.filter { + followedPodcasts.contains(it.uri) + }.map { p -> + PodcastWithExtraInfo().apply { + podcast = p + isFollowed = true } } + } - override fun searchPodcastByTitle( - keyword: String, - limit: Int - ): Flow> = - podcastFlow.map { podcastList -> - podcastList.filter { - it.title.contains(keyword) - }.map { p -> - PodcastWithExtraInfo().apply { - podcast = p - isFollowed = true - } + override fun searchPodcastByTitle(keyword: String, limit: Int): Flow> = podcastFlow.map { podcastList -> + podcastList.filter { + it.title.contains(keyword) + }.map { p -> + PodcastWithExtraInfo().apply { + podcast = p + isFollowed = true } } + } override fun searchPodcastByTitleAndCategories( keyword: String, categories: List, - limit: Int - ): Flow> = - podcastFlow.map { podcastList -> - podcastList.filter { - it.title.contains(keyword) - }.map { p -> - PodcastWithExtraInfo().apply { - podcast = p - isFollowed = true - } + limit: Int, + ): Flow> = podcastFlow.map { podcastList -> + podcastList.filter { + it.title.contains(keyword) + }.map { p -> + PodcastWithExtraInfo().apply { + podcast = p + isFollowed = true } } + } override suspend fun togglePodcastFollowed(podcastUri: String) { if (podcastUri in followedPodcasts) { @@ -113,9 +104,7 @@ class TestPodcastStore : PodcastStore { followedPodcasts.remove(podcastUri) } - override suspend fun addPodcast(podcast: Podcast) = - podcastFlow.update { it + podcast } + override suspend fun addPodcast(podcast: Podcast) = podcastFlow.update { it + podcast } - override suspend fun isEmpty(): Boolean = - podcastFlow.first().isEmpty() + override suspend fun isEmpty(): Boolean = podcastFlow.first().isEmpty() } diff --git a/Jetcaster/core/data/build.gradle.kts b/Jetcaster/core/data/build.gradle.kts index bd81f036e7..ab7e5cacbb 100644 --- a/Jetcaster/core/data/build.gradle.kts +++ b/Jetcaster/core/data/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2025 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. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -7,10 +24,16 @@ plugins { android { namespace = "com.example.jetcaster.core.data" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/JetcasterDatabase.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/JetcasterDatabase.kt index ced5d408b0..b1804d5276 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/JetcasterDatabase.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/JetcasterDatabase.kt @@ -40,10 +40,10 @@ import com.example.jetcaster.core.data.database.model.PodcastFollowedEntry Episode::class, PodcastCategoryEntry::class, Category::class, - PodcastFollowedEntry::class + PodcastFollowedEntry::class, ], version = 1, - exportSchema = false + exportSchema = false, ) @TypeConverters(DateTimeTypeConverters::class) abstract class JetcasterDatabase : RoomDatabase() { diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/CategoriesDao.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/CategoriesDao.kt index baf958f139..a3b0f7eb3d 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/CategoriesDao.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/CategoriesDao.kt @@ -35,11 +35,9 @@ abstract class CategoriesDao : BaseDao { ) ON category_id = categories.id ORDER BY podcast_count DESC LIMIT :limit - """ + """, ) - abstract fun categoriesSortedByPodcastCount( - limit: Int - ): Flow> + abstract fun categoriesSortedByPodcastCount(limit: Int): Flow> @Query("SELECT * FROM categories WHERE name = :name") abstract suspend fun getCategoryWithName(name: String): Category? diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/EpisodesDao.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/EpisodesDao.kt index e1d60d5f07..c7373840c5 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/EpisodesDao.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/EpisodesDao.kt @@ -32,7 +32,7 @@ abstract class EpisodesDao : BaseDao { @Query( """ SELECT * FROM episodes WHERE uri = :uri - """ + """, ) abstract fun episode(uri: String): Flow @@ -42,7 +42,7 @@ abstract class EpisodesDao : BaseDao { SELECT episodes.* FROM episodes INNER JOIN podcasts ON episodes.podcast_uri = podcasts.uri WHERE episodes.uri = :episodeUri - """ + """, ) abstract fun episodeAndPodcast(episodeUri: String): Flow @@ -52,12 +52,9 @@ abstract class EpisodesDao : BaseDao { SELECT * FROM episodes WHERE podcast_uri = :podcastUri ORDER BY datetime(published) DESC LIMIT :limit - """ + """, ) - abstract fun episodesForPodcastUri( - podcastUri: String, - limit: Int - ): Flow> + abstract fun episodesForPodcastUri(podcastUri: String, limit: Int): Flow> @Transaction @Query( @@ -67,12 +64,9 @@ abstract class EpisodesDao : BaseDao { WHERE category_id = :categoryId ORDER BY datetime(published) DESC LIMIT :limit - """ + """, ) - abstract fun episodesFromPodcastsInCategory( - categoryId: Long, - limit: Int - ): Flow> + abstract fun episodesFromPodcastsInCategory(categoryId: Long, limit: Int): Flow> @Query("SELECT COUNT(*) FROM episodes") abstract suspend fun count(): Int @@ -83,10 +77,7 @@ abstract class EpisodesDao : BaseDao { SELECT * FROM episodes WHERE podcast_uri IN (:podcastUris) ORDER BY datetime(published) DESC LIMIT :limit - """ + """, ) - abstract fun episodesForPodcasts( - podcastUris: List, - limit: Int - ): Flow> + abstract fun episodesForPodcasts(podcastUris: List, limit: Int): Flow> } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastsDao.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastsDao.kt index 4d5ce71755..b010938e0a 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastsDao.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/dao/PodcastsDao.kt @@ -44,7 +44,7 @@ abstract class PodcastsDao : BaseDao { LEFT JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = podcasts.uri WHERE podcasts.uri = :podcastUri ORDER BY datetime(last_episode_date) DESC - """ + """, ) abstract fun podcastWithExtraInfo(podcastUri: String): Flow @@ -61,11 +61,9 @@ abstract class PodcastsDao : BaseDao { LEFT JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = episodes.podcast_uri ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) - abstract fun podcastsSortedByLastEpisode( - limit: Int - ): Flow> + abstract fun podcastsSortedByLastEpisode(limit: Int): Flow> @Transaction @Query( @@ -82,12 +80,9 @@ abstract class PodcastsDao : BaseDao { LEFT JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = inner_query.podcast_uri ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) - abstract fun podcastsInCategorySortedByLastEpisode( - categoryId: Long, - limit: Int - ): Flow> + abstract fun podcastsInCategorySortedByLastEpisode(categoryId: Long, limit: Int): Flow> @Transaction @Query( @@ -100,11 +95,9 @@ abstract class PodcastsDao : BaseDao { INNER JOIN podcast_followed_entries AS followed_entries ON followed_entries.podcast_uri = episodes.podcast_uri ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) - abstract fun followedPodcastsSortedByLastEpisode( - limit: Int - ): Flow> + abstract fun followedPodcastsSortedByLastEpisode(limit: Int): Flow> @Transaction @Query( @@ -118,7 +111,7 @@ abstract class PodcastsDao : BaseDao { WHERE podcasts.title LIKE '%' || :keyword || '%' ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) abstract fun searchPodcastByTitle(keyword: String, limit: Int): Flow> @@ -138,13 +131,9 @@ abstract class PodcastsDao : BaseDao { WHERE podcasts.title LIKE '%' || :keyword || '%' ORDER BY datetime(last_episode_date) DESC LIMIT :limit - """ + """, ) - abstract fun searchPodcastByTitleAndCategory( - keyword: String, - categoryIdList: List, - limit: Int - ): Flow> + abstract fun searchPodcastByTitleAndCategory(keyword: String, categoryIdList: List, limit: Int): Flow> @Query("SELECT COUNT(*) FROM podcasts") abstract suspend fun count(): Int diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Category.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Category.kt index 4dff2871ef..9391e859f8 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Category.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Category.kt @@ -25,11 +25,11 @@ import androidx.room.PrimaryKey @Entity( tableName = "categories", indices = [ - Index("name", unique = true) - ] + Index("name", unique = true), + ], ) @Immutable data class Category( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, - @ColumnInfo(name = "name") val name: String + @ColumnInfo(name = "name") val name: String, ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Episode.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Episode.kt index 6a035d9646..e159e5bd20 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Episode.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Episode.kt @@ -16,12 +16,13 @@ package com.example.jetcaster.core.data.database.model -import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey +import androidx.room.TypeConverter +import androidx.room.TypeConverters import java.time.Duration import java.time.OffsetDateTime @@ -29,7 +30,7 @@ import java.time.OffsetDateTime tableName = "episodes", indices = [ Index("uri", unique = true), - Index("podcast_uri") + Index("podcast_uri"), ], foreignKeys = [ ForeignKey( @@ -37,11 +38,11 @@ import java.time.OffsetDateTime parentColumns = ["uri"], childColumns = ["podcast_uri"], onUpdate = ForeignKey.CASCADE, - onDelete = ForeignKey.CASCADE - ) - ] + onDelete = ForeignKey.CASCADE, + ), + ], ) -@Immutable +@TypeConverters(ListOfStringConverter::class) data class Episode( @PrimaryKey @ColumnInfo(name = "uri") val uri: String, @ColumnInfo(name = "podcast_uri") val podcastUri: String, @@ -50,5 +51,18 @@ data class Episode( @ColumnInfo(name = "summary") val summary: String? = null, @ColumnInfo(name = "author") val author: String? = null, @ColumnInfo(name = "published") val published: OffsetDateTime, - @ColumnInfo(name = "duration") val duration: Duration? = null + @ColumnInfo(name = "duration") val duration: Duration? = null, + @ColumnInfo(name = "media_urls") val mediaUrls: List, ) + +class ListOfStringConverter { + @TypeConverter + fun fromString(value: String): List { + return value.split(",") + } + + @TypeConverter + fun fromList(list: List): String { + return list.joinToString(",") + } +} diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Podcast.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Podcast.kt index 1d86f31f91..8e8d28f1b5 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Podcast.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/Podcast.kt @@ -25,8 +25,8 @@ import androidx.room.PrimaryKey @Entity( tableName = "podcasts", indices = [ - Index("uri", unique = true) - ] + Index("uri", unique = true), + ], ) @Immutable data class Podcast( @@ -35,5 +35,5 @@ data class Podcast( @ColumnInfo(name = "description") val description: String? = null, @ColumnInfo(name = "author") val author: String? = null, @ColumnInfo(name = "image_url") val imageUrl: String? = null, - @ColumnInfo(name = "copyright") val copyright: String? = null + @ColumnInfo(name = "copyright") val copyright: String? = null, ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastCategoryEntry.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastCategoryEntry.kt index 3c2c67878d..1a99493f05 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastCategoryEntry.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastCategoryEntry.kt @@ -31,25 +31,25 @@ import androidx.room.PrimaryKey parentColumns = ["id"], childColumns = ["category_id"], onUpdate = ForeignKey.CASCADE, - onDelete = ForeignKey.CASCADE + onDelete = ForeignKey.CASCADE, ), ForeignKey( entity = Podcast::class, parentColumns = ["uri"], childColumns = ["podcast_uri"], onUpdate = ForeignKey.CASCADE, - onDelete = ForeignKey.CASCADE - ) + onDelete = ForeignKey.CASCADE, + ), ], indices = [ Index("podcast_uri", "category_id", unique = true), Index("category_id"), - Index("podcast_uri") - ] + Index("podcast_uri"), + ], ) @Immutable data class PodcastCategoryEntry( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, @ColumnInfo(name = "podcast_uri") val podcastUri: String, - @ColumnInfo(name = "category_id") val categoryId: Long + @ColumnInfo(name = "category_id") val categoryId: Long, ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastFollowedEntry.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastFollowedEntry.kt index 420e68f38f..7452f09a6a 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastFollowedEntry.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/database/model/PodcastFollowedEntry.kt @@ -31,15 +31,15 @@ import androidx.room.PrimaryKey parentColumns = ["uri"], childColumns = ["podcast_uri"], onUpdate = ForeignKey.CASCADE, - onDelete = ForeignKey.CASCADE - ) + onDelete = ForeignKey.CASCADE, + ), ], indices = [ - Index("podcast_uri", unique = true) - ] + Index("podcast_uri", unique = true), + ], ) @Immutable data class PodcastFollowedEntry( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, - @ColumnInfo(name = "podcast_uri") val podcastUri: String + @ColumnInfo(name = "podcast_uri") val podcastUri: String, ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/di/DataDiModule.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/di/DataDiModule.kt index 68d4b1920f..2cf54ca720 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/di/DataDiModule.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/di/DataDiModule.kt @@ -55,9 +55,7 @@ object DataDiModule { @Provides @Singleton - fun provideOkHttpClient( - @ApplicationContext context: Context - ): OkHttpClient = OkHttpClient.Builder() + fun provideOkHttpClient(@ApplicationContext context: Context): OkHttpClient = OkHttpClient.Builder() .cache(Cache(File(context.cacheDir, "http_cache"), (20 * 1024 * 1024).toLong())) .apply { if (BuildConfig.DEBUG) eventListenerFactory(LoggingEventListener.Factory()) @@ -66,9 +64,7 @@ object DataDiModule { @Provides @Singleton - fun provideDatabase( - @ApplicationContext context: Context - ): JetcasterDatabase = + fun provideDatabase(@ApplicationContext context: Context): JetcasterDatabase = Room.databaseBuilder(context, JetcasterDatabase::class.java, "data.db") // This is not recommended for normal apps, but the goal of this sample isn't to // showcase all of Room. @@ -77,48 +73,34 @@ object DataDiModule { @Provides @Singleton - fun provideImageLoader( - @ApplicationContext context: Context - ): ImageLoader = ImageLoader.Builder(context) + fun provideImageLoader(@ApplicationContext context: Context): ImageLoader = ImageLoader.Builder(context) // Disable `Cache-Control` header support as some podcast images disable disk caching. .respectCacheHeaders(false) .build() @Provides @Singleton - fun provideCategoriesDao( - database: JetcasterDatabase - ): CategoriesDao = database.categoriesDao() + fun provideCategoriesDao(database: JetcasterDatabase): CategoriesDao = database.categoriesDao() @Provides @Singleton - fun providePodcastCategoryEntryDao( - database: JetcasterDatabase - ): PodcastCategoryEntryDao = database.podcastCategoryEntryDao() + fun providePodcastCategoryEntryDao(database: JetcasterDatabase): PodcastCategoryEntryDao = database.podcastCategoryEntryDao() @Provides @Singleton - fun providePodcastsDao( - database: JetcasterDatabase - ): PodcastsDao = database.podcastsDao() + fun providePodcastsDao(database: JetcasterDatabase): PodcastsDao = database.podcastsDao() @Provides @Singleton - fun provideEpisodesDao( - database: JetcasterDatabase - ): EpisodesDao = database.episodesDao() + fun provideEpisodesDao(database: JetcasterDatabase): EpisodesDao = database.episodesDao() @Provides @Singleton - fun providePodcastFollowedEntryDao( - database: JetcasterDatabase - ): PodcastFollowedEntryDao = database.podcastFollowedEntryDao() + fun providePodcastFollowedEntryDao(database: JetcasterDatabase): PodcastFollowedEntryDao = database.podcastFollowedEntryDao() @Provides @Singleton - fun provideTransactionRunner( - database: JetcasterDatabase - ): TransactionRunner = database.transactionRunnerDao() + fun provideTransactionRunner(database: JetcasterDatabase): TransactionRunner = database.transactionRunnerDao() @Provides @Singleton @@ -136,9 +118,7 @@ object DataDiModule { @Provides @Singleton - fun provideEpisodeStore( - episodeDao: EpisodesDao - ): EpisodeStore = LocalEpisodeStore(episodeDao) + fun provideEpisodeStore(episodeDao: EpisodesDao): EpisodeStore = LocalEpisodeStore(episodeDao) @Provides @Singleton @@ -149,7 +129,7 @@ object DataDiModule { ): PodcastStore = LocalPodcastStore( podcastDao = podcastDao, podcastFollowedEntryDao = podcastFollowedEntryDao, - transactionRunner = transactionRunner + transactionRunner = transactionRunner, ) @Provides diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/Feeds.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/Feeds.kt index 216cce6b9d..ff1ef0dc0b 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/Feeds.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/Feeds.kt @@ -43,5 +43,5 @@ val SampleFeeds = listOf( "https://audioboom.com/channels/5025217.rss", "https://feeds.simplecast.com/7PvD7RPL", "https://feeds.buzzsprout.com/1006078.rss", - "https://feeds.megaphone.fm/HSW9992617712" + "https://feeds.megaphone.fm/HSW9992617712", ) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/OkHttpExtensions.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/OkHttpExtensions.kt index e572f23abb..b0b75933c3 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/OkHttpExtensions.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/OkHttpExtensions.kt @@ -45,7 +45,7 @@ suspend fun Call.await(): Response = suspendCancellableCoroutine { continuation override fun onFailure(call: Call, e: IOException) { continuation.resumeWithException(e) } - } + }, ) continuation.invokeOnCancellation { diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/PodcastFetcher.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/PodcastFetcher.kt index 60af89df98..ea74d99801 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/PodcastFetcher.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/network/PodcastFetcher.kt @@ -24,6 +24,7 @@ import com.example.jetcaster.core.data.database.model.Episode import com.example.jetcaster.core.data.database.model.Podcast import com.rometools.modules.itunes.EntryInformation import com.rometools.modules.itunes.FeedInformation +import com.rometools.rome.feed.synd.SyndEnclosure import com.rometools.rome.feed.synd.SyndEntry import com.rometools.rome.feed.synd.SyndFeed import com.rometools.rome.io.SyndFeedInput @@ -53,7 +54,7 @@ import okhttp3.Request class PodcastsFetcher @Inject constructor( private val okHttpClient: OkHttpClient, private val syndFeedInput: SyndFeedInput, - @Dispatcher(JetcasterDispatchers.IO) private val ioDispatcher: CoroutineDispatcher + @Dispatcher(JetcasterDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, ) { /** @@ -108,15 +109,9 @@ class PodcastsFetcher @Inject constructor( } sealed class PodcastRssResponse { - data class Error( - val throwable: Throwable?, - ) : PodcastRssResponse() - - data class Success( - val podcast: Podcast, - val episodes: List, - val categories: Set - ) : PodcastRssResponse() + data class Error(val throwable: Throwable?) : PodcastRssResponse() + + data class Success(val podcast: Podcast, val episodes: List, val categories: Set) : PodcastRssResponse() } /** @@ -124,7 +119,7 @@ sealed class PodcastRssResponse { */ private fun SyndFeed.toPodcastResponse(feedUrl: String): PodcastRssResponse { val podcastUri = uri ?: feedUrl - val episodes = entries.map { it.toEpisode(podcastUri) } + val episodes = entries.map { it.toEpisode(podcastUri, it.enclosures) } val feedInfo = getModule(PodcastModuleDtd) as? FeedInformation val podcast = Podcast( @@ -133,7 +128,7 @@ private fun SyndFeed.toPodcastResponse(feedUrl: String): PodcastRssResponse { description = feedInfo?.summary ?: description, author = author, copyright = copyright, - imageUrl = feedInfo?.imageUri?.toString() + imageUrl = feedInfo?.imageUri?.toString(), ) val categories = feedInfo?.categories @@ -146,7 +141,7 @@ private fun SyndFeed.toPodcastResponse(feedUrl: String): PodcastRssResponse { /** * Map a Rome [SyndEntry] instance to our own [Episode] data class. */ -private fun SyndEntry.toEpisode(podcastUri: String): Episode { +private fun SyndEntry.toEpisode(podcastUri: String, enclosures: List): Episode { val entryInformation = getModule(PodcastModuleDtd) as? EntryInformation return Episode( uri = uri, @@ -156,7 +151,8 @@ private fun SyndEntry.toEpisode(podcastUri: String): Episode { summary = entryInformation?.summary ?: description?.value, subtitle = entryInformation?.subtitle, published = Instant.ofEpochMilli(publishedDate.time).atOffset(ZoneOffset.UTC), - duration = entryInformation?.duration?.milliseconds?.let { Duration.ofMillis(it) } + duration = entryInformation?.duration?.milliseconds?.let { Duration.ofMillis(it) }, + mediaUrls = enclosures.map { it.url }, ) } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/CategoryStore.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/CategoryStore.kt index 0c29188054..820f61af7c 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/CategoryStore.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/CategoryStore.kt @@ -30,27 +30,19 @@ interface CategoryStore { * Returns a flow containing a list of categories which is sorted by the number * of podcasts in each category. */ - fun categoriesSortedByPodcastCount( - limit: Int = Integer.MAX_VALUE - ): Flow> + fun categoriesSortedByPodcastCount(limit: Int = Integer.MAX_VALUE): Flow> /** * Returns a flow containing a list of podcasts in the category with the given [categoryId], * sorted by the their last episode date. */ - fun podcastsInCategorySortedByPodcastCount( - categoryId: Long, - limit: Int = Int.MAX_VALUE - ): Flow> + fun podcastsInCategorySortedByPodcastCount(categoryId: Long, limit: Int = Int.MAX_VALUE): Flow> /** * Returns a flow containing a list of episodes from podcasts in the category with the * given [categoryId], sorted by the their last episode date. */ - fun episodesFromPodcastsInCategory( - categoryId: Long, - limit: Int = Integer.MAX_VALUE - ): Flow> + fun episodesFromPodcastsInCategory(categoryId: Long, limit: Int = Integer.MAX_VALUE): Flow> /** * Adds the category to the database if it doesn't already exist. @@ -74,7 +66,7 @@ class LocalCategoryStore constructor( private val categoriesDao: CategoriesDao, private val categoryEntryDao: PodcastCategoryEntryDao, private val episodesDao: EpisodesDao, - private val podcastsDao: PodcastsDao + private val podcastsDao: PodcastsDao, ) : CategoryStore { /** * Returns a flow containing a list of categories which is sorted by the number @@ -88,10 +80,7 @@ class LocalCategoryStore constructor( * Returns a flow containing a list of podcasts in the category with the given [categoryId], * sorted by the their last episode date. */ - override fun podcastsInCategorySortedByPodcastCount( - categoryId: Long, - limit: Int - ): Flow> { + override fun podcastsInCategorySortedByPodcastCount(categoryId: Long, limit: Int): Flow> { return podcastsDao.podcastsInCategorySortedByLastEpisode(categoryId, limit) } @@ -99,10 +88,7 @@ class LocalCategoryStore constructor( * Returns a flow containing a list of episodes from podcasts in the category with the * given [categoryId], sorted by the their last episode date. */ - override fun episodesFromPodcastsInCategory( - categoryId: Long, - limit: Int - ): Flow> { + override fun episodesFromPodcastsInCategory(categoryId: Long, limit: Int): Flow> { return episodesDao.episodesFromPodcastsInCategory(categoryId, limit) } @@ -120,10 +106,9 @@ class LocalCategoryStore constructor( override suspend fun addPodcastToCategory(podcastUri: String, categoryId: Long) { categoryEntryDao.insert( - PodcastCategoryEntry(podcastUri = podcastUri, categoryId = categoryId) + PodcastCategoryEntry(podcastUri = podcastUri, categoryId = categoryId), ) } - override fun getCategory(name: String): Flow = - categoriesDao.observeCategory(name) + override fun getCategory(name: String): Flow = categoriesDao.observeCategory(name) } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/EpisodeStore.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/EpisodeStore.kt index 26af92e97c..126c6a5bfe 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/EpisodeStore.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/EpisodeStore.kt @@ -36,19 +36,13 @@ interface EpisodeStore { * Returns a flow containing the list of episodes associated with the podcast with the * given [podcastUri]. */ - fun episodesInPodcast( - podcastUri: String, - limit: Int = Integer.MAX_VALUE - ): Flow> + fun episodesInPodcast(podcastUri: String, limit: Int = Integer.MAX_VALUE): Flow> /** * Returns a list of episodes for the given podcast URIs ordering by most recently published * to least recently published. */ - fun episodesInPodcasts( - podcastUris: List, - limit: Int = Integer.MAX_VALUE - ): Flow> + fun episodesInPodcasts(podcastUris: List, limit: Int = Integer.MAX_VALUE): Flow> /** * Add a new [Episode] to this store. @@ -57,15 +51,18 @@ interface EpisodeStore { */ suspend fun addEpisodes(episodes: Collection) + /** + * Deletes an [Episode] from this store. + */ + suspend fun deleteEpisode(episode: Episode) + suspend fun isEmpty(): Boolean } /** * A data repository for [Episode] instances. */ -class LocalEpisodeStore( - private val episodesDao: EpisodesDao -) : EpisodeStore { +class LocalEpisodeStore(private val episodesDao: EpisodesDao) : EpisodeStore { /** * Returns a flow containing the episode given [episodeUri]. */ @@ -73,27 +70,21 @@ class LocalEpisodeStore( return episodesDao.episode(episodeUri) } - override fun episodeAndPodcastWithUri(episodeUri: String): Flow = - episodesDao.episodeAndPodcast(episodeUri) + override fun episodeAndPodcastWithUri(episodeUri: String): Flow = episodesDao.episodeAndPodcast(episodeUri) /** * Returns a flow containing the list of episodes associated with the podcast with the * given [podcastUri]. */ - override fun episodesInPodcast( - podcastUri: String, - limit: Int - ): Flow> { + override fun episodesInPodcast(podcastUri: String, limit: Int): Flow> { return episodesDao.episodesForPodcastUri(podcastUri, limit) } + /** * Returns a list of episodes for the given podcast URIs ordering by most recently published * to least recently published. */ - override fun episodesInPodcasts( - podcastUris: List, - limit: Int - ): Flow> = + override fun episodesInPodcasts(podcastUris: List, limit: Int): Flow> = episodesDao.episodesForPodcasts(podcastUris, limit) /** @@ -101,8 +92,14 @@ class LocalEpisodeStore( * * This automatically switches to the main thread to maintain thread consistency. */ - override suspend fun addEpisodes(episodes: Collection) = - episodesDao.insertAll(episodes) + override suspend fun addEpisodes(episodes: Collection) = episodesDao.insertAll(episodes) + + /** + * Deletes an [Episode] from this store. + */ + override suspend fun deleteEpisode(episode: Episode) { + episodesDao.delete(episode) + } override suspend fun isEmpty(): Boolean = episodesDao.count() == 0 } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastStore.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastStore.kt index ee809c9e30..57e4328c6f 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastStore.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastStore.kt @@ -40,26 +40,19 @@ interface PodcastStore { * Returns a flow containing the entire collection of podcasts, sorted by the last episode * publish date for each podcast. */ - fun podcastsSortedByLastEpisode( - limit: Int = Int.MAX_VALUE - ): Flow> + fun podcastsSortedByLastEpisode(limit: Int = Int.MAX_VALUE): Flow> /** * Returns a flow containing a list of all followed podcasts, sorted by the their last * episode date. */ - fun followedPodcastsSortedByLastEpisode( - limit: Int = Int.MAX_VALUE - ): Flow> + fun followedPodcastsSortedByLastEpisode(limit: Int = Int.MAX_VALUE): Flow> /** * Returns a flow containing a list of podcasts such that its name partially matches * with the specified keyword */ - fun searchPodcastByTitle( - keyword: String, - limit: Int = Int.MAX_VALUE - ): Flow> + fun searchPodcastByTitle(keyword: String, limit: Int = Int.MAX_VALUE): Flow> /** * Return a flow containing a list of podcast such that it belongs to the any of categories @@ -69,7 +62,7 @@ interface PodcastStore { fun searchPodcastByTitleAndCategories( keyword: String, categories: List, - limit: Int = Int.MAX_VALUE + limit: Int = Int.MAX_VALUE, ): Flow> suspend fun togglePodcastFollowed(podcastUri: String) @@ -94,7 +87,7 @@ interface PodcastStore { class LocalPodcastStore constructor( private val podcastDao: PodcastsDao, private val podcastFollowedEntryDao: PodcastFollowedEntryDao, - private val transactionRunner: TransactionRunner + private val transactionRunner: TransactionRunner, ) : PodcastStore { /** * Return a flow containing the [Podcast] with the given [uri]. @@ -106,16 +99,13 @@ class LocalPodcastStore constructor( /** * Return a flow containing the [PodcastWithExtraInfo] with the given [podcastUri]. */ - override fun podcastWithExtraInfo(podcastUri: String): Flow = - podcastDao.podcastWithExtraInfo(podcastUri) + override fun podcastWithExtraInfo(podcastUri: String): Flow = podcastDao.podcastWithExtraInfo(podcastUri) /** * Returns a flow containing the entire collection of podcasts, sorted by the last episode * publish date for each podcast. */ - override fun podcastsSortedByLastEpisode( - limit: Int - ): Flow> { + override fun podcastsSortedByLastEpisode(limit: Int): Flow> { return podcastDao.podcastsSortedByLastEpisode(limit) } @@ -123,23 +113,18 @@ class LocalPodcastStore constructor( * Returns a flow containing a list of all followed podcasts, sorted by the their last * episode date. */ - override fun followedPodcastsSortedByLastEpisode( - limit: Int - ): Flow> { + override fun followedPodcastsSortedByLastEpisode(limit: Int): Flow> { return podcastDao.followedPodcastsSortedByLastEpisode(limit) } - override fun searchPodcastByTitle( - keyword: String, - limit: Int - ): Flow> { + override fun searchPodcastByTitle(keyword: String, limit: Int): Flow> { return podcastDao.searchPodcastByTitle(keyword, limit) } override fun searchPodcastByTitleAndCategories( keyword: String, categories: List, - limit: Int + limit: Int, ): Flow> { val categoryIdList = categories.map { it.id } return podcastDao.searchPodcastByTitleAndCategory(keyword, categoryIdList, limit) diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt index a102caf961..60dd6c4b09 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/data/repository/PodcastsRepository.kt @@ -39,7 +39,7 @@ class PodcastsRepository @Inject constructor( private val episodeStore: EpisodeStore, private val categoryStore: CategoryStore, private val transactionRunner: TransactionRunner, - @Dispatcher(JetcasterDispatchers.Main) mainDispatcher: CoroutineDispatcher + @Dispatcher(JetcasterDispatchers.Main) mainDispatcher: CoroutineDispatcher, ) { private var refreshingJob: Job? = null @@ -65,7 +65,7 @@ class PodcastsRepository @Inject constructor( // Now we can add the podcast to the category categoryStore.addPodcastToCategory( podcastUri = podcast.uri, - categoryId = categoryId + categoryId = categoryId, ) } } diff --git a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/util/Flows.kt b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/util/Flows.kt index a9940f315d..f03fc493fd 100644 --- a/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/util/Flows.kt +++ b/Jetcaster/core/data/src/main/java/com/example/jetcaster/core/util/Flows.kt @@ -32,23 +32,17 @@ fun combine( flow3: Flow, flow4: Flow, flow5: Flow, - transform: suspend (T1, T2, T3, T4, T5) -> R -): Flow = - kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - ) - } -fun combine( - flow: Flow, - flow2: Flow, - - transform: suspend (T1, T2) -> R -): Flow = + transform: suspend (T1, T2, T3, T4, T5) -> R, +): Flow = kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + ) +} +fun combine(flow: Flow, flow2: Flow, transform: suspend (T1, T2) -> R): Flow = kotlinx.coroutines.flow.combine(flow, flow2) { args: Array<*> -> transform( args[0] as T1, @@ -75,18 +69,17 @@ fun combine( flow4: Flow, flow5: Flow, flow6: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6) -> R -): Flow = - kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, - ) - } + transform: suspend (T1, T2, T3, T4, T5, T6) -> R, +): Flow = kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + ) +} /** * Combines seven flows into a single flow by combining their latest values using the provided transform function. @@ -109,24 +102,23 @@ fun combine( flow5: Flow, flow6: Flow, flow7: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R -): Flow = - kotlinx.coroutines.flow.combine( - flow, - flow2, - flow3, - flow4, - flow5, - flow6, - flow7 - ) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, - args[6] as T7, - ) - } + transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R, +): Flow = kotlinx.coroutines.flow.combine( + flow, + flow2, + flow3, + flow4, + flow5, + flow6, + flow7, +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + ) +} diff --git a/Jetcaster/core/designsystem/build.gradle.kts b/Jetcaster/core/designsystem/build.gradle.kts index 3e4ba24cb3..8d3650b7e1 100644 --- a/Jetcaster/core/designsystem/build.gradle.kts +++ b/Jetcaster/core/designsystem/build.gradle.kts @@ -1,51 +1,74 @@ +/* + * Copyright 2025 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. + */ + + plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.compose) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose) } // TODO(chris): Set up convention plugin android { - namespace = "com.example.jetcaster.core.designsystem" - compileSdk = libs.versions.compileSdk.get().toInt() - - defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() - vectorDrawables.useSupportLibrary = true - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + namespace = "com.example.jetcaster.core.designsystem" + compileSdk = + libs.versions.compileSdk + .get() + .toInt() + + defaultConfig { + minSdk = + libs.versions.minSdk + .get() + .toInt() + vectorDrawables.useSupportLibrary = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } } - } - buildFeatures { - compose = true - buildConfig = true - } + buildFeatures { + compose = true + buildConfig = true + } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } } kotlin { - jvmToolchain(17) + jvmToolchain(17) } 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) - implementation(libs.coil.kt.compose) - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) + 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) + implementation(libs.coil.kt.compose) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) } diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/HtmlTextContainer.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/HtmlTextContainer.kt index e0e91040fe..b9e8bd06b2 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/HtmlTextContainer.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/HtmlTextContainer.kt @@ -27,10 +27,7 @@ import androidx.compose.ui.text.fromHtml * annotated string from [text], and enable text selection if [text] has any selectable element. */ @Composable -fun HtmlTextContainer( - text: String, - content: @Composable (AnnotatedString) -> Unit -) { +fun HtmlTextContainer(text: String, content: @Composable (AnnotatedString) -> Unit) { val annotatedString = remember(key1 = text) { AnnotatedString.fromHtml(htmlString = text) } diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ImageBackground.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ImageBackground.kt index 4cb124dc65..28bd129d72 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ImageBackground.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/ImageBackground.kt @@ -29,26 +29,18 @@ import androidx.compose.ui.layout.ContentScale import coil.compose.AsyncImage @Composable -fun ImageBackgroundColorScrim( - url: String?, - color: Color, - modifier: Modifier = Modifier, -) { +fun ImageBackgroundColorScrim(url: String?, color: Color, modifier: Modifier = Modifier) { ImageBackground( url = url, modifier = modifier, overlay = { drawRect(color) - } + }, ) } @Composable -fun ImageBackgroundRadialGradientScrim( - url: String?, - colors: List, - modifier: Modifier = Modifier, -) { +fun ImageBackgroundRadialGradientScrim(url: String?, colors: List, modifier: Modifier = Modifier) { ImageBackground( url = url, modifier = modifier, @@ -56,10 +48,10 @@ fun ImageBackgroundRadialGradientScrim( val brush = Brush.radialGradient( colors = colors, center = Offset(0f, size.height), - radius = size.width * 1.5f + radius = size.width * 1.5f, ) drawRect(brush, blendMode = BlendMode.Multiply) - } + }, ) } @@ -67,11 +59,7 @@ fun ImageBackgroundRadialGradientScrim( * Displays an image scaled 150% overlaid by [overlay] */ @Composable -fun ImageBackground( - url: String?, - overlay: DrawScope.() -> Unit, - modifier: Modifier = Modifier, -) { +fun ImageBackground(url: String?, overlay: DrawScope.() -> Unit, modifier: Modifier = Modifier) { AsyncImage( model = url, contentDescription = null, @@ -83,6 +71,6 @@ fun ImageBackground( drawContent() overlay() } - } + }, ) } diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt index f7b8966196..a17d35fa85 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt @@ -43,6 +43,8 @@ fun PodcastImage( podcastImageUrl: String, contentDescription: String?, modifier: Modifier = Modifier, + // TODO: Remove the nested component modifier when shared elements are applied to entire app + imageModifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Crop, placeholderBrush: Brush = thumbnailPlaceholderDefaultBrush(), ) { @@ -61,28 +63,29 @@ fun PodcastImage( .crossfade(true) .build(), contentScale = contentScale, - onState = { state -> imagePainterState = state } + onState = { state -> imagePainterState = state }, ) Box( modifier = modifier, - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { when (imagePainterState) { is AsyncImagePainter.State.Loading, - is AsyncImagePainter.State.Error -> { + is AsyncImagePainter.State.Error, + -> { Image( painter = painterResource(id = R.drawable.img_empty), contentDescription = null, modifier = Modifier - .fillMaxSize() + .fillMaxSize(), ) } else -> { Box( - modifier = Modifier + modifier = modifier .background(placeholderBrush) - .fillMaxSize() + .fillMaxSize(), ) } @@ -92,7 +95,7 @@ fun PodcastImage( painter = imageLoader, contentDescription = contentDescription, contentScale = contentScale, - modifier = modifier, + modifier = modifier.then(imageModifier), ) } } diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt index 865dac3130..80f1f21bca 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/thumbnailPlaceholder.kt @@ -25,16 +25,12 @@ import com.example.jetcaster.designsystem.theme.surfaceVariantDark import com.example.jetcaster.designsystem.theme.surfaceVariantLight @Composable -internal fun thumbnailPlaceholderDefaultBrush( - color: Color = thumbnailPlaceHolderDefaultColor() -): Brush { +internal fun thumbnailPlaceholderDefaultBrush(color: Color = thumbnailPlaceHolderDefaultColor()): Brush { return SolidColor(color) } @Composable -private fun thumbnailPlaceHolderDefaultColor( - isInDarkMode: Boolean = isSystemInDarkTheme() -): Color { +private fun thumbnailPlaceHolderDefaultColor(isInDarkMode: Boolean = isSystemInDarkTheme()): Color { return if (isInDarkMode) { surfaceVariantDark } else { diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Color.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Color.kt index 51ab6000bc..cb09a54069 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Color.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Color.kt @@ -17,218 +17,218 @@ package com.example.jetcaster.designsystem.theme import androidx.compose.ui.graphics.Color -val primaryLight = Color(0xFF885200) -val onPrimaryLight = Color(0xFFFFFFFF) -val primaryContainerLight = Color(0xFFFFAC46) -val onPrimaryContainerLight = Color(0xFF482900) -val secondaryLight = Color(0xFF7A5817) -val onSecondaryLight = Color(0xFFFFFFFF) -val secondaryContainerLight = Color(0xFFFFD798) -val onSecondaryContainerLight = Color(0xFF5C3F00) -val tertiaryLight = Color(0xFF994700) +val primaryLight = Color(0xFFFF792C) +val onPrimaryLight = Color(0xFFFFECE0) +val primaryContainerLight = Color(0xFFFFDBCC) +val onPrimaryContainerLight = Color(0xFF662500) +val secondaryLight = Color(0xFFFFBD1D) +val onSecondaryLight = Color(0xFF664800) +val secondaryContainerLight = Color(0xFFFFEECC) +val onSecondaryContainerLight = Color(0xFF664800) +val tertiaryLight = Color(0xFFFF8F0F) val onTertiaryLight = Color(0xFFFFFFFF) -val tertiaryContainerLight = Color(0xFFFF801F) -val onTertiaryContainerLight = Color(0xFF2D1000) -val errorLight = Color(0xFFA4384A) +val tertiaryContainerLight = Color(0xFFFFE5CC) +val onTertiaryContainerLight = Color(0xFF663600) +val errorLight = Color(0xFFBA1A1A) val onErrorLight = Color(0xFFFFFFFF) -val errorContainerLight = Color(0xFFF87889) -val onErrorContainerLight = Color(0xFF32000A) -val backgroundLight = Color(0xFFFFF8F4) -val onBackgroundLight = Color(0xFF221A11) -val surfaceLight = Color(0xFFFFF8F4) -val onSurfaceLight = Color(0xFF221A11) -val surfaceVariantLight = Color(0xFFF7DEC8) -val onSurfaceVariantLight = Color(0xFF544434) -val outlineLight = Color(0xFF877461) -val outlineVariantLight = Color(0xFFDAC3AD) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF93000A) +val backgroundLight = Color(0xFFFEF7FF) +val onBackgroundLight = Color(0xFF1D1B20) +val surfaceLight = Color(0xFFFEF7FF) +val onSurfaceLight = Color(0xFF1D1B20) +val surfaceVariantLight = Color(0xFFE7E0EB) +val onSurfaceVariantLight = Color(0xFF49454E) +val outlineLight = Color(0xFF7A757F) +val outlineVariantLight = Color(0xFFCBC4CF) val scrimLight = Color(0xFF000000) -val inverseSurfaceLight = Color(0xFF382F25) -val inverseOnSurfaceLight = Color(0xFFFFEEDF) -val inversePrimaryLight = Color(0xFFFFB868) -val surfaceDimLight = Color(0xFFE8D7C9) -val surfaceBrightLight = Color(0xFFFFF8F4) +val inverseSurfaceLight = Color(0xFF322F35) +val inverseOnSurfaceLight = Color(0xFFF5EFF7) +val inversePrimaryLight = Color(0xFFD3BCFD) +val surfaceDimLight = Color(0xFFDED8E0) +val surfaceBrightLight = Color(0xFFFEF7FF) val surfaceContainerLowestLight = Color(0xFFFFFFFF) -val surfaceContainerLowLight = Color(0xFFFFF1E6) -val surfaceContainerLight = Color(0xFFFCEBDC) -val surfaceContainerHighLight = Color(0xFFF6E5D7) -val surfaceContainerHighestLight = Color(0xFFF1E0D1) +val surfaceContainerLowLight = Color(0xFFF8F1FA) +val surfaceContainerLight = Color(0xFFF2ECF4) +val surfaceContainerHighLight = Color(0xFFEDE6EE) +val surfaceContainerHighestLight = Color(0xFFE7E0E8) -val primaryLightMediumContrast = Color(0xFF623A00) +val primaryLightMediumContrast = Color(0xFF3E2C62) val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) -val primaryContainerLightMediumContrast = Color(0xFFA76600) +val primaryContainerLightMediumContrast = Color(0xFF77639D) val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) -val secondaryLightMediumContrast = Color(0xFF5A3D00) +val secondaryLightMediumContrast = Color(0xFF3A3346) val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) -val secondaryContainerLightMediumContrast = Color(0xFF936E2B) +val secondaryContainerLightMediumContrast = Color(0xFF72697F) val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) -val tertiaryLightMediumContrast = Color(0xFF6F3100) +val tertiaryLightMediumContrast = Color(0xFF512A36) val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) -val tertiaryContainerLightMediumContrast = Color(0xFFBC5800) +val tertiaryContainerLightMediumContrast = Color(0xFF8F606C) val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) -val errorLightMediumContrast = Color(0xFF7F1B30) +val errorLightMediumContrast = Color(0xFF740006) val onErrorLightMediumContrast = Color(0xFFFFFFFF) -val errorContainerLightMediumContrast = Color(0xFFC14E5F) +val errorContainerLightMediumContrast = Color(0xFFCF2C27) val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) -val backgroundLightMediumContrast = Color(0xFFFFF8F4) -val onBackgroundLightMediumContrast = Color(0xFF221A11) -val surfaceLightMediumContrast = Color(0xFFFFF8F4) -val onSurfaceLightMediumContrast = Color(0xFF221A11) -val surfaceVariantLightMediumContrast = Color(0xFFF7DEC8) -val onSurfaceVariantLightMediumContrast = Color(0xFF504030) -val outlineLightMediumContrast = Color(0xFF6E5C4A) -val outlineVariantLightMediumContrast = Color(0xFF8B7765) +val backgroundLightMediumContrast = Color(0xFFFEF7FF) +val onBackgroundLightMediumContrast = Color(0xFF1D1B20) +val surfaceLightMediumContrast = Color(0xFFFEF7FF) +val onSurfaceLightMediumContrast = Color(0xFF121016) +val surfaceVariantLightMediumContrast = Color(0xFFE7E0EB) +val onSurfaceVariantLightMediumContrast = Color(0xFF38353D) +val outlineLightMediumContrast = Color(0xFF55515A) +val outlineVariantLightMediumContrast = Color(0xFF706B75) val scrimLightMediumContrast = Color(0xFF000000) -val inverseSurfaceLightMediumContrast = Color(0xFF382F25) -val inverseOnSurfaceLightMediumContrast = Color(0xFFFFEEDF) -val inversePrimaryLightMediumContrast = Color(0xFFFFB868) -val surfaceDimLightMediumContrast = Color(0xFFE8D7C9) -val surfaceBrightLightMediumContrast = Color(0xFFFFF8F4) +val inverseSurfaceLightMediumContrast = Color(0xFF322F35) +val inverseOnSurfaceLightMediumContrast = Color(0xFFF5EFF7) +val inversePrimaryLightMediumContrast = Color(0xFFD3BCFD) +val surfaceDimLightMediumContrast = Color(0xFFCAC4CC) +val surfaceBrightLightMediumContrast = Color(0xFFFEF7FF) val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) -val surfaceContainerLowLightMediumContrast = Color(0xFFFFF1E6) -val surfaceContainerLightMediumContrast = Color(0xFFFCEBDC) -val surfaceContainerHighLightMediumContrast = Color(0xFFF6E5D7) -val surfaceContainerHighestLightMediumContrast = Color(0xFFF1E0D1) +val surfaceContainerLowLightMediumContrast = Color(0xFFF8F1FA) +val surfaceContainerLightMediumContrast = Color(0xFFEDE6EE) +val surfaceContainerHighLightMediumContrast = Color(0xFFE1DBE3) +val surfaceContainerHighestLightMediumContrast = Color(0xFFD6D0D7) -val primaryLightHighContrast = Color(0xFF351D00) +val primaryLightHighContrast = Color(0xFF342157) val onPrimaryLightHighContrast = Color(0xFFFFFFFF) -val primaryContainerLightHighContrast = Color(0xFF623A00) +val primaryContainerLightHighContrast = Color(0xFF523F77) val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) -val secondaryLightHighContrast = Color(0xFF301F00) +val secondaryLightHighContrast = Color(0xFF30293C) val onSecondaryLightHighContrast = Color(0xFFFFFFFF) -val secondaryContainerLightHighContrast = Color(0xFF5A3D00) +val secondaryContainerLightHighContrast = Color(0xFF4D465A) val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) -val tertiaryLightHighContrast = Color(0xFF3C1800) +val tertiaryLightHighContrast = Color(0xFF45212C) val onTertiaryLightHighContrast = Color(0xFFFFFFFF) -val tertiaryContainerLightHighContrast = Color(0xFF6F3100) +val tertiaryContainerLightHighContrast = Color(0xFF673D48) val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) -val errorLightHighContrast = Color(0xFF4C0014) +val errorLightHighContrast = Color(0xFF600004) val onErrorLightHighContrast = Color(0xFFFFFFFF) -val errorContainerLightHighContrast = Color(0xFF7F1B30) +val errorContainerLightHighContrast = Color(0xFF98000A) val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) -val backgroundLightHighContrast = Color(0xFFFFF8F4) -val onBackgroundLightHighContrast = Color(0xFF221A11) -val surfaceLightHighContrast = Color(0xFFFFF8F4) +val backgroundLightHighContrast = Color(0xFFFEF7FF) +val onBackgroundLightHighContrast = Color(0xFF1D1B20) +val surfaceLightHighContrast = Color(0xFFFEF7FF) val onSurfaceLightHighContrast = Color(0xFF000000) -val surfaceVariantLightHighContrast = Color(0xFFF7DEC8) -val onSurfaceVariantLightHighContrast = Color(0xFF2E2113) -val outlineLightHighContrast = Color(0xFF504030) -val outlineVariantLightHighContrast = Color(0xFF504030) +val surfaceVariantLightHighContrast = Color(0xFFE7E0EB) +val onSurfaceVariantLightHighContrast = Color(0xFF000000) +val outlineLightHighContrast = Color(0xFF2E2B33) +val outlineVariantLightHighContrast = Color(0xFF4C4751) val scrimLightHighContrast = Color(0xFF000000) -val inverseSurfaceLightHighContrast = Color(0xFF382F25) +val inverseSurfaceLightHighContrast = Color(0xFF322F35) val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) -val inversePrimaryLightHighContrast = Color(0xFFFFE8D4) -val surfaceDimLightHighContrast = Color(0xFFE8D7C9) -val surfaceBrightLightHighContrast = Color(0xFFFFF8F4) +val inversePrimaryLightHighContrast = Color(0xFFD3BCFD) +val surfaceDimLightHighContrast = Color(0xFFBCB7BF) +val surfaceBrightLightHighContrast = Color(0xFFFEF7FF) val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) -val surfaceContainerLowLightHighContrast = Color(0xFFFFF1E6) -val surfaceContainerLightHighContrast = Color(0xFFFCEBDC) -val surfaceContainerHighLightHighContrast = Color(0xFFF6E5D7) -val surfaceContainerHighestLightHighContrast = Color(0xFFF1E0D1) +val surfaceContainerLowLightHighContrast = Color(0xFFF5EFF7) +val surfaceContainerLightHighContrast = Color(0xFFE7E0E8) +val surfaceContainerHighLightHighContrast = Color(0xFFD8D2DA) +val surfaceContainerHighestLightHighContrast = Color(0xFFCAC4CC) -val primaryDark = Color(0xFFFFCF9E) -val onPrimaryDark = Color(0xFF482900) -val primaryContainerDark = Color(0xFFF79900) -val onPrimaryContainerDark = Color(0xFF371E00) -val secondaryDark = Color(0xFFFFFEFF) -val onSecondaryDark = Color(0xFF422C00) -val secondaryContainerDark = Color(0xFFFBCC80) -val onSecondaryContainerDark = Color(0xFF553A00) -val tertiaryDark = Color(0xFFFFB68B) -val onTertiaryDark = Color(0xFF522300) -val tertiaryContainerDark = Color(0xFFE76E00) -val onTertiaryContainerDark = Color(0xFF000000) -val errorDark = Color(0xFFFFB2B9) -val onErrorDark = Color(0xFF65041F) -val errorContainerDark = Color(0xFFC14E5F) -val onErrorContainerDark = Color(0xFFFFFFFF) -val backgroundDark = Color(0xFF1A120A) -val onBackgroundDark = Color(0xFFF1E0D1) -val surfaceDark = Color(0xFF1A120A) -val onSurfaceDark = Color(0xFFF1E0D1) -val surfaceVariantDark = Color(0xFF544434) -val onSurfaceVariantDark = Color(0xFFDAC3AD) -val outlineDark = Color(0xFFA28D7A) -val outlineVariantDark = Color(0xFF544434) +val primaryDark = Color(0xFFFF792C) +val onPrimaryDark = Color(0xFFFFECE0) +val primaryContainerDark = Color(0xFF703717) +val onPrimaryContainerDark = Color(0xFFFFDBCC) +val secondaryDark = Color(0xFFFFBD1D) +val onSecondaryDark = Color(0xFF412D00) +val secondaryContainerDark = Color(0xFFF0C470) +val onSecondaryContainerDark = Color(0xFF704700) +val tertiaryDark = Color(0xFFFF8F0F) +val onTertiaryDark = Color(0xFF663600) +val tertiaryContainerDark = Color(0xFF6C3A06) +val onTertiaryContainerDark = Color(0xFFFFDCC2) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF151218) +val onBackgroundDark = Color(0xFFE7E0E8) +val surfaceDark = Color(0xFF151218) +val onSurfaceDark = Color(0xFFE7E0E8) +val surfaceVariantDark = Color(0xFF49454E) +val onSurfaceVariantDark = Color(0xFFCBC4CF) +val outlineDark = Color(0xFF948F99) +val outlineVariantDark = Color(0xFF49454E) val scrimDark = Color(0xFF000000) -val inverseSurfaceDark = Color(0xFFF1E0D1) -val inverseOnSurfaceDark = Color(0xFF382F25) -val inversePrimaryDark = Color(0xFF885200) -val surfaceDimDark = Color(0xFF1A120A) -val surfaceBrightDark = Color(0xFF42372D) -val surfaceContainerLowestDark = Color(0xFF140D06) -val surfaceContainerLowDark = Color(0xFF221A11) -val surfaceContainerDark = Color(0xFF271E15) -val surfaceContainerHighDark = Color(0xFF32281F) -val surfaceContainerHighestDark = Color(0xFF3D3329) +val inverseSurfaceDark = Color(0xFFE7E0E8) +val inverseOnSurfaceDark = Color(0xFF322F35) +val inversePrimaryDark = Color(0xFF68548E) +val surfaceDimDark = Color(0xFF19120C) +val surfaceBrightDark = Color(0xFF413731) +val surfaceContainerLowestDark = Color(0xFF140D08) +val surfaceContainerLowDark = Color(0xFF221A14) +val surfaceContainerDark = Color(0xFF261E18) +val surfaceContainerHighDark = Color(0xFF312822) +val surfaceContainerHighestDark = Color(0xFF3C332C) -val primaryDarkMediumContrast = Color(0xFFFFCF9E) -val onPrimaryDarkMediumContrast = Color(0xFF351D00) -val primaryContainerDarkMediumContrast = Color(0xFFF79900) +val primaryDarkMediumContrast = Color(0xFFFDB186) +val onPrimaryDarkMediumContrast = Color(0xFF2C0C00) +val primaryContainerDarkMediumContrast = Color(0xFFC97E59) val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) -val secondaryDarkMediumContrast = Color(0xFFFFFEFF) -val onSecondaryDarkMediumContrast = Color(0xFF422C00) -val secondaryContainerDarkMediumContrast = Color(0xFFFBCC80) -val onSecondaryContainerDarkMediumContrast = Color(0xFF2C1C00) -val tertiaryDarkMediumContrast = Color(0xFFFFBC95) -val onTertiaryDarkMediumContrast = Color(0xFF2A0E00) -val tertiaryContainerDarkMediumContrast = Color(0xFFE76E00) -val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) -val errorDarkMediumContrast = Color(0xFFFFB8BE) -val onErrorDarkMediumContrast = Color(0xFF36000C) -val errorContainerDarkMediumContrast = Color(0xFFE5697A) +val secondaryDarkMediumContrast = Color(0xFFF0C470) +val onSecondaryDarkMediumContrast = Color(0xFF201400) +val secondaryContainerDarkMediumContrast = Color(0xFFB18B3D) +val onSecondaryContainerDarkMediumContrast = Color(0xFF664800) +val tertiaryDarkMediumContrast = Color(0xFFFFBD87) +val onTertiaryDarkMediumContrast = Color(0xFF663600) +val tertiaryContainerDarkMediumContrast = Color(0xFFC3824A) +val onTertiaryContainerDarkMediumContrast = Color(0xFF663600) +val errorDarkMediumContrast = Color(0xFFFFD2CC) +val onErrorDarkMediumContrast = Color(0xFF540003) +val errorContainerDarkMediumContrast = Color(0xFFFF5449) val onErrorContainerDarkMediumContrast = Color(0xFF000000) -val backgroundDarkMediumContrast = Color(0xFF1A120A) -val onBackgroundDarkMediumContrast = Color(0xFFF1E0D1) -val surfaceDarkMediumContrast = Color(0xFF1A120A) -val onSurfaceDarkMediumContrast = Color(0xFFFFFAF8) -val surfaceVariantDarkMediumContrast = Color(0xFF544434) -val onSurfaceVariantDarkMediumContrast = Color(0xFFDEC7B1) -val outlineDarkMediumContrast = Color(0xFFB59F8B) -val outlineVariantDarkMediumContrast = Color(0xFF93806D) +val backgroundDarkMediumContrast = Color(0xFF151218) +val onBackgroundDarkMediumContrast = Color(0xFFE7E0E8) +val surfaceDarkMediumContrast = Color(0xFF151218) +val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkMediumContrast = Color(0xFF49454E) +val onSurfaceVariantDarkMediumContrast = Color(0xFFE1DAE5) +val outlineDarkMediumContrast = Color(0xFFB6B0BA) +val outlineVariantDarkMediumContrast = Color(0xFF948E98) val scrimDarkMediumContrast = Color(0xFF000000) -val inverseSurfaceDarkMediumContrast = Color(0xFFF1E0D1) -val inverseOnSurfaceDarkMediumContrast = Color(0xFF32281F) -val inversePrimaryDarkMediumContrast = Color(0xFF693E00) -val surfaceDimDarkMediumContrast = Color(0xFF1A120A) -val surfaceBrightDarkMediumContrast = Color(0xFF42372D) -val surfaceContainerLowestDarkMediumContrast = Color(0xFF140D06) -val surfaceContainerLowDarkMediumContrast = Color(0xFF221A11) -val surfaceContainerDarkMediumContrast = Color(0xFF271E15) -val surfaceContainerHighDarkMediumContrast = Color(0xFF32281F) -val surfaceContainerHighestDarkMediumContrast = Color(0xFF3D3329) +val inverseSurfaceDarkMediumContrast = Color(0xFFE7E0E8) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF2C292F) +val inversePrimaryDarkMediumContrast = Color(0xFF513E75) +val surfaceDimDarkMediumContrast = Color(0xFF19120C) +val surfaceBrightDarkMediumContrast = Color(0xFF413731) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF140D08) +val surfaceContainerLowDarkMediumContrast = Color(0xFF221A14) +val surfaceContainerDarkMediumContrast = Color(0xFF261E18) +val surfaceContainerHighDarkMediumContrast = Color(0xFF312822) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3C332C) -val primaryDarkHighContrast = Color(0xFFFFFAF8) +val primaryDarkHighContrast = Color(0xFFFFF9F8) val onPrimaryDarkHighContrast = Color(0xFF000000) -val primaryContainerDarkHighContrast = Color(0xFFFFBE76) -val onPrimaryContainerDarkHighContrast = Color(0xFF000000) -val secondaryDarkHighContrast = Color(0xFFFFFEFF) +val primaryContainerDarkHighContrast = Color(0xFFFFBC9C) +val onPrimaryContainerDarkHighContrast = Color(0xFF662500) +val secondaryDarkHighContrast = Color(0xFFFFFAF7) val onSecondaryDarkHighContrast = Color(0xFF000000) -val secondaryContainerDarkHighContrast = Color(0xFFFBCC80) -val onSecondaryContainerDarkHighContrast = Color(0xFF000000) -val tertiaryDarkHighContrast = Color(0xFFFFFAF8) +val secondaryContainerDarkHighContrast = Color(0xFFF0C470) +val onSecondaryContainerDarkHighContrast = Color(0xFF664800) +val tertiaryDarkHighContrast = Color(0xFFFFE5CC) val onTertiaryDarkHighContrast = Color(0xFF000000) -val tertiaryContainerDarkHighContrast = Color(0xFFFFBC95) +val tertiaryContainerDarkHighContrast = Color(0xFFFFBD87) val onTertiaryContainerDarkHighContrast = Color(0xFF000000) -val errorDarkHighContrast = Color(0xFFFFF9F9) +val errorDarkHighContrast = Color(0xFFFFECE9) val onErrorDarkHighContrast = Color(0xFF000000) -val errorContainerDarkHighContrast = Color(0xFFFFB8BE) -val onErrorContainerDarkHighContrast = Color(0xFF000000) -val backgroundDarkHighContrast = Color(0xFF1A120A) -val onBackgroundDarkHighContrast = Color(0xFFF1E0D1) -val surfaceDarkHighContrast = Color(0xFF1A120A) +val errorContainerDarkHighContrast = Color(0xFFFFAEA4) +val onErrorContainerDarkHighContrast = Color(0xFF220001) +val backgroundDarkHighContrast = Color(0xFF151218) +val onBackgroundDarkHighContrast = Color(0xFFE7E0E8) +val surfaceDarkHighContrast = Color(0xFF151218) val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) -val surfaceVariantDarkHighContrast = Color(0xFF544434) -val onSurfaceVariantDarkHighContrast = Color(0xFFFFFAF8) -val outlineDarkHighContrast = Color(0xFFDEC7B1) -val outlineVariantDarkHighContrast = Color(0xFFDEC7B1) +val surfaceVariantDarkHighContrast = Color(0xFF49454E) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF) +val outlineDarkHighContrast = Color(0xFFF5EDF9) +val outlineVariantDarkHighContrast = Color(0xFFC7C0CB) val scrimDarkHighContrast = Color(0xFF000000) -val inverseSurfaceDarkHighContrast = Color(0xFFF1E0D1) +val inverseSurfaceDarkHighContrast = Color(0xFFE7E0E8) val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) -val inversePrimaryDarkHighContrast = Color(0xFF3F2400) -val surfaceDimDarkHighContrast = Color(0xFF1A120A) -val surfaceBrightDarkHighContrast = Color(0xFF42372D) -val surfaceContainerLowestDarkHighContrast = Color(0xFF140D06) -val surfaceContainerLowDarkHighContrast = Color(0xFF221A11) -val surfaceContainerDarkHighContrast = Color(0xFF271E15) -val surfaceContainerHighDarkHighContrast = Color(0xFF32281F) -val surfaceContainerHighestDarkHighContrast = Color(0xFF3D3329) +val inversePrimaryDarkHighContrast = Color(0xFF513E75) +val surfaceDimDarkHighContrast = Color(0xFF19120C) +val surfaceBrightDarkHighContrast = Color(0xFF413731) +val surfaceContainerLowestDarkHighContrast = Color(0xFF140D08) +val surfaceContainerLowDarkHighContrast = Color(0xFF221A14) +val surfaceContainerDarkHighContrast = Color(0xFF261E18) +val surfaceContainerHighDarkHighContrast = Color(0xFF312822) +val surfaceContainerHighestDarkHighContrast = Color(0xFF3C332C) diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt index 41bbfefbb6..a5f74d71de 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Shape.kt @@ -23,5 +23,5 @@ import androidx.compose.ui.unit.dp val JetcasterShapes = Shapes( small = RoundedCornerShape(percent = 50), medium = RoundedCornerShape(size = 8.dp), - large = RoundedCornerShape(size = 16.dp) + large = RoundedCornerShape(size = 16.dp), ) diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt index b9d6eb171e..be704c3eba 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Type.kt @@ -18,106 +18,107 @@ package com.example.jetcaster.designsystem.theme import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp val JetcasterTypography = androidx.compose.material3.Typography( displayLarge = TextStyle( - fontFamily = Montserrat, - fontSize = 57.sp, - fontWeight = FontWeight.W400, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp + fontSize = 64.sp, + lineHeight = 56.sp, + fontFamily = RobotoFlex, + fontWeight = FontWeight(738), + textAlign = TextAlign.Center, ), displayMedium = TextStyle( - fontFamily = Montserrat, + fontFamily = RobotoFlex, fontSize = 45.sp, fontWeight = FontWeight.W400, - lineHeight = 52.sp + lineHeight = 52.sp, ), displaySmall = TextStyle( fontFamily = Montserrat, fontSize = 36.sp, fontWeight = FontWeight.W400, - lineHeight = 44.sp + lineHeight = 44.sp, ), headlineLarge = TextStyle( fontFamily = Montserrat, fontSize = 32.sp, fontWeight = FontWeight.W500, - lineHeight = 40.sp + lineHeight = 40.sp, ), headlineMedium = TextStyle( fontFamily = Montserrat, fontSize = 28.sp, fontWeight = FontWeight.W500, - lineHeight = 36.sp + lineHeight = 36.sp, ), headlineSmall = TextStyle( fontFamily = Montserrat, fontSize = 24.sp, fontWeight = FontWeight.W500, - lineHeight = 32.sp + lineHeight = 32.sp, ), titleLarge = TextStyle( fontFamily = Montserrat, fontSize = 22.sp, fontWeight = FontWeight.W400, - lineHeight = 28.sp + lineHeight = 28.sp, ), titleMedium = TextStyle( fontFamily = Montserrat, fontSize = 16.sp, fontWeight = FontWeight.W500, lineHeight = 24.sp, - letterSpacing = 0.15.sp + letterSpacing = 0.15.sp, ), titleSmall = TextStyle( fontFamily = Montserrat, fontSize = 14.sp, fontWeight = FontWeight.W500, lineHeight = 20.sp, - letterSpacing = 0.1.sp + letterSpacing = 0.1.sp, ), labelLarge = TextStyle( fontFamily = Montserrat, fontSize = 14.sp, fontWeight = FontWeight.W500, lineHeight = 20.sp, - letterSpacing = 0.1.sp + letterSpacing = 0.1.sp, ), labelMedium = TextStyle( fontFamily = Montserrat, fontSize = 12.sp, fontWeight = FontWeight.W500, lineHeight = 16.sp, - letterSpacing = 0.5.sp + letterSpacing = 0.5.sp, ), labelSmall = TextStyle( fontFamily = Montserrat, fontSize = 11.sp, fontWeight = FontWeight.W500, lineHeight = 16.sp, - letterSpacing = 0.5.sp + letterSpacing = 0.5.sp, ), bodyLarge = TextStyle( fontFamily = Montserrat, fontSize = 16.sp, fontWeight = FontWeight.W500, lineHeight = 24.sp, - letterSpacing = 0.5.sp + letterSpacing = 0.5.sp, ), bodyMedium = TextStyle( fontFamily = Montserrat, fontSize = 14.sp, fontWeight = FontWeight.W500, lineHeight = 20.sp, - letterSpacing = 0.25.sp + letterSpacing = 0.25.sp, ), bodySmall = TextStyle( fontFamily = Montserrat, fontSize = 12.sp, fontWeight = FontWeight.W500, lineHeight = 16.sp, - letterSpacing = 0.4.sp + letterSpacing = 0.4.sp, ), ) diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Typography.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Typography.kt index bac67e41f7..4f494ccb21 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Typography.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/theme/Typography.kt @@ -25,5 +25,9 @@ val Montserrat = FontFamily( Font(R.font.montserrat_light, FontWeight.Light), Font(R.font.montserrat_regular, FontWeight.Normal), Font(R.font.montserrat_medium, FontWeight.Medium), - Font(R.font.montserrat_semibold, FontWeight.SemiBold) + Font(R.font.montserrat_semibold, FontWeight.SemiBold), +) + +val RobotoFlex = FontFamily( + Font(R.font.roboto_flex), ) diff --git a/Jetcaster/core/designsystem/src/main/res/font/roboto_flex.ttf b/Jetcaster/core/designsystem/src/main/res/font/roboto_flex.ttf new file mode 100644 index 0000000000..2e5c2a26a7 Binary files /dev/null and b/Jetcaster/core/designsystem/src/main/res/font/roboto_flex.ttf differ diff --git a/Jetcaster/core/domain-testing/build.gradle.kts b/Jetcaster/core/domain-testing/build.gradle.kts index 19d9964324..56cdb8f860 100644 --- a/Jetcaster/core/domain-testing/build.gradle.kts +++ b/Jetcaster/core/domain-testing/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2025 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. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -5,10 +22,16 @@ plugins { android { namespace = "com.example.jetcaster.core.domain.testing" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -19,7 +42,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/Jetcaster/core/domain-testing/src/main/java/com/example/jetcaster/core/domain/testing/PreviewData.kt b/Jetcaster/core/domain-testing/src/main/java/com/example/jetcaster/core/domain/testing/PreviewData.kt index de1dfde9ab..c53c717137 100644 --- a/Jetcaster/core/domain-testing/src/main/java/com/example/jetcaster/core/domain/testing/PreviewData.kt +++ b/Jetcaster/core/domain-testing/src/main/java/com/example/jetcaster/core/domain/testing/PreviewData.kt @@ -27,7 +27,7 @@ import java.time.ZoneOffset val PreviewCategories = listOf( CategoryInfo(id = 1, name = "Crime"), CategoryInfo(id = 2, name = "News"), - CategoryInfo(id = 3, name = "Comedy") + CategoryInfo(id = 3, name = "Comedy"), ) val PreviewPodcasts = listOf( @@ -36,14 +36,14 @@ val PreviewPodcasts = listOf( title = "Android Developers Backstage", author = "Android Developers", isSubscribed = true, - lastEpisodeDate = OffsetDateTime.now() + lastEpisodeDate = OffsetDateTime.now(), ), PodcastInfo( uri = "fakeUri://podcast/2", title = "Google Developers podcast", author = "Google Developers", - lastEpisodeDate = OffsetDateTime.now() - ) + lastEpisodeDate = OffsetDateTime.now(), + ), ) val PreviewEpisodes = listOf( @@ -54,21 +54,21 @@ val PreviewEpisodes = listOf( "Tsurkan from the System UI team about... Bubbles!", published = OffsetDateTime.of( 2020, 6, 2, 9, - 27, 0, 0, ZoneOffset.of("-0800") - ) - ) + 27, 0, 0, ZoneOffset.of("-0800"), + ), + ), ) val PreviewPlayerEpisodes = listOf( PlayerEpisode( PreviewPodcasts[0], - PreviewEpisodes[0] - ) + PreviewEpisodes[0], + ), ) val PreviewPodcastEpisodes = listOf( PodcastToEpisodeInfo( podcast = PreviewPodcasts[0], episode = PreviewEpisodes[0], - ) + ), ) diff --git a/Jetcaster/core/domain/build.gradle.kts b/Jetcaster/core/domain/build.gradle.kts index bca3291357..4c28b53356 100644 --- a/Jetcaster/core/domain/build.gradle.kts +++ b/Jetcaster/core/domain/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2025 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. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -6,11 +23,17 @@ plugins { } android { - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() namespace = "com.example.jetcaster.core.domain" defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -21,7 +44,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/di/DomainDiModule.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/di/DomainDiModule.kt index 13aa949a51..7cd2ce606b 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/di/DomainDiModule.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/di/DomainDiModule.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.CoroutineDispatcher object DomainDiModule { @Provides @Singleton - fun provideEpisodePlayer( - @Dispatcher(JetcasterDispatchers.Main) mainDispatcher: CoroutineDispatcher - ): EpisodePlayer = MockEpisodePlayer(mainDispatcher) + fun provideEpisodePlayer(@Dispatcher(JetcasterDispatchers.Main) mainDispatcher: CoroutineDispatcher): EpisodePlayer = + MockEpisodePlayer(mainDispatcher) } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/FilterableCategoriesUseCase.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/FilterableCategoriesUseCase.kt index 8b5f1a9b1c..255f3458ef 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/FilterableCategoriesUseCase.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/FilterableCategoriesUseCase.kt @@ -27,22 +27,19 @@ import kotlinx.coroutines.flow.map /** * Use case for categories that can be used to filter podcasts. */ -class FilterableCategoriesUseCase @Inject constructor( - private val categoryStore: CategoryStore -) { +class FilterableCategoriesUseCase @Inject constructor(private val categoryStore: CategoryStore) { /** * Created a [FilterableCategoriesModel] from the list of categories in [categoryStore]. * @param selectedCategory the currently selected category. If null, the first category * returned by the backing category list will be selected in the returned * FilterableCategoriesModel */ - operator fun invoke(selectedCategory: CategoryInfo?): Flow = - categoryStore.categoriesSortedByPodcastCount() - .map { categories -> - FilterableCategoriesModel( - categories = categories.map { it.asExternalModel() }, - selectedCategory = selectedCategory - ?: categories.firstOrNull()?.asExternalModel() - ) - } + operator fun invoke(selectedCategory: CategoryInfo?): Flow = categoryStore.categoriesSortedByPodcastCount() + .map { categories -> + FilterableCategoriesModel( + categories = categories.map { it.asExternalModel() }, + selectedCategory = selectedCategory + ?: categories.firstOrNull()?.asExternalModel(), + ) + } } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCase.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCase.kt index 7e72545254..344c65b94b 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCase.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCase.kt @@ -32,12 +32,11 @@ class GetLatestFollowedEpisodesUseCase @Inject constructor( private val podcastStore: PodcastStore, ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(): Flow> = - podcastStore.followedPodcastsSortedByLastEpisode() - .flatMapLatest { followedPodcasts -> - episodeStore.episodesInPodcasts( - followedPodcasts.map { it.podcast.uri }, - followedPodcasts.size * 5 - ) - } + operator fun invoke(): Flow> = podcastStore.followedPodcastsSortedByLastEpisode() + .flatMapLatest { followedPodcasts -> + episodeStore.episodesInPodcasts( + followedPodcasts.map { it.podcast.uri }, + followedPodcasts.size * 5, + ) + } } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCase.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCase.kt index 97620a63c2..dbfbba91b1 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCase.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCase.kt @@ -30,9 +30,7 @@ import kotlinx.coroutines.flow.flowOf /** * A use case which returns top podcasts and matching episodes in a given [Category]. */ -class PodcastCategoryFilterUseCase @Inject constructor( - private val categoryStore: CategoryStore -) { +class PodcastCategoryFilterUseCase @Inject constructor(private val categoryStore: CategoryStore) { operator fun invoke(category: CategoryInfo?): Flow { if (category == null) { return flowOf(PodcastCategoryFilterResult()) @@ -40,19 +38,19 @@ class PodcastCategoryFilterUseCase @Inject constructor( val recentPodcastsFlow = categoryStore.podcastsInCategorySortedByPodcastCount( category.id, - limit = 10 + limit = 10, ) val episodesFlow = categoryStore.episodesFromPodcastsInCategory( category.id, - limit = 20 + limit = 20, ) // Combine our flows and collect them into the view state StateFlow return combine(recentPodcastsFlow, episodesFlow) { topPodcasts, episodes -> PodcastCategoryFilterResult( topPodcasts = topPodcasts.map { it.asExternalModel() }, - episodes = episodes.map { it.asPodcastToEpisodeInfo() } + episodes = episodes.map { it.asPodcastToEpisodeInfo() }, ) } } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/CategoryInfo.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/CategoryInfo.kt index 833ada892c..f462e4cb90 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/CategoryInfo.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/CategoryInfo.kt @@ -18,15 +18,11 @@ package com.example.jetcaster.core.model import com.example.jetcaster.core.data.database.model.Category -data class CategoryInfo( - val id: Long, - val name: String -) +data class CategoryInfo(val id: Long, val name: String) const val CategoryTechnology = "Technology" -fun Category.asExternalModel() = - CategoryInfo( - id = id, - name = name - ) +fun Category.asExternalModel() = CategoryInfo( + id = id, + name = name, +) diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/EpisodeInfo.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/EpisodeInfo.kt index 8b8757bb8c..8bf70ed115 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/EpisodeInfo.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/EpisodeInfo.kt @@ -25,21 +25,36 @@ import java.time.OffsetDateTime */ data class EpisodeInfo( val uri: String = "", + val podcastUri: String = "", val title: String = "", val subTitle: String = "", val summary: String = "", val author: String = "", val published: OffsetDateTime = OffsetDateTime.MIN, val duration: Duration? = null, + val mediaUrls: List = emptyList(), ) -fun Episode.asExternalModel(): EpisodeInfo = - EpisodeInfo( - uri = uri, - title = title, - subTitle = subtitle ?: "", - summary = summary ?: "", - author = author ?: "", - published = published, - duration = duration, - ) +fun Episode.asExternalModel(): EpisodeInfo = EpisodeInfo( + uri = uri, + podcastUri = podcastUri, + title = title, + subTitle = subtitle ?: "", + summary = summary ?: "", + author = author ?: "", + published = published, + duration = duration, + mediaUrls = mediaUrls, +) + +fun EpisodeInfo.asDaoModel(): Episode = Episode( + uri = uri, + title = title, + subtitle = subTitle, + summary = summary, + author = author, + published = published, + duration = duration, + podcastUri = podcastUri, + mediaUrls = mediaUrls, +) diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/FilterableCategoriesModel.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/FilterableCategoriesModel.kt index 4cca646940..a359437924 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/FilterableCategoriesModel.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/FilterableCategoriesModel.kt @@ -19,9 +19,6 @@ package com.example.jetcaster.core.model /** * Model holding a list of categories and a selected category in the collection */ -data class FilterableCategoriesModel( - val categories: List = emptyList(), - val selectedCategory: CategoryInfo? = null -) { +data class FilterableCategoriesModel(val categories: List = emptyList(), val selectedCategory: CategoryInfo? = null) { val isEmpty = categories.isEmpty() || selectedCategory == null } diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/LibraryInfo.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/LibraryInfo.kt index 5731b07f80..d268ea97c4 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/LibraryInfo.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/LibraryInfo.kt @@ -16,6 +16,4 @@ package com.example.jetcaster.core.model -data class LibraryInfo( - val episodes: List = emptyList() -) : List by episodes +data class LibraryInfo(val episodes: List = emptyList()) : List by episodes diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastCategoryFilterResult.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastCategoryFilterResult.kt index c0e6761ed0..871780034d 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastCategoryFilterResult.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastCategoryFilterResult.kt @@ -21,5 +21,5 @@ package com.example.jetcaster.core.model */ data class PodcastCategoryFilterResult( val topPodcasts: List = emptyList(), - val episodes: List = emptyList() + val episodes: List = emptyList(), ) diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastInfo.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastInfo.kt index 6f03fe56a7..2150c06ec6 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastInfo.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastInfo.kt @@ -33,17 +33,15 @@ data class PodcastInfo( val lastEpisodeDate: OffsetDateTime? = null, ) -fun Podcast.asExternalModel(): PodcastInfo = - PodcastInfo( - uri = this.uri, - title = this.title, - author = this.author ?: "", - imageUrl = this.imageUrl ?: "", - description = this.description ?: "", - ) +fun Podcast.asExternalModel(): PodcastInfo = PodcastInfo( + uri = this.uri, + title = this.title, + author = this.author ?: "", + imageUrl = this.imageUrl ?: "", + description = this.description ?: "", +) -fun PodcastWithExtraInfo.asExternalModel(): PodcastInfo = - this.podcast.asExternalModel().copy( - isSubscribed = isFollowed, - lastEpisodeDate = lastEpisodeDate, - ) +fun PodcastWithExtraInfo.asExternalModel(): PodcastInfo = this.podcast.asExternalModel().copy( + isSubscribed = isFollowed, + lastEpisodeDate = lastEpisodeDate, +) diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastToEpisodeInfo.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastToEpisodeInfo.kt index a7e458cad1..2b69d1fe4f 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastToEpisodeInfo.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/model/PodcastToEpisodeInfo.kt @@ -18,13 +18,9 @@ package com.example.jetcaster.core.model import com.example.jetcaster.core.data.database.model.EpisodeToPodcast -data class PodcastToEpisodeInfo( - val episode: EpisodeInfo, - val podcast: PodcastInfo, -) +data class PodcastToEpisodeInfo(val episode: EpisodeInfo, val podcast: PodcastInfo) -fun EpisodeToPodcast.asPodcastToEpisodeInfo(): PodcastToEpisodeInfo = - PodcastToEpisodeInfo( - episode = episode.asExternalModel(), - podcast = podcast.asExternalModel(), - ) +fun EpisodeToPodcast.asPodcastToEpisodeInfo(): PodcastToEpisodeInfo = PodcastToEpisodeInfo( + episode = episode.asExternalModel(), + podcast = podcast.asExternalModel(), +) diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt index eb88fdef51..c01d64c61c 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/EpisodePlayer.kt @@ -53,8 +53,8 @@ interface EpisodePlayer { fun addToQueue(episode: PlayerEpisode) /* - * Flushes the queue - */ + * Flushes the queue + */ fun removeAllFromQueue() /** diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt index f94a552b5b..ab7c17c9fb 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/MockEpisodePlayer.kt @@ -32,9 +32,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -class MockEpisodePlayer( - private val mainDispatcher: CoroutineDispatcher -) : EpisodePlayer { +class MockEpisodePlayer(private val mainDispatcher: CoroutineDispatcher) : EpisodePlayer { private val _playerState = MutableStateFlow(EpisodePlayerState()) private val _currentEpisode = MutableStateFlow(null) @@ -54,14 +52,14 @@ class MockEpisodePlayer( queue, isPlaying, timeElapsed, - _playerSpeed + _playerSpeed, ) { currentEpisode, queue, isPlaying, timeElapsed, playerSpeed -> EpisodePlayerState( currentEpisode = currentEpisode, queue = queue, isPlaying = isPlaying, timeElapsed = timeElapsed, - playbackSpeed = playerSpeed + playbackSpeed = playerSpeed, ) }.catch { // TODO handle error state @@ -218,13 +216,8 @@ class MockEpisodePlayer( } // Used to enable property delegation -private operator fun MutableStateFlow.setValue( - thisObj: Any?, - property: KProperty<*>, - value: T -) { +private operator fun MutableStateFlow.setValue(thisObj: Any?, property: KProperty<*>, value: T) { this.value = value } -private operator fun MutableStateFlow.getValue(thisObj: Any?, property: KProperty<*>): T = - this.value +private operator fun MutableStateFlow.getValue(thisObj: Any?, property: KProperty<*>): T = this.value diff --git a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/model/PlayerEpisode.kt b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/model/PlayerEpisode.kt index 8ebd257a88..adc1a21c09 100644 --- a/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/model/PlayerEpisode.kt +++ b/Jetcaster/core/domain/src/main/java/com/example/jetcaster/core/player/model/PlayerEpisode.kt @@ -35,6 +35,7 @@ data class PlayerEpisode( val author: String = "", val summary: String = "", val podcastImageUrl: String = "", + val mediaUrls: List = emptyList(), ) { constructor(podcastInfo: PodcastInfo, episodeInfo: EpisodeInfo) : this( title = episodeInfo.title, @@ -45,19 +46,20 @@ data class PlayerEpisode( author = episodeInfo.author, summary = episodeInfo.summary, podcastImageUrl = podcastInfo.imageUrl, - uri = episodeInfo.uri + uri = episodeInfo.uri, + mediaUrls = episodeInfo.mediaUrls, ) } -fun EpisodeToPodcast.toPlayerEpisode(): PlayerEpisode = - PlayerEpisode( - uri = episode.uri, - title = episode.title, - subTitle = episode.subtitle ?: "", - published = episode.published, - duration = episode.duration, - podcastName = podcast.title, - author = episode.author ?: podcast.author ?: "", - summary = episode.summary ?: "", - podcastImageUrl = podcast.imageUrl ?: "", - ) +fun EpisodeToPodcast.toPlayerEpisode(): PlayerEpisode = PlayerEpisode( + uri = episode.uri, + title = episode.title, + subTitle = episode.subtitle ?: "", + published = episode.published, + duration = episode.duration, + podcastName = podcast.title, + author = episode.author ?: podcast.author ?: "", + summary = episode.summary ?: "", + podcastImageUrl = podcast.imageUrl ?: "", + mediaUrls = episode.mediaUrls, +) diff --git a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/FilterableCategoriesUseCaseTest.kt b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/FilterableCategoriesUseCaseTest.kt index 4431dc29f3..71ef508c2c 100644 --- a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/FilterableCategoriesUseCaseTest.kt +++ b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/FilterableCategoriesUseCaseTest.kt @@ -36,7 +36,7 @@ class FilterableCategoriesUseCaseTest { ) val useCase = FilterableCategoriesUseCase( - categoryStore = categoriesStore + categoryStore = categoriesStore, ) @Before @@ -49,7 +49,7 @@ class FilterableCategoriesUseCaseTest { val filterableCategories = useCase(null).first() assertEquals( filterableCategories.categories[0], - filterableCategories.selectedCategory + filterableCategories.selectedCategory, ) } @@ -59,7 +59,7 @@ class FilterableCategoriesUseCaseTest { val filterableCategories = useCase(selectedCategory.asExternalModel()).first() assertEquals( selectedCategory.asExternalModel(), - filterableCategories.selectedCategory + filterableCategories.selectedCategory, ) } } diff --git a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCaseTest.kt b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCaseTest.kt index c2a3133ed1..11b2d31535 100644 --- a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCaseTest.kt +++ b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/GetLatestFollowedEpisodesUseCaseTest.kt @@ -19,6 +19,7 @@ package com.example.jetcaster.core.domain import com.example.jetcaster.core.data.database.model.Episode import com.example.jetcaster.core.data.testing.repository.TestEpisodeStore import com.example.jetcaster.core.data.testing.repository.TestPodcastStore +import java.time.Duration import java.time.OffsetDateTime import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -32,7 +33,7 @@ class GetLatestFollowedEpisodesUseCaseTest { val useCase = GetLatestFollowedEpisodesUseCase( episodeStore = episodeStore, - podcastStore = podcastStore + podcastStore = podcastStore, ) val testEpisodes = listOf( @@ -40,20 +41,35 @@ class GetLatestFollowedEpisodesUseCaseTest { uri = "", podcastUri = testPodcasts[0].podcast.uri, title = "title1", - published = OffsetDateTime.MIN + published = OffsetDateTime.MIN, + subtitle = "subtitle1", + summary = "summary1", + author = "author1", + duration = Duration.ofMinutes(1), + mediaUrls = listOf("Url1"), ), Episode( uri = "", podcastUri = testPodcasts[0].podcast.uri, title = "title2", - published = OffsetDateTime.now() + published = OffsetDateTime.now(), + subtitle = "subtitle2", + summary = "summary2", + author = "author2", + duration = Duration.ofMinutes(1), + mediaUrls = listOf("Url1"), ), Episode( uri = "", podcastUri = testPodcasts[1].podcast.uri, title = "title3", - published = OffsetDateTime.MAX - ) + published = OffsetDateTime.MAX, + subtitle = "subtitle3", + summary = "summary3", + author = "author3", + duration = Duration.ofMinutes(1), + mediaUrls = listOf("Url1"), + ), ) @Test @@ -91,8 +107,7 @@ class GetLatestFollowedEpisodesUseCaseTest { } podcastStore.togglePodcastFollowed(testPodcasts[0].podcast.uri) - result.first().zipWithNext { - ep1, ep2 -> + result.first().zipWithNext { ep1, ep2 -> ep1.episode.published > ep2.episode.published }.all { it } } diff --git a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCaseTest.kt b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCaseTest.kt index 7568d175b8..e8ac3f4fc9 100644 --- a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCaseTest.kt +++ b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/PodcastCategoryFilterUseCaseTest.kt @@ -24,6 +24,7 @@ import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo import com.example.jetcaster.core.data.testing.repository.TestCategoryStore import com.example.jetcaster.core.model.asExternalModel import com.example.jetcaster.core.model.asPodcastToEpisodeInfo +import java.time.Duration import java.time.OffsetDateTime import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -40,13 +41,18 @@ class PodcastCategoryFilterUseCaseTest { "", "", "Episode 1", - published = OffsetDateTime.now() + published = OffsetDateTime.now(), + subtitle = "subtitle1", + summary = "summary1", + author = "author1", + duration = Duration.ofMinutes(1), + mediaUrls = listOf("Url1"), ) _podcasts = listOf( Podcast( uri = "", - title = "Podcast 1" - ) + title = "Podcast 1", + ), ) }, EpisodeToPodcast().apply { @@ -54,13 +60,18 @@ class PodcastCategoryFilterUseCaseTest { "", "", "Episode 2", - published = OffsetDateTime.now() + published = OffsetDateTime.now(), + subtitle = "subtitle2", + summary = "summary2", + author = "author2", + duration = Duration.ofMinutes(1), + mediaUrls = listOf("Url1"), ) _podcasts = listOf( Podcast( uri = "", - title = "Podcast 2" - ) + title = "Podcast 2", + ), ) }, EpisodeToPodcast().apply { @@ -68,20 +79,25 @@ class PodcastCategoryFilterUseCaseTest { "", "", "Episode 3", - published = OffsetDateTime.now() + published = OffsetDateTime.now(), + subtitle = "subtitle3", + summary = "summary3", + author = "author2", + duration = Duration.ofMinutes(1), + mediaUrls = listOf("Url1"), ) _podcasts = listOf( Podcast( uri = "", - title = "Podcast 3" - ) + title = "Podcast 3", + ), ) - } + }, ) private val testCategory = Category(1, "Technology") val useCase = PodcastCategoryFilterUseCase( - categoryStore = categoriesStore + categoryStore = categoriesStore, ) @Test @@ -106,11 +122,11 @@ class PodcastCategoryFilterUseCaseTest { val result = resultFlow.first() assertEquals( testPodcasts.map { it.asExternalModel() }, - result.topPodcasts + result.topPodcasts, ) assertEquals( testEpisodeToPodcast.map { it.asPodcastToEpisodeInfo() }, - result.episodes + result.episodes, ) } @@ -120,11 +136,11 @@ class PodcastCategoryFilterUseCaseTest { categoriesStore.setEpisodesFromPodcast( testCategory.id, - List(8) { testEpisodeToPodcast }.flatten() + List(8) { testEpisodeToPodcast }.flatten(), ) categoriesStore.setPodcastsInCategory( testCategory.id, - List(4) { testPodcasts }.flatten() + List(4) { testPodcasts }.flatten(), ) val result = resultFlow.first() diff --git a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/player/MockEpisodePlayerTest.kt b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/player/MockEpisodePlayerTest.kt index 96c91a66ce..e43e47c972 100644 --- a/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/player/MockEpisodePlayerTest.kt +++ b/Jetcaster/core/domain/src/test/kotlin/com/example/jetcaster/core/domain/player/MockEpisodePlayerTest.kt @@ -36,15 +36,15 @@ class MockEpisodePlayerTest { private val testEpisodes = listOf( PlayerEpisode( uri = "uri1", - duration = Duration.ofSeconds(60) + duration = Duration.ofSeconds(60), ), PlayerEpisode( uri = "uri2", - duration = Duration.ofSeconds(60) + duration = Duration.ofSeconds(60), ), PlayerEpisode( uri = "uri3", - duration = Duration.ofSeconds(60) + duration = Duration.ofSeconds(60), ), ) @@ -53,7 +53,7 @@ class MockEpisodePlayerTest { val playSpeed = Duration.ofSeconds(2) val currEpisode = PlayerEpisode( uri = "currentEpisode", - duration = Duration.ofSeconds(60) + duration = Duration.ofSeconds(60), ) mockEpisodePlayer.currentEpisode = currEpisode mockEpisodePlayer.playerSpeed = playSpeed @@ -69,7 +69,7 @@ class MockEpisodePlayerTest { val duration = Duration.ofSeconds(60) val currEpisode = PlayerEpisode( uri = "currentEpisode", - duration = duration + duration = duration, ) mockEpisodePlayer.currentEpisode = currEpisode testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } @@ -85,7 +85,7 @@ class MockEpisodePlayerTest { val duration = Duration.ofSeconds(60) val currEpisode = PlayerEpisode( uri = "currentEpisode", - duration = duration + duration = duration, ) mockEpisodePlayer.currentEpisode = currEpisode @@ -101,18 +101,18 @@ class MockEpisodePlayerTest { val duration = Duration.ofSeconds(60) val currEpisode = PlayerEpisode( uri = "currentEpisode", - duration = duration + duration = duration, ) val firstEpisodeFromList = PlayerEpisode( uri = "firstEpisodeFromList", - duration = duration + duration = duration, ) val secondEpisodeFromList = PlayerEpisode( uri = "secondEpisodeFromList", - duration = duration + duration = duration, ) val episodeListToBeAddedToTheQueue: List = listOf( - firstEpisodeFromList, secondEpisodeFromList + firstEpisodeFromList, secondEpisodeFromList, ) mockEpisodePlayer.currentEpisode = currEpisode @@ -154,7 +154,7 @@ class MockEpisodePlayerTest { fun whenNext_queueIsNotEmpty_removeFromQueue() = runTest(testDispatcher) { mockEpisodePlayer.currentEpisode = PlayerEpisode( uri = "currentEpisode", - duration = Duration.ofSeconds(60) + duration = Duration.ofSeconds(60), ) testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } @@ -174,7 +174,7 @@ class MockEpisodePlayerTest { fun whenNext_queueIsNotEmpty_notRemovedFromQueue() = runTest(testDispatcher) { mockEpisodePlayer.currentEpisode = PlayerEpisode( uri = "currentEpisode", - duration = Duration.ofSeconds(60) + duration = Duration.ofSeconds(60), ) testEpisodes.forEach { mockEpisodePlayer.addToQueue(it) } diff --git a/Jetcaster/glancewidget/build.gradle.kts b/Jetcaster/glancewidget/build.gradle.kts index a29291be40..bebf465b9c 100644 --- a/Jetcaster/glancewidget/build.gradle.kts +++ b/Jetcaster/glancewidget/build.gradle.kts @@ -1,3 +1,20 @@ +/* + * Copyright 2025 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. + */ + + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -6,10 +23,16 @@ plugins { android { namespace = "com.example.jetcaster.glancewidget" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt index c932bbf1e2..3840e7c006 100644 --- a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt +++ b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidget.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Android Open Source Project + * Copyright 2024-2025 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. @@ -78,12 +78,7 @@ class JetcasterAppWidgetReceiver : GlanceAppWidgetReceiver() { get() = JetcasterAppWidget() } -data class JetcasterAppWidgetViewState( - val episodeTitle: String, - val podcastTitle: String, - val isPlaying: Boolean, - val albumArtUri: String, -) +data class JetcasterAppWidgetViewState(val episodeTitle: String, val podcastTitle: String, val isPlaying: Boolean, val albumArtUri: String) private object Sizes { val short = 72.dp @@ -117,14 +112,13 @@ class JetcasterAppWidget : GlanceAppWidget() { get() = SizeMode.Exact override suspend fun provideGlance(context: Context, id: GlanceId) { - val testState = JetcasterAppWidgetViewState( episodeTitle = "100 - Android 15 DP 1, Stable Studio Iguana, Cloud Photo Picker, and more!", podcastTitle = "Now in Android", isPlaying = false, albumArtUri = "https://static.libsyn.com/p/assets/9/f/f/3/" + - "9ff3cb5dc6cfb3e2e5bbc093207a2619/NIA000_PodcastThumbnail.png" + "9ff3cb5dc6cfb3e2e5bbc093207a2619/NIA000_PodcastThumbnail.png", ) provideContent { @@ -138,7 +132,7 @@ class JetcasterAppWidget : GlanceAppWidget() { SizeBucket.Narrow -> Widget( iconSize = Sizes.medium, imageUri = artUri, - playPauseIcon = playPauseIcon + playPauseIcon = playPauseIcon, ) SizeBucket.Normal -> WidgetUiNormal( @@ -146,13 +140,13 @@ class JetcasterAppWidget : GlanceAppWidget() { title = testState.episodeTitle, subtitle = testState.podcastTitle, imageUri = artUri, - playPauseIcon = playPauseIcon + playPauseIcon = playPauseIcon, ) SizeBucket.NarrowShort -> Widget( iconSize = Sizes.condensed, imageUri = artUri, - playPauseIcon = playPauseIcon + playPauseIcon = playPauseIcon, ) SizeBucket.NormalShort -> WidgetUiNormal( @@ -160,7 +154,7 @@ class JetcasterAppWidget : GlanceAppWidget() { title = testState.episodeTitle, subtitle = testState.podcastTitle, imageUri = artUri, - playPauseIcon = playPauseIcon + playPauseIcon = playPauseIcon, ) } } @@ -169,17 +163,11 @@ class JetcasterAppWidget : GlanceAppWidget() { } @Composable -private fun WidgetUiNormal( - title: String, - subtitle: String, - imageUri: Uri, - playPauseIcon: PlayPauseIcon, - iconSize: Dp, -) { - +private fun WidgetUiNormal(title: String, subtitle: String, imageUri: Uri, playPauseIcon: PlayPauseIcon, iconSize: Dp) { Scaffold { Row( - GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.Vertical.CenterVertically + GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.Vertical.CenterVertically, ) { AlbumArt(imageUri, GlanceModifier.size(iconSize)) PodcastText(title, subtitle, modifier = GlanceModifier.padding(16.dp).defaultWeight()) @@ -189,15 +177,12 @@ private fun WidgetUiNormal( } @Composable -private fun Widget( - iconSize: Dp, - imageUri: Uri, - playPauseIcon: PlayPauseIcon, -) { - Scaffold(titleBar = {} /* title bar will be optional in scaffold in glance 1.1.0-beta3*/) { +private fun Widget(iconSize: Dp, imageUri: Uri, playPauseIcon: PlayPauseIcon) { + /* title bar will be optional in scaffold in glance 1.1.0-beta3*/ + Scaffold(titleBar = {}) { Row( modifier = GlanceModifier.fillMaxSize(), - verticalAlignment = Alignment.Vertical.CenterVertically + verticalAlignment = Alignment.Vertical.CenterVertically, ) { AlbumArt(imageUri, GlanceModifier.size(iconSize)) Spacer(GlanceModifier.defaultWeight()) @@ -214,10 +199,7 @@ private fun WidgetUiInvalidSize() { } @Composable -private fun AlbumArt( - imageUri: Uri, - modifier: GlanceModifier = GlanceModifier -) { +private fun AlbumArt(imageUri: Uri, modifier: GlanceModifier = GlanceModifier) { WidgetAsyncImage(uri = imageUri, contentDescription = null, modifier = modifier) } @@ -232,7 +214,7 @@ fun PodcastText(title: String, subtitle: String, modifier: GlanceModifier = Glan style = TextStyle( fontSize = 16.sp, fontWeight = FontWeight.Medium, - color = fgColor + color = fgColor, ), maxLines = 2, ) @@ -248,7 +230,7 @@ fun PodcastText(title: String, subtitle: String, modifier: GlanceModifier = Glan style = TextStyle( fontSize = 12.sp, fontWeight = FontWeight.Medium, - color = fgColor + color = fgColor, ), maxLines = 1, ) @@ -257,11 +239,7 @@ fun PodcastText(title: String, subtitle: String, modifier: GlanceModifier = Glan } @Composable -private fun PlayPauseButton( - modifier: GlanceModifier = GlanceModifier.size(Sizes.normal), - state: PlayPauseIcon, - onClick: () -> Unit -) { +private fun PlayPauseButton(modifier: GlanceModifier = GlanceModifier.size(Sizes.normal), state: PlayPauseIcon, onClick: () -> Unit) { val (iconRes: Int, description: Int) = when (state) { PlayPauseIcon.Play -> R.drawable.outline_play_arrow_24 to R.string.content_description_play PlayPauseIcon.Pause -> R.drawable.outline_pause_24 to R.string.content_description_pause @@ -274,7 +252,7 @@ private fun PlayPauseButton( modifier = modifier, imageProvider = provider, contentDescription = contentDescription, - onClick = onClick + onClick = onClick, ) } @@ -284,11 +262,7 @@ enum class PlayPauseIcon { Play, Pause } * Uses Coil to load images. */ @Composable -private fun WidgetAsyncImage( - uri: Uri, - contentDescription: String?, - modifier: GlanceModifier = GlanceModifier -) { +private fun WidgetAsyncImage(uri: Uri, contentDescription: String?, modifier: GlanceModifier = GlanceModifier) { var bitmap by remember { mutableStateOf(null) } val context = LocalContext.current val scope = rememberCoroutineScope() @@ -316,7 +290,7 @@ private fun WidgetAsyncImage( provider = ImageProvider(bitmap), contentDescription = contentDescription, contentScale = ContentScale.FillBounds, - modifier = modifier.cornerRadius(12.dp) // TODO: confirm radius with design + modifier = modifier.cornerRadius(12.dp), // TODO: confirm radius with design ) } } diff --git a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidgetPreview.kt b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidgetPreview.kt index 8e26736f5b..186e4495b7 100644 --- a/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidgetPreview.kt +++ b/Jetcaster/glancewidget/src/main/java/com/example/jetcaster/glancewidget/JetcasterAppWidgetPreview.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Android Open Source Project + * Copyright 2024-2025 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. @@ -56,7 +56,6 @@ private object SizesPreview { * In a real application, this would be called whenever the widget's state changes. */ fun updateWidgetPreview(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { CoroutineScope(Dispatchers.IO).launch { try { @@ -67,7 +66,7 @@ fun updateWidgetPreview(context: Context) { AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN, JetcasterAppWidgetPreview().compose( context, - size = DpSize(160.dp, 64.dp) + size = DpSize(160.dp, 64.dp), ), ) } catch (e: Exception) { @@ -82,7 +81,6 @@ class JetcasterAppWidgetPreview : GlanceAppWidget() { get() = SizeMode.Exact override suspend fun provideGlance(context: Context, id: GlanceId) { - provideContent { GlanceTheme { Widget() @@ -93,23 +91,22 @@ class JetcasterAppWidgetPreview : GlanceAppWidget() { @Composable private fun Widget() { - Scaffold { Row( modifier = GlanceModifier.fillMaxSize(), - verticalAlignment = Alignment.Vertical.CenterVertically + verticalAlignment = Alignment.Vertical.CenterVertically, ) { Image( modifier = GlanceModifier.wrapContentSize().size(SizesPreview.medium), provider = ImageProvider(R.drawable.widget_preview_thumbnail), - contentDescription = "" + contentDescription = "", ) Spacer(GlanceModifier.defaultWeight()) SquareIconButton( modifier = GlanceModifier.size(SizesPreview.medium), imageProvider = ImageProvider(R.drawable.outline_play_arrow_24), contentDescription = "", - onClick = { } + onClick = { }, ) } } diff --git a/Jetcaster/gradle/libs.versions.toml b/Jetcaster/gradle/libs.versions.toml index d40115dfaf..c04ecb4911 100644 --- a/Jetcaster/gradle/libs.versions.toml +++ b/Jetcaster/gradle/libs.versions.toml @@ -44,6 +44,7 @@ kotlinx-serialization-json = "1.8.1" kotlinx_immutable = "0.3.8" ksp = "2.1.20-2.0.1" maps-compose = "6.6.0" +media3 = "1.6.1" # @keep minSdk = "21" okhttp = "4.12.0" @@ -66,7 +67,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity-compose" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-compose-animation = { module = "androidx.compose.animation:animation" } -androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-bom = { module = "androidx.compose:compose-bom-alpha", version.ref = "androidx-compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } @@ -103,6 +104,7 @@ androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-v androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle-compose" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } +androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "media3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" } androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" } @@ -158,6 +160,8 @@ roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose" roborazzi-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } rometools-modules = { module = "com.rometools:rome-modules", version.ref = "rome" } rometools-rome = { module = "com.rometools:rome", version.ref = "rome" } +androidx-media3-session = {module = "androidx.media3:media3-session",version.ref = "media3"} +androidx-media3-exoplayer = {module = "androidx.media3:media3-exoplayer", version.ref = "media3"} [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/Jetcaster/mobile/build.gradle.kts b/Jetcaster/mobile/build.gradle.kts index 044fc24423..8121ed9239 100644 --- a/Jetcaster/mobile/build.gradle.kts +++ b/Jetcaster/mobile/build.gradle.kts @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,6 +14,7 @@ * limitations under the License. */ + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -22,15 +23,23 @@ plugins { alias(libs.plugins.compose) } - android { - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() namespace = "com.example.jetcaster" defaultConfig { applicationId = "com.example.jetcaster" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -52,7 +61,6 @@ android { buildTypes { getByName("debug") { - } getByName("release") { @@ -60,7 +68,7 @@ android { signingConfig = signingConfigs.getByName("release") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/JetcasterApplication.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/JetcasterApplication.kt index 120187d2d5..af37480202 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/JetcasterApplication.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/JetcasterApplication.kt @@ -26,7 +26,9 @@ import javax.inject.Inject * Application which sets up our dependency [Graph] with a context. */ @HiltAndroidApp -class JetcasterApplication : Application(), ImageLoaderFactory { +class JetcasterApplication : + Application(), + ImageLoaderFactory { @Inject lateinit var imageLoader: ImageLoader diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt index 9d22c05fbf..f67a0295ff 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Android Open Source Project + * Copyright 2020-2025 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. @@ -14,16 +14,23 @@ * limitations under the License. */ +@file:OptIn(ExperimentalSharedTransitionApi::class) + package com.example.jetcaster.ui +import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.scaleOut import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.res.stringResource import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -32,34 +39,45 @@ import com.example.jetcaster.R import com.example.jetcaster.ui.home.MainScreen import com.example.jetcaster.ui.player.PlayerScreen -@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -fun JetcasterApp( - displayFeatures: List, - appState: JetcasterAppState = rememberJetcasterAppState() -) { +@OptIn(ExperimentalSharedTransitionApi::class) +fun JetcasterApp(displayFeatures: List, appState: JetcasterAppState = rememberJetcasterAppState()) { val adaptiveInfo = currentWindowAdaptiveInfo() if (appState.isOnline) { - NavHost( - navController = appState.navController, - startDestination = Screen.Home.route, - popExitTransition = { scaleOut(targetScale = 0.9f) }, - popEnterTransition = { EnterTransition.None } - ) { - composable(Screen.Home.route) { backStackEntry -> - MainScreen( - windowSizeClass = adaptiveInfo.windowSizeClass, - navigateToPlayer = { episode -> - appState.navigateToPlayer(episode.uri, backStackEntry) + SharedTransitionLayout { + CompositionLocalProvider( + LocalSharedTransitionScope provides this, + ) { + NavHost( + navController = appState.navController, + startDestination = Screen.Home.route, + popExitTransition = { scaleOut(targetScale = 0.9f) }, + popEnterTransition = { EnterTransition.None }, + ) { + composable(Screen.Home.route) { backStackEntry -> + CompositionLocalProvider( + LocalAnimatedVisibilityScope provides this, + ) { + MainScreen( + windowSizeClass = adaptiveInfo.windowSizeClass, + navigateToPlayer = { episode -> + appState.navigateToPlayer(episode.uri, backStackEntry) + }, + ) + } } - ) - } - composable(Screen.Player.route) { - PlayerScreen( - windowSizeClass = adaptiveInfo.windowSizeClass, - displayFeatures = displayFeatures, - onBackPress = appState::navigateBack - ) + composable(Screen.Player.route) { + CompositionLocalProvider( + LocalAnimatedVisibilityScope provides this, + ) { + PlayerScreen( + windowSizeClass = adaptiveInfo.windowSizeClass, + displayFeatures = displayFeatures, + onBackPress = appState::navigateBack, + ) + } + } + } } } } else { @@ -77,6 +95,9 @@ fun OfflineDialog(onRetry: () -> Unit) { TextButton(onClick = onRetry) { Text(stringResource(R.string.retry_label)) } - } + }, ) } + +val LocalAnimatedVisibilityScope = compositionLocalOf { null } +val LocalSharedTransitionScope = compositionLocalOf { null } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterAppState.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterAppState.kt index ee938066a7..a3f5645adf 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterAppState.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterAppState.kt @@ -55,17 +55,12 @@ sealed class Screen(val route: String) { } @Composable -fun rememberJetcasterAppState( - navController: NavHostController = rememberNavController(), - context: Context = LocalContext.current -) = remember(navController, context) { - JetcasterAppState(navController, context) -} +fun rememberJetcasterAppState(navController: NavHostController = rememberNavController(), context: Context = LocalContext.current) = + remember(navController, context) { + JetcasterAppState(navController, context) + } -class JetcasterAppState( - val navController: NavHostController, - private val context: Context -) { +class JetcasterAppState(val navController: NavHostController, private val context: Context) { var isOnline by mutableStateOf(checkIfOnline()) private set @@ -111,5 +106,4 @@ class JetcasterAppState( * * This is used to de-duplicate navigation events. */ -private fun NavBackStackEntry.lifecycleIsResumed() = - this.lifecycle.currentState == Lifecycle.State.RESUMED +private fun NavBackStackEntry.lifecycleIsResumed() = this.lifecycle.currentState == Lifecycle.State.RESUMED diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/MainActivity.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/MainActivity.kt index c038c5d506..d7cfca1a73 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/MainActivity.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/MainActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Android Open Source Project + * Copyright 2020-2025 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. @@ -37,7 +37,7 @@ class MainActivity : ComponentActivity() { JetcasterTheme { JetcasterApp( - displayFeatures + displayFeatures, ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt index fe597ccc48..79e6f9095e 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt @@ -14,12 +14,9 @@ * limitations under the License. */ -@file:OptIn(ExperimentalFoundationApi::class) - package com.example.jetcaster.ui.home import androidx.activity.compose.BackHandler -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -37,21 +34,20 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PageSize -import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.LibraryMusic import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FloatingToolbarColors +import androidx.compose.material3.HorizontalFloatingToolbar import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme @@ -61,10 +57,6 @@ import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState 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.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.Posture @@ -80,6 +72,8 @@ import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator import androidx.compose.material3.adaptive.occludingVerticalHingeBounds import androidx.compose.material3.adaptive.separatingVerticalHingeBounds +import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel +import androidx.compose.material3.carousel.rememberCarouselState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -92,7 +86,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -138,7 +134,6 @@ private fun ThreePaneScaffoldNavigator.isMainPaneHidden(): Boolean = * Copied from `calculatePaneScaffoldDirective()` in [PaneScaffoldDirective], with modifications to * only show 1 pane horizontally if either width or height size class is compact. */ -@OptIn(ExperimentalMaterial3AdaptiveApi::class) fun calculateScaffoldDirective( windowAdaptiveInfo: WindowAdaptiveInfo, verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating, @@ -186,28 +181,22 @@ fun calculateScaffoldDirective( maxVerticalPartitions, horizontalSpacerSize, defaultPanePreferredWidth, - getExcludedVerticalBounds(windowAdaptiveInfo.windowPosture, verticalHingePolicy) + getExcludedVerticalBounds(windowAdaptiveInfo.windowPosture, verticalHingePolicy), ) } /** * Copied from `getExcludedVerticalBounds()` in [PaneScaffoldDirective] since it is private. */ -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -private fun getExcludedVerticalBounds(posture: Posture, hingePolicy: HingePolicy): List = - when (hingePolicy) { - HingePolicy.AvoidSeparating -> posture.separatingVerticalHingeBounds - HingePolicy.AvoidOccluding -> posture.occludingVerticalHingeBounds - HingePolicy.AlwaysAvoid -> posture.allVerticalHingeBounds - else -> emptyList() - } +private fun getExcludedVerticalBounds(posture: Posture, hingePolicy: HingePolicy): List = when (hingePolicy) { + HingePolicy.AvoidSeparating -> posture.separatingVerticalHingeBounds + HingePolicy.AvoidOccluding -> posture.occludingVerticalHingeBounds + HingePolicy.AlwaysAvoid -> posture.allVerticalHingeBounds + else -> emptyList() +} @Composable -fun MainScreen( - windowSizeClass: WindowSizeClass, - navigateToPlayer: (EpisodeInfo) -> Unit, - viewModel: HomeViewModel = hiltViewModel(), -) { +fun MainScreen(windowSizeClass: WindowSizeClass, navigateToPlayer: (EpisodeInfo) -> Unit, viewModel: HomeViewModel = hiltViewModel()) { val homeScreenUiState by viewModel.state.collectAsStateWithLifecycle() val uiState = homeScreenUiState Box { @@ -215,7 +204,7 @@ fun MainScreen( uiState = uiState, windowSizeClass = windowSizeClass, navigateToPlayer = navigateToPlayer, - viewModel = viewModel + viewModel = viewModel, ) if (uiState.errorMessage != null) { @@ -230,11 +219,11 @@ private fun HomeScreenError(onRetry: () -> Unit, modifier: Modifier = Modifier) Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { Text( text = stringResource(id = R.string.an_error_has_occurred), - modifier = Modifier.padding(16.dp) + modifier = Modifier.padding(16.dp), ) Button(onClick = onRetry) { Text(text = stringResource(id = R.string.retry_label)) @@ -260,13 +249,14 @@ private fun HomeScreenReady( viewModel: HomeViewModel = hiltViewModel(), ) { val navigator = rememberSupportingPaneScaffoldNavigator( - scaffoldDirective = calculateScaffoldDirective(currentWindowAdaptiveInfo()) + scaffoldDirective = calculateScaffoldDirective(currentWindowAdaptiveInfo()), ) - val scope = rememberCoroutineScope() BackHandler(enabled = navigator.canNavigateBack()) { - scope.launch { navigator.navigateBack() } + scope.launch { + navigator.navigateBack() + } } Surface { @@ -290,7 +280,7 @@ private fun HomeScreenReady( } }, navigateToPlayer = navigateToPlayer, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) }, supportingPane = { @@ -298,7 +288,7 @@ private fun HomeScreenReady( if (!podcastUri.isNullOrEmpty()) { val podcastDetailsViewModel = hiltViewModel( - key = podcastUri + key = podcastUri, ) { it.create(podcastUri) } @@ -312,11 +302,11 @@ private fun HomeScreenReady( } } }, - showBackButton = navigator.isMainPaneHidden() + showBackButton = navigator.isMainPaneHidden(), ) } }, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } } @@ -332,7 +322,7 @@ private fun HomeAppBar(isExpanded: Boolean, modifier: Modifier = Modifier) { modifier = modifier .fillMaxWidth() .background(Color.Transparent) - .padding(horizontal = 16.dp, vertical = 8.dp) + .padding(horizontal = 16.dp, vertical = 8.dp), ) { SearchBar( inputField = { @@ -349,38 +339,35 @@ private fun HomeAppBar(isExpanded: Boolean, modifier: Modifier = Modifier) { leadingIcon = { Icon( imageVector = Icons.Default.Search, - contentDescription = null + contentDescription = null, ) }, trailingIcon = { Icon( imageVector = Icons.Default.AccountCircle, - contentDescription = stringResource(R.string.cd_account) + contentDescription = stringResource(R.string.cd_account), ) }, interactionSource = null, - modifier = if (isExpanded) Modifier.fillMaxWidth() else Modifier + modifier = if (isExpanded) Modifier.fillMaxWidth() else Modifier, ) }, expanded = false, - onExpandedChange = {} + onExpandedChange = {}, ) {} } } @Composable -private fun HomeScreenBackground( - modifier: Modifier = Modifier, - content: @Composable BoxScope.() -> Unit, -) { +private fun HomeScreenBackground(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { Box( modifier = modifier - .background(MaterialTheme.colorScheme.background) + .background(MaterialTheme.colorScheme.background), ) { Box( modifier = Modifier .fillMaxSize() - .radialGradientScrim(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)) + .radialGradientScrim(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)), ) content() } @@ -411,20 +398,20 @@ private fun HomeScreen( val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } HomeScreenBackground( - modifier = modifier.windowInsetsPadding(WindowInsets.navigationBars) + modifier = modifier.windowInsetsPadding(WindowInsets.navigationBars), ) { Scaffold( topBar = { Column { HomeAppBar( isExpanded = windowSizeClass.isCompact, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) if (isLoading) { LinearProgressIndicator( Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .padding(horizontal = 16.dp), ) } } @@ -432,16 +419,14 @@ private fun HomeScreen( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - containerColor = Color.Transparent + containerColor = Color.Transparent, ) { contentPadding -> // Main Content val snackBarText = stringResource(id = R.string.episode_added_to_your_queue) val showHomeCategoryTabs = featuredPodcasts.isNotEmpty() && homeCategories.isNotEmpty() HomeContent( - showHomeCategoryTabs = showHomeCategoryTabs, featuredPodcasts = featuredPodcasts, selectedHomeCategory = selectedHomeCategory, - homeCategories = homeCategories, filterableCategoriesModel = filterableCategoriesModel, podcastCategoryFilterResult = podcastCategoryFilterResult, library = library, @@ -455,18 +440,110 @@ private fun HomeScreen( onHomeAction(action) }, navigateToPodcastDetails = navigateToPodcastDetails, - navigateToPlayer = navigateToPlayer + navigateToPlayer = navigateToPlayer, ) + + if (showHomeCategoryTabs) { + PillToolbar( + selectedHomeCategory, + onHomeAction, + Modifier.align(Alignment.BottomCenter), + ) + } } } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun PillToolbar(selectedHomeCategory: HomeCategory, onHomeAction: (HomeAction) -> Unit, modifier: Modifier = Modifier) { + HorizontalFloatingToolbar( + modifier = modifier, + colors = FloatingToolbarColors( + toolbarContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + toolbarContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + fabContainerColor = MaterialTheme.colorScheme.tertiary, + fabContentColor = MaterialTheme.colorScheme.onTertiary, + ), + expanded = true, + content = { + val libraryContainerColor = + if (selectedHomeCategory.name == HomeCategory.Library.name) { + MaterialTheme.colorScheme.secondary + } else { + MaterialTheme.colorScheme.surfaceContainerHighest + } + + val libraryContentColor = + if (selectedHomeCategory.name == HomeCategory.Library.name) { + MaterialTheme.colorScheme.surfaceContainerHighest + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + + Button( + onClick = { onHomeAction(HomeAction.HomeCategorySelected(HomeCategory.Library)) }, + colors = ButtonColors( + containerColor = libraryContainerColor, + contentColor = libraryContentColor, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) { + Row(Modifier) { + Icon( + Icons.Filled.LibraryMusic, + modifier = Modifier.padding(end = 8.dp), + contentDescription = stringResource( + R.string.library_toolbar_content_description, + ), + ) + Text(stringResource(R.string.library_toolbar)) + } + } + + val discoverContainerColor = + if (selectedHomeCategory.name == HomeCategory.Library.name) { + MaterialTheme.colorScheme.surfaceContainerHighest + } else { + MaterialTheme.colorScheme.secondary + } + + val discoverContentColor = + if (selectedHomeCategory.name == HomeCategory.Library.name) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.surfaceContainerHighest + } + + Button( + onClick = { onHomeAction(HomeAction.HomeCategorySelected(HomeCategory.Discover)) }, + colors = ButtonColors( + containerColor = discoverContainerColor, + contentColor = discoverContentColor, + disabledContainerColor = MaterialTheme.colorScheme.secondary, + disabledContentColor = MaterialTheme.colorScheme.surfaceContainerHighest, + ), + ) { + Row { + Icon( + painterResource(R.drawable.genres), + modifier = Modifier.padding(end = 8.dp), + contentDescription = stringResource( + R.string.discover_toolbar_content_description, + ), + ) + Text(stringResource(R.string.discover_toolbar)) + } + } + }, + ) +} + @Composable private fun HomeContent( - showHomeCategoryTabs: Boolean, featuredPodcasts: PersistentList, selectedHomeCategory: HomeCategory, - homeCategories: List, filterableCategoriesModel: FilterableCategoriesModel, podcastCategoryFilterResult: PodcastCategoryFilterResult, library: LibraryInfo, @@ -485,28 +562,22 @@ private fun HomeContent( } HomeContentGrid( - pagerState = pagerState, - showHomeCategoryTabs = showHomeCategoryTabs, featuredPodcasts = featuredPodcasts, selectedHomeCategory = selectedHomeCategory, - homeCategories = homeCategories, filterableCategoriesModel = filterableCategoriesModel, podcastCategoryFilterResult = podcastCategoryFilterResult, library = library, modifier = modifier, onHomeAction = onHomeAction, navigateToPodcastDetails = navigateToPodcastDetails, - navigateToPlayer = navigateToPlayer + navigateToPlayer = navigateToPlayer, ) } @Composable private fun HomeContentGrid( - showHomeCategoryTabs: Boolean, - pagerState: PagerState, featuredPodcasts: PersistentList, selectedHomeCategory: HomeCategory, - homeCategories: List, filterableCategoriesModel: FilterableCategoriesModel, podcastCategoryFilterResult: PodcastCategoryFilterResult, library: LibraryInfo, @@ -517,41 +588,29 @@ private fun HomeContentGrid( ) { LazyVerticalGrid( columns = GridCells.Adaptive(362.dp), - modifier = modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), ) { - if (featuredPodcasts.isNotEmpty()) { - fullWidthItem { - FollowedPodcastItem( - pagerState = pagerState, - items = featuredPodcasts, - onPodcastUnfollowed = { onHomeAction(HomeAction.PodcastUnfollowed(it)) }, - navigateToPodcastDetails = navigateToPodcastDetails, - modifier = Modifier - .fillMaxWidth() - ) - } - } - - if (showHomeCategoryTabs) { - fullWidthItem { - Row { - HomeCategoryTabs( - categories = homeCategories, - selectedCategory = selectedHomeCategory, - showHorizontalLine = false, - onCategorySelected = { onHomeAction(HomeAction.HomeCategorySelected(it)) }, - modifier = Modifier.width(240.dp) - ) - } - } - } - when (selectedHomeCategory) { HomeCategory.Library -> { + if (featuredPodcasts.isNotEmpty()) { + fullWidthItem { + FollowedPodcastItem( + items = featuredPodcasts, + onPodcastUnfollowed = { + onHomeAction(HomeAction.PodcastUnfollowed(it)) + }, + navigateToPodcastDetails = navigateToPodcastDetails, + modifier = Modifier + .fillMaxWidth(), + ) + } + } + libraryItems( library = library, navigateToPlayer = navigateToPlayer, - onQueueEpisode = { onHomeAction(HomeAction.QueueEpisode(it)) } + onQueueEpisode = { onHomeAction(HomeAction.QueueEpisode(it)) }, + removeFromQueue = { onHomeAction(HomeAction.RemoveEpisode(it)) }, ) } @@ -565,7 +624,8 @@ private fun HomeContentGrid( onTogglePodcastFollowed = { onHomeAction(HomeAction.TogglePodcastFollowed(it)) }, - onQueueEpisode = { onHomeAction(HomeAction.QueueEpisode(it)) } + onQueueEpisode = { onHomeAction(HomeAction.QueueEpisode(it)) }, + removeFromQueue = { onHomeAction(HomeAction.RemoveEpisode(it)) }, ) } } @@ -574,7 +634,6 @@ private fun HomeContentGrid( @Composable private fun FollowedPodcastItem( - pagerState: PagerState, items: PersistentList, onPodcastUnfollowed: (PodcastInfo) -> Unit, navigateToPodcastDetails: (PodcastInfo) -> Unit, @@ -584,83 +643,19 @@ private fun FollowedPodcastItem( Spacer(Modifier.height(16.dp)) FollowedPodcasts( - pagerState = pagerState, items = items, onPodcastUnfollowed = onPodcastUnfollowed, navigateToPodcastDetails = navigateToPodcastDetails, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(16.dp)) } } -@Composable -private fun HomeCategoryTabs( - categories: List, - selectedCategory: HomeCategory, - onCategorySelected: (HomeCategory) -> Unit, - showHorizontalLine: Boolean, - modifier: Modifier = Modifier, -) { - if (categories.isEmpty()) { - return - } - - val selectedIndex = categories.indexOfFirst { it == selectedCategory } - val indicator = @Composable { tabPositions: List -> - HomeCategoryTabIndicator( - Modifier.tabIndicatorOffset(tabPositions[selectedIndex]) - ) - } - - TabRow( - selectedTabIndex = selectedIndex, - containerColor = Color.Transparent, - indicator = indicator, - modifier = modifier, - divider = { - if (showHorizontalLine) { - HorizontalDivider() - } - } - ) { - categories.forEachIndexed { index, category -> - Tab( - selected = index == selectedIndex, - onClick = { onCategorySelected(category) }, - text = { - Text( - text = when (category) { - HomeCategory.Library -> stringResource(R.string.home_library) - HomeCategory.Discover -> stringResource(R.string.home_discover) - }, - style = MaterialTheme.typography.bodyMedium - ) - } - ) - } - } -} - -@Composable -private fun HomeCategoryTabIndicator( - modifier: Modifier = Modifier, - color: Color = MaterialTheme.colorScheme.onSurface, -) { - Spacer( - modifier - .padding(horizontal = 24.dp) - .height(4.dp) - .background(color, RoundedCornerShape(topStartPercent = 100, topEndPercent = 100)) - ) -} - -private val FEATURED_PODCAST_IMAGE_SIZE_DP = 160.dp - +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun FollowedPodcasts( - pagerState: PagerState, items: PersistentList, onPodcastUnfollowed: (PodcastInfo) -> Unit, navigateToPodcastDetails: (PodcastInfo) -> Unit, @@ -672,17 +667,14 @@ private fun FollowedPodcasts( // which solves this problem and avoids this calculation altogether. Once 1.7.0 is // stable, this implementation can be updated. BoxWithConstraints( - modifier = modifier.background(Color.Transparent) + modifier = modifier.background(Color.Transparent), ) { - val horizontalPadding = (this.maxWidth - FEATURED_PODCAST_IMAGE_SIZE_DP) / 2 - HorizontalPager( - state = pagerState, - contentPadding = PaddingValues( - horizontal = horizontalPadding, - vertical = 16.dp - ), - pageSpacing = 24.dp, - pageSize = PageSize.Fixed(FEATURED_PODCAST_IMAGE_SIZE_DP) + val horizontalPadding = this.maxWidth + HorizontalMultiBrowseCarousel( + state = rememberCarouselState { items.count() }, + preferredItemWidth = 205.dp, + itemSpacing = 12.dp, + contentPadding = PaddingValues(8.dp), ) { page -> val podcast = items[page] FollowedPodcastCarouselItem( @@ -692,9 +684,10 @@ private fun FollowedPodcasts( lastEpisodeDateText = podcast.lastEpisodeDate?.let { lastUpdated(it) }, modifier = Modifier .fillMaxSize() + .maskClip(MaterialTheme.shapes.large) .clickable { navigateToPodcastDetails(podcast) - } + }, ) } } @@ -708,36 +701,35 @@ private fun FollowedPodcastCarouselItem( lastEpisodeDateText: String? = null, onUnfollowedClick: () -> Unit, ) { - Column(modifier) { - Box( - Modifier - .size(FEATURED_PODCAST_IMAGE_SIZE_DP) - .align(Alignment.CenterHorizontally) - ) { - PodcastImage( - podcastImageUrl = podcastImageUrl, - contentDescription = podcastTitle, - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.medium) - ) - - ToggleFollowPodcastIconButton( - onClick = onUnfollowedClick, - isFollowed = true, /* All podcasts are followed in this feed */ - modifier = Modifier.align(Alignment.BottomEnd) - ) - } + val gradient = Brush.verticalGradient(listOf(Color.Transparent, Color.Black)) + Box( + modifier + .height(230.dp), + ) { + PodcastImage( + podcastImageUrl = podcastImageUrl, + contentDescription = podcastTitle, + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium), + ) + ToggleFollowPodcastIconButton( + onClick = onUnfollowedClick, + isFollowed = true, /* All podcasts are followed in this feed */ + modifier = Modifier.align(Alignment.TopStart), + ) + Box(modifier = Modifier.matchParentSize().background(gradient)) if (lastEpisodeDateText != null) { Text( text = lastEpisodeDateText, style = MaterialTheme.typography.bodySmall, + color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier - .padding(top = 8.dp) - .align(Alignment.CenterHorizontally) + .padding(12.dp) + .align(Alignment.BottomStart), ) } } @@ -760,13 +752,12 @@ private fun lastUpdated(updated: OffsetDateTime): String { } } -@OptIn(ExperimentalMaterial3Api::class) @Preview @Composable private fun HomeAppBarPreview() { JetcasterTheme { HomeAppBar( - isExpanded = false + isExpanded = false, ) } } @@ -785,16 +776,16 @@ private fun PreviewHome() { selectedHomeCategory = HomeCategory.Discover, filterableCategoriesModel = FilterableCategoriesModel( categories = PreviewCategories, - selectedCategory = PreviewCategories.firstOrNull() + selectedCategory = PreviewCategories.firstOrNull(), ), podcastCategoryFilterResult = PodcastCategoryFilterResult( topPodcasts = PreviewPodcasts, - episodes = PreviewPodcastEpisodes + episodes = PreviewPodcastEpisodes, ), library = LibraryInfo(), onHomeAction = {}, navigateToPodcastDetails = {}, - navigateToPlayer = {} + navigateToPlayer = {}, ) } } @@ -807,7 +798,7 @@ private fun PreviewPodcastCard() { modifier = Modifier.size(128.dp), podcastTitle = "", podcastImageUrl = "", - onUnfollowedClick = {} + onUnfollowedClick = {}, ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt index f84b68cd32..b3106ee1ee 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt @@ -26,10 +26,12 @@ import com.example.jetcaster.core.data.repository.PodcastsRepository import com.example.jetcaster.core.domain.FilterableCategoriesUseCase import com.example.jetcaster.core.domain.PodcastCategoryFilterUseCase import com.example.jetcaster.core.model.CategoryInfo +import com.example.jetcaster.core.model.EpisodeInfo import com.example.jetcaster.core.model.FilterableCategoriesModel import com.example.jetcaster.core.model.LibraryInfo import com.example.jetcaster.core.model.PodcastCategoryFilterResult import com.example.jetcaster.core.model.PodcastInfo +import com.example.jetcaster.core.model.asDaoModel import com.example.jetcaster.core.model.asExternalModel import com.example.jetcaster.core.model.asPodcastToEpisodeInfo import com.example.jetcaster.core.player.EpisodePlayer @@ -100,16 +102,18 @@ class HomeViewModel @Inject constructor( subscribedPodcasts.flatMapLatest { podcasts -> episodeStore.episodesInPodcasts( podcastUris = podcasts.map { it.podcast.uri }, - limit = 20 + limit = 20, ) - } - ) { homeCategories, - homeCategory, - podcasts, - refreshing, - filterableCategories, - podcastCategoryFilterResult, - libraryEpisodes -> + }, + ) { + homeCategories, + homeCategory, + podcasts, + refreshing, + filterableCategories, + podcastCategoryFilterResult, + libraryEpisodes, + -> _selectedCategory.value = filterableCategories.selectedCategory @@ -125,14 +129,14 @@ class HomeViewModel @Inject constructor( featuredPodcasts = podcasts.map { it.asExternalModel() }.toPersistentList(), filterableCategoriesModel = filterableCategories, podcastCategoryFilterResult = podcastCategoryFilterResult, - library = libraryEpisodes.asLibrary() + library = libraryEpisodes.asLibrary(), ) }.catch { throwable -> emit( HomeScreenUiState( isLoading = false, - errorMessage = throwable.message - ) + errorMessage = throwable.message, + ), ) }.collect { _state.value = it @@ -161,6 +165,7 @@ class HomeViewModel @Inject constructor( is HomeAction.LibraryPodcastSelected -> onLibraryPodcastSelected(action.podcast) is HomeAction.PodcastUnfollowed -> onPodcastUnfollowed(action.podcast) is HomeAction.QueueEpisode -> onQueueEpisode(action.episode) + is HomeAction.RemoveEpisode -> deleteEpisode(action.episodeInfo) is HomeAction.TogglePodcastFollowed -> onTogglePodcastFollowed(action.podcast) } } @@ -192,15 +197,21 @@ class HomeViewModel @Inject constructor( private fun onQueueEpisode(episode: PlayerEpisode) { episodePlayer.addToQueue(episode) } + + fun deleteEpisode(episode: EpisodeInfo) { + viewModelScope.launch { + episodeStore.deleteEpisode(episode.asDaoModel()) + } + } } -private fun List.asLibrary(): LibraryInfo = - LibraryInfo( - episodes = this.map { it.asPodcastToEpisodeInfo() } - ) +private fun List.asLibrary(): LibraryInfo = LibraryInfo( + episodes = this.map { it.asPodcastToEpisodeInfo() }, +) enum class HomeCategory { - Library, Discover + Library, + Discover, } @Immutable @@ -211,6 +222,7 @@ sealed interface HomeAction { data class TogglePodcastFollowed(val podcast: PodcastInfo) : HomeAction data class LibraryPodcastSelected(val podcast: PodcastInfo?) : HomeAction data class QueueEpisode(val episode: PlayerEpisode) : HomeAction + data class RemoveEpisode(val episodeInfo: EpisodeInfo) : HomeAction } @Immutable diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt index e1ab2bb7bf..fa84db7012 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt @@ -14,29 +14,33 @@ * limitations under the License. */ +@file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalSharedTransitionApi::class) + package com.example.jetcaster.ui.home.category +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.carousel.HorizontalUncontainedCarousel +import androidx.compose.material3.carousel.rememberCarouselState 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.semantics.semantics +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -47,7 +51,8 @@ import com.example.jetcaster.core.model.PodcastCategoryFilterResult import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.designsystem.component.PodcastImage -import com.example.jetcaster.designsystem.theme.Keyline1 +import com.example.jetcaster.ui.LocalAnimatedVisibilityScope +import com.example.jetcaster.ui.LocalSharedTransitionScope import com.example.jetcaster.ui.shared.EpisodeListItem import com.example.jetcaster.ui.theme.JetcasterTheme import com.example.jetcaster.util.ToggleFollowPodcastIconButton @@ -58,25 +63,42 @@ fun LazyGridScope.podcastCategory( navigateToPodcastDetails: (PodcastInfo) -> Unit, navigateToPlayer: (EpisodeInfo) -> Unit, onQueueEpisode: (PlayerEpisode) -> Unit, + removeFromQueue: (EpisodeInfo) -> Unit, onTogglePodcastFollowed: (PodcastInfo) -> Unit, ) { fullWidthItem { CategoryPodcasts( topPodcasts = podcastCategoryFilterResult.topPodcasts, navigateToPodcastDetails = navigateToPodcastDetails, - onTogglePodcastFollowed = onTogglePodcastFollowed + onTogglePodcastFollowed = onTogglePodcastFollowed, ) } val episodes = podcastCategoryFilterResult.episodes items(episodes, key = { it.episode.uri }) { item -> - EpisodeListItem( - episode = item.episode, - podcast = item.podcast, - onClick = navigateToPlayer, - onQueueEpisode = onQueueEpisode, - modifier = Modifier.fillMaxWidth() - ) + val sharedTransitionScope = LocalSharedTransitionScope.current + ?: throw IllegalStateException("No SharedElementScope found") + val animatedVisibilityScope = LocalAnimatedVisibilityScope.current + ?: throw IllegalStateException("No SharedElementScope found") + with(sharedTransitionScope) { + EpisodeListItem( + episode = item.episode, + podcast = item.podcast, + onClick = navigateToPlayer, + onQueueEpisode = onQueueEpisode, + modifier = Modifier + .fillMaxWidth() + .animateItem(), + imageModifier = Modifier.sharedElement( + sharedContentState = rememberSharedContentState( + key = item.episode.title, + ), + animatedVisibilityScope = animatedVisibilityScope, + clipInOverlayDuringTransition = OverlayClip(MaterialTheme.shapes.medium), + ), + removeFromQueue = removeFromQueue, + ) + } } } @@ -84,49 +106,43 @@ fun LazyGridScope.podcastCategory( private fun CategoryPodcasts( topPodcasts: List, navigateToPodcastDetails: (PodcastInfo) -> Unit, - onTogglePodcastFollowed: (PodcastInfo) -> Unit + onTogglePodcastFollowed: (PodcastInfo) -> Unit, ) { CategoryPodcastRow( podcasts = topPodcasts, onTogglePodcastFollowed = onTogglePodcastFollowed, navigateToPodcastDetails = navigateToPodcastDetails, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun CategoryPodcastRow( podcasts: List, onTogglePodcastFollowed: (PodcastInfo) -> Unit, navigateToPodcastDetails: (PodcastInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - LazyRow( - modifier = modifier, - contentPadding = PaddingValues( - start = Keyline1, - top = 8.dp, - end = Keyline1, - bottom = 24.dp - ), - horizontalArrangement = Arrangement.spacedBy(24.dp) - ) { - items( - items = podcasts, - key = { it.uri } - ) { podcast -> - TopPodcastRowItem( - podcastTitle = podcast.title, - podcastImageUrl = podcast.imageUrl, - isFollowed = podcast.isSubscribed ?: false, - onToggleFollowClicked = { onTogglePodcastFollowed(podcast) }, - modifier = Modifier - .width(128.dp) - .clickable { - navigateToPodcastDetails(podcast) - } - ) - } + HorizontalUncontainedCarousel( + state = rememberCarouselState { podcasts.count() }, + modifier = modifier.padding(start = 8.dp), + itemWidth = 128.dp, + itemSpacing = 4.dp, + ) { i -> + val podcast = podcasts[i] + TopPodcastRowItem( + podcastTitle = podcast.title, + podcastImageUrl = podcast.imageUrl, + isFollowed = podcast.isSubscribed ?: false, + onToggleFollowClicked = { onTogglePodcastFollowed(podcast) }, + modifier = Modifier + .width(128.dp) + .clickable { + navigateToPodcastDetails(podcast) + } + .maskClip(MaterialTheme.shapes.large), + ) } } @@ -138,38 +154,40 @@ private fun TopPodcastRowItem( modifier: Modifier = Modifier, onToggleFollowClicked: () -> Unit, ) { - Column( - modifier.semantics(mergeDescendants = true) {} + val gradient = Brush.verticalGradient(listOf(Color.Transparent, Color.Black)) + + Box( + modifier + .fillMaxWidth() + .height(128.dp) + .aspectRatio(1f) + .clip(MaterialTheme.shapes.large), ) { - Box( - Modifier - .fillMaxWidth() - .aspectRatio(1f) - .align(Alignment.CenterHorizontally) - ) { - PodcastImage( - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.medium), - podcastImageUrl = podcastImageUrl, - contentDescription = podcastTitle - ) + PodcastImage( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium), + podcastImageUrl = podcastImageUrl, + contentDescription = podcastTitle, + ) - ToggleFollowPodcastIconButton( - onClick = onToggleFollowClicked, - isFollowed = isFollowed, - modifier = Modifier.align(Alignment.BottomEnd) - ) - } + ToggleFollowPodcastIconButton( + onClick = onToggleFollowClicked, + isFollowed = isFollowed, + modifier = Modifier.align(Alignment.TopStart), + ) + + Box(modifier = Modifier.matchParentSize().background(gradient)) Text( text = podcastTitle, + color = Color.White, style = MaterialTheme.typography.bodyMedium, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier - .padding(top = 8.dp) - .fillMaxWidth() + .align(Alignment.BottomStart) + .padding(start = 16.dp, bottom = 16.dp), ) } } @@ -183,7 +201,7 @@ fun PreviewEpisodeListItem() { podcast = PreviewPodcasts[0], onClick = { }, onQueueEpisode = { }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt index 1bf0d6017e..45011d4fe8 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt @@ -53,6 +53,7 @@ fun LazyGridScope.discoverItems( podcastCategoryFilterResult: PodcastCategoryFilterResult, navigateToPodcastDetails: (PodcastInfo) -> Unit, navigateToPlayer: (EpisodeInfo) -> Unit, + removeFromQueue: (EpisodeInfo) -> Unit, onCategorySelected: (CategoryInfo) -> Unit, onTogglePodcastFollowed: (PodcastInfo) -> Unit, onQueueEpisode: (PlayerEpisode) -> Unit, @@ -68,7 +69,7 @@ fun LazyGridScope.discoverItems( PodcastCategoryTabs( filterableCategoriesModel = filterableCategoriesModel, onCategorySelected = onCategorySelected, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(8.dp)) @@ -80,6 +81,7 @@ fun LazyGridScope.discoverItems( navigateToPlayer = navigateToPlayer, onTogglePodcastFollowed = onTogglePodcastFollowed, onQueueEpisode = onQueueEpisode, + removeFromQueue = removeFromQueue, ) } @@ -87,10 +89,10 @@ fun LazyGridScope.discoverItems( private fun PodcastCategoryTabs( filterableCategoriesModel: FilterableCategoriesModel, onCategorySelected: (CategoryInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val selectedIndex = filterableCategoriesModel.categories.indexOf( - filterableCategoriesModel.selectedCategory + filterableCategoriesModel.selectedCategory, ) LazyRow( modifier = modifier, @@ -99,7 +101,7 @@ private fun PodcastCategoryTabs( ) { itemsIndexed( items = filterableCategoriesModel.categories, - key = { i, category -> category.id } + key = { i, category -> category.id }, ) { index, category -> ChoiceChipContent( text = category.name, @@ -113,12 +115,7 @@ private fun PodcastCategoryTabs( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun ChoiceChipContent( - text: String, - selected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { +private fun ChoiceChipContent(text: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { FilterChip( selected = selected, onClick = onClick, @@ -127,7 +124,7 @@ private fun ChoiceChipContent( Icon( imageVector = Icons.Default.Check, contentDescription = stringResource(id = R.string.cd_selected_category), - modifier = Modifier.height(18.dp) + modifier = Modifier.height(18.dp), ) } }, @@ -144,7 +141,7 @@ private fun ChoiceChipContent( selectedLabelColor = MaterialTheme.colorScheme.onSecondaryContainer, selectedLeadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer, ), - shape = MaterialTheme.shapes.medium, + shape = MaterialTheme.shapes.large, border = null, modifier = modifier, ) diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt index 9d654dbdec..95c8109446 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt @@ -36,7 +36,8 @@ import com.example.jetcaster.util.fullWidthItem fun LazyGridScope.libraryItems( library: LibraryInfo, navigateToPlayer: (EpisodeInfo) -> Unit, - onQueueEpisode: (PlayerEpisode) -> Unit + onQueueEpisode: (PlayerEpisode) -> Unit, + removeFromQueue: (EpisodeInfo) -> Unit, ) { fullWidthItem { Text( @@ -45,20 +46,23 @@ fun LazyGridScope.libraryItems( start = Keyline1, top = 16.dp, ), - style = MaterialTheme.typography.headlineLarge, + style = MaterialTheme.typography.headlineMedium, ) } items( library, - key = { it.episode.uri } + key = { it.episode.uri }, ) { item -> EpisodeListItem( episode = item.episode, podcast = item.podcast, onClick = navigateToPlayer, onQueueEpisode = onQueueEpisode, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .animateItem(), + removeFromQueue = removeFromQueue, ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt index 6a82a232cc..9c7a4498b2 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt @@ -14,13 +14,16 @@ * limitations under the License. */ +@file:OptIn(ExperimentalSharedTransitionApi::class) + package com.example.jetcaster.ui.player -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -40,10 +43,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -55,9 +59,12 @@ import androidx.compose.material.icons.filled.SkipNext import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.material.icons.outlined.Pause import androidx.compose.material.icons.outlined.PlayArrow +import androidx.compose.material3.ButtonGroup import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonColors import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -65,6 +72,9 @@ import androidx.compose.material3.Slider import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.ToggleButton +import androidx.compose.material3.ToggleButtonColors +import androidx.compose.material3.ToggleButtonShapes import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -74,18 +84,12 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.window.core.layout.WindowSizeClass @@ -98,6 +102,8 @@ import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.designsystem.component.HtmlTextContainer import com.example.jetcaster.designsystem.component.ImageBackgroundColorScrim import com.example.jetcaster.designsystem.component.PodcastImage +import com.example.jetcaster.ui.LocalAnimatedVisibilityScope +import com.example.jetcaster.ui.LocalSharedTransitionScope import com.example.jetcaster.ui.theme.JetcasterTheme import com.example.jetcaster.ui.tooling.DevicePreviews import com.example.jetcaster.util.isBookPosture @@ -153,7 +159,7 @@ private fun PlayerScreen( onAddToQueue: () -> Unit, onStop: () -> Unit, playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { DisposableEffect(Unit) { onDispose { @@ -168,7 +174,7 @@ private fun PlayerScreen( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - modifier = modifier + modifier = modifier, ) { contentPadding -> if (uiState.episodePlayerState.currentEpisode != null) { PlayerContentWithBackground( @@ -192,10 +198,7 @@ private fun PlayerScreen( } @Composable -private fun PlayerBackground( - episode: PlayerEpisode?, - modifier: Modifier, -) { +private fun PlayerBackground(episode: PlayerEpisode?, modifier: Modifier) { ImageBackgroundColorScrim( url = episode?.podcastImageUrl, color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f), @@ -212,14 +215,14 @@ fun PlayerContentWithBackground( onAddToQueue: () -> Unit, playerControlActions: PlayerControlActions, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp) + contentPadding: PaddingValues = PaddingValues(0.dp), ) { Box(modifier = modifier, contentAlignment = Alignment.Center) { PlayerBackground( episode = uiState.episodePlayerState.currentEpisode, modifier = Modifier .fillMaxSize() - .padding(contentPadding) + .padding(contentPadding), ) PlayerContent( uiState = uiState, @@ -254,7 +257,7 @@ fun PlayerContent( onBackPress: () -> Unit, onAddToQueue: () -> Unit, playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val foldingFeature = displayFeatures.filterIsInstance().firstOrNull() @@ -273,7 +276,8 @@ fun PlayerContent( isTableTopPosture(foldingFeature) || ( isSeparatingPosture(foldingFeature) && - foldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL + foldingFeature.orientation == + FoldingFeature.Orientation.HORIZONTAL ) if (usingVerticalStrategy) { @@ -302,10 +306,10 @@ fun PlayerContent( .verticalGradientScrim( color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f), startYPercentage = 1f, - endYPercentage = 0f + endYPercentage = 0f, ) .systemBarsPadding() - .padding(horizontal = 8.dp) + .padding(horizontal = 8.dp), ) { TopAppBar( onBackPress = onBackPress, @@ -322,7 +326,7 @@ fun PlayerContent( ) }, strategy = HorizontalTwoPaneStrategy(splitFraction = 0.5f), - displayFeatures = displayFeatures + displayFeatures = displayFeatures, ) } } @@ -346,20 +350,26 @@ private fun PlayerContentRegular( onBackPress: () -> Unit, onAddToQueue: () -> Unit, playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val playerEpisode = uiState.episodePlayerState val currentEpisode = playerEpisode.currentEpisode ?: return + + val sharedTransitionScope = LocalSharedTransitionScope.current + ?: throw IllegalStateException("No SharedElementScope found") + val animatedVisibilityScope = LocalAnimatedVisibilityScope.current + ?: throw IllegalStateException("No SharedElementScope found") + Column( modifier = modifier .fillMaxSize() .verticalGradientScrim( color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f), startYPercentage = 1f, - endYPercentage = 0f + endYPercentage = 0f, ) .systemBarsPadding() - .padding(horizontal = 8.dp) + .padding(horizontal = 8.dp), ) { TopAppBar( onBackPress = onBackPress, @@ -367,39 +377,56 @@ private fun PlayerContentRegular( ) Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 8.dp) + modifier = Modifier.padding(horizontal = 8.dp), ) { Spacer(modifier = Modifier.weight(1f)) - PlayerImage( - podcastImageUrl = currentEpisode.podcastImageUrl, - modifier = Modifier.weight(10f) - ) - Spacer(modifier = Modifier.height(32.dp)) - PodcastDescription(currentEpisode.title, currentEpisode.podcastName) - Spacer(modifier = Modifier.height(32.dp)) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.weight(10f) - ) { - PlayerSlider( - timeElapsed = playerEpisode.timeElapsed, - episodeDuration = currentEpisode.duration, - onSeekingStarted = playerControlActions.onSeekingStarted, - onSeekingFinished = playerControlActions.onSeekingFinished - ) - PlayerButtons( - hasNext = playerEpisode.queue.isNotEmpty(), - isPlaying = playerEpisode.isPlaying, - onPlayPress = playerControlActions.onPlayPress, - onPausePress = playerControlActions.onPausePress, - onAdvanceBy = playerControlActions.onAdvanceBy, - onRewindBy = playerControlActions.onRewindBy, - onNext = playerControlActions.onNext, - onPrevious = playerControlActions.onPrevious, - Modifier.padding(vertical = 8.dp) - ) + with(sharedTransitionScope) { + with(animatedVisibilityScope) { + PlayerImage( + podcastImageUrl = currentEpisode.podcastImageUrl, + modifier = Modifier + .weight(10f) + .animateEnterExit( + enter = fadeIn(spring(stiffness = Spring.StiffnessLow)), + exit = fadeOut(), + ), + imageModifier = Modifier.sharedElement( + sharedContentState = rememberSharedContentState( + key = currentEpisode.title, + ), + animatedVisibilityScope = animatedVisibilityScope, + clipInOverlayDuringTransition = + OverlayClip(MaterialTheme.shapes.medium), + ), + ) + } + Spacer(modifier = Modifier.height(32.dp)) + PodcastDescription(currentEpisode.title, currentEpisode.podcastName) + Spacer(modifier = Modifier.height(32.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.weight(10f), + ) { + PlayerSlider( + timeElapsed = playerEpisode.timeElapsed, + episodeDuration = currentEpisode.duration, + onSeekingStarted = playerControlActions.onSeekingStarted, + onSeekingFinished = playerControlActions.onSeekingFinished, + ) + PlayerButtons( + hasNext = playerEpisode.queue.isNotEmpty(), + isPlaying = playerEpisode.isPlaying, + onPlayPress = playerControlActions.onPlayPress, + onPausePress = playerControlActions.onPausePress, + onAdvanceBy = playerControlActions.onAdvanceBy, + onRewindBy = playerControlActions.onRewindBy, + onNext = playerControlActions.onNext, + onPrevious = playerControlActions.onPrevious, + Modifier.padding(vertical = 8.dp), + ) + } + Spacer(modifier = Modifier.weight(1f)) } - Spacer(modifier = Modifier.weight(1f)) } } } @@ -408,10 +435,7 @@ private fun PlayerContentRegular( * The UI for the top pane of a tabletop layout. */ @Composable -private fun PlayerContentTableTopTop( - uiState: PlayerUiState, - modifier: Modifier = Modifier -) { +private fun PlayerContentTableTopTop(uiState: PlayerUiState, modifier: Modifier = Modifier) { // Content for the top part of the screen val episode = uiState.episodePlayerState.currentEpisode ?: return Column( @@ -420,15 +444,15 @@ private fun PlayerContentTableTopTop( .verticalGradientScrim( color = MaterialTheme.colorScheme.primary.copy(alpha = 0.50f), startYPercentage = 1f, - endYPercentage = 0f + endYPercentage = 0f, ) .windowInsetsPadding( WindowInsets.systemBars.only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Top - ) + WindowInsetsSides.Horizontal + WindowInsetsSides.Top, + ), ) .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { PlayerImage(episode.podcastImageUrl) } @@ -443,7 +467,7 @@ private fun PlayerContentTableTopBottom( onBackPress: () -> Unit, onAddToQueue: () -> Unit, playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val episodePlayerState = uiState.episodePlayerState val episode = uiState.episodePlayerState.currentEpisode ?: return @@ -452,11 +476,11 @@ private fun PlayerContentTableTopBottom( modifier = modifier .windowInsetsPadding( WindowInsets.systemBars.only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom - ) + WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom, + ), ) .padding(horizontal = 32.dp, vertical = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { TopAppBar( onBackPress = onBackPress, @@ -465,30 +489,28 @@ private fun PlayerContentTableTopBottom( PodcastDescription( title = episode.title, podcastName = episode.podcastName, - titleTextStyle = MaterialTheme.typography.titleLarge ) Spacer(modifier = Modifier.weight(0.5f)) Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.weight(10f) + modifier = Modifier.weight(10f), ) { PlayerButtons( hasNext = episodePlayerState.queue.isNotEmpty(), isPlaying = episodePlayerState.isPlaying, onPlayPress = playerControlActions.onPlayPress, onPausePress = playerControlActions.onPausePress, - playerButtonSize = 92.dp, onAdvanceBy = playerControlActions.onAdvanceBy, onRewindBy = playerControlActions.onRewindBy, onNext = playerControlActions.onNext, onPrevious = playerControlActions.onPrevious, - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier.padding(top = 8.dp), ) PlayerSlider( timeElapsed = episodePlayerState.timeElapsed, episodeDuration = episode.duration, onSeekingStarted = playerControlActions.onSeekingStarted, - onSeekingFinished = playerControlActions.onSeekingFinished + onSeekingFinished = playerControlActions.onSeekingFinished, ) } } @@ -498,10 +520,7 @@ private fun PlayerContentTableTopBottom( * The UI for the start pane of a book layout. */ @Composable -private fun PlayerContentBookStart( - uiState: PlayerUiState, - modifier: Modifier = Modifier -) { +private fun PlayerContentBookStart(uiState: PlayerUiState, modifier: Modifier = Modifier) { val episode = uiState.episodePlayerState.currentEpisode ?: return Column( modifier = modifier @@ -509,7 +528,7 @@ private fun PlayerContentBookStart( .verticalScroll(rememberScrollState()) .padding( vertical = 40.dp, - horizontal = 16.dp + horizontal = 16.dp, ), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -525,11 +544,7 @@ private fun PlayerContentBookStart( * The UI for the end pane of a book layout. */ @Composable -private fun PlayerContentBookEnd( - uiState: PlayerUiState, - playerControlActions: PlayerControlActions, - modifier: Modifier = Modifier -) { +private fun PlayerContentBookEnd(uiState: PlayerUiState, playerControlActions: PlayerControlActions, modifier: Modifier = Modifier) { val episodePlayerState = uiState.episodePlayerState val episode = episodePlayerState.currentEpisode ?: return Column( @@ -543,7 +558,7 @@ private fun PlayerContentBookEnd( podcastImageUrl = episode.podcastImageUrl, modifier = Modifier .padding(vertical = 16.dp) - .weight(1f) + .weight(1f), ) PlayerSlider( timeElapsed = episodePlayerState.timeElapsed, @@ -560,44 +575,38 @@ private fun PlayerContentBookEnd( onRewindBy = playerControlActions.onRewindBy, onNext = playerControlActions.onNext, onPrevious = playerControlActions.onPrevious, - Modifier.padding(vertical = 8.dp) + Modifier.padding(vertical = 8.dp), ) } } @Composable -private fun TopAppBar( - onBackPress: () -> Unit, - onAddToQueue: () -> Unit, -) { +private fun TopAppBar(onBackPress: () -> Unit, onAddToQueue: () -> Unit) { Row(Modifier.fillMaxWidth()) { IconButton(onClick = onBackPress) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.cd_back) + contentDescription = stringResource(R.string.cd_back), ) } Spacer(Modifier.weight(1f)) IconButton(onClick = onAddToQueue) { Icon( imageVector = Icons.AutoMirrored.Filled.PlaylistAdd, - contentDescription = stringResource(R.string.cd_add) + contentDescription = stringResource(R.string.cd_add), ) } IconButton(onClick = { /* TODO */ }) { Icon( imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.cd_more) + contentDescription = stringResource(R.string.cd_more), ) } } } @Composable -private fun PlayerImage( - podcastImageUrl: String, - modifier: Modifier = Modifier -) { +private fun PlayerImage(podcastImageUrl: String, modifier: Modifier = Modifier, imageModifier: Modifier = Modifier) { PodcastImage( podcastImageUrl = podcastImageUrl, contentDescription = null, @@ -605,29 +614,25 @@ private fun PlayerImage( modifier = modifier .sizeIn(maxWidth = 500.dp, maxHeight = 500.dp) .aspectRatio(1f) - .clip(MaterialTheme.shapes.medium) + .clip(MaterialTheme.shapes.medium), + imageModifier = imageModifier, ) } -@OptIn(ExperimentalFoundationApi::class) @Composable -private fun PodcastDescription( - title: String, - podcastName: String, - titleTextStyle: TextStyle = MaterialTheme.typography.headlineSmall -) { +private fun PodcastDescription(title: String, podcastName: String) { Text( text = title, - style = titleTextStyle, - maxLines = 1, + style = MaterialTheme.typography.displayLarge, + maxLines = 2, color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.basicMarquee() + modifier = Modifier.basicMarquee(), ) Text( text = podcastName, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface, - maxLines = 1 + maxLines = 1, ) } @@ -637,7 +642,7 @@ private fun PodcastInformation( name: String, summary: String, modifier: Modifier = Modifier, - titleTextStyle: TextStyle = MaterialTheme.typography.headlineSmall, + titleTextStyle: TextStyle = MaterialTheme.typography.headlineLarge, nameTextStyle: TextStyle = MaterialTheme.typography.displaySmall, ) { Column( @@ -649,19 +654,19 @@ private fun PodcastInformation( text = name, style = nameTextStyle, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Text( text = title, style = titleTextStyle, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) HtmlTextContainer(text = summary) { Text( text = it, style = MaterialTheme.typography.bodyMedium, - color = LocalContentColor.current + color = LocalContentColor.current, ) } } @@ -683,7 +688,7 @@ private fun PlayerSlider( Column( Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) + .padding(horizontal = 16.dp), ) { var sliderValue by remember(timeElapsed) { mutableStateOf(timeElapsed) } val maxRange = (episodeDuration?.toSeconds() ?: 0).toFloat() @@ -692,7 +697,7 @@ private fun PlayerSlider( Text( text = "${sliderValue.formatString()} • ${episodeDuration?.formatString()}", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -703,11 +708,12 @@ private fun PlayerSlider( onSeekingStarted() sliderValue = Duration.ofSeconds(it.toLong()) }, - onValueChangeFinished = { onSeekingFinished(sliderValue) } + onValueChangeFinished = { onSeekingFinished(sliderValue) }, ) } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun PlayerButtons( hasNext: Boolean, @@ -719,93 +725,123 @@ private fun PlayerButtons( onNext: () -> Unit, onPrevious: () -> Unit, modifier: Modifier = Modifier, - playerButtonSize: Dp = 72.dp, - sideButtonSize: Dp = 48.dp, ) { - Row( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - val sideButtonsModifier = Modifier - .size(sideButtonSize) - .background( - color = MaterialTheme.colorScheme.surfaceContainerHighest, - shape = CircleShape - ) - .semantics { role = Role.Button } - - val primaryButtonModifier = Modifier - .size(playerButtonSize) - .background( - color = MaterialTheme.colorScheme.primaryContainer, - shape = CircleShape - ) - .semantics { role = Role.Button } - - Image( - imageVector = Icons.Filled.SkipPrevious, - contentDescription = stringResource(R.string.cd_skip_previous), - contentScale = ContentScale.Inside, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), - modifier = sideButtonsModifier - .clickable(enabled = isPlaying, onClick = onPrevious) - .alpha(if (isPlaying) 1f else 0.25f) - ) - Image( - imageVector = Icons.Filled.Replay10, - contentDescription = stringResource(R.string.cd_replay10), - contentScale = ContentScale.Inside, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), - modifier = sideButtonsModifier - .clickable { - onRewindBy(Duration.ofSeconds(10)) + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { + ToggleButton( + checked = isPlaying, + onCheckedChange = { + if (isPlaying) { + onPausePress() + } else { + onPlayPress() } - ) - if (isPlaying) { - Image( - imageVector = Icons.Outlined.Pause, - contentDescription = stringResource(R.string.cd_pause), - contentScale = ContentScale.Fit, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer), - modifier = primaryButtonModifier - .padding(8.dp) - .clickable { - onPausePress() - } - ) - } else { - Image( - imageVector = Icons.Outlined.PlayArrow, - contentDescription = stringResource(R.string.cd_play), - contentScale = ContentScale.Fit, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer), - modifier = primaryButtonModifier - .padding(8.dp) - .clickable { - onPlayPress() - } + }, + colors = ToggleButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = MaterialTheme.colorScheme.primary, + disabledContentColor = MaterialTheme.colorScheme.onPrimary, + checkedContainerColor = MaterialTheme.colorScheme.primary, + checkedContentColor = MaterialTheme.colorScheme.onPrimary, + ), + shapes = ToggleButtonShapes( + shape = RoundedCornerShape(60.dp), + pressedShape = RoundedCornerShape(if (isPlaying) 60.dp else 30.dp), + checkedShape = RoundedCornerShape(30.dp), + ), + modifier = Modifier + .width(186.dp) + .height(136.dp), + ) { + Icon( + imageVector = if (isPlaying) Icons.Outlined.Pause else Icons.Outlined.PlayArrow, + modifier = Modifier.fillMaxSize(), + contentDescription = null, ) } - Image( - imageVector = Icons.Filled.Forward10, - contentDescription = stringResource(R.string.cd_forward10), - contentScale = ContentScale.Inside, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), - modifier = sideButtonsModifier - .clickable { - onAdvanceBy(Duration.ofSeconds(10)) - } - ) - Image( - imageVector = Icons.Filled.SkipNext, - contentDescription = stringResource(R.string.cd_skip_next), - contentScale = ContentScale.Inside, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), - modifier = sideButtonsModifier - .clickable(enabled = hasNext, onClick = onNext) - .alpha(if (hasNext) 1f else 0.25f) - ) + ButtonGroup( + modifier = Modifier.padding(vertical = 16.dp, horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + val skipButtonsModifier = Modifier + .width(56.dp) + .height(68.dp) + + val rewindFastForwardButtonsModifier = Modifier + .size(68.dp) + + IconButton( + onClick = onPrevious, + modifier = skipButtonsModifier, + shape = RoundedCornerShape(50.dp), + colors = IconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledContentColor = MaterialTheme.colorScheme.onPrimary, + ), + enabled = isPlaying, + ) { + Icon( + imageVector = Icons.Filled.SkipPrevious, + contentDescription = null, + ) + } + + IconButton( + onClick = { onRewindBy(Duration.ofSeconds(10)) }, + modifier = rewindFastForwardButtonsModifier, + shape = RoundedCornerShape(15.dp), + colors = IconButtonColors( + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + disabledContainerColor = MaterialTheme.colorScheme.secondary, + disabledContentColor = MaterialTheme.colorScheme.onSecondary, + ), + enabled = isPlaying, + ) { + Icon( + imageVector = Icons.Filled.Replay10, + contentDescription = null, + ) + } + + IconButton( + onClick = { onAdvanceBy(Duration.ofSeconds(10)) }, + modifier = rewindFastForwardButtonsModifier, + shape = RoundedCornerShape(15.dp), + colors = IconButtonColors( + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + disabledContainerColor = MaterialTheme.colorScheme.secondary, + disabledContentColor = MaterialTheme.colorScheme.onSecondary, + ), + enabled = isPlaying, + ) { + Icon( + imageVector = Icons.Filled.Forward10, + contentDescription = null, + ) + } + + IconButton( + onClick = onNext, + modifier = skipButtonsModifier, + shape = RoundedCornerShape(50.dp), + colors = IconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledContentColor = MaterialTheme.colorScheme.onSurface, + ), + enabled = hasNext, + ) { + Icon( + imageVector = Icons.Filled.SkipNext, + contentDescription = null, + ) + } + } } } @@ -817,7 +853,7 @@ private fun FullScreenLoading(modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxSize() - .wrapContentSize(Alignment.Center) + .wrapContentSize(Alignment.Center), ) { CircularProgressIndicator() } @@ -869,7 +905,7 @@ fun PlayerScreenPreview() { PlayerEpisode(), PlayerEpisode(), PlayerEpisode(), - ) + ), ), ), displayFeatures = emptyList(), @@ -886,7 +922,7 @@ fun PlayerScreenPreview() { onSeekingFinished = {}, onNext = {}, onPrevious = {}, - ) + ), ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt index a264db77cb..a123007908 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt @@ -36,9 +36,7 @@ import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -data class PlayerUiState( - val episodePlayerState: EpisodePlayerState = EpisodePlayerState() -) +data class PlayerUiState(val episodePlayerState: EpisodePlayerState = EpisodePlayerState()) /** * ViewModel that handles the business logic and screen state of the Player screen @@ -48,7 +46,7 @@ data class PlayerUiState( class PlayerViewModel @Inject constructor( episodeStore: EpisodeStore, private val episodePlayer: EpisodePlayer, - savedStateHandle: SavedStateHandle + savedStateHandle: SavedStateHandle, ) : ViewModel() { // episodeUri should always be present in the PlayerViewModel. diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt index 4d96997193..a087527843 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt @@ -22,25 +22,26 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults +import androidx.compose.material.icons.filled.NotificationsActive +import androidx.compose.material.icons.filled.NotificationsNone +import androidx.compose.material3.ButtonGroup import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -48,6 +49,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.ToggleButton +import androidx.compose.material3.ToggleButtonColors +import androidx.compose.material3.ToggleButtonShapes import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -65,7 +69,6 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.jetcaster.R import com.example.jetcaster.core.domain.testing.PreviewEpisodes @@ -87,21 +90,23 @@ fun PodcastDetailsScreen( navigateToPlayer: (EpisodeInfo) -> Unit, navigateBack: () -> Unit, showBackButton: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val state by viewModel.state.collectAsStateWithLifecycle() when (val s = state) { is PodcastUiState.Loading -> { PodcastDetailsLoadingScreen( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } + is PodcastUiState.Ready -> { PodcastDetailsScreen( podcast = s.podcast, episodes = s.episodes, toggleSubscribe = viewModel::toggleSusbcribe, onQueueEpisode = viewModel::onQueueEpisode, + removeFromQueue = viewModel::deleteEpisode, navigateToPlayer = navigateToPlayer, navigateBack = navigateBack, showBackButton = showBackButton, @@ -112,9 +117,7 @@ fun PodcastDetailsScreen( } @Composable -private fun PodcastDetailsLoadingScreen( - modifier: Modifier = Modifier -) { +private fun PodcastDetailsLoadingScreen(modifier: Modifier = Modifier) { Loading(modifier = modifier) } @@ -127,7 +130,8 @@ fun PodcastDetailsScreen( navigateToPlayer: (EpisodeInfo) -> Unit, navigateBack: () -> Unit, showBackButton: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + removeFromQueue: (EpisodeInfo) -> Unit = {}, ) { val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } @@ -138,18 +142,19 @@ fun PodcastDetailsScreen( if (showBackButton) { PodcastDetailsTopAppBar( navigateBack = navigateBack, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) - } + }, ) { contentPadding -> PodcastDetailsContent( podcast = podcast, episodes = episodes, toggleSubscribe = toggleSubscribe, + removeFromQueue = removeFromQueue, onQueueEpisode = { coroutineScope.launch { snackbarHostState.showSnackbar(snackBarText) @@ -157,7 +162,7 @@ fun PodcastDetailsScreen( onQueueEpisode(it) }, navigateToPlayer = navigateToPlayer, - modifier = Modifier.padding(contentPadding) + modifier = Modifier.padding(contentPadding), ) } } @@ -166,20 +171,21 @@ fun PodcastDetailsScreen( fun PodcastDetailsContent( podcast: PodcastInfo, episodes: List, + removeFromQueue: (EpisodeInfo) -> Unit, toggleSubscribe: (PodcastInfo) -> Unit, onQueueEpisode: (PlayerEpisode) -> Unit, navigateToPlayer: (EpisodeInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { LazyVerticalGrid( columns = GridCells.Adaptive(362.dp), - modifier.fillMaxSize() + modifier.fillMaxSize(), ) { fullWidthItem { PodcastDetailsHeaderItem( podcast = podcast, toggleSubscribe = toggleSubscribe, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } items(episodes, key = { it.uri }) { episode -> @@ -187,75 +193,63 @@ fun PodcastDetailsContent( episode = episode, podcast = podcast, onClick = navigateToPlayer, + removeFromQueue = removeFromQueue, onQueueEpisode = onQueueEpisode, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .animateItem(), showPodcastImage = false, - showSummary = true + showSummary = true, ) } } } @Composable -fun PodcastDetailsHeaderItem( - podcast: PodcastInfo, - toggleSubscribe: (PodcastInfo) -> Unit, - modifier: Modifier = Modifier -) { - BoxWithConstraints( - modifier = modifier.padding(Keyline1) +fun PodcastDetailsHeaderItem(podcast: PodcastInfo, toggleSubscribe: (PodcastInfo) -> Unit, modifier: Modifier = Modifier) { + Box( + modifier = modifier.padding(Keyline1), ) { - val maxImageSize = this.maxWidth / 2 - val imageSize = min(maxImageSize, 148.dp) Column { - Row( - verticalAlignment = Alignment.Bottom, - modifier = Modifier.fillMaxWidth() - ) { - PodcastImage( - modifier = Modifier - .size(imageSize) - .clip(MaterialTheme.shapes.large), - podcastImageUrl = podcast.imageUrl, - contentDescription = podcast.title - ) - Column( - modifier = Modifier.padding(start = 16.dp) - ) { - Text( - text = podcast.title, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.headlineMedium - ) - PodcastDetailsHeaderItemButtons( - isSubscribed = podcast.isSubscribed ?: false, - onClick = { - toggleSubscribe(podcast) - }, - modifier = Modifier.fillMaxWidth() - ) - } - } + PodcastImage( + modifier = Modifier + .size(280.dp) + .clip(MaterialTheme.shapes.large) + .align(Alignment.CenterHorizontally), + podcastImageUrl = podcast.imageUrl, + contentDescription = podcast.title, + ) + Text( + text = podcast.title, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.displayMedium, + modifier = Modifier.padding(top = 16.dp), + ) PodcastDetailsDescription( podcast = podcast, modifier = Modifier .fillMaxWidth() - .padding(vertical = 16.dp) + .padding(vertical = 16.dp), + ) + PodcastDetailsHeaderItemButtons( + isSubscribed = podcast.isSubscribed ?: false, + onClick = { + toggleSubscribe(podcast) + }, + modifier = Modifier.fillMaxWidth(), ) } } } @Composable -fun PodcastDetailsDescription( - podcast: PodcastInfo, - modifier: Modifier -) { +fun PodcastDetailsDescription(podcast: PodcastInfo, modifier: Modifier) { var isExpanded by remember { mutableStateOf(false) } var showSeeMore by remember { mutableStateOf(false) } Box( - modifier = modifier.clickable { isExpanded = !isExpanded } + modifier = modifier.clickable { isExpanded = !isExpanded }, ) { Text( text = podcast.description, @@ -268,72 +262,88 @@ fun PodcastDetailsDescription( modifier = Modifier.animateContentSize( animationSpec = tween( durationMillis = 200, - easing = EaseOutExpo - ) - ) + easing = EaseOutExpo, + ), + ), ) if (showSeeMore) { Box( modifier = Modifier .align(Alignment.BottomEnd) - .background(MaterialTheme.colorScheme.surface) + .background(MaterialTheme.colorScheme.surface), ) { - // TODO: Add gradient effect Text( text = stringResource(id = R.string.see_more), style = MaterialTheme.typography.bodyMedium.copy( textDecoration = TextDecoration.Underline, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ), - modifier = Modifier.padding(start = 16.dp) + modifier = Modifier.padding(start = 16.dp), ) } } } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun PodcastDetailsHeaderItemButtons( - isSubscribed: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - Row(modifier.padding(top = 16.dp)) { - Button( - onClick = onClick, - colors = ButtonDefaults.buttonColors( - containerColor = if (isSubscribed) - MaterialTheme.colorScheme.tertiary - else - MaterialTheme.colorScheme.secondary +fun PodcastDetailsHeaderItemButtons(isSubscribed: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + var isNotificationOn by remember { mutableStateOf(false) } + ButtonGroup(modifier = modifier) { + ToggleButton( + checked = isSubscribed, + onCheckedChange = { onClick() }, + colors = ToggleButtonColors( + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + disabledContainerColor = MaterialTheme.colorScheme.inverseSurface, + disabledContentColor = MaterialTheme.colorScheme.surfaceVariant, + checkedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + checkedContentColor = MaterialTheme.colorScheme.secondary, + ), + shapes = ToggleButtonShapes( + shape = RoundedCornerShape(15.dp), + pressedShape = RoundedCornerShape(if (isSubscribed) 15.dp else 60.dp), + checkedShape = RoundedCornerShape(60.dp), ), - modifier = Modifier.semantics(mergeDescendants = true) { } + modifier = Modifier + .width(76.dp) + .height(56.dp) + .semantics(mergeDescendants = true) { }, ) { Icon( imageVector = if (isSubscribed) Icons.Default.Check else Icons.Default.Add, - contentDescription = null - ) - Text( - text = if (isSubscribed) - stringResource(id = R.string.subscribed) - else - stringResource(id = R.string.subscribe), - modifier = Modifier.padding(start = 8.dp) + contentDescription = null, ) } - - Spacer(modifier = Modifier.weight(1f)) - - IconButton( - onClick = { /* TODO */ }, - modifier = Modifier.padding(start = 8.dp) + ToggleButton( + checked = isNotificationOn, + onCheckedChange = { isNotificationOn = !isNotificationOn }, + colors = ToggleButtonColors( + containerColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.surfaceVariant, + disabledContainerColor = MaterialTheme.colorScheme.inverseSurface, + disabledContentColor = MaterialTheme.colorScheme.surfaceVariant, + checkedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + checkedContentColor = MaterialTheme.colorScheme.secondary, + ), + shapes = ToggleButtonShapes( + shape = RoundedCornerShape(100.dp), + pressedShape = RoundedCornerShape(if (isNotificationOn) 100.dp else 20.dp), + checkedShape = RoundedCornerShape(20.dp), + ), + modifier = Modifier.size(56.dp), ) { Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.cd_more) + imageVector = if (isNotificationOn) { + Icons.Default.NotificationsActive + } else { + Icons.Default.NotificationsNone + }, + contentDescription = stringResource(R.string.cd_more), ) } } @@ -341,21 +351,18 @@ fun PodcastDetailsHeaderItemButtons( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PodcastDetailsTopAppBar( - navigateBack: () -> Unit, - modifier: Modifier = Modifier -) { +fun PodcastDetailsTopAppBar(navigateBack: () -> Unit, modifier: Modifier = Modifier) { TopAppBar( title = { }, navigationIcon = { IconButton(onClick = navigateBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(id = R.string.cd_back) + contentDescription = stringResource(id = R.string.cd_back), ) } }, - modifier = modifier + modifier = modifier, ) } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt index ac3c6a4267..9d43327434 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt @@ -23,6 +23,7 @@ import com.example.jetcaster.core.data.repository.EpisodeStore import com.example.jetcaster.core.data.repository.PodcastStore import com.example.jetcaster.core.model.EpisodeInfo import com.example.jetcaster.core.model.PodcastInfo +import com.example.jetcaster.core.model.asDaoModel import com.example.jetcaster.core.model.asExternalModel import com.example.jetcaster.core.player.EpisodePlayer import com.example.jetcaster.core.player.model.PlayerEpisode @@ -38,10 +39,7 @@ import kotlinx.coroutines.launch sealed interface PodcastUiState { data object Loading : PodcastUiState - data class Ready( - val podcast: PodcastInfo, - val episodes: List, - ) : PodcastUiState + data class Ready(val podcast: PodcastInfo, val episodes: List) : PodcastUiState } /** @@ -60,7 +58,7 @@ class PodcastDetailsViewModel @AssistedInject constructor( val state: StateFlow = combine( podcastStore.podcastWithExtraInfo(decodedPodcastUri), - episodeStore.episodesInPodcast(decodedPodcastUri) + episodeStore.episodesInPodcast(decodedPodcastUri), ) { podcast, episodeToPodcasts -> val episodes = episodeToPodcasts.map { it.episode.asExternalModel() } PodcastUiState.Ready( @@ -70,7 +68,7 @@ class PodcastDetailsViewModel @AssistedInject constructor( }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = PodcastUiState.Loading + initialValue = PodcastUiState.Loading, ) fun toggleSusbcribe(podcast: PodcastInfo) { @@ -83,6 +81,12 @@ class PodcastDetailsViewModel @AssistedInject constructor( episodePlayer.addToQueue(playerEpisode) } + fun deleteEpisode(episodeInfo: EpisodeInfo) { + viewModelScope.launch { + episodeStore.deleteEpisode(episodeInfo.asDaoModel()) + } + } + @AssistedFactory interface Factory { fun create(podcastUri: String): PodcastDetailsViewModel diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/EpisodeListItem.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/EpisodeListItem.kt index 215a9b24c9..acb2933365 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/EpisodeListItem.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/EpisodeListItem.kt @@ -23,17 +23,23 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.PlaylistAdd +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.rounded.PlayCircleFilled import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -66,35 +72,73 @@ fun EpisodeListItem( episode: EpisodeInfo, podcast: PodcastInfo, onClick: (EpisodeInfo) -> Unit, + removeFromQueue: (EpisodeInfo) -> Unit = {}, onQueueEpisode: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, + imageModifier: Modifier = Modifier, showPodcastImage: Boolean = true, showSummary: Boolean = false, ) { - Box(modifier = modifier.padding(vertical = 8.dp, horizontal = 16.dp)) { - Surface( - shape = MaterialTheme.shapes.large, - color = MaterialTheme.colorScheme.surfaceContainer, - onClick = { onClick(episode) } - ) { - Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + val dismissState = rememberSwipeToDismissBoxState() + SwipeToDismissBox( + modifier = modifier, + state = dismissState, + enableDismissFromStartToEnd = false, + backgroundContent = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(end = 40.dp), ) { - // Top Part - EpisodeListItemHeader( - episode = episode, - podcast = podcast, - showPodcastImage = showPodcastImage, - showSummary = showSummary, - modifier = Modifier.padding(bottom = 8.dp) + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.align(Alignment.CenterEnd), ) + } + }, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp), + ) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainer, + onClick = { onClick(episode) }, + ) { + Column( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) { + // Top Part + EpisodeListItemHeader( + episode = episode, + podcast = podcast, + showPodcastImage = showPodcastImage, + showSummary = showSummary, + modifier = Modifier.padding(bottom = 8.dp), + imageModifier = imageModifier, + ) - // Bottom Part - EpisodeListItemFooter( - episode = episode, - podcast = podcast, - onQueueEpisode = onQueueEpisode, - ) + // Bottom Part + EpisodeListItemFooter( + episode = episode, + podcast = podcast, + onQueueEpisode = onQueueEpisode, + ) + } + } + } + when (dismissState.currentValue) { + SwipeToDismissBoxValue.EndToStart -> { + removeFromQueue(episode) + } + + SwipeToDismissBoxValue.StartToEnd -> { + } + + SwipeToDismissBoxValue.Settled -> { } } } @@ -105,11 +149,11 @@ private fun EpisodeListItemFooter( episode: EpisodeInfo, podcast: PodcastInfo, onQueueEpisode: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier + modifier = modifier, ) { Image( imageVector = Icons.Rounded.PlayCircleFilled, @@ -119,11 +163,11 @@ private fun EpisodeListItemFooter( modifier = Modifier .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false, radius = 24.dp) + indication = ripple(bounded = false, radius = 24.dp), ) { /* TODO */ } .size(48.dp) .padding(6.dp) - .semantics { role = Role.Button } + .semantics { role = Role.Button }, ) val duration = episode.duration @@ -135,7 +179,7 @@ private fun EpisodeListItemFooter( stringResource( R.string.episode_date_duration, MediumDateFormatter.format(episode.published), - duration.toMinutes().toInt() + duration.toMinutes().toInt(), ) } // Otherwise we just use the date @@ -146,7 +190,7 @@ private fun EpisodeListItemFooter( style = MaterialTheme.typography.bodySmall, modifier = Modifier .padding(horizontal = 8.dp) - .weight(1f) + .weight(1f), ) IconButton( @@ -154,15 +198,15 @@ private fun EpisodeListItemFooter( onQueueEpisode( PlayerEpisode( podcastInfo = podcast, - episodeInfo = episode - ) + episodeInfo = episode, + ), ) }, ) { Icon( imageVector = Icons.AutoMirrored.Filled.PlaylistAdd, contentDescription = stringResource(R.string.cd_add), - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -172,7 +216,7 @@ private fun EpisodeListItemFooter( Icon( imageVector = Icons.Default.MoreVert, contentDescription = stringResource(R.string.cd_more), - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -184,14 +228,15 @@ private fun EpisodeListItemHeader( podcast: PodcastInfo, showPodcastImage: Boolean, showSummary: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + imageModifier: Modifier = Modifier, ) { Row(modifier = modifier) { Column( modifier = Modifier .weight(1f) - .padding(end = 16.dp) + .padding(end = 16.dp), ) { Text( text = episode.title, @@ -199,7 +244,7 @@ private fun EpisodeListItemHeader( minLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 2.dp) + modifier = Modifier.padding(vertical = 2.dp), ) if (showSummary) { @@ -227,33 +272,32 @@ private fun EpisodeListItemHeader( podcast = podcast, modifier = Modifier .size(56.dp) - .clip(MaterialTheme.shapes.medium) + .clip(MaterialTheme.shapes.medium), + imageModifier = imageModifier, ) } } } @Composable -private fun EpisodeListItemImage( - podcast: PodcastInfo, - modifier: Modifier = Modifier -) { +private fun EpisodeListItemImage(podcast: PodcastInfo, modifier: Modifier = Modifier, imageModifier: Modifier = Modifier) { PodcastImage( podcastImageUrl = podcast.imageUrl, contentDescription = null, modifier = modifier, + imageModifier = imageModifier, ) } @Preview( name = "Light Mode", showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_NO + uiMode = Configuration.UI_MODE_NIGHT_NO, ) @Preview( name = "Dark Mode", showBackground = true, - uiMode = Configuration.UI_MODE_NIGHT_YES + uiMode = Configuration.UI_MODE_NIGHT_YES, ) @Composable private fun EpisodeListItemPreview() { @@ -263,7 +307,7 @@ private fun EpisodeListItemPreview() { podcast = PreviewPodcasts[0], onClick = {}, onQueueEpisode = {}, - showSummary = true + showSummary = true, ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/Loading.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/Loading.kt index 4b96dc6e8a..4b81f235d9 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/Loading.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/shared/Loading.kt @@ -28,10 +28,10 @@ import androidx.compose.ui.Modifier fun Loading(modifier: Modifier = Modifier) { Surface(modifier = modifier) { Box( - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) { CircularProgressIndicator( - Modifier.align(Alignment.Center) + Modifier.align(Alignment.Center), ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/theme/Theme.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/theme/Theme.kt index bff7e5e4b4..38e15668c2 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/theme/Theme.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/theme/Theme.kt @@ -18,7 +18,9 @@ package com.example.jetcaster.ui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme +import androidx.compose.material3.MotionScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme @@ -469,23 +471,19 @@ private val highContrastDarkColorScheme = darkColorScheme( ) @Immutable -data class ColorFamily( - val color: Color, - val onColor: Color, - val colorContainer: Color, - val onColorContainer: Color -) +data class ColorFamily(val color: Color, val onColor: Color, val colorContainer: Color, val onColorContainer: Color) val unspecified_scheme = ColorFamily( - Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified + Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified, ) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun JetcasterTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = false, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { @@ -497,10 +495,11 @@ fun JetcasterTheme( else -> lightScheme } - MaterialTheme( + MaterialExpressiveTheme( colorScheme = colorScheme, + motionScheme = MotionScheme.expressive(), shapes = JetcasterShapes, typography = JetcasterTypography, - content = content + content = content, ) } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/Buttons.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/Buttons.kt index c90ffc9d82..a3e1353755 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/Buttons.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/Buttons.kt @@ -16,38 +16,43 @@ package com.example.jetcaster.util -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.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.IconToggleButtonColors +import androidx.compose.material3.IconToggleButtonShapes 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.res.stringResource -import androidx.compose.ui.semantics.onClick -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.example.jetcaster.R +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun ToggleFollowPodcastIconButton( - isFollowed: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - val clickLabel = stringResource(if (isFollowed) R.string.cd_unfollow else R.string.cd_follow) - IconButton( - onClick = onClick, - modifier = modifier.semantics { - onClick(label = clickLabel, action = null) - } +fun ToggleFollowPodcastIconButton(isFollowed: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + IconToggleButton( + checked = isFollowed, + onCheckedChange = { onClick() }, + modifier = modifier, + colors = IconToggleButtonColors( + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary, + disabledContainerColor = MaterialTheme.colorScheme.secondary, + disabledContentColor = MaterialTheme.colorScheme.onSecondary, + checkedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + checkedContentColor = MaterialTheme.colorScheme.secondary, + ), + shapes = IconToggleButtonShapes( + shape = RoundedCornerShape(10.dp), + pressedShape = if (isFollowed) RoundedCornerShape(10.dp) else CircleShape, + checkedShape = CircleShape, + ), ) { Icon( // TODO: think about animating these icons @@ -59,27 +64,6 @@ fun ToggleFollowPodcastIconButton( isFollowed -> stringResource(R.string.cd_following) else -> stringResource(R.string.cd_not_following) }, - tint = animateColorAsState( - when { - isFollowed -> MaterialTheme.colorScheme.onPrimary - else -> MaterialTheme.colorScheme.primary - } - ).value, - modifier = Modifier - .shadow( - elevation = animateDpAsState(if (isFollowed) 0.dp else 1.dp).value, - shape = MaterialTheme.shapes.small - ) - .background( - color = animateColorAsState( - when { - isFollowed -> MaterialTheme.colorScheme.primary - else -> MaterialTheme.colorScheme.surfaceContainerHighest - } - ).value, - shape = CircleShape - ) - .padding(4.dp) ) } } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/GradientScrim.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/GradientScrim.kt index 6713734728..796447d1bf 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/GradientScrim.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/GradientScrim.kt @@ -49,7 +49,7 @@ fun Modifier.radialGradientScrim(color: Color): Modifier { center = size.center.copy(y = size.height / 4), colors = listOf(color, Color.Transparent), radius = largerDimension / 2, - colorStops = listOf(0f, 0.9f) + colorStops = listOf(0f, 0.9f), ) } } @@ -74,7 +74,7 @@ fun Modifier.verticalGradientScrim( @FloatRange(from = 0.0, to = 1.0) startYPercentage: Float = 0f, @FloatRange(from = 0.0, to = 1.0) endYPercentage: Float = 1f, decay: Float = 1.0f, - numStops: Int = 16 + numStops: Int = 16, ) = this then VerticalGradientElement(color, startYPercentage, endYPercentage, decay, numStops) private data class VerticalGradientElement( @@ -82,7 +82,7 @@ private data class VerticalGradientElement( var startYPercentage: Float = 0f, var endYPercentage: Float = 1f, var decay: Float = 1.0f, - var numStops: Int = 16 + var numStops: Int = 16, ) : ModifierNodeElement() { fun createOnDraw(): DrawScope.() -> Unit { val colors = if (decay != 1f) { @@ -113,7 +113,7 @@ private data class VerticalGradientElement( drawRect( topLeft = topLeft, size = Rect(topLeft, bottomRight).size, - brush = brush + brush = brush, ) } } @@ -137,9 +137,9 @@ private data class VerticalGradientElement( } } -private class VerticalGradientModifier( - var onDraw: DrawScope.() -> Unit -) : Modifier.Node(), DrawModifierNode { +private class VerticalGradientModifier(var onDraw: DrawScope.() -> Unit) : + Modifier.Node(), + DrawModifierNode { override fun ContentDrawScope.draw() { onDraw() diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/LazyVerticalGrid.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/LazyVerticalGrid.kt index 6233653f67..bf4e357861 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/LazyVerticalGrid.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/LazyVerticalGrid.kt @@ -24,13 +24,9 @@ import androidx.compose.runtime.Composable /** * An item that occupies the entire width. */ -fun LazyGridScope.fullWidthItem( - key: Any? = null, - contentType: Any? = null, - content: @Composable LazyGridItemScope.() -> Unit -) = item( +fun LazyGridScope.fullWidthItem(key: Any? = null, contentType: Any? = null, content: @Composable LazyGridItemScope.() -> Unit) = item( span = { GridItemSpan(this.maxLineSpan) }, key = key, contentType = contentType, - content = content + content = content, ) diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/ViewModel.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/ViewModel.kt index ff27946656..1d2e5d1685 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/ViewModel.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/ViewModel.kt @@ -28,9 +28,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory * If the created [ViewModel] does not match the requested class, an [IllegalArgumentException] * exception is thrown. */ -inline fun viewModelProviderFactoryOf( - crossinline create: () -> VM -): ViewModelProvider.Factory = viewModelFactory { +inline fun viewModelProviderFactoryOf(crossinline create: () -> VM): ViewModelProvider.Factory = viewModelFactory { initializer { create() } diff --git a/Jetcaster/mobile/src/main/res/drawable/genres.xml b/Jetcaster/mobile/src/main/res/drawable/genres.xml new file mode 100644 index 0000000000..fcb2c6079f --- /dev/null +++ b/Jetcaster/mobile/src/main/res/drawable/genres.xml @@ -0,0 +1,10 @@ + + + diff --git a/Jetcaster/mobile/src/main/res/values/strings.xml b/Jetcaster/mobile/src/main/res/values/strings.xml index 078f542b1f..4a32133d5e 100644 --- a/Jetcaster/mobile/src/main/res/values/strings.xml +++ b/Jetcaster/mobile/src/main/res/values/strings.xml @@ -27,6 +27,11 @@ Your library Discover + Discover + Library + Discover Tab Icon + Library Tab Icon + Updated a while ago Updated %d week ago diff --git a/Jetcaster/tv/build.gradle.kts b/Jetcaster/tv/build.gradle.kts index 3ff7ae7b46..d8befdfe15 100644 --- a/Jetcaster/tv/build.gradle.kts +++ b/Jetcaster/tv/build.gradle.kts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -14,6 +14,7 @@ * limitations under the License. */ + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -24,18 +25,26 @@ plugins { android { namespace = "com.example.jetcaster.tv" - compileSdk = libs.versions.compileSdk.get().toInt() + compileSdk = + libs.versions.compileSdk + .get() + .toInt() defaultConfig { applicationId = "com.example.jetcaster" - minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() + minSdk = + libs.versions.minSdk + .get() + .toInt() + targetSdk = + libs.versions.targetSdk + .get() + .toInt() versionCode = 1 versionName = "1.0" vectorDrawables { useSupportLibrary = true } - } signingConfigs { // Important: change the keystore for a production deployment @@ -57,7 +66,7 @@ android { signingConfig = signingConfigs.getByName("release") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -85,7 +94,6 @@ android { } } - dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) @@ -107,6 +115,11 @@ dependencies { implementation(projects.core.designsystem) implementation(projects.core.domain) + // Media3 + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.session) + implementation(libs.androidx.media3.ui.compose) + androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/MainActivity.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/MainActivity.kt index 1c978f952d..9edf820c07 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/MainActivity.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/MainActivity.kt @@ -36,7 +36,7 @@ class MainActivity : ComponentActivity() { JetcasterTheme(isInDarkTheme = true) { Surface( modifier = Modifier.fillMaxSize(), - shape = RectangleShape + shape = RectangleShape, ) { JetcasterApp() } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt index c5943815be..a0a4df910e 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt @@ -22,6 +22,4 @@ import com.example.jetcaster.core.model.CategoryInfo data class CategorySelection(val categoryInfo: CategoryInfo, val isSelected: Boolean = false) @Immutable -data class CategorySelectionList( - val member: List -) : List by member +data class CategorySelectionList(val member: List) : List by member diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt index f9f07236a5..56405f9ac9 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt @@ -64,7 +64,7 @@ fun JetcasterApp(jetcasterAppState: JetcasterAppState = rememberJetcasterAppStat private fun GlobalNavigationContainer( jetcasterAppState: JetcasterAppState, modifier: Modifier = Modifier, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val (discover, library) = remember { FocusRequester.createRefs() } val currentRoute @@ -85,7 +85,7 @@ private fun GlobalNavigationContainer( } } } - .focusGroup() + .focusGroup(), ) { NavigationDrawerItem( selected = isClosed && currentRoute == Screen.Profile.route, @@ -96,7 +96,7 @@ private fun GlobalNavigationContainer( Text(text = "Name") Text( text = "Switch Account", - style = MaterialTheme.typography.labelSmall + style = MaterialTheme.typography.labelSmall, ) } } @@ -107,9 +107,9 @@ private fun GlobalNavigationContainer( leadingContent = { Icon( Icons.Default.Search, - contentDescription = null + contentDescription = null, ) - } + }, ) { Text(text = "Search") } @@ -119,10 +119,10 @@ private fun GlobalNavigationContainer( leadingContent = { Icon( Icons.Default.Home, - contentDescription = null + contentDescription = null, ) }, - modifier = Modifier.focusRequester(discover) + modifier = Modifier.focusRequester(discover), ) { Text(text = "Discover") } @@ -132,10 +132,10 @@ private fun GlobalNavigationContainer( leadingContent = { Icon( Icons.Default.VideoLibrary, - contentDescription = null + contentDescription = null, ) }, - modifier = Modifier.focusRequester(library) + modifier = Modifier.focusRequester(library), ) { Text(text = "Library") } @@ -143,14 +143,14 @@ private fun GlobalNavigationContainer( NavigationDrawerItem( selected = isClosed && currentRoute == Screen.Settings.route, onClick = jetcasterAppState::navigateToSettings, - leadingContent = { Icon(Icons.Default.Settings, contentDescription = null) } + leadingContent = { Icon(Icons.Default.Settings, contentDescription = null) }, ) { Text(text = "Settings") } } }, content = content, - modifier = modifier + modifier = modifier, ) } @@ -166,7 +166,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) { playEpisode = { jetcasterAppState.playEpisode() }, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } } @@ -181,7 +181,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) { playEpisode = { jetcasterAppState.playEpisode() }, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), ) } } @@ -193,7 +193,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) { }, modifier = Modifier .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()) - .fillMaxSize() + .fillMaxSize(), ) } @@ -231,7 +231,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) { ProfileScreen( modifier = Modifier .fillMaxSize() - .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()) + .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()), ) } @@ -239,7 +239,7 @@ private fun Route(jetcasterAppState: JetcasterAppState) { SettingsScreen( modifier = Modifier .fillMaxSize() - .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()) + .padding(JetcasterAppDefaults.overScanMargin.default.intoPaddingValues()), ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt index bc714c99a0..05f6001714 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt @@ -24,9 +24,7 @@ import androidx.navigation.compose.rememberNavController import com.example.jetcaster.core.player.model.PlayerEpisode import kotlinx.coroutines.flow.map -class JetcasterAppState( - val navHostController: NavHostController -) { +class JetcasterAppState(val navHostController: NavHostController) { val currentRouteFlow = navHostController.currentBackStackEntryFlow.map { it.destination.route @@ -83,12 +81,9 @@ class JetcasterAppState( } @Composable -fun rememberJetcasterAppState( - navHostController: NavHostController = rememberNavController() -) = - remember(navHostController) { - JetcasterAppState(navHostController) - } +fun rememberJetcasterAppState(navHostController: NavHostController = rememberNavController()) = remember(navHostController) { + JetcasterAppState(navHostController) +} sealed interface Screen { val route: String diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt index 4cdd5ccb52..9a9cb17eae 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Background.kt @@ -32,30 +32,28 @@ internal fun BackgroundContainer( playerEpisode: PlayerEpisode, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, - content: @Composable BoxScope.() -> Unit -) = - BackgroundContainer( - imageUrl = playerEpisode.podcastImageUrl, - modifier, - contentAlignment, - content - ) + content: @Composable BoxScope.() -> Unit, +) = BackgroundContainer( + imageUrl = playerEpisode.podcastImageUrl, + modifier, + contentAlignment, + content, +) @Composable internal fun BackgroundContainer( podcastInfo: PodcastInfo, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, - content: @Composable BoxScope.() -> Unit -) = - BackgroundContainer(imageUrl = podcastInfo.imageUrl, modifier, contentAlignment, content) + content: @Composable BoxScope.() -> Unit, +) = BackgroundContainer(imageUrl = podcastInfo.imageUrl, modifier, contentAlignment, content) @Composable internal fun BackgroundContainer( imageUrl: String, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, - content: @Composable BoxScope.() -> Unit + content: @Composable BoxScope.() -> Unit, ) { Box(modifier = modifier, contentAlignment = contentAlignment) { Background(imageUrl = imageUrl, modifier = Modifier.fillMaxSize()) @@ -64,10 +62,7 @@ internal fun BackgroundContainer( } @Composable -private fun Background( - imageUrl: String, - modifier: Modifier = Modifier, -) { +private fun Background(imageUrl: String, modifier: Modifier = Modifier) { ImageBackgroundRadialGradientScrim( url = imageUrl, colors = listOf(Color.Black, Color.Transparent), diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Button.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Button.kt index aeeaee4137..79f2f8016c 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Button.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Button.kt @@ -38,24 +38,16 @@ import androidx.tv.material3.IconButton import com.example.jetcaster.tv.R @Composable -internal fun PlayButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - scale: ButtonScale = ButtonDefaults.scale(), -) = - ButtonWithIcon( - icon = Icons.Outlined.PlayArrow, - label = stringResource(R.string.label_play), - onClick = onClick, - modifier = modifier, - scale = scale - ) +internal fun PlayButton(onClick: () -> Unit, modifier: Modifier = Modifier, scale: ButtonScale = ButtonDefaults.scale()) = ButtonWithIcon( + icon = Icons.Outlined.PlayArrow, + label = stringResource(R.string.label_play), + onClick = onClick, + modifier = modifier, + scale = scale, +) @Composable -internal fun EnqueueButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { +internal fun EnqueueButton(onClick: () -> Unit, modifier: Modifier = Modifier) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.AutoMirrored.Filled.PlaylistAdd, @@ -65,10 +57,7 @@ internal fun EnqueueButton( } @Composable -internal fun InfoButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { +internal fun InfoButton(onClick: () -> Unit, modifier: Modifier = Modifier) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.Outlined.Info, @@ -78,37 +67,27 @@ internal fun InfoButton( } @Composable -internal fun PreviousButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { +internal fun PreviousButton(onClick: () -> Unit, modifier: Modifier = Modifier) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.Default.SkipPrevious, - contentDescription = stringResource(R.string.label_previous_episode) + contentDescription = stringResource(R.string.label_previous_episode), ) } } @Composable -internal fun NextButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { +internal fun NextButton(onClick: () -> Unit, modifier: Modifier = Modifier) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.Default.SkipNext, - contentDescription = stringResource(R.string.label_next_episode) + contentDescription = stringResource(R.string.label_next_episode), ) } } @Composable -internal fun PlayPauseButton( - isPlaying: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { +internal fun PlayPauseButton(isPlaying: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { val (icon, description) = if (isPlaying) { Icons.Default.Pause to stringResource(R.string.label_pause) } else { @@ -120,27 +99,21 @@ internal fun PlayPauseButton( } @Composable -internal fun RewindButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { +internal fun RewindButton(onClick: () -> Unit, modifier: Modifier = Modifier) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.Default.Replay10, - contentDescription = stringResource(R.string.label_rewind) + contentDescription = stringResource(R.string.label_rewind), ) } } @Composable -internal fun SkipButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { +internal fun SkipButton(onClick: () -> Unit, modifier: Modifier = Modifier) { IconButton(onClick = onClick, modifier = modifier) { Icon( Icons.Default.Forward10, - contentDescription = stringResource(R.string.label_skip) + contentDescription = stringResource(R.string.label_skip), ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt index 649fbd5c5e..eef6f9e92e 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt @@ -68,14 +68,14 @@ internal fun Catalog( PodcastSection( podcastList = podcastList, onPodcastSelected = onPodcastSelected, - title = stringResource(R.string.label_podcast) + title = stringResource(R.string.label_podcast), ) } item { LatestEpisodeSection( episodeList = latestEpisodeList, onEpisodeSelected = onEpisodeSelected, - title = stringResource(R.string.label_latest_episode) + title = stringResource(R.string.label_latest_episode), ) } } @@ -90,7 +90,7 @@ private fun PodcastSection( ) { Section( title = title, - modifier = modifier + modifier = modifier, ) { PodcastRow( podcastList = podcastList, @@ -104,11 +104,11 @@ private fun LatestEpisodeSection( episodeList: EpisodeList, onEpisodeSelected: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - title: String? = null + title: String? = null, ) { Section( modifier = modifier, - title = title + title = title, ) { EpisodeRow( playerEpisodeList = episodeList, @@ -129,7 +129,7 @@ private fun Section( Text( text = title, style = style, - modifier = Modifier.padding(JetcasterAppDefaults.padding.sectionTitle) + modifier = Modifier.padding(JetcasterAppDefaults.padding.sectionTitle), ) } content() @@ -176,7 +176,7 @@ private fun PodcastRow( PodcastCard( podcastInfo = podcastInfo, onClick = { onPodcastSelected(podcastInfo) }, - modifier = cardModifier.width(JetcasterAppDefaults.cardWidth.medium) + modifier = cardModifier.width(JetcasterAppDefaults.cardWidth.medium), ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt index ddde4bcb56..2193379a0c 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt @@ -55,10 +55,10 @@ internal fun EpisodeCard( playerEpisode = playerEpisode, modifier = Modifier .padding(horizontal = 16.dp, vertical = 12.dp) - .width(JetcasterAppDefaults.cardWidth.small * 2) + .width(JetcasterAppDefaults.cardWidth.small * 2), ) }, - modifier = modifier + modifier = modifier, ) } @@ -67,7 +67,7 @@ private fun EpisodeThumbnail( playerEpisode: PlayerEpisode, onClick: () -> Unit, modifier: Modifier = Modifier, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { Card( onClick = onClick, @@ -81,22 +81,19 @@ private fun EpisodeThumbnail( } @Composable -private fun EpisodeMetaData( - playerEpisode: PlayerEpisode, - modifier: Modifier = Modifier -) { +private fun EpisodeMetaData(playerEpisode: PlayerEpisode, modifier: Modifier = Modifier) { val duration = playerEpisode.duration Column(modifier = modifier) { Text( text = playerEpisode.title, style = MaterialTheme.typography.bodyLarge, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Text(text = playerEpisode.podcastName, style = MaterialTheme.typography.bodySmall) if (duration != null) { Spacer( - modifier = Modifier.height(JetcasterAppDefaults.gap.podcastRow * 0.8f) + modifier = Modifier.height(JetcasterAppDefaults.gap.podcastRow * 0.8f), ) EpisodeDataAndDuration(offsetDateTime = playerEpisode.published, duration = duration) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDateAndDuration.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDateAndDuration.kt index 0ce6dbeaf7..97684ada0b 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDateAndDuration.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDateAndDuration.kt @@ -43,9 +43,9 @@ internal fun EpisodeDataAndDuration( text = stringResource( R.string.episode_date_duration, MediumDateFormatter.format(offsetDateTime), - duration.toMinutes().toInt() + duration.toMinutes().toInt(), ), style = style, - modifier = modifier + modifier = modifier, ) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt index 01664adb9a..46e108a6fd 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt @@ -33,20 +33,20 @@ internal fun EpisodeDetails( modifier: Modifier = Modifier, controls: (@Composable () -> Unit)? = null, verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(JetcasterAppDefaults.gap.item), - content: @Composable ColumnScope.() -> Unit + content: @Composable ColumnScope.() -> Unit, ) { TwoColumn( modifier = modifier, first = { Thumbnail( playerEpisode, - size = JetcasterAppDefaults.thumbnailSize.episodeDetails + size = JetcasterAppDefaults.thumbnailSize.episodeDetails, ) }, second = { Column( modifier = modifier, - verticalArrangement = verticalArrangement + verticalArrangement = verticalArrangement, ) { EpisodeAuthor(playerEpisode = playerEpisode) EpisodeTitle(playerEpisode = playerEpisode) @@ -55,7 +55,7 @@ internal fun EpisodeDetails( controls() } } - } + }, ) } @@ -63,7 +63,7 @@ internal fun EpisodeDetails( internal fun EpisodeAuthor( playerEpisode: PlayerEpisode, modifier: Modifier = Modifier, - style: TextStyle = MaterialTheme.typography.bodySmall + style: TextStyle = MaterialTheme.typography.bodySmall, ) { Text(text = playerEpisode.author, modifier = modifier, style = style) } @@ -72,7 +72,7 @@ internal fun EpisodeAuthor( internal fun EpisodeTitle( playerEpisode: PlayerEpisode, modifier: Modifier = Modifier, - style: TextStyle = MaterialTheme.typography.headlineLarge + style: TextStyle = MaterialTheme.typography.headlineLarge, ) { Text(text = playerEpisode.title, modifier = modifier, style = style) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeRow.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeRow.kt index 3861482cbb..70def2b173 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeRow.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeRow.kt @@ -45,7 +45,7 @@ internal fun EpisodeRow( Arrangement.spacedBy(JetcasterAppDefaults.gap.item), contentPadding: PaddingValues = JetcasterAppDefaults.padding.episodeRowContentPadding, focusRequester: FocusRequester = remember { FocusRequester() }, - lazyListState: LazyListState = remember(playerEpisodeList) { LazyListState() } + lazyListState: LazyListState = remember(playerEpisodeList) { LazyListState() }, ) { val firstItem = remember { FocusRequester() } var previousEpisodeListHash by remember { mutableIntStateOf(playerEpisodeList.hashCode()) } @@ -82,7 +82,7 @@ internal fun EpisodeRow( EpisodeCard( playerEpisode = item, onClick = { onSelected(item) }, - modifier = cardModifier + modifier = cardModifier, ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/ErrorState.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/ErrorState.kt index be7f99cabd..961f0099ca 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/ErrorState.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/ErrorState.kt @@ -34,11 +34,7 @@ import com.example.jetcaster.tv.R import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults @Composable -fun ErrorState( - backToHome: () -> Unit, - modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } -) { +fun ErrorState(backToHome: () -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() }) { LaunchedEffect(Unit) { focusRequester.requestFocus() } @@ -46,13 +42,13 @@ fun ErrorState( Column { Text( text = stringResource(R.string.display_error_state), - style = MaterialTheme.typography.displayMedium + style = MaterialTheme.typography.displayMedium, ) Button( onClick = backToHome, modifier .padding(top = JetcasterAppDefaults.gap.podcastRow) - .focusRequester(focusRequester) + .focusRequester(focusRequester), ) { Text(text = stringResource(R.string.label_back_to_home)) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Loading.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Loading.kt index 4603497509..ff77ba006e 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Loading.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Loading.kt @@ -62,11 +62,11 @@ fun Loading( ) { Box( modifier = modifier, - contentAlignment = contentAlignment + contentAlignment = contentAlignment, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.default) + verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.default), ) { CircularProgressIndicator() Text(text = message, style = style) @@ -95,10 +95,10 @@ fun CircularProgressIndicator( infiniteRepeatable( animation = tween( durationMillis = RotationDuration * RotationsPerCycle, - easing = LinearEasing - ) + easing = LinearEasing, + ), ), - "loading_current_rotation" + "loading_current_rotation", ) // How far forward (degrees) the base point should be from the start point val baseRotation = transition.animateFloat( @@ -107,10 +107,10 @@ fun CircularProgressIndicator( infiniteRepeatable( animation = tween( durationMillis = RotationDuration, - easing = LinearEasing - ) + easing = LinearEasing, + ), ), - "loading_base_rotation_angle" + "loading_base_rotation_angle", ) // How far forward (degrees) both the head and tail should be from the base point val endAngle = transition.animateFloat( @@ -121,9 +121,9 @@ fun CircularProgressIndicator( durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration 0f at 0 using CircularEasing JumpRotationAngle at HeadAndTailAnimationDuration - } + }, ), - "loading_end_rotation_angle" + "loading_end_rotation_angle", ) val startAngle = transition.animateFloat( 0f, @@ -133,15 +133,15 @@ fun CircularProgressIndicator( durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration 0f at HeadAndTailDelayDuration using CircularEasing JumpRotationAngle at durationMillis - } + }, ), - "loading_start_angle" + "loading_start_angle", ) Canvas( modifier .progressSemantics() - .size(CircularIndicatorDiameter) + .size(CircularIndicatorDiameter), ) { drawCircularIndicatorTrack(trackColor, stroke) @@ -157,17 +157,12 @@ fun CircularProgressIndicator( strokeWidth, sweep, color, - stroke + stroke, ) } } -private fun DrawScope.drawCircularIndicator( - startAngle: Float, - sweep: Float, - color: Color, - stroke: Stroke -) { +private fun DrawScope.drawCircularIndicator(startAngle: Float, sweep: Float, color: Color, stroke: Stroke) { // To draw this circle we need a rect with edges that line up with the midpoint of the stroke. // To do this we need to remove half the stroke width from the total diameter for both sides. val diameterOffset = stroke.width / 2 @@ -179,22 +174,13 @@ private fun DrawScope.drawCircularIndicator( useCenter = false, topLeft = Offset(diameterOffset, diameterOffset), size = Size(arcDimen, arcDimen), - style = stroke + style = stroke, ) } -private fun DrawScope.drawCircularIndicatorTrack( - color: Color, - stroke: Stroke -) = drawCircularIndicator(0f, 360f, color, stroke) - -private fun DrawScope.drawIndeterminateCircularIndicator( - startAngle: Float, - strokeWidth: Dp, - sweep: Float, - color: Color, - stroke: Stroke -) { +private fun DrawScope.drawCircularIndicatorTrack(color: Color, stroke: Stroke) = drawCircularIndicator(0f, 360f, color, stroke) + +private fun DrawScope.drawIndeterminateCircularIndicator(startAngle: Float, strokeWidth: Dp, sweep: Float, color: Color, stroke: Stroke) { val strokeCapOffset = if (stroke.cap == StrokeCap.Butt) { 0f } else { diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/NotAvailableFeature.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/NotAvailableFeature.kt index 21575e4c25..11b5681f33 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/NotAvailableFeature.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/NotAvailableFeature.kt @@ -25,7 +25,7 @@ import com.example.jetcaster.tv.R @Composable internal fun NotAvailableFeature( modifier: Modifier = Modifier, - message: String = stringResource(id = R.string.message_not_available_feature) + message: String = stringResource(id = R.string.message_not_available_feature), ) { Text(message, modifier = modifier) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt index 3524cae812..a1a683f34f 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt @@ -30,22 +30,18 @@ import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults @Composable -internal fun PodcastCard( - podcastInfo: PodcastInfo, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { +internal fun PodcastCard(podcastInfo: PodcastInfo, onClick: () -> Unit, modifier: Modifier = Modifier) { StandardCardContainer( imageCard = { Card( onClick = onClick, interactionSource = it, scale = CardScale.None, - shape = CardDefaults.shape(RoundedCornerShape(12.dp)) + shape = CardDefaults.shape(RoundedCornerShape(12.dp)), ) { Thumbnail( podcastInfo = podcastInfo, - size = JetcasterAppDefaults.thumbnailSize.podcast + size = JetcasterAppDefaults.thumbnailSize.podcast, ) } }, diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Seekbar.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Seekbar.kt index c36c3c7fce..4ffa5f50f5 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Seekbar.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Seekbar.kt @@ -63,7 +63,7 @@ internal fun Seekbar( val end = start.copy(x = size.width) val knobCenter = start.copy( - x = timeElapsed.seconds.toFloat() / length.seconds.toFloat() * size.width + x = timeElapsed.seconds.toFloat() / length.seconds.toFloat() * size.width, ) drawLine( brush, start, end, @@ -91,6 +91,6 @@ internal fun Seekbar( else -> false } - } + }, ) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt index ba3046716b..fa5f30104e 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/Thumbnail.kt @@ -36,17 +36,16 @@ fun Thumbnail( shape: RoundedCornerShape = RoundedCornerShape(12.dp), size: DpSize = DpSize( JetcasterAppDefaults.cardWidth.medium, - JetcasterAppDefaults.cardWidth.medium + JetcasterAppDefaults.cardWidth.medium, ), - contentScale: ContentScale = ContentScale.Crop -) = - Thumbnail( - podcastInfo.imageUrl, - modifier, - shape, - size, - contentScale - ) + contentScale: ContentScale = ContentScale.Crop, +) = Thumbnail( + podcastInfo.imageUrl, + modifier, + shape, + size, + contentScale, +) @Composable fun Thumbnail( @@ -55,17 +54,16 @@ fun Thumbnail( shape: RoundedCornerShape = RoundedCornerShape(12.dp), size: DpSize = DpSize( JetcasterAppDefaults.cardWidth.medium, - JetcasterAppDefaults.cardWidth.medium + JetcasterAppDefaults.cardWidth.medium, ), - contentScale: ContentScale = ContentScale.Crop -) = - Thumbnail( - episode.podcastImageUrl, - modifier, - shape, - size, - contentScale - ) + contentScale: ContentScale = ContentScale.Crop, +) = Thumbnail( + episode.podcastImageUrl, + modifier, + shape, + size, + contentScale, +) @Composable fun Thumbnail( @@ -74,15 +72,14 @@ fun Thumbnail( shape: RoundedCornerShape = RoundedCornerShape(12.dp), size: DpSize = DpSize( JetcasterAppDefaults.cardWidth.medium, - JetcasterAppDefaults.cardWidth.medium + JetcasterAppDefaults.cardWidth.medium, ), - contentScale: ContentScale = ContentScale.Crop -) = - PodcastImage( - podcastImageUrl = url, - contentDescription = null, - contentScale = contentScale, - modifier = modifier - .clip(shape) - .size(size), - ) + contentScale: ContentScale = ContentScale.Crop, +) = PodcastImage( + podcastImageUrl = url, + contentDescription = null, + contentScale = contentScale, + modifier = modifier + .clip(shape) + .size(size), +) diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/TwoColumn.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/TwoColumn.kt index 94658ad170..7f5e4b9190 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/TwoColumn.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/component/TwoColumn.kt @@ -29,11 +29,11 @@ internal fun TwoColumn( second: (@Composable RowScope.() -> Unit), modifier: Modifier = Modifier, horizontalArrangement: Arrangement.Horizontal = - Arrangement.spacedBy(JetcasterAppDefaults.gap.twoColumn) + Arrangement.spacedBy(JetcasterAppDefaults.gap.twoColumn), ) { Row( horizontalArrangement = horizontalArrangement, - modifier = modifier + modifier = modifier, ) { first() second() diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt index a0727cd559..15cbfc7869 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt @@ -49,7 +49,7 @@ fun DiscoverScreen( showPodcastDetails: (PodcastInfo) -> Unit, playEpisode: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - discoverScreenViewModel: DiscoverScreenViewModel = hiltViewModel() + discoverScreenViewModel: DiscoverScreenViewModel = hiltViewModel(), ) { val uiState by discoverScreenViewModel.uiState.collectAsState() @@ -58,7 +58,7 @@ fun DiscoverScreen( Loading( modifier = Modifier .fillMaxSize() - .then(modifier) + .then(modifier), ) } @@ -76,7 +76,7 @@ fun DiscoverScreen( }, modifier = Modifier .fillMaxSize() - .then(modifier) + .then(modifier), ) } } @@ -87,7 +87,6 @@ fun DiscoverScreen( private fun CatalogWithCategorySelection( categoryInfoList: CategoryInfoList, podcastList: PodcastList, - selectedCategory: CategoryInfo, latestEpisodeList: EpisodeList, onPodcastSelected: (PodcastInfo) -> Unit, @@ -124,7 +123,7 @@ private fun CatalogWithCategorySelection( enter = { selectedTab } - } + }, ) { categoryInfoList.forEachIndexed { index, category -> val tabModifier = if (selectedTabIndex == index) { @@ -142,7 +141,7 @@ private fun CatalogWithCategorySelection( ) { Text( text = category.name, - modifier = Modifier.padding(JetcasterAppDefaults.padding.tab) + modifier = Modifier.padding(JetcasterAppDefaults.padding.tab), ) } } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt index 925fd321b3..46b3188560 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt @@ -55,14 +55,14 @@ class DiscoverScreenViewModel @Inject constructor( categoryList.map { category -> CategoryInfo( id = category.id, - name = category.name.filter { !it.isWhitespace() } + name = category.name.filter { !it.isWhitespace() }, ) } } private val selectedCategoryFlow = combine( categoryListFlow, - _selectedCategory + _selectedCategory, ) { categoryList, category -> category ?: categoryList.firstOrNull() } @@ -100,7 +100,7 @@ class DiscoverScreenViewModel @Inject constructor( CategoryInfoList(categoryList), category, podcastList, - latestEpisodes + latestEpisodes, ) } else { DiscoverScreenUiState.Loading @@ -108,7 +108,7 @@ class DiscoverScreenViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - DiscoverScreenUiState.Loading + DiscoverScreenUiState.Loading, ) init { diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt index b5b02abfb4..3042cd4609 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt @@ -48,7 +48,7 @@ fun EpisodeScreen( playEpisode: () -> Unit, backToHome: () -> Unit, modifier: Modifier = Modifier, - episodeScreenViewModel: EpisodeScreenViewModel = hiltViewModel() + episodeScreenViewModel: EpisodeScreenViewModel = hiltViewModel(), ) { val uiState by episodeScreenViewModel.uiStateFlow.collectAsState() @@ -64,7 +64,7 @@ fun EpisodeScreen( playEpisode() }, addPlayList = episodeScreenViewModel::addPlayList, - modifier = screenModifier + modifier = screenModifier, ) } } @@ -79,14 +79,14 @@ private fun EpisodeDetailsWithBackground( BackgroundContainer( playerEpisode = playerEpisode, contentAlignment = Alignment.Center, - modifier = modifier + modifier = modifier, ) { EpisodeDetails( playerEpisode = playerEpisode, playEpisode = playEpisode, addPlayList = addPlayList, modifier = Modifier - .padding(JetcasterAppDefaults.overScanMargin.episode.intoPaddingValues()) + .padding(JetcasterAppDefaults.overScanMargin.episode.intoPaddingValues()), ) } } @@ -102,7 +102,7 @@ private fun EpisodeDetails( first = { Thumbnail( episode = playerEpisode, - size = JetcasterAppDefaults.thumbnailSize.episodeDetails + size = JetcasterAppDefaults.thumbnailSize.episodeDetails, ) }, second = { @@ -110,7 +110,7 @@ private fun EpisodeDetails( playerEpisode = playerEpisode, playEpisode = { playEpisode(playerEpisode) }, addPlayList = { addPlayList(playerEpisode) }, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) }, modifier = modifier, @@ -118,12 +118,7 @@ private fun EpisodeDetails( } @Composable -private fun EpisodeInfo( - playerEpisode: PlayerEpisode, - playEpisode: () -> Unit, - addPlayList: () -> Unit, - modifier: Modifier = Modifier -) { +private fun EpisodeInfo(playerEpisode: PlayerEpisode, playEpisode: () -> Unit, addPlayList: () -> Unit, modifier: Modifier = Modifier) { val duration = playerEpisode.duration Column(modifier) { @@ -137,7 +132,7 @@ private fun EpisodeInfo( text = playerEpisode.summary, softWrap = true, maxLines = 5, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) Spacer(modifier = Modifier.height(JetcasterAppDefaults.gap.paragraph)) Controls(playEpisode = playEpisode, addPlayList = addPlayList) @@ -145,15 +140,11 @@ private fun EpisodeInfo( } @Composable -private fun Controls( - playEpisode: () -> Unit, - addPlayList: () -> Unit, - modifier: Modifier = Modifier -) { +private fun Controls(playEpisode: () -> Unit, addPlayList: () -> Unit, modifier: Modifier = Modifier) { Row( horizontalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.item), verticalAlignment = Alignment.CenterVertically, - modifier = modifier + modifier = modifier, ) { PlayButton(onClick = playEpisode) EnqueueButton(onClick = addPlayList) diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt index 2a5bec06f2..dd773b50d5 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreenViewModel.kt @@ -55,7 +55,7 @@ class EpisodeScreenViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - null + null, ) val uiStateFlow = episodeToPodcastFlow.map { @@ -67,7 +67,7 @@ class EpisodeScreenViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - EpisodeScreenUiState.Loading + EpisodeScreenUiState.Loading, ) fun addPlayList(episode: PlayerEpisode) { diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt index ed73883b0d..2a823d8e62 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreen.kt @@ -50,7 +50,7 @@ fun LibraryScreen( navigateToDiscover: () -> Unit, showPodcastDetails: (PodcastInfo) -> Unit, playEpisode: (PlayerEpisode) -> Unit, - libraryScreenViewModel: LibraryScreenViewModel = hiltViewModel() + libraryScreenViewModel: LibraryScreenViewModel = hiltViewModel(), ) { val uiState by libraryScreenViewModel.uiState.collectAsState() when (val s = uiState) { @@ -93,15 +93,12 @@ private fun Library( onEpisodeSelected = onEpisodeSelected, modifier = modifier .focusRequester(focusRequester) - .focusRestorer() + .focusRestorer(), ) } @Composable -private fun NavigateToDiscover( - onNavigationRequested: () -> Unit, - modifier: Modifier = Modifier, -) { +private fun NavigateToDiscover(onNavigationRequested: () -> Unit, modifier: Modifier = Modifier) { val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { focusRequester.requestFocus() @@ -110,14 +107,14 @@ private fun NavigateToDiscover( Column { Text( text = stringResource(id = R.string.display_no_subscribed_podcast), - style = MaterialTheme.typography.displayMedium + style = MaterialTheme.typography.displayMedium, ) Text(text = stringResource(id = R.string.message_no_subscribed_podcast)) Button( onClick = onNavigationRequested, modifier = Modifier .padding(top = JetcasterAppDefaults.gap.podcastRow) - .focusRequester(focusRequester) + .focusRequester(focusRequester), ) { Text(text = stringResource(id = R.string.label_navigate_to_discover)) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt index 3797b98e89..03418dc485 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/library/LibraryScreenViewModel.kt @@ -78,7 +78,7 @@ class LibraryScreenViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - LibraryScreenUiState.Loading + LibraryScreenUiState.Loading, ) init { @@ -95,8 +95,5 @@ class LibraryScreenViewModel @Inject constructor( sealed interface LibraryScreenUiState { data object Loading : LibraryScreenUiState data object NoSubscribedPodcast : LibraryScreenUiState - data class Ready( - val subscribedPodcastList: PodcastList, - val latestEpisodeList: EpisodeList, - ) : LibraryScreenUiState + data class Ready(val subscribedPodcastList: PodcastList, val latestEpisodeList: EpisodeList) : LibraryScreenUiState } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreen.kt index bf81680771..7717daf35b 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreen.kt @@ -16,6 +16,8 @@ package com.example.jetcaster.tv.ui.player +import android.content.Context +import android.net.Uri import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement @@ -33,6 +35,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -49,6 +52,8 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp @@ -56,6 +61,16 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.Player.REPEAT_MODE_ALL +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultDataSource +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.session.MediaSession +import androidx.media3.ui.compose.PlayerSurface +import androidx.media3.ui.compose.modifiers.resizeWithContentScale import androidx.tv.material3.Button import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -85,7 +100,7 @@ fun PlayerScreen( backToHome: () -> Unit, showDetails: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - playScreenViewModel: PlayerScreenViewModel = hiltViewModel() + playScreenViewModel: PlayerScreenViewModel = hiltViewModel(), ) { val uiState by playScreenViewModel.uiStateFlow.collectAsStateWithLifecycle() @@ -112,6 +127,7 @@ fun PlayerScreen( } } +@androidx.annotation.OptIn(UnstableApi::class) @Composable private fun Player( episodePlayerState: EpisodePlayerState, @@ -125,7 +141,7 @@ private fun Player( showDetails: (PlayerEpisode) -> Unit, playEpisode: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - autoStart: Boolean = true + autoStart: Boolean = true, ) { LaunchedEffect(key1 = autoStart) { if (autoStart && !episodePlayerState.isPlaying) { @@ -136,22 +152,67 @@ private fun Player( val currentEpisode = episodePlayerState.currentEpisode if (currentEpisode != null) { - EpisodePlayerWithBackground( - playerEpisode = currentEpisode, - queue = EpisodeList(episodePlayerState.queue), - isPlaying = episodePlayerState.isPlaying, - timeElapsed = episodePlayerState.timeElapsed, - play = play, - pause = pause, - previous = previous, - next = next, - skip = skip, - rewind = rewind, - enqueue = enqueue, - showDetails = showDetails, - playEpisode = playEpisode, - modifier = modifier, - ) + val context = LocalContext.current + + val exoPlayer = rememberPlayer(context) + + DisposableEffect(exoPlayer, playEpisode) { + exoPlayer.setMediaItem(MediaItem.fromUri(Uri.parse(currentEpisode.mediaUrls[0]))) + val mediaSession = MediaSession.Builder(context, exoPlayer).build() + exoPlayer.prepare() + exoPlayer.play() + onDispose { + mediaSession.release() + exoPlayer.release() + } + } + // Adding PlayerSurface at the bottom of the stack + // as it is just the audio player + Box { + PlayerSurface( + player = exoPlayer, + modifier = Modifier.resizeWithContentScale( + contentScale = ContentScale.Fit, + sourceSizeDp = null, + ), + ) + EpisodePlayerWithBackground( + playerEpisode = currentEpisode, + queue = EpisodeList(episodePlayerState.queue), + isPlaying = episodePlayerState.isPlaying, + timeElapsed = episodePlayerState.timeElapsed, + play = ( + { + play() + exoPlayer.play() + } + ), + pause = ( + { + pause() + exoPlayer.pause() + } + ), + previous = previous, + next = next, + skip = ( + { + skip() + exoPlayer.seekForward() + } + ), + rewind = ( + { + rewind() + exoPlayer.seekBack() + } + ), + enqueue = enqueue, + showDetails = showDetails, + playEpisode = playEpisode, + modifier = modifier, + ) + } } } @@ -171,7 +232,7 @@ private fun EpisodePlayerWithBackground( enqueue: (PlayerEpisode) -> Unit, showDetails: (PlayerEpisode) -> Unit, playEpisode: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val episodePlayer = remember { FocusRequester() } @@ -182,7 +243,7 @@ private fun EpisodePlayerWithBackground( BackgroundContainer( playerEpisode = playerEpisode, modifier = modifier, - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { EpisodePlayer( @@ -199,7 +260,7 @@ private fun EpisodePlayerWithBackground( showDetails = showDetails, focusRequester = episodePlayer, modifier = Modifier - .padding(JetcasterAppDefaults.overScanMargin.player.intoPaddingValues()) + .padding(JetcasterAppDefaults.overScanMargin.player.intoPaddingValues()), ) PlayerQueueOverlay( @@ -230,7 +291,7 @@ private fun EpisodePlayer( modifier: Modifier = Modifier, bringIntoViewRequester: BringIntoViewRequester = remember { BringIntoViewRequester() }, coroutineScope: CoroutineScope = rememberCoroutineScope(), - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { Column( verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.section), @@ -243,7 +304,7 @@ private fun EpisodePlayer( } } } - .then(modifier) + .then(modifier), ) { EpisodeDetails( playerEpisode = playerEpisode, @@ -251,7 +312,7 @@ private fun EpisodePlayer( controls = { EpisodeControl( showDetails = { showDetails(playerEpisode) }, - enqueue = { enqueue(playerEpisode) } + enqueue = { enqueue(playerEpisode) }, ) }, ) @@ -265,28 +326,24 @@ private fun EpisodePlayer( next = next, skip = skip, rewind = rewind, - focusRequester = focusRequester + focusRequester = focusRequester, ) } } @Composable -private fun EpisodeControl( - showDetails: () -> Unit, - enqueue: () -> Unit, - modifier: Modifier = Modifier, -) { +private fun EpisodeControl(showDetails: () -> Unit, enqueue: () -> Unit, modifier: Modifier = Modifier) { Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.item) + horizontalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.item), ) { EnqueueButton( onClick = enqueue, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.default.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.default.intoDpSize()), ) InfoButton( onClick = showDetails, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.default.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.default.intoDpSize()), ) } } @@ -303,7 +360,7 @@ private fun PlayerControl( skip: () -> Unit, rewind: () -> Unit, modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { val playPauseButton = remember { FocusRequester() } @@ -314,7 +371,7 @@ private fun PlayerControl( Row( horizontalArrangement = Arrangement.spacedBy( JetcasterAppDefaults.gap.default, - Alignment.CenterHorizontally + Alignment.CenterHorizontally, ), verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -329,11 +386,11 @@ private fun PlayerControl( ) { PreviousButton( onClick = previous, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()), ) RewindButton( onClick = rewind, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()), ) PlayPauseButton( isPlaying = isPlaying, @@ -346,15 +403,15 @@ private fun PlayerControl( }, modifier = Modifier .size(JetcasterAppDefaults.iconButtonSize.large.intoDpSize()) - .focusRequester(playPauseButton) + .focusRequester(playPauseButton), ) SkipButton( onClick = skip, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()), ) NextButton( onClick = next, - modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()) + modifier = Modifier.size(JetcasterAppDefaults.iconButtonSize.medium.intoDpSize()), ) } if (length != null) { @@ -370,11 +427,11 @@ private fun ElapsedTimeIndicator( skip: () -> Unit, rewind: () -> Unit, modifier: Modifier = Modifier, - knobSize: Dp = 8.dp + knobSize: Dp = 8.dp, ) { Column( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.tiny) + verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.tiny), ) { ElapsedTime(timeElapsed = timeElapsed, length = length) Seekbar( @@ -383,7 +440,7 @@ private fun ElapsedTimeIndicator( knobSize = knobSize, onMoveLeft = rewind, onMoveRight = skip, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } @@ -393,20 +450,20 @@ private fun ElapsedTime( timeElapsed: Duration, length: Duration, modifier: Modifier = Modifier, - style: TextStyle = MaterialTheme.typography.bodySmall + style: TextStyle = MaterialTheme.typography.bodySmall, ) { val elapsed = stringResource( R.string.minutes_seconds, timeElapsed.toMinutes(), - timeElapsed.toSeconds() % 60 + timeElapsed.toSeconds() % 60, ) val l = stringResource(R.string.minutes_seconds, length.toMinutes(), length.toSeconds() % 60) Text( text = stringResource(R.string.elapsed_time, elapsed, l), style = style, - modifier = modifier + modifier = modifier, ) } @@ -414,7 +471,7 @@ private fun ElapsedTime( private fun NoEpisodeInQueue( backToHome: () -> Unit, modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { LaunchedEffect(Unit) { focusRequester.requestFocus() @@ -423,7 +480,7 @@ private fun NoEpisodeInQueue( Column { Text( text = stringResource(R.string.display_nothing_in_queue), - style = MaterialTheme.typography.displayMedium + style = MaterialTheme.typography.displayMedium, ) Spacer(modifier = Modifier.height(JetcasterAppDefaults.gap.paragraph)) Text(text = stringResource(R.string.message_nothing_in_queue)) @@ -474,7 +531,22 @@ private fun PlayerQueueOverlay( contentPadding = contentPadding, modifier = Modifier .offset(actualOffset.x, actualOffset.y) - .onFocusChanged { hasFocus = it.hasFocus } + .onFocusChanged { hasFocus = it.hasFocus }, ) } } + +@androidx.annotation.OptIn(UnstableApi::class) +@Composable +internal fun rememberPlayer(context: Context) = remember { + ExoPlayer.Builder(context) + .setSeekForwardIncrementMs(10 * 1000) + .setSeekBackIncrementMs(10 * 1000) + .setMediaSourceFactory(ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))) + .setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING) + .build() + .apply { + playWhenReady = true + repeatMode = REPEAT_MODE_ALL + } +} diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreenViewModel.kt index 9b66a9359d..a03381604e 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/player/PlayerScreenViewModel.kt @@ -29,9 +29,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @HiltViewModel -class PlayerScreenViewModel @Inject constructor( - private val episodePlayer: EpisodePlayer, -) : ViewModel() { +class PlayerScreenViewModel @Inject constructor(private val episodePlayer: EpisodePlayer) : ViewModel() { val uiStateFlow = episodePlayer.playerState.map { if (it.currentEpisode == null && it.queue.isEmpty()) { @@ -42,7 +40,7 @@ class PlayerScreenViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - PlayerScreenUiState.Loading + PlayerScreenUiState.Loading, ) private val skipAmount = Duration.ofSeconds(10L) @@ -75,9 +73,7 @@ class PlayerScreenViewModel @Inject constructor( sealed interface PlayerScreenUiState { data object Loading : PlayerScreenUiState - data class Ready( - val playerState: EpisodePlayerState - ) : PlayerScreenUiState + data class Ready(val playerState: EpisodePlayerState) : PlayerScreenUiState data object NoEpisodeInQueue : PlayerScreenUiState } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreen.kt index 26e84b7dc8..75b4b743fb 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreen.kt @@ -112,7 +112,7 @@ private fun PodcastDetailsWithBackground( showEpisodeDetails: (PlayerEpisode) -> Unit, enqueue: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { BackgroundContainer(podcastInfo = podcastInfo, modifier = modifier) { @@ -127,7 +127,7 @@ private fun PodcastDetailsWithBackground( showEpisodeDetails = showEpisodeDetails, enqueue = enqueue, modifier = Modifier - .fillMaxSize() + .fillMaxSize(), ) } } @@ -144,7 +144,7 @@ private fun PodcastDetails( showEpisodeDetails: (PlayerEpisode) -> Unit, enqueue: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, - focusRequester: FocusRequester = remember { FocusRequester() } + focusRequester: FocusRequester = remember { FocusRequester() }, ) { TwoColumn( modifier = modifier, @@ -160,7 +160,7 @@ private fun PodcastDetails( .weight(0.3f) .padding( JetcasterAppDefaults.overScanMargin.podcast.copy(end = 0.dp) - .intoPaddingValues() + .intoPaddingValues(), ), ) }, @@ -173,9 +173,9 @@ private fun PodcastDetails( modifier = Modifier .focusRequester(focusRequester) .focusRestorer() - .weight(0.7f) + .weight(0.7f), ) - } + }, ) LaunchedEffect(Unit) { @@ -197,7 +197,7 @@ private fun PodcastInfo( Text( text = podcastInfo.author, - style = MaterialTheme.typography.bodySmall + style = MaterialTheme.typography.bodySmall, ) Text( text = podcastInfo.title, @@ -207,7 +207,7 @@ private fun PodcastInfo( text = podcastInfo.description, maxLines = 2, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) ToggleSubscriptionButton( podcastInfo, @@ -215,7 +215,7 @@ private fun PodcastInfo( subscribe, unsubscribe, modifier = Modifier - .padding(top = JetcasterAppDefaults.gap.podcastRow) + .padding(top = JetcasterAppDefaults.gap.podcastRow), ) } } @@ -226,7 +226,7 @@ private fun ToggleSubscriptionButton( isSubscribed: Boolean, subscribe: (PodcastInfo, Boolean) -> Unit, unsubscribe: (PodcastInfo, Boolean) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val icon = if (isSubscribed) { Icons.Default.Remove @@ -248,7 +248,7 @@ private fun ToggleSubscriptionButton( icon = icon, onClick = { action(podcastInfo, isSubscribed) }, scale = ButtonDefaults.scale(scale = 1f), - modifier = modifier + modifier = modifier, ) } @@ -258,12 +258,12 @@ private fun PodcastEpisodeList( playEpisode: (PlayerEpisode) -> Unit, showDetails: (PlayerEpisode) -> Unit, enqueue: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { LazyColumn( verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.podcastRow), modifier = modifier, - contentPadding = JetcasterAppDefaults.overScanMargin.podcast.intoPaddingValues() + contentPadding = JetcasterAppDefaults.overScanMargin.podcast.intoPaddingValues(), ) { items(episodeList) { EpisodeListItem( @@ -321,7 +321,7 @@ private fun EpisodeListItem( .border(borderWidth, borderColor, shape) .background(backgroundColor) .shadow(elevation, shape) - .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 16.dp) + .padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 16.dp), ) } @@ -337,7 +337,7 @@ private fun EpisodeListItemContentLayer( val playButton = remember { FocusRequester() } Box( contentAlignment = Alignment.CenterStart, - modifier = modifier + modifier = modifier, ) { Column( @@ -348,11 +348,11 @@ private fun EpisodeListItemContentLayer( horizontalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.default), verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(top = JetcasterAppDefaults.gap.paragraph) + .padding(top = JetcasterAppDefaults.gap.paragraph), ) { PlayButton( onClick = onEpisodeSelected, - modifier = Modifier.focusRequester(playButton) + modifier = Modifier.focusRequester(playButton), ) if (duration != null) { EpisodeDataAndDuration(playerEpisode.published, duration) @@ -370,6 +370,6 @@ private fun EpisodeTitle(playerEpisode: PlayerEpisode, modifier: Modifier = Modi Text( text = playerEpisode.title, style = MaterialTheme.typography.titleLarge, - modifier = modifier + modifier = modifier, ) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreenViewModel.kt index c68033c656..09731eca09 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/podcast/PodcastDetailsScreenViewModel.kt @@ -76,7 +76,7 @@ class PodcastDetailsScreenViewModel @Inject constructor( val uiStateFlow = combine( podcastFlow, episodeListFlow, - subscribedPodcastListFlow + subscribedPodcastListFlow, ) { podcast, episodeList, subscribedPodcastList -> if (podcast != null) { val isSubscribed = subscribedPodcastList.any { it.podcast.uri == podcastUri } @@ -87,7 +87,7 @@ class PodcastDetailsScreenViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - PodcastScreenUiState.Loading + PodcastScreenUiState.Loading, ) fun subscribe(podcastInfo: PodcastInfo, isSubscribed: Boolean) { @@ -118,9 +118,5 @@ class PodcastDetailsScreenViewModel @Inject constructor( sealed interface PodcastScreenUiState { data object Loading : PodcastScreenUiState data object Error : PodcastScreenUiState - data class Ready( - val podcastInfo: PodcastInfo, - val episodeList: EpisodeList, - val isSubscribed: Boolean - ) : PodcastScreenUiState + data class Ready(val podcastInfo: PodcastInfo, val episodeList: EpisodeList, val isSubscribed: Boolean) : PodcastScreenUiState } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt index f4b7cd100c..0fd3bb0ee9 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt @@ -68,7 +68,7 @@ import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults fun SearchScreen( onPodcastSelected: (PodcastInfo) -> Unit, modifier: Modifier = Modifier, - searchScreenViewModel: SearchScreenViewModel = hiltViewModel() + searchScreenViewModel: SearchScreenViewModel = hiltViewModel(), ) { val uiState by searchScreenViewModel.uiStateFlow.collectAsState() @@ -80,7 +80,7 @@ fun SearchScreen( onKeywordInput = searchScreenViewModel::setKeyword, onCategorySelected = searchScreenViewModel::addCategoryToSelectedCategoryList, onCategoryUnselected = searchScreenViewModel::removeCategoryFromSelectedCategoryList, - modifier = modifier + modifier = modifier, ) is SearchScreenUiState.HasResult -> HasResult( @@ -103,7 +103,7 @@ private fun Ready( onKeywordInput: (String) -> Unit, onCategorySelected: (CategoryInfo) -> Unit, onCategoryUnselected: (CategoryInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Controls( keyword = keyword, @@ -112,7 +112,7 @@ private fun Ready( onCategorySelected = onCategorySelected, onCategoryUnselected = onCategoryUnselected, modifier = modifier, - toRequestFocus = true + toRequestFocus = true, ) } @@ -125,7 +125,7 @@ private fun HasResult( onCategorySelected: (CategoryInfo) -> Unit, onCategoryUnselected: (CategoryInfo) -> Unit, onPodcastSelected: (PodcastInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { SearchResult( podcastList = podcastList, @@ -139,7 +139,7 @@ private fun HasResult( onCategoryUnselected = onCategoryUnselected, ) }, - modifier = modifier + modifier = modifier, ) } @@ -153,7 +153,7 @@ private fun Controls( onCategoryUnselected: (CategoryInfo) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() }, - toRequestFocus: Boolean = false + toRequestFocus: Boolean = false, ) { LaunchedEffect(toRequestFocus) { if (toRequestFocus) { @@ -163,7 +163,7 @@ private fun Controls( Column( verticalArrangement = Arrangement.spacedBy(JetcasterAppDefaults.gap.item), - modifier = modifier + modifier = modifier, ) { KeywordInput( keyword = keyword, @@ -175,19 +175,15 @@ private fun Controls( onCategoryUnselected = onCategoryUnselected, modifier = Modifier .focusRestorer() - .focusRequester(focusRequester) + .focusRequester(focusRequester), ) } } @Composable -private fun KeywordInput( - keyword: String, - onKeywordInput: (String) -> Unit, - modifier: Modifier = Modifier, -) { +private fun KeywordInput(keyword: String, onKeywordInput: (String) -> Unit, modifier: Modifier = Modifier) { val textStyle = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) val cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) BasicTextField( @@ -203,22 +199,22 @@ private fun KeywordInput( .fillMaxWidth() .background( MaterialTheme.colorScheme.surfaceVariant, - RoundedCornerShape(percent = 50) - ) + RoundedCornerShape(percent = 50), + ), ) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Icon( Icons.Default.Search, contentDescription = stringResource(R.string.label_search), - modifier = Modifier.padding(end = 12.dp) + modifier = Modifier.padding(end = 12.dp), ) innerTextField() } } - } + }, ) } @@ -228,7 +224,7 @@ private fun CategorySelection( categorySelectionList: CategorySelectionList, onCategorySelected: (CategoryInfo) -> Unit, onCategoryUnselected: (CategoryInfo) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { FlowRow( modifier = modifier, @@ -244,7 +240,7 @@ private fun CategorySelection( } else { onCategorySelected(it.categoryInfo) } - } + }, ) { Text(text = it.categoryInfo.name) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt index d16d143b7b..e6df198458 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt @@ -55,7 +55,7 @@ class SearchScreenViewModel @Inject constructor( combine( keywordFlow, selectedCategoryListFlow, - categoryInfoListFlow + categoryInfoListFlow, ) { keyword, selectedCategories, categories -> val selected = selectedCategories.ifEmpty { categories @@ -67,18 +67,18 @@ class SearchScreenViewModel @Inject constructor( private val searchResultFlow = searchConditionFlow.flatMapLatest { podcastStore.searchPodcastByTitleAndCategories( it.keyword, - it.selectedCategories.intoCategoryList() + it.selectedCategories.intoCategoryList(), ) }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - emptyList() + emptyList(), ) private val categorySelectionFlow = combine( categoryInfoListFlow, - selectedCategoryListFlow + selectedCategoryListFlow, ) { categoryList, selectedCategories -> val list = categoryList.map { CategorySelection(it, selectedCategories.contains(it)) @@ -90,7 +90,7 @@ class SearchScreenViewModel @Inject constructor( combine( keywordFlow, categorySelectionFlow, - searchResultFlow + searchResultFlow, ) { keyword, categorySelection, result -> val podcastList = result.map { it.asExternalModel() } when { @@ -133,20 +133,14 @@ class SearchScreenViewModel @Inject constructor( private data class SearchCondition(val keyword: String, val selectedCategories: CategoryInfoList) { constructor(keyword: String, categoryInfoList: List) : this( keyword, - CategoryInfoList(categoryInfoList) + CategoryInfoList(categoryInfoList), ) } sealed interface SearchScreenUiState { data object Loading : SearchScreenUiState - data class Ready( - val keyword: String, - val categorySelectionList: CategorySelectionList - ) : SearchScreenUiState - - data class HasResult( - val keyword: String, - val categorySelectionList: CategorySelectionList, - val result: PodcastList - ) : SearchScreenUiState + data class Ready(val keyword: String, val categorySelectionList: CategorySelectionList) : SearchScreenUiState + + data class HasResult(val keyword: String, val categorySelectionList: CategorySelectionList, val result: PodcastList) : + SearchScreenUiState } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/settings/SettingsScreen.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/settings/SettingsScreen.kt index 53bf32f50c..c7a54f16d4 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/settings/SettingsScreen.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/settings/SettingsScreen.kt @@ -21,8 +21,6 @@ import androidx.compose.ui.Modifier import com.example.jetcaster.tv.ui.component.NotAvailableFeature @Composable -fun SettingsScreen( - modifier: Modifier = Modifier -) { +fun SettingsScreen(modifier: Modifier = Modifier) { NotAvailableFeature(modifier = modifier) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt index def4a37865..ad8cdb0a19 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt @@ -39,37 +39,28 @@ internal data class OverScanMarginSettings( top = 40.dp, bottom = 40.dp, start = 80.dp, - end = 80.dp + end = 80.dp, ), val player: OverScanMargin = OverScanMargin( top = 40.dp, bottom = 40.dp, start = 80.dp, - end = 80.dp + end = 80.dp, ), ) -internal data class OverScanMargin( - val top: Dp = 24.dp, - val bottom: Dp = 24.dp, - val start: Dp = 48.dp, - val end: Dp = 48.dp, -) { +internal data class OverScanMargin(val top: Dp = 24.dp, val bottom: Dp = 24.dp, val start: Dp = 48.dp, val end: Dp = 48.dp) { fun intoPaddingValues(): PaddingValues { return PaddingValues(start, top, end, bottom) } } -internal data class CardWidth( - val large: Dp = 268.dp, - val medium: Dp = 196.dp, - val small: Dp = 124.dp -) +internal data class CardWidth(val large: Dp = 268.dp, val medium: Dp = 196.dp, val small: Dp = 124.dp) internal data class ThumbnailSize( val episodeDetails: DpSize = DpSize(266.dp, 266.dp), val podcast: DpSize = DpSize(196.dp, 196.dp), - val episode: DpSize = DpSize(124.dp, 124.dp) + val episode: DpSize = DpSize(124.dp, 124.dp), ) internal data class PaddingSettings( @@ -98,7 +89,7 @@ internal data class GapSettings( internal data class IconButtonSize( val default: Radius = Radius(14.dp), val medium: Radius = Radius(20.dp), - val large: Radius = Radius(28.dp) + val large: Radius = Radius(28.dp), ) internal data class Radius(private val value: Dp) { diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Theme.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Theme.kt index f895300f78..c490ce6cd5 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Theme.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Theme.kt @@ -21,10 +21,7 @@ import androidx.compose.runtime.Composable import androidx.tv.material3.MaterialTheme @Composable -fun JetcasterTheme( - isInDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit, -) { +fun JetcasterTheme(isInDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colorScheme = if (isInDarkTheme) { colorSchemeForDarkMode } else { @@ -33,6 +30,6 @@ fun JetcasterTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } diff --git a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Type.kt b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Type.kt index 1be9cc97c1..8075fe231d 100644 --- a/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Type.kt +++ b/Jetcaster/tv/src/main/java/com/example/jetcaster/tv/ui/theme/Type.kt @@ -113,5 +113,5 @@ val Typography = Typography( fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp, - ) + ), ) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/JetcasterWearApplication.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/JetcasterWearApplication.kt index a633b967a8..a5d2125935 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/JetcasterWearApplication.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/JetcasterWearApplication.kt @@ -24,7 +24,9 @@ import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject @HiltAndroidApp -class JetcasterWearApplication : Application(), ImageLoaderFactory { +class JetcasterWearApplication : + Application(), + ImageLoaderFactory { @Inject lateinit var imageLoader: ImageLoader @@ -44,6 +46,5 @@ class JetcasterWearApplication : Application(), ImageLoaderFactory { ) } - override fun newImageLoader(): ImageLoader = - imageLoader + override fun newImageLoader(): ImageLoader = imageLoader } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/MainActivity.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/MainActivity.kt index 64577ff932..e567f8e4f0 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/MainActivity.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/MainActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2022-2025 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. diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt index 997fea950f..67014e02bf 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/WearApp.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Android Open Source Project + * Copyright 2024-2025 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. @@ -87,7 +87,7 @@ fun WearApp(navController: NavHostController) { volumeViewModel = volumeViewModel, onVolumeClick = { navController.navigateToVolume() - } + }, ) }, libraryScreen = { @@ -118,13 +118,13 @@ fun WearApp(navController: NavHostController) { onPlayButtonClick = { navController.navigateToPlayer() }, - onDismiss = { navController.popBackStack() } + onDismiss = { navController.popBackStack() }, ) } composable(route = YourPodcasts.navRoute) { PodcastsScreen( onPodcastsItemClick = { navController.navigateToPodcastDetails(it.uri) }, - onDismiss = { navController.popBackStack() } + onDismiss = { navController.popBackStack() }, ) } composable(route = PodcastDetails.navRoute) { @@ -133,7 +133,7 @@ fun WearApp(navController: NavHostController) { navController.navigateToPlayer() }, onEpisodeItemClick = { navController.navigateToEpisode(it.uri) }, - onDismiss = { navController.popBackStack() } + onDismiss = { navController.popBackStack() }, ) } composable(route = UpNext.navRoute) { @@ -145,7 +145,7 @@ fun WearApp(navController: NavHostController) { onDismiss = { navController.popBackStack() navController.navigateToYourPodcast() - } + }, ) } composable(route = Episode.navRoute) { @@ -156,7 +156,7 @@ fun WearApp(navController: NavHostController) { onDismiss = { navController.popBackStack() navController.navigateToYourPodcast() - } + }, ) } } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Color.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Color.kt index 2bb7c18ffa..cbec73b344 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Color.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Color.kt @@ -34,5 +34,5 @@ internal val wearColorPalette: Colors = Colors( error = errorDark, onPrimary = onPrimaryDark, onSecondary = onSecondaryDark, - onError = onErrorDark + onError = onErrorDark, ) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Type.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Type.kt index 2ce188903a..39eecfb363 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Type.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/Type.kt @@ -27,8 +27,8 @@ val Typography = Typography( body1 = TextStyle( fontFamily = Montserrat, fontWeight = FontWeight.Normal, - fontSize = 16.sp - ) + fontSize = 16.sp, + ), /* Other default text styles to override button = TextStyle( fontFamily = FontFamily.Default, diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/WearAppTheme.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/WearAppTheme.kt index c797a8c041..b8411cdd27 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/WearAppTheme.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/theme/WearAppTheme.kt @@ -20,14 +20,12 @@ import androidx.compose.runtime.Composable import androidx.wear.compose.material.MaterialTheme @Composable -fun WearAppTheme( - content: @Composable () -> Unit -) { +fun WearAppTheme(content: @Composable () -> Unit) { MaterialTheme( colors = wearColorPalette, typography = Typography, // For shapes, we generally recommend using the default Material Wear shapes which are // optimized for round and non-round devices. - content = content + content = content, ) } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt index fe8b33c1d7..cf5e6aa4a7 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt @@ -33,7 +33,7 @@ fun MediaContent( episode: PlayerEpisode, episodeArtworkPlaceholder: Painter?, onItemClick: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val mediaTitle = episode.title val duration = episode.duration @@ -45,7 +45,7 @@ fun MediaContent( stringResource( R.string.episode_date_duration, MediumDateFormatter.format(episode.published), - duration.toMinutes().toInt() + duration.toMinutes().toInt(), ) } // Otherwise we just use the date @@ -59,7 +59,7 @@ fun MediaContent( icon = CoilPaintable(episode.podcastImageUrl, episodeArtworkPlaceholder), largeIcon = true, colors = ChipDefaults.secondaryChipColors(), - modifier = modifier + modifier = modifier, ) } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/SettingsButtons.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/SettingsButtons.kt index 91f32debc5..86684701ca 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/SettingsButtons.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/SettingsButtons.kt @@ -55,7 +55,7 @@ fun SettingsButtons( currentPlayerSpeed = playerUiState.episodePlayerState .playbackSpeed.toMillis().toFloat() / 1000, onPlaybackSpeedChange = onPlaybackSpeedChange, - enabled = enabled + enabled = enabled, ) SettingsButtonsDefaults.BrandIcon( @@ -66,7 +66,7 @@ fun SettingsButtons( SetVolumeButton( onVolumeClick = onVolumeClick, volumeUiState = volumeUiState, - enabled = enabled + enabled = enabled, ) } } @@ -86,7 +86,9 @@ fun PlaybackSpeedButton( when (currentPlayerSpeed) { 1f -> ImageVector.vectorResource(R.drawable.speed_1x) 1.5f -> ImageVector.vectorResource(R.drawable.speed_15x) - else -> { ImageVector.vectorResource(R.drawable.speed_2x) } + else -> { + ImageVector.vectorResource(R.drawable.speed_2x) + } }, iconRtlMode = IconRtlMode.Mirrored, contentDescription = stringResource(R.string.change_playback_speed_content_description), diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt index d03b8fb2dd..02b68ff9d1 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt @@ -63,7 +63,7 @@ fun EpisodeScreen( onPlayButtonClick: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, - episodeViewModel: EpisodeViewModel = hiltViewModel() + episodeViewModel: EpisodeViewModel = hiltViewModel(), ) { val uiState by episodeViewModel.uiState.collectAsStateWithLifecycle() @@ -89,12 +89,12 @@ fun EpisodeScreen( val columnState = rememberResponsiveColumnState( contentPadding = padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (uiState) { is EpisodeScreenState.Loaded -> { @@ -103,12 +103,12 @@ fun EpisodeScreen( EntityScreen( headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text( text = title, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, ) } }, @@ -117,12 +117,12 @@ fun EpisodeScreen( episode = uiState.episode, onPlayButtonClick = onPlayButtonClick, onPlayEpisode = onPlayEpisode, - onAddToQueue = onAddToQueue + onAddToQueue = onAddToQueue, ) }, content = { episodeInfoContent(episode = uiState.episode) - } + }, ) } @@ -131,7 +131,7 @@ fun EpisodeScreen( AlertDialog( showDialog = true, onDismiss = { onDismiss }, - message = stringResource(R.string.episode_info_not_available) + message = stringResource(R.string.episode_info_not_available), ) } EpisodeScreenState.Loading -> { @@ -148,7 +148,7 @@ fun LoadedButtonsContent( onPlayButtonClick: () -> Unit, onPlayEpisode: (PlayerEpisode) -> Unit, onAddToQueue: (PlayerEpisode) -> Unit, - enabled: Boolean = true + enabled: Boolean = true, ) { Row( @@ -187,7 +187,7 @@ fun LoadingScreen() { EntityScreen( headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(R.string.loading)) } @@ -199,7 +199,7 @@ fun LoadingScreen() { items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } @@ -246,7 +246,7 @@ private fun ScalingLazyListScope.episodeInfoContent(episode: EpisodeToPodcast) { text = author, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.body2 + style = MaterialTheme.typography.body2, ) } } @@ -260,7 +260,7 @@ private fun ScalingLazyListScope.episodeInfoContent(episode: EpisodeToPodcast) { stringResource( R.string.episode_date_duration, MediumDateFormatter.format(published), - duration.toMinutes().toInt() + duration.toMinutes().toInt(), ) } // Otherwise we just use the date @@ -270,7 +270,7 @@ private fun ScalingLazyListScope.episodeInfoContent(episode: EpisodeToPodcast) { overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.body2, modifier = Modifier - .padding(horizontal = 8.dp) + .padding(horizontal = 8.dp), ) } if (summary != null) { @@ -281,7 +281,7 @@ private fun ScalingLazyListScope.episodeInfoContent(episode: EpisodeToPodcast) { text = it, style = MaterialTheme.typography.body2, color = LocalContentColor.current, - modifier = Modifier.listTextPadding() + modifier = Modifier.listTextPadding(), ) } } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeViewModel.kt index 1381462913..7a15b622b3 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeViewModel.kt @@ -72,7 +72,7 @@ class EpisodeViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - null + null, ) val uiState: StateFlow = @@ -102,9 +102,7 @@ sealed interface EpisodeScreenState { data object Loading : EpisodeScreenState - data class Loaded( - val episode: EpisodeToPodcast - ) : EpisodeScreenState + data class Loaded(val episode: EpisodeToPodcast) : EpisodeScreenState data object Empty : EpisodeScreenState } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodeViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodeViewModel.kt index cc78891709..c0324b2816 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodeViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodeViewModel.kt @@ -41,7 +41,7 @@ class LatestEpisodeViewModel @Inject constructor( LatestEpisodeScreenState.Loaded( episodeToPodcastList.map { it.toPlayerEpisode() - } + }, ) } else { LatestEpisodeScreenState.Empty @@ -67,9 +67,7 @@ sealed interface LatestEpisodeScreenState { data object Loading : LatestEpisodeScreenState - data class Loaded( - val episodeList: List - ) : LatestEpisodeScreenState + data class Loaded(val episodeList: List) : LatestEpisodeScreenState data object Empty : LatestEpisodeScreenState } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt index 732e0f68c8..4cd2952505 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt @@ -56,7 +56,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen onPlayButtonClick: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, - latestEpisodeViewModel: LatestEpisodeViewModel = hiltViewModel() + latestEpisodeViewModel: LatestEpisodeViewModel = hiltViewModel(), ) { val uiState by latestEpisodeViewModel.uiState.collectAsStateWithLifecycle() LatestEpisodeScreen( @@ -65,7 +65,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen onPlayButtonClick = onPlayButtonClick, onDismiss = onDismiss, onPlayEpisodes = latestEpisodeViewModel::onPlayEpisodes, - onPlayEpisode = latestEpisodeViewModel::onPlayEpisode + onPlayEpisode = latestEpisodeViewModel::onPlayEpisode, ) } @@ -81,12 +81,12 @@ fun LatestEpisodeScreen( val columnState = rememberResponsiveColumnState( contentPadding = padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (uiState) { is LatestEpisodeScreenState.Loaded -> { @@ -95,7 +95,7 @@ fun LatestEpisodeScreen( onPlayButtonClick = onPlayButtonClick, onPlayEpisode = onPlayEpisode, onPlayEpisodes = onPlayEpisodes, - modifier = modifier + modifier = modifier, ) } @@ -103,13 +103,13 @@ fun LatestEpisodeScreen( AlertDialog( showDialog = true, onDismiss = onDismiss, - message = stringResource(R.string.podcasts_no_episode_podcasts) + message = stringResource(R.string.podcasts_no_episode_podcasts), ) } is LatestEpisodeScreenState.Loading -> { LatestEpisodesScreenLoading( - modifier = modifier + modifier = modifier, ) } } @@ -122,7 +122,7 @@ fun ButtonsContent( episodes: List, onPlayButtonClick: () -> Unit, onPlayEpisodes: (List) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Chip( label = stringResource(id = R.string.button_play_content_description), @@ -141,15 +141,15 @@ fun LatestEpisodesScreen( onPlayButtonClick: () -> Unit, onPlayEpisode: (PlayerEpisode) -> Unit, onPlayEpisodes: (List) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { EntityScreen( modifier = modifier, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { - Text(text = stringResource(id = R.string.latest_episodes),) + Text(text = stringResource(id = R.string.latest_episodes)) } }, content = { @@ -163,7 +163,7 @@ fun LatestEpisodesScreen( onItemClick = { onPlayButtonClick() onPlayEpisode(episodeList[index]) - } + }, ) } }, @@ -171,7 +171,7 @@ fun LatestEpisodesScreen( ButtonsContent( episodes = episodeList, onPlayButtonClick = onPlayButtonClick, - onPlayEpisodes = onPlayEpisodes + onPlayEpisodes = onPlayEpisodes, ) }, ) @@ -179,16 +179,14 @@ fun LatestEpisodesScreen( @OptIn(ExperimentalWearMaterialApi::class) @Composable -fun LatestEpisodesScreenLoading( - modifier: Modifier = Modifier -) { +fun LatestEpisodesScreenLoading(modifier: Modifier = Modifier) { EntityScreen( modifier = modifier, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { - Text(text = stringResource(id = R.string.latest_episodes),) + Text(text = stringResource(id = R.string.latest_episodes)) } }, content = { @@ -211,19 +209,19 @@ fun LatestEpisodesScreenLoading( @Composable fun LatestEpisodeScreenLoadedPreview( @PreviewParameter(WearPreviewEpisodes::class) - episode: PlayerEpisode + episode: PlayerEpisode, ) { val columnState = rememberResponsiveColumnState( contentPadding = padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) LatestEpisodesScreen( episodeList = listOf(episode), onPlayButtonClick = { }, onPlayEpisode = { }, - onPlayEpisodes = { } + onPlayEpisodes = { }, ) } @@ -234,8 +232,8 @@ fun LatestEpisodeScreenLoadingPreview() { val columnState = rememberResponsiveColumnState( contentPadding = padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) LatestEpisodesScreenLoading() } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt index 3c80928328..8b722a1496 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt @@ -58,7 +58,7 @@ fun LibraryScreen( onYourPodcastClick: () -> Unit, onUpNextClick: () -> Unit, modifier: Modifier = Modifier, - libraryScreenViewModel: LibraryViewModel = hiltViewModel() + libraryScreenViewModel: LibraryViewModel = hiltViewModel(), ) { val uiState by libraryScreenViewModel.uiState.collectAsState() @@ -72,14 +72,14 @@ fun LibraryScreen( when (val s = uiState) { is LibraryScreenUiState.Loading -> LoadingScreen( - modifier = modifier + modifier = modifier, ) is LibraryScreenUiState.NoSubscribedPodcast -> NoSubscribedPodcastScreen( columnState = columnState, modifier = modifier, topPodcasts = s.topPodcasts, - onTogglePodcastFollowed = libraryScreenViewModel::onTogglePodcastFollowed + onTogglePodcastFollowed = libraryScreenViewModel::onTogglePodcastFollowed, ) is LibraryScreenUiState.Ready -> @@ -89,20 +89,18 @@ fun LibraryScreen( onLatestEpisodeClick = onLatestEpisodeClick, onYourPodcastClick = onYourPodcastClick, onUpNextClick = onUpNextClick, - queue = s.queue + queue = s.queue, ) } } @OptIn(ExperimentalWearMaterialApi::class) @Composable -fun LoadingScreen( - modifier: Modifier, -) { +fun LoadingScreen(modifier: Modifier) { EntityScreen( headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(R.string.loading)) } @@ -112,7 +110,7 @@ fun LoadingScreen( items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } @@ -122,14 +120,14 @@ fun NoSubscribedPodcastScreen( columnState: ScalingLazyColumnState, modifier: Modifier, topPodcasts: List, - onTogglePodcastFollowed: (uri: String) -> Unit + onTogglePodcastFollowed: (uri: String) -> Unit, ) { ScreenScaffold(scrollState = columnState, modifier = modifier) { ScalingLazyColumn(columnState = columnState) { item { ResponsiveListHeader( modifier = modifier.listTextPadding(), - contentColor = MaterialTheme.colors.onSurface + contentColor = MaterialTheme.colors.onSurface, ) { Text(stringResource(R.string.entity_no_featured_podcasts)) } @@ -151,7 +149,7 @@ fun NoSubscribedPodcastScreen( item { PlaceholderChip( contentDescription = "", - colors = ChipDefaults.secondaryChipColors() + colors = ChipDefaults.secondaryChipColors(), ) } } @@ -185,7 +183,7 @@ fun LibraryScreen( onLatestEpisodeClick: () -> Unit, onYourPodcastClick: () -> Unit, onUpNextClick: () -> Unit, - queue: List + queue: List, ) { ScreenScaffold(scrollState = columnState, modifier = modifier) { ScalingLazyColumn(columnState = columnState) { @@ -199,7 +197,7 @@ fun LibraryScreen( label = stringResource(R.string.latest_episodes), onClick = onLatestEpisodeClick, icon = DrawableResPaintable(R.drawable.new_releases), - colors = ChipDefaults.secondaryChipColors() + colors = ChipDefaults.secondaryChipColors(), ) } item { @@ -207,7 +205,7 @@ fun LibraryScreen( label = stringResource(R.string.podcasts), onClick = onYourPodcastClick, icon = DrawableResPaintable(R.drawable.podcast), - colors = ChipDefaults.secondaryChipColors() + colors = ChipDefaults.secondaryChipColors(), ) } item { @@ -223,7 +221,7 @@ fun LibraryScreen( label = stringResource(R.string.up_next), onClick = onUpNextClick, icon = DrawableResPaintable(R.drawable.up_next), - colors = ChipDefaults.secondaryChipColors() + colors = ChipDefaults.secondaryChipColors(), ) } } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt index 27ae2e85e1..ff30dfcac1 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryViewModel.kt @@ -64,7 +64,7 @@ class LibraryViewModel @Inject constructor( private val podcastStore: PodcastStore, private val episodePlayer: EpisodePlayer, private val categoryStore: CategoryStore, - private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase + private val podcastCategoryFilterUseCase: PodcastCategoryFilterUseCase, ) : ViewModel() { private val defaultCategory = categoryStore.getCategory(CategoryTechnology) @@ -100,7 +100,7 @@ class LibraryViewModel @Inject constructor( topPodcastsFlow, followingPodcastListFlow, latestEpisodeListFlow, - queue + queue, ) { topPodcasts, podcastList, episodeList, queue -> if (podcastList.isEmpty()) { LibraryScreenUiState.NoSubscribedPodcast(topPodcasts.topPodcasts) @@ -110,7 +110,7 @@ class LibraryViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - LibraryScreenUiState.Loading + LibraryScreenUiState.Loading, ) init { @@ -132,12 +132,10 @@ class LibraryViewModel @Inject constructor( sealed interface LibraryScreenUiState { data object Loading : LibraryScreenUiState - data class NoSubscribedPodcast( - val topPodcasts: List - ) : LibraryScreenUiState + data class NoSubscribedPodcast(val topPodcasts: List) : LibraryScreenUiState data class Ready( val subscribedPodcastList: List, val latestEpisodeList: List, - val queue: List + val queue: List, ) : LibraryScreenUiState } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt index 7e6f957db2..b24b8645bf 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt @@ -71,7 +71,7 @@ fun PlayerScreen( volumeUiState = volumeUiState, onVolumeClick = onVolumeClick, onUpdateVolume = { newVolume -> volumeViewModel.setVolume(newVolume) }, - modifier = modifier + modifier = modifier, ) } @@ -93,7 +93,7 @@ private fun PlayerScreen( mediaDisplay = { TextMediaDisplay( title = stringResource(R.string.nothing_playing), - subtitle = "" + subtitle = "", ) }, controlButtons = { @@ -105,7 +105,7 @@ private fun PlayerScreen( onSeekBackButtonClick = playerScreenViewModel::onRewindBy, seekBackButtonEnabled = false, onSeekForwardButtonClick = playerScreenViewModel::onAdvanceBy, - seekForwardButtonEnabled = false + seekForwardButtonEnabled = false, ) }, buttons = { @@ -130,12 +130,12 @@ private fun PlayerScreen( if (episode != null && episode.title.isNotEmpty()) { TextMediaDisplay( title = episode.podcastName, - subtitle = episode.title + subtitle = episode.title, ) } else { TextMediaDisplay( title = stringResource(R.string.nothing_playing), - subtitle = "" + subtitle = "", ) } }, @@ -152,7 +152,7 @@ private fun PlayerScreen( seekForwardButtonEnabled = true, seekBackButtonIncrement = SeekButtonIncrement.Ten, seekForwardButtonIncrement = SeekButtonIncrement.Ten, - trackPositionUiModel = state.playerState.trackPositionUiModel + trackPositionUiModel = state.playerState.trackPositionUiModel, ) }, buttons = { @@ -178,7 +178,7 @@ private fun PlayerScreen( defaultColor = MaterialTheme.colors.primary, modifier = Modifier.fillMaxSize(), ) - } + }, ) } } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt index c640b92021..79f93d8d4e 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerViewModel.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalHorologistApi::class) data class PlayerUiState( val episodePlayerState: EpisodePlayerState = EpisodePlayerState(), - var trackPositionUiModel: TrackPositionUiModel = TrackPositionUiModel.Actual.ZERO + var trackPositionUiModel: TrackPositionUiModel = TrackPositionUiModel.Actual.ZERO, ) /** @@ -41,9 +41,7 @@ data class PlayerUiState( */ @HiltViewModel @OptIn(ExperimentalHorologistApi::class) -class PlayerViewModel @Inject constructor( - private val episodePlayer: EpisodePlayer, -) : ViewModel() { +class PlayerViewModel @Inject constructor(private val episodePlayer: EpisodePlayer) : ViewModel() { val uiState = episodePlayer.playerState.map { if (it.currentEpisode == null && it.queue.isEmpty()) { @@ -54,24 +52,23 @@ class PlayerViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - PlayerScreenUiState.Loading + PlayerScreenUiState.Loading, ) - private fun buildPositionModel(it: EpisodePlayerState) = - if (it.currentEpisode != null) { - TrackPositionUiModel.Actual( - percent = it.timeElapsed.toMillis().toFloat() / - ( - it.currentEpisode?.duration?.toMillis() - ?.toFloat() ?: 0f - ), - duration = it.currentEpisode?.duration?.toKotlinDuration() - ?: Duration.ZERO.toKotlinDuration(), - position = it.timeElapsed.toKotlinDuration() - ) - } else { - TrackPositionUiModel.Actual.ZERO - } + private fun buildPositionModel(it: EpisodePlayerState) = if (it.currentEpisode != null) { + TrackPositionUiModel.Actual( + percent = it.timeElapsed.toMillis().toFloat() / + ( + it.currentEpisode?.duration?.toMillis() + ?.toFloat() ?: 0f + ), + duration = it.currentEpisode?.duration?.toKotlinDuration() + ?: Duration.ZERO.toKotlinDuration(), + position = it.timeElapsed.toKotlinDuration(), + ) + } else { + TrackPositionUiModel.Actual.ZERO + } fun onPlay() { episodePlayer.play() @@ -100,9 +97,7 @@ class PlayerViewModel @Inject constructor( sealed class PlayerScreenUiState { data object Loading : PlayerScreenUiState() - data class Ready( - val playerState: PlayerUiState - ) : PlayerScreenUiState() + data class Ready(val playerState: PlayerUiState) : PlayerScreenUiState() data object Empty : PlayerScreenUiState() } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt index cd0644d5e8..e1b2e03f6e 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt @@ -60,7 +60,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen onEpisodeItemClick: (PlayerEpisode) -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, - podcastDetailsViewModel: PodcastDetailsViewModel = hiltViewModel() + podcastDetailsViewModel: PodcastDetailsViewModel = hiltViewModel(), ) { val uiState by podcastDetailsViewModel.uiState.collectAsStateWithLifecycle() @@ -82,24 +82,24 @@ fun PodcastDetailsScreen( modifier: Modifier = Modifier, onEpisodeItemClick: (PlayerEpisode) -> Unit, onPlayEpisode: (List) -> Unit, - onDismiss: () -> Unit + onDismiss: () -> Unit, ) { val columnState = rememberResponsiveColumnState( contentPadding = padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (uiState) { is PodcastDetailsScreenState.Loaded -> { EntityScreen( headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = uiState.podcast.title) } @@ -108,7 +108,7 @@ fun PodcastDetailsScreen( ButtonsContent( episodes = uiState.episodeList, onPlayButtonClick = onPlayButtonClick, - onPlayEpisode = onPlayEpisode + onPlayEpisode = onPlayEpisode, ) }, content = { @@ -119,10 +119,10 @@ fun PodcastDetailsScreen( image = Icons.Default.MusicNote, tintColor = Color.Blue, ), - onEpisodeItemClick + onEpisodeItemClick, ) } - } + }, ) } @@ -130,14 +130,14 @@ fun PodcastDetailsScreen( AlertDialog( showDialog = true, onDismiss = { onDismiss }, - message = stringResource(R.string.podcasts_no_episode_podcasts) + message = stringResource(R.string.podcasts_no_episode_podcasts), ) } PodcastDetailsScreenState.Loading -> { EntityScreen( headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(id = R.string.loading)) } @@ -146,14 +146,14 @@ fun PodcastDetailsScreen( ButtonsContent( episodes = emptyList(), onPlayButtonClick = { }, - onPlayEpisode = { } + onPlayEpisode = { }, ) }, content = { items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } } @@ -162,11 +162,7 @@ fun PodcastDetailsScreen( @OptIn(ExperimentalHorologistApi::class) @Composable -fun ButtonsContent( - episodes: List, - onPlayButtonClick: () -> Unit, - onPlayEpisode: (List) -> Unit, -) { +fun ButtonsContent(episodes: List, onPlayButtonClick: () -> Unit, onPlayEpisode: (List) -> Unit) { Chip( label = stringResource(id = R.string.button_play_content_description), @@ -184,10 +180,7 @@ sealed class PodcastDetailsScreenState { data object Loading : PodcastDetailsScreenState() - data class Loaded( - val episodeList: List, - val podcast: PodcastInfo, - ) : PodcastDetailsScreenState() + data class Loaded(val episodeList: List, val podcast: PodcastInfo) : PodcastDetailsScreenState() data object Empty : PodcastDetailsScreenState() } @@ -197,17 +190,17 @@ sealed class PodcastDetailsScreenState { @Composable fun PodcastDetailsScreenLoadedPreview( @PreviewParameter(WearPreviewEpisodes::class) - episode: PlayerEpisode + episode: PlayerEpisode, ) { PodcastDetailsScreen( uiState = PodcastDetailsScreenState.Loaded( episodeList = listOf(episode), - podcast = PreviewPodcastEpisodes.first().podcast + podcast = PreviewPodcastEpisodes.first().podcast, ), onPlayButtonClick = { }, onEpisodeItemClick = {}, onPlayEpisode = {}, - onDismiss = {} + onDismiss = {}, ) } @@ -216,13 +209,13 @@ fun PodcastDetailsScreenLoadedPreview( @Composable fun PodcastDetailsScreenLoadingPreview( @PreviewParameter(WearPreviewEpisodes::class) - episode: PlayerEpisode + episode: PlayerEpisode, ) { PodcastDetailsScreen( uiState = PodcastDetailsScreenState.Loading, onPlayButtonClick = { }, onEpisodeItemClick = {}, onPlayEpisode = {}, - onDismiss = {} + onDismiss = {}, ) } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt index b8c778044a..8ef4f321d9 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsViewModel.kt @@ -61,7 +61,7 @@ class PodcastDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, episodeStore: EpisodeStore, private val episodePlayer: EpisodePlayer, - podcastStore: PodcastStore + podcastStore: PodcastStore, ) : ViewModel() { private val podcastUri: String = @@ -76,7 +76,7 @@ class PodcastDetailsViewModel @Inject constructor( }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), - null + null, ) private val episodeListFlow = podcastFlow.flatMapLatest { @@ -92,7 +92,7 @@ class PodcastDetailsViewModel @Inject constructor( val uiState: StateFlow = combine( podcastFlow, - episodeListFlow + episodeListFlow, ) { podcast, episodes -> if (podcast != null) { PodcastDetailsScreenState.Loaded( diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt index 55d941d6a3..2c4a79dab1 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt @@ -75,7 +75,7 @@ fun PodcastsScreen( PodcastsScreen( podcastsScreenState = modifiedState, onPodcastsItemClick = onPodcastsItemClick, - onDismiss = onDismiss + onDismiss = onDismiss, ) } @@ -91,12 +91,12 @@ fun PodcastsScreen( val columnState = rememberResponsiveColumnState() ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (podcastsScreenState) { is PodcastsScreenState.Loaded -> PodcastScreenLoaded( podcastList = podcastsScreenState.podcastList, - onPodcastsItemClick = onPodcastsItemClick + onPodcastsItemClick = onPodcastsItemClick, ) PodcastsScreenState.Empty -> PodcastScreenEmpty(onDismiss) @@ -107,85 +107,73 @@ fun PodcastsScreen( } @Composable -fun PodcastScreenLoaded( - podcastList: List, - onPodcastsItemClick: (PodcastInfo) -> Unit, - modifier: Modifier = Modifier -) { +fun PodcastScreenLoaded(podcastList: List, onPodcastsItemClick: (PodcastInfo) -> Unit, modifier: Modifier = Modifier) { EntityScreen( modifier = modifier, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(id = R.string.podcasts)) } }, content = { - items(count = podcastList.size) { - index -> + items(count = podcastList.size) { index -> MediaContent( podcast = podcastList[index], downloadItemArtworkPlaceholder = rememberVectorPainter( image = Icons.Default.MusicNote, tintColor = Color.Blue, ), - onPodcastsItemClick = onPodcastsItemClick + onPodcastsItemClick = onPodcastsItemClick, ) } - } + }, ) } @Composable -fun PodcastScreenEmpty( - onDismiss: () -> Unit, - modifier: Modifier = Modifier -) { +fun PodcastScreenEmpty(onDismiss: () -> Unit, modifier: Modifier = Modifier) { AlertDialog( showDialog = true, message = stringResource(R.string.podcasts_no_podcasts), onDismiss = onDismiss, - modifier = modifier + modifier = modifier, ) } @OptIn(ExperimentalWearMaterialApi::class) @Composable -fun PodcastScreenLoading( - modifier: Modifier = Modifier -) { +fun PodcastScreenLoading(modifier: Modifier = Modifier) { EntityScreen( modifier = modifier, headerContent = { DefaultEntityScreenHeader( - title = stringResource(R.string.podcasts) + title = stringResource(R.string.podcasts), ) }, content = { items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } @WearPreviewDevices @WearPreviewFontScales @Composable -fun PodcastScreenLoadedPreview( - @PreviewParameter(WearPreviewPodcasts::class) podcasts: PodcastInfo -) { +fun PodcastScreenLoadedPreview(@PreviewParameter(WearPreviewPodcasts::class) podcasts: PodcastInfo) { val columnState = rememberResponsiveColumnState( contentPadding = ScalingLazyColumnDefaults.padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) PodcastScreenLoaded( podcastList = listOf(podcasts), - onPodcastsItemClick = {} + onPodcastsItemClick = {}, ) } @@ -196,8 +184,8 @@ fun PodcastScreenLoadingPreview() { val columnState = rememberResponsiveColumnState( contentPadding = ScalingLazyColumnDefaults.padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) PodcastScreenLoading() } @@ -210,11 +198,7 @@ fun PodcastScreenEmptyPreview() { } @Composable -fun MediaContent( - podcast: PodcastInfo, - downloadItemArtworkPlaceholder: Painter?, - onPodcastsItemClick: (PodcastInfo) -> Unit -) { +fun MediaContent(podcast: PodcastInfo, downloadItemArtworkPlaceholder: Painter?, onPodcastsItemClick: (PodcastInfo) -> Unit) { val mediaTitle = podcast.title val secondaryLabel = podcast.author diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsViewModel.kt index 65d7f0666b..667eccf7a6 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsViewModel.kt @@ -32,9 +32,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @HiltViewModel -class PodcastsViewModel @Inject constructor( - podcastStore: PodcastStore, -) : ViewModel() { +class PodcastsViewModel @Inject constructor(podcastStore: PodcastStore) : ViewModel() { val uiState: StateFlow = podcastStore.followedPodcastsSortedByLastEpisode(limit = 10).map { @@ -57,10 +55,7 @@ object PodcastMapper { /** * Maps from [Podcast]. */ - fun map( - podcastWithExtraInfo: PodcastWithExtraInfo, - ): PodcastInfo = - podcastWithExtraInfo.asExternalModel() + fun map(podcastWithExtraInfo: PodcastWithExtraInfo): PodcastInfo = podcastWithExtraInfo.asExternalModel() } @ExperimentalHorologistApi @@ -68,9 +63,7 @@ sealed interface PodcastsScreenState { data object Loading : PodcastsScreenState - data class Loaded( - val podcastList: List, - ) : PodcastsScreenState + data class Loaded(val podcastList: List) : PodcastsScreenState data object Empty : PodcastsScreenState } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt index f06d09c88e..2a69c4be70 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt @@ -63,7 +63,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen onEpisodeItemClick: (PlayerEpisode) -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, - queueViewModel: QueueViewModel = hiltViewModel() + queueViewModel: QueueViewModel = hiltViewModel(), ) { val uiState by queueViewModel.uiState.collectAsStateWithLifecycle() @@ -74,7 +74,7 @@ import com.google.android.horologist.media.ui.screens.entity.EntityScreen modifier = modifier, onEpisodeItemClick = onEpisodeItemClick, onDeleteQueueEpisodes = queueViewModel::onDeleteQueueEpisodes, - onDismiss = onDismiss + onDismiss = onDismiss, ) } @@ -86,17 +86,17 @@ fun QueueScreen( modifier: Modifier = Modifier, onEpisodeItemClick: (PlayerEpisode) -> Unit, onDeleteQueueEpisodes: () -> Unit, - onDismiss: () -> Unit + onDismiss: () -> Unit, ) { val columnState = rememberResponsiveColumnState( contentPadding = padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) ScreenScaffold( scrollState = columnState, - modifier = modifier + modifier = modifier, ) { when (uiState) { is QueueScreenState.Loaded -> QueueScreenLoaded( @@ -104,7 +104,7 @@ fun QueueScreen( onPlayButtonClick = onPlayButtonClick, onPlayEpisodes = onPlayEpisodes, onDeleteQueueEpisodes = onDeleteQueueEpisodes, - onEpisodeItemClick = onEpisodeItemClick + onEpisodeItemClick = onEpisodeItemClick, ) QueueScreenState.Loading -> QueueScreenLoading() QueueScreenState.Empty -> QueueScreenEmpty(onDismiss) @@ -119,13 +119,13 @@ fun QueueScreenLoaded( onPlayEpisodes: (List) -> Unit, onDeleteQueueEpisodes: () -> Unit, onEpisodeItemClick: (PlayerEpisode) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { EntityScreen( modifier = modifier, headerContent = { ResponsiveListHeader( - contentPadding = ListHeaderDefaults.firstItemPadding() + contentPadding = ListHeaderDefaults.firstItemPadding(), ) { Text(text = stringResource(R.string.queue)) } @@ -135,7 +135,7 @@ fun QueueScreenLoaded( episodes = episodeList, onPlayButtonClick = onPlayButtonClick, onPlayEpisodes = onPlayEpisodes, - onDeleteQueueEpisodes = onDeleteQueueEpisodes + onDeleteQueueEpisodes = onDeleteQueueEpisodes, ) }, content = { @@ -146,23 +146,21 @@ fun QueueScreenLoaded( image = Icons.Default.MusicNote, tintColor = Color.Blue, ), - onItemClick = onEpisodeItemClick + onItemClick = onEpisodeItemClick, ) } - } + }, ) } @OptIn(ExperimentalWearMaterialApi::class) @Composable -fun QueueScreenLoading( - modifier: Modifier = Modifier -) { +fun QueueScreenLoading(modifier: Modifier = Modifier) { EntityScreen( modifier = modifier, headerContent = { DefaultEntityScreenHeader( - title = stringResource(R.string.queue) + title = stringResource(R.string.queue), ) }, buttonsContent = { @@ -171,28 +169,25 @@ fun QueueScreenLoading( onPlayButtonClick = {}, onPlayEpisodes = {}, onDeleteQueueEpisodes = { }, - enabled = false + enabled = false, ) }, content = { items(count = 2) { PlaceholderChip(colors = ChipDefaults.secondaryChipColors()) } - } + }, ) } @Composable -fun QueueScreenEmpty( - onDismiss: () -> Unit, - modifier: Modifier = Modifier -) { +fun QueueScreenEmpty(onDismiss: () -> Unit, modifier: Modifier = Modifier) { AlertDialog( showDialog = true, onDismiss = onDismiss, title = stringResource(R.string.display_nothing_in_queue), message = stringResource(R.string.no_episodes_from_queue), - modifier = modifier + modifier = modifier, ) } @@ -204,7 +199,7 @@ fun ButtonsContent( onPlayEpisodes: (List) -> Unit, onDeleteQueueEpisodes: () -> Unit, enabled: Boolean = true, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Row( @@ -223,7 +218,7 @@ fun ButtonsContent( }, modifier = Modifier .weight(weight = 0.3F, fill = false), - enabled = enabled + enabled = enabled, ) Button( imageVector = Icons.Outlined.Delete, @@ -232,7 +227,7 @@ fun ButtonsContent( onClick = onDeleteQueueEpisodes, modifier = Modifier .weight(weight = 0.3F, fill = false), - enabled = enabled + enabled = enabled, ) } } @@ -242,20 +237,20 @@ fun ButtonsContent( @Composable fun QueueScreenLoadedPreview( @PreviewParameter(WearPreviewEpisodes::class) - episode: PlayerEpisode + episode: PlayerEpisode, ) { val columnState = rememberResponsiveColumnState( contentPadding = padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) QueueScreenLoaded( episodeList = listOf(episode), onPlayButtonClick = { }, onPlayEpisodes = { }, onDeleteQueueEpisodes = { }, - onEpisodeItemClick = { } + onEpisodeItemClick = { }, ) } @@ -266,8 +261,8 @@ fun QueueScreenLoadingPreview() { val columnState = rememberResponsiveColumnState( contentPadding = padding( first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Chip - ) + last = ScalingLazyColumnDefaults.ItemType.Chip, + ), ) QueueScreenLoading() } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueViewModel.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueViewModel.kt index bdd38f694d..3f99ca3f45 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueViewModel.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueViewModel.kt @@ -32,10 +32,7 @@ import kotlinx.coroutines.flow.stateIn * ViewModel that handles the business logic and screen state of the Queue screen. */ @HiltViewModel -class QueueViewModel @Inject constructor( - private val episodePlayer: EpisodePlayer, - -) : ViewModel() { +class QueueViewModel @Inject constructor(private val episodePlayer: EpisodePlayer) : ViewModel() { val uiState: StateFlow = episodePlayer.playerState.map { if (it.queue.isNotEmpty()) { @@ -69,9 +66,7 @@ sealed interface QueueScreenState { data object Loading : QueueScreenState - data class Loaded( - val episodeList: List - ) : QueueScreenState + data class Loaded(val episodeList: List) : QueueScreenState data object Empty : QueueScreenState }