Add a lot of code documentation (#1145)

## Description
A brain dump documenting many classes and functions throughout the app.
Definitely does not document everything, but covers most of the major
components. This should be useful for new contributors.

There is a small amount of code clean up too.

There are no user facing changes.

### Related issues
N/A

### Testing
N/A

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-03-29 10:52:25 -04:00 • committed by GitHub
parent 2485d2c644
commit 66f060dccb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 719 additions and 222 deletions

View file

@ -35,6 +35,11 @@ import androidx.tv.material3.ProvideTextStyle
import androidx.tv.material3.Surface
import androidx.tv.material3.Text
/**
* This is a re-implementation of [androidx.tv.material3.Button] with altered sizing, padding, colors, etc
*
* This allows for creating smaller and fully circular Buttons.
*/
@Composable
fun Button(
onClick: () -> Unit,

View file

@ -90,6 +90,9 @@ sealed interface DialogItemEntry
data object DialogItemDivider : DialogItemEntry
/**
* An item within a [DialogPopup]
*/
data class DialogItem(
val headlineContent: @Composable () -> Unit,
val onClick: () -> Unit,

View file

@ -34,7 +34,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@ -49,8 +48,10 @@ import androidx.tv.material3.MaterialTheme
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.google.protobuf.value
/**
* An input field for text customized for TV entry
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditTextBox(

View file

@ -55,6 +55,14 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import java.util.UUID
/**
* Button for filtering data.
*
* Clicking on it will show a drop-down menu of filterable options. Many of these show a second drop-down menu when clicked.
*
* @see GetItemsFilter
* @see ItemFilterBy
*/
@Composable
fun FilterByButton(
filterOptions: List<ItemFilterBy<*>>,

View file

@ -156,6 +156,9 @@ private val genreCache by lazy {
}
}
/**
* Create a mapping from genre IDs to image URLs using random items within each genre
*/
suspend fun getGenreImageMap(
api: ApiClient,
userId: UUID?,
@ -230,6 +233,9 @@ data class Genre(
override val sortName: String get() = name
}
/**
* Show an optimized grid of genres for a library
*/
@Composable
fun GenreCardGrid(
itemId: UUID,

View file

@ -11,6 +11,11 @@ import androidx.tv.material3.Text
private const val MAX_TO_SHOW = 4
/**
* Display a comma separated list of genres
*
* Only the first few will be shown
*/
@Composable
fun GenreText(
genres: List<String>,

View file

@ -82,6 +82,9 @@ class ItemGridViewModel
}
}
/**
* Display a grid of a list of arbitrary item IDs such as for [com.github.damontecres.wholphin.data.ExtrasItem]
*/
@Composable
fun ItemGrid(
destination: Destination.ItemGrid,

View file

@ -6,6 +6,9 @@ import androidx.compose.ui.Modifier
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
/**
* Displays dependencies' license information to comply with attribution
*/
@Composable
fun LicenseInfo(modifier: Modifier = Modifier) {
val libraries by produceLibraries()

View file

@ -23,6 +23,9 @@ import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.playOnClickSound
import com.github.damontecres.wholphin.ui.playSoundOnFocus
/**
* Show the overview text for an item. Uses a fixed size and allows for clicking.
*/
@Composable
fun OverviewText(
overview: String,

View file

@ -43,6 +43,7 @@ import com.github.damontecres.wholphin.ui.rememberPosition
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@ -50,8 +51,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import org.jellyfin.sdk.model.api.MediaType
import java.util.UUID
/**
* Abstract [ViewModel] for the "Recommended" tab for a library
*/
abstract class RecommendedViewModel(
val context: Context,
@param:ApplicationContext val context: Context,
val navigationManager: NavigationManager,
val favoriteWatchManager: FavoriteWatchManager,
val mediaReportService: MediaReportService,

View file

@ -13,6 +13,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.tv.material3.LocalContentColor
/**
* Shows a dot indicator
*
* Intended for using within the `leadingContent` of a [androidx.tv.material3.ListItem]
*/
@Composable
fun BoxScope.SelectedLeadingContent(
selected: Boolean,

View file

@ -126,16 +126,19 @@ data class SliderColors(
@Composable
fun default() =
SliderColors(
activeFocused = SliderActiveColor(true),
activeUnfocused = SliderActiveColor(false),
inactiveFocused = SliderInactiveColor(true),
inactiveUnfocused = SliderInactiveColor(false),
activeFocused = sliderActiveColor(true),
activeUnfocused = sliderActiveColor(false),
inactiveFocused = sliderInactiveColor(true),
inactiveUnfocused = sliderInactiveColor(false),
)
}
}
/**
* Determines the active color for the slider. This is the "filled" left side of the slider
*/
@Composable
fun SliderActiveColor(focused: Boolean): Color {
fun sliderActiveColor(focused: Boolean): Color {
val theme = LocalTheme.current
return when (theme) {
AppThemeColors.UNRECOGNIZED,
@ -165,8 +168,11 @@ fun SliderActiveColor(focused: Boolean): Color {
}
}
/**
* Determines the inactive color for the slider. This is background of "unfilled" right side of the slider.
*/
@Composable
fun SliderInactiveColor(focused: Boolean): Color {
fun sliderInactiveColor(focused: Boolean): Color {
val theme = LocalTheme.current
return when (theme) {
AppThemeColors.UNRECOGNIZED,

View file

@ -12,6 +12,9 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.util.LocalClock
/**
* Displays the [LocalClock] in the upper right corner of the parent [androidx.compose.foundation.layout.Box]
*/
@Composable
fun BoxScope.TimeDisplay(modifier: Modifier = Modifier) {
val timeString by LocalClock.current.timeString

View file

@ -39,6 +39,11 @@ import com.github.damontecres.wholphin.ui.tryRequestFocus
import kotlinx.serialization.Serializable
import org.jellyfin.sdk.model.api.ImageType
/**
* A dialog that shows the controls for making changes to a [ViewOptions] object. The caller must manage the state of the [ViewOptions].
*
* It displays the [AppPreference] objects from [ViewOptions.OPTIONS]
*/
@Composable
fun ViewOptionsDialog(
viewOptions: ViewOptions,
@ -105,6 +110,11 @@ fun ViewOptionsDialog(
}
}
/**
* Stores the customizable view changes from the user
*
* This is used to determine how the UI looks such as card size and shape
*/
@Serializable
data class ViewOptions(
val columns: Int = 6,

View file

@ -19,6 +19,10 @@ import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
/**
* A supplementary [ViewModel] for adding items to a server playlist
* @see com.github.damontecres.wholphin.ui.detail.PlaylistDialog
*/
@HiltViewModel
class AddPlaylistViewModel
@Inject

View file

@ -42,6 +42,9 @@ data class ItemDetailsDialogInfo(
val files: List<MediaSourceInfo>,
)
/**
* Dialog showing metadata about an item
*/
@Composable
fun ItemDetailsDialog(
info: ItemDetailsDialogInfo,

View file

@ -85,6 +85,9 @@ interface CardGridItem {
val sortName: String
}
/**
* Shows a vertical grid of [CardGridItem]s
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun <T : CardGridItem> CardGrid(

View file

@ -49,9 +49,8 @@ enum class ClearChosenStreams {
* If there are any (ie one or more) subtitle tracks, adds an option to disable or pick one
*
* @param item the media item to build for, typically an Episode or Movie
* @param series the item's series or null if not a TV episode; a non-null value will include a "Go to Series" option
* @param seriesId the item's series or null if not a TV episode; a non-null value will include a "Go to Series" option
* @param sourceId the item's media source UUID
* @param navigateTo a function to trigger a navigation
* @param onChooseVersion callback to pick a version of the item
* @param onChooseTracks callback to pick a track for the given type of the item
* @param onShowOverview callback to show overview dialog with media information

View file

@ -256,7 +256,7 @@ class PlaylistViewModel
/**
* This method tries to determine the [MediaType] of a playlist
*
* In theory, the server will set the type, but sometime it doesn't
* In theory, the server will set the type, but sometimes it doesn't
*/
private suspend fun determineMediaType() {
// Use the type the server says

View file

@ -172,6 +172,9 @@ class LiveTvViewModel
}
}
/**
* Creates a list of [LocalDateTime] for each hour from now
*/
private fun buildGuideTimes() =
buildList {
val start = LocalDateTime.now().roundDownToHalfHour()
@ -194,15 +197,22 @@ class LiveTvViewModel
loading.setValueOnMain(LoadingState.Success)
}
/**
* Get live TV programs for a subset of channels
*
* @param guideStart The start timestamp to fetch from
* @param channels The full list of channels
* @param channelIndices The indices of which channels to fetch programs for
*/
private suspend fun fetchPrograms(
guideStart: LocalDateTime,
channels: List<TvChannel>,
range: IntRange,
channelIndices: IntRange,
) = mutex.withLock {
val maxStartDate = guideStart.plusHours(MAX_HOURS).minusMinutes(1)
val minEndDate = guideStart.plusMinutes(1L)
val channelsToFetch = channels.subList(range.first, range.last + 1)
Timber.v("Fetching programs for $range channels ${channelsToFetch.size}")
val channelsToFetch = channels.subList(channelIndices.first, channelIndices.last + 1)
Timber.v("Fetching programs for $channelIndices channels ${channelsToFetch.size}")
val request =
GetProgramsDto(
maxStartDate = maxStartDate,
@ -390,7 +400,7 @@ class LiveTvViewModel
Timber.d("Got ${fetchedPrograms.size} programs & ${finalProgramList.size} total programs")
withContext(Dispatchers.Main) {
this@LiveTvViewModel.programs.value =
FetchedPrograms(range, finalProgramList, programsByChannel)
FetchedPrograms(channelIndices, finalProgramList, programsByChannel)
}
}
@ -472,6 +482,11 @@ class LiveTvViewModel
private var focusLoadingJob: Job? = null
/**
* Callback when focusing on the EPG grid
*
* This determines if more programs/channels should be fetched based on the current position
*/
fun onFocusChannel(position: RowColumn) {
channels.value?.let { channels ->
val fetchedRange = programs.value!!.range

View file

@ -37,6 +37,9 @@ import com.github.damontecres.wholphin.ui.CrossFadeFactory
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/**
* Shows the current backdrop images provided by [com.github.damontecres.wholphin.services.BackdropService]
*/
@Composable
fun Backdrop(
drawerIsOpen: Boolean,
@ -59,6 +62,9 @@ fun Backdrop(
)
}
/**
* Shows the current backdrop images provided by the [BackdropResult]
*/
@Composable
fun Backdrop(
backdrop: BackdropResult,

View file

@ -157,6 +157,9 @@ class NavDrawerViewModel
fun getUserImage(user: JellyfinUser): String = api.imageApi.getUserImageUrl(user.id)
/**
* Determine which nav drawer item should be highlighted as currently selected
*/
fun updateSelectedIndex() {
viewModelScope.launchDefault {
val asDestinations =
@ -215,6 +218,11 @@ class NavDrawerViewModel
}
}
/**
* An item that can be shown in the nav drawer
*
* Some are built-in such as Favorites. Others are created dynamically for libraries.
*/
sealed interface NavDrawerItem {
val id: String
@ -242,6 +250,9 @@ sealed interface NavDrawerItem {
}
}
/**
* A server provided nav drawer item, typically a library
*/
data class ServerNavDrawerItem(
val itemId: UUID,
val name: String,

View file

@ -57,6 +57,11 @@ import androidx.tv.material3.NavigationDrawerItemDefaults
import androidx.tv.material3.NavigationDrawerScope
import androidx.tv.material3.rememberDrawerState
/**
* This is a re-implementation of [androidx.tv.material3.ModalNavigationDrawer].
*
* It changes padding, sizing, and animations that couldn't be changed in the Androidx implementation.
*/
@Composable
fun ModalNavigationDrawer(
drawerContent: @Composable NavigationDrawerScope.(DrawerValue) -> Unit,

View file

@ -8,6 +8,11 @@ import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.TrickplayInfo
import org.jellyfin.sdk.model.extensions.ticks
/**
* Metadata about the currently playing media
*
* @see CurrentPlayback
*/
data class CurrentMediaInfo(
val sourceId: String?,
val videoStream: SimpleVideoStream?,

View file

@ -8,6 +8,11 @@ import org.jellyfin.sdk.model.api.PlayMethod
import org.jellyfin.sdk.model.api.TranscodingInfo
import kotlin.time.Duration
/**
* Information about how the current media is being played such transcoding and decoder info
*
* @see CurrentMediaInfo
*/
data class CurrentPlayback(
val item: BaseItem,
val tracks: List<TrackSupport>,

View file

@ -51,7 +51,7 @@ import org.jellyfin.sdk.model.api.RemoteSubtitleInfo
@Composable
fun DownloadSubtitlesContent(
state: SubtitleSearch,
state: SubtitleSearchStatus,
language: String,
onSearch: (String) -> Unit,
onClickDownload: (RemoteSubtitleInfo) -> Unit,
@ -59,7 +59,7 @@ fun DownloadSubtitlesContent(
modifier: Modifier = Modifier,
) {
when (val s = state) {
SubtitleSearch.Searching -> {
SubtitleSearchStatus.Searching -> {
Wrapper {
Text(
text = stringResource(R.string.searching),
@ -69,7 +69,7 @@ fun DownloadSubtitlesContent(
}
}
SubtitleSearch.Downloading -> {
SubtitleSearchStatus.Downloading -> {
Wrapper {
Text(
text = stringResource(R.string.downloading),
@ -79,11 +79,11 @@ fun DownloadSubtitlesContent(
}
}
is SubtitleSearch.Error -> {
is SubtitleSearchStatus.Error -> {
Wrapper { ErrorMessage(null, s.ex, modifier) }
}
is SubtitleSearch.Success -> {
is SubtitleSearchStatus.Success -> {
val dialogItems = convertRemoteSubtitles(s.options, onClickDownload)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {

View file

@ -84,6 +84,9 @@ import org.jellyfin.sdk.model.extensions.ticks
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/**
* Possible actions the user can take during playback
*/
sealed interface PlaybackAction {
data object ShowDebug : PlaybackAction
@ -114,6 +117,9 @@ sealed interface PlaybackAction {
data object Next : PlaybackAction
}
/**
* UI for actual playback controls such as seek bar, play/pause and seek buttons
*/
@OptIn(UnstableApi::class)
@Composable
fun PlaybackControls(
@ -515,6 +521,11 @@ fun PlaybackFaButton(
}
}
/**
* Show a dialog aligned to the bottom of the screen instead of centered.
*
* @param gravity The [Gravity] to align the dialog to left or right
*/
@Composable
fun <T> BottomDialog(
choices: List<BottomDialogItem<T>>,
@ -594,10 +605,6 @@ fun <T> BottomDialog(
}
}
data class MoreButtonOptions(
val options: Map<String, PlaybackAction>,
)
data class BottomDialogItem<T>(
val data: T,
val headline: String,

View file

@ -62,6 +62,14 @@ data class PlaybackSettings(
val playbackSpeedEnabled: Boolean,
)
/**
* Centralized UI component for displaying dialogs during playback
*
* Typically, the user will click something generating a [PlaybackAction] which translates into the
* [PlaybackDialogType] determining which dialog is shown by this component.
*
* @see PlaybackAction
*/
@Composable
fun PlaybackDialog(
enableSubtitleDelay: Boolean,

View file

@ -575,6 +575,8 @@ enum class OverlayViewState {
/**
* A wrapper for the playback controls to show title and other information, plus the actual controls
*
* @see PlaybackControls
*/
@Composable
fun Controller(

View file

@ -168,7 +168,7 @@ fun PlaybackPageContent(
val nextUp by viewModel.nextUp.observeAsState(null)
val playlist by viewModel.playlist.observeAsState(Playlist(listOf()))
val subtitleSearch by viewModel.subtitleSearch.observeAsState(null)
val subtitleSearch by viewModel.subtitleSearchStatus.observeAsState(null)
val subtitleSearchLanguage by viewModel.subtitleSearchLanguage.observeAsState(Locale.current.language)
var playbackDialog by remember { mutableStateOf<PlaybackDialogType?>(null) }

View file

@ -187,7 +187,7 @@ class PlaybackViewModel
private var isPlaylist = false
val playlist = MutableLiveData<Playlist>(Playlist(listOf()))
val subtitleSearch = MutableLiveData<SubtitleSearch?>(null)
val subtitleSearchStatus = MutableLiveData<SubtitleSearchStatus?>(null)
val subtitleSearchLanguage = MutableLiveData<String>(Locale.current.language)
val currentUserDto = serverRepository.currentUserDto

View file

@ -1,39 +0,0 @@
package com.github.damontecres.wholphin.ui.playback
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.media3.common.Player
import androidx.media3.common.listen
import androidx.media3.common.util.UnstableApi
@UnstableApi
@Composable
fun rememberPlayerLoadingState(player: Player): PlayerLoadingState {
val state = remember(player) { PlayerLoadingState(player) }
LaunchedEffect(player) {
state.observe()
}
return state
}
@UnstableApi
class PlayerLoadingState(
private val player: Player,
) : State<Boolean> {
override var value by mutableStateOf(player.isLoading)
private set
suspend fun observe() {
value = player.isLoading
player.listen {
if (it.contains(Player.EVENT_IS_LOADING_CHANGED)) {
value = player.isLoading
}
}
}
}

View file

@ -49,6 +49,11 @@ import androidx.tv.material3.MaterialTheme
import kotlinx.coroutines.FlowPreview
import kotlin.time.Duration
/**
* This is a seek bar which seeks by a percentage of the duration instead of a fixed amount of time
*
* For example, if [intervals] is 10, then each seek will be 10% of total media duration
*/
@Composable
fun SteppedSeekBarImpl(
progress: Float,
@ -97,6 +102,9 @@ fun SteppedSeekBarImpl(
)
}
/**
* A seek (or scrubber) bar which seeks forward or back a fixed amount of time per move
*/
@OptIn(FlowPreview::class)
@Composable
fun IntervalSeekBarImpl(
@ -147,8 +155,14 @@ fun IntervalSeekBarImpl(
)
}
/**
* Actually renders the seek bar. It has callbacks for when the user moves to the left or right
*
* @see IntervalSeekBarImpl
* @see SteppedSeekBarImpl
*/
@Composable
fun SeekBarDisplay(
private fun SeekBarDisplay(
progress: Float,
bufferedProgress: Float,
durationMs: Long,

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.playback
import androidx.annotation.FloatRange
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -52,20 +53,21 @@ fun Modifier.offsetByPercent(
*
* @param xPercentage percent offset between 0 inclusive and 1 inclusive
*/
fun Modifier.offsetByPercent(xPercentage: Float) =
this.then(
Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(
x =
((constraints.maxWidth * xPercentage).toInt() - placeable.width / 2)
.coerceIn(0, constraints.maxWidth - placeable.width),
y = 0,
)
}
},
)
fun Modifier.offsetByPercent(
@FloatRange(0.0, 1.0) xPercentage: Float,
) = this.then(
Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(
x =
((constraints.maxWidth * xPercentage).toInt() - placeable.width / 2)
.coerceIn(0, constraints.maxWidth - placeable.width),
y = 0,
)
}
},
)
/**
* Show trickplay preview image. This composable assumes the provided URL is for the correct index.

View file

@ -5,6 +5,11 @@ import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.util.StreamFormatting.mediaStreamDisplayTitle
import org.jellyfin.sdk.model.api.MediaStream
/**
* A slimmer [MediaStream] with minimal data for UI purposes
*
* @see SimpleVideoStream
*/
data class SimpleMediaStream(
val index: Int,
val streamTitle: String?,

View file

@ -25,6 +25,9 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.ifElse
import kotlin.math.abs
/**
* Shows a rotating indicator when seeking. Displays the amount of seconds in the [durationMs].
*/
@Composable
fun SkipIndicator(
durationMs: Long,

View file

@ -30,6 +30,9 @@ import kotlin.time.Duration.Companion.seconds
private val delayIncrements = listOf(50.milliseconds, 250.milliseconds, 1.seconds)
/**
* Shows buttons for adding to or subtracting from the given [Duration], ie subtitle delay
*/
@Composable
fun SubtitleDelay(
delay: Duration,

View file

@ -20,23 +20,26 @@ import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.RemoteSubtitleInfo
import timber.log.Timber
sealed interface SubtitleSearch {
data object Searching : SubtitleSearch
sealed interface SubtitleSearchStatus {
data object Searching : SubtitleSearchStatus
data object Downloading : SubtitleSearch
data object Downloading : SubtitleSearchStatus
data class Success(
val options: List<RemoteSubtitleInfo>,
) : SubtitleSearch
) : SubtitleSearchStatus
data class Error(
val message: String?,
val ex: Exception?,
) : SubtitleSearch
) : SubtitleSearchStatus
}
/**
* Trigger a search for subtitles in the given language for the currently playing media
*/
fun PlaybackViewModel.searchForSubtitles(language: String = Locale.current.language) {
subtitleSearch.value = SubtitleSearch.Searching
subtitleSearchStatus.value = SubtitleSearchStatus.Searching
subtitleSearchLanguage.value = language
viewModelScope.launchIO {
try {
@ -52,23 +55,26 @@ fun PlaybackViewModel.searchForSubtitles(language: String = Locale.current.langu
compareByDescending<RemoteSubtitleInfo> { it.communityRating }
.thenByDescending { it.downloadCount },
)
subtitleSearch.setValueOnMain(SubtitleSearch.Success(results))
subtitleSearchStatus.setValueOnMain(SubtitleSearchStatus.Success(results))
}
} catch (ex: Exception) {
Timber.e(ex, "Exception while searching for subtitles")
subtitleSearch.setValueOnMain(SubtitleSearch.Error(null, ex))
subtitleSearchStatus.setValueOnMain(SubtitleSearchStatus.Error(null, ex))
}
}
}
/**
* Download the remote subtitles and attempt to activate them once complete
*/
fun PlaybackViewModel.downloadAndSwitchSubtitles(
subtitleId: String?,
wasPlaying: Boolean,
) {
if (subtitleId == null) {
subtitleSearch.value = SubtitleSearch.Error("Subtitle has no ID", null)
subtitleSearchStatus.value = SubtitleSearchStatus.Error("Subtitle has no ID", null)
} else {
subtitleSearch.value = SubtitleSearch.Downloading
subtitleSearchStatus.value = SubtitleSearchStatus.Downloading
viewModelScope.launchIO {
try {
currentItemPlayback.value?.let {
@ -161,7 +167,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
)
}
}
subtitleSearch.setValueOnMain(null)
subtitleSearchStatus.setValueOnMain(null)
withContext(Dispatchers.Main) {
if (wasPlaying) {
player.play()
@ -170,12 +176,12 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
}
} catch (ex: Exception) {
Timber.e(ex, "Exception while downloading subtitles: $subtitleId")
subtitleSearch.setValueOnMain(SubtitleSearch.Error(null, ex))
subtitleSearchStatus.setValueOnMain(SubtitleSearchStatus.Error(null, ex))
}
}
}
}
fun PlaybackViewModel.cancelSubtitleSearch() {
subtitleSearch.value = null
subtitleSearchStatus.value = null
}

View file

@ -15,6 +15,9 @@ import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
import timber.log.Timber
import kotlin.math.max
/**
* Functions for selecting which audio & subtitle tracks to activate in the [androidx.media3.common.Player]
*/
object TrackSelectionUtils {
@OptIn(UnstableApi::class)
fun createTrackSelections(
@ -258,6 +261,9 @@ fun List<MediaStream>.findExternalSubtitle(subtitleIndex: Int?): MediaStream? =
}
}
/**
* The result of [TrackSelectionUtils.createTrackSelections]
*/
data class TrackSelectionResult(
val trackSelectionParameters: TrackSelectionParameters,
val audioSelected: Boolean,