Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search to TV demo #336

Merged
merged 1 commit into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ch.srgssr.dataprovider.paging.DataProviderPaging
import ch.srgssr.pillarbox.core.business.DefaultPillarbox
import ch.srgssr.pillarbox.core.business.MediaCompositionMediaItemSource
import ch.srgssr.pillarbox.core.business.images.DefaultImageScalingService
import ch.srgssr.pillarbox.core.business.images.ImageScalingService
import ch.srgssr.pillarbox.core.business.integrationlayer.service.DefaultMediaCompositionDataSource
import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost
import ch.srgssr.pillarbox.core.business.integrationlayer.service.Vector.getVector
Expand All @@ -27,7 +28,7 @@ object PlayerModule {
private fun provideIntegrationLayerItemSource(context: Context): MediaCompositionMediaItemSource =
MediaCompositionMediaItemSource(
mediaCompositionDataSource = DefaultMediaCompositionDataSource(vector = context.getVector()),
imageScalingService = DefaultImageScalingService()
imageScalingService = provideImageScalingService()
)

/**
Expand All @@ -53,6 +54,13 @@ object PlayerModule {
return ILRepository(dataProviderPaging = DataProviderPaging(ilService), ilService = ilService)
}

/**
* Provide a default implementation for the image scaling service.
*/
fun provideImageScalingService(): ImageScalingService {
return DefaultImageScalingService()
}

private fun providerIlHostFromUrl(ilHost: URL): ch.srg.dataProvider.integrationlayer.request.IlHost {
return when (ilHost) {
IlHost.STAGE -> ch.srg.dataProvider.integrationlayer.request.IlHost.STAGE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import ch.srg.dataProvider.integrationlayer.data.IlImage
import ch.srgssr.pillarbox.core.business.images.DefaultImageScalingService
import ch.srgssr.pillarbox.core.business.images.ImageScalingService
import ch.srgssr.pillarbox.core.business.images.ImageScalingService.ImageFormat
import ch.srgssr.pillarbox.core.business.images.ImageScalingService.ImageWidth
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.Content
import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ILRepository
import kotlinx.coroutines.flow.Flow
Expand All @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.map
class ContentListViewModel(
private val ilRepository: ILRepository,
private val contentList: ContentList,
private val imageScalingService: ImageScalingService = DefaultImageScalingService()
private val imageScalingService: ImageScalingService
) : ViewModel() {

/**
Expand Down Expand Up @@ -118,9 +118,10 @@ class ContentListViewModel(
class Factory(
private var ilRepository: ILRepository,
private val contentList: ContentList,
private val imageScalingService: ImageScalingService = PlayerModule.provideImageScalingService()
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ContentListViewModel(ilRepository, contentList) as T
return ContentListViewModel(ilRepository, contentList, imageScalingService) as T
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.demo.ui.integrationLayer
package ch.srgssr.pillarbox.demo.shared.ui.integrationLayer

import androidx.annotation.Px
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
Expand All @@ -12,7 +13,10 @@ import androidx.paging.LoadStates
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import ch.srg.dataProvider.integrationlayer.data.IlImage
import ch.srg.dataProvider.integrationlayer.request.parameters.Bu
import ch.srgssr.pillarbox.core.business.images.ImageScalingService
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.Content
import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ILRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -28,12 +32,17 @@ import kotlinx.coroutines.flow.transformLatest
import kotlin.time.Duration.Companion.milliseconds

/**
* Search view model to search media for the chosen bu
* [ViewModel] used to search for content in a specific BU.
*
* @param ilRepository
* @constructor Create empty Search view model
* @param ilRepository The repository used to load the data from the integration layer.
* @param imageScalingService The service to scale the image to display.
*
* @constructor Create a new [SearchViewModel].
*/
class SearchViewModel(private val ilRepository: ILRepository) : ViewModel() {
class SearchViewModel(
private val ilRepository: ILRepository,
private val imageScalingService: ImageScalingService
) : ViewModel() {
private val _bu = MutableStateFlow(Bu.RTS)

/**
Expand Down Expand Up @@ -120,14 +129,39 @@ class SearchViewModel(private val ilRepository: ILRepository) : ViewModel() {
return query.length >= VALID_SEARCH_QUERY_THRESHOLD
}

/**
* Get the URL of the scaled image, in the specified format, to match as much as possible the container width.
*
* @param imageUrl The original image URL.
* @param containerWidth The width, in pixels, of the image container.
* @param format The desired format of the transformed image.
*
* @return xx
*/
fun getScaledImageUrl(
imageUrl: String,
@Px containerWidth: Int,
format: ImageScalingService.ImageFormat = ImageScalingService.ImageFormat.WEBP
): String {
val size = IlImage.Size.getClosest(containerWidth)
val width = enumValueOf<ImageScalingService.ImageWidth>(size.name)

return imageScalingService.getScaledImageUrl(
imageUrl = imageUrl,
width = width,
format = format
)
}

internal data class Config(val bu: Bu, val query: String)

@Suppress("UndocumentedPublicClass")
class Factory(
private var ilRepository: ILRepository,
private val imageScalingService: ImageScalingService = PlayerModule.provideImageScalingService()
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SearchViewModel(ilRepository) as T
return SearchViewModel(ilRepository, imageScalingService) as T
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ package ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data
import ch.srg.dataProvider.integrationlayer.request.parameters.Bu
import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.ContentList

private val bus = listOf(Bu.RSI, Bu.RTR, Bu.RTS, Bu.SRF, Bu.SWI)
/**
* All the supported BUs.
*/
val bus = listOf(Bu.RSI, Bu.RTR, Bu.RTS, Bu.SRF, Bu.SWI)

/**
* All the sections available in the "Lists" tab.
Expand Down
3 changes: 3 additions & 0 deletions pillarbox-demo-shared/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@
<string name="lists">Lists</string>
<string name="search">Search</string>
<string name="showcases">Showcases</string>
<string name="search_placeholder">Search for content</string>
<string name="no_results">No results</string>
<string name="empty_search_query">Enter something to search</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package ch.srgssr.pillarbox.demo.tv
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
Expand Down Expand Up @@ -43,6 +44,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 58.dp)
) {
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurface
Expand All @@ -59,30 +61,27 @@ class MainActivity : ComponentActivity() {
?.let { selectedDestination = it }
}

TVDemoTopBar(
destinations = destinations,
selectedDestination = selectedDestination,
onDestinationSelected = {
selectedDestination = it
AnimatedVisibility(visible = selectedDestination != HomeDestination.Search) {
TVDemoTopBar(
destinations = destinations,
selectedDestination = selectedDestination,
modifier = Modifier.padding(vertical = 16.dp),
onDestinationClick = { destination ->
selectedDestination = destination

navController.navigate(it.route)
}
)
navController.navigate(destination.route)
}
)
}

TVDemoNavigation(
navController = navController,
startDestination = startDestination,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = HorizontalPadding)
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}

private companion object {
private val HorizontalPadding = 58.dp
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@
*/
package ch.srgssr.pillarbox.demo.tv.ui

import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
import ch.srgssr.pillarbox.demo.shared.ui.HomeDestination
import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.SearchViewModel
import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.contentListSections
import ch.srgssr.pillarbox.demo.tv.examples.ExamplesHome
import ch.srgssr.pillarbox.demo.tv.player.PlayerActivity
import ch.srgssr.pillarbox.demo.tv.ui.integrationLayer.ListsHome
import ch.srgssr.pillarbox.demo.tv.ui.integrationLayer.SearchView
import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme

/**
Expand Down Expand Up @@ -50,6 +57,22 @@ fun TVDemoNavigation(
sections = contentListSections
)
}

composable(HomeDestination.Search.route) {
val context = LocalContext.current
val ilRepository = remember {
PlayerModule.createIlRepository(context)
}

val searchViewModel = viewModel<SearchViewModel>(
factory = SearchViewModel.Factory(ilRepository)
)

SearchView(
searchViewModel = searchViewModel,
modifier = Modifier.padding(top = 16.dp)
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,25 @@ package ch.srgssr.pillarbox.demo.tv.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.ListItem
import androidx.tv.material3.ListItemDefaults
import androidx.tv.material3.Text
import ch.srgssr.pillarbox.demo.shared.ui.HomeDestination
import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme
Expand All @@ -36,51 +35,59 @@ import ch.srgssr.pillarbox.demo.tv.ui.theme.PillarboxTheme
* @param destinations The list of destinations to display.
* @param selectedDestination The currently selected destination.
* @param modifier The [Modifier] to apply to the top bar.
* @param onDestinationSelected The action to perform the selected a destination.
* @param onDestinationClick The action to perform the selected a destination.
*/
@Composable
@OptIn(ExperimentalComposeUiApi::class, ExperimentalTvMaterial3Api::class)
fun TVDemoTopBar(
destinations: List<HomeDestination>,
selectedDestination: HomeDestination,
modifier: Modifier = Modifier,
onDestinationSelected: (destination: HomeDestination) -> Unit
onDestinationClick: (destination: HomeDestination) -> Unit
) {
var isTabRowFocused by remember { mutableStateOf(false) }

TvLazyRow(
Row(
modifier = modifier
.fillMaxWidth()
.focusRestorer()
.onFocusChanged { isTabRowFocused = it.isFocused || it.hasFocus },
contentPadding = PaddingValues(
horizontal = 58.dp,
vertical = 16.dp
),
.focusRestorer(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
items(destinations) { destination ->
destinations.forEach { destination ->
ListItem(
selected = destination == selectedDestination,
onClick = { onDestinationSelected(destination) },
onClick = { onDestinationClick(destination) },
modifier = Modifier.width(IntrinsicSize.Max),
headlineContent = {
Text(text = stringResource(destination.labelResId))
}
)
}

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

ListItem(
selected = selectedDestination == HomeDestination.Search,
onClick = { onDestinationClick(HomeDestination.Search) },
modifier = Modifier.width(IntrinsicSize.Max),
shape = ListItemDefaults.shape(CircleShape),
headlineContent = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(HomeDestination.Search.labelResId)
)
}
)
}
}

@Preview
@Composable
@Preview(showBackground = true)
private fun TVDemoTopBarPreview() {
PillarboxTheme {
TVDemoTopBar(
destinations = listOf(HomeDestination.Examples, HomeDestination.Lists),
selectedDestination = HomeDestination.Examples,
onDestinationSelected = {}
onDestinationClick = {}
)
}
}
Loading