diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategoryList.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategoryInfoList.kt similarity index 56% rename from Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategoryList.kt rename to Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategoryInfoList.kt index 34643d8e2a..8e2c46f759 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategoryList.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategoryInfoList.kt @@ -18,6 +18,24 @@ package com.example.jetcaster.tv.model import androidx.compose.runtime.Immutable import com.example.jetcaster.core.data.database.model.Category +import com.example.jetcaster.core.data.database.model.asExternalModel +import com.example.jetcaster.core.model.CategoryInfo @Immutable -data class CategoryList(val member: List) : List by member +data class CategoryInfoList(val member: List) : List by member { + + fun intoCategoryList(): List { + return map(CategoryInfo::intoCategory) + } + + companion object { + fun from(list: List): CategoryInfoList { + val member = list.map(Category::asExternalModel) + return CategoryInfoList(member) + } + } +} + +private fun CategoryInfo.intoCategory(): Category { + return Category(id, name) +} diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt index 0c82639585..c5943815be 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/model/CategorySelection.kt @@ -17,9 +17,9 @@ package com.example.jetcaster.tv.model import androidx.compose.runtime.Immutable -import com.example.jetcaster.core.data.database.model.Category +import com.example.jetcaster.core.model.CategoryInfo -data class CategorySelection(val category: Category, val isSelected: Boolean = false) +data class CategorySelection(val categoryInfo: CategoryInfo, val isSelected: Boolean = false) @Immutable data class CategorySelectionList( diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt index 529da265b4..d5b4f3b257 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterApp.kt @@ -27,9 +27,11 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.VideoLibrary import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.tv.material3.DrawerValue import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme @@ -58,15 +60,17 @@ private fun WithGlobalNavigation( modifier: Modifier = Modifier, content: @Composable () -> Unit ) { + val currentScreen by jetcasterAppState.currentScreenState + NavigationDrawer( drawerContent = { + val isClosed = it == DrawerValue.Closed Column( modifier = Modifier .padding(JetcasterAppDefaults.overScanMargin.drawer.intoPaddingValues()) ) { - NavigationDrawerItem( - selected = false, + selected = isClosed && currentScreen.index == Screen.Profile.index, onClick = jetcasterAppState::navigateToProfile, leadingContent = { Icon(Icons.Default.Person, contentDescription = null) }, ) { @@ -77,21 +81,21 @@ private fun WithGlobalNavigation( } Spacer(modifier = Modifier.weight(1f)) NavigationDrawerItem( - selected = false, + selected = isClosed && currentScreen.index == Screen.Search.index, onClick = jetcasterAppState::navigateToSearch, leadingContent = { Icon(Icons.Default.Search, contentDescription = null) } ) { Text(text = "Search") } NavigationDrawerItem( - selected = false, + selected = isClosed && currentScreen.index == Screen.Discover.index, onClick = jetcasterAppState::navigateToDiscover, leadingContent = { Icon(Icons.Default.Home, contentDescription = null) }, ) { Text(text = "Discover") } NavigationDrawerItem( - selected = false, + selected = isClosed && currentScreen.index == Screen.Library.index, onClick = jetcasterAppState::navigateToLibrary, leadingContent = { Icon(Icons.Default.VideoLibrary, contentDescription = null) } ) { @@ -99,7 +103,7 @@ private fun WithGlobalNavigation( } Spacer(modifier = Modifier.weight(1f)) NavigationDrawerItem( - selected = false, + selected = isClosed && currentScreen.index == Screen.Settings.index, onClick = jetcasterAppState::navigateToSettings, leadingContent = { Icon(Icons.Default.Settings, contentDescription = null) } ) { diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt index 74077a81e6..8a508efacc 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/JetcasterAppState.kt @@ -18,6 +18,7 @@ package com.example.jetcaster.tv.ui import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController @@ -26,36 +27,44 @@ import com.example.jetcaster.core.model.PlayerEpisode class JetcasterAppState( val navHostController: NavHostController ) { + + private var _currentScreenState = mutableStateOf(Screen.Discover) + val currentScreenState = _currentScreenState + private fun navigate(screen: Screen) { + _currentScreenState.value = screen + navHostController.navigate(screen.route) + } + fun navigateToDiscover() { - navHostController.navigate(Screen.Discover.route) + navigate(Screen.Discover) } fun navigateToLibrary() { - navHostController.navigate(Screen.Library.route) + navigate(Screen.Library) } fun navigateToProfile() { - navHostController.navigate(Screen.Profile.route) + navigate(Screen.Profile) } fun navigateToSearch() { - navHostController.navigate(Screen.Search.route) + navigate(Screen.Search) } fun navigateToSettings() { - navHostController.navigate(Screen.Settings.route) + navigate(Screen.Settings) } fun showPodcastDetails(podcastUri: String) { val encodedUrL = Uri.encode(podcastUri) val screen = Screen.Podcast(encodedUrL) - navHostController.navigate(screen.route) + navigate(screen) } fun showEpisodeDetails(episodeUri: String) { val encodeUrl = Uri.encode(episodeUri) val screen = Screen.Episode(encodeUrl) - navHostController.navigate(screen.route) + navigate(screen) } fun showEpisodeDetails(playerEpisode: PlayerEpisode) { @@ -63,7 +72,7 @@ class JetcasterAppState( } fun playEpisode() { - navHostController.navigate(Screen.Player.route) + navigate(Screen.Player) } fun backToHome() { @@ -82,48 +91,60 @@ fun rememberJetcasterAppState( sealed interface Screen { val route: String + val index: Int data object Discover : Screen { override val route = "/discover" + override val index = 0 } data object Library : Screen { override val route = "library" + override val index = 1 } data object Search : Screen { override val route = "search" + override val index = 2 } data object Profile : Screen { override val route = "profile" + override val index = 3 } data object Settings : Screen { override val route: String = "settings" + override val index = 4 } data class Podcast(private val podcastUri: String) : Screen { override val route = "$ROOT/$podcastUri" + override val index = Companion.index companion object : Screen { private const val ROOT = "podcast" const val PARAMETER_NAME = "podcastUri" override val route = "$ROOT/{$PARAMETER_NAME}" + override val index = 5 } } data class Episode(private val episodeUri: String) : Screen { override val route: String = "$ROOT/$episodeUri" + override val index = Companion.index + companion object : Screen { private const val ROOT = "episode" const val PARAMETER_NAME = "episodeUri" override val route = "$ROOT/{$PARAMETER_NAME}" + override val index = 6 } } data object Player : Screen { override val route = "player" + override val index = 7 } } diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt index 1a3a03f0ff..1fb1f9b8d4 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/Catalog.kt @@ -27,20 +27,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp import androidx.tv.foundation.lazy.list.TvLazyColumn import androidx.tv.foundation.lazy.list.TvLazyListState import androidx.tv.foundation.lazy.list.TvLazyRow import androidx.tv.foundation.lazy.list.items import androidx.tv.foundation.lazy.list.rememberTvLazyListState -import androidx.tv.material3.Card -import androidx.tv.material3.CardScale import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.StandardCardLayout import androidx.tv.material3.Text -import coil.compose.AsyncImage -import com.example.jetcaster.core.data.database.model.Podcast import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo import com.example.jetcaster.core.model.PlayerEpisode import com.example.jetcaster.tv.R @@ -168,27 +162,3 @@ private fun PodcastRow( } } } - -@OptIn(ExperimentalTvMaterial3Api::class) -@Composable -internal fun PodcastCard( - podcast: Podcast, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - StandardCardLayout( - imageCard = { - Card( - onClick = onClick, - interactionSource = it, - scale = CardScale.None, - ) { - AsyncImage(model = podcast.imageUrl, contentDescription = null) - } - }, - title = { - Text(text = podcast.title, modifier = Modifier.padding(top = 12.dp)) - }, - modifier = modifier, - ) -} diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt index 0976f08218..7b2e22e851 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeCard.kt @@ -19,14 +19,16 @@ package com.example.jetcaster.tv.ui.component import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize 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.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.tv.material3.Card import androidx.tv.material3.CardScale @@ -35,31 +37,20 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.WideCardLayout import coil.compose.AsyncImage -import com.example.jetcaster.core.data.database.model.EpisodeToPodcast -import com.example.jetcaster.core.data.database.model.toPlayerEpisode import com.example.jetcaster.core.model.PlayerEpisode import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults -@Composable -internal fun EpisodeCard( - episode: EpisodeToPodcast, - onClick: () -> Unit, - modifier: Modifier = Modifier, - cardWidth: Dp = JetcasterAppDefaults.cardWidth.small, -) = - EpisodeCard(episode.toPlayerEpisode(), onClick, modifier, cardWidth) - @OptIn(ExperimentalTvMaterial3Api::class) @Composable internal fun EpisodeCard( playerEpisode: PlayerEpisode, onClick: () -> Unit, modifier: Modifier = Modifier, - cardWidth: Dp = JetcasterAppDefaults.cardWidth.small, + cardSize: DpSize = JetcasterAppDefaults.thumbnailSize.episode, ) { WideCardLayout( imageCard = { - EpisodeThumbnail(playerEpisode, onClick = onClick, modifier = Modifier.width(cardWidth)) + EpisodeThumbnail(playerEpisode, onClick = onClick, modifier = Modifier.size(cardSize)) }, title = { EpisodeMetaData( @@ -87,7 +78,12 @@ private fun EpisodeThumbnail( scale = CardScale.None, modifier = modifier, ) { - AsyncImage(model = playerEpisode.podcastImageUrl, contentDescription = null) + AsyncImage( + model = playerEpisode.podcastImageUrl, + contentDescription = null, + placeholder = thumbnailPlaceholder(), + modifier = Modifier.fillMaxSize() + ) } } diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt index 6fb101fc71..c40845cae5 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/EpisodeDetails.kt @@ -41,7 +41,7 @@ internal fun EpisodeDetails( first = { Thumbnail( playerEpisode, - size = JetcasterAppDefaults.thumbnailSize.episode + size = JetcasterAppDefaults.thumbnailSize.episodeDetails ) }, second = { diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt new file mode 100644 index 0000000000..1df5a815f0 --- /dev/null +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/PodcastCard.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetcaster.tv.ui.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Card +import androidx.tv.material3.CardScale +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.StandardCardLayout +import androidx.tv.material3.Text +import coil.compose.AsyncImage +import com.example.jetcaster.core.data.database.model.Podcast +import com.example.jetcaster.tv.ui.theme.JetcasterAppDefaults + +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +internal fun PodcastCard( + podcast: Podcast, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + StandardCardLayout( + imageCard = { + Card( + onClick = onClick, + interactionSource = it, + scale = CardScale.None, + ) { + AsyncImage( + model = podcast.imageUrl, + contentDescription = null, + placeholder = thumbnailPlaceholder(), + modifier = Modifier.size(JetcasterAppDefaults.thumbnailSize.podcast) + ) + } + }, + title = { + Text(text = podcast.title, modifier = Modifier.padding(top = 12.dp)) + }, + modifier = modifier, + ) +} diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/thumbnailPlaceholder.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/thumbnailPlaceholder.kt new file mode 100644 index 0000000000..f7ad98cfec --- /dev/null +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/component/thumbnailPlaceholder.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.jetcaster.tv.ui.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.BrushPainter +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.MaterialTheme + +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +internal fun thumbnailPlaceholder( + brush: Brush = SolidColor(MaterialTheme.colorScheme.surfaceVariant) +): BrushPainter { + return BrushPainter(brush) +} diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt index 49ea74414b..627f9e1aa7 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreen.kt @@ -36,11 +36,11 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Tab import androidx.tv.material3.TabRow import androidx.tv.material3.Text -import com.example.jetcaster.core.data.database.model.Category import com.example.jetcaster.core.data.database.model.Podcast import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo +import com.example.jetcaster.core.model.CategoryInfo import com.example.jetcaster.core.model.PlayerEpisode -import com.example.jetcaster.tv.model.CategoryList +import com.example.jetcaster.tv.model.CategoryInfoList import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.model.PodcastList import com.example.jetcaster.tv.ui.component.Catalog @@ -67,7 +67,7 @@ fun DiscoverScreen( is DiscoverScreenUiState.Ready -> { CatalogWithCategorySelection( - categoryList = s.categoryList, + categoryInfoList = s.categoryInfoList, podcastList = s.podcastList, selectedCategory = s.selectedCategory, latestEpisodeList = s.latestEpisodeList, @@ -88,13 +88,14 @@ fun DiscoverScreen( @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable private fun CatalogWithCategorySelection( - categoryList: CategoryList, + categoryInfoList: CategoryInfoList, podcastList: PodcastList, - selectedCategory: Category, + + selectedCategory: CategoryInfo, latestEpisodeList: EpisodeList, onPodcastSelected: (PodcastWithExtraInfo) -> Unit, onEpisodeSelected: (PlayerEpisode) -> Unit, - onCategorySelected: (Category) -> Unit, + onCategorySelected: (CategoryInfo) -> Unit, modifier: Modifier = Modifier, state: TvLazyListState = rememberTvLazyListState(), ) { @@ -104,7 +105,7 @@ private fun CatalogWithCategorySelection( LaunchedEffect(Unit) { focusRequester.requestFocus() } - val selectedTabIndex = categoryList.indexOf(selectedCategory) + val selectedTabIndex = categoryInfoList.indexOf(selectedCategory) Catalog( podcastList = podcastList, @@ -131,7 +132,7 @@ private fun CatalogWithCategorySelection( } } ) { - categoryList.forEachIndexed { index, category -> + categoryInfoList.forEachIndexed { index, category -> val tabModifier = if (selectedTabIndex == index) { Modifier.focusRequester(selectedTab) } else { diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt index 44b638aad4..ef766d7e9c 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/discover/DiscoverScreenViewModel.kt @@ -18,13 +18,13 @@ package com.example.jetcaster.tv.ui.discover import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.jetcaster.core.data.database.model.Category import com.example.jetcaster.core.data.database.model.toPlayerEpisode import com.example.jetcaster.core.data.repository.CategoryStore import com.example.jetcaster.core.data.repository.PodcastsRepository +import com.example.jetcaster.core.model.CategoryInfo import com.example.jetcaster.core.model.PlayerEpisode import com.example.jetcaster.core.player.EpisodePlayer -import com.example.jetcaster.tv.model.CategoryList +import com.example.jetcaster.tv.model.CategoryInfoList import com.example.jetcaster.tv.model.EpisodeList import com.example.jetcaster.tv.model.PodcastList import dagger.hilt.android.lifecycle.HiltViewModel @@ -46,13 +46,13 @@ class DiscoverScreenViewModel @Inject constructor( private val episodePlayer: EpisodePlayer, ) : ViewModel() { - private val _selectedCategory = MutableStateFlow(null) + private val _selectedCategory = MutableStateFlow(null) private val categoryListFlow = categoryStore .categoriesSortedByPodcastCount() .map { categoryList -> categoryList.map { category -> - Category( + CategoryInfo( id = category.id, name = category.name.filter { !it.isWhitespace() } ) @@ -69,7 +69,7 @@ class DiscoverScreenViewModel @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private val podcastInSelectedCategory = selectedCategoryFlow.flatMapLatest { if (it != null) { - categoryStore.podcastsInCategorySortedByPodcastCount(it.id) + categoryStore.podcastsInCategorySortedByPodcastCount(it.id, limit = 10) } else { flowOf(emptyList()) } @@ -96,7 +96,7 @@ class DiscoverScreenViewModel @Inject constructor( ) { categoryList, category, podcastList, latestEpisodes -> if (category != null) { DiscoverScreenUiState.Ready( - CategoryList(categoryList), + CategoryInfoList(categoryList), category, podcastList, latestEpisodes @@ -114,7 +114,7 @@ class DiscoverScreenViewModel @Inject constructor( refresh() } - fun selectCategory(category: Category) { + fun selectCategory(category: CategoryInfo) { _selectedCategory.value = category } @@ -132,8 +132,8 @@ class DiscoverScreenViewModel @Inject constructor( sealed interface DiscoverScreenUiState { data object Loading : DiscoverScreenUiState data class Ready( - val categoryList: CategoryList, - val selectedCategory: Category, + val categoryInfoList: CategoryInfoList, + val selectedCategory: CategoryInfo, val podcastList: PodcastList, val latestEpisodeList: EpisodeList, ) : DiscoverScreenUiState diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt index aaea796f6e..3093bf1856 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/episode/EpisodeScreen.kt @@ -102,7 +102,7 @@ private fun EpisodeDetails( first = { Thumbnail( podcast = episodeToPodcast.podcast, - size = JetcasterAppDefaults.thumbnailSize.episode + size = JetcasterAppDefaults.thumbnailSize.episodeDetails ) }, second = { diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt index 813cd19597..5fd4e0fecc 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreen.kt @@ -55,8 +55,8 @@ import androidx.tv.material3.FilterChip import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import com.example.jetcaster.core.data.database.model.Category import com.example.jetcaster.core.data.database.model.PodcastWithExtraInfo +import com.example.jetcaster.core.model.CategoryInfo import com.example.jetcaster.tv.R import com.example.jetcaster.tv.model.CategorySelectionList import com.example.jetcaster.tv.model.PodcastList @@ -101,8 +101,8 @@ private fun Ready( keyword: String, categorySelectionList: CategorySelectionList, onKeywordInput: (String) -> Unit, - onCategorySelected: (Category) -> Unit, - onCategoryUnselected: (Category) -> Unit, + onCategorySelected: (CategoryInfo) -> Unit, + onCategoryUnselected: (CategoryInfo) -> Unit, modifier: Modifier = Modifier ) { Controls( @@ -122,8 +122,8 @@ private fun HasResult( categorySelectionList: CategorySelectionList, podcastList: PodcastList, onKeywordInput: (String) -> Unit, - onCategorySelected: (Category) -> Unit, - onCategoryUnselected: (Category) -> Unit, + onCategorySelected: (CategoryInfo) -> Unit, + onCategoryUnselected: (CategoryInfo) -> Unit, onPodcastSelected: (PodcastWithExtraInfo) -> Unit, modifier: Modifier = Modifier ) { @@ -149,8 +149,8 @@ private fun Controls( keyword: String, categorySelectionList: CategorySelectionList, onKeywordInput: (String) -> Unit, - onCategorySelected: (Category) -> Unit, - onCategoryUnselected: (Category) -> Unit, + onCategorySelected: (CategoryInfo) -> Unit, + onCategoryUnselected: (CategoryInfo) -> Unit, modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() }, toRequestFocus: Boolean = false @@ -226,8 +226,8 @@ private fun KeywordInput( @Composable private fun CategorySelection( categorySelectionList: CategorySelectionList, - onCategorySelected: (Category) -> Unit, - onCategoryUnselected: (Category) -> Unit, + onCategorySelected: (CategoryInfo) -> Unit, + onCategoryUnselected: (CategoryInfo) -> Unit, modifier: Modifier = Modifier ) { FlowRow( @@ -240,13 +240,13 @@ private fun CategorySelection( selected = it.isSelected, onClick = { if (it.isSelected) { - onCategoryUnselected(it.category) + onCategoryUnselected(it.categoryInfo) } else { - onCategorySelected(it.category) + onCategorySelected(it.categoryInfo) } } ) { - Text(text = it.category.name) + Text(text = it.categoryInfo.name) } } } diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt index 24863951ae..5622aa4d91 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/search/SearchScreenViewModel.kt @@ -18,11 +18,11 @@ package com.example.jetcaster.tv.ui.search import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.jetcaster.core.data.database.model.Category import com.example.jetcaster.core.data.repository.CategoryStore import com.example.jetcaster.core.data.repository.PodcastStore import com.example.jetcaster.core.data.repository.PodcastsRepository -import com.example.jetcaster.tv.model.CategoryList +import com.example.jetcaster.core.model.CategoryInfo +import com.example.jetcaster.tv.model.CategoryInfoList import com.example.jetcaster.tv.model.CategorySelection import com.example.jetcaster.tv.model.CategorySelectionList import com.example.jetcaster.tv.model.PodcastList @@ -45,20 +45,29 @@ class SearchScreenViewModel @Inject constructor( ) : ViewModel() { private val keywordFlow = MutableStateFlow("") - private val selectedCategoryListFlow = MutableStateFlow>(emptyList()) + private val selectedCategoryListFlow = MutableStateFlow>(emptyList()) - private val categoryListFlow = categoryStore.categoriesSortedByPodcastCount().map { - CategoryList(it) - } + private val categoryInfoListFlow = + categoryStore.categoriesSortedByPodcastCount().map(CategoryInfoList::from) private val searchConditionFlow = - combine(keywordFlow, selectedCategoryListFlow) { keyword, selectedCategories -> - SearchCondition(keyword, selectedCategories) + combine( + keywordFlow, + selectedCategoryListFlow, + categoryInfoListFlow + ) { keyword, selectedCategories, categories -> + val selected = selectedCategories.ifEmpty { + categories + } + SearchCondition(keyword, selected) } @OptIn(ExperimentalCoroutinesApi::class) private val searchResultFlow = searchConditionFlow.flatMapLatest { - podcastStore.searchPodcastByTitleAndCategories(it.keyword, it.selectedCategories) + podcastStore.searchPodcastByTitleAndCategories( + it.keyword, + it.selectedCategories.intoCategoryList() + ) }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5_000), @@ -66,7 +75,10 @@ class SearchScreenViewModel @Inject constructor( ) private val categorySelectionFlow = - combine(categoryListFlow, selectedCategoryListFlow) { categoryList, selectedCategories -> + combine( + categoryInfoListFlow, + selectedCategoryListFlow + ) { categoryList, selectedCategories -> val list = categoryList.map { CategorySelection(it, selectedCategories.contains(it)) } @@ -94,14 +106,14 @@ class SearchScreenViewModel @Inject constructor( keywordFlow.value = keyword } - fun addCategoryToSelectedCategoryList(category: Category) { + fun addCategoryToSelectedCategoryList(category: CategoryInfo) { val list = selectedCategoryListFlow.value if (!list.contains(category)) { selectedCategoryListFlow.value = list + listOf(category) } } - fun removeCategoryFromSelectedCategoryList(category: Category) { + fun removeCategoryFromSelectedCategoryList(category: CategoryInfo) { val list = selectedCategoryListFlow.value if (list.contains(category)) { val mutable = list.toMutableList() @@ -117,7 +129,12 @@ class SearchScreenViewModel @Inject constructor( } } -private data class SearchCondition(val keyword: String, val selectedCategories: List) +private data class SearchCondition(val keyword: String, val selectedCategories: CategoryInfoList) { + constructor(keyword: String, categoryInfoList: List) : this( + keyword, + CategoryInfoList(categoryInfoList) + ) +} sealed interface SearchScreenUiState { data object Loading : SearchScreenUiState diff --git a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt index 9e9f3edfc9..47d7fbb527 100644 --- a/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt +++ b/Jetcaster/tv-app/src/main/java/com/example/jetcaster/tv/ui/theme/Space.kt @@ -67,7 +67,9 @@ internal data class CardWidth( ) internal data class ThumbnailSize( - val episode: DpSize = DpSize(266.dp, 266.dp), + val episodeDetails: DpSize = DpSize(266.dp, 266.dp), + val podcast: DpSize = DpSize(196.dp, 196.dp), + val episode: DpSize = DpSize(124.dp, 124.dp) ) internal data class PaddingSettings( diff --git a/Jetcaster/tv-app/src/main/res/drawable-nodpi/ic_text_logo.xml b/Jetcaster/tv-app/src/main/res/drawable-nodpi/ic_text_logo.xml new file mode 100644 index 0000000000..e422c1c25a --- /dev/null +++ b/Jetcaster/tv-app/src/main/res/drawable-nodpi/ic_text_logo.xml @@ -0,0 +1,32 @@ + + + + + + + diff --git a/Jetcaster/tv-app/src/main/res/drawable-v26/ic_launcher_foreground.xml b/Jetcaster/tv-app/src/main/res/drawable-v26/ic_launcher_foreground.xml new file mode 100644 index 0000000000..930f227590 --- /dev/null +++ b/Jetcaster/tv-app/src/main/res/drawable-v26/ic_launcher_foreground.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/Jetcaster/tv-app/src/main/res/drawable/ic_launcher_background.xml b/Jetcaster/tv-app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..7f2643db2d --- /dev/null +++ b/Jetcaster/tv-app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Jetcaster/tv-app/src/main/res/drawable/ic_launcher_foreground.xml b/Jetcaster/tv-app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..c19b699858 --- /dev/null +++ b/Jetcaster/tv-app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Jetcaster/tv-app/src/main/res/drawable/ic_launcher_monochrome.xml b/Jetcaster/tv-app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000000..e71686aef8 --- /dev/null +++ b/Jetcaster/tv-app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + diff --git a/Jetcaster/tv-app/src/main/res/drawable/ic_logo.xml b/Jetcaster/tv-app/src/main/res/drawable/ic_logo.xml new file mode 100644 index 0000000000..8d00d29968 --- /dev/null +++ b/Jetcaster/tv-app/src/main/res/drawable/ic_logo.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/Jetcaster/tv-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Jetcaster/tv-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..96e4ade2ed --- /dev/null +++ b/Jetcaster/tv-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/Jetcaster/tv-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Jetcaster/tv-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..96e4ade2ed --- /dev/null +++ b/Jetcaster/tv-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/Jetcaster/tv-app/src/main/res/mipmap-hdpi/ic_launcher.png b/Jetcaster/tv-app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..1e97e1b9ec Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Jetcaster/tv-app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd..0000000000 Binary files a/Jetcaster/tv-app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/Jetcaster/tv-app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000..1e97e1b9ec Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-mdpi/ic_launcher.png b/Jetcaster/tv-app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..821e87fac3 Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Jetcaster/tv-app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e5..0000000000 Binary files a/Jetcaster/tv-app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/Jetcaster/tv-app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000..821e87fac3 Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Jetcaster/tv-app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..347493f918 Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Jetcaster/tv-app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe..0000000000 Binary files a/Jetcaster/tv-app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/Jetcaster/tv-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..347493f918 Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Jetcaster/tv-app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..463f54c5d2 Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Jetcaster/tv-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77f9f..0000000000 Binary files a/Jetcaster/tv-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/Jetcaster/tv-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..463f54c5d2 Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Jetcaster/tv-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..50721da443 Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Jetcaster/tv-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d6427e6..0000000000 Binary files a/Jetcaster/tv-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/Jetcaster/tv-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/Jetcaster/tv-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..50721da443 Binary files /dev/null and b/Jetcaster/tv-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/Jetcaster/tv-app/src/main/res/values/colors.xml b/Jetcaster/tv-app/src/main/res/values/colors.xml new file mode 100644 index 0000000000..fd3d732d7d --- /dev/null +++ b/Jetcaster/tv-app/src/main/res/values/colors.xml @@ -0,0 +1,20 @@ + + + + + #121212 +