mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge branch 'main' into develop/jellyseerr
This commit is contained in:
commit
8ff530a4f9
19 changed files with 415 additions and 67 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.data
|
package com.github.damontecres.wholphin.data
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
|
@ -11,22 +12,25 @@ import java.util.UUID
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ItemPlaybackDao {
|
interface ItemPlaybackDao {
|
||||||
fun getItem(
|
suspend fun getItem(
|
||||||
user: JellyfinUser,
|
user: JellyfinUser,
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
): ItemPlayback? = getItem(user.rowId, itemId)
|
): ItemPlayback? = getItem(user.rowId, itemId)
|
||||||
|
|
||||||
@Query("SELECT * from ItemPlayback WHERE userId=:userId AND itemId=:itemId")
|
@Query("SELECT * from ItemPlayback WHERE userId=:userId AND itemId=:itemId")
|
||||||
fun getItem(
|
suspend fun getItem(
|
||||||
userId: Int,
|
userId: Int,
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
): ItemPlayback?
|
): ItemPlayback?
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun saveItem(item: ItemPlayback): Long
|
suspend fun saveItem(item: ItemPlayback): Long
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteItem(item: ItemPlayback)
|
||||||
|
|
||||||
@Query("SELECT * from ItemPlayback WHERE userId=:userId")
|
@Query("SELECT * from ItemPlayback WHERE userId=:userId")
|
||||||
fun getItems(userId: Int): List<ItemPlayback>
|
suspend fun getItems(userId: Int): List<ItemPlayback>
|
||||||
|
|
||||||
@Query("SELECT * FROM ItemTrackModification WHERE userId=:userId AND itemId=:itemId")
|
@Query("SELECT * FROM ItemTrackModification WHERE userId=:userId AND itemId=:itemId")
|
||||||
suspend fun getTrackModifications(
|
suspend fun getTrackModifications(
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ class ItemPlaybackRepository
|
||||||
constructor(
|
constructor(
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val itemPlaybackDao: ItemPlaybackDao,
|
val itemPlaybackDao: ItemPlaybackDao,
|
||||||
|
private val playbackLanguageChoiceDao: PlaybackLanguageChoiceDao,
|
||||||
private val streamChoiceService: StreamChoiceService,
|
private val streamChoiceService: StreamChoiceService,
|
||||||
) {
|
) {
|
||||||
suspend fun getSelectedTracks(
|
suspend fun getSelectedTracks(
|
||||||
|
|
@ -204,6 +205,18 @@ class ItemPlaybackRepository
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun deleteChosenStreams(chosenStreams: ChosenStreams?) {
|
||||||
|
Timber.d("deleteChosenStreams: %s", chosenStreams)
|
||||||
|
chosenStreams?.plc?.let {
|
||||||
|
Timber.d("Deleting %s", it)
|
||||||
|
playbackLanguageChoiceDao.delete(it)
|
||||||
|
}
|
||||||
|
chosenStreams?.itemPlayback?.let {
|
||||||
|
Timber.d("Deleting %s", it)
|
||||||
|
itemPlaybackDao.deleteItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ChosenStreams(
|
data class ChosenStreams(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.data
|
package com.github.damontecres.wholphin.data
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
|
@ -17,4 +18,7 @@ interface PlaybackLanguageChoiceDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun save(plc: PlaybackLanguageChoice): Long
|
suspend fun save(plc: PlaybackLanguageChoice): Long
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(plc: PlaybackLanguageChoice)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,15 +199,17 @@ class StreamChoiceService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val candidates =
|
val candidates =
|
||||||
candidates.sortedWith(
|
candidates
|
||||||
compareBy<MediaStream>(
|
.sortedWith(
|
||||||
{ it.isDefault },
|
compareByDescending<MediaStream> { it.isExternal }
|
||||||
{ !it.isForced && it.language.equals(subtitleLanguage, true) },
|
.thenByDescending { it.isDefault }
|
||||||
{ it.isForced && it.language.equals(subtitleLanguage, true) },
|
.thenByDescending {
|
||||||
{ it.isForced && it.language.isUnknown },
|
!it.isForced && it.language.equals(subtitleLanguage, true)
|
||||||
{ it.isForced },
|
}.thenByDescending {
|
||||||
),
|
it.isForced && it.language.equals(subtitleLanguage, true)
|
||||||
)
|
}.thenByDescending { it.isForced && it.language.isUnknown }
|
||||||
|
.thenByDescending { it.isForced },
|
||||||
|
)
|
||||||
return when (subtitleMode) {
|
return when (subtitleMode) {
|
||||||
SubtitlePlaybackMode.ALWAYS -> {
|
SubtitlePlaybackMode.ALWAYS -> {
|
||||||
if (subtitleLanguage.isNotNullOrBlank()) {
|
if (subtitleLanguage.isNotNullOrBlank()) {
|
||||||
|
|
@ -232,7 +234,7 @@ class StreamChoiceService
|
||||||
SubtitlePlaybackMode.SMART -> {
|
SubtitlePlaybackMode.SMART -> {
|
||||||
if (subtitleLanguage.isNotNullOrBlank()) {
|
if (subtitleLanguage.isNotNullOrBlank()) {
|
||||||
val audioLanguage = userConfig?.audioLanguagePreference
|
val audioLanguage = userConfig?.audioLanguagePreference
|
||||||
if (audioLanguage.isNotNullOrBlank() && audioLanguage != audioStreamLang) {
|
if (audioLanguage.isNullOrBlank() || audioLanguage != audioStreamLang) {
|
||||||
candidates.firstOrNull { it.language == subtitleLanguage }
|
candidates.firstOrNull { it.language == subtitleLanguage }
|
||||||
?: candidates.firstOrNull { it.language.isUnknown }
|
?: candidates.firstOrNull { it.language.isUnknown }
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -240,7 +242,7 @@ class StreamChoiceService
|
||||||
?: candidates.firstOrNull { it.isForced && it.language.isUnknown }
|
?: candidates.firstOrNull { it.isForced && it.language.isUnknown }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
candidates.firstOrNull { it.isForced }
|
candidates.firstOrNull { it.isDefault }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,12 +78,13 @@ inline fun <T> List<T>.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? {
|
||||||
/**
|
/**
|
||||||
* Try to call [FocusRequester.requestFocus], but catch & log the exception if something is not configured properly
|
* Try to call [FocusRequester.requestFocus], but catch & log the exception if something is not configured properly
|
||||||
*/
|
*/
|
||||||
fun FocusRequester.tryRequestFocus(): Boolean =
|
fun FocusRequester.tryRequestFocus(tag: String? = null): Boolean =
|
||||||
try {
|
try {
|
||||||
requestFocus()
|
requestFocus()
|
||||||
|
tag?.let { Timber.v("Request focus tag=%s", tag) }
|
||||||
true
|
true
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (ex: IllegalStateException) {
|
||||||
Timber.w(ex, "Failed to request focus")
|
Timber.w(ex, "Failed to request focus, tag=%s", tag)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,9 @@ fun TabRow(
|
||||||
) {
|
) {
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
LaunchedEffect(selectedTabIndex) {
|
LaunchedEffect(selectedTabIndex) {
|
||||||
state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt())
|
if (selectedTabIndex >= 0) {
|
||||||
|
state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val focusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } }
|
val focusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } }
|
||||||
var rowHasFocus by remember { mutableStateOf(false) }
|
var rowHasFocus by remember { mutableStateOf(false) }
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.ArrowForward
|
import androidx.compose.material.icons.filled.ArrowForward
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
|
@ -29,6 +30,12 @@ data class MoreDialogActions(
|
||||||
var onClickAddPlaylist: (UUID) -> Unit,
|
var onClickAddPlaylist: (UUID) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class ClearChosenStreams {
|
||||||
|
NONE,
|
||||||
|
ITEM_AND_SERIES,
|
||||||
|
SERIES,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the [DialogItem]s when clicking "More"
|
* Build the [DialogItem]s when clicking "More"
|
||||||
*
|
*
|
||||||
|
|
@ -53,10 +60,12 @@ fun buildMoreDialogItems(
|
||||||
sourceId: UUID?,
|
sourceId: UUID?,
|
||||||
watched: Boolean,
|
watched: Boolean,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
|
canClearChosenStreams: Boolean,
|
||||||
actions: MoreDialogActions,
|
actions: MoreDialogActions,
|
||||||
onChooseVersion: () -> Unit,
|
onChooseVersion: () -> Unit,
|
||||||
onChooseTracks: (MediaStreamType) -> Unit,
|
onChooseTracks: (MediaStreamType) -> Unit,
|
||||||
onShowOverview: () -> Unit,
|
onShowOverview: () -> Unit,
|
||||||
|
onClearChosenStreams: () -> Unit,
|
||||||
): List<DialogItem> =
|
): List<DialogItem> =
|
||||||
buildList {
|
buildList {
|
||||||
add(
|
add(
|
||||||
|
|
@ -172,6 +181,16 @@ fun buildMoreDialogItems(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (canClearChosenStreams) {
|
||||||
|
add(
|
||||||
|
DialogItem(
|
||||||
|
context.getString(R.string.clear_track_choices),
|
||||||
|
Icons.Default.Delete,
|
||||||
|
) {
|
||||||
|
onClearChosenStreams()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
add(
|
add(
|
||||||
DialogItem(
|
DialogItem(
|
||||||
context.getString(R.string.play_with_transcoding),
|
context.getString(R.string.play_with_transcoding),
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,7 @@ fun EpisodeDetails(
|
||||||
favorite = ep.data.userData?.isFavorite ?: false,
|
favorite = ep.data.userData?.isFavorite ?: false,
|
||||||
seriesId = ep.data.seriesId,
|
seriesId = ep.data.seriesId,
|
||||||
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||||
|
canClearChosenStreams = chosenStreams?.itemPlayback != null || chosenStreams?.plc != null,
|
||||||
actions = moreActions,
|
actions = moreActions,
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
|
|
@ -204,6 +205,9 @@ fun EpisodeDetails(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onClearChosenStreams = {
|
||||||
|
viewModel.clearChosenStreams(chosenStreams)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -182,4 +182,19 @@ class EpisodeViewModel
|
||||||
release()
|
release()
|
||||||
navigationManager.navigateTo(destination)
|
navigationManager.navigateTo(destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearChosenStreams(chosenStreams: ChosenStreams?) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
itemPlaybackRepository.deleteChosenStreams(chosenStreams)
|
||||||
|
item.value?.let { item ->
|
||||||
|
val result =
|
||||||
|
itemPlaybackRepository.getSelectedTracks(
|
||||||
|
itemId,
|
||||||
|
item,
|
||||||
|
userPreferencesService.getCurrent(),
|
||||||
|
)
|
||||||
|
this@EpisodeViewModel.chosenStreams.setValueOnMain(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,7 @@ fun MovieDetails(
|
||||||
favorite = movie.data.userData?.isFavorite ?: false,
|
favorite = movie.data.userData?.isFavorite ?: false,
|
||||||
seriesId = null,
|
seriesId = null,
|
||||||
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||||
|
canClearChosenStreams = chosenStreams?.itemPlayback != null || chosenStreams?.plc != null,
|
||||||
actions = moreActions,
|
actions = moreActions,
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
|
|
@ -241,6 +242,9 @@ fun MovieDetails(
|
||||||
files = movie.data.mediaSources.orEmpty(),
|
files = movie.data.mediaSources.orEmpty(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onClearChosenStreams = {
|
||||||
|
viewModel.clearChosenStreams(chosenStreams)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -247,4 +247,19 @@ class MovieViewModel
|
||||||
release()
|
release()
|
||||||
navigationManager.navigateTo(destination)
|
navigationManager.navigateTo(destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearChosenStreams(chosenStreams: ChosenStreams?) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
itemPlaybackRepository.deleteChosenStreams(chosenStreams)
|
||||||
|
item.value?.let { item ->
|
||||||
|
val result =
|
||||||
|
itemPlaybackRepository.getSelectedTracks(
|
||||||
|
itemId,
|
||||||
|
item,
|
||||||
|
userPreferencesService.getCurrent(),
|
||||||
|
)
|
||||||
|
this@MovieViewModel.chosenStreams.setValueOnMain(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import com.github.damontecres.wholphin.R
|
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.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||||
|
|
@ -179,6 +180,7 @@ fun SeriesOverview(
|
||||||
|
|
||||||
fun buildMoreForEpisode(
|
fun buildMoreForEpisode(
|
||||||
ep: BaseItem,
|
ep: BaseItem,
|
||||||
|
chosenStreams: ChosenStreams?,
|
||||||
fromLongClick: Boolean,
|
fromLongClick: Boolean,
|
||||||
): DialogParams =
|
): DialogParams =
|
||||||
DialogParams(
|
DialogParams(
|
||||||
|
|
@ -192,6 +194,7 @@ fun SeriesOverview(
|
||||||
favorite = ep.data.userData?.isFavorite ?: false,
|
favorite = ep.data.userData?.isFavorite ?: false,
|
||||||
seriesId = series.id,
|
seriesId = series.id,
|
||||||
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||||
|
canClearChosenStreams = chosenStreams?.itemPlayback != null || chosenStreams?.plc != null,
|
||||||
actions =
|
actions =
|
||||||
MoreDialogActions(
|
MoreDialogActions(
|
||||||
navigateTo = viewModel::navigateTo,
|
navigateTo = viewModel::navigateTo,
|
||||||
|
|
@ -259,6 +262,9 @@ fun SeriesOverview(
|
||||||
files = ep.data.mediaSources.orEmpty(),
|
files = ep.data.mediaSources.orEmpty(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onClearChosenStreams = {
|
||||||
|
viewModel.clearChosenStreams(ep, chosenStreams)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -304,7 +310,7 @@ fun SeriesOverview(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onLongClick = { ep ->
|
onLongClick = { ep ->
|
||||||
moreDialog = buildMoreForEpisode(ep, true)
|
moreDialog = buildMoreForEpisode(ep, chosenStreams, true)
|
||||||
},
|
},
|
||||||
playOnClick = { resume ->
|
playOnClick = { resume ->
|
||||||
rowFocused = EPISODE_ROW
|
rowFocused = EPISODE_ROW
|
||||||
|
|
@ -333,7 +339,7 @@ fun SeriesOverview(
|
||||||
},
|
},
|
||||||
moreOnClick = {
|
moreOnClick = {
|
||||||
episodeList?.getOrNull(position.episodeRowIndex)?.let { ep ->
|
episodeList?.getOrNull(position.episodeRowIndex)?.let { ep ->
|
||||||
moreDialog = buildMoreForEpisode(ep, false)
|
moreDialog = buildMoreForEpisode(ep, chosenStreams, false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
overviewOnClick = {
|
overviewOnClick = {
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ class SeriesViewModel
|
||||||
} ?: 0
|
} ?: 0
|
||||||
Timber.v("Got initial season index: $index")
|
Timber.v("Got initial season index: $index")
|
||||||
position.update {
|
position.update {
|
||||||
it.copy(seasonTabIndex = index)
|
it.copy(seasonTabIndex = index.coerceAtLeast(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -510,6 +510,16 @@ class SeriesViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearChosenStreams(
|
||||||
|
item: BaseItem,
|
||||||
|
chosenStreams: ChosenStreams?,
|
||||||
|
) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
itemPlaybackRepository.deleteChosenStreams(chosenStreams)
|
||||||
|
lookUpChosenTracks(item.id, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface EpisodeList {
|
sealed interface EpisodeList {
|
||||||
|
|
@ -547,7 +557,10 @@ private suspend fun findIndexOf(
|
||||||
val index =
|
val index =
|
||||||
if (targetId != null && (targetNum == null || targetNum !in pager.indices)) {
|
if (targetId != null && (targetNum == null || targetNum !in pager.indices)) {
|
||||||
// No hint info, so have to check everything
|
// No hint info, so have to check everything
|
||||||
pager.indexOfBlocking { equalsNotNull(it?.id, targetId) }
|
pager.indexOfBlocking {
|
||||||
|
equalsNotNull(it?.indexNumber, targetNum) ||
|
||||||
|
equalsNotNull(it?.id, targetId)
|
||||||
|
}
|
||||||
} else if (targetNum != null && targetNum in pager.indices) {
|
} else if (targetNum != null && targetNum in pager.indices) {
|
||||||
// Start searching from the season number and choose direction from there
|
// Start searching from the season number and choose direction from there
|
||||||
val num = pager.getBlocking(targetNum)?.indexNumber
|
val num = pager.getBlocking(targetNum)?.indexNumber
|
||||||
|
|
|
||||||
|
|
@ -226,28 +226,34 @@ fun HomePageContent(
|
||||||
mutableStateOf(RowColumn(firstRow, 0))
|
mutableStateOf(RowColumn(firstRow, 0))
|
||||||
}
|
}
|
||||||
val focusedItem =
|
val focusedItem =
|
||||||
remember(position) {
|
position.let {
|
||||||
position.let {
|
(homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column)
|
||||||
(homeRows.getOrNull(it.row) as? HomeRowLoadingState.Success)?.items?.getOrNull(it.column)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val rowFocusRequesters = remember(homeRows.size) { List(homeRows.size) { FocusRequester() } }
|
val rowFocusRequesters = remember(homeRows.size) { List(homeRows.size) { FocusRequester() } }
|
||||||
var focused by rememberSaveable { mutableStateOf(false) }
|
var firstFocused by rememberSaveable { mutableStateOf(false) }
|
||||||
LaunchedEffect(homeRows) {
|
LaunchedEffect(homeRows) {
|
||||||
if (!focused) {
|
if (!firstFocused) {
|
||||||
|
// Waiting for the first home row to load, then focus on it
|
||||||
homeRows
|
homeRows
|
||||||
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
|
.indexOfFirst { it is HomeRowLoadingState.Success && it.items.isNotEmpty() }
|
||||||
.takeIf { it >= 0 }
|
.takeIf { it >= 0 }
|
||||||
?.let {
|
?.let {
|
||||||
rowFocusRequesters[it].tryRequestFocus()
|
rowFocusRequesters[it].tryRequestFocus()
|
||||||
delay(50)
|
delay(50)
|
||||||
listState.animateScrollToItem(position.row)
|
listState.scrollToItem(it)
|
||||||
focused = true
|
firstFocused = true
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
rowFocusRequesters.getOrNull(position.row)?.tryRequestFocus()
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (firstFocused) {
|
||||||
|
// After the first home row was loaded & focused, page recompositions should focus on the positioned row
|
||||||
|
val index = position.row
|
||||||
|
rowFocusRequesters.getOrNull(index)?.tryRequestFocus()
|
||||||
|
delay(50)
|
||||||
|
listState.scrollToItem(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(position) {
|
LaunchedEffect(position) {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ data class PlaybackSettings(
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaybackDialog(
|
fun PlaybackDialog(
|
||||||
enableSubtitleDelay: Boolean,
|
enableSubtitleDelay: Boolean,
|
||||||
|
enableVideoScale: Boolean,
|
||||||
type: PlaybackDialogType,
|
type: PlaybackDialogType,
|
||||||
settings: PlaybackSettings,
|
settings: PlaybackSettings,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
|
|
@ -117,7 +118,9 @@ fun PlaybackDialog(
|
||||||
buildList {
|
buildList {
|
||||||
add(stringResource(R.string.audio))
|
add(stringResource(R.string.audio))
|
||||||
add(stringResource(R.string.playback_speed))
|
add(stringResource(R.string.playback_speed))
|
||||||
add(stringResource(R.string.video_scale))
|
if (enableVideoScale) {
|
||||||
|
add(stringResource(R.string.video_scale))
|
||||||
|
}
|
||||||
if (enableSubtitleDelay) {
|
if (enableSubtitleDelay) {
|
||||||
add(stringResource(R.string.subtitle_delay))
|
add(stringResource(R.string.subtitle_delay))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
|
|
@ -152,7 +153,7 @@ fun PlaybackPage(
|
||||||
var playbackDialog by remember { mutableStateOf<PlaybackDialogType?>(null) }
|
var playbackDialog by remember { mutableStateOf<PlaybackDialogType?>(null) }
|
||||||
OneTimeLaunchedEffect {
|
OneTimeLaunchedEffect {
|
||||||
if (prefs.playerBackend == PlayerBackend.MPV) {
|
if (prefs.playerBackend == PlayerBackend.MPV) {
|
||||||
scope.launch(Dispatchers.Main + ExceptionHandler()) {
|
scope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv(
|
preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv(
|
||||||
configuration,
|
configuration,
|
||||||
density,
|
density,
|
||||||
|
|
@ -161,7 +162,15 @@ fun PlaybackPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AmbientPlayerListener(player)
|
AmbientPlayerListener(player)
|
||||||
var contentScale by remember { mutableStateOf(prefs.globalContentScale.scale) }
|
var contentScale by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
if (prefs.playerBackend == PlayerBackend.MPV) {
|
||||||
|
ContentScale.FillBounds
|
||||||
|
} else {
|
||||||
|
prefs.globalContentScale.scale
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
var playbackSpeed by remember { mutableFloatStateOf(1.0f) }
|
var playbackSpeed by remember { mutableFloatStateOf(1.0f) }
|
||||||
LaunchedEffect(playbackSpeed) { player.setPlaybackSpeed(playbackSpeed) }
|
LaunchedEffect(playbackSpeed) { player.setPlaybackSpeed(playbackSpeed) }
|
||||||
|
|
||||||
|
|
@ -572,6 +581,7 @@ fun PlaybackPage(
|
||||||
onPlaybackActionClick = onPlaybackActionClick,
|
onPlaybackActionClick = onPlaybackActionClick,
|
||||||
onChangeSubtitleDelay = { viewModel.updateSubtitleDelay(it) },
|
onChangeSubtitleDelay = { viewModel.updateSubtitleDelay(it) },
|
||||||
enableSubtitleDelay = player is MpvPlayer,
|
enableSubtitleDelay = player is MpvPlayer,
|
||||||
|
enableVideoScale = player !is MpvPlayer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,8 @@ class MpvPlayer(
|
||||||
) : BasePlayer(),
|
) : BasePlayer(),
|
||||||
MPVLib.EventObserver,
|
MPVLib.EventObserver,
|
||||||
TrackSelector.InvalidationListener,
|
TrackSelector.InvalidationListener,
|
||||||
Handler.Callback {
|
Handler.Callback,
|
||||||
|
SurfaceHolder.Callback {
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEBUG = false
|
private const val DEBUG = false
|
||||||
}
|
}
|
||||||
|
|
@ -467,26 +468,55 @@ class MpvPlayer(
|
||||||
|
|
||||||
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException()
|
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
private var surfaceHolder: SurfaceHolder? = null
|
||||||
|
|
||||||
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||||
if (DEBUG) Timber.v("setVideoSurfaceView")
|
if (DEBUG) Timber.v("setVideoSurfaceView")
|
||||||
val surface = surfaceView?.holder?.surface
|
if (surfaceView != null) {
|
||||||
if (surface != null && surface.isValid) {
|
this.surfaceHolder?.removeCallback(this)
|
||||||
Timber.v("Queued attach")
|
this.surfaceHolder = surfaceView.holder
|
||||||
sendCommand(MpvCommand.ATTACH_SURFACE, surface)
|
if (surfaceView.holder != null) {
|
||||||
} else {
|
val surface = surfaceView.holder?.surface
|
||||||
clearVideoSurfaceView(null)
|
surfaceView.holder.addCallback(this)
|
||||||
|
Timber.v("Got surface holder: isValid=${surface?.isValid}")
|
||||||
|
if (surface != null && surface.isValid) {
|
||||||
|
Timber.v("Queued attach")
|
||||||
|
sendCommand(MpvCommand.ATTACH_SURFACE, surface)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
clearVideoSurfaceView(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||||
if (surface == surfaceView?.holder?.surface) {
|
if (surface != null && surface == surfaceView?.holder?.surface) {
|
||||||
Timber.d("clearVideoSurfaceView")
|
Timber.d("clearVideoSurfaceView")
|
||||||
sendCommand(MpvCommand.ATTACH_SURFACE, null)
|
sendCommand(MpvCommand.ATTACH_SURFACE, null)
|
||||||
} else {
|
} else {
|
||||||
Timber.w("clearVideoSurfaceView called with different surface")
|
Timber.w("clearVideoSurfaceView called with different surface: %s", surfaceView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun surfaceChanged(
|
||||||
|
holder: SurfaceHolder,
|
||||||
|
format: Int,
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
) {
|
||||||
|
Timber.v("surfaceChanged: format=$format, width=$width, height=$height")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
|
Timber.v("surfaceCreated")
|
||||||
|
sendCommand(MpvCommand.ATTACH_SURFACE, holder.surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||||
|
Timber.v("surfaceDestroyed")
|
||||||
|
sendCommand(MpvCommand.ATTACH_SURFACE, null)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
override fun setVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun clearVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
override fun clearVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
@ -660,6 +690,7 @@ class MpvPlayer(
|
||||||
MPV_EVENT_VIDEO_RECONFIG -> {
|
MPV_EVENT_VIDEO_RECONFIG -> {
|
||||||
Timber.d("event: MPV_EVENT_VIDEO_RECONFIG")
|
Timber.d("event: MPV_EVENT_VIDEO_RECONFIG")
|
||||||
updateTracksAndNotify()
|
updateTracksAndNotify()
|
||||||
|
updateVideoSizeAndNotify()
|
||||||
}
|
}
|
||||||
|
|
||||||
MPV_EVENT_END_FILE -> {
|
MPV_EVENT_END_FILE -> {
|
||||||
|
|
@ -720,6 +751,19 @@ class MpvPlayer(
|
||||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(tracks) }
|
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(tracks) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateVideoSizeAndNotify() {
|
||||||
|
val width = MPVLib.getPropertyInt("width")
|
||||||
|
val height = MPVLib.getPropertyInt("height")
|
||||||
|
val videoSize =
|
||||||
|
if (width != null && height != null) {
|
||||||
|
VideoSize(width, height)
|
||||||
|
} else {
|
||||||
|
VideoSize.UNKNOWN
|
||||||
|
}
|
||||||
|
playbackState.update { it.copy(videoSize = videoSize) }
|
||||||
|
notifyListeners(EVENT_VIDEO_SIZE_CHANGED) { onVideoSizeChanged(videoSize) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadFile(media: MediaAndPosition) {
|
private fun loadFile(media: MediaAndPosition) {
|
||||||
Timber.v("loadFile: media=$media")
|
Timber.v("loadFile: media=$media")
|
||||||
playbackState.update {
|
playbackState.update {
|
||||||
|
|
@ -836,23 +880,34 @@ class MpvPlayer(
|
||||||
internalHandler.obtainMessage(cmd.ordinal, obj).sendToTarget()
|
internalHandler.obtainMessage(cmd.ordinal, obj).sendToTarget()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val queuedCommands = mutableListOf<Pair<MpvCommand, Any?>>()
|
||||||
|
|
||||||
override fun handleMessage(msg: Message): Boolean {
|
override fun handleMessage(msg: Message): Boolean {
|
||||||
val cmd = MpvCommand.entries[msg.what]
|
val cmd = MpvCommand.entries[msg.what]
|
||||||
Timber.d("handleMessage: cmd=$cmd")
|
|
||||||
if (isReleased && cmd != MpvCommand.DESTROY) {
|
if (isReleased && cmd != MpvCommand.DESTROY) {
|
||||||
Timber.w("Player is released, ignoring command %s", cmd)
|
Timber.w("Player is released, ignoring command %s", cmd)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (surface == null && !cmd.isLifecycle) {
|
if (surface == null && !cmd.isLifecycle) {
|
||||||
// If libmpv isn't ready, re-enqueue the messages
|
// If libmpv isn't ready, ueue the messages
|
||||||
// Note: this means nothing will play until it is attached to a surface,
|
// Note: this means nothing will play until it is attached to a surface,
|
||||||
// so MpvPlayer can't be used for background audio/music playback
|
// so MpvPlayer can't be used for background audio/music playback
|
||||||
Timber.v("MPV is not initialized/attached yet, requeue cmd %s", cmd)
|
Timber.v("MPV is not initialized/attached yet, queue cmd %s", cmd)
|
||||||
internalHandler.sendMessageDelayed(Message.obtain(msg), 50)
|
queuedCommands.add(Pair(cmd, msg.obj))
|
||||||
|
} else {
|
||||||
|
handleCommand(cmd, msg.obj)
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCommand(
|
||||||
|
cmd: MpvCommand,
|
||||||
|
obj: Any?,
|
||||||
|
) {
|
||||||
|
Timber.d("handleCommand: cmd=$cmd")
|
||||||
when (cmd) {
|
when (cmd) {
|
||||||
MpvCommand.PLAY_PAUSE -> {
|
MpvCommand.PLAY_PAUSE -> {
|
||||||
val playWhenReady = msg.obj as Boolean
|
val playWhenReady = obj as Boolean
|
||||||
MPVLib.setPropertyBoolean("pause", !playWhenReady)
|
MPVLib.setPropertyBoolean("pause", !playWhenReady)
|
||||||
playbackState.update {
|
playbackState.update {
|
||||||
it.copy(isPaused = !playWhenReady)
|
it.copy(isPaused = !playWhenReady)
|
||||||
|
|
@ -866,13 +921,13 @@ class MpvPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
MpvCommand.SET_TRACK_SELECTION -> {
|
MpvCommand.SET_TRACK_SELECTION -> {
|
||||||
val (propertyName, trackId) = msg.obj as TrackSelection
|
val (propertyName, trackId) = obj as TrackSelection
|
||||||
MPVLib.setPropertyString(propertyName, trackId)
|
MPVLib.setPropertyString(propertyName, trackId)
|
||||||
updateTracksAndNotify()
|
updateTracksAndNotify()
|
||||||
}
|
}
|
||||||
|
|
||||||
MpvCommand.SEEK -> {
|
MpvCommand.SEEK -> {
|
||||||
val positionMs = msg.obj as Long
|
val positionMs = obj as Long
|
||||||
MPVLib.setPropertyDouble("time-pos", positionMs / 1000.0)
|
MPVLib.setPropertyDouble("time-pos", positionMs / 1000.0)
|
||||||
playbackState.update {
|
playbackState.update {
|
||||||
it.copy(positionMs = positionMs)
|
it.copy(positionMs = positionMs)
|
||||||
|
|
@ -880,7 +935,7 @@ class MpvPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
MpvCommand.SET_SPEED -> {
|
MpvCommand.SET_SPEED -> {
|
||||||
val value = msg.obj as Float
|
val value = obj as Float
|
||||||
MPVLib.setPropertyDouble("speed", value.toDouble())
|
MPVLib.setPropertyDouble("speed", value.toDouble())
|
||||||
playbackState.update {
|
playbackState.update {
|
||||||
it.copy(speed = value)
|
it.copy(speed = value)
|
||||||
|
|
@ -888,7 +943,7 @@ class MpvPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
MpvCommand.SET_SUBTITLE_DELAY -> {
|
MpvCommand.SET_SUBTITLE_DELAY -> {
|
||||||
val value = msg.obj as Double
|
val value = obj as Double
|
||||||
MPVLib.setPropertyDouble("sub-delay", value)
|
MPVLib.setPropertyDouble("sub-delay", value)
|
||||||
playbackState.update {
|
playbackState.update {
|
||||||
it.copy(subtitleDelay = value)
|
it.copy(subtitleDelay = value)
|
||||||
|
|
@ -896,11 +951,11 @@ class MpvPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
MpvCommand.LOAD_FILE -> {
|
MpvCommand.LOAD_FILE -> {
|
||||||
loadFile(msg.obj as MediaAndPosition)
|
loadFile(obj as MediaAndPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
MpvCommand.ATTACH_SURFACE -> {
|
MpvCommand.ATTACH_SURFACE -> {
|
||||||
val surface = msg.obj as Surface?
|
val surface = obj as Surface?
|
||||||
if (surface == null || (this.surface != null && this.surface != surface)) {
|
if (surface == null || (this.surface != null && this.surface != surface)) {
|
||||||
// If clearing or changing the surface
|
// If clearing or changing the surface
|
||||||
MPVLib.detachSurface()
|
MPVLib.detachSurface()
|
||||||
|
|
@ -913,6 +968,13 @@ class MpvPlayer(
|
||||||
this.surface = surface
|
this.surface = surface
|
||||||
MPVLib.setOptionString("force-window", "yes")
|
MPVLib.setOptionString("force-window", "yes")
|
||||||
Timber.d("Attached surface")
|
Timber.d("Attached surface")
|
||||||
|
if (queuedCommands.isNotEmpty()) {
|
||||||
|
Timber.d("Processing queued commands")
|
||||||
|
while (queuedCommands.isNotEmpty()) {
|
||||||
|
val msg = queuedCommands.removeAt(0)
|
||||||
|
handleCommand(msg.first, msg.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -927,7 +989,6 @@ class MpvPlayer(
|
||||||
Timber.d("MPVLib destroyed")
|
Timber.d("MPVLib destroyed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1023,7 +1084,7 @@ private data class PlaybackState(
|
||||||
val EMPTY =
|
val EMPTY =
|
||||||
PlaybackState(
|
PlaybackState(
|
||||||
timestamp = C.TIME_UNSET,
|
timestamp = C.TIME_UNSET,
|
||||||
isLoadingFile = false,
|
isLoadingFile = true,
|
||||||
media = null,
|
media = null,
|
||||||
positionMs = C.TIME_UNSET,
|
positionMs = C.TIME_UNSET,
|
||||||
durationMs = C.TIME_UNSET,
|
durationMs = C.TIME_UNSET,
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,7 @@
|
||||||
<string name="local">Local</string>
|
<string name="local">Local</string>
|
||||||
<string name="play_trailer">Play trailer</string>
|
<string name="play_trailer">Play trailer</string>
|
||||||
<string name="no_trailers">No trailers</string>
|
<string name="no_trailers">No trailers</string>
|
||||||
|
<string name="clear_track_choices">Clear track choices</string>
|
||||||
|
|
||||||
<string name="discover">Discover</string>
|
<string name="discover">Discover</string>
|
||||||
<string name="request">Request</string>
|
<string name="request">Request</string>
|
||||||
|
|
|
||||||
|
|
@ -104,16 +104,6 @@ class TestStreamChoiceServiceBasic(
|
||||||
),
|
),
|
||||||
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.UNSPECIFIED),
|
itemPlayback = itemPlayback(subtitleIndex = TrackIndex.UNSPECIFIED),
|
||||||
),
|
),
|
||||||
TestInput(
|
|
||||||
1,
|
|
||||||
SubtitlePlaybackMode.ALWAYS,
|
|
||||||
subtitles =
|
|
||||||
listOf(
|
|
||||||
subtitle(0, "eng", forced = true, default = true),
|
|
||||||
subtitle(1, "eng", false),
|
|
||||||
subtitle(2, "eng", false),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -352,6 +342,31 @@ class TestStreamChoiceServiceSmart(
|
||||||
userSubtitleLang = "spa",
|
userSubtitleLang = "spa",
|
||||||
userAudioLang = "eng",
|
userAudioLang = "eng",
|
||||||
),
|
),
|
||||||
|
TestInput(
|
||||||
|
1,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", false),
|
||||||
|
subtitle(1, "spa", false),
|
||||||
|
),
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
userSubtitleLang = "spa",
|
||||||
|
userAudioLang = null,
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
1,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", false),
|
||||||
|
subtitle(1, "eng", true),
|
||||||
|
subtitle(2, "spa", false),
|
||||||
|
),
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
userSubtitleLang = "eng",
|
||||||
|
userAudioLang = null,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -448,6 +463,156 @@ class TestStreamChoiceServiceOnlyForced(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class TestStreamChoiceServiceMultipleChoices(
|
||||||
|
val input: TestInput,
|
||||||
|
) {
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
runTest(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@Parameterized.Parameters(name = "{index}: {0}")
|
||||||
|
fun data(): Collection<TestInput> =
|
||||||
|
listOf(
|
||||||
|
TestInput(
|
||||||
|
0,
|
||||||
|
SubtitlePlaybackMode.ALWAYS,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true, default = true),
|
||||||
|
subtitle(1, "eng", false),
|
||||||
|
subtitle(2, "eng", false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
2,
|
||||||
|
SubtitlePlaybackMode.ALWAYS,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true, default = false),
|
||||||
|
subtitle(1, "eng", false),
|
||||||
|
subtitle(2, "eng", default = true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
2,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true, default = false),
|
||||||
|
subtitle(1, "eng", false),
|
||||||
|
subtitle(2, "eng", default = true),
|
||||||
|
),
|
||||||
|
userAudioLang = null,
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
2,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true, default = false),
|
||||||
|
subtitle(1, "eng", false),
|
||||||
|
subtitle(2, "eng", default = true),
|
||||||
|
),
|
||||||
|
userSubtitleLang = null,
|
||||||
|
userAudioLang = null,
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
null,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true, default = false),
|
||||||
|
subtitle(1, "eng", false),
|
||||||
|
subtitle(2, "eng", default = true),
|
||||||
|
),
|
||||||
|
userSubtitleLang = "spa",
|
||||||
|
userAudioLang = null,
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
2,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true, default = true),
|
||||||
|
subtitle(1, "eng", false),
|
||||||
|
subtitle(2, "eng", default = true),
|
||||||
|
),
|
||||||
|
userSubtitleLang = "eng",
|
||||||
|
userAudioLang = null,
|
||||||
|
streamAudioLang = "spa",
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
0,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true, default = true),
|
||||||
|
subtitle(1, "eng", false),
|
||||||
|
subtitle(2, "eng", default = true),
|
||||||
|
),
|
||||||
|
userSubtitleLang = "eng",
|
||||||
|
userAudioLang = "eng",
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
null,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true, default = false),
|
||||||
|
subtitle(1, "eng", false),
|
||||||
|
subtitle(2, "eng", default = true),
|
||||||
|
),
|
||||||
|
userSubtitleLang = "spa",
|
||||||
|
userAudioLang = "eng",
|
||||||
|
streamAudioLang = "spa",
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
0,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "spa", forced = true, default = false),
|
||||||
|
subtitle(1, "spa", false),
|
||||||
|
subtitle(2, "spa", default = true),
|
||||||
|
),
|
||||||
|
userSubtitleLang = "spa",
|
||||||
|
userAudioLang = "eng",
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
2,
|
||||||
|
SubtitlePlaybackMode.SMART,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "spa", forced = true, default = false),
|
||||||
|
subtitle(1, "spa", false),
|
||||||
|
subtitle(2, "spa", default = true),
|
||||||
|
),
|
||||||
|
userSubtitleLang = "spa",
|
||||||
|
userAudioLang = "",
|
||||||
|
streamAudioLang = "eng",
|
||||||
|
),
|
||||||
|
TestInput(
|
||||||
|
2,
|
||||||
|
SubtitlePlaybackMode.DEFAULT,
|
||||||
|
subtitles =
|
||||||
|
listOf(
|
||||||
|
subtitle(0, "eng", forced = true, default = false),
|
||||||
|
subtitle(1, "eng", false),
|
||||||
|
subtitle(2, "eng", default = true),
|
||||||
|
),
|
||||||
|
userSubtitleLang = null,
|
||||||
|
userAudioLang = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class TestInput(
|
data class TestInput(
|
||||||
val expectedIndex: Int?,
|
val expectedIndex: Int?,
|
||||||
val userSubtitleMode: SubtitlePlaybackMode?,
|
val userSubtitleMode: SubtitlePlaybackMode?,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue