mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Playlist UI updates & more handling of unsupported playback
This commit is contained in:
parent
aab7e2eb17
commit
9148ca0719
3 changed files with 337 additions and 201 deletions
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.dolphin.ui.detail
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.itemsIndexed
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
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.ui.DefaultItemFields
|
||||
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.LoadingPage
|
||||
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.isNotNullOrBlank
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
|
|
@ -116,6 +126,8 @@ fun PlaylistDetails(
|
|||
val playlist by viewModel.item.observeAsState(null)
|
||||
val items by viewModel.items.observeAsState(listOf())
|
||||
|
||||
var longClickDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
|
||||
when (val st = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(st, modifier)
|
||||
LoadingState.Pending, LoadingState.Loading -> LoadingPage(modifier)
|
||||
|
|
@ -126,7 +138,8 @@ fun PlaylistDetails(
|
|||
PlaylistDetailsContent(
|
||||
playlist = it,
|
||||
items = items,
|
||||
onClickIndex = { index ->
|
||||
focusRequester = focusRequester,
|
||||
onClickIndex = { index, _ ->
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.Playback(
|
||||
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
|
||||
fun PlaylistDetailsContent(
|
||||
playlist: BaseItem,
|
||||
items: List<BaseItem?>,
|
||||
onClickIndex: (Int) -> Unit,
|
||||
onClickIndex: (Int, BaseItem) -> Unit,
|
||||
onLongClickIndex: (Int, BaseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
) {
|
||||
var savedIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||
var focusedIndex by remember { mutableIntStateOf(savedIndex) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
val focus = remember { FocusRequester() }
|
||||
val focusedItem = items.getOrNull(focusedIndex)
|
||||
|
||||
Box(
|
||||
|
|
@ -190,95 +244,81 @@ fun PlaylistDetailsContent(
|
|||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp, top = 16.dp)
|
||||
.padding(top = 16.dp)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Text(
|
||||
text = playlist.name ?: "Playlist",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
)
|
||||
PlaylistDetailsHeader(
|
||||
focusedItem = focusedItem,
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
.fillMaxWidth(.66f),
|
||||
)
|
||||
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),
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
itemsIndexed(items) { index, item ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
ListItem(
|
||||
selected = false,
|
||||
onClick = {
|
||||
savedIndex = index
|
||||
onClickIndex.invoke(index)
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = item?.title ?: "",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = item?.subtitle ?: "",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
},
|
||||
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,
|
||||
)
|
||||
}
|
||||
},
|
||||
PlaylistDetailsHeader(
|
||||
focusedItem = focusedItem,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
.fillMaxWidth(.25f),
|
||||
)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = playlist.name ?: "Playlist",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.height(80.dp)
|
||||
.ifElse(
|
||||
index == savedIndex,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
focusedIndex = index
|
||||
.padding(bottom = 32.dp)
|
||||
.fillMaxHeight()
|
||||
// .fillMaxWidth(.8f)
|
||||
.weight(1f)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||
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(
|
||||
overview = focusedItem?.data?.overview ?: "",
|
||||
maxLines = 2,
|
||||
maxLines = 10,
|
||||
onClick = {},
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.github.damontecres.dolphin.preferences.SkipSegmentBehavior
|
|||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
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.ExceptionHandler
|
||||
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.seasonEpisodePadded
|
||||
import com.github.damontecres.dolphin.util.subtitleMimeTypes
|
||||
import com.github.damontecres.dolphin.util.supportItemKinds
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -81,7 +83,7 @@ data class StreamDecision(
|
|||
class PlaybackViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context,
|
||||
@param:ApplicationContext val context: Context,
|
||||
val api: ApiClient,
|
||||
val playlistCreator: PlaylistCreator,
|
||||
val navigationManager: NavigationManager,
|
||||
|
|
@ -168,133 +170,139 @@ class PlaybackViewModel
|
|||
private suspend fun play(
|
||||
base: BaseItemDto,
|
||||
positionMs: Long,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
Timber.i("Playing ${base.id}")
|
||||
dto = base
|
||||
val title =
|
||||
if (base.type == BaseItemKind.EPISODE) {
|
||||
base.seriesName
|
||||
} else {
|
||||
base.name
|
||||
): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.i("Playing ${base.id}")
|
||||
if (base.type !in supportItemKinds) {
|
||||
showToast(context, "Unsupported type '${base.type}', skipping...")
|
||||
return@withContext false
|
||||
}
|
||||
val subtitle =
|
||||
if (base.type == BaseItemKind.EPISODE) {
|
||||
buildList {
|
||||
add(base.seasonEpisodePadded)
|
||||
add(base.name)
|
||||
add(base.premiereDate?.let { formatDateTime(it) })
|
||||
}.filterNotNull().joinToString(" - ")
|
||||
} else {
|
||||
base.productionYear?.toString()
|
||||
dto = base
|
||||
val title =
|
||||
if (base.type == BaseItemKind.EPISODE) {
|
||||
base.seriesName
|
||||
} else {
|
||||
base.name
|
||||
}
|
||||
val subtitle =
|
||||
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
|
||||
?.filter { it.type == MediaStreamType.SUBTITLE }
|
||||
?.map {
|
||||
SubtitleStream(
|
||||
it.index,
|
||||
it.language,
|
||||
it.title,
|
||||
it.codec,
|
||||
it.codecTag,
|
||||
it.isExternal,
|
||||
it.isForced,
|
||||
it.isDefault,
|
||||
)
|
||||
}.orEmpty()
|
||||
val audioStreams =
|
||||
base.mediaStreams
|
||||
?.filter { it.type == MediaStreamType.AUDIO }
|
||||
?.map {
|
||||
AudioStream(
|
||||
it.index,
|
||||
it.language,
|
||||
it.title,
|
||||
it.codec,
|
||||
it.codecTag,
|
||||
it.channels,
|
||||
it.channelLayout,
|
||||
)
|
||||
}?.sortedWith(compareBy<AudioStream> { it.language }.thenByDescending { it.channels })
|
||||
.orEmpty()
|
||||
?.filter { it.type == MediaStreamType.VIDEO }
|
||||
?.forEach { Timber.v("${it.videoRangeType}, ${it.videoRange}") }
|
||||
val subtitleStreams =
|
||||
base.mediaStreams
|
||||
?.filter { it.type == MediaStreamType.SUBTITLE }
|
||||
?.map {
|
||||
SubtitleStream(
|
||||
it.index,
|
||||
it.language,
|
||||
it.title,
|
||||
it.codec,
|
||||
it.codecTag,
|
||||
it.isExternal,
|
||||
it.isForced,
|
||||
it.isDefault,
|
||||
)
|
||||
}.orEmpty()
|
||||
val audioStreams =
|
||||
base.mediaStreams
|
||||
?.filter { it.type == MediaStreamType.AUDIO }
|
||||
?.map {
|
||||
AudioStream(
|
||||
it.index,
|
||||
it.language,
|
||||
it.title,
|
||||
it.codec,
|
||||
it.codecTag,
|
||||
it.channels,
|
||||
it.channelLayout,
|
||||
)
|
||||
}?.sortedWith(compareBy<AudioStream> { it.language }.thenByDescending { it.channels })
|
||||
.orEmpty()
|
||||
|
||||
// TODO audio selection based on channel layout or preferences or default
|
||||
val audioLanguage = preferences.userConfig.audioLanguagePreference
|
||||
val audioIndex =
|
||||
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
|
||||
}
|
||||
// TODO audio selection based on channel layout or preferences or default
|
||||
val audioLanguage = preferences.userConfig.audioLanguagePreference
|
||||
val audioIndex =
|
||||
if (audioLanguage != null) {
|
||||
audioStreams.firstOrNull { it.language == audioLanguage }?.index
|
||||
?: audioStreams.firstOrNull()?.index
|
||||
} else {
|
||||
audioStreams.firstOrNull()?.index
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.ONLY_FORCED ->
|
||||
if (subtitleLanguage != null) {
|
||||
subtitleStreams.firstOrNull { it.language == subtitleLanguage && it.forced }?.index
|
||||
} else {
|
||||
subtitleStreams.firstOrNull { it.forced }?.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.SMART -> {
|
||||
if (audioLanguage != null && subtitleLanguage != null && audioLanguage != subtitleLanguage) {
|
||||
subtitleStreams.firstOrNull { it.language == subtitleLanguage }?.index
|
||||
} else {
|
||||
null
|
||||
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.DEFAULT -> {
|
||||
// TODO check for language?
|
||||
(
|
||||
subtitleStreams.firstOrNull { it.default && it.forced }
|
||||
?: subtitleStreams.firstOrNull { it.default }
|
||||
?: subtitleStreams.firstOrNull { it.forced }
|
||||
)?.index
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.NONE -> null
|
||||
}
|
||||
SubtitlePlaybackMode.NONE -> null
|
||||
}
|
||||
|
||||
// Timber.v("base.mediaStreams=${base.mediaStreams}")
|
||||
// Timber.v("subtitleTracks=$subtitleStreams")
|
||||
// Timber.v("audioStreams=$audioStreams")
|
||||
Timber.d("Selected audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
||||
Timber.d("Selected audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
this@PlaybackViewModel.audioStreams.value = audioStreams
|
||||
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
|
||||
withContext(Dispatchers.Main) {
|
||||
this@PlaybackViewModel.audioStreams.value = audioStreams
|
||||
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
|
||||
|
||||
changeStreams(
|
||||
base,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
if (positionMs > 0) positionMs else C.TIME_UNSET,
|
||||
)
|
||||
player.prepare()
|
||||
changeStreams(
|
||||
base,
|
||||
audioIndex,
|
||||
subtitleIndex,
|
||||
if (positionMs > 0) positionMs else C.TIME_UNSET,
|
||||
)
|
||||
player.prepare()
|
||||
|
||||
this@PlaybackViewModel.chapters.value = Chapter.fromDto(base, api)
|
||||
Timber.v("chapters=${this@PlaybackViewModel.chapters.value?.size}")
|
||||
this@PlaybackViewModel.chapters.value = Chapter.fromDto(base, api)
|
||||
Timber.v("chapters=${this@PlaybackViewModel.chapters.value?.size}")
|
||||
}
|
||||
listenForSegments()
|
||||
return@withContext true
|
||||
}
|
||||
listenForSegments()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private suspend fun changeStreams(
|
||||
|
|
@ -582,7 +590,11 @@ class PlaybackViewModel
|
|||
if (it.hasNext()) {
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
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()) {
|
||||
viewModelScope.launch(ExceptionHandler()) {
|
||||
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() {
|
||||
nextUp.value = null
|
||||
}
|
||||
|
||||
private fun toastUnsupported(item: BaseItemDto) {
|
||||
}
|
||||
}
|
||||
|
||||
data class CurrentPlayback(
|
||||
|
|
@ -687,6 +706,3 @@ private fun applyTrackSelections(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val singlePlays =
|
||||
setOf(BaseItemKind.EPISODE, BaseItemKind.MOVIE, BaseItemKind.VIDEO, BaseItemKind.MUSIC_VIDEO)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue