Refactoring switching server or user (#205)

Now when initiating a user or server switch, you can't back out of it
and must select a user/server. This will be useful when PIN code are
implemented down the road.

The app will now disconnect the web socket when backgrounded.

Also lots of internal refactoring
This commit is contained in:
damontecres 2025-11-12 17:44:16 -05:00 committed by GitHub
parent 2e3e4e3f5f
commit 58395e9adf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 442 additions and 277 deletions

View file

@ -12,6 +12,7 @@ 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.key import androidx.compose.runtime.key
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -96,7 +97,6 @@ class MainActivity : AppCompatActivity() {
) { ) {
var isRestoringSession by remember { mutableStateOf(true) } var isRestoringSession by remember { mutableStateOf(true) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (appPreferences.currentServerId.isNotBlank() && appPreferences.currentUserId.isNotBlank()) {
try { try {
serverRepository.restoreSession( serverRepository.restoreSession(
appPreferences.currentServerId?.toUUIDOrNull(), appPreferences.currentServerId?.toUUIDOrNull(),
@ -105,13 +105,11 @@ class MainActivity : AppCompatActivity() {
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.e(ex, "Exception restoring session") Timber.e(ex, "Exception restoring session")
} }
Timber.d("MainActivity session restored")
}
isRestoringSession = false isRestoringSession = false
} }
val server = serverRepository.currentServer val server by serverRepository.currentServer.observeAsState()
val user = serverRepository.currentUser val user by serverRepository.currentUser.observeAsState()
val userDto = serverRepository.currentUserDto val userDto by serverRepository.currentUserDto.observeAsState()
val preferences = val preferences =
UserPreferences( UserPreferences(
@ -139,8 +137,6 @@ class MainActivity : AppCompatActivity() {
val initialDestination = val initialDestination =
if (server != null && user != null) { if (server != null && user != null) {
Destination.Home() Destination.Home()
} else if (server != null) {
Destination.UserList
} else { } else {
Destination.ServerList Destination.ServerList
} }
@ -155,9 +151,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
LaunchedEffect(server, user) {
serverEventListener.init(server, user)
}
ApplicationContent( ApplicationContent(
user = user, user = user,
server = server, server = server,

View file

@ -25,7 +25,7 @@ class ItemPlaybackRepository
itemId: UUID, itemId: UUID,
item: BaseItem, item: BaseItem,
): ChosenStreams? = ): ChosenStreams? =
serverRepository.currentUser?.let { user -> serverRepository.currentUser.value?.let { user ->
val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId) val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId)
if (itemPlayback != null) { if (itemPlayback != null) {
Timber.v("Got itemPlayback for %s", itemId) Timber.v("Got itemPlayback for %s", itemId)
@ -72,7 +72,7 @@ class ItemPlaybackRepository
sourceId: UUID, sourceId: UUID,
): ItemPlayback? = ): ItemPlayback? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
serverRepository.currentUser?.let { user -> serverRepository.currentUser.value?.let { user ->
val itemPlayback = val itemPlayback =
ItemPlayback( ItemPlayback(
userId = user.rowId, userId = user.rowId,
@ -89,7 +89,7 @@ class ItemPlaybackRepository
itemPlayback: ItemPlayback?, itemPlayback: ItemPlayback?,
trackIndex: Int, trackIndex: Int,
type: MediaStreamType, type: MediaStreamType,
) = serverRepository.currentUser?.let { user -> ) = serverRepository.currentUser.value?.let { user ->
var toSave = var toSave =
itemPlayback ?: ItemPlayback( itemPlayback ?: ItemPlayback(
userId = user.rowId, userId = user.rowId,
@ -113,7 +113,9 @@ class ItemPlaybackRepository
val toSave = val toSave =
if (itemPlayback.userId < 0) { if (itemPlayback.userId < 0) {
val userRowId = val userRowId =
serverRepository.currentUser?.rowId?.takeIf { it >= 0 } serverRepository.currentUser.value
?.rowId
?.takeIf { it >= 0 }
?: throw IllegalStateException("Trying to save an ItemPlayback without a user, but there is no current user") ?: throw IllegalStateException("Trying to save an ItemPlayback without a user, but there is no current user")
itemPlayback.copy(userId = userRowId) itemPlayback.copy(userId = userRowId)
} else { } else {

View file

@ -27,7 +27,7 @@ class NavDrawerItemRepository
val user = serverRepository.currentUser val user = serverRepository.currentUser
val userViews = val userViews =
api.userViewsApi api.userViewsApi
.getUserViews(userId = user?.id) .getUserViews(userId = user.value?.id)
.content.items .content.items
val builtins = listOf(NavDrawerItem.Favorites) val builtins = listOf(NavDrawerItem.Favorites)
@ -46,7 +46,7 @@ class NavDrawerItemRepository
} }
suspend fun getFilteredNavDrawerItems(items: List<NavDrawerItem>): List<NavDrawerItem> { suspend fun getFilteredNavDrawerItems(items: List<NavDrawerItem>): List<NavDrawerItem> {
val user = serverRepository.currentUser val user = serverRepository.currentUser.value
val navDrawerPins = val navDrawerPins =
user user
?.let { ?.let {

View file

@ -2,14 +2,15 @@ package com.github.damontecres.wholphin.data
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.edit import androidx.core.content.edit
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -41,12 +42,12 @@ class ServerRepository
) { ) {
private val sharedPreferences = getServerSharedPreferences(context) private val sharedPreferences = getServerSharedPreferences(context)
private var _currentServer by mutableStateOf<JellyfinServer?>(null) private var _current = MutableLiveData<CurrentUser?>(null)
val currentServer get() = _currentServer val current: LiveData<CurrentUser?> = _current
private var _currentUser by mutableStateOf<JellyfinUser?>(null)
val currentUser get() = _currentUser val currentServer: LiveData<JellyfinServer?> get() = _current.map { it?.server }
private var _currentUserDto by mutableStateOf<UserDto?>(null) val currentUser: LiveData<JellyfinUser?> get() = _current.map { it?.user }
val currentUserDto get() = _currentUserDto val currentUserDto: LiveData<UserDto?> get() = _current.map { it?.userDto }
/** /**
* Adds a server to the app database and updated the [ApiClient] to the server's URL * Adds a server to the app database and updated the [ApiClient] to the server's URL
@ -57,19 +58,8 @@ class ServerRepository
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
serverDao.addOrUpdateServer(server) serverDao.addOrUpdateServer(server)
} }
changeServer(server)
}
/**
* Updates the [ApiClient] to the server's URL
*
* The current user is removed
*/
fun changeServer(server: JellyfinServer) {
apiClient.update(baseUrl = server.url, accessToken = null) apiClient.update(baseUrl = server.url, accessToken = null)
_currentServer = server _current.setValueOnMain(null)
_currentUser = null
_currentUserDto = null
} }
/** /**
@ -109,9 +99,7 @@ class ServerRepository
}.build() }.build()
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_currentUserDto = userDto _current.value = CurrentUser(updatedServer, updatedUser, userDto)
_currentServer = updatedServer
_currentUser = updatedUser
} }
sharedPreferences.edit(true) { sharedPreferences.edit(true) {
putString(SERVER_URL_KEY, updatedServer.url) putString(SERVER_URL_KEY, updatedServer.url)
@ -127,6 +115,7 @@ class ServerRepository
userId: UUID?, userId: UUID?,
): Boolean { ): Boolean {
if (serverId == null || userId == null) { if (serverId == null || userId == null) {
_current.setValueOnMain(null)
return false return false
} }
val serverAndUsers = val serverAndUsers =
@ -186,8 +175,9 @@ class ServerRepository
suspend fun removeUser(user: JellyfinUser) { suspend fun removeUser(user: JellyfinUser) {
if (currentUser == user) { if (currentUser == user) {
_currentUser = null withContext(Dispatchers.Main) {
_currentUserDto = null _current.value = null
}
userPreferencesDataStore.updateData { userPreferencesDataStore.updateData {
it it
.toBuilder() .toBuilder()
@ -204,9 +194,9 @@ class ServerRepository
suspend fun removeServer(server: JellyfinServer) { suspend fun removeServer(server: JellyfinServer) {
if (currentServer == server) { if (currentServer == server) {
_currentServer = null withContext(Dispatchers.Main) {
_currentUser = null _current.value = null
_currentUserDto = null }
userPreferencesDataStore.updateData { userPreferencesDataStore.updateData {
it it
.toBuilder() .toBuilder()
@ -222,6 +212,18 @@ class ServerRepository
} }
} }
suspend fun switchServerOrUser() {
apiClient.update(baseUrl = null, accessToken = null)
userPreferencesDataStore.updateData {
it
.toBuilder()
.apply {
currentServerId = ""
currentUserId = ""
}.build()
}
}
companion object { companion object {
fun getServerSharedPreferences(context: Context): SharedPreferences = fun getServerSharedPreferences(context: Context): SharedPreferences =
context.getSharedPreferences( context.getSharedPreferences(
@ -233,3 +235,9 @@ class ServerRepository
const val ACCESS_TOKEN_KEY = "current.accessToken" const val ACCESS_TOKEN_KEY = "current.accessToken"
} }
} }
data class CurrentUser(
val server: JellyfinServer,
val user: JellyfinUser,
val userDto: UserDto,
)

View file

@ -1,3 +1,5 @@
@file:UseSerializers(UUIDSerializer::class)
package com.github.damontecres.wholphin.data.model package com.github.damontecres.wholphin.data.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
@ -8,9 +10,13 @@ import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.Relation import androidx.room.Relation
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import org.jellyfin.sdk.model.serializer.UUIDSerializer
import java.util.UUID import java.util.UUID
@Entity(tableName = "servers") @Entity(tableName = "servers")
@Serializable
data class JellyfinServer( data class JellyfinServer(
@PrimaryKey val id: UUID, @PrimaryKey val id: UUID,
val name: String?, val name: String?,

View file

@ -145,7 +145,7 @@ class PlaylistCreator
name: String, name: String,
initialItems: List<UUID>, initialItems: List<UUID>,
): UUID? = ): UUID? =
serverRepository.currentUser?.let { user -> serverRepository.currentUser.value?.let { user ->
api.playlistsApi api.playlistsApi
.createPlaylist( .createPlaylist(
CreatePlaylistDto( CreatePlaylistDto(

View file

@ -95,7 +95,7 @@ object AppModule {
.addInterceptor { .addInterceptor {
val request = it.request() val request = it.request()
val newRequest = val newRequest =
serverRepository.currentUser?.accessToken?.let { token -> serverRepository.currentUser.value?.accessToken?.let { token ->
request request
.newBuilder() .newBuilder()
.addHeader( .addHeader(
@ -105,7 +105,7 @@ object AppModule {
clientVersion = clientInfo.version, clientVersion = clientInfo.version,
deviceId = deviceInfo.id, deviceId = deviceInfo.id,
deviceName = deviceInfo.name, deviceName = deviceInfo.name,
accessToken = serverRepository.currentUser?.accessToken, accessToken = token,
), ),
).build() ).build()
} }
@ -149,7 +149,7 @@ object AppModule {
appPreference: DataStore<AppPreferences>, appPreference: DataStore<AppPreferences>,
@IoCoroutineScope scope: CoroutineScope, @IoCoroutineScope scope: CoroutineScope,
) = object : RememberTabManager { ) = object : RememberTabManager {
fun key(itemId: String) = "${serverRepository.currentServer?.id}_${serverRepository.currentUser?.id}_$itemId" fun key(itemId: String) = "${serverRepository.currentServer.value?.id}_${serverRepository.currentUser.value?.id}_$itemId"
override fun getRememberedTab( override fun getRememberedTab(
preferences: UserPreferences, preferences: UserPreferences,

View file

@ -108,7 +108,7 @@ class CollectionFolderViewModel
val sortAndDirection = val sortAndDirection =
if (initialSortAndDirection == null) { if (initialSortAndDirection == null) {
serverRepository.currentUser?.let { user -> serverRepository.currentUser.value?.let { user ->
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
} ?: SortAndDirection.DEFAULT } ?: SortAndDirection.DEFAULT
} else { } else {
@ -123,7 +123,7 @@ class CollectionFolderViewModel
recursive: Boolean, recursive: Boolean,
filter: GetItemsFilter, filter: GetItemsFilter,
) { ) {
serverRepository.currentUser?.let { user -> serverRepository.currentUser.value?.let { user ->
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val libraryDisplayInfo = val libraryDisplayInfo =
LibraryDisplayInfo( LibraryDisplayInfo(

View file

@ -133,7 +133,7 @@ class RecommendedMovieViewModel
val suggestionsRequest = val suggestionsRequest =
GetSuggestionsRequest( GetSuggestionsRequest(
userId = serverRepository.currentUser?.id, userId = serverRepository.currentUser.value?.id,
type = listOf(BaseItemKind.MOVIE), type = listOf(BaseItemKind.MOVIE),
) )
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope) val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)

View file

@ -161,7 +161,7 @@ class RecommendedTvShowViewModel
val suggestionsRequest = val suggestionsRequest =
GetSuggestionsRequest( GetSuggestionsRequest(
userId = serverRepository.currentUser?.id, userId = serverRepository.currentUser.value?.id,
type = listOf(BaseItemKind.SERIES), type = listOf(BaseItemKind.SERIES),
) )
val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope) val suggestedItems = ApiRequestPager(api, suggestionsRequest, GetSuggestionsRequestHandler, viewModelScope)

View file

@ -66,7 +66,7 @@ class LiveTvCollectionViewModel
viewModelScope.launchIO { viewModelScope.launchIO {
val folders = val folders =
api.liveTvApi api.liveTvApi
.getRecordingFolders(userId = serverRepository.currentUser?.id) .getRecordingFolders(userId = serverRepository.currentUser.value?.id)
.content.items .content.items
.map { TabId(it.name ?: "Recordings", it.id) } .map { TabId(it.name ?: "Recordings", it.id) }
this@LiveTvCollectionViewModel.recordingFolders.setValueOnMain(folders) this@LiveTvCollectionViewModel.recordingFolders.setValueOnMain(folders)

View file

@ -61,7 +61,7 @@ class DebugViewModel
init { init {
viewModelScope.launchIO { viewModelScope.launchIO {
serverRepository.currentUser?.rowId?.let { serverRepository.currentUser.value?.rowId?.let {
val results = itemPlaybackDao.getItems(it) val results = itemPlaybackDao.getItems(it)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
itemPlaybacks.value = results itemPlaybacks.value = results

View file

@ -303,7 +303,7 @@ fun MovieDetails(
ItemDetailsDialog( ItemDetailsDialog(
info = info, info = info,
showFilePath = showFilePath =
viewModel.serverRepository.currentUserDto viewModel.serverRepository.currentUserDto.value
?.policy ?.policy
?.isAdministrator == true, ?.isAdministrator == true,
onDismissRequest = { overviewDialog = null }, onDismissRequest = { overviewDialog = null },

View file

@ -147,7 +147,7 @@ class MovieViewModel
api.libraryApi api.libraryApi
.getSimilarItems( .getSimilarItems(
GetSimilarItemsRequest( GetSimilarItemsRequest(
userId = serverRepository.currentUser?.id, userId = serverRepository.currentUser.value?.id,
itemId = itemId, itemId = itemId,
fields = SlimItemFields, fields = SlimItemFields,
limit = 25, limit = 25,

View file

@ -346,7 +346,7 @@ fun SeriesOverview(
ItemDetailsDialog( ItemDetailsDialog(
info = info, info = info,
showFilePath = showFilePath =
viewModel.serverRepository.currentUserDto viewModel.serverRepository.currentUserDto.value
?.policy ?.policy
?.isAdministrator == true, ?.isAdministrator == true,
onDismissRequest = { overviewDialog = null }, onDismissRequest = { overviewDialog = null },

View file

@ -121,7 +121,7 @@ class SeriesViewModel
api.libraryApi api.libraryApi
.getSimilarItems( .getSimilarItems(
GetSimilarItemsRequest( GetSimilarItemsRequest(
userId = serverRepository.currentUser?.id, userId = serverRepository.currentUser.value?.id,
itemId = itemId, itemId = itemId,
fields = SlimItemFields, fields = SlimItemFields,
limit = 25, limit = 25,

View file

@ -77,7 +77,7 @@ class HomeViewModel
) { ) {
Timber.d("init HomeViewModel") Timber.d("init HomeViewModel")
serverRepository.currentUserDto?.let { userDto -> serverRepository.currentUserDto.value?.let { userDto ->
val includedIds = val includedIds =
navDrawerItemRepository navDrawerItemRepository
.getFilteredNavDrawerItems(navDrawerItemRepository.getNavDrawerItems()) .getFilteredNavDrawerItems(navDrawerItemRepository.getNavDrawerItems())

View file

@ -10,6 +10,7 @@ import androidx.navigation3.ui.NavDisplay
import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DeviceProfile
/** /**
@ -45,7 +46,7 @@ fun ApplicationContent(
deviceProfile = deviceProfile, deviceProfile = deviceProfile,
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) )
} else { } else if (user != null && server != null) {
NavDrawer( NavDrawer(
destination = key, destination = key,
preferences = preferences, preferences = preferences,
@ -54,6 +55,8 @@ fun ApplicationContent(
server = server, server = server,
modifier = modifier, modifier = modifier,
) )
} else {
ErrorMessage("Trying to go to $key without a user logged in", null)
} }
} }
}, },

View file

@ -6,6 +6,7 @@ import androidx.navigation3.runtime.NavKey
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
@ -29,7 +30,9 @@ sealed class Destination(
data object ServerList : Destination(true) data object ServerList : Destination(true)
@Serializable @Serializable
data object UserList : Destination(true) data class UserList(
val server: JellyfinServer,
) : Destination(true)
@Serializable @Serializable
data class Home( data class Home(

View file

@ -54,7 +54,7 @@ fun DestinationContent(
) )
Destination.ServerList -> SwitchServerContent(modifier) Destination.ServerList -> SwitchServerContent(modifier)
Destination.UserList -> SwitchUserContent(modifier) is Destination.UserList -> SwitchUserContent(destination.server, modifier)
is Destination.Settings -> is Destination.Settings ->
PreferencesPage( PreferencesPage(

View file

@ -84,7 +84,6 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber
import java.time.LocalTime import java.time.LocalTime
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -141,7 +140,7 @@ class NavDrawerViewModel
null null
} }
} }
Timber.v("Found $index => $key") // Timber.v("Found $index => $key")
if (index != null) { if (index != null) {
selectedIndex.setValueOnMain(index) selectedIndex.setValueOnMain(index)
break break
@ -198,8 +197,8 @@ data class ServerNavDrawerItem(
fun NavDrawer( fun NavDrawer(
destination: Destination, destination: Destination,
preferences: UserPreferences, preferences: UserPreferences,
user: JellyfinUser?, user: JellyfinUser,
server: JellyfinServer?, server: JellyfinServer,
deviceProfile: DeviceProfile, deviceProfile: DeviceProfile,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: NavDrawerViewModel = viewModel: NavDrawerViewModel =
@ -319,7 +318,11 @@ fun NavDrawer(
selected = false, selected = false,
interactionSource = interactionSource, interactionSource = interactionSource,
onClick = { onClick = {
viewModel.navigationManager.navigateTo(Destination.UserList) viewModel.navigationManager.navigateToFromDrawer(
Destination.UserList(
server,
),
)
}, },
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
) )

View file

@ -28,7 +28,12 @@ class NavigationManager
*/ */
fun navigateToFromDrawer(destination: Destination) { fun navigateToFromDrawer(destination: Destination) {
goToHome() goToHome()
if (destination == Destination.ServerList || destination is Destination.UserList) {
backStack.add(destination) backStack.add(destination)
(0..<backStack.size - 1).forEach { _ -> backStack.removeAt(0) }
} else {
backStack.add(destination)
}
log() log()
} }

View file

@ -268,7 +268,7 @@ class PlaybackViewModel
if (itemPlayback != null) { if (itemPlayback != null) {
itemPlayback itemPlayback
} else { } else {
val user = serverRepository.currentUser!! serverRepository.currentUser.value?.let { user ->
itemPlaybackDao.getItem(user, base.id)?.let { itemPlaybackDao.getItem(user, base.id)?.let {
Timber.v("Fetched itemPlayback from DB: %s", it) Timber.v("Fetched itemPlayback from DB: %s", it)
if (it.sourceId != null) { if (it.sourceId != null) {
@ -278,6 +278,7 @@ class PlaybackViewModel
} }
} }
} }
}
val mediaSource = chooseSource(base, playbackConfig) val mediaSource = chooseSource(base, playbackConfig)
if (mediaSource == null) { if (mediaSource == null) {

View file

@ -49,7 +49,7 @@ class PreferencesViewModel
init { init {
viewModelScope.launchIO { viewModelScope.launchIO {
serverRepository.currentUser?.let { user -> serverRepository.currentUser.value?.let { user ->
allNavDrawerItems = navDrawerItemRepository.getNavDrawerItems() allNavDrawerItems = navDrawerItemRepository.getNavDrawerItems()
val pins = serverPreferencesDao.getNavDrawerPinnedItems(user) val pins = serverPreferencesDao.getNavDrawerPinnedItems(user)
val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) } val navDrawerPins = allNavDrawerItems.associateWith { pins.isPinned(it.id) }
@ -60,7 +60,7 @@ class PreferencesViewModel
fun updatePins(newSelectedItems: List<NavDrawerItem>) { fun updatePins(newSelectedItems: List<NavDrawerItem>) {
viewModelScope.launchIO(ExceptionHandler(true)) { viewModelScope.launchIO(ExceptionHandler(true)) {
serverRepository.currentUser?.let { user -> serverRepository.currentUser.value?.let { user ->
val disabledItems = val disabledItems =
mutableListOf<NavDrawerItem>().apply { mutableListOf<NavDrawerItem>().apply {
addAll(allNavDrawerItems) addAll(allNavDrawerItems)

View file

@ -49,6 +49,7 @@ fun ServerList(
servers: List<JellyfinServer>, servers: List<JellyfinServer>,
connectionStatus: Map<UUID, ServerConnectionStatus>, connectionStatus: Map<UUID, ServerConnectionStatus>,
onSwitchServer: (JellyfinServer) -> Unit, onSwitchServer: (JellyfinServer) -> Unit,
onTestServer: (JellyfinServer) -> Unit,
onAddServer: () -> Unit, onAddServer: () -> Unit,
onRemoveServer: (JellyfinServer) -> Unit, onRemoveServer: (JellyfinServer) -> Unit,
allowAdd: Boolean, allowAdd: Boolean,
@ -61,7 +62,7 @@ fun ServerList(
items(servers) { server -> items(servers) { server ->
val status = connectionStatus[server.id] ?: ServerConnectionStatus.Pending val status = connectionStatus[server.id] ?: ServerConnectionStatus.Pending
ListItem( ListItem(
enabled = status == ServerConnectionStatus.Success, enabled = true,
selected = false, selected = false,
headlineContent = { Text(text = server.name?.ifBlank { null } ?: server.url) }, headlineContent = { Text(text = server.name?.ifBlank { null } ?: server.url) },
supportingContent = { if (server.name.isNotNullOrBlank()) Text(text = server.url) }, supportingContent = { if (server.name.isNotNullOrBlank()) Text(text = server.url) },
@ -77,12 +78,18 @@ fun ServerList(
Icon( Icon(
imageVector = Icons.Default.Warning, imageVector = Icons.Default.Warning,
contentDescription = status.message, contentDescription = status.message,
tint = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.errorContainer,
) )
} }
} }
}, },
onClick = { onSwitchServer.invoke(server) }, onClick = {
when (status) {
ServerConnectionStatus.Success -> onSwitchServer.invoke(server)
ServerConnectionStatus.Pending -> {}
is ServerConnectionStatus.Error -> onTestServer.invoke(server)
}
},
onLongClick = { onLongClick = {
if (allowDelete) { if (allowDelete) {
showDeleteDialog = server showDeleteDialog = server

View file

@ -43,9 +43,8 @@ import com.github.damontecres.wholphin.util.LoadingState
@Composable @Composable
fun SwitchServerContent( fun SwitchServerContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SwitchUserViewModel = hiltViewModel(), viewModel: SwitchServerViewModel = hiltViewModel(),
) { ) {
val currentServer = viewModel.serverRepository.currentServer
val servers by viewModel.servers.observeAsState(listOf()) val servers by viewModel.servers.observeAsState(listOf())
val serverStatus by viewModel.serverStatus.observeAsState(mapOf()) val serverStatus by viewModel.serverStatus.observeAsState(mapOf())
@ -90,6 +89,9 @@ fun SwitchServerContent(
onSwitchServer = { onSwitchServer = {
viewModel.addServer(it.url) viewModel.addServer(it.url)
}, },
onTestServer = {
viewModel.testServer(it)
},
onAddServer = { onAddServer = {
showAddServer = true showAddServer = true
}, },
@ -142,6 +144,9 @@ fun SwitchServerContent(
onSwitchServer = { onSwitchServer = {
viewModel.addServer(it.url) viewModel.addServer(it.url)
}, },
onTestServer = {
viewModel.testServer(it)
},
onAddServer = {}, onAddServer = {},
onRemoveServer = {}, onRemoveServer = {},
allowAdd = false, allowAdd = false,

View file

@ -0,0 +1,214 @@
package com.github.damontecres.wholphin.ui.setup
import android.content.Context
import android.widget.Toast
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.JellyfinServerDao
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.HttpClientOptions
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class SwitchServerViewModel
@Inject
constructor(
@param:ApplicationContext private val context: Context,
val jellyfin: Jellyfin,
val serverRepository: ServerRepository,
val serverDao: JellyfinServerDao,
val navigationManager: NavigationManager,
) : ViewModel() {
val servers = MutableLiveData<List<JellyfinServer>>(listOf())
val serverStatus = MutableLiveData<Map<UUID, ServerConnectionStatus>>(mapOf())
val serverQuickConnect = MutableLiveData<Map<UUID, Boolean>>(mapOf())
val discoveredServers = MutableLiveData<List<JellyfinServer>>(listOf())
val addServerState = MutableLiveData<LoadingState>(LoadingState.Pending)
fun clearAddServerState() {
addServerState.value = LoadingState.Pending
}
init {
init()
}
fun init() {
viewModelScope.launchIO {
withContext(Dispatchers.Main) {
serverStatus.value = mapOf()
serverQuickConnect.value = mapOf()
}
val allServers =
serverDao
.getServers()
.map { it.server }
.sortedWith(compareBy<JellyfinServer> { it.name }.thenBy { it.url })
withContext(Dispatchers.Main) {
servers.value = allServers
}
allServers.forEach { server ->
internalTestServer(server)
}
}
}
fun testServer(server: JellyfinServer) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Pending,
)
}
viewModelScope.launchIO {
delay(1000)
val result = internalTestServer(server)
if (result == ServerConnectionStatus.Success) {
showToast(context, context.getString(R.string.success), Toast.LENGTH_SHORT)
} else if (result is ServerConnectionStatus.Error) {
showToast(context, result.message ?: "Error", Toast.LENGTH_SHORT)
}
}
}
private suspend fun internalTestServer(server: JellyfinServer): ServerConnectionStatus =
try {
jellyfin
.createApi(
server.url,
httpClientOptions =
HttpClientOptions(
requestTimeout = 6.seconds,
connectTimeout = 6.seconds,
socketTimeout = 6.seconds,
),
).systemApi
.getPublicSystemInfo()
withContext(Dispatchers.Main) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Success,
)
}
}
ServerConnectionStatus.Success
} catch (ex: Exception) {
val status = ServerConnectionStatus.Error(ex.localizedMessage)
Timber.w(ex, "Error checking server ${server.url}")
withContext(Dispatchers.Main) {
serverStatus.value =
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
status,
)
}
}
status
}
fun addServer(serverUrl: String) {
addServerState.value = LoadingState.Loading
viewModelScope.launchIO {
try {
val serverInfo by jellyfin
.createApi(serverUrl)
.systemApi
.getPublicSystemInfo()
val id = serverInfo.id?.toUUIDOrNull()
if (id != null && serverInfo.startupWizardCompleted == true) {
val server =
JellyfinServer(
id = id,
name = serverInfo.serverName,
url = serverUrl,
)
serverRepository.addAndChangeServer(server)
val quickConnect =
jellyfin
.createApi(serverUrl)
.quickConnectApi
.getQuickConnectEnabled()
.content
withContext(Dispatchers.Main) {
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(id, quickConnect)
}
}
withContext(Dispatchers.Main) {
addServerState.value = LoadingState.Success
navigationManager.navigateTo(Destination.UserList(server))
}
} else {
withContext(Dispatchers.Main) {
addServerState.value =
LoadingState.Error("Server returned invalid response")
}
}
} catch (ex: Exception) {
Timber.w(ex, "Error creating API for $serverUrl")
withContext(Dispatchers.Main) {
addServerState.value =
LoadingState.Error(exception = ex)
}
}
}
}
fun removeServer(server: JellyfinServer) {
viewModelScope.launchIO {
serverRepository.removeServer(server)
init()
}
}
fun discoverServers() {
viewModelScope.launchIO {
jellyfin.discovery.discoverLocalServers().collect { server ->
val newServerList =
discoveredServers.value!!
.toMutableList()
.apply {
add(
JellyfinServer(
server.id.toUUID(),
server.name,
server.address,
),
)
}
withContext(Dispatchers.Main) {
discoveredServers.value = newServerList
}
}
}
}
}

View file

@ -39,6 +39,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.BasicDialog
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.EditTextBox
@ -49,17 +50,20 @@ import com.github.damontecres.wholphin.util.LoadingState
@Composable @Composable
fun SwitchUserContent( fun SwitchUserContent(
currentServer: JellyfinServer,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SwitchUserViewModel = hiltViewModel(), viewModel: SwitchUserViewModel =
hiltViewModel<SwitchUserViewModel, SwitchUserViewModel.Factory>(
creationCallback = { it.create(currentServer) },
),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val currentServer = viewModel.serverRepository.currentServer // val currentServer by viewModel.serverRepository.currentServer.observeAsState()
val currentUser = viewModel.serverRepository.currentUser val currentUser by viewModel.serverRepository.currentUser.observeAsState()
val users by viewModel.users.observeAsState(listOf()) val users by viewModel.users.observeAsState(listOf())
val serverQuickConnect by viewModel.serverQuickConnect.observeAsState(mapOf()) val quickConnectEnabled by viewModel.serverQuickConnect.observeAsState(false)
val quickConnectEnabled = currentServer?.let { serverQuickConnect[it.id] ?: false } ?: false
val quickConnect by viewModel.quickConnectState.observeAsState(null) val quickConnect by viewModel.quickConnectState.observeAsState(null)
var showAddUser by remember { mutableStateOf(false) } var showAddUser by remember { mutableStateOf(false) }
@ -109,7 +113,7 @@ fun SwitchUserContent(
users = users, users = users,
currentUser = currentUser, currentUser = currentUser,
onSwitchUser = { user -> onSwitchUser = { user ->
viewModel.switchUser(server, user) viewModel.switchUser(user)
}, },
onAddUser = { showAddUser = true }, onAddUser = { showAddUser = true },
onRemoveUser = { user -> onRemoveUser = { user ->

View file

@ -8,13 +8,18 @@ import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.Jellyfin import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.HttpClientOptions import org.jellyfin.sdk.api.client.HttpClientOptions
@ -25,42 +30,41 @@ import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.api.client.extensions.userApi import org.jellyfin.sdk.api.client.extensions.userApi
import org.jellyfin.sdk.model.api.QuickConnectDto import org.jellyfin.sdk.model.api.QuickConnectDto
import org.jellyfin.sdk.model.api.QuickConnectResult import org.jellyfin.sdk.model.api.QuickConnectResult
import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@HiltViewModel @HiltViewModel(assistedFactory = SwitchUserViewModel.Factory::class)
class SwitchUserViewModel class SwitchUserViewModel
@Inject @AssistedInject
constructor( constructor(
val jellyfin: Jellyfin, val jellyfin: Jellyfin,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val serverDao: JellyfinServerDao, val serverDao: JellyfinServerDao,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
@Assisted val server: JellyfinServer,
) : ViewModel() { ) : ViewModel() {
val servers = MutableLiveData<List<JellyfinServer>>(listOf()) @AssistedFactory
val serverStatus = MutableLiveData<Map<UUID, ServerConnectionStatus>>(mapOf()) interface Factory {
val serverQuickConnect = MutableLiveData<Map<UUID, Boolean>>(mapOf()) fun create(server: JellyfinServer): SwitchUserViewModel
}
init {
viewModelScope.launch(Dispatchers.Main + ExceptionHandler()) {
serverRepository.switchServerOrUser()
}
}
val serverQuickConnect = MutableLiveData<Boolean>(false)
val users = MutableLiveData<List<JellyfinUser>>(listOf()) val users = MutableLiveData<List<JellyfinUser>>(listOf())
val quickConnectState = MutableLiveData<QuickConnectResult?>(null) val quickConnectState = MutableLiveData<QuickConnectResult?>(null)
private var quickConnectJob: Job? = null private var quickConnectJob: Job? = null
val discoveredServers = MutableLiveData<List<JellyfinServer>>(listOf())
val addServerState = MutableLiveData<LoadingState>(LoadingState.Pending)
val switchUserState = MutableLiveData<LoadingState>(LoadingState.Pending) val switchUserState = MutableLiveData<LoadingState>(LoadingState.Pending)
val loginAttempts = MutableLiveData(0) val loginAttempts = MutableLiveData(0)
fun clearAddServerState() {
addServerState.value = LoadingState.Pending
}
fun clearSwitchUserState() { fun clearSwitchUserState() {
switchUserState.value = LoadingState.Pending switchUserState.value = LoadingState.Pending
} }
@ -74,23 +78,17 @@ class SwitchUserViewModel
} }
fun init() { fun init() {
viewModelScope.launchIO {
quickConnectJob?.cancel() quickConnectJob?.cancel()
viewModelScope.launchIO {
users.setValueOnMain(listOf())
val serverUsers =
serverDao.getServer(server.id)?.users?.sortedBy { it.name } ?: listOf()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
users.value = listOf() users.setValueOnMain(serverUsers)
serverStatus.value = mapOf() }
serverQuickConnect.value = mapOf()
} }
val allServers = viewModelScope.launchIO {
serverDao
.getServers()
.map { it.server }
.sortedWith(compareBy<JellyfinServer> { it.name }.thenBy { it.url })
withContext(Dispatchers.Main) {
servers.value = allServers
}
allServers.forEach { server ->
try { try {
jellyfin jellyfin
.createApi( .createApi(
@ -109,56 +107,18 @@ class SwitchUserViewModel
.quickConnectApi .quickConnectApi
.getQuickConnectEnabled() .getQuickConnectEnabled()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
serverStatus.value = serverQuickConnect.value = quickConnect
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Success,
)
}
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(server.id, quickConnect)
}
} }
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.w(ex, "Error checking quick connect for server ${server.url}") Timber.w(ex, "Error checking quick connect for server ${server.url}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
serverStatus.value = serverQuickConnect.value = false
serverStatus.value!!.toMutableMap().apply {
put(
server.id,
ServerConnectionStatus.Error(ex.localizedMessage),
)
}
}
}
}
}
viewModelScope.launchIO {
serverRepository.currentServer?.let {
val quickConnect =
jellyfin
.createApi(it.url)
.quickConnectApi
.getQuickConnectEnabled()
.content
val serverUsers = serverDao.getServer(it.id)?.users?.sortedBy { it.name } ?: listOf()
withContext(Dispatchers.Main) {
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(it.id, quickConnect)
}
users.value = serverUsers
} }
} }
} }
} }
fun switchUser( fun switchUser(user: JellyfinUser) {
server: JellyfinServer,
user: JellyfinUser,
) {
viewModelScope.launchIO { viewModelScope.launchIO {
try { try {
serverRepository.changeUser(server, user) serverRepository.changeUser(server, user)
@ -242,10 +202,7 @@ class SwitchUserViewModel
if (ex is InvalidStatusException && ex.status == 401) { if (ex is InvalidStatusException && ex.status == 401) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
quickConnectState.value = null quickConnectState.value = null
serverQuickConnect.value = serverQuickConnect.value = false
serverQuickConnect.value!!.toMutableMap().apply {
put(server.id, false)
}
} }
} }
setError("Error with Quick Connect", ex) setError("Error with Quick Connect", ex)
@ -258,55 +215,6 @@ class SwitchUserViewModel
quickConnectState.value = null quickConnectState.value = null
} }
fun addServer(serverUrl: String) {
addServerState.value = LoadingState.Loading
viewModelScope.launchIO {
try {
val serverInfo by jellyfin
.createApi(serverUrl)
.systemApi
.getPublicSystemInfo()
val id = serverInfo.id?.toUUIDOrNull()
if (id != null && serverInfo.startupWizardCompleted == true) {
serverRepository.addAndChangeServer(
JellyfinServer(
id = id,
name = serverInfo.serverName,
url = serverUrl,
),
)
val quickConnect =
jellyfin
.createApi(serverUrl)
.quickConnectApi
.getQuickConnectEnabled()
.content
withContext(Dispatchers.Main) {
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(id, quickConnect)
}
}
withContext(Dispatchers.Main) {
addServerState.value = LoadingState.Success
navigationManager.navigateTo(Destination.UserList)
}
} else {
withContext(Dispatchers.Main) {
addServerState.value =
LoadingState.Error("Server returned invalid response")
}
}
} catch (ex: Exception) {
Timber.w(ex, "Error creating API for $serverUrl")
withContext(Dispatchers.Main) {
addServerState.value =
LoadingState.Error(exception = ex)
}
}
}
}
fun removeUser(user: JellyfinUser) { fun removeUser(user: JellyfinUser) {
viewModelScope.launchIO { viewModelScope.launchIO {
serverRepository.removeUser(user) serverRepository.removeUser(user)
@ -318,35 +226,6 @@ class SwitchUserViewModel
} }
} }
fun removeServer(server: JellyfinServer) {
viewModelScope.launchIO {
serverRepository.removeServer(server)
init()
}
}
fun discoverServers() {
viewModelScope.launchIO {
jellyfin.discovery.discoverLocalServers().collect { server ->
val newServerList =
discoveredServers.value!!
.toMutableList()
.apply {
add(
JellyfinServer(
server.id.toUUID(),
server.name,
server.address,
),
)
}
withContext(Dispatchers.Main) {
discoveredServers.value = newServerList
}
}
}
}
private suspend fun setError( private suspend fun setError(
msg: String? = null, msg: String? = null,
ex: Exception? = null, ex: Exception? = null,

View file

@ -2,13 +2,17 @@ package com.github.damontecres.wholphin.util
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.hilt.IoCoroutineScope
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ActivityContext
import kotlinx.coroutines.CoroutineScope import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -20,24 +24,36 @@ import org.jellyfin.sdk.model.api.GeneralCommandType
import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.MediaType
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
@Singleton @ActivityScoped
class ServerEventListener class ServerEventListener
@Inject @Inject
constructor( constructor(
@param:ApplicationContext private val context: Context, @param:ActivityContext private val context: Context,
private val api: ApiClient, private val api: ApiClient,
@param:IoCoroutineScope private val scope: CoroutineScope, private val serverRepository: ServerRepository,
) { ) : DefaultLifecycleObserver {
private val activity = (context as AppCompatActivity)
private var listenJob: Job? = null private var listenJob: Job? = null
init {
activity.lifecycle.addObserver(this)
serverRepository.current.observe(activity) {
Timber.d("New user/server: %s", it)
listenJob?.cancel()
if (it != null) {
init(it.server, it.user)
}
}
}
fun init( fun init(
server: JellyfinServer?, server: JellyfinServer?,
user: JellyfinUser?, user: JellyfinUser?,
) { ) {
if (server != null && user != null && api.baseUrl != null && api.accessToken != null) { if (server != null && user != null && api.baseUrl != null && api.accessToken != null) {
scope.launchIO { (context as AppCompatActivity).lifecycleScope.launchIO {
api.sessionApi.postCapabilities( api.sessionApi.postCapabilities(
playableMediaTypes = listOf(MediaType.VIDEO), playableMediaTypes = listOf(MediaType.VIDEO),
supportedCommands = supportedCommands =
@ -53,6 +69,7 @@ class ServerEventListener
} }
fun setupListeners() { fun setupListeners() {
serverRepository.currentUser
Timber.v("Subscribing to WebSocket") Timber.v("Subscribing to WebSocket")
listenJob?.cancel() listenJob?.cancel()
listenJob = listenJob =
@ -73,6 +90,20 @@ class ServerEventListener
.joinToString("\n") .joinToString("\n")
showToast(context, toast, Toast.LENGTH_LONG) showToast(context, toast, Toast.LENGTH_LONG)
} }
}.launchIn(scope) }.launchIn(activity.lifecycleScope)
}
override fun onResume(owner: LifecycleOwner) {
serverRepository.current.value?.let { init(it.server, it.user) }
}
override fun onPause(owner: LifecycleOwner) {
Timber.v("Cancelling WebSocket")
listenJob?.cancel()
}
override fun onStop(owner: LifecycleOwner) {
Timber.v("Cancelling WebSocket")
listenJob?.cancel()
} }
} }

View file

@ -147,6 +147,7 @@
<string name="italic_font">Italicize font</string> <string name="italic_font">Italicize font</string>
<string name="font">Font</string> <string name="font">Font</string>
<string name="background">Background</string> <string name="background">Background</string>
<string name="success">Success</string>
<plurals name="downloads"> <plurals name="downloads">
<item quantity="zero">%s downloads</item> <item quantity="zero">%s downloads</item>