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

@ -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)

View file

@ -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 ->
if (preferences.currentServerId.isNotBlank() && preferences.currentUserId.isNotBlank()) { LaunchedEffect(Unit) {
scope.launch { if (preferences.currentServerId.isNotBlank() && preferences.currentUserId.isNotBlank()) {
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())
} }
} }
} }

View file

@ -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
} }

View file

@ -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(

View file

@ -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 {
apiClient.userApi val userDto =
.getCurrentUser() apiClient.userApi
.content.name .getCurrentUser()
_currentServer = server .content
_currentUser = user val updatedServer = server.copy(name = userDto.serverName)
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)

View file

@ -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}")
} }

View file

@ -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,
)
}
}

View file

@ -30,7 +30,8 @@ object DatabaseModule {
context, context,
DolphinDatabase::class.java, DolphinDatabase::class.java,
"dolphin", "dolphin",
).build() ).fallbackToDestructiveMigration(false)
.build()
@Provides @Provides
@Singleton @Singleton

View file

@ -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,

View file

@ -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()
} }
} }

View file

@ -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,
) )
} }

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()
}

View file

@ -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)
}
}

View file

@ -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" }