mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Basic navigation
This commit is contained in:
parent
51ad92f539
commit
80070f4174
18 changed files with 496 additions and 22 deletions
|
|
@ -8,6 +8,7 @@ plugins {
|
||||||
alias(libs.plugins.hilt)
|
alias(libs.plugins.hilt)
|
||||||
alias(libs.plugins.room)
|
alias(libs.plugins.room)
|
||||||
alias(libs.plugins.protobuf)
|
alias(libs.plugins.protobuf)
|
||||||
|
alias(libs.plugins.kotlin.plugin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -112,6 +113,8 @@ dependencies {
|
||||||
implementation(libs.androidx.room.common.jvm)
|
implementation(libs.androidx.room.common.jvm)
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.lifecycle.viewmodel.navigation3)
|
||||||
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,31 @@ package com.github.damontecres.dolphin
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import com.github.damontecres.dolphin.data.ServerRepository
|
import com.github.damontecres.dolphin.data.ServerRepository
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.ServerLoginPage
|
import com.github.damontecres.dolphin.ui.ServerLoginPage
|
||||||
import com.github.damontecres.dolphin.ui.main.MainPage
|
import com.github.damontecres.dolphin.ui.nav.ApplicationContent
|
||||||
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
|
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -45,26 +54,37 @@ class MainActivity : AppCompatActivity() {
|
||||||
) {
|
) {
|
||||||
CoilConfig(serverRepository, okHttpClient, false)
|
CoilConfig(serverRepository, okHttpClient, false)
|
||||||
|
|
||||||
|
var isRestoringSession by remember { mutableStateOf(true) }
|
||||||
val preferences by userPreferencesDataStore.data.collectAsState(null)
|
val preferences by userPreferencesDataStore.data.collectAsState(null)
|
||||||
preferences?.let { preferences ->
|
preferences?.let { preferences ->
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
if (preferences.currentServerId.isNotBlank() && preferences.currentUserId.isNotBlank()) {
|
if (preferences.currentServerId.isNotBlank() && preferences.currentUserId.isNotBlank()) {
|
||||||
scope.launch {
|
|
||||||
serverRepository.restoreSession(
|
serverRepository.restoreSession(
|
||||||
preferences.currentServerId,
|
preferences.currentServerId,
|
||||||
preferences.currentUserId,
|
preferences.currentUserId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
isRestoringSession = false
|
||||||
ServerLoginPage(modifier = Modifier.fillMaxSize())
|
|
||||||
}
|
}
|
||||||
val server = serverRepository.currentServer
|
val server = serverRepository.currentServer
|
||||||
val user = serverRepository.currentUser
|
val user = serverRepository.currentUser
|
||||||
if (server != null && user != null) {
|
if (server != null && user != null) {
|
||||||
// TODO navigation
|
ApplicationContent(
|
||||||
MainPage(
|
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
} else if (isRestoringSession) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(200.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color = MaterialTheme.colorScheme.border,
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ServerLoginPage(modifier = Modifier.fillMaxSize())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@ package com.github.damontecres.dolphin.data
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
|
||||||
@Database(entities = [JellyfinServer::class, JellyfinUser::class], version = 1, exportSchema = false)
|
@Database(
|
||||||
|
entities = [JellyfinServer::class, JellyfinUser::class],
|
||||||
|
version = 2,
|
||||||
|
exportSchema = false,
|
||||||
|
)
|
||||||
abstract class DolphinDatabase : RoomDatabase() {
|
abstract class DolphinDatabase : RoomDatabase() {
|
||||||
abstract fun serverDao(): JellyfinServerDao
|
abstract fun serverDao(): JellyfinServerDao
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import androidx.room.Transaction
|
||||||
@Entity(tableName = "servers")
|
@Entity(tableName = "servers")
|
||||||
data class JellyfinServer(
|
data class JellyfinServer(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
val name: String,
|
val name: String?,
|
||||||
val url: String,
|
val url: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ data class JellyfinUser(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
@ColumnInfo(index = true)
|
@ColumnInfo(index = true)
|
||||||
val serverId: String,
|
val serverId: String,
|
||||||
val accessToken: String,
|
val accessToken: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class JellyfinServerUsers(
|
data class JellyfinServerUsers(
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
||||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
class ServerRepository
|
class ServerRepository
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -55,11 +57,22 @@ class ServerRepository
|
||||||
Timber.v("Changing user to ${user.name} on ${server.url}")
|
Timber.v("Changing user to ${user.name} on ${server.url}")
|
||||||
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
||||||
try {
|
try {
|
||||||
|
val userDto =
|
||||||
apiClient.userApi
|
apiClient.userApi
|
||||||
.getCurrentUser()
|
.getCurrentUser()
|
||||||
.content.name
|
.content
|
||||||
_currentServer = server
|
val updatedServer = server.copy(name = userDto.serverName)
|
||||||
_currentUser = user
|
val updatedUser =
|
||||||
|
user.copy(
|
||||||
|
id = userDto.id.toString(),
|
||||||
|
name = userDto.name,
|
||||||
|
)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
serverDao.addServer(updatedServer)
|
||||||
|
serverDao.addUser(updatedUser)
|
||||||
|
}
|
||||||
|
_currentServer = updatedServer
|
||||||
|
_currentUser = updatedUser
|
||||||
} catch (e: InvalidStatusException) {
|
} catch (e: InvalidStatusException) {
|
||||||
// TODO
|
// TODO
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ fun convertModel(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
): DolphinModel =
|
): DolphinModel =
|
||||||
when (dto.type) {
|
when (dto.type) {
|
||||||
|
BaseItemKind.COLLECTION_FOLDER -> Library.fromDto(dto, api)
|
||||||
|
|
||||||
|
// TODO
|
||||||
BaseItemKind.VIDEO -> Video.fromDto(dto, api)
|
BaseItemKind.VIDEO -> Video.fromDto(dto, api)
|
||||||
|
BaseItemKind.SERIES -> Video.fromDto(dto, api)
|
||||||
|
BaseItemKind.MOVIE -> Video.fromDto(dto, api)
|
||||||
|
BaseItemKind.SEASON -> Video.fromDto(dto, api)
|
||||||
|
BaseItemKind.EPISODE -> Video.fromDto(dto, api)
|
||||||
else -> throw IllegalArgumentException("Unsupported item type: ${dto.type}")
|
else -> throw IllegalArgumentException("Unsupported item type: ${dto.type}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.github.damontecres.dolphin.data.model
|
||||||
|
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class Library(
|
||||||
|
override val id: UUID,
|
||||||
|
override val name: String?,
|
||||||
|
override val imageUrl: String?,
|
||||||
|
val type: CollectionType,
|
||||||
|
) : DolphinModel {
|
||||||
|
companion object {
|
||||||
|
fun fromDto(
|
||||||
|
dto: BaseItemDto,
|
||||||
|
api: ApiClient,
|
||||||
|
): Library =
|
||||||
|
Library(
|
||||||
|
id = dto.id,
|
||||||
|
name = dto.name,
|
||||||
|
imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY),
|
||||||
|
type = dto.collectionType ?: CollectionType.UNKNOWN,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,8 @@ object DatabaseModule {
|
||||||
context,
|
context,
|
||||||
DolphinDatabase::class.java,
|
DolphinDatabase::class.java,
|
||||||
"dolphin",
|
"dolphin",
|
||||||
).build()
|
).fallbackToDestructiveMigration(false)
|
||||||
|
.build()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
|
||||||
|
|
@ -57,17 +57,18 @@ class ServerManagementViewModel
|
||||||
|
|
||||||
val accessToken = authenticationResult.accessToken
|
val accessToken = authenticationResult.accessToken
|
||||||
if (accessToken != null) {
|
if (accessToken != null) {
|
||||||
|
val authedUser = authenticationResult.user
|
||||||
val server =
|
val server =
|
||||||
authenticationResult.serverId?.let {
|
authenticationResult.serverId?.let {
|
||||||
JellyfinServer(
|
JellyfinServer(
|
||||||
id = it,
|
id = it,
|
||||||
name = serverUrl,
|
name = authedUser?.serverName,
|
||||||
url = serverUrl,
|
url = serverUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
val user =
|
val user =
|
||||||
authenticationResult.user?.let {
|
authedUser?.let {
|
||||||
JellyfinUser(
|
JellyfinUser(
|
||||||
id = it.id.toString(),
|
id = it.id.toString(),
|
||||||
name = it.name,
|
name = it.name,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.dolphin.ui.cards
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.github.damontecres.dolphin.data.model.DolphinModel
|
import com.github.damontecres.dolphin.data.model.DolphinModel
|
||||||
|
import com.github.damontecres.dolphin.data.model.Library
|
||||||
import com.github.damontecres.dolphin.data.model.Video
|
import com.github.damontecres.dolphin.data.model.Video
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -18,6 +19,8 @@ fun DolphinCard(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is Library -> TODO()
|
||||||
null -> TODO()
|
null -> TODO()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,18 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.dolphin.data.model.DolphinModel
|
import com.github.damontecres.dolphin.data.model.DolphinModel
|
||||||
import com.github.damontecres.dolphin.data.model.convertModel
|
import com.github.damontecres.dolphin.data.model.convertModel
|
||||||
import com.github.damontecres.dolphin.isNotNullOrBlank
|
import com.github.damontecres.dolphin.isNotNullOrBlank
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.cards.DolphinCard
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -118,8 +120,9 @@ class MainViewModel
|
||||||
@Composable
|
@Composable
|
||||||
fun MainPage(
|
fun MainPage(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
|
navigationManager: NavigationManager,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
viewModel: MainViewModel = viewModel(),
|
viewModel: MainViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val homeRows by viewModel.homeRows.observeAsState(listOf())
|
val homeRows by viewModel.homeRows.observeAsState(listOf())
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
|
|
@ -129,6 +132,7 @@ fun MainPage(
|
||||||
item {
|
item {
|
||||||
HomePageRow(
|
HomePageRow(
|
||||||
row = row,
|
row = row,
|
||||||
|
onClickItem = { navigationManager.navigateTo(Destination.MediaItem(it.id)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +144,7 @@ fun MainPage(
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePageRow(
|
fun HomePageRow(
|
||||||
row: HomeRow,
|
row: HomeRow,
|
||||||
|
onClickItem: (DolphinModel) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -160,7 +165,7 @@ fun HomePageRow(
|
||||||
items(row.items) { item ->
|
items(row.items) { item ->
|
||||||
DolphinCard(
|
DolphinCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = {},
|
onClick = { onClickItem.invoke(item) },
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.nav
|
||||||
|
|
||||||
|
interface NavigationManager {
|
||||||
|
fun navigateTo(destination: Destination)
|
||||||
|
|
||||||
|
fun goBack()
|
||||||
|
|
||||||
|
fun goToHome()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.github.damontecres.dolphin.util
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class UuidSerializer : KSerializer<UUID> {
|
||||||
|
override val descriptor: SerialDescriptor
|
||||||
|
get() = PrimitiveSerialDescriptor("UUID", PrimitiveKind.LONG)
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): UUID = UUID(decoder.decodeLong(), decoder.decodeLong())
|
||||||
|
|
||||||
|
override fun serialize(
|
||||||
|
encoder: Encoder,
|
||||||
|
value: UUID,
|
||||||
|
) {
|
||||||
|
encoder.encodeLong(value.mostSignificantBits)
|
||||||
|
encoder.encodeLong(value.leastSignificantBits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
[versions]
|
[versions]
|
||||||
agp = "8.13.0"
|
agp = "8.13.0"
|
||||||
|
hiltNavigationCompose = "1.3.0"
|
||||||
kotlin = "2.2.20"
|
kotlin = "2.2.20"
|
||||||
ksp = "2.2.20-2.0.2" # https://github.com/google/ksp/issues/2596 2.0.3 is broken
|
ksp = "2.2.20-2.0.2" # https://github.com/google/ksp/issues/2596 2.0.3 is broken
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
|
|
@ -25,6 +26,7 @@ protobuf-javalite = "4.32.1"
|
||||||
hilt = "2.57.1"
|
hilt = "2.57.1"
|
||||||
room = "2.8.0"
|
room = "2.8.0"
|
||||||
material3 = "1.3.2"
|
material3 = "1.3.2"
|
||||||
|
lifecycleViewmodelNavigation3 = "2.10.0-alpha03"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
|
@ -40,6 +42,7 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat
|
||||||
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime-android", version.ref = "compose-runtime" }
|
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime-android", version.ref = "compose-runtime" }
|
||||||
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "compose-runtime" }
|
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "compose-runtime" }
|
||||||
|
|
||||||
|
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
|
||||||
androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" }
|
androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" }
|
||||||
androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" }
|
androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" }
|
||||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||||
|
|
@ -79,6 +82,7 @@ androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref =
|
||||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
|
|
||||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||||
|
lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNavigation3" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue