Basic navigation

This commit is contained in:
Damontecres 2025-09-22 12:09:41 -04:00
parent 51ad92f539
commit 80070f4174
No known key found for this signature in database
18 changed files with 496 additions and 22 deletions

View file

@ -57,17 +57,18 @@ class ServerManagementViewModel
val accessToken = authenticationResult.accessToken
if (accessToken != null) {
val authedUser = authenticationResult.user
val server =
authenticationResult.serverId?.let {
JellyfinServer(
id = it,
name = serverUrl,
name = authedUser?.serverName,
url = serverUrl,
)
}
if (server != null) {
val user =
authenticationResult.user?.let {
authedUser?.let {
JellyfinUser(
id = it.id.toString(),
name = it.name,

View file

@ -3,6 +3,7 @@ package com.github.damontecres.dolphin.ui.cards
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.github.damontecres.dolphin.data.model.DolphinModel
import com.github.damontecres.dolphin.data.model.Library
import com.github.damontecres.dolphin.data.model.Video
@Composable
@ -18,6 +19,8 @@ fun DolphinCard(
onClick = onClick,
modifier = modifier,
)
is Library -> TODO()
null -> TODO()
}
}

View file

@ -14,16 +14,18 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.data.model.DolphinModel
import com.github.damontecres.dolphin.data.model.convertModel
import com.github.damontecres.dolphin.isNotNullOrBlank
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.cards.DolphinCard
import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -118,8 +120,9 @@ class MainViewModel
@Composable
fun MainPage(
preferences: UserPreferences,
navigationManager: NavigationManager,
modifier: Modifier,
viewModel: MainViewModel = viewModel(),
viewModel: MainViewModel = hiltViewModel(),
) {
val homeRows by viewModel.homeRows.observeAsState(listOf())
Column(modifier = modifier) {
@ -129,6 +132,7 @@ fun MainPage(
item {
HomePageRow(
row = row,
onClickItem = { navigationManager.navigateTo(Destination.MediaItem(it.id)) },
modifier = Modifier.fillMaxWidth(),
)
}
@ -140,6 +144,7 @@ fun MainPage(
@Composable
fun HomePageRow(
row: HomeRow,
onClickItem: (DolphinModel) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -160,7 +165,7 @@ fun HomePageRow(
items(row.items) { item ->
DolphinCard(
item = item,
onClick = {},
onClick = { onClickItem.invoke(item) },
modifier = Modifier,
)
}

View file

@ -0,0 +1,67 @@
package com.github.damontecres.dolphin.ui.nav
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
import androidx.navigation3.scene.rememberSceneSetupNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.github.damontecres.dolphin.preferences.UserPreferences
@Composable
fun ApplicationContent(
preferences: UserPreferences,
modifier: Modifier = Modifier,
) {
val backStack = rememberNavBackStack(Destination.Main)
val navigationManager =
object : NavigationManager {
override fun navigateTo(destination: Destination) {
backStack.add(destination)
}
override fun goBack() {
backStack.removeLastOrNull()
}
override fun goToHome() {
backStack.clear()
backStack.add(Destination.Main)
}
}
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryDecorators =
listOf(
rememberSceneSetupNavEntryDecorator(),
rememberSavedStateNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider = { key ->
NavEntry(key) {
key as Destination
if (key.fullScreen) {
DestinationContent(
destination = key,
preferences = preferences,
navigationManager = navigationManager,
modifier = modifier.fillMaxSize(),
)
} else {
NavDrawer(
destination = key,
preferences = preferences,
navigationManager = navigationManager,
modifier = modifier,
)
}
}
},
)
}

View file

@ -0,0 +1,34 @@
package com.github.damontecres.dolphin.ui.nav
import androidx.navigation3.runtime.NavKey
import com.github.damontecres.dolphin.util.UuidSerializer
import kotlinx.serialization.Serializable
import java.util.UUID
@Serializable
sealed class Destination(
val fullScreen: Boolean = false,
) : NavKey {
@Serializable
data object Setup : Destination(true)
@Serializable
data object Main : Destination()
@Serializable
data object Settings : Destination(true)
@Serializable
data object Search : Destination()
@Serializable
data class MediaItem(
@Serializable(with = UuidSerializer::class) val itemId: UUID,
) : Destination()
@Serializable
data class Playback(
val itemId: String,
val positionMs: Long,
) : Destination(true)
}

View file

@ -0,0 +1,32 @@
package com.github.damontecres.dolphin.ui.nav
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.main.MainPage
@Composable
fun DestinationContent(
destination: Destination,
preferences: UserPreferences,
navigationManager: NavigationManager,
modifier: Modifier = Modifier,
) {
when (destination) {
Destination.Main -> {
MainPage(
preferences = preferences,
navigationManager = navigationManager,
modifier = modifier,
)
}
is Destination.MediaItem -> {
Text("MediaItem: ${destination.itemId}")
}
is Destination.Playback -> TODO()
Destination.Search -> TODO()
Destination.Settings -> TODO()
Destination.Setup -> TODO()
}
}

View file

@ -0,0 +1,219 @@
package com.github.damontecres.dolphin.ui.nav
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.findViewTreeViewModelStoreOwner
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.DrawerValue
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.NavigationDrawer
import androidx.tv.material3.NavigationDrawerItem
import androidx.tv.material3.NavigationDrawerScope
import androidx.tv.material3.ProvideTextStyle
import androidx.tv.material3.Text
import androidx.tv.material3.rememberDrawerState
import com.github.damontecres.dolphin.data.ServerRepository
import com.github.damontecres.dolphin.data.model.Library
import com.github.damontecres.dolphin.preferences.UserPreferences
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userViewsApi
import org.jellyfin.sdk.model.api.CollectionType
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class NavDrawerViewModel
@Inject
constructor(
val serverRepository: ServerRepository,
val api: ApiClient,
) : ViewModel() {
val libraries = MutableLiveData<List<Library>>(listOf())
init {
viewModelScope.launch {
val userViews =
api.userViewsApi
.getUserViews()
.content.items
Timber.v("userViews: ${userViews.map { it.type }}")
libraries.value = userViews.map { Library.fromDto(it, api) }
}
}
}
@Composable
fun NavDrawer(
destination: Destination,
preferences: UserPreferences,
navigationManager: NavigationManager,
modifier: Modifier = Modifier,
viewModel: NavDrawerViewModel = hiltViewModel(LocalView.current.findViewTreeViewModelStoreOwner()!!),
) {
val drawerState = rememberDrawerState(DrawerValue.Closed)
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
val context = LocalContext.current
val drawerFocusRequester = remember { FocusRequester() }
BackHandler(enabled = (drawerState.currentValue == DrawerValue.Closed && destination == Destination.Main)) {
drawerState.setValue(DrawerValue.Open)
drawerFocusRequester.requestFocus()
}
val user = viewModel.serverRepository.currentUser
val libraries by viewModel.libraries.observeAsState(listOf())
NavigationDrawer(
modifier =
modifier
.focusRequester(drawerFocusRequester),
drawerState = drawerState,
drawerContent = {
ProvideTextStyle(MaterialTheme.typography.labelMedium) {
LazyColumn(
state = listState,
contentPadding = PaddingValues(0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
modifier =
Modifier
.fillMaxHeight()
.background(MaterialTheme.colorScheme.background)
.focusGroup(),
) {
item {
IconNavItem(
text = user?.name ?: "",
icon = Icons.Default.AccountCircle,
onClick = {},
)
}
item {
IconNavItem(
text = "Home",
icon = Icons.Default.Home,
onClick = {},
)
}
items(libraries) {
LibraryNavItem(
library = it,
onClick = { navigationManager.navigateTo(Destination.MediaItem(it.id)) },
)
}
item {
IconNavItem(
text = "Settings",
icon = Icons.Default.Settings,
onClick = {},
)
}
}
}
},
) {
// Drawer content
DestinationContent(
destination = destination,
preferences = preferences,
navigationManager = navigationManager,
modifier = Modifier.fillMaxSize(),
)
}
}
@Composable
fun NavigationDrawerScope.IconNavItem(
text: String,
icon: ImageVector,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
NavigationDrawerItem(
modifier = modifier,
selected = false,
onClick = onClick,
leadingContent = {
Icon(
icon,
contentDescription = null,
)
},
interactionSource = null,
) {
Text(
modifier = Modifier,
text = text,
maxLines = 1,
)
}
}
@Composable
fun NavigationDrawerScope.LibraryNavItem(
library: Library,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
// TODO
val icon =
when (library.type) {
CollectionType.MOVIES -> Icons.Default.Email
CollectionType.TVSHOWS -> Icons.Default.DateRange
CollectionType.HOMEVIDEOS -> Icons.Default.ShoppingCart
else -> Icons.Default.Info
}
NavigationDrawerItem(
modifier = modifier,
selected = false,
onClick = onClick,
leadingContent = {
Icon(
icon,
contentDescription = null,
)
},
interactionSource = null,
) {
Text(
modifier = Modifier,
text = library.name ?: library.id.toString(),
maxLines = 1,
)
}
}

View file

@ -0,0 +1,9 @@
package com.github.damontecres.dolphin.ui.nav
interface NavigationManager {
fun navigateTo(destination: Destination)
fun goBack()
fun goToHome()
}