mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-10 00:21:19 +02:00
Remember playback choices & fix some track selection issues (#26)
Remembers changes in playback tracks across playbacks. So, if you change the audio track, quit the app, and return to the same id, the app will play the audio track previously chosen. Additionally, this PR fixes a UI inconsistency when there are multiple versions of a item. Previously sometimes the audio/subtitle tracks for wrong version would be shown during playback.
This commit is contained in:
parent
f220aeee44
commit
6194eba8c5
31 changed files with 1152 additions and 154 deletions
|
|
@ -40,6 +40,7 @@ import kotlinx.coroutines.withContext
|
|||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
|
@ -351,3 +352,11 @@ fun CoroutineScope.launchIO(
|
|||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
): Job = launch(context = Dispatchers.IO + context, start = start, block = block)
|
||||
|
||||
/**
|
||||
* Converts a UUID to the format used server-side (ie without hyphens).
|
||||
*
|
||||
* This is the inverse of [org.jellyfin.sdk.model.serializer.toUUID]
|
||||
*
|
||||
*/
|
||||
fun UUID.toServerString() = this.toString().replace("-", "")
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@ import androidx.media3.common.util.UnstableApi
|
|||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.Person
|
||||
import com.github.damontecres.wholphin.hilt.AuthOkHttpClient
|
||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||
|
|
@ -29,6 +32,7 @@ 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.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -57,6 +61,7 @@ class SeriesViewModel
|
|||
@param:ApplicationContext val context: Context,
|
||||
@param:AuthOkHttpClient private val okHttpClient: OkHttpClient,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val itemPlaybackRepository: ItemPlaybackRepository,
|
||||
) : ItemViewModel(api) {
|
||||
private var player: Player? = null
|
||||
private lateinit var seriesId: UUID
|
||||
|
|
@ -306,6 +311,23 @@ class SeriesViewModel
|
|||
release()
|
||||
navigationManager.navigateTo(destination)
|
||||
}
|
||||
|
||||
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||
private var chosenStreamsJob: Job? = null
|
||||
|
||||
fun lookUpChosenTracks(
|
||||
itemId: UUID,
|
||||
item: BaseItem,
|
||||
) {
|
||||
chosenStreamsJob?.cancel()
|
||||
chosenStreamsJob =
|
||||
viewModelScope.launchIO {
|
||||
val result = itemPlaybackRepository.getSelectedTracks(itemId, item)
|
||||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ItemListAndMapping(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.Chapter
|
||||
import com.github.damontecres.wholphin.data.model.Person
|
||||
|
|
@ -52,6 +54,7 @@ import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
|||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||
import com.github.damontecres.wholphin.ui.detail.LoadingItemViewModel
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||
|
|
@ -62,6 +65,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
|
|
@ -75,10 +79,12 @@ class MovieViewModel
|
|||
constructor(
|
||||
api: ApiClient,
|
||||
val navigationManager: NavigationManager,
|
||||
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||
) : LoadingItemViewModel(api) {
|
||||
private lateinit var itemId: UUID
|
||||
val people = MutableLiveData<List<Person>>(listOf())
|
||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||
|
||||
override fun init(
|
||||
itemId: UUID,
|
||||
|
|
@ -88,6 +94,12 @@ class MovieViewModel
|
|||
return viewModelScope.launch(ExceptionHandler()) {
|
||||
super.init(itemId, potential)?.join()
|
||||
item.value?.let { item ->
|
||||
viewModelScope.launchIO {
|
||||
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
|
||||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = result
|
||||
}
|
||||
}
|
||||
people.value =
|
||||
item.data.people
|
||||
?.letNotEmpty { people ->
|
||||
|
|
@ -123,6 +135,7 @@ fun MovieDetails(
|
|||
val people by viewModel.people.observeAsState(listOf())
|
||||
val chapters by viewModel.chapters.observeAsState(listOf())
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
val chosenStreams by viewModel.chosenStreams.observeAsState(null)
|
||||
|
||||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
|
|
@ -137,6 +150,7 @@ fun MovieDetails(
|
|||
MovieDetailsContent(
|
||||
preferences = preferences,
|
||||
movie = movie,
|
||||
chosenStreams = chosenStreams,
|
||||
people = people,
|
||||
chapters = chapters,
|
||||
playOnClick = {
|
||||
|
|
@ -222,6 +236,7 @@ fun MovieDetails(
|
|||
fun MovieDetailsContent(
|
||||
preferences: UserPreferences,
|
||||
movie: BaseItem,
|
||||
chosenStreams: ChosenStreams?,
|
||||
people: List<Person>,
|
||||
chapters: List<Chapter>,
|
||||
playOnClick: (Duration) -> Unit,
|
||||
|
|
@ -286,6 +301,7 @@ fun MovieDetailsContent(
|
|||
) {
|
||||
MovieDetailsHeader(
|
||||
movie = movie,
|
||||
chosenStreams = chosenStreams,
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
overviewOnClick = overviewOnClick,
|
||||
Modifier
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
|
|
@ -24,6 +25,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.components.DotSeparatedRow
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
|
|
@ -33,7 +35,8 @@ import com.github.damontecres.wholphin.ui.components.TitleValueText
|
|||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||
import com.github.damontecres.wholphin.ui.timeRemaining
|
||||
import com.github.damontecres.wholphin.util.formatSubtitleLang
|
||||
import com.github.damontecres.wholphin.util.getAudioDisplay
|
||||
import com.github.damontecres.wholphin.util.getSubtitleDisplay
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.PersonKind
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
|
|
@ -41,6 +44,7 @@ import org.jellyfin.sdk.model.extensions.ticks
|
|||
@Composable
|
||||
fun MovieDetailsHeader(
|
||||
movie: BaseItem,
|
||||
chosenStreams: ChosenStreams?,
|
||||
bringIntoViewRequester: BringIntoViewRequester,
|
||||
overviewOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -144,24 +148,18 @@ fun MovieDetailsHeader(
|
|||
modifier = Modifier.widthIn(max = 200.dp),
|
||||
)
|
||||
}
|
||||
dto.mediaStreams
|
||||
?.firstOrNull { it.type == MediaStreamType.AUDIO }
|
||||
?.displayTitle
|
||||
val audioDisplay =
|
||||
remember(movie.id, chosenStreams) { getAudioDisplay(movie.data, chosenStreams) }
|
||||
audioDisplay
|
||||
?.let {
|
||||
// TODO probably a cleaner way to do this
|
||||
// Removes part of "5.1 Surround - English - AAC - Default"
|
||||
it
|
||||
.replace(" - Default", "")
|
||||
.ifBlank { null }
|
||||
?.let {
|
||||
TitleValueText(
|
||||
stringResource(R.string.audio),
|
||||
it,
|
||||
modifier = Modifier.widthIn(max = 200.dp),
|
||||
)
|
||||
}
|
||||
TitleValueText(
|
||||
stringResource(R.string.audio),
|
||||
it,
|
||||
modifier = Modifier.widthIn(max = 200.dp),
|
||||
)
|
||||
}
|
||||
formatSubtitleLang(dto.mediaStreams)
|
||||
|
||||
getSubtitleDisplay(movie.data, chosenStreams)
|
||||
?.let {
|
||||
if (it.isNotNullOrBlank()) {
|
||||
TitleValueText(
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ import androidx.compose.ui.focus.FocusRequester
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons
|
||||
import com.github.damontecres.wholphin.ui.components.TitleValueText
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.util.formatSubtitleLang
|
||||
import com.github.damontecres.wholphin.util.getAudioDisplay
|
||||
import com.github.damontecres.wholphin.util.getSubtitleDisplay
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import kotlin.time.Duration
|
||||
|
|
@ -23,6 +25,7 @@ import kotlin.time.Duration
|
|||
@Composable
|
||||
fun FocusedEpisodeFooter(
|
||||
ep: BaseItem,
|
||||
chosenStreams: ChosenStreams?,
|
||||
playOnClick: (Duration) -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
|
|
@ -58,25 +61,17 @@ fun FocusedEpisodeFooter(
|
|||
it,
|
||||
)
|
||||
}
|
||||
val audioDisplay =
|
||||
remember(ep.id, chosenStreams) { getAudioDisplay(ep.data, chosenStreams) }
|
||||
audioDisplay?.let {
|
||||
TitleValueText(
|
||||
stringResource(R.string.audio),
|
||||
it,
|
||||
)
|
||||
}
|
||||
|
||||
dto.mediaStreams
|
||||
?.firstOrNull { it.type == MediaStreamType.AUDIO }
|
||||
?.displayTitle
|
||||
?.let {
|
||||
// TODO probably a cleaner way to do this
|
||||
// Removes part of "5.1 Surround - English - AAC - Default"
|
||||
it
|
||||
.replace(" - Default", "")
|
||||
.ifBlank { null }
|
||||
?.let {
|
||||
TitleValueText(
|
||||
stringResource(R.string.audio),
|
||||
it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
formatSubtitleLang(dto.mediaStreams)
|
||||
val subtitleText = getSubtitleDisplay(ep.data, chosenStreams)
|
||||
subtitleText
|
||||
?.let {
|
||||
if (it.isNotNullOrBlank()) {
|
||||
TitleValueText(
|
||||
|
|
|
|||
|
|
@ -116,6 +116,17 @@ fun SeriesOverview(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(position) {
|
||||
(episodes as? EpisodeList.Success)
|
||||
?.episodes
|
||||
?.items
|
||||
?.getOrNull(position.episodeRowIndex)
|
||||
?.let {
|
||||
viewModel.lookUpChosenTracks(it.id, it)
|
||||
}
|
||||
}
|
||||
val chosenStreams by viewModel.chosenStreams.observeAsState(null)
|
||||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
||||
|
|
@ -130,6 +141,7 @@ fun SeriesOverview(
|
|||
series = series,
|
||||
seasons = seasons.items,
|
||||
episodes = episodes,
|
||||
chosenStreams = chosenStreams,
|
||||
position = position,
|
||||
backdropImageUrl =
|
||||
remember {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import androidx.tv.material3.TabRow
|
|||
import androidx.tv.material3.TabRowDefaults
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
|
|
@ -60,6 +61,7 @@ fun SeriesOverviewContent(
|
|||
series: BaseItem,
|
||||
seasons: List<BaseItem?>,
|
||||
episodes: EpisodeList,
|
||||
chosenStreams: ChosenStreams?,
|
||||
position: SeriesOverviewPosition,
|
||||
backdropImageUrl: String?,
|
||||
firstItemFocusRequester: FocusRequester,
|
||||
|
|
@ -281,6 +283,7 @@ fun SeriesOverviewContent(
|
|||
focusedEpisode?.let { ep ->
|
||||
FocusedEpisodeFooter(
|
||||
ep = ep,
|
||||
chosenStreams = chosenStreams,
|
||||
playOnClick = playOnClick,
|
||||
moreOnClick = moreOnClick,
|
||||
watchOnClick = watchOnClick,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package com.github.damontecres.wholphin.ui.nav
|
|||
import androidx.navigation3.runtime.NavKey
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||
import com.github.damontecres.wholphin.util.UuidSerializer
|
||||
|
|
@ -70,6 +71,7 @@ sealed class Destination(
|
|||
@Transient val item: BaseItem? = null,
|
||||
val startIndex: Int? = null,
|
||||
val shuffle: Boolean = false,
|
||||
val itemPlayback: ItemPlayback? = null,
|
||||
) : Destination(true) {
|
||||
override fun toString(): String = "Playback(itemId=$itemId, positionMs=$positionMs)"
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import androidx.tv.material3.ListItem
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||
import com.github.damontecres.wholphin.ui.seekBack
|
||||
|
|
@ -398,7 +399,7 @@ fun RightPlaybackButtons(
|
|||
onSelectChoice = { index, _ ->
|
||||
val send =
|
||||
if (index == 0) {
|
||||
-1
|
||||
TrackIndex.DISABLED
|
||||
} else {
|
||||
subtitleStreams[index - 1].index
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.Chapter
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.Playlist
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.cards.ChapterCard
|
||||
|
|
@ -84,6 +85,7 @@ fun PlaybackOverlay(
|
|||
playbackSpeed: Float,
|
||||
moreButtonOptions: MoreButtonOptions,
|
||||
currentPlayback: CurrentPlayback?,
|
||||
currentItemPlayback: ItemPlayback,
|
||||
audioStreams: List<AudioStream>,
|
||||
currentSegment: MediaSegmentDto?,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -156,6 +158,7 @@ fun PlaybackOverlay(
|
|||
playbackSpeed = playbackSpeed,
|
||||
moreButtonOptions = moreButtonOptions,
|
||||
currentPlayback = currentPlayback,
|
||||
currentItemPlayback = currentItemPlayback,
|
||||
audioStreams = audioStreams,
|
||||
subtitle = subtitle,
|
||||
seekBarInteractionSource = seekBarInteractionSource,
|
||||
|
|
@ -414,6 +417,7 @@ fun Controller(
|
|||
playbackSpeed: Float,
|
||||
moreButtonOptions: MoreButtonOptions,
|
||||
currentPlayback: CurrentPlayback?,
|
||||
currentItemPlayback: ItemPlayback,
|
||||
audioStreams: List<AudioStream>,
|
||||
nextState: OverlayViewState?,
|
||||
currentSegment: MediaSegmentDto?,
|
||||
|
|
@ -461,8 +465,8 @@ fun Controller(
|
|||
seekEnabled = seekEnabled,
|
||||
seekBarInteractionSource = seekBarInteractionSource,
|
||||
moreButtonOptions = moreButtonOptions,
|
||||
subtitleIndex = currentPlayback?.subtitleIndex,
|
||||
audioIndex = currentPlayback?.audioIndex,
|
||||
subtitleIndex = currentItemPlayback.subtitleIndex,
|
||||
audioIndex = currentItemPlayback.audioIndex,
|
||||
audioStreams = audioStreams,
|
||||
playbackSpeed = playbackSpeed,
|
||||
scale = scale,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import androidx.tv.material3.Button
|
|||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.Playlist
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.preferences.skipBackOnResume
|
||||
|
|
@ -67,6 +68,7 @@ import com.github.damontecres.wholphin.util.seasonEpisode
|
|||
import kotlinx.coroutines.delay
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
|
@ -99,6 +101,12 @@ fun PlaybackPage(
|
|||
val trickplay by viewModel.trickplay.observeAsState(null)
|
||||
val chapters by viewModel.chapters.observeAsState(listOf())
|
||||
val currentPlayback by viewModel.currentPlayback.observeAsState(null)
|
||||
val currentItemPlayback by viewModel.currentItemPlayback.observeAsState(
|
||||
ItemPlayback(
|
||||
userId = -1,
|
||||
itemId = UUID.randomUUID(),
|
||||
),
|
||||
)
|
||||
val currentSegment by viewModel.currentSegment.observeAsState(null)
|
||||
var segmentCancelled by remember(currentSegment?.id) { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -318,6 +326,7 @@ fun PlaybackPage(
|
|||
playbackSpeed = playbackSpeed,
|
||||
moreButtonOptions = MoreButtonOptions(mapOf()),
|
||||
currentPlayback = currentPlayback,
|
||||
currentItemPlayback = currentItemPlayback,
|
||||
audioStreams = audioStreams,
|
||||
trickplayInfo = trickplay,
|
||||
trickplayUrlFor = viewModel::getTrickplayUrl,
|
||||
|
|
@ -331,7 +340,7 @@ fun PlaybackPage(
|
|||
}
|
||||
|
||||
// Subtitles
|
||||
if (!controllerViewState.controlsVisible && skipIndicatorDuration == 0L && currentPlayback?.subtitleIndex != null) {
|
||||
if (!controllerViewState.controlsVisible && skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
SubtitleView(context).apply {
|
||||
|
|
|
|||
|
|
@ -15,16 +15,21 @@ import androidx.media3.common.TrackSelectionOverride
|
|||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.Chapter
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.Playlist
|
||||
import com.github.damontecres.wholphin.data.model.PlaylistCreator
|
||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
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.ui.toServerString
|
||||
import com.github.damontecres.wholphin.util.EqualityMutableLiveData
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener
|
||||
|
|
@ -88,7 +93,10 @@ class PlaybackViewModel
|
|||
val api: ApiClient,
|
||||
val playlistCreator: PlaylistCreator,
|
||||
val navigationManager: NavigationManager,
|
||||
) : ViewModel() {
|
||||
val itemPlaybackDao: ItemPlaybackDao,
|
||||
val serverRepository: ServerRepository,
|
||||
) : ViewModel(),
|
||||
Player.Listener {
|
||||
val player: ExoPlayer =
|
||||
ExoPlayer
|
||||
.Builder(context)
|
||||
|
|
@ -105,6 +113,7 @@ class PlaybackViewModel
|
|||
val audioStreams = MutableLiveData<List<AudioStream>>(listOf())
|
||||
val subtitleStreams = MutableLiveData<List<SubtitleStream>>(listOf())
|
||||
val currentPlayback = MutableLiveData<CurrentPlayback?>(null)
|
||||
val currentItemPlayback = MutableLiveData<ItemPlayback>()
|
||||
val trickplay = MutableLiveData<TrickplayInfo?>(null)
|
||||
val chapters = MutableLiveData<List<Chapter>>(listOf())
|
||||
val currentSegment = EqualityMutableLiveData<MediaSegmentDto?>(null)
|
||||
|
|
@ -121,7 +130,14 @@ class PlaybackViewModel
|
|||
val playlist = MutableLiveData<Playlist>(Playlist(listOf()))
|
||||
|
||||
init {
|
||||
addCloseable { this@PlaybackViewModel.activityListener?.release() }
|
||||
player.addListener(this)
|
||||
addCloseable { player.removeListener(this@PlaybackViewModel) }
|
||||
addCloseable {
|
||||
this@PlaybackViewModel.activityListener?.let {
|
||||
it.release()
|
||||
player.removeListener(it)
|
||||
}
|
||||
}
|
||||
addCloseable { player.release() }
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +172,7 @@ class PlaybackViewModel
|
|||
queriedItem
|
||||
}
|
||||
|
||||
val played = play(base, destination.positionMs)
|
||||
val played = play(base, destination.positionMs, destination.itemPlayback)
|
||||
if (!played) {
|
||||
playUpNextUp()
|
||||
}
|
||||
|
|
@ -175,6 +191,7 @@ class PlaybackViewModel
|
|||
private suspend fun play(
|
||||
base: BaseItemDto,
|
||||
positionMs: Long,
|
||||
itemPlayback: ItemPlayback? = null,
|
||||
): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.i("Playing ${base.id}")
|
||||
|
|
@ -186,6 +203,38 @@ class PlaybackViewModel
|
|||
)
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
val playbackConfig =
|
||||
if (itemPlayback != null) {
|
||||
itemPlayback
|
||||
} else {
|
||||
val user = serverRepository.currentUser!!
|
||||
itemPlaybackDao.getItem(user, base.id)?.let {
|
||||
Timber.v("Fetched itemPlayback from DB: %s", it)
|
||||
if (it.sourceId == null) {
|
||||
it.copy(
|
||||
// TODO Better choice than first (which may not be the best resolution), using getPostedPlaybackInfo might be better?
|
||||
sourceId =
|
||||
base.mediaSources
|
||||
?.firstOrNull()
|
||||
?.id
|
||||
?.toUUIDOrNull(),
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
?: ItemPlayback(
|
||||
userId = user.rowId,
|
||||
itemId = base.id,
|
||||
sourceId =
|
||||
base.mediaSources
|
||||
?.firstOrNull()
|
||||
?.id
|
||||
?.toUUIDOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
dto = base
|
||||
val title =
|
||||
if (base.type == BaseItemKind.EPISODE) {
|
||||
|
|
@ -206,12 +255,28 @@ class PlaybackViewModel
|
|||
withContext(Dispatchers.Main) {
|
||||
this@PlaybackViewModel.title.value = title
|
||||
this@PlaybackViewModel.subtitle.value = subtitle
|
||||
this@PlaybackViewModel.currentItemPlayback.value = playbackConfig
|
||||
}
|
||||
base.mediaStreams
|
||||
?.filter { it.type == MediaStreamType.VIDEO }
|
||||
?.forEach { Timber.v("${it.videoRangeType}, ${it.videoRange}") }
|
||||
val mediaSource =
|
||||
if (playbackConfig.sourceId != null) {
|
||||
base.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == playbackConfig.sourceId }
|
||||
} else {
|
||||
base.mediaSources?.firstOrNull()
|
||||
}
|
||||
if (mediaSource == null) {
|
||||
showToast(
|
||||
context,
|
||||
"Item has no media sources, skipping...",
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
// mediaSource.mediaStreams
|
||||
// ?.filter { it.type == MediaStreamType.VIDEO }
|
||||
// ?.forEach { Timber.v("${it.videoRangeType}, ${it.videoRange}") }
|
||||
val subtitleStreams =
|
||||
base.mediaStreams
|
||||
mediaSource.mediaStreams
|
||||
?.filter { it.type == MediaStreamType.SUBTITLE }
|
||||
?.map {
|
||||
SubtitleStream(
|
||||
|
|
@ -226,7 +291,7 @@ class PlaybackViewModel
|
|||
)
|
||||
}.orEmpty()
|
||||
val audioStreams =
|
||||
base.mediaStreams
|
||||
mediaSource.mediaStreams
|
||||
?.filter { it.type == MediaStreamType.AUDIO }
|
||||
?.map {
|
||||
AudioStream(
|
||||
|
|
@ -244,50 +309,23 @@ class PlaybackViewModel
|
|||
// TODO audio selection based on channel layout or preferences or default
|
||||
val audioLanguage = preferences.userConfig.audioLanguagePreference
|
||||
val audioIndex =
|
||||
if (audioLanguage != null) {
|
||||
if (playbackConfig.audioIndexEnabled) {
|
||||
playbackConfig.audioIndex
|
||||
} else if (audioLanguage != null) {
|
||||
audioStreams.firstOrNull { it.language == audioLanguage }?.index
|
||||
?: audioStreams.firstOrNull()?.index
|
||||
} else {
|
||||
audioStreams.firstOrNull()?.index
|
||||
}
|
||||
val subtitleMode = preferences.userConfig.subtitleMode
|
||||
val subtitleLanguage = preferences.userConfig.subtitleLanguagePreference
|
||||
|
||||
val subtitleIndex =
|
||||
when (subtitleMode) {
|
||||
SubtitlePlaybackMode.ALWAYS -> {
|
||||
if (subtitleLanguage != null) {
|
||||
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index
|
||||
} else {
|
||||
subtitleStreams.firstOrNull()?.index
|
||||
}
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.ONLY_FORCED ->
|
||||
if (subtitleLanguage != null) {
|
||||
subtitleStreams.firstOrNull { it.language == subtitleLanguage && it.forced }?.index
|
||||
} else {
|
||||
subtitleStreams.firstOrNull { it.forced }?.index
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.SMART -> {
|
||||
if (audioLanguage != null && subtitleLanguage != null && audioLanguage != subtitleLanguage) {
|
||||
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.DEFAULT -> {
|
||||
// TODO check for language?
|
||||
(
|
||||
subtitleStreams.firstOrNull { it.default && it.forced }
|
||||
?: subtitleStreams.firstOrNull { it.default }
|
||||
?: subtitleStreams.firstOrNull { it.forced }
|
||||
)?.index
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.NONE -> null
|
||||
}
|
||||
determineSubtitleIndex(
|
||||
subtitleStreams = subtitleStreams,
|
||||
subtitleMode = preferences.userConfig.subtitleMode,
|
||||
subtitleLanguage = preferences.userConfig.subtitleLanguagePreference,
|
||||
audioLanguage = audioLanguage,
|
||||
playbackConfig = playbackConfig,
|
||||
)
|
||||
|
||||
// Timber.v("base.mediaStreams=${base.mediaStreams}")
|
||||
// Timber.v("subtitleTracks=$subtitleStreams")
|
||||
|
|
@ -300,9 +338,11 @@ class PlaybackViewModel
|
|||
|
||||
changeStreams(
|
||||
base,
|
||||
playbackConfig,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
if (positionMs > 0) positionMs else C.TIME_UNSET,
|
||||
itemPlayback != null, // If it was passed in, then it was not queried from the database
|
||||
)
|
||||
player.prepare()
|
||||
|
||||
|
|
@ -316,20 +356,26 @@ class PlaybackViewModel
|
|||
@OptIn(UnstableApi::class)
|
||||
private suspend fun changeStreams(
|
||||
item: BaseItemDto,
|
||||
currentItemPlayback: ItemPlayback = this@PlaybackViewModel.currentItemPlayback.value!!,
|
||||
audioIndex: Int?,
|
||||
subtitleIndex: Int?,
|
||||
positionMs: Long = C.TIME_UNSET,
|
||||
) {
|
||||
userInitiated: Boolean,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val itemId = item.id
|
||||
if (currentPlayback.value?.let {
|
||||
it.itemId == itemId &&
|
||||
it.audioIndex == audioIndex &&
|
||||
it.subtitleIndex == subtitleIndex
|
||||
} == true
|
||||
) {
|
||||
Timber.i("No change in playback for changeStreams")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
// if (currentItemPlayback.let {
|
||||
// it.itemId == itemId &&
|
||||
// it.audioIndex == audioIndex &&
|
||||
// it.subtitleIndex == subtitleIndex
|
||||
// } == true
|
||||
// ) {
|
||||
// Timber.i("No change in playback for changeStreams")
|
||||
// return@withContext
|
||||
// }
|
||||
Timber.d("changeStreams: userInitiated=$userInitiated, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
||||
|
||||
// TODO if the new audio or subtitle index is already in the streams (eg direct play), should toggle in the player instead
|
||||
val maxBitrate =
|
||||
preferences.appPreferences.playbackPreferences.maxBitrate
|
||||
|
|
@ -348,8 +394,8 @@ class PlaybackViewModel
|
|||
subtitleStreamIndex = subtitleIndex,
|
||||
allowVideoStreamCopy = true,
|
||||
allowAudioStreamCopy = true,
|
||||
autoOpenLiveStream = true,
|
||||
mediaSourceId = null,
|
||||
autoOpenLiveStream = false,
|
||||
mediaSourceId = currentItemPlayback.sourceId?.toServerString(),
|
||||
alwaysBurnInSubtitleWhenTranscoding = null,
|
||||
maxStreamingBitrate = maxBitrate.toInt(),
|
||||
),
|
||||
|
|
@ -409,14 +455,26 @@ class PlaybackViewModel
|
|||
|
||||
val playback =
|
||||
CurrentPlayback(
|
||||
itemId,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
source.id?.toUUIDOrNull(),
|
||||
listOf(),
|
||||
playMethod = transcodeType,
|
||||
playSessionId = response.playSessionId,
|
||||
)
|
||||
val itemPlayback =
|
||||
currentItemPlayback.copy(
|
||||
sourceId = source.id?.toUUIDOrNull(),
|
||||
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
|
||||
subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED,
|
||||
)
|
||||
if (userInitiated) {
|
||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||
Timber.v("Saving user initiated item playback: %s", itemPlayback)
|
||||
val rowId = itemPlaybackDao.saveItem(itemPlayback)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@PlaybackViewModel.currentItemPlayback.value =
|
||||
itemPlayback.copy(rowId = rowId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
// TODO, don't need to release & recreate when switching streams
|
||||
|
|
@ -429,6 +487,7 @@ class PlaybackViewModel
|
|||
api = api,
|
||||
player = player,
|
||||
playback = playback,
|
||||
itemPlayback = itemPlayback,
|
||||
)
|
||||
player.addListener(activityListener)
|
||||
this@PlaybackViewModel.activityListener = activityListener
|
||||
|
|
@ -436,6 +495,7 @@ class PlaybackViewModel
|
|||
duration.value = source.runTimeTicks?.ticks
|
||||
stream.value = decision
|
||||
currentPlayback.value = playback
|
||||
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
||||
player.setMediaItem(
|
||||
mediaItem,
|
||||
positionMs,
|
||||
|
|
@ -452,10 +512,6 @@ class PlaybackViewModel
|
|||
audioIndex,
|
||||
subtitleIndex,
|
||||
)
|
||||
currentPlayback.value =
|
||||
currentPlayback.value?.copy(
|
||||
tracks = checkForSupport(tracks),
|
||||
)
|
||||
player.removeListener(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -476,32 +532,34 @@ class PlaybackViewModel
|
|||
}
|
||||
|
||||
fun changeAudioStream(index: Int) {
|
||||
val itemId = currentPlayback.value?.itemId ?: return
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
changeStreams(
|
||||
dto,
|
||||
currentItemPlayback.value!!,
|
||||
index,
|
||||
currentPlayback.value?.subtitleIndex,
|
||||
currentItemPlayback.value?.subtitleIndex,
|
||||
player.currentPosition,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeSubtitleStream(index: Int?) {
|
||||
val itemId = currentPlayback.value?.itemId ?: return
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
changeStreams(
|
||||
dto,
|
||||
currentPlayback.value?.audioIndex,
|
||||
currentItemPlayback.value!!,
|
||||
currentItemPlayback.value?.audioIndex,
|
||||
index,
|
||||
player.currentPosition,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTrickplayUrl(index: Int): String? {
|
||||
val itemId = dto.id
|
||||
val mediaSourceId = currentPlayback.value?.mediaSourceId
|
||||
val mediaSourceId = currentItemPlayback.value?.sourceId
|
||||
val trickPlayInfo = trickplay.value ?: return null
|
||||
return api.trickplayApi.getTrickplayTileImageUrl(
|
||||
itemId,
|
||||
|
|
@ -643,13 +701,16 @@ class PlaybackViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
currentPlayback.value =
|
||||
currentPlayback.value?.copy(
|
||||
tracks = checkForSupport(tracks),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class CurrentPlayback(
|
||||
val itemId: UUID,
|
||||
val audioIndex: Int?,
|
||||
val subtitleIndex: Int?,
|
||||
val mediaSourceId: UUID?,
|
||||
val tracks: List<TrackSupport>,
|
||||
val playMethod: PlayMethod,
|
||||
val playSessionId: String?,
|
||||
|
|
@ -728,3 +789,53 @@ private fun applyTrackSelections(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun determineSubtitleIndex(
|
||||
subtitleStreams: List<SubtitleStream>,
|
||||
subtitleMode: SubtitlePlaybackMode,
|
||||
audioLanguage: String?,
|
||||
subtitleLanguage: String?,
|
||||
playbackConfig: ItemPlayback,
|
||||
): Int? {
|
||||
if (playbackConfig.subtitleIndex == TrackIndex.DISABLED) {
|
||||
return null
|
||||
} else if (playbackConfig.subtitleIndexEnabled) {
|
||||
return playbackConfig.subtitleIndex
|
||||
} else {
|
||||
return when (subtitleMode) {
|
||||
SubtitlePlaybackMode.ALWAYS -> {
|
||||
if (subtitleLanguage != null) {
|
||||
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index
|
||||
} else {
|
||||
subtitleStreams.firstOrNull()?.index
|
||||
}
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.ONLY_FORCED ->
|
||||
if (subtitleLanguage != null) {
|
||||
subtitleStreams.firstOrNull { it.language == subtitleLanguage && it.forced }?.index
|
||||
} else {
|
||||
subtitleStreams.firstOrNull { it.forced }?.index
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.SMART -> {
|
||||
if (audioLanguage != null && subtitleLanguage != null && audioLanguage != subtitleLanguage) {
|
||||
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.DEFAULT -> {
|
||||
// TODO check for language?
|
||||
(
|
||||
subtitleStreams.firstOrNull { it.default && it.forced }
|
||||
?: subtitleStreams.firstOrNull { it.default }
|
||||
?: subtitleStreams.firstOrNull { it.forced }
|
||||
)?.index
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.NONE -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.github.damontecres.wholphin.ui.components.CircularProgress
|
|||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import java.util.UUID
|
||||
|
||||
sealed interface ServerConnectionStatus {
|
||||
object Success : ServerConnectionStatus
|
||||
|
|
@ -45,7 +46,7 @@ sealed interface ServerConnectionStatus {
|
|||
@Composable
|
||||
fun ServerList(
|
||||
servers: List<JellyfinServer>,
|
||||
connectionStatus: Map<String, ServerConnectionStatus>,
|
||||
connectionStatus: Map<UUID, ServerConnectionStatus>,
|
||||
onSwitchServer: (JellyfinServer) -> Unit,
|
||||
onAddServer: () -> Unit,
|
||||
onRemoveServer: (JellyfinServer) -> Unit,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ import org.jellyfin.sdk.api.client.extensions.systemApi
|
|||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.model.api.QuickConnectDto
|
||||
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 java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
|
@ -39,8 +42,8 @@ class SwitchUserViewModel
|
|||
val navigationManager: NavigationManager,
|
||||
) : ViewModel() {
|
||||
val servers = MutableLiveData<List<JellyfinServer>>(listOf())
|
||||
val serverStatus = MutableLiveData<Map<String, ServerConnectionStatus>>(mapOf())
|
||||
val serverQuickConnect = MutableLiveData<Map<String, Boolean>>(mapOf())
|
||||
val serverStatus = MutableLiveData<Map<UUID, ServerConnectionStatus>>(mapOf())
|
||||
val serverQuickConnect = MutableLiveData<Map<UUID, Boolean>>(mapOf())
|
||||
|
||||
val users = MutableLiveData<List<JellyfinUser>>(listOf())
|
||||
val quickConnectState = MutableLiveData<QuickConnectResult?>(null)
|
||||
|
|
@ -245,7 +248,7 @@ class SwitchUserViewModel
|
|||
.createApi(serverUrl)
|
||||
.systemApi
|
||||
.getPublicSystemInfo()
|
||||
val id = serverInfo.id
|
||||
val id = serverInfo.id?.toUUIDOrNull()
|
||||
if (id != null && serverInfo.startupWizardCompleted == true) {
|
||||
serverRepository.addAndChangeServer(
|
||||
JellyfinServer(
|
||||
|
|
@ -310,7 +313,15 @@ class SwitchUserViewModel
|
|||
val newServerList =
|
||||
discoveredServers.value!!
|
||||
.toMutableList()
|
||||
.apply { add(JellyfinServer(server.id, server.name, server.address)) }
|
||||
.apply {
|
||||
add(
|
||||
JellyfinServer(
|
||||
server.id.toUUID(),
|
||||
server.name,
|
||||
server.address,
|
||||
),
|
||||
)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
discoveredServers.value = newServerList
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ fun UserList(
|
|||
ListItem(
|
||||
enabled = true,
|
||||
selected = user == currentUser,
|
||||
headlineContent = { Text(text = user.name ?: user.id) },
|
||||
headlineContent = { Text(text = user.name ?: user.id.toString()) },
|
||||
leadingContent = {
|
||||
if (user.id == currentUser?.id) {
|
||||
Icon(
|
||||
|
|
@ -99,7 +99,7 @@ fun UserList(
|
|||
showDeleteDialog?.let { user ->
|
||||
DialogPopup(
|
||||
showDialog = true,
|
||||
title = user.name ?: user.id,
|
||||
title = user.name ?: user.id.toString(),
|
||||
dialogItems =
|
||||
listOf(
|
||||
DialogItem("Switch", R.string.fa_arrow_left_arrow_right) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue