mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Better error handling during setup
This commit is contained in:
parent
59b3ef2644
commit
3afc4f022b
24 changed files with 382 additions and 198 deletions
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
|
@ -30,7 +29,8 @@
|
|||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Dolphin"
|
||||
android:name=".DolphinApplication">
|
||||
android:name=".DolphinApplication"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
|
|
@ -43,4 +43,4 @@
|
|||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -32,8 +32,6 @@ import com.github.damontecres.dolphin.ui.CoilConfig
|
|||
import com.github.damontecres.dolphin.ui.nav.ApplicationContent
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.dolphin.ui.setup.SwitchServerContent
|
||||
import com.github.damontecres.dolphin.ui.setup.SwitchUserContent
|
||||
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
|
||||
import com.github.damontecres.dolphin.util.profile.createDeviceProfile
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
|
@ -78,10 +76,14 @@ class MainActivity : AppCompatActivity() {
|
|||
var isRestoringSession by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
if (appPreferences.currentServerId.isNotBlank()) {
|
||||
serverRepository.restoreSession(
|
||||
appPreferences.currentServerId,
|
||||
appPreferences.currentUserId,
|
||||
)
|
||||
try {
|
||||
serverRepository.restoreSession(
|
||||
appPreferences.currentServerId,
|
||||
appPreferences.currentUserId,
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception restoring session")
|
||||
}
|
||||
Timber.d("MainActivity session restored")
|
||||
}
|
||||
isRestoringSession = false
|
||||
|
|
@ -100,19 +102,8 @@ class MainActivity : AppCompatActivity() {
|
|||
remember(appPreferences) {
|
||||
createDeviceProfile(this@MainActivity, preferences, false)
|
||||
}
|
||||
val backStack = rememberNavBackStack(Destination.Home())
|
||||
navigationManager.backStack = backStack
|
||||
|
||||
if (server != null && user != null) {
|
||||
ApplicationContent(
|
||||
user = user,
|
||||
server = server,
|
||||
navigationManager = navigationManager,
|
||||
preferences = preferences,
|
||||
deviceProfile = deviceProfile,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
} else if (isRestoringSession) {
|
||||
if (isRestoringSession) {
|
||||
Box(
|
||||
modifier = Modifier.size(200.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -122,13 +113,21 @@ class MainActivity : AppCompatActivity() {
|
|||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
} else if (server != null) {
|
||||
// Have server but no user, go to user selection
|
||||
SwitchUserContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
} else {
|
||||
SwitchServerContent(
|
||||
val initialDestination =
|
||||
if (server != null && user != null) {
|
||||
Destination.Home()
|
||||
} else {
|
||||
Destination.ServerList
|
||||
}
|
||||
val backStack = rememberNavBackStack(initialDestination)
|
||||
navigationManager.backStack = backStack
|
||||
ApplicationContent(
|
||||
user = user,
|
||||
server = server,
|
||||
navigationManager = navigationManager,
|
||||
preferences = preferences,
|
||||
deviceProfile = deviceProfile,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.model.api.AuthenticationResult
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
|
|
@ -66,49 +65,36 @@ class ServerRepository
|
|||
server: JellyfinServer,
|
||||
user: JellyfinUser,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (server.id != user.serverId) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
Timber.v("Changing user to ${user.name} on ${server.url}")
|
||||
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
||||
val userDto =
|
||||
apiClient.userApi
|
||||
.getCurrentUser()
|
||||
.content
|
||||
if (server.id != user.serverId) {
|
||||
throw IllegalStateException("User is not part of the server")
|
||||
}
|
||||
Timber.v("Changing user to ${user.name} on ${server.url}")
|
||||
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
||||
val userDto =
|
||||
apiClient.userApi
|
||||
.getCurrentUser()
|
||||
.content
|
||||
|
||||
val updatedServer = server.copy(name = userDto.serverName)
|
||||
val updatedUser =
|
||||
user.copy(
|
||||
id = userDto.id.toString(),
|
||||
name = userDto.name,
|
||||
)
|
||||
serverDao.addServer(updatedServer)
|
||||
serverDao.addUser(updatedUser)
|
||||
userPreferencesDataStore.updateData {
|
||||
it
|
||||
.toBuilder()
|
||||
.apply {
|
||||
currentServerId = updatedServer.id
|
||||
currentUserId = updatedUser.id
|
||||
}.build()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_currentUserDto = userDto
|
||||
_currentServer = updatedServer
|
||||
_currentUser = updatedUser
|
||||
}
|
||||
} catch (e: InvalidStatusException) {
|
||||
// TODO
|
||||
Timber.e(e)
|
||||
withContext(Dispatchers.Main) {
|
||||
// Unauthorized
|
||||
_currentServer = null
|
||||
_currentUser = null
|
||||
}
|
||||
if (e.status == 401) {
|
||||
return@withContext
|
||||
}
|
||||
val updatedServer = server.copy(name = userDto.serverName)
|
||||
val updatedUser =
|
||||
user.copy(
|
||||
id = userDto.id.toString(),
|
||||
name = userDto.name,
|
||||
)
|
||||
serverDao.addServer(updatedServer)
|
||||
serverDao.addUser(updatedUser)
|
||||
userPreferencesDataStore.updateData {
|
||||
it
|
||||
.toBuilder()
|
||||
.apply {
|
||||
currentServerId = updatedServer.id
|
||||
currentUserId = updatedUser.id
|
||||
}.build()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_currentUserDto = userDto
|
||||
_currentServer = updatedServer
|
||||
_currentUser = updatedUser
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ val DefaultButtonPadding =
|
|||
bottom = 10.dp / 2,
|
||||
)
|
||||
|
||||
object CardDefaults {
|
||||
object Cards {
|
||||
val height2x3 = 180.dp
|
||||
val playedPercentHeight = 6.dp
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.dolphin.ui.AppColors
|
||||
import com.github.damontecres.dolphin.ui.CardDefaults
|
||||
import com.github.damontecres.dolphin.ui.Cards
|
||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
|
||||
/**
|
||||
|
|
@ -131,7 +131,7 @@ fun BannerCard(
|
|||
.background(
|
||||
MaterialTheme.colorScheme.tertiary,
|
||||
).clip(RectangleShape)
|
||||
.height(CardDefaults.playedPercentHeight)
|
||||
.height(Cards.playedPercentHeight)
|
||||
.fillMaxWidth((playPercent / 100).toFloat()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ import androidx.tv.material3.Text
|
|||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.dolphin.R
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.ui.CardDefaults
|
||||
import com.github.damontecres.dolphin.ui.Cards
|
||||
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||
import com.github.damontecres.dolphin.ui.enableMarquee
|
||||
import com.github.damontecres.dolphin.ui.ifElse
|
||||
|
|
@ -282,7 +282,7 @@ fun ItemCardImage(
|
|||
.background(
|
||||
MaterialTheme.colorScheme.tertiary,
|
||||
).clip(RectangleShape)
|
||||
.height(CardDefaults.playedPercentHeight)
|
||||
.height(Cards.playedPercentHeight)
|
||||
.fillMaxWidth((percent / 100.0).toFloat()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,10 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import com.github.damontecres.dolphin.ui.ifElse
|
||||
|
||||
@Composable
|
||||
fun CircularProgress(
|
||||
modifier: Modifier = Modifier,
|
||||
fillMaxSize: Boolean = true,
|
||||
) {
|
||||
Box(modifier = modifier.ifElse(fillMaxSize, Modifier.fillMaxSize())) {
|
||||
fun CircularProgress(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.border,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
|
|
|
|||
|
|
@ -182,7 +182,9 @@ fun CollectionFolderGrid(
|
|||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
pager?.let { pager ->
|
||||
CollectionFolderGridContent(
|
||||
|
|
|
|||
|
|
@ -143,7 +143,9 @@ fun RecommendedMovie(
|
|||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
|
||||
LoadingState.Success ->
|
||||
HomePageContent(
|
||||
|
|
|
|||
|
|
@ -155,7 +155,9 @@ fun RecommendedTvShow(
|
|||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
|
||||
LoadingState.Success ->
|
||||
HomePageContent(
|
||||
|
|
|
|||
|
|
@ -64,7 +64,9 @@ fun EpisodeDetails(
|
|||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
item?.let { item ->
|
||||
EpisodeDetailsContent(
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import com.github.damontecres.dolphin.R
|
|||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.data.model.Person
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.CardDefaults
|
||||
import com.github.damontecres.dolphin.ui.Cards
|
||||
import com.github.damontecres.dolphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.dolphin.ui.cards.PersonRow
|
||||
import com.github.damontecres.dolphin.ui.cards.SeasonCard
|
||||
|
|
@ -95,7 +95,9 @@ fun SeriesDetails(
|
|||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
item?.let { item ->
|
||||
val played = item.data.userData?.played ?: false
|
||||
|
|
@ -241,7 +243,7 @@ fun SeriesDetailsContent(
|
|||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = mod,
|
||||
imageHeight = CardDefaults.height2x3,
|
||||
imageHeight = Cards.height2x3,
|
||||
imageWidth = Dp.Unspecified,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ fun VideoDetails(
|
|||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
item?.let { item ->
|
||||
val dto = item.data
|
||||
|
|
|
|||
|
|
@ -131,7 +131,9 @@ fun MovieDetails(
|
|||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
LoadingState.Success -> {
|
||||
item?.let { movie ->
|
||||
MovieDetailsContent(
|
||||
|
|
|
|||
|
|
@ -111,7 +111,9 @@ fun SeriesOverview(
|
|||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
|
||||
LoadingState.Success -> {
|
||||
series?.let { series ->
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import androidx.tv.material3.Text
|
|||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.CardDefaults
|
||||
import com.github.damontecres.dolphin.ui.Cards
|
||||
import com.github.damontecres.dolphin.ui.cards.BannerCard
|
||||
import com.github.damontecres.dolphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
|
||||
|
|
@ -76,7 +76,9 @@ fun HomePage(
|
|||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
||||
LoadingState.Loading -> LoadingPage()
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
|
||||
LoadingState.Success -> {
|
||||
val homeRows by viewModel.homeRows.observeAsState(listOf())
|
||||
|
|
@ -195,7 +197,7 @@ fun HomePageContent(
|
|||
Modifier.focusRequester(positionFocusRequester),
|
||||
),
|
||||
interactionSource = null,
|
||||
cardHeight = CardDefaults.height2x3,
|
||||
cardHeight = Cards.height2x3,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.CardDefaults
|
||||
import com.github.damontecres.dolphin.ui.Cards
|
||||
import com.github.damontecres.dolphin.ui.DefaultItemFields
|
||||
import com.github.damontecres.dolphin.ui.cards.EpisodeCard
|
||||
import com.github.damontecres.dolphin.ui.cards.ItemRow
|
||||
|
|
@ -177,7 +177,7 @@ fun SearchPage(
|
|||
item = item,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
imageHeight = CardDefaults.height2x3,
|
||||
imageHeight = Cards.height2x3,
|
||||
modifier = mod,
|
||||
)
|
||||
},
|
||||
|
|
@ -199,7 +199,7 @@ fun SearchPage(
|
|||
item = item,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
imageHeight = CardDefaults.height2x3,
|
||||
imageHeight = Cards.height2x3,
|
||||
modifier = mod,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import timber.log.Timber
|
|||
*/
|
||||
@Composable
|
||||
fun ApplicationContent(
|
||||
server: JellyfinServer,
|
||||
user: JellyfinUser,
|
||||
server: JellyfinServer?,
|
||||
user: JellyfinUser?,
|
||||
navigationManager: NavigationManager,
|
||||
preferences: UserPreferences,
|
||||
deviceProfile: DeviceProfile,
|
||||
|
|
@ -39,7 +39,7 @@ fun ApplicationContent(
|
|||
),
|
||||
entryProvider = { key ->
|
||||
key as Destination
|
||||
val contentKey = "${key}_${server.id}_${user.id}"
|
||||
val contentKey = "${key}_${server?.id}_${user?.id}"
|
||||
Timber.d("Navigate: %s", key)
|
||||
NavEntry(key, contentKey = contentKey) {
|
||||
if (key.fullScreen) {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ class NavigationManager
|
|||
while (backStack.size > 1) {
|
||||
backStack.removeLastOrNull()
|
||||
}
|
||||
if (backStack[0] !is Destination.Home) {
|
||||
backStack[0] = Destination.Home()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.dolphin.ui.setup
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -17,6 +18,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.IconButtonDefaults
|
||||
import androidx.tv.material3.ListItem
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
|
|
@ -47,6 +49,8 @@ fun ServerList(
|
|||
onSwitchServer: (JellyfinServer) -> Unit,
|
||||
onAddServer: () -> Unit,
|
||||
onRemoveServer: (JellyfinServer) -> Unit,
|
||||
allowAdd: Boolean,
|
||||
allowDelete: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showDeleteDialog by remember { mutableStateOf<JellyfinServer?>(null) }
|
||||
|
|
@ -63,7 +67,9 @@ fun ServerList(
|
|||
when (status) {
|
||||
ServerConnectionStatus.Success -> {}
|
||||
ServerConnectionStatus.Pending -> {
|
||||
CircularProgress()
|
||||
CircularProgress(
|
||||
Modifier.size(IconButtonDefaults.MediumIconSize),
|
||||
)
|
||||
}
|
||||
is ServerConnectionStatus.Error -> {
|
||||
Icon(
|
||||
|
|
@ -76,32 +82,36 @@ fun ServerList(
|
|||
},
|
||||
onClick = { onSwitchServer.invoke(server) },
|
||||
onLongClick = {
|
||||
showDeleteDialog = server
|
||||
if (allowDelete) {
|
||||
showDeleteDialog = server
|
||||
}
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
item {
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
enabled = true,
|
||||
selected = false,
|
||||
headlineContent = { Text(text = "Add Server") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
tint = Color.Green.copy(alpha = .8f),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = { onAddServer.invoke() },
|
||||
modifier = Modifier,
|
||||
)
|
||||
if (allowAdd) {
|
||||
item {
|
||||
HorizontalDivider()
|
||||
ListItem(
|
||||
enabled = true,
|
||||
selected = false,
|
||||
headlineContent = { Text(text = "Add Server") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
tint = Color.Green.copy(alpha = .8f),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = { onAddServer.invoke() },
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
showDeleteDialog?.let { server ->
|
||||
DialogPopup(
|
||||
showDialog = true,
|
||||
showDialog = allowDelete,
|
||||
title = server.name ?: server.url,
|
||||
dialogItems =
|
||||
listOf(
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
|
|
@ -30,10 +32,11 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.dolphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.dolphin.ui.components.CircularProgress
|
||||
import com.github.damontecres.dolphin.ui.components.EditTextBox
|
||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.dolphin.util.LoadingState
|
||||
|
||||
@Composable
|
||||
fun SwitchServerContent(
|
||||
|
|
@ -44,64 +47,131 @@ fun SwitchServerContent(
|
|||
val servers by viewModel.servers.observeAsState(listOf())
|
||||
val serverStatus by viewModel.serverStatus.observeAsState(mapOf())
|
||||
|
||||
val discoveredServers by viewModel.discoveredServers.observeAsState(listOf())
|
||||
|
||||
var showAddServer by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.discoverServers()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(.5f)
|
||||
.align(Alignment.Center)
|
||||
.padding(16.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
),
|
||||
.padding(32.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Select Server",
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
ServerList(
|
||||
servers = servers,
|
||||
connectionStatus = serverStatus,
|
||||
onSwitchServer = {
|
||||
viewModel.addServer(it.url) {
|
||||
viewModel.navigationManager.navigateTo(Destination.UserList)
|
||||
}
|
||||
},
|
||||
onAddServer = {
|
||||
showAddServer = true
|
||||
},
|
||||
onRemoveServer = {
|
||||
viewModel.removeServer(it)
|
||||
},
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(16.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
),
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Select Server",
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
ServerList(
|
||||
servers = servers,
|
||||
connectionStatus = serverStatus,
|
||||
onSwitchServer = {
|
||||
viewModel.addServer(it.url)
|
||||
},
|
||||
onAddServer = {
|
||||
showAddServer = true
|
||||
},
|
||||
onRemoveServer = {
|
||||
viewModel.removeServer(it)
|
||||
},
|
||||
allowAdd = true,
|
||||
allowDelete = true,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
),
|
||||
)
|
||||
}
|
||||
// Discover
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(16.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = "Discovered Servers",
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
if (discoveredServers.isEmpty()) {
|
||||
Text(
|
||||
text = "Searching...",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
} else {
|
||||
ServerList(
|
||||
servers = discoveredServers,
|
||||
connectionStatus =
|
||||
discoveredServers
|
||||
.map { it.id }
|
||||
.associateWith { ServerConnectionStatus.Success },
|
||||
onSwitchServer = {
|
||||
viewModel.addServer(it.url)
|
||||
},
|
||||
onAddServer = {},
|
||||
onRemoveServer = {},
|
||||
allowAdd = false,
|
||||
allowDelete = false,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddServer) {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.clearAddServerState()
|
||||
}
|
||||
val state by viewModel.addServerState.observeAsState(LoadingState.Pending)
|
||||
var url by remember { mutableStateOf("") }
|
||||
val submit = {
|
||||
showAddServer = false
|
||||
viewModel.addServer(url) {
|
||||
viewModel.navigationManager.navigateTo(Destination.UserList)
|
||||
}
|
||||
viewModel.addServer(url)
|
||||
}
|
||||
BasicDialog(
|
||||
onDismissRequest = { showAddServer = false },
|
||||
onDismissRequest = {
|
||||
showAddServer = false
|
||||
viewModel.clearAddServerState()
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
elevation = 10.dp,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
|
|
@ -118,7 +188,10 @@ fun SwitchServerContent(
|
|||
)
|
||||
EditTextBox(
|
||||
value = url,
|
||||
onValueChange = { url = it },
|
||||
onValueChange = {
|
||||
url = it
|
||||
viewModel.clearAddServerState()
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
|
|
@ -134,12 +207,28 @@ fun SwitchServerContent(
|
|||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
when (val st = state) {
|
||||
is LoadingState.Error -> {
|
||||
Text(
|
||||
text =
|
||||
st.message ?: st.exception?.localizedMessage
|
||||
?: "An error occurred",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
Button(
|
||||
onClick = { submit.invoke() },
|
||||
enabled = url.isNotNullOrBlank(),
|
||||
enabled = url.isNotNullOrBlank() && state == LoadingState.Pending,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
Text(text = "Submit")
|
||||
if (state == LoadingState.Loading) {
|
||||
CircularProgress(Modifier.size(32.dp))
|
||||
} else {
|
||||
Text(text = "Submit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.dolphin.ui.setup
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -22,6 +23,7 @@ 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.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -35,12 +37,15 @@ import com.github.damontecres.dolphin.ui.components.EditTextBox
|
|||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.dolphin.util.LoadingState
|
||||
|
||||
@Composable
|
||||
fun SwitchUserContent(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SwitchUserViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val currentServer = viewModel.serverRepository.currentServer
|
||||
val currentUser = viewModel.serverRepository.currentUser
|
||||
val users by viewModel.users.observeAsState(listOf())
|
||||
|
|
@ -50,6 +55,20 @@ fun SwitchUserContent(
|
|||
val quickConnect by viewModel.quickConnectState.observeAsState(null)
|
||||
var showAddUser by remember { mutableStateOf(false) }
|
||||
|
||||
val userState by viewModel.switchUserState.observeAsState(LoadingState.Pending)
|
||||
LaunchedEffect(userState) {
|
||||
if (!showAddUser) {
|
||||
when (val s = userState) {
|
||||
is LoadingState.Error -> {
|
||||
val msg = s.message ?: s.exception?.localizedMessage
|
||||
Toast.makeText(context, "Error: $msg", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentServer?.let { server ->
|
||||
Box(
|
||||
modifier = modifier,
|
||||
|
|
@ -81,9 +100,7 @@ fun SwitchUserContent(
|
|||
users = users,
|
||||
currentUser = currentUser,
|
||||
onSwitchUser = { user ->
|
||||
viewModel.switchUser(server, user) {
|
||||
viewModel.navigationManager.goToHome()
|
||||
}
|
||||
viewModel.switchUser(server, user)
|
||||
},
|
||||
onAddUser = { showAddUser = true },
|
||||
onRemoveUser = { user ->
|
||||
|
|
@ -106,6 +123,7 @@ fun SwitchUserContent(
|
|||
if (showAddUser) {
|
||||
var useQuickConnect by remember { mutableStateOf(quickConnectEnabled) }
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.clearSwitchUserState()
|
||||
if (useQuickConnect) {
|
||||
viewModel.initiateQuickConnect(server) {
|
||||
viewModel.navigationManager.goToHome()
|
||||
|
|
@ -151,10 +169,7 @@ fun SwitchUserContent(
|
|||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
val onSubmit = {
|
||||
viewModel.login(server, username, password) {
|
||||
showAddUser = false
|
||||
viewModel.navigationManager.goToHome()
|
||||
}
|
||||
viewModel.login(server, username, password)
|
||||
}
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
|
|
@ -173,7 +188,10 @@ fun SwitchUserContent(
|
|||
)
|
||||
EditTextBox(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
onValueChange = {
|
||||
username = it
|
||||
viewModel.clearSwitchUserState()
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
|
|
@ -193,7 +211,10 @@ fun SwitchUserContent(
|
|||
)
|
||||
EditTextBox(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
onValueChange = {
|
||||
password = it
|
||||
viewModel.clearSwitchUserState()
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
|
|
@ -215,6 +236,16 @@ fun SwitchUserContent(
|
|||
Text("Login")
|
||||
}
|
||||
}
|
||||
when (val s = userState) {
|
||||
is LoadingState.Error -> {
|
||||
Text(
|
||||
text = s.message ?: s.exception?.localizedMessage ?: "Error",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import com.github.damontecres.dolphin.data.JellyfinServer
|
|||
import com.github.damontecres.dolphin.data.JellyfinServerDao
|
||||
import com.github.damontecres.dolphin.data.JellyfinUser
|
||||
import com.github.damontecres.dolphin.data.ServerRepository
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
import com.github.damontecres.dolphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||
import com.github.damontecres.dolphin.util.LoadingState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -16,6 +18,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.HttpClientOptions
|
||||
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
||||
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
||||
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
|
||||
|
|
@ -25,6 +28,7 @@ import org.jellyfin.sdk.model.api.QuickConnectDto
|
|||
import org.jellyfin.sdk.model.api.QuickConnectResult
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltViewModel
|
||||
class SwitchUserViewModel
|
||||
|
|
@ -44,6 +48,19 @@ class SwitchUserViewModel
|
|||
|
||||
private var quickConnectJob: Job? = null
|
||||
|
||||
val discoveredServers = MutableLiveData<List<JellyfinServer>>(listOf())
|
||||
|
||||
val addServerState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val switchUserState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
|
||||
fun clearAddServerState() {
|
||||
addServerState.value = LoadingState.Pending
|
||||
}
|
||||
|
||||
fun clearSwitchUserState() {
|
||||
switchUserState.value = LoadingState.Pending
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
val allServers =
|
||||
|
|
@ -57,8 +74,15 @@ class SwitchUserViewModel
|
|||
allServers.forEach { server ->
|
||||
try {
|
||||
jellyfin
|
||||
.createApi(server.url)
|
||||
.systemApi
|
||||
.createApi(
|
||||
server.url,
|
||||
httpClientOptions =
|
||||
HttpClientOptions(
|
||||
requestTimeout = 6.seconds,
|
||||
connectTimeout = 6.seconds,
|
||||
socketTimeout = 6.seconds,
|
||||
),
|
||||
).systemApi
|
||||
.getPublicSystemInfo()
|
||||
val quickConnect by
|
||||
jellyfin
|
||||
|
|
@ -80,13 +104,15 @@ class SwitchUserViewModel
|
|||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Error checking quick connect for server ${server.url}")
|
||||
serverStatus.value =
|
||||
serverStatus.value!!.toMutableMap().apply {
|
||||
put(
|
||||
server.id,
|
||||
ServerConnectionStatus.Error(ex.localizedMessage),
|
||||
)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
serverStatus.value =
|
||||
serverStatus.value!!.toMutableMap().apply {
|
||||
put(
|
||||
server.id,
|
||||
ServerConnectionStatus.Error(ex.localizedMessage),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -113,11 +139,14 @@ class SwitchUserViewModel
|
|||
fun switchUser(
|
||||
server: JellyfinServer,
|
||||
user: JellyfinUser,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
serverRepository.changeUser(server, user)
|
||||
callback.invoke()
|
||||
try {
|
||||
serverRepository.changeUser(server, user)
|
||||
navigationManager.goToHome()
|
||||
} catch (ex: Exception) {
|
||||
switchUserState.value = LoadingState.Error(exception = ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +154,6 @@ class SwitchUserViewModel
|
|||
server: JellyfinServer,
|
||||
username: String,
|
||||
password: String,
|
||||
onAuthenticated: () -> Unit,
|
||||
) {
|
||||
quickConnectJob?.cancel()
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
|
|
@ -137,12 +165,10 @@ class SwitchUserViewModel
|
|||
)
|
||||
serverRepository.changeUser(server.url, authenticationResult)
|
||||
withContext(Dispatchers.Main) {
|
||||
onAuthenticated.invoke()
|
||||
}
|
||||
} catch (err: InvalidStatusException) {
|
||||
if (err.status == 401) {
|
||||
// TODO set state to notify user
|
||||
navigationManager.goToHome()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
switchUserState.value = LoadingState.Error(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -185,14 +211,15 @@ class SwitchUserViewModel
|
|||
onAuthenticated.invoke()
|
||||
}
|
||||
}
|
||||
} catch (err: InvalidStatusException) {
|
||||
if (err.status == 401) {
|
||||
} catch (ex: Exception) {
|
||||
if (ex is InvalidStatusException && ex.status == 401) {
|
||||
quickConnectState.value = null
|
||||
serverQuickConnect.value =
|
||||
serverQuickConnect.value!!.toMutableMap().apply {
|
||||
put(server.id, false)
|
||||
}
|
||||
}
|
||||
switchUserState.value = LoadingState.Error(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -202,18 +229,16 @@ class SwitchUserViewModel
|
|||
quickConnectState.value = null
|
||||
}
|
||||
|
||||
fun addServer(
|
||||
serverUrl: String,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
fun addServer(serverUrl: String) {
|
||||
addServerState.value = LoadingState.Loading
|
||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
try {
|
||||
val serverInfo by jellyfin
|
||||
.createApi(serverUrl)
|
||||
.systemApi
|
||||
.getPublicSystemInfo()
|
||||
val id = serverInfo.id
|
||||
if (id != null) {
|
||||
if (id != null && serverInfo.startupWizardCompleted == true) {
|
||||
serverRepository.addAndChangeServer(
|
||||
JellyfinServer(
|
||||
id = id,
|
||||
|
|
@ -233,13 +258,22 @@ class SwitchUserViewModel
|
|||
put(id, quickConnect)
|
||||
}
|
||||
}
|
||||
callback.invoke()
|
||||
withContext(Dispatchers.Main) {
|
||||
addServerState.value = LoadingState.Success
|
||||
navigationManager.navigateTo(Destination.UserList)
|
||||
}
|
||||
} else {
|
||||
// TODO
|
||||
withContext(Dispatchers.Main) {
|
||||
addServerState.value =
|
||||
LoadingState.Error("Server returned invalid response")
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Error creating API for $serverUrl")
|
||||
// TODO notify user
|
||||
withContext(Dispatchers.Main) {
|
||||
addServerState.value =
|
||||
LoadingState.Error(exception = ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -255,4 +289,18 @@ class SwitchUserViewModel
|
|||
serverRepository.removeServer(server)
|
||||
}
|
||||
}
|
||||
|
||||
fun discoverServers() {
|
||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
jellyfin.discovery.discoverLocalServers().collect { server ->
|
||||
val newServerList =
|
||||
discoveredServers.value!!
|
||||
.toMutableList()
|
||||
.apply { add(JellyfinServer(server.id, server.name, server.address)) }
|
||||
withContext(Dispatchers.Main) {
|
||||
discoveredServers.value = newServerList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ package com.github.damontecres.dolphin.util
|
|||
* Generic state for loading something from the API
|
||||
*/
|
||||
sealed interface LoadingState {
|
||||
object Pending : LoadingState
|
||||
|
||||
object Loading : LoadingState
|
||||
|
||||
object Success : LoadingState
|
||||
|
|
@ -11,5 +13,7 @@ sealed interface LoadingState {
|
|||
data class Error(
|
||||
val message: String? = null,
|
||||
val exception: Throwable? = null,
|
||||
) : LoadingState
|
||||
) : LoadingState {
|
||||
constructor(exception: Throwable) : this(null, exception)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue