Playlist UI updates & more handling of unsupported playback

This commit is contained in:
Damontecres 2025-10-13 16:23:50 -04:00
parent aab7e2eb17
commit 9148ca0719
No known key found for this signature in database
3 changed files with 337 additions and 201 deletions

View file

@ -236,6 +236,24 @@ fun DialogPopup(
} }
} }
@Composable
fun DialogPopup(
params: DialogParams,
onDismissRequest: () -> Unit,
dismissOnClick: Boolean = true,
properties: DialogProperties = DialogProperties(),
elevation: Dp = 3.dp,
) = DialogPopup(
showDialog = true,
waitToLoad = params.fromLongClick,
title = params.title,
dialogItems = params.items,
onDismissRequest = onDismissRequest,
dismissOnClick = dismissOnClick,
properties = properties,
elevation = elevation,
)
/** /**
* A dialog that can be scrolled, typically for longer text content * A dialog that can be scrolled, typically for longer text content
*/ */

View file

@ -3,6 +3,7 @@ package com.github.damontecres.dolphin.ui.detail
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -17,11 +18,15 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -36,6 +41,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -49,9 +55,13 @@ import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.data.model.Library import com.github.damontecres.dolphin.data.model.Library
import com.github.damontecres.dolphin.ui.DefaultItemFields import com.github.damontecres.dolphin.ui.DefaultItemFields
import com.github.damontecres.dolphin.ui.cards.ItemCardImage import com.github.damontecres.dolphin.ui.cards.ItemCardImage
import com.github.damontecres.dolphin.ui.components.DialogItem
import com.github.damontecres.dolphin.ui.components.DialogParams
import com.github.damontecres.dolphin.ui.components.DialogPopup
import com.github.damontecres.dolphin.ui.components.ErrorMessage import com.github.damontecres.dolphin.ui.components.ErrorMessage
import com.github.damontecres.dolphin.ui.components.LoadingPage import com.github.damontecres.dolphin.ui.components.LoadingPage
import com.github.damontecres.dolphin.ui.components.OverviewText import com.github.damontecres.dolphin.ui.components.OverviewText
import com.github.damontecres.dolphin.ui.enableMarquee
import com.github.damontecres.dolphin.ui.ifElse import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
@ -116,6 +126,8 @@ fun PlaylistDetails(
val playlist by viewModel.item.observeAsState(null) val playlist by viewModel.item.observeAsState(null)
val items by viewModel.items.observeAsState(listOf()) val items by viewModel.items.observeAsState(listOf())
var longClickDialog by remember { mutableStateOf<DialogParams?>(null) }
when (val st = loading) { when (val st = loading) {
is LoadingState.Error -> ErrorMessage(st, modifier) is LoadingState.Error -> ErrorMessage(st, modifier)
LoadingState.Pending, LoadingState.Loading -> LoadingPage(modifier) LoadingState.Pending, LoadingState.Loading -> LoadingPage(modifier)
@ -126,7 +138,8 @@ fun PlaylistDetails(
PlaylistDetailsContent( PlaylistDetailsContent(
playlist = it, playlist = it,
items = items, items = items,
onClickIndex = { index -> focusRequester = focusRequester,
onClickIndex = { index, _ ->
viewModel.navigationManager.navigateTo( viewModel.navigationManager.navigateTo(
Destination.Playback( Destination.Playback(
itemId = it.id, itemId = it.id,
@ -135,23 +148,64 @@ fun PlaylistDetails(
), ),
) )
}, },
modifier = modifier.focusRequester(focusRequester), onLongClickIndex = { index, item ->
longClickDialog =
DialogParams(
fromLongClick = true,
title = item.name ?: "",
items =
listOf(
DialogItem(
"Go to",
Icons.Default.ArrowForward,
) {
viewModel.navigationManager.navigateTo(
Destination.MediaItem(
itemId = item.id,
type = item.type,
item = item,
),
)
},
DialogItem(
"Play from here",
Icons.Default.PlayArrow,
) {
viewModel.navigationManager.navigateTo(
Destination.Playback(
itemId = it.id,
positionMs = 0L,
startIndex = index,
),
)
},
),
)
},
modifier = modifier,
) )
} }
} }
longClickDialog?.let { params ->
DialogPopup(
params = params,
onDismissRequest = { longClickDialog = null },
)
}
} }
@Composable @Composable
fun PlaylistDetailsContent( fun PlaylistDetailsContent(
playlist: BaseItem, playlist: BaseItem,
items: List<BaseItem?>, items: List<BaseItem?>,
onClickIndex: (Int) -> Unit, onClickIndex: (Int, BaseItem) -> Unit,
onLongClickIndex: (Int, BaseItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
focusRequester: FocusRequester = remember { FocusRequester() },
) { ) {
var savedIndex by rememberSaveable { mutableIntStateOf(0) } var savedIndex by rememberSaveable { mutableIntStateOf(0) }
var focusedIndex by remember { mutableIntStateOf(savedIndex) } var focusedIndex by remember { mutableIntStateOf(savedIndex) }
val focusRequester = remember { FocusRequester() } val focus = remember { FocusRequester() }
val focusedItem = items.getOrNull(focusedIndex) val focusedItem = items.getOrNull(focusedIndex)
Box( Box(
@ -190,95 +244,81 @@ fun PlaylistDetailsContent(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier =
Modifier Modifier
.padding(start = 16.dp, top = 16.dp) .padding(top = 16.dp)
.fillMaxSize(), .fillMaxSize(),
) { ) {
Text( Row(
text = playlist.name ?: "Playlist", horizontalArrangement = Arrangement.spacedBy(32.dp),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.displayMedium,
)
PlaylistDetailsHeader(
focusedItem = focusedItem,
modifier = modifier =
Modifier Modifier
.padding(start = 16.dp) .padding(horizontal = 16.dp)
.fillMaxWidth(.66f), .fillMaxWidth(),
)
LazyColumn(
contentPadding = PaddingValues(8.dp),
modifier =
Modifier
.fillMaxWidth(.8f)
.align(Alignment.CenterHorizontally)
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
shape = RoundedCornerShape(16.dp),
).focusGroup()
.focusRestorer(focusRequester),
) { ) {
itemsIndexed(items) { index, item -> PlaylistDetailsHeader(
val interactionSource = remember { MutableInteractionSource() } focusedItem = focusedItem,
ListItem( modifier =
selected = false, Modifier
onClick = { .padding(start = 16.dp)
savedIndex = index .fillMaxWidth(.25f),
onClickIndex.invoke(index) )
}, Column(
interactionSource = interactionSource, modifier =
headlineContent = { Modifier
Text( .fillMaxWidth()
text = item?.title ?: "", .padding(horizontal = 16.dp),
style = MaterialTheme.typography.titleLarge, ) {
) Text(
}, text = playlist.name ?: "Playlist",
supportingContent = { color = MaterialTheme.colorScheme.onSurface,
Text( style = MaterialTheme.typography.displayMedium,
text = item?.subtitle ?: "", textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleSmall, modifier = Modifier.fillMaxWidth(),
) )
}, LazyColumn(
trailingContent = { contentPadding = PaddingValues(8.dp),
item?.data?.runTimeTicks?.ticks?.roundMinutes?.let {
Text(
text = it.toString(),
)
}
},
leadingContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = "${index + 1}.",
style = MaterialTheme.typography.labelLarge,
)
ItemCardImage(
imageUrl = item?.imageUrl,
name = item?.name,
showOverlay = true,
favorite = item?.data?.userData?.isFavorite ?: false,
watched = item?.data?.userData?.played ?: false,
unwatchedCount = item?.data?.userData?.unplayedItemCount ?: -1,
watchedPercent = 0.0,
modifier = Modifier.width(160.dp),
useFallbackText = false,
)
}
},
modifier = modifier =
Modifier Modifier
.height(80.dp) .padding(bottom = 32.dp)
.ifElse( .fillMaxHeight()
index == savedIndex, // .fillMaxWidth(.8f)
Modifier.focusRequester(focusRequester), .weight(1f)
).onFocusChanged { .background(
if (it.isFocused) { MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
focusedIndex = index shape = RoundedCornerShape(16.dp),
).focusRequester(focusRequester)
.focusGroup()
.focusRestorer(focus),
) {
itemsIndexed(items) { index, item ->
PlaylistItem(
item = item,
index = index,
onClick = {
savedIndex = index
item?.let {
onClickIndex.invoke(index, item)
} }
}, },
) onLongClick = {
savedIndex = index
item?.let {
onLongClickIndex.invoke(index, item)
}
},
modifier =
Modifier
.height(80.dp)
.ifElse(
index == savedIndex,
Modifier.focusRequester(focus),
).onFocusChanged {
if (it.isFocused) {
focusedIndex = index
}
},
)
}
}
} }
} }
} }
@ -306,9 +346,71 @@ fun PlaylistDetailsHeader(
) )
OverviewText( OverviewText(
overview = focusedItem?.data?.overview ?: "", overview = focusedItem?.data?.overview ?: "",
maxLines = 2, maxLines = 10,
onClick = {}, onClick = {},
enabled = false, enabled = false,
) )
} }
} }
@Composable
fun PlaylistItem(
item: BaseItem?,
index: Int,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val focused by interactionSource.collectIsFocusedAsState()
ListItem(
selected = false,
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
headlineContent = {
Text(
text = item?.title ?: "",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.enableMarquee(focused),
)
},
supportingContent = {
Text(
text = item?.subtitle ?: "",
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.enableMarquee(focused),
)
},
trailingContent = {
item?.data?.runTimeTicks?.ticks?.roundMinutes?.let {
Text(
text = it.toString(),
)
}
},
leadingContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = "${index + 1}.",
style = MaterialTheme.typography.labelLarge,
)
ItemCardImage(
imageUrl = item?.imageUrl,
name = item?.name,
showOverlay = true,
favorite = item?.data?.userData?.isFavorite ?: false,
watched = item?.data?.userData?.played ?: false,
unwatchedCount = item?.data?.userData?.unplayedItemCount ?: -1,
watchedPercent = 0.0,
modifier = Modifier.width(160.dp),
useFallbackText = false,
)
}
},
modifier = modifier,
)
}

View file

@ -23,6 +23,7 @@ import com.github.damontecres.dolphin.preferences.SkipSegmentBehavior
import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.showToast
import com.github.damontecres.dolphin.util.EqualityMutableLiveData import com.github.damontecres.dolphin.util.EqualityMutableLiveData
import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.TrackActivityPlaybackListener import com.github.damontecres.dolphin.util.TrackActivityPlaybackListener
@ -31,6 +32,7 @@ import com.github.damontecres.dolphin.util.checkForSupport
import com.github.damontecres.dolphin.util.formatDateTime import com.github.damontecres.dolphin.util.formatDateTime
import com.github.damontecres.dolphin.util.seasonEpisodePadded import com.github.damontecres.dolphin.util.seasonEpisodePadded
import com.github.damontecres.dolphin.util.subtitleMimeTypes import com.github.damontecres.dolphin.util.subtitleMimeTypes
import com.github.damontecres.dolphin.util.supportItemKinds
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -81,7 +83,7 @@ data class StreamDecision(
class PlaybackViewModel class PlaybackViewModel
@Inject @Inject
constructor( constructor(
@ApplicationContext context: Context, @param:ApplicationContext val context: Context,
val api: ApiClient, val api: ApiClient,
val playlistCreator: PlaylistCreator, val playlistCreator: PlaylistCreator,
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
@ -168,133 +170,139 @@ class PlaybackViewModel
private suspend fun play( private suspend fun play(
base: BaseItemDto, base: BaseItemDto,
positionMs: Long, positionMs: Long,
) = withContext(Dispatchers.IO) { ): Boolean =
Timber.i("Playing ${base.id}") withContext(Dispatchers.IO) {
dto = base Timber.i("Playing ${base.id}")
val title = if (base.type !in supportItemKinds) {
if (base.type == BaseItemKind.EPISODE) { showToast(context, "Unsupported type '${base.type}', skipping...")
base.seriesName return@withContext false
} else {
base.name
} }
val subtitle = dto = base
if (base.type == BaseItemKind.EPISODE) { val title =
buildList { if (base.type == BaseItemKind.EPISODE) {
add(base.seasonEpisodePadded) base.seriesName
add(base.name) } else {
add(base.premiereDate?.let { formatDateTime(it) }) base.name
}.filterNotNull().joinToString(" - ") }
} else { val subtitle =
base.productionYear?.toString() if (base.type == BaseItemKind.EPISODE) {
buildList {
add(base.seasonEpisodePadded)
add(base.name)
add(base.premiereDate?.let { formatDateTime(it) })
}.filterNotNull().joinToString(" - ")
} else {
base.productionYear?.toString()
}
withContext(Dispatchers.Main) {
this@PlaybackViewModel.title.value = title
this@PlaybackViewModel.subtitle.value = subtitle
} }
withContext(Dispatchers.Main) {
this@PlaybackViewModel.title.value = title
this@PlaybackViewModel.subtitle.value = subtitle
}
base.mediaStreams
?.filter { it.type == MediaStreamType.VIDEO }
?.forEach { Timber.v("${it.videoRangeType}, ${it.videoRange}") }
val subtitleStreams =
base.mediaStreams base.mediaStreams
?.filter { it.type == MediaStreamType.SUBTITLE } ?.filter { it.type == MediaStreamType.VIDEO }
?.map { ?.forEach { Timber.v("${it.videoRangeType}, ${it.videoRange}") }
SubtitleStream( val subtitleStreams =
it.index, base.mediaStreams
it.language, ?.filter { it.type == MediaStreamType.SUBTITLE }
it.title, ?.map {
it.codec, SubtitleStream(
it.codecTag, it.index,
it.isExternal, it.language,
it.isForced, it.title,
it.isDefault, it.codec,
) it.codecTag,
}.orEmpty() it.isExternal,
val audioStreams = it.isForced,
base.mediaStreams it.isDefault,
?.filter { it.type == MediaStreamType.AUDIO } )
?.map { }.orEmpty()
AudioStream( val audioStreams =
it.index, base.mediaStreams
it.language, ?.filter { it.type == MediaStreamType.AUDIO }
it.title, ?.map {
it.codec, AudioStream(
it.codecTag, it.index,
it.channels, it.language,
it.channelLayout, it.title,
) it.codec,
}?.sortedWith(compareBy<AudioStream> { it.language }.thenByDescending { it.channels }) it.codecTag,
.orEmpty() it.channels,
it.channelLayout,
)
}?.sortedWith(compareBy<AudioStream> { it.language }.thenByDescending { it.channels })
.orEmpty()
// TODO audio selection based on channel layout or preferences or default // TODO audio selection based on channel layout or preferences or default
val audioLanguage = preferences.userConfig.audioLanguagePreference val audioLanguage = preferences.userConfig.audioLanguagePreference
val audioIndex = val audioIndex =
if (audioLanguage != null) { if (audioLanguage != null) {
audioStreams.firstOrNull { it.language == audioLanguage }?.index audioStreams.firstOrNull { it.language == audioLanguage }?.index
?: audioStreams.firstOrNull()?.index ?: audioStreams.firstOrNull()?.index
} else { } else {
audioStreams.firstOrNull()?.index 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
}
} }
val subtitleMode = preferences.userConfig.subtitleMode
SubtitlePlaybackMode.ONLY_FORCED -> val subtitleLanguage = preferences.userConfig.subtitleLanguagePreference
if (subtitleLanguage != null) { val subtitleIndex =
subtitleStreams.firstOrNull { it.language == subtitleLanguage && it.forced }?.index when (subtitleMode) {
} else { SubtitlePlaybackMode.ALWAYS -> {
subtitleStreams.firstOrNull { it.forced }?.index if (subtitleLanguage != null) {
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index
} else {
subtitleStreams.firstOrNull()?.index
}
} }
SubtitlePlaybackMode.SMART -> { SubtitlePlaybackMode.ONLY_FORCED ->
if (audioLanguage != null && subtitleLanguage != null && audioLanguage != subtitleLanguage) { if (subtitleLanguage != null) {
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index subtitleStreams.firstOrNull { it.language == subtitleLanguage && it.forced }?.index
} else { } else {
null subtitleStreams.firstOrNull { it.forced }?.index
}
SubtitlePlaybackMode.SMART -> {
if (audioLanguage != null && subtitleLanguage != null && audioLanguage != subtitleLanguage) {
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index
} else {
null
}
} }
}
SubtitlePlaybackMode.DEFAULT -> { SubtitlePlaybackMode.DEFAULT -> {
// TODO check for language? // TODO check for language?
( (
subtitleStreams.firstOrNull { it.default && it.forced } subtitleStreams.firstOrNull { it.default && it.forced }
?: subtitleStreams.firstOrNull { it.default } ?: subtitleStreams.firstOrNull { it.default }
?: subtitleStreams.firstOrNull { it.forced } ?: subtitleStreams.firstOrNull { it.forced }
)?.index )?.index
} }
SubtitlePlaybackMode.NONE -> null SubtitlePlaybackMode.NONE -> null
} }
// Timber.v("base.mediaStreams=${base.mediaStreams}") // Timber.v("base.mediaStreams=${base.mediaStreams}")
// Timber.v("subtitleTracks=$subtitleStreams") // Timber.v("subtitleTracks=$subtitleStreams")
// Timber.v("audioStreams=$audioStreams") // Timber.v("audioStreams=$audioStreams")
Timber.d("Selected audioIndex=$audioIndex, subtitleIndex=$subtitleIndex") Timber.d("Selected audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@PlaybackViewModel.audioStreams.value = audioStreams this@PlaybackViewModel.audioStreams.value = audioStreams
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
changeStreams( changeStreams(
base, base,
audioIndex, audioIndex,
subtitleIndex, subtitleIndex,
if (positionMs > 0) positionMs else C.TIME_UNSET, if (positionMs > 0) positionMs else C.TIME_UNSET,
) )
player.prepare() player.prepare()
this@PlaybackViewModel.chapters.value = Chapter.fromDto(base, api) this@PlaybackViewModel.chapters.value = Chapter.fromDto(base, api)
Timber.v("chapters=${this@PlaybackViewModel.chapters.value?.size}") Timber.v("chapters=${this@PlaybackViewModel.chapters.value?.size}")
}
listenForSegments()
return@withContext true
} }
listenForSegments()
}
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private suspend fun changeStreams( private suspend fun changeStreams(
@ -582,7 +590,11 @@ class PlaybackViewModel
if (it.hasNext()) { if (it.hasNext()) {
viewModelScope.launch(ExceptionHandler()) { viewModelScope.launch(ExceptionHandler()) {
cancelUpNextEpisode() cancelUpNextEpisode()
play(it.getAndAdvance().data, 0) val item = it.getAndAdvance()
val played = play(item.data, 0)
if (!played) {
playUpNextUp()
}
} }
} }
} }
@ -593,7 +605,11 @@ class PlaybackViewModel
if (it.hasPrevious()) { if (it.hasPrevious()) {
viewModelScope.launch(ExceptionHandler()) { viewModelScope.launch(ExceptionHandler()) {
cancelUpNextEpisode() cancelUpNextEpisode()
play(it.getPreviousAndReverse().data, 0) val item = it.getPreviousAndReverse()
val played = play(item.data, 0)
if (!played) {
playPrevious()
}
} }
} }
} }
@ -602,6 +618,9 @@ class PlaybackViewModel
fun cancelUpNextEpisode() { fun cancelUpNextEpisode() {
nextUp.value = null nextUp.value = null
} }
private fun toastUnsupported(item: BaseItemDto) {
}
} }
data class CurrentPlayback( data class CurrentPlayback(
@ -687,6 +706,3 @@ private fun applyTrackSelections(
} }
} }
} }
private val singlePlays =
setOf(BaseItemKind.EPISODE, BaseItemKind.MOVIE, BaseItemKind.VIDEO, BaseItemKind.MUSIC_VIDEO)