Merge branch 'main' into develop/mpv

This commit is contained in:
Damontecres 2025-11-14 16:16:40 -05:00
commit e3efb2e500
No known key found for this signature in database
23 changed files with 166 additions and 97 deletions

View file

@ -5,13 +5,13 @@ import android.content.SharedPreferences
import androidx.core.content.edit
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.JellyfinUser
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.util.EqualityMutableLiveData
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -42,7 +42,7 @@ class ServerRepository
) {
private val sharedPreferences = getServerSharedPreferences(context)
private var _current = MutableLiveData<CurrentUser?>(null)
private var _current = EqualityMutableLiveData<CurrentUser?>(null)
val current: LiveData<CurrentUser?> = _current
val currentServer: LiveData<JellyfinServer?> get() = _current.map { it?.server }

View file

@ -74,6 +74,12 @@ object Cards {
val playedPercentHeight = 6.dp
}
object AspectRatios {
const val WIDE = 16f / 9f
const val FOUR_THREE = 4f / 3f
const val TALL = 2f / 3f
}
@Preview(
device = "spec:parent=tv_1080p",
backgroundColor = 0xFF383535,

View file

@ -34,6 +34,7 @@ import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
@ -53,7 +54,7 @@ fun BannerCard(
favorite: Boolean = false,
playPercent: Double = 0.0,
cardHeight: Dp = 140.dp * .85f,
aspectRatio: Float = 16f / 9,
aspectRatio: Float = AspectRatios.WIDE,
interactionSource: MutableInteractionSource? = null,
) {
var imageError by remember { mutableStateOf(false) }

View file

@ -30,9 +30,9 @@ fun ChapterCard(
position: Duration,
imageUrl: String?,
onClick: () -> Unit,
aspectRatio: Float,
modifier: Modifier = Modifier,
cardHeight: Dp = 140.dp * .85f,
aspectRatio: Float = 16f / 9,
onLongClick: (() -> Unit)? = null,
interactionSource: MutableInteractionSource? = null,
) {

View file

@ -21,6 +21,7 @@ import com.github.damontecres.wholphin.data.model.Chapter
@Composable
fun ChapterRow(
chapters: List<Chapter>,
aspectRatio: Float,
onClick: (Chapter) -> Unit,
modifier: Modifier = Modifier,
onLongClick: ((Chapter) -> Unit)? = null,
@ -50,6 +51,7 @@ fun ChapterRow(
position = item.position,
imageUrl = item.imageUrl,
onClick = { onClick(item) },
aspectRatio = aspectRatio,
modifier = Modifier,
onLongClick = onLongClick?.let { { it(item) } },
)

View file

@ -30,6 +30,7 @@ import androidx.tv.material3.CardDefaults
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.seasonEpisode
import kotlinx.coroutines.delay
@ -63,7 +64,7 @@ fun EpisodeCard(
} else {
focusedAfterDelay = false
}
val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f)
val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: AspectRatios.TALL
val width = imageHeight * aspectRatio
val height = imageWidth * (1f / aspectRatio)
Column(

View file

@ -25,6 +25,7 @@ import androidx.tv.material3.CardDefaults
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.enableMarquee
import kotlinx.coroutines.delay
@ -38,7 +39,7 @@ fun GridCard(
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
imageAspectRatio: Float = 2f / 3f,
imageAspectRatio: Float = AspectRatios.TALL,
) {
val dto = item?.data
val focused by interactionSource.collectIsFocusedAsState()

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.util.FocusPair
@Composable
@ -112,7 +113,8 @@ fun BannerItemRow(
name = title,
imageUrl = item?.imageUrl,
aspectRatio =
aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat()
?: AspectRatios.WIDE,
cornerText = item?.data?.indexNumber?.let { "E$it" },
played = item?.data?.userData?.played ?: false,
playPercent = item?.data?.userData?.playedPercentage ?: 0.0,

View file

@ -22,6 +22,7 @@ import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.enableMarquee
import kotlinx.coroutines.delay
@ -81,7 +82,7 @@ fun PersonCard(
modifier =
Modifier
.fillMaxWidth()
.aspectRatio(2f / 3f), // TODO,
.aspectRatio(AspectRatios.TALL), // TODO,
)
}
Column(

View file

@ -26,6 +26,7 @@ import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.enableMarquee
import kotlinx.coroutines.delay
@ -42,7 +43,7 @@ fun SeasonCard(
imageWidth: Dp = Dp.Unspecified,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
showImageOverlay: Boolean = false,
aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f),
aspectRatio: Float = item?.data?.primaryImageAspectRatio?.toFloat() ?: AspectRatios.TALL,
) = SeasonCard(
title = item?.title,
subtitle = item?.subtitle,
@ -82,7 +83,7 @@ fun SeasonCard(
imageWidth: Dp = Dp.Unspecified,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
showImageOverlay: Boolean = false,
aspectRatio: Float = (2f / 3f),
aspectRatio: Float = AspectRatios.TALL,
) {
val focused by interactionSource.collectIsFocusedAsState()
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
@ -102,7 +103,6 @@ fun SeasonCard(
} else {
focusedAfterDelay = false
}
// val aspectRatio = dto?.primaryImageAspectRatio?.toFloat() ?: (2f / 3f)
val width = imageHeight * aspectRatio
val height = imageWidth * (1f / aspectRatio)
Column(

View file

@ -12,7 +12,7 @@ import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
@ -26,6 +26,8 @@ import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -95,8 +97,8 @@ class RecommendedMovieViewModel
),
)
withContext(Dispatchers.Main) {
loading.value = LoadingState.Success
if (resumeItems.isNotEmpty()) {
loading.setValueOnMain(LoadingState.Success)
}
} catch (ex: Exception) {
Timber.e(ex, "Exception fetching movie recommendations")
@ -159,23 +161,31 @@ class RecommendedMovieViewModel
R.string.suggestions to suggestedItems,
R.string.top_unwatched to unwatchedTopRatedItems,
)
rows.forEachIndexed { index, (title, pager) ->
viewModelScope.launchIO {
val title = context.getString(title)
val result =
try {
pager.init()
if (pager.isNotEmpty()) {
pager.getBlocking(0)
rows
.mapIndexed { index, (title, pager) ->
viewModelScope.async(Dispatchers.IO + ExceptionHandler()) {
val title = context.getString(title)
val result =
try {
pager.init()
if (pager.isNotEmpty()) {
pager.getBlocking(0)
}
HomeRowLoadingState.Success(title, pager)
} catch (ex: Exception) {
Timber.e(ex, "Error fetching %s", title)
HomeRowLoadingState.Error(title, null, ex)
}
HomeRowLoadingState.Success(title, pager)
} catch (ex: Exception) {
Timber.e(ex, "Error fetching %s", title)
HomeRowLoadingState.Error(title, null, ex)
update(index + 1, result)
if (result is HomeRowLoadingState.Success && result.items.isNotEmpty() &&
(loading.value == LoadingState.Loading || loading.value == LoadingState.Pending)
) {
loading.setValueOnMain(LoadingState.Success)
}
update(index + 1, result)
}
}
}.awaitAll()
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
loading.setValueOnMain(LoadingState.Success)
}
}
}

View file

@ -12,7 +12,7 @@ import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
@ -27,6 +27,8 @@ import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -123,8 +125,8 @@ class RecommendedTvShowViewModel
),
)
withContext(Dispatchers.Main) {
loading.value = LoadingState.Success
if (resumeItems.isNotEmpty() || nextUpItems.isNotEmpty()) {
loading.setValueOnMain(LoadingState.Success)
}
} catch (ex: Exception) {
Timber.e(ex, "Exception fetching tv recommendations")
@ -188,19 +190,31 @@ class RecommendedTvShowViewModel
R.string.top_unwatched to unwatchedTopRatedItems,
)
rows.forEachIndexed { index, (title, pager) ->
viewModelScope.launchIO {
val title = context.getString(title)
val result =
try {
pager.init()
HomeRowLoadingState.Success(title, pager)
} catch (ex: Exception) {
Timber.e(ex, "Error fetching %s", title)
HomeRowLoadingState.Error(title, null, ex)
rows
.mapIndexed { index, (title, pager) ->
viewModelScope.async(Dispatchers.IO + ExceptionHandler()) {
val title = context.getString(title)
val result =
try {
pager.init()
if (pager.isNotEmpty()) {
pager.getBlocking(0)
}
HomeRowLoadingState.Success(title, pager)
} catch (ex: Exception) {
Timber.e(ex, "Error fetching %s", title)
HomeRowLoadingState.Error(title, null, ex)
}
update(index + 2, result)
if (result is HomeRowLoadingState.Success && result.items.isNotEmpty() &&
(loading.value == LoadingState.Loading || loading.value == LoadingState.Pending)
) {
loading.setValueOnMain(LoadingState.Success)
}
update(index + 2, result)
}
}
}.awaitAll()
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
loading.setValueOnMain(LoadingState.Success)
}
}
}

View file

@ -51,9 +51,11 @@ import com.github.damontecres.wholphin.data.model.LocalTrailer
import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
import com.github.damontecres.wholphin.data.model.chooseSource
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.TrailerService
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.ChapterRow
import com.github.damontecres.wholphin.ui.cards.ItemRow
@ -486,6 +488,7 @@ fun MovieDetailsContent(
item {
ChapterRow(
chapters = chapters,
aspectRatio = movie.data.aspectRatioFloat ?: AspectRatios.WIDE,
onClick = {
position = CHAPTER_ROW
playOnClick.invoke(it.position)

View file

@ -42,8 +42,8 @@ import coil3.compose.AsyncImage
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.data.model.aspectRatioFloat
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.cards.BannerCard
import com.github.damontecres.wholphin.ui.components.ErrorMessage
@ -216,10 +216,8 @@ fun SeriesOverviewContent(
?.data
?.primaryImageAspectRatio
?.toFloat()
?.coerceAtLeast(
episode.data.aspectRatioFloat ?: (4f / 3f),
)
?: (16f / 9), // TODO some episode images don't match the file's aspect ratio
?.coerceAtLeast(AspectRatios.FOUR_THREE)
?: (AspectRatios.WIDE),
cornerText = cornerText,
played = episode?.data?.userData?.played ?: false,
playPercent =

View file

@ -46,6 +46,7 @@ import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.BannerCard
import com.github.damontecres.wholphin.ui.cards.ItemRow
@ -197,8 +198,20 @@ fun HomePageContent(
loadingState: LoadingState? = null,
) {
val scope = rememberCoroutineScope()
val firstRow =
remember {
homeRows
.indexOfFirst {
when (it) {
is HomeRowLoadingState.Error -> false
is HomeRowLoadingState.Loading -> true
is HomeRowLoadingState.Pending -> true
is HomeRowLoadingState.Success -> it.items.isNotEmpty()
}
}.coerceAtLeast(0)
}
var position by rememberSaveable(stateSaver = RowColumnSaver) {
mutableStateOf(RowColumn(0, 0))
mutableStateOf(RowColumn(firstRow, 0))
}
var focusedItem =
position.let {
@ -208,11 +221,19 @@ fun HomePageContent(
val listState = rememberLazyListState()
val focusRequester = remember { FocusRequester() }
val positionFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
positionFocusRequester.tryRequestFocus()
// Hacky, but mostly works
delay(50)
listState.animateScrollToItem(position.row)
var focused by remember { mutableStateOf(false) }
LaunchedEffect(homeRows) {
if (!focused) {
homeRows
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
.takeIf { it >= 0 }
?.let {
positionFocusRequester.tryRequestFocus()
delay(50)
listState.animateScrollToItem(position.row)
focused = true
}
}
}
LaunchedEffect(position) {
listState.animateScrollToItem(position.row)
@ -333,11 +354,10 @@ fun HomePageContent(
.fillMaxWidth()
.animateItem(),
cardContent = { index, item, cardModifier, onClick, onLongClick ->
// TODO better aspect ration handling?
BannerCard(
name = item?.data?.seriesName ?: item?.name,
imageUrl = item?.imageUrl,
aspectRatio = (2f / 3f),
aspectRatio = AspectRatios.TALL,
cornerText =
item?.data?.indexNumber?.let { "E$it" }
?: item?.data?.childCount?.let { if (it > 0) it.toString() else null },

View file

@ -45,6 +45,7 @@ import coil3.compose.AsyncImagePreviewHandler
import coil3.compose.LocalAsyncImagePreviewHandler
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
@ -62,7 +63,7 @@ fun NextUpEpisode(
onClick: () -> Unit,
timeLeft: Duration?,
modifier: Modifier = Modifier,
aspectRatio: Float = 16f / 9,
aspectRatio: Float = AspectRatios.WIDE,
) {
val interactionSource = remember { MutableInteractionSource() }
val focusRequester = remember { FocusRequester() }
@ -202,7 +203,7 @@ private fun NextUpEpisodePreview() {
"But it just keeps going and going.",
imageUrl = "",
onClick = {},
aspectRatio = 4f / 3,
aspectRatio = AspectRatios.FOUR_THREE,
timeLeft = 30.seconds,
modifier =
Modifier

View file

@ -50,7 +50,9 @@ 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.aspectRatioFloat
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.TimeFormatter
import com.github.damontecres.wholphin.ui.cards.ChapterCard
import com.github.damontecres.wholphin.ui.cards.SeasonCard
@ -75,6 +77,7 @@ private val subtitleTextSize = 18.sp
*/
@Composable
fun PlaybackOverlay(
item: BaseItem?,
title: String?,
subtitleStreams: List<SubtitleStream>,
chapters: List<Chapter>,
@ -236,6 +239,7 @@ fun PlaybackOverlay(
name = chapter.name,
position = chapter.position,
imageUrl = chapter.imageUrl,
aspectRatio = item?.data?.aspectRatioFloat ?: AspectRatios.WIDE,
onClick = {
playerControls.seekTo(chapter.position.inWholeMilliseconds)
controllerViewState.hideControls()

View file

@ -69,6 +69,7 @@ import com.github.damontecres.wholphin.data.model.Playlist
import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.preferences.skipBackOnResume
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage
@ -332,6 +333,7 @@ fun PlaybackPage(
.padding(WindowInsets.systemBars.asPaddingValues())
.fillMaxSize()
.background(Color.Transparent),
item = currentPlayback?.item,
title = title,
subtitle = subtitle,
subtitleStreams = subtitleStreams,
@ -488,7 +490,9 @@ fun PlaybackPage(
).joinToString(" - "),
description = it.data.overview,
imageUrl = it.imageUrl,
aspectRatio = it.data.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
aspectRatio =
it.data.primaryImageAspectRatio?.toFloat()
?: AspectRatios.WIDE,
onClick = {
viewModel.reportInteraction()
viewModel.playNextUp()

View file

@ -75,7 +75,6 @@ import org.jellyfin.sdk.api.client.extensions.trickplayApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.client.extensions.videosApi
import org.jellyfin.sdk.api.sockets.subscribe
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.MediaSegmentDto
@ -142,7 +141,7 @@ class PlaybackViewModel
private lateinit var preferences: UserPreferences
private lateinit var deviceProfile: DeviceProfile
private lateinit var itemId: UUID
private lateinit var dto: BaseItemDto
private lateinit var item: BaseItem
private var activityListener: TrackActivityPlaybackListener? = null
val nextUp = MutableLiveData<BaseItem?>()
@ -204,10 +203,11 @@ class PlaybackViewModel
} else {
queriedItem
}
val item = BaseItem.from(base, api)
val played =
play(
base,
item,
destination.positionMs,
destination.itemPlayback,
destination.forceTranscoding,
@ -228,25 +228,25 @@ class PlaybackViewModel
}
private suspend fun play(
base: BaseItemDto,
item: BaseItem,
positionMs: Long,
itemPlayback: ItemPlayback? = null,
forceTranscoding: Boolean = false,
): Boolean =
withContext(Dispatchers.IO) {
Timber.i("Playing ${base.id}")
Timber.i("Playing ${item.id}")
autoSkippedSegments.clear()
if (base.type !in supportItemKinds) {
if (item.type !in supportItemKinds) {
showToast(
context,
"Unsupported type '${base.type}', skipping...",
"Unsupported type '${item.type}', skipping...",
Toast.LENGTH_SHORT,
)
return@withContext false
}
val isLiveTv = base.type == BaseItemKind.TV_CHANNEL
val isLiveTv = item.type == BaseItemKind.TV_CHANNEL
dto = base
val base = item.data
val title =
if (base.type == BaseItemKind.EPISODE) {
base.seriesName
@ -358,7 +358,7 @@ class PlaybackViewModel
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
changeStreams(
base,
item,
itemPlaybackToUse,
audioIndex,
subtitleIndex,
@ -379,7 +379,7 @@ class PlaybackViewModel
@OptIn(UnstableApi::class)
private suspend fun changeStreams(
item: BaseItemDto,
item: BaseItem,
currentItemPlayback: ItemPlayback = this@PlaybackViewModel.currentItemPlayback.value!!,
audioIndex: Int?,
subtitleIndex: Int?,
@ -392,7 +392,7 @@ class PlaybackViewModel
val playerBackend = preferences.appPreferences.playbackPreferences.playerBackend
val currentPlayback = this@PlaybackViewModel.currentPlayback.value
if (currentPlayback != null && currentPlayback.itemId == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY) {
if (currentPlayback != null && currentPlayback.item.id == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY) {
// If direct playing, can try to switch tracks without playback restarting
// Except for external subtitles
// TODO there's probably no reason why we can't add external subtitles?
@ -549,9 +549,9 @@ class PlaybackViewModel
val playback =
CurrentPlayback(
itemId = item.id,
backend = preferences.appPreferences.playbackPreferences.playerBackend,
item = item,
tracks = listOf(),
backend = preferences.appPreferences.playbackPreferences.playerBackend,
playMethod = transcodeType,
playSessionId = response.playSessionId,
liveStreamId = source.liveStreamId,
@ -623,7 +623,7 @@ class PlaybackViewModel
}
}
val trickPlayInfo =
item.trickplay
item.data.trickplay
?.get(source.id)
?.values
?.firstOrNull()
@ -637,7 +637,7 @@ class PlaybackViewModel
fun changeAudioStream(index: Int) {
viewModelScope.launchIO {
changeStreams(
dto,
item,
currentItemPlayback.value!!,
index,
currentItemPlayback.value?.subtitleIndex,
@ -650,7 +650,7 @@ class PlaybackViewModel
fun changeSubtitleStream(index: Int?): Job =
viewModelScope.launchIO {
changeStreams(
dto,
item,
currentItemPlayback.value!!,
currentItemPlayback.value?.audioIndex,
index,
@ -660,7 +660,7 @@ class PlaybackViewModel
}
fun getTrickplayUrl(index: Int): String? {
val itemId = dto.id
val itemId = item.id
val mediaSourceId = currentItemPlayback.value?.sourceId
val trickPlayInfo = trickplay.value ?: return null
return api.trickplayApi.getTrickplayTileImageUrl(
@ -792,7 +792,7 @@ class PlaybackViewModel
viewModelScope.launchIO {
cancelUpNextEpisode()
val item = it.getAndAdvance()
val played = play(item.data, 0)
val played = play(item, 0)
if (!played) {
playNextUp()
}
@ -807,7 +807,7 @@ class PlaybackViewModel
viewModelScope.launchIO {
cancelUpNextEpisode()
val item = it.getPreviousAndReverse()
val played = play(item.data, 0)
val played = play(item, 0)
if (!played) {
playPrevious()
}
@ -825,7 +825,7 @@ class PlaybackViewModel
viewModelScope.launchIO {
val toPlay = playlist.advanceTo(item.id)
if (toPlay != null) {
val played = play(toPlay.data, 0)
val played = play(toPlay, 0)
if (!played) {
playNextUp()
}
@ -859,7 +859,7 @@ class PlaybackViewModel
PlayMethod.DIRECT_STREAM, PlayMethod.DIRECT_PLAY -> {
Timber.w("Playback error during ${it.playMethod}, falling back to transcoding")
changeStreams(
dto,
item,
currentItemPlayback.value!!,
currentItemPlayback.value?.audioIndex,
currentItemPlayback.value?.subtitleIndex,
@ -978,8 +978,12 @@ class PlaybackViewModel
while (maxAttempts > 0 && subtitleCount == newCount) {
maxAttempts--
delay(1500)
dto = api.userLibraryApi.getItem(itemId = dto.id).content
val mediaSource = chooseSource(dto, it)
item =
BaseItem.from(
api.userLibraryApi.getItem(itemId = item.id).content,
api,
)
val mediaSource = chooseSource(item.data, it)
if (mediaSource == null) {
// This shouldn't happen, but just in case
showToast(
@ -1036,7 +1040,7 @@ class PlaybackViewModel
audioIndex += 1
}
changeStreams(
dto,
item,
currentItemPlayback.value!!,
audioIndex,
newStream.index,
@ -1066,9 +1070,9 @@ class PlaybackViewModel
}
data class CurrentPlayback(
val itemId: UUID,
val backend: PlayerBackend,
val item: BaseItem,
val tracks: List<TrackSupport>,
val backend: PlayerBackend,
val playMethod: PlayMethod,
val playSessionId: String?,
val liveStreamId: String?,

View file

@ -53,6 +53,7 @@ fun SwitchServerContent(
var showAddServer by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.init()
viewModel.discoverServers()
}

View file

@ -52,10 +52,6 @@ class SwitchServerViewModel
addServerState.value = LoadingState.Pending
}
init {
init()
}
fun init() {
viewModelScope.launchIO {
withContext(Dispatchers.Main) {

View file

@ -58,6 +58,9 @@ fun SwitchUserContent(
),
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
viewModel.init()
}
// val currentServer by viewModel.serverRepository.currentServer.observeAsState()
val currentUser by viewModel.serverRepository.currentUser.observeAsState()

View file

@ -48,12 +48,6 @@ class SwitchUserViewModel
fun create(server: JellyfinServer): SwitchUserViewModel
}
init {
viewModelScope.launch(Dispatchers.Main + ExceptionHandler()) {
serverRepository.switchServerOrUser()
}
}
val serverQuickConnect = MutableLiveData<Boolean>(false)
val users = MutableLiveData<List<JellyfinUser>>(listOf())
@ -78,6 +72,9 @@ class SwitchUserViewModel
}
fun init() {
viewModelScope.launch(Dispatchers.Main + ExceptionHandler()) {
serverRepository.switchServerOrUser()
}
quickConnectJob?.cancel()
viewModelScope.launchIO {
users.setValueOnMain(listOf())