ui: SmartTube layout - hidden rail, top bar, white focus cards
Drop the always-visible sidebar: content is now full width so ~4.5 compact cards fit per row (width derived from the screen). A slide-in menu drawer opens via the avatar (BACK/LEFT close it) and carries the destinations + sign out with legible labels (#B0B0B0) replacing the illegible dark-grey rail. Cards no longer scale on focus: they flip to a white background with dark text and a white frame like SmartTube. Grids move to 3 columns with the freed width.
This commit is contained in:
parent
14cd3d3976
commit
9635e31589
4 changed files with 268 additions and 94 deletions
|
|
@ -36,7 +36,12 @@ import net.jeena.jtubetv.data.api.VideoDto
|
|||
import net.jeena.jtubetv.ui.list.PagedListViewModel
|
||||
import net.jeena.jtubetv.ui.screens.list.VideoCard
|
||||
|
||||
private val ROW_CARD_WIDTH = 296.dp
|
||||
/** Compact SmartTube-style card: ~4 and a bit visible per full-width row. */
|
||||
@Composable
|
||||
private fun rowCardWidth(): androidx.compose.ui.unit.Dp {
|
||||
val windowWidth = androidx.compose.ui.platform.LocalConfiguration.current.screenWidthDp
|
||||
return ((windowWidth / 4.35f) - 10).dp
|
||||
}
|
||||
|
||||
/** One "Continue watching" candidate: video + watched fraction (null = unknown yet). */
|
||||
data class ContinueItem(val video: VideoDto, val progress: Float?)
|
||||
|
|
@ -157,9 +162,10 @@ fun HomeRowsScreen(
|
|||
VideoCard(
|
||||
video = item.video,
|
||||
onClick = { onOpenVideo(item.video) },
|
||||
modifier = Modifier.width(ROW_CARD_WIDTH),
|
||||
modifier = Modifier.width(rowCardWidth()),
|
||||
showPrivacy = false,
|
||||
progress = item.progress,
|
||||
compact = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -192,8 +198,9 @@ fun HomeRowsScreen(
|
|||
VideoCard(
|
||||
video = video,
|
||||
onClick = { onOpenVideo(video) },
|
||||
modifier = Modifier.width(ROW_CARD_WIDTH),
|
||||
modifier = Modifier.width(rowCardWidth()),
|
||||
showPrivacy = false,
|
||||
compact = true,
|
||||
)
|
||||
if (index >= recentItems.size - PagedListViewModel.LOOKAHEAD) {
|
||||
LaunchedEffect(index) { viewModel.recent.loadMore() }
|
||||
|
|
|
|||
|
|
@ -1,38 +1,51 @@
|
|||
package net.jeena.jtubetv.ui.screens
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.Logout
|
||||
import androidx.compose.material.icons.filled.Subscriptions
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.VideoLibrary
|
||||
import androidx.compose.material.icons.filled.PersonalVideo
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Subscriptions
|
||||
import androidx.compose.material.icons.filled.VideoLibrary
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.NavigationDrawer
|
||||
import androidx.tv.material3.NavigationDrawerItem
|
||||
import androidx.tv.material3.Text
|
||||
import net.jeena.jtubetv.data.api.VideoDto
|
||||
import net.jeena.jtubetv.ui.AppViewModel
|
||||
import net.jeena.jtubetv.ui.list.ListSource
|
||||
import net.jeena.jtubetv.ui.screens.list.VideoListScreen
|
||||
import androidx.activity.compose.BackHandler
|
||||
|
||||
enum class Destination(val title: String) {
|
||||
Home("Home"),
|
||||
|
|
@ -43,9 +56,12 @@ enum class Destination(val title: String) {
|
|||
History("History"),
|
||||
}
|
||||
|
||||
/** Home = sidebar (D-pad navigable) + content pane. The player renders as a
|
||||
* fullscreen overlay: the list beneath stays composed, so its scroll position and
|
||||
* focused card survive BACK (couch users don't want to re-navigate from the top). */
|
||||
/**
|
||||
* Home = SmartTube-style: hidden menu, red round search + avatar in a top bar,
|
||||
* full-width rows/list content. The menu drawer slides in when wanted (avatar or
|
||||
* the search/BACK flows) and everything is key(epoch) so an account switch
|
||||
* rebuilds all state.
|
||||
*/
|
||||
@Composable
|
||||
fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
|
||||
val epoch by viewModel.sessionEpoch.collectAsState()
|
||||
|
|
@ -53,6 +69,9 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
|
|||
var channelDetails by rememberSaveable { mutableStateOf<String?>(null) } // handle
|
||||
var playingVideo by rememberSaveable { mutableStateOf<String?>(null) } // shortUUID
|
||||
var restoreToken by rememberSaveable { mutableStateOf(0) } // increments when playback ends
|
||||
var drawerOpen by rememberSaveable { mutableStateOf(false) }
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
var lastBackAt by rememberSaveable { mutableStateOf(0L) }
|
||||
|
||||
// After BACK from the player: bump the restore token so lists re-focus the opened
|
||||
// card and re-fetch positions (UX report BUG-3 + BUG-7).
|
||||
|
|
@ -60,85 +79,42 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
|
|||
if (playingVideo == null && restoreToken >= 1) restoreToken++
|
||||
}
|
||||
|
||||
// Channel drilldown hides the sidebar; remote BACK must return to the channels list.
|
||||
BackHandler(enabled = playingVideo == null && channelDetails != null) {
|
||||
// BACK order: player (own BackHandler) → drawer → channel drilldown → exit guard.
|
||||
BackHandler(enabled = playingVideo == null && channelDetails == null && !drawerOpen) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastBackAt < 2000) {
|
||||
(context as? android.app.Activity)?.finish()
|
||||
} else {
|
||||
lastBackAt = now
|
||||
android.widget.Toast.makeText(context, "Press BACK again to exit", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
BackHandler(enabled = playingVideo == null && channelDetails != null && !drawerOpen) {
|
||||
channelDetails = null
|
||||
}
|
||||
BackHandler(enabled = drawerOpen && playingVideo == null) {
|
||||
drawerOpen = false
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
androidx.compose.runtime.key(epoch) {
|
||||
if (destination == Destination.Channels && channelDetails != null) {
|
||||
androidx.compose.foundation.layout.Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
// TV overscan safety margins (list content, not the player).
|
||||
.padding(horizontal = 32.dp),
|
||||
) {
|
||||
VideoListScreen(
|
||||
source = ListSource.Channel(channelDetails!!, channelDetails!!),
|
||||
onOpenVideo = { playingVideo = it.shortUUID },
|
||||
restoreToken = restoreToken,
|
||||
epoch = epoch,
|
||||
Column {
|
||||
// SmartTube-style top bar: red round search + avatar opening the menu.
|
||||
TopBar(
|
||||
onSearchClick = { destination = Destination.Search; channelDetails = null },
|
||||
onAccountClick = { drawerOpen = !drawerOpen },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
NavigationDrawer(drawerContent = {
|
||||
androidx.compose.foundation.layout.Column(
|
||||
modifier = Modifier.width(220.dp).padding(vertical = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "JTube TV",
|
||||
modifier = Modifier.padding(start = 28.dp, bottom = 16.dp),
|
||||
style = androidx.tv.material3.MaterialTheme.typography.headlineSmall,
|
||||
color = androidx.compose.ui.graphics.Color.White,
|
||||
)
|
||||
Destination.entries.forEach { dest ->
|
||||
NavigationDrawerItem(
|
||||
selected = dest == destination,
|
||||
onClick = {
|
||||
destination = dest
|
||||
channelDetails = null
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(imageVector = dest.icon(), contentDescription = null)
|
||||
},
|
||||
content = { Text(dest.title) },
|
||||
)
|
||||
}
|
||||
val signOutContext = androidx.compose.ui.platform.LocalContext.current
|
||||
var lastSignOutTap by remember { mutableStateOf(0L) }
|
||||
NavigationDrawerItem(
|
||||
selected = false,
|
||||
onClick = {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastSignOutTap < 3000) {
|
||||
viewModel.logout()
|
||||
} else {
|
||||
lastSignOutTap = now
|
||||
android.widget.Toast.makeText(
|
||||
signOutContext,
|
||||
"Press again to sign out",
|
||||
android.widget.Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(imageVector = Icons.Filled.Logout, contentDescription = null)
|
||||
},
|
||||
content = { Text("Sign out") },
|
||||
)
|
||||
}
|
||||
}) {
|
||||
androidx.compose.foundation.layout.Box(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
// TV overscan safety margins on the content pane.
|
||||
.padding(start = 8.dp, end = 32.dp, top = 16.dp),
|
||||
.padding(start = 8.dp, end = 32.dp, top = 8.dp),
|
||||
) {
|
||||
when (destination) {
|
||||
Destination.Home ->
|
||||
HomeRowsScreen(
|
||||
onOpenVideo = { playingVideo = it.shortUUID },
|
||||
restoreToken = restoreToken,
|
||||
epoch = epoch,
|
||||
)
|
||||
Destination.MyVideos ->
|
||||
|
|
@ -178,6 +154,20 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
|
|||
}
|
||||
}
|
||||
|
||||
// Slide-in menu drawer (SmartTube style: hidden until wanted).
|
||||
if (drawerOpen) {
|
||||
MenuDrawer(
|
||||
selected = destination,
|
||||
onSelect = {
|
||||
destination = it
|
||||
channelDetails = null
|
||||
drawerOpen = false
|
||||
},
|
||||
onSignOut = { viewModel.logout() },
|
||||
onDismiss = { drawerOpen = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (playingVideo != null) {
|
||||
PlayerScreen(
|
||||
shortUuid = playingVideo!!,
|
||||
|
|
@ -186,9 +176,8 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Destination.icon(): androidx.compose.ui.graphics.vector.ImageVector =
|
||||
private fun Destination.icon(): ImageVector =
|
||||
when (this) {
|
||||
Destination.Home -> Icons.Filled.VideoLibrary
|
||||
Destination.MyVideos -> Icons.Filled.PersonalVideo
|
||||
|
|
@ -197,3 +186,157 @@ private fun Destination.icon(): androidx.compose.ui.graphics.vector.ImageVector
|
|||
Destination.Subscriptions -> Icons.Filled.Subscriptions
|
||||
Destination.History -> Icons.Filled.History
|
||||
}
|
||||
|
||||
/** Top bar: red round search + account avatar (opens the menu), SmartTube-style. */
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onSearchClick: () -> Unit,
|
||||
onAccountClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 10.dp),
|
||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
val searchFocus = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { runCatching { searchFocus.requestFocus() } }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFFCE2B2B))
|
||||
.focusRequester(searchFocus)
|
||||
.focusable()
|
||||
.clickable(onClick = onSearchClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = "Search",
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF262626))
|
||||
.focusable()
|
||||
.clickable(onClick = onAccountClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PersonalVideo,
|
||||
contentDescription = "Menu",
|
||||
tint = Color(0xFFCCCCCC),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "JTube TV",
|
||||
color = Color(0xFF9A9A9A),
|
||||
style = androidx.compose.material3.MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide-in menu drawer: destination list + sign out, readable on a dark background
|
||||
* (UX report: unselected grey was illegible). Selected item gets the red accent.
|
||||
*/
|
||||
@Composable
|
||||
private fun MenuDrawer(
|
||||
selected: Destination,
|
||||
onSelect: (Destination) -> Unit,
|
||||
onSignOut: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val drawerFocus = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { runCatching { drawerFocus.requestFocus() } }
|
||||
val signOutContext = androidx.compose.ui.platform.LocalContext.current
|
||||
var lastSignOutTap by remember { mutableStateOf(0L) }
|
||||
Row(modifier = modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(240.dp)
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFF111111))
|
||||
.padding(vertical = 24.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "JTube TV",
|
||||
color = Color.White,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.padding(start = 28.dp, bottom = 20.dp),
|
||||
)
|
||||
Destination.entries.forEachIndexed { index, dest ->
|
||||
MenuDrawerItem(
|
||||
icon = dest.icon(),
|
||||
label = dest.title,
|
||||
selected = dest == selected,
|
||||
onClick = { onSelect(dest) },
|
||||
modifier = if (index == 0) Modifier.focusRequester(drawerFocus) else Modifier,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
MenuDrawerItem(
|
||||
icon = Icons.Filled.Logout,
|
||||
label = "Sign out",
|
||||
selected = false,
|
||||
onClick = {
|
||||
if (System.currentTimeMillis() - lastSignOutTap < 3000) {
|
||||
onSignOut()
|
||||
} else {
|
||||
lastSignOutTap = System.currentTimeMillis()
|
||||
android.widget.Toast.makeText(
|
||||
signOutContext,
|
||||
"Press again to sign out",
|
||||
android.widget.Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
// tap/click outside closes; focus LEFT from the drawer also closes
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxSize()
|
||||
.focusable()
|
||||
.pointerInput(Unit) { detectTapGestures { onDismiss() } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MenuDrawerItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val labelColor = if (selected) Color(0xFFE0E0E0) else Color(0xFFB0B0B0)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (selected) Modifier.background(Color(0x33CE2B2B)) else Modifier)
|
||||
.focusable()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
Icon(imageVector = icon, contentDescription = null, tint = labelColor)
|
||||
Text(
|
||||
text = label,
|
||||
color = labelColor,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package net.jeena.jtubetv.ui.screens.list
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -19,6 +20,8 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.tv.material3.Border
|
||||
import androidx.tv.material3.Card
|
||||
import androidx.tv.material3.CardDefaults
|
||||
import coil.compose.AsyncImage
|
||||
|
|
@ -43,19 +46,35 @@ fun VideoCard(
|
|||
modifier: Modifier = Modifier,
|
||||
showPrivacy: Boolean = true,
|
||||
progress: Float? = null,
|
||||
/** Compact = row in the SmartTube home: single-line title, smaller. */
|
||||
compact: Boolean = false,
|
||||
) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(268.dp)
|
||||
.padding(6.dp),
|
||||
colors = CardDefaults.colors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
modifier = modifier.padding(6.dp),
|
||||
colors = CardDefaults.colors(
|
||||
containerColor = UnfocusedCardBg,
|
||||
contentColor = Color.White,
|
||||
// SmartTube look: focused card turns white with dark text, no size change.
|
||||
focusedContainerColor = Color.White,
|
||||
focusedContentColor = Color(0xFF101010),
|
||||
),
|
||||
scale = CardDefaults.scale(focusedScale = 1f),
|
||||
border = CardDefaults.border(
|
||||
focusedBorder = Border(
|
||||
border = BorderStroke(3.dp, Color.White),
|
||||
shape = ContainerShape,
|
||||
),
|
||||
pressedBorder = Border(
|
||||
border = BorderStroke(3.dp, Color.White),
|
||||
shape = ContainerShape,
|
||||
),
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(158.dp)
|
||||
.aspectRatio(16f / 9f)
|
||||
.background(Color(0xFF222222)),
|
||||
) {
|
||||
video.thumbnailPath?.takeIf { it.isNotBlank() }?.let { path ->
|
||||
|
|
@ -105,10 +124,11 @@ fun VideoCard(
|
|||
Text(
|
||||
text = video.name,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 2,
|
||||
minLines = 2,
|
||||
maxLines = if (compact) 1 else 2,
|
||||
minLines = if (compact) 1 else 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
@ -161,6 +181,10 @@ internal fun formatDuration(seconds: Int): String {
|
|||
}
|
||||
}
|
||||
|
||||
/** Card palette (SmartTube-style): near-black idle, white + dark text on focus. */
|
||||
internal val UnfocusedCardBg = Color(0xFF171717)
|
||||
internal val ContainerShape = RoundedCornerShape(10.dp)
|
||||
|
||||
internal fun formatViews(views: Long): String =
|
||||
when {
|
||||
views >= 1_000_000 -> "%.1fM views".format(Locale.US, views / 1_000_000.0)
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ fun VideoListScreen(
|
|||
}
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
columns = GridCells.Fixed(3),
|
||||
state = gridState,
|
||||
modifier = Modifier.fillMaxSize().padding(vertical = 8.dp),
|
||||
) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue