diff --git a/.github/pics/tv.png b/.github/pics/tv.png index e25278da..c1c385a1 100644 Binary files a/.github/pics/tv.png and b/.github/pics/tv.png differ diff --git a/auto/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt b/auto/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt index 4831ae03..fe029155 100644 --- a/auto/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt +++ b/auto/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt @@ -7,24 +7,20 @@ sealed interface AppNavigation { @Serializable data object Main: AppNavigation - //@Parcelize @Serializable data class Details( val id: Int - ): AppNavigation//, Parcelable + ): AppNavigation } sealed interface TabNavigation { - //@Parcelize @Serializable - data object Home: TabNavigation//, Parcelable + data object Home: TabNavigation - //@Parcelize @Serializable - data object Settings: TabNavigation//, Parcelable + data object Settings: TabNavigation - //@Parcelize @Serializable - data object About: TabNavigation//, Parcelable + data object About: TabNavigation } \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/AppModule.kt b/tv/src/main/kotlin/org/michaelbel/template/AppModule.kt index 2dde872d..1f971b73 100644 --- a/tv/src/main/kotlin/org/michaelbel/template/AppModule.kt +++ b/tv/src/main/kotlin/org/michaelbel/template/AppModule.kt @@ -25,6 +25,8 @@ import org.michaelbel.template.ktor.AppService import org.michaelbel.template.repository.AppRepository import org.michaelbel.template.room.AppDao import org.michaelbel.template.room.AppDatabase +import org.michaelbel.template.ui.list.ListViewModel +import org.michaelbel.template.ui.details2.DetailsViewModel2 val appModule = module { includes(dispatchersKoinModule) @@ -80,4 +82,6 @@ val appModule = module { single { AppRepository(get(), get(), get()) } single { AppInteractor(get(), get()) } viewModelOf(::MainViewModel) + viewModelOf(::ListViewModel) + viewModelOf(::DetailsViewModel2) } \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/interactor/AppInteractor.kt b/tv/src/main/kotlin/org/michaelbel/template/interactor/AppInteractor.kt index 6b8bc916..b1efc209 100644 --- a/tv/src/main/kotlin/org/michaelbel/template/interactor/AppInteractor.kt +++ b/tv/src/main/kotlin/org/michaelbel/template/interactor/AppInteractor.kt @@ -1,9 +1,24 @@ package org.michaelbel.template.interactor +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext import org.michaelbel.core.dispatchers.AppDispatchers import org.michaelbel.template.repository.AppRepository +import org.michaelbel.template.room.AppEntity class AppInteractor( private val appDispatchers: AppDispatchers, private val appRepository: AppRepository -) \ No newline at end of file +) { + fun entitiesFlow(): Flow> { + return appRepository.entitiesFlow() + } + + fun entityFlow(id: Int): Flow { + return appRepository.entityFlow(id) + } + + suspend fun loadDataResponse() { + withContext(appDispatchers.io) { appRepository.loadDataResponse() } + } +} \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/ktor/AppResponse.kt b/tv/src/main/kotlin/org/michaelbel/template/ktor/AppResponse.kt index 9d1a41ef..73089ea3 100644 --- a/tv/src/main/kotlin/org/michaelbel/template/ktor/AppResponse.kt +++ b/tv/src/main/kotlin/org/michaelbel/template/ktor/AppResponse.kt @@ -6,5 +6,7 @@ import kotlinx.serialization.Serializable @Serializable data class AppResponse( @SerialName("id") val id: Int, - @SerialName("name") val name: String + @SerialName("name") val name: String, + @SerialName("description") val description: String, + @SerialName("picture") val picture: String ) \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/ktor/AppService.kt b/tv/src/main/kotlin/org/michaelbel/template/ktor/AppService.kt index c0b99046..430d5a93 100644 --- a/tv/src/main/kotlin/org/michaelbel/template/ktor/AppService.kt +++ b/tv/src/main/kotlin/org/michaelbel/template/ktor/AppService.kt @@ -1,18 +1,17 @@ package org.michaelbel.template.ktor import io.ktor.client.HttpClient -import io.ktor.client.call.body import io.ktor.client.request.get -import io.ktor.client.request.parameter +import io.ktor.client.statement.bodyAsText +import kotlinx.serialization.json.Json class AppService( private val httpClient: HttpClient ) { - suspend fun getAppResponse(id: Int): AppResponse { - return httpClient.get("route/$id") { - parameter("key", "1234") - }.body() + suspend fun getDataResponse(): List { + val response: String = httpClient.get(DATA_URL).bodyAsText() + return Json.decodeFromString>(response) } companion object { @@ -20,5 +19,7 @@ class AppService( const val SOCKET_TIMEOUT_SECONDS = 10_000L const val HTTP_CACHE_SIZE_BYTES = 1024 * 1024 * 50 const val CONNECT_TIMEOUT_MILLIS = 10_000L + + const val DATA_URL = "https://raw.githubusercontent.com/michaelbel/android-template/refs/heads/develop/.github/data/data.json" } } \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/repository/AppRepository.kt b/tv/src/main/kotlin/org/michaelbel/template/repository/AppRepository.kt index d11f8593..454cbc24 100644 --- a/tv/src/main/kotlin/org/michaelbel/template/repository/AppRepository.kt +++ b/tv/src/main/kotlin/org/michaelbel/template/repository/AppRepository.kt @@ -1,11 +1,33 @@ package org.michaelbel.template.repository +import kotlinx.coroutines.flow.Flow import org.michaelbel.template.datastore.AppPreferences import org.michaelbel.template.ktor.AppService import org.michaelbel.template.room.AppDao +import org.michaelbel.template.room.AppEntity class AppRepository( private val appPreferences: AppPreferences, private val appDao: AppDao, private val appService: AppService -) \ No newline at end of file +) { + fun entitiesFlow(): Flow> { + return appDao.entitiesFlow() + } + + fun entityFlow(id: Int): Flow { + return appDao.entityFlow(id) + } + + suspend fun loadDataResponse() { + val appEntities = appService.getDataResponse().map { + AppEntity( + id = it.id, + name = it.name, + description = it.description, + picture = it.picture + ) + } + appDao.insertEntities(appEntities) + } +} \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/room/AppDao.kt b/tv/src/main/kotlin/org/michaelbel/template/room/AppDao.kt index 105f0a6e..e0e2db77 100644 --- a/tv/src/main/kotlin/org/michaelbel/template/room/AppDao.kt +++ b/tv/src/main/kotlin/org/michaelbel/template/room/AppDao.kt @@ -2,6 +2,7 @@ package org.michaelbel.template.room import androidx.room.Dao import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query import kotlinx.coroutines.flow.Flow @@ -11,6 +12,9 @@ interface AppDao { @Query("SELECT * FROM entities") fun entitiesFlow(): Flow> - @Insert + @Query("SELECT * FROM entities WHERE id = :id") + fun entityFlow(id: Int): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertEntities(entities: List) } \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/room/AppDatabase.kt b/tv/src/main/kotlin/org/michaelbel/template/room/AppDatabase.kt index b3d0e938..3e9809eb 100644 --- a/tv/src/main/kotlin/org/michaelbel/template/room/AppDatabase.kt +++ b/tv/src/main/kotlin/org/michaelbel/template/room/AppDatabase.kt @@ -17,7 +17,7 @@ abstract class AppDatabase: RoomDatabase() { abstract fun appDao(): AppDao companion object { - const val DATABASE_NAME = "app.db" + private const val DATABASE_NAME = "app.db" const val DATABASE_VERSION = 1 @Volatile diff --git a/tv/src/main/kotlin/org/michaelbel/template/room/AppEntity.kt b/tv/src/main/kotlin/org/michaelbel/template/room/AppEntity.kt index 1fe35185..bb945cae 100644 --- a/tv/src/main/kotlin/org/michaelbel/template/room/AppEntity.kt +++ b/tv/src/main/kotlin/org/michaelbel/template/room/AppEntity.kt @@ -5,5 +5,11 @@ import androidx.room.Entity @Entity(tableName = "entities", primaryKeys = ["id"]) data class AppEntity( val id: Int, - val name: String -) \ No newline at end of file + val name: String, + val description: String, + val picture: String +) { + companion object { + val Empty = AppEntity(id = 0, name = "", description = "", picture = "") + } +} \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/ui/MainActivityContent.kt b/tv/src/main/kotlin/org/michaelbel/template/ui/MainActivityContent.kt index bc9d5bac..3db1a82a 100644 --- a/tv/src/main/kotlin/org/michaelbel/template/ui/MainActivityContent.kt +++ b/tv/src/main/kotlin/org/michaelbel/template/ui/MainActivityContent.kt @@ -1,72 +1,144 @@ +@file:OptIn(ExperimentalMaterial3AdaptiveApi::class) + package org.michaelbel.template.ui -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement +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.navigationBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold 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.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.tv.material3.Button +import androidx.tv.material3.Icon import androidx.tv.material3.Text import org.koin.androidx.compose.koinViewModel import org.michaelbel.template.MainViewModel +import org.michaelbel.template.ui.details2.DetailsScreen2 +import org.michaelbel.template.ui.details2.empty.DetailsEmptyScreen +import org.michaelbel.template.ui.list.ListScreen @Composable fun MainActivityContent( - modifier: Modifier = Modifier, viewModel: MainViewModel = koinViewModel() ) { - val navController = rememberNavController() + val navHostController = rememberNavController() + var selectedRoute by remember { mutableStateOf(TabNavigation.Home) } + val listDetailPaneScaffoldNavigator = rememberListDetailPaneScaffoldNavigator() - NavHost( - navController = navController, - startDestination = Navigation.Home.route - ) { - composable(Navigation.Home.route) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Button( - onClick = { navController.navigate(Navigation.Chat.route) } - ) { - Text("Navigate to Chat") + NavigationSuiteScaffold( + navigationSuiteItems = { + item( + selected = selectedRoute == TabNavigation.Home, + onClick = { selectedRoute = TabNavigation.Home }, + icon = { + Icon( + imageVector = Icons.Outlined.Home, + contentDescription = null + ) } - } + ) + item( + selected = selectedRoute == TabNavigation.Settings, + onClick = { selectedRoute = TabNavigation.Settings }, + icon = { + Icon( + imageVector = Icons.Outlined.Settings, + contentDescription = null + ) + } + ) + item( + selected = selectedRoute == TabNavigation.About, + onClick = { selectedRoute = TabNavigation.About }, + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null + ) + } + ) } - composable(Navigation.Chat.route) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Button( - onClick = { navController.navigate(Navigation.Settings.route) } + ) { + NavHost( + navController = navHostController, + startDestination = selectedRoute + ) { + composable { + ListDetailPaneScaffold( + directive = listDetailPaneScaffoldNavigator.scaffoldDirective, + value = listDetailPaneScaffoldNavigator.scaffoldValue, + listPane = { + AnimatedPane( + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth(0.5F) + ) { + ListScreen( + onClick = { listDetailPaneScaffoldNavigator.navigateTo( + ListDetailPaneScaffoldRole.Detail, AppNavigation.Details(it)) } + ) + } + }, + detailPane = { + AnimatedPane( + modifier = Modifier.fillMaxWidth(0.5F) + ) { + when { + listDetailPaneScaffoldNavigator.currentDestination?.content != null -> { + DetailsScreen2( + id = listDetailPaneScaffoldNavigator.currentDestination?.content?.id!! + ) + } + else -> { + DetailsEmptyScreen() + } + } + } + }, + modifier = Modifier.fillMaxWidth() + ) + } + composable { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - Text("Navigate to Settings") + Text( + text = "Settings" + ) } } - } - composable(Navigation.Settings.route) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Button( - onClick = { navController.navigate(Navigation.Home.route) } + composable { + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Text("Navigate to Home") + Text( + text = "About" + ) } } } } -} - -sealed class Navigation( - val route: String -) { - data object Home: Navigation("home") - data object Chat: Navigation("chat") - data object Settings: Navigation("settings") } \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt b/tv/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt new file mode 100644 index 00000000..fe029155 --- /dev/null +++ b/tv/src/main/kotlin/org/michaelbel/template/ui/TabNavigation.kt @@ -0,0 +1,26 @@ +package org.michaelbel.template.ui + +import kotlinx.serialization.Serializable + +sealed interface AppNavigation { + + @Serializable + data object Main: AppNavigation + + @Serializable + data class Details( + val id: Int + ): AppNavigation +} + +sealed interface TabNavigation { + + @Serializable + data object Home: TabNavigation + + @Serializable + data object Settings: TabNavigation + + @Serializable + data object About: TabNavigation +} \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsScreen2.kt b/tv/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsScreen2.kt new file mode 100644 index 00000000..06233765 --- /dev/null +++ b/tv/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsScreen2.kt @@ -0,0 +1,70 @@ +package org.michaelbel.template.ui.details2 + +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.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import org.koin.androidx.compose.koinViewModel + +@Composable +fun DetailsScreen2( + id: Int, + modifier: Modifier = Modifier, + viewModel: DetailsViewModel2 = koinViewModel() +) { + val appEntity by viewModel.appEntity.collectAsStateWithLifecycle() + + LaunchedEffect(id) { + viewModel.idFlow.value = id + } + + Scaffold( + modifier = modifier.fillMaxSize() + ) { innerPadding -> + LazyColumn( + modifier = modifier + .padding(innerPadding) + .padding(top = 16.dp, end = 16.dp) + .fillMaxSize() + ) { + item { + AsyncImage( + model = appEntity.picture, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + ) + } + item { + Text( + text = appEntity.name, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(top = 16.dp) + ) + } + item { + Text( + text = appEntity.description, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsViewModel2.kt b/tv/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsViewModel2.kt new file mode 100644 index 00000000..7ede58af --- /dev/null +++ b/tv/src/main/kotlin/org/michaelbel/template/ui/details2/DetailsViewModel2.kt @@ -0,0 +1,28 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package org.michaelbel.template.ui.details2 + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.stateIn +import org.michaelbel.core.viewmodel.BaseViewModel +import org.michaelbel.template.interactor.AppInteractor +import org.michaelbel.template.room.AppEntity + +class DetailsViewModel2( + appInteractor: AppInteractor +): BaseViewModel() { + + var idFlow = MutableStateFlow(0) + + val appEntity: StateFlow = idFlow.flatMapLatest { + appInteractor.entityFlow(it) + }.stateIn( + scope = this, + started = SharingStarted.Lazily, + initialValue = AppEntity.Empty + ) +} \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/ui/details2/empty/DetailsEmptyScreen.kt b/tv/src/main/kotlin/org/michaelbel/template/ui/details2/empty/DetailsEmptyScreen.kt new file mode 100644 index 00000000..e22cb6ea --- /dev/null +++ b/tv/src/main/kotlin/org/michaelbel/template/ui/details2/empty/DetailsEmptyScreen.kt @@ -0,0 +1,33 @@ +package org.michaelbel.template.ui.details2.empty + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun DetailsEmptyScreen( + modifier: Modifier = Modifier, +) { + + Scaffold( + modifier = modifier.fillMaxSize() + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Empty" + ) + } + } +} \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/ui/list/ListScreen.kt b/tv/src/main/kotlin/org/michaelbel/template/ui/list/ListScreen.kt new file mode 100644 index 00000000..9a60ea2d --- /dev/null +++ b/tv/src/main/kotlin/org/michaelbel/template/ui/list/ListScreen.kt @@ -0,0 +1,138 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package org.michaelbel.template.ui.list + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +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.wrapContentHeight +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.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import org.koin.androidx.compose.koinViewModel +import org.michaelbel.core.ktx.isTabletPortrait +import org.michaelbel.template.room.AppEntity +import org.michaelbel.template.ui.AppTheme + +@Composable +fun ListScreen( + onClick: (Int) -> Unit, + modifier: Modifier = Modifier, + viewModel: ListViewModel = koinViewModel() +) { + val entities by viewModel.appEntities.collectAsStateWithLifecycle() + val layoutDirection = LocalLayoutDirection.current + + Scaffold( + modifier = modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { + Text( + text = "Android Template" + ) + } + ) + } + ) { innerPadding -> + LazyVerticalGrid( + columns = GridCells.Fixed(1), + modifier = Modifier + .padding( + start = innerPadding.calculateStartPadding(layoutDirection), + top = innerPadding.calculateTopPadding(), + end = innerPadding.calculateEndPadding(layoutDirection), + bottom = 0.dp + ) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(8.dp) + ) { + items(entities) { entity -> + ListElement( + entity = entity, + onCLick = onClick + ) + } + } + } +} + +@Composable +fun ListElement( + entity: AppEntity, + onCLick: (Int) -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column( + modifier = Modifier + .clickable { onCLick(entity.id) } + .fillMaxWidth() + ) { + AsyncImage( + model = entity.picture, + contentDescription = "Image", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(if (isTabletPortrait) 400.dp else 220.dp) + .clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)) + ) + + Text( + text = entity.name, + style = MaterialTheme.typography.bodyLarge.copy(fontSize = 20.sp), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + ) + } + } +} + +@Preview +@Composable +private fun ListElementPreview() { + AppTheme { + ListElement( + entity = AppEntity(0, "", "", ""), + onCLick = {} + ) + } +} \ No newline at end of file diff --git a/tv/src/main/kotlin/org/michaelbel/template/ui/list/ListViewModel.kt b/tv/src/main/kotlin/org/michaelbel/template/ui/list/ListViewModel.kt new file mode 100644 index 00000000..be4f51e4 --- /dev/null +++ b/tv/src/main/kotlin/org/michaelbel/template/ui/list/ListViewModel.kt @@ -0,0 +1,25 @@ +package org.michaelbel.template.ui.list + +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.michaelbel.core.viewmodel.BaseViewModel +import org.michaelbel.template.interactor.AppInteractor +import org.michaelbel.template.room.AppEntity + +class ListViewModel( + private val appInteractor: AppInteractor +): BaseViewModel() { + + val appEntities: StateFlow> = appInteractor.entitiesFlow() + .stateIn( + scope = this, + started = SharingStarted.Lazily, + initialValue = emptyList() + ) + + init { + launch { appInteractor.loadDataResponse() } + } +} \ No newline at end of file