ui: Fix compile errors in first build

Composable API mismatches with tv-material 1.0.1: NavigationDrawerItem
takes leadingContent/content (not icon/label), Card needs no shape.
Move VideoListItem to top level so its supertype reference resolves.
Pass onOpenVideo by name because the trailing lambda slot on
VideoListScreen goes to the non-functional viewModel parameter.
Nullable search param on local channel listings; make StateFlow
collection use collectAsState directly to avoid a JVM signature clash.

Verified by building app-debug.apk inside the container (JDK 21
installed there for this purpose).
This commit is contained in:
Jeena 2026-10-10 10:27:30 +00:00
parent 59029438ad
commit 7eecfd72ac
8 changed files with 60 additions and 59 deletions

View file

@ -72,7 +72,7 @@ interface PeerTubeApi {
@GET("video-channels")
suspend fun channels(
@Query("search") search: String,
@Query("search") search: String?,
@Query("searchTarget") searchTarget: String = "local",
@Query("start") start: Long,
@Query("count") count: Int,

View file

@ -5,6 +5,7 @@ import net.jeena.jtubetv.data.api.PeerTubeApi
import net.jeena.jtubetv.data.api.TokenResponse
import net.jeena.jtubetv.data.auth.TokenStore
import okhttp3.FormBody
import okhttp3.Authenticator
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient

View file

@ -16,8 +16,8 @@ sealed interface SessionState {
class AppViewModel(private val repository: PeerTubeRepository = AppGraph.repository) : ViewModel() {
val sessionState: StateFlow<SessionState> = _sessionState
private val _sessionState = MutableStateFlow<SessionState>(SessionState.Loading)
val sessionState: StateFlow<SessionState> = _sessionState
val loginError = MutableStateFlow<String?>(null)
val loggingIn = MutableStateFlow(false)

View file

@ -16,6 +16,12 @@ sealed interface ListSource {
data class Channel(val handle: String, val displayName: String) : ListSource
}
/** One list row: a plain video, or a history entry (same video, server metadata). */
sealed interface VideoListItem {
data class Video(val video: net.jeena.jtubetv.data.api.VideoDto) : VideoListItem
data class HistoryEntry(val history: net.jeena.jtubetv.data.api.HistoryElement) : VideoListItem
}
class VideoListViewModel(
private val repository: PeerTubeRepository,
val source: ListSource,
@ -53,12 +59,6 @@ class VideoListViewModel(
if (items.isEmpty() && loadingInitial) loadMore(force = true)
}
/** History/search entries can hide the underlying video shape. */
sealed interface VideoListItem {
data class Video(val video: net.jeena.jtubetv.data.api.VideoDto) : VideoListItem
data class HistoryEntry(val history: net.jeena.jtubetv.data.api.HistoryElement) : VideoListItem
}
companion object {
fun factory(source: ListSource): ViewModelProvider.Factory =
object : ViewModelProvider.Factory {

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@ -119,8 +120,6 @@ private fun ChannelCard(channel: VideoChannelDto, onClick: () -> Unit) {
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp),
shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(12.dp)) {
val avatar = channel.avatars.maxByOrNull { it.width }

View file

@ -60,43 +60,9 @@ fun HomeScreen() {
}
NavigationDrawer(drawerContent = {
NavigationDrawerColumn(
selected = destination,
onSelect = {
destination = it
channelDetails = null
},
)
}) {
Row(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.fillMaxSize()) {
when (destination) {
Destination.LocalVideos ->
VideoListScreen(ListSource.LocalVideos) { playingVideo = it.shortUUID }
Destination.MyVideos ->
VideoListScreen(ListSource.MyVideos) { playingVideo = it.shortUUID }
Destination.Channels -> ChannelsScreen(
onOpenChannel = { handle -> channelDetails = handle },
)
Destination.Search ->
VideoListScreen(ListSource.Search) { playingVideo = it.shortUUID }
Destination.Subscriptions ->
VideoListScreen(ListSource.Subscriptions) { playingVideo = it.shortUUID }
Destination.History ->
VideoListScreen(ListSource.History) { playingVideo = it.shortUUID }
}
}
}
}
}
@Composable
private fun NavigationDrawerColumn(
selected: Destination,
onSelect: (Destination) -> Unit,
modifier: Modifier = Modifier,
) {
androidx.compose.foundation.layout.Column(modifier = modifier.width(220.dp).padding(vertical = 16.dp)) {
androidx.compose.foundation.layout.Column(
modifier = Modifier.width(220.dp).padding(vertical = 16.dp),
) {
Text(
text = "JTube",
modifier = Modifier.padding(start = 28.dp, bottom = 16.dp),
@ -104,13 +70,54 @@ private fun NavigationDrawerColumn(
)
Destination.entries.forEach { dest ->
NavigationDrawerItem(
icon = { Icon(imageVector = dest.icon(), contentDescription = null) },
label = { Text(dest.title) },
selected = dest == selected,
onClick = { onSelect(dest) },
selected = dest == destination,
onClick = {
destination = dest
channelDetails = null
},
leadingContent = {
Icon(imageVector = dest.icon(), contentDescription = null)
},
content = { Text(dest.title) },
)
}
}
}) {
Row(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.fillMaxSize()) {
when (destination) {
Destination.LocalVideos ->
VideoListScreen(
source = ListSource.LocalVideos,
onOpenVideo = { playingVideo = it.shortUUID },
)
Destination.MyVideos ->
VideoListScreen(
source = ListSource.MyVideos,
onOpenVideo = { playingVideo = it.shortUUID },
)
Destination.Channels -> ChannelsScreen(
onOpenChannel = { handle -> channelDetails = handle },
)
Destination.Search ->
VideoListScreen(
source = ListSource.Search,
onOpenVideo = { playingVideo = it.shortUUID },
)
Destination.Subscriptions ->
VideoListScreen(
source = ListSource.Subscriptions,
onOpenVideo = { playingVideo = it.shortUUID },
)
Destination.History ->
VideoListScreen(
source = ListSource.History,
onOpenVideo = { playingVideo = it.shortUUID },
)
}
}
}
}
}
private fun Destination.icon(): androidx.compose.ui.graphics.vector.ImageVector =

View file

@ -34,8 +34,8 @@ fun SetupScreen(
val url = rememberSaveable { mutableStateOf("https://tube.jeena.net") }
val username = rememberSaveable { mutableStateOf("") }
val password = rememberSaveable { mutableStateOf("") }
val busy = loggingIn.collectAsStateFlow()
val message = error.collectAsStateFlow()
val busy = loggingIn.collectAsState()
val message = error.collectAsState()
Column(
modifier = Modifier
@ -92,11 +92,3 @@ fun SetupScreen(
}
}
}
@Composable
private fun StateFlow<Boolean>.collectAsStateFlow(): State<Boolean> =
collectAsState()
@Composable
private fun StateFlow<String?>.collectAsStateFlow(): State<String?> =
collectAsState()

View file

@ -27,6 +27,7 @@ import net.jeena.jtubetv.R
import net.jeena.jtubetv.data.api.VideoDto
import net.jeena.jtubetv.ui.list.PagedListViewModel
import net.jeena.jtubetv.ui.list.ListSource
import net.jeena.jtubetv.ui.list.VideoListItem
import net.jeena.jtubetv.ui.list.VideoListViewModel
@Composable
@ -82,10 +83,11 @@ fun VideoListScreen(
}
LazyColumn(modifier = Modifier.fillMaxSize().padding(vertical = 8.dp)) {
itemsIndexed(items) { index, item ->
val video = when (item) {
is VideoListViewModel.VideoListItem.HistoryEntry -> item.history.video
is VideoListViewModel.VideoListItem.Video -> item.video
itemsIndexed<VideoListItem>(items) { index, item ->
val video: VideoDto = when (item) {
is VideoListItem.HistoryEntry -> item.history.video
is VideoListItem.Video -> item.video
else -> return@itemsIndexed
}
VideoCard(
video = video,