diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6931dcad..741efb5b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,5 @@
-
+
@@ -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">
@@ -43,4 +43,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt
index 3121618a..bd9210d0 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/MainActivity.kt
@@ -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(),
)
}
diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/ServerRepository.kt b/app/src/main/java/com/github/damontecres/dolphin/data/ServerRepository.kt
index ebafab9a..fd24aab9 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/data/ServerRepository.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/data/ServerRepository.kt
@@ -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
}
}
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt
index 3539ea82..c75cfeef 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/UiConstants.kt
@@ -48,7 +48,7 @@ val DefaultButtonPadding =
bottom = 10.dp / 2,
)
-object CardDefaults {
+object Cards {
val height2x3 = 180.dp
val playedPercentHeight = 6.dp
}
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt
index 85ab0cc7..22d2ccb9 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/BannerCard.kt
@@ -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()),
)
}
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt
index 51bbf671..a7cbe5ce 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemCard.kt
@@ -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()),
)
}
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/CircularProgress.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/CircularProgress.kt
index f854abe7..ec8b6b03 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/CircularProgress.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/CircularProgress.kt
@@ -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),
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/CollectionFolderGrid.kt
index 7244a132..56318a95 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/CollectionFolderGrid.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/CollectionFolderGrid.kt
@@ -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(
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedMovie.kt
index c0f73274..9c301bfe 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedMovie.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedMovie.kt
@@ -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(
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedTvShow.kt
index 80c1d6ba..e5815536 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedTvShow.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/RecommendedTvShow.kt
@@ -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(
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/EpisodeDetails.kt
index a8a3d96f..d0d7d8cb 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/EpisodeDetails.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/EpisodeDetails.kt
@@ -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(
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt
index 1e0ba7b5..83079193 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt
@@ -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,
)
},
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/VideoDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/VideoDetails.kt
index ac56a876..90a41b3b 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/VideoDetails.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/VideoDetails.kt
@@ -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
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt
index 981d833a..44ad8f07 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/movie/MovieDetails.kt
@@ -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(
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverview.kt
index 908533be..4710e7fc 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverview.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/series/SeriesOverview.kt
@@ -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 ->
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt
index a7493af9..36a487ae 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/HomePage.kt
@@ -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,
)
},
)
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/SearchPage.kt
index 1b258aa2..051c394f 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/SearchPage.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/SearchPage.kt
@@ -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,
)
},
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/ApplicationContent.kt
index dcd484c6..8e3f0927 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/ApplicationContent.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/ApplicationContent.kt
@@ -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) {
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavigationManager.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavigationManager.kt
index 87d22d5d..b54e6666 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavigationManager.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavigationManager.kt
@@ -42,6 +42,9 @@ class NavigationManager
while (backStack.size > 1) {
backStack.removeLastOrNull()
}
+ if (backStack[0] !is Destination.Home) {
+ backStack[0] = Destination.Home()
+ }
}
/**
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/ServerList.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/ServerList.kt
index 51795849..66346cbd 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/ServerList.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/ServerList.kt
@@ -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(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(
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchServerContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchServerContent.kt
index 5eb1f57b..d9bf2779 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchServerContent.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchServerContent.kt
@@ -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")
+ }
}
}
}
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserContent.kt
index 10795af7..04a6b124 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserContent.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserContent.kt
@@ -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 -> {}
+ }
}
}
}
diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserViewModel.kt
index c0815838..09ead631 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserViewModel.kt
@@ -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>(listOf())
+
+ val addServerState = MutableLiveData(LoadingState.Pending)
+ val switchUserState = MutableLiveData(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
+ }
+ }
+ }
+ }
}
diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/LoadingState.kt b/app/src/main/java/com/github/damontecres/dolphin/util/LoadingState.kt
index 3007ba6c..fea44fd0 100644
--- a/app/src/main/java/com/github/damontecres/dolphin/util/LoadingState.kt
+++ b/app/src/main/java/com/github/damontecres/dolphin/util/LoadingState.kt
@@ -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)
+ }
}