Skip to content

Commit

Permalink
Minor UI tweak on Jetcaster/tv-app (#1353)
Browse files Browse the repository at this point in the history
This pull request aims to polish UI of the Jetcaster TV app. More
specifically, the following changes are applied:

- Switch app icon from default one to the proper Jetcaster icon
- Set a placeholder for thumbnails in case thumbnail is not available
- Change the search screen behavior to be consistent with the expected
filter chip behavior
  • Loading branch information
chikoski authored Apr 22, 2024
2 parents 25cde73 + a01eadc commit 8f10e6e
Show file tree
Hide file tree
Showing 39 changed files with 694 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Category>) : List<Category> by member
data class CategoryInfoList(val member: List<CategoryInfo>) : List<CategoryInfo> by member {

fun intoCategoryList(): List<Category> {
return map(CategoryInfo::intoCategory)
}

companion object {
fun from(list: List<Category>): CategoryInfoList {
val member = list.map(Category::asExternalModel)
return CategoryInfoList(member)
}
}
}

private fun CategoryInfo.intoCategory(): Category {
return Category(id, name)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) },
) {
Expand All @@ -77,29 +81,29 @@ 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) }
) {
Text(text = "Library")
}
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) }
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,44 +27,52 @@ import com.example.jetcaster.core.model.PlayerEpisode
class JetcasterAppState(
val navHostController: NavHostController
) {

private var _currentScreenState = mutableStateOf<Screen>(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) {
showEpisodeDetails(playerEpisode.uri)
}

fun playEpisode() {
navHostController.navigate(Screen.Player.route)
navigate(Screen.Player)
}

fun backToHome() {
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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()
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal fun EpisodeDetails(
first = {
Thumbnail(
playerEpisode,
size = JetcasterAppDefaults.thumbnailSize.episode
size = JetcasterAppDefaults.thumbnailSize.episodeDetails
)
},
second = {
Expand Down
Loading

0 comments on commit 8f10e6e

Please sign in to comment.