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") @GET("video-channels")
suspend fun channels( suspend fun channels(
@Query("search") search: String, @Query("search") search: String?,
@Query("searchTarget") searchTarget: String = "local", @Query("searchTarget") searchTarget: String = "local",
@Query("start") start: Long, @Query("start") start: Long,
@Query("count") count: Int, @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.api.TokenResponse
import net.jeena.jtubetv.data.auth.TokenStore import net.jeena.jtubetv.data.auth.TokenStore
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Authenticator
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient

View file

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

View file

@ -16,6 +16,12 @@ sealed interface ListSource {
data class Channel(val handle: String, val displayName: String) : 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( class VideoListViewModel(
private val repository: PeerTubeRepository, private val repository: PeerTubeRepository,
val source: ListSource, val source: ListSource,
@ -53,12 +59,6 @@ class VideoListViewModel(
if (items.isEmpty() && loadingInitial) loadMore(force = true) 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 { companion object {
fun factory(source: ListSource): ViewModelProvider.Factory = fun factory(source: ListSource): ViewModelProvider.Factory =
object : 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -119,8 +120,6 @@ private fun ChannelCard(channel: VideoChannelDto, onClick: () -> Unit) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp), .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)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(12.dp)) {
val avatar = channel.avatars.maxByOrNull { it.width } val avatar = channel.avatars.maxByOrNull { it.width }

View file

@ -60,43 +60,9 @@ fun HomeScreen() {
} }
NavigationDrawer(drawerContent = { NavigationDrawer(drawerContent = {
NavigationDrawerColumn( androidx.compose.foundation.layout.Column(
selected = destination, modifier = Modifier.width(220.dp).padding(vertical = 16.dp),
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)) {
Text( Text(
text = "JTube", text = "JTube",
modifier = Modifier.padding(start = 28.dp, bottom = 16.dp), modifier = Modifier.padding(start = 28.dp, bottom = 16.dp),
@ -104,13 +70,54 @@ private fun NavigationDrawerColumn(
) )
Destination.entries.forEach { dest -> Destination.entries.forEach { dest ->
NavigationDrawerItem( NavigationDrawerItem(
icon = { Icon(imageVector = dest.icon(), contentDescription = null) }, selected = dest == destination,
label = { Text(dest.title) }, onClick = {
selected = dest == selected, destination = dest
onClick = { onSelect(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 = 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 url = rememberSaveable { mutableStateOf("https://tube.jeena.net") }
val username = rememberSaveable { mutableStateOf("") } val username = rememberSaveable { mutableStateOf("") }
val password = rememberSaveable { mutableStateOf("") } val password = rememberSaveable { mutableStateOf("") }
val busy = loggingIn.collectAsStateFlow() val busy = loggingIn.collectAsState()
val message = error.collectAsStateFlow() val message = error.collectAsState()
Column( Column(
modifier = Modifier 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.data.api.VideoDto
import net.jeena.jtubetv.ui.list.PagedListViewModel import net.jeena.jtubetv.ui.list.PagedListViewModel
import net.jeena.jtubetv.ui.list.ListSource import net.jeena.jtubetv.ui.list.ListSource
import net.jeena.jtubetv.ui.list.VideoListItem
import net.jeena.jtubetv.ui.list.VideoListViewModel import net.jeena.jtubetv.ui.list.VideoListViewModel
@Composable @Composable
@ -82,10 +83,11 @@ fun VideoListScreen(
} }
LazyColumn(modifier = Modifier.fillMaxSize().padding(vertical = 8.dp)) { LazyColumn(modifier = Modifier.fillMaxSize().padding(vertical = 8.dp)) {
itemsIndexed(items) { index, item -> itemsIndexed<VideoListItem>(items) { index, item ->
val video = when (item) { val video: VideoDto = when (item) {
is VideoListViewModel.VideoListItem.HistoryEntry -> item.history.video is VideoListItem.HistoryEntry -> item.history.video
is VideoListViewModel.VideoListItem.Video -> item.video is VideoListItem.Video -> item.video
else -> return@itemsIndexed
} }
VideoCard( VideoCard(
video = video, video = video,