From 253b203c024b4b46f9d56f31a2564543f2171f7e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:44:41 -0400 Subject: [PATCH 1/3] Update Androidx Media3 to v1.10.0 (#1149) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b7337845..c462981e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ tvFoundation = "1.0.0-beta01" tvMaterial = "1.0.1" lifecycleRuntimeKtx = "2.10.0" activityCompose = "1.13.0" -androidx-media3 = "1.9.3" +androidx-media3 = "1.10.0" coil = "3.4.0" jellyfin-sdk = "1.7.1" nav3Core = "1.0.1" From 2485d2c644290c2a1e690cbe45c3e98cd5520c1d Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sun, 29 Mar 2026 10:38:23 -0400 Subject: [PATCH 2/3] Add prefer logos instead of text titles setting (#1151) ## Description Adds an advanced settings to prefer logos for titles instead of text. It is enabled by default. ### Related issues Closes #383 ### Testing Emulator & onn ## Screenshots
Click to show

### Home page ![logo_1 Large](https://github.com/user-attachments/assets/629a65ab-cf43-4050-af4c-5925763a5f4f) ### Series overview ![logo_2 Large](https://github.com/user-attachments/assets/13ca2d28-cc61-4f4e-abaa-61a310b5c707) ### Movie Page ![logo_movie Large](https://github.com/user-attachments/assets/a590e6cf-452d-4313-b6a3-35c6f59dc1ff) ### Square-ish logo ![logo_square Large](https://github.com/user-attachments/assets/7d1340c2-4a3e-42c3-b7bd-ae1114cb6645)

## AI or LLM usage None --- .../wholphin/preferences/AppPreference.kt | 13 +++ .../preferences/AppPreferencesSerializer.kt | 1 + .../wholphin/services/AppUpgradeHandler.kt | 10 +- .../wholphin/services/HomeSettingsService.kt | 2 +- .../wholphin/services/LatestNextUpService.kt | 21 ++-- .../wholphin/services/PlaylistCreator.kt | 7 +- .../wholphin/ui/cards/BannerCard.kt | 2 +- .../ui/components/CollectionFolderGrid.kt | 4 +- .../wholphin/ui/components/HeaderUtils.kt | 24 +++++ .../ui/components/RecommendedContent.kt | 1 + .../wholphin/ui/components/TitleOrLogo.kt | 95 +++++++++++++++++++ .../ui/detail/collection/CollectionDetails.kt | 19 ++-- .../collection/CollectionDetailsHeader.kt | 46 +++------ .../ui/detail/collection/CollectionRows.kt | 1 + .../detail/collection/CollectionViewModel.kt | 14 +-- .../ui/detail/episode/EpisodeDetails.kt | 5 +- .../wholphin/ui/detail/movie/MovieDetails.kt | 5 +- .../ui/detail/movie/MovieDetailsHeader.kt | 30 +++--- .../ui/detail/series/FocusedEpisodeHeader.kt | 11 ++- .../ui/detail/series/SeriesDetails.kt | 32 +++---- .../ui/detail/series/SeriesOverviewContent.kt | 19 ++-- .../wholphin/ui/discover/SeerrDiscoverPage.kt | 2 + .../damontecres/wholphin/ui/main/HomePage.kt | 41 ++++---- .../ui/main/settings/HomeSettingsPage.kt | 3 + .../wholphin/ui/nav/DestinationContent.kt | 2 +- app/src/main/proto/WholphinDataStore.proto | 1 + app/src/main/res/values/strings.xml | 1 + 27 files changed, 290 insertions(+), 122 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/HeaderUtils.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index ef3f14e7..e88c1ffb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -501,6 +501,18 @@ sealed interface AppPreference { valueToIndex = { if (it != AppThemeColors.UNRECOGNIZED) it.number else 0 }, ) + val ShowLogos = + AppSwitchPreference( + title = R.string.prefer_logos, + defaultValue = true, + getter = { it.interfacePreferences.showLogos }, + setter = { prefs, value -> + prefs.updateInterfacePreferences { showLogos = value } + }, + summaryOn = R.string.enabled, + summaryOff = R.string.disabled, + ) + val InstalledVersion = AppClickablePreference( title = R.string.installed_version, @@ -1122,6 +1134,7 @@ val advancedPreferences = preferences = listOf( AppPreference.ShowClock, + AppPreference.ShowLogos, AppPreference.ManageMedia, AppPreference.CombineContinueNext, // Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418 diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index f55b4ea9..e202fca3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -95,6 +95,7 @@ class AppPreferencesSerializer AppPreference.NavDrawerSwitchOnFocus.defaultValue showClock = AppPreference.ShowClock.defaultValue backdropStyle = AppPreference.BackdropStylePref.defaultValue + showLogos = AppPreference.ShowLogos.defaultValue subtitlesPreferences = SubtitlePreferences diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 617944fc..07b5fc7e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -282,7 +282,7 @@ class AppUpgradeHandler } } - if (previous.isEqualOrBefore(Version.fromString("0.6.0-0-g0"))) { + if (previous.isEqualOrBefore(Version.fromString("0.5.4-6-g0"))) { appPreferences.updateData { it.updateMusicPreferences { showBackdrop = true @@ -291,5 +291,13 @@ class AppUpgradeHandler } } } + + if (previous.isEqualOrBefore(Version.fromString("0.5.4-15-g0"))) { + appPreferences.updateData { + it.updateInterfacePreferences { + showLogos = AppPreference.ShowLogos.defaultValue + } + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index b28dded4..0b3b0887 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -886,7 +886,7 @@ class HomeSettingsService limit = limit, enableUserData = true, enableImages = true, - enableImageTypes = listOf(ImageType.PRIMARY), + enableImageTypes = listOf(ImageType.PRIMARY, ImageType.LOGO), imageTypeLimit = 1, ) api.liveTvApi diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt index e30a39ca..f6e02df2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt @@ -20,7 +20,6 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userViewsApi import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.UserDto import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import org.jellyfin.sdk.model.api.request.GetNextUpRequest @@ -61,12 +60,13 @@ class LatestNextUpService remove(BaseItemKind.EPISODE) } }, - enableImageTypes = - listOf( - ImageType.PRIMARY, - ImageType.THUMB, - ImageType.BACKDROP, - ), +// enableImageTypes = +// listOf( +// ImageType.PRIMARY, +// ImageType.THUMB, +// ImageType.BACKDROP, +// ImageType.LOGO, +// ), ) val items = api.itemsApi @@ -98,6 +98,13 @@ class LatestNextUpService enableUserData = true, enableRewatching = enableRewatching, nextUpDateCutoff = nextUpDateCutoff, +// enableImageTypes = +// listOf( +// ImageType.PRIMARY, +// ImageType.THUMB, +// ImageType.BACKDROP, +// ImageType.LOGO, +// ), ) val nextUp = api.tvShowsApi diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt index 4231d9b9..b47ae33f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt @@ -114,7 +114,12 @@ class PlaylistCreator req = GetItemsRequest( parentId = item.id, - enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), + enableImageTypes = + listOf( + ImageType.PRIMARY, + ImageType.THUMB, + ImageType.LOGO, + ), includeItemTypes = includeItemTypes, recursive = true, excludeItemIds = listOf(item.id), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt index 51d003fe..a05af761 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt @@ -214,7 +214,7 @@ fun BannerCardWithTitle( ) { val focused by interactionSource.collectIsFocusedAsState() val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp) - val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp) + val spaceBelow by animateDpAsState(if (focused) 0.dp else 8.dp) val focusedAfterDelay by rememberFocusedAfterDelay(interactionSource) val aspectRationToUse = aspectRatio.coerceAtLeast(AspectRatios.MIN) val width = cardHeight * aspectRationToUse diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index a4e4ef22..4c8090e3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -398,6 +398,7 @@ class CollectionFolderViewModel ImageType.PRIMARY, ImageType.THUMB, ImageType.BACKDROP, + ImageType.LOGO, ), includeItemTypes = includeItemTypes, recursive = recursive, @@ -960,11 +961,12 @@ fun CollectionFolderGridContent( AnimatedVisibility(viewOptions.showDetails) { HomePageHeader( item = focusedItem, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, modifier = Modifier .fillMaxWidth() .height(200.dp) - .padding(top = 48.dp, bottom = 32.dp, start = 8.dp), + .padding(HeaderUtils.padding), ) } when (val state = loadingState) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/HeaderUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/HeaderUtils.kt new file mode 100644 index 00000000..0ebdd1db --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/HeaderUtils.kt @@ -0,0 +1,24 @@ +package com.github.damontecres.wholphin.ui.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +object HeaderUtils { + val topPadding = 48.dp + val bottomPadding = 32.dp + val startPadding = 8.dp + + val padding = PaddingValues(top = topPadding, bottom = bottomPadding, start = startPadding) + + val height = 180.dp + + val logoHeight = 60.dp + + val modifier = + Modifier + .padding(padding) + .height(height) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt index ae7edb08..db40ff0e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt @@ -207,6 +207,7 @@ fun RecommendedContent( }, showClock = preferences.appPreferences.interfacePreferences.showClock, onUpdateBackdrop = viewModel::updateBackdrop, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, modifier = modifier, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt new file mode 100644 index 00000000..5af10b8b --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TitleOrLogo.kt @@ -0,0 +1,95 @@ +package com.github.damontecres.wholphin.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import coil3.compose.AsyncImage +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.ui.LocalImageUrlService +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ImageType + +@Composable +fun TitleOrLogo( + title: String?, + logoImageUrl: String?, + showLogo: Boolean, + modifier: Modifier = Modifier, +) { + var imageError by remember { mutableStateOf(false) } + Box( + modifier = modifier.heightIn(max = HeaderUtils.logoHeight), + ) { + if (showLogo && logoImageUrl != null && !imageError) { + AsyncImage( + model = logoImageUrl, + contentDescription = title, + contentScale = ContentScale.Fit, + modifier = + Modifier + .height(HeaderUtils.logoHeight) + .widthIn(max = 320.dp), + ) + } else { + Title(title, Modifier) + } + } +} + +@Composable +private fun Title( + title: String?, + modifier: Modifier = Modifier, +) { + Text( + text = title ?: "", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = modifier, + ) +} + +@Composable +fun TitleOrLogo( + item: BaseItem?, + showLogo: Boolean, + modifier: Modifier = Modifier, +) { + val logoImageUrl = rememberLogoUrl(item) + TitleOrLogo( + title = item?.title, + logoImageUrl = logoImageUrl, + showLogo = showLogo, + modifier = modifier, + ) +} + +@Composable +fun rememberLogoUrl(item: BaseItem?): String? { + val imageUrlService = LocalImageUrlService.current + return remember(item?.id) { + if (item?.type == BaseItemKind.EPISODE && item.data.seriesId != null && item.data.parentLogoImageTag != null) { + imageUrlService.getItemImageUrl(item.data.seriesId!!, ImageType.LOGO) + } else if (ImageType.LOGO in item?.data?.imageTags.orEmpty()) { + imageUrlService.getItemImageUrl(item, ImageType.LOGO) + } else { + null + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt index 7f9cc1ca..bd647d47 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetails.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -50,6 +49,7 @@ import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.ErrorMessage +import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel @@ -379,11 +379,13 @@ fun CollectionDetailsContent( if (state.viewOptions.cardViewOptions.showDetails) { HomePageHeader( item = focusedItem, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, modifier = Modifier - .padding(top = 48.dp, start = 8.dp) - .fillMaxHeight(.33f) - .fillMaxWidth(), + .padding( + top = HeaderUtils.topPadding, + bottom = 8.dp, + ).height(HeaderUtils.height), ) } } @@ -412,15 +414,16 @@ fun CollectionDetailsContent( ) { CollectionDetailsHeader( collection = state.collection!!, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, logoImageUrl = state.logoImageUrl, overviewOnClick = overviewOnClick, bringIntoViewRequester = bringIntoViewRequester, modifier = Modifier - .padding(top = 48.dp, start = 8.dp) - // TODO - .fillMaxHeight(.36f) - .fillMaxWidth(), + .padding( + top = HeaderUtils.topPadding, + bottom = HeaderUtils.bottomPadding, + ).height(HeaderUtils.height), ) CollectionButtons( state = state, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetailsHeader.kt index a71ac187..cbdf1560 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionDetailsHeader.kt @@ -3,28 +3,24 @@ package com.github.damontecres.wholphin.ui.detail.collection import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text -import coil3.compose.AsyncImage import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.components.GenreText +import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.QuickDetails +import com.github.damontecres.wholphin.ui.components.TitleOrLogo import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.util.ExceptionHandler import kotlinx.coroutines.launch @@ -32,6 +28,7 @@ import kotlinx.coroutines.launch @Composable fun CollectionDetailsHeader( collection: BaseItem, + showLogo: Boolean, logoImageUrl: String?, overviewOnClick: () -> Unit, bringIntoViewRequester: BringIntoViewRequester, @@ -44,28 +41,15 @@ fun CollectionDetailsHeader( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier, ) { - if (logoImageUrl != null) { - AsyncImage( - model = logoImageUrl, - contentDescription = collection.name, - modifier = Modifier.height(80.dp), - alignment = Alignment.TopStart, - contentScale = ContentScale.Fit, - ) - } else { - // Title - Text( - text = collection.name ?: "", - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .fillMaxWidth(.75f) - .padding(start = 8.dp), - ) - } + TitleOrLogo( + title = collection.name, + showLogo = showLogo, + logoImageUrl = logoImageUrl, + modifier = + Modifier + .fillMaxWidth(.75f) + .padding(start = HeaderUtils.startPadding), + ) Column( verticalArrangement = Arrangement.spacedBy(4.dp), @@ -74,18 +58,18 @@ fun CollectionDetailsHeader( QuickDetails( collection.ui.quickDetails, collection.timeRemainingOrRuntime, - Modifier.padding(start = 8.dp), + Modifier.padding(start = HeaderUtils.startPadding), ) dto.genres?.letNotEmpty { - GenreText(it, Modifier.padding(start = 8.dp)) + GenreText(it, Modifier.padding(start = HeaderUtils.startPadding)) } dto.taglines?.firstOrNull()?.let { tagline -> Text( text = tagline, style = MaterialTheme.typography.bodyLarge, fontStyle = FontStyle.Italic, - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = HeaderUtils.startPadding), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionRows.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionRows.kt index ef78fb3b..b33e751f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionRows.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionRows.kt @@ -83,6 +83,7 @@ fun CollectionRows( onUpdateBackdrop = {}, headerComposable = {}, takeFocus = false, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, modifier = Modifier, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt index badc4e9d..4a717ee0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/collection/CollectionViewModel.kt @@ -62,6 +62,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.request.GetItemsRequest @@ -139,13 +140,12 @@ class CollectionViewModel .content .let { BaseItem(it, false) } backdropService.submit(collection) - val logoImageUrl = null - // TODO add logo back -// if (ImageType.LOGO in collection.data.imageTags.orEmpty()) { -// imageUrlService.getItemImageUrl(collection, ImageType.LOGO) -// } else { -// null -// } + val logoImageUrl = + if (ImageType.LOGO in collection.data.imageTags.orEmpty()) { + imageUrlService.getItemImageUrl(collection, ImageType.LOGO) + } else { + null + } _state.update { it.copy( collection = collection, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index a89db27b..e4b5a047 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -36,6 +36,7 @@ import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons +import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.chooseStream @@ -332,7 +333,7 @@ fun EpisodeDetailsContent( Box(modifier = modifier) { LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(vertical = 8.dp), + contentPadding = PaddingValues(bottom = 8.dp), modifier = Modifier.fillMaxSize(), ) { item { @@ -352,7 +353,7 @@ fun EpisodeDetailsContent( modifier = Modifier .fillMaxWidth() - .padding(top = 32.dp, bottom = 16.dp), + .padding(top = HeaderUtils.topPadding, bottom = 16.dp), ) ExpandablePlayButtons( resumePosition = resumePosition, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 4195e9a8..47a80e49 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -51,6 +51,7 @@ import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons +import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.chooseStream @@ -439,7 +440,7 @@ fun MovieDetailsContent( Box(modifier = modifier) { LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(vertical = 8.dp), + contentPadding = PaddingValues(bottom = 8.dp), modifier = Modifier.fillMaxSize(), ) { item { @@ -459,7 +460,7 @@ fun MovieDetailsContent( modifier = Modifier .fillMaxWidth() - .padding(top = 40.dp, bottom = 16.dp), + .padding(top = HeaderUtils.topPadding, bottom = 16.dp), ) ExpandablePlayButtons( resumePosition = resumePosition, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt index 45e3ee54..ee8f74f0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt @@ -13,8 +13,6 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.tv.material3.MaterialTheme @@ -24,8 +22,10 @@ import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.GenreText +import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.QuickDetails +import com.github.damontecres.wholphin.ui.components.TitleOrLogo import com.github.damontecres.wholphin.ui.components.VideoStreamDetails import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.letNotEmpty @@ -50,16 +50,13 @@ fun MovieDetailsHeader( modifier = modifier, ) { // Title - Text( - text = movie.name ?: "", - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold), - maxLines = 1, - overflow = TextOverflow.Ellipsis, + TitleOrLogo( + item = movie, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, modifier = Modifier .fillMaxWidth(.75f) - .padding(start = 8.dp), + .padding(start = HeaderUtils.startPadding), ) Column( @@ -69,24 +66,29 @@ fun MovieDetailsHeader( QuickDetails( movie.ui.quickDetails, movie.timeRemainingOrRuntime, - Modifier.padding(start = 8.dp), + Modifier.padding(start = HeaderUtils.startPadding), ) dto.genres?.letNotEmpty { - GenreText(it, Modifier.padding(start = 8.dp)) + GenreText(it, Modifier.padding(start = HeaderUtils.startPadding)) } VideoStreamDetails( chosenStreams = chosenStreams, numberOfVersions = movie.data.mediaSourceCount ?: 0, - modifier = Modifier.padding(start = 8.dp, top = 4.dp, bottom = 16.dp), + modifier = + Modifier.padding( + start = HeaderUtils.startPadding, + top = 4.dp, + bottom = 16.dp, + ), ) dto.taglines?.firstOrNull()?.let { tagline -> Text( text = tagline, style = MaterialTheme.typography.bodyLarge, fontStyle = FontStyle.Italic, - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = HeaderUtils.startPadding), ) } @@ -122,7 +124,7 @@ fun MovieDetailsHeader( text = stringResource(R.string.directed_by, it), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = HeaderUtils.startPadding), ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt index aab6e056..b63f1cbc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt @@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.EpisodeName +import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.QuickDetails import com.github.damontecres.wholphin.ui.components.VideoStreamDetails @@ -32,17 +33,21 @@ fun FocusedEpisodeHeader( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier, ) { - EpisodeName(dto, modifier = Modifier.padding(start = 8.dp)) + EpisodeName(dto, modifier = Modifier.padding(start = HeaderUtils.startPadding)) ep?.ui?.quickDetails?.let { - QuickDetails(it, ep.timeRemainingOrRuntime, Modifier.padding(start = 8.dp)) + QuickDetails( + it, + ep.timeRemainingOrRuntime, + Modifier.padding(start = HeaderUtils.startPadding), + ) } if (dto != null) { VideoStreamDetails( chosenStreams = chosenStreams, numberOfVersions = dto.mediaSourceCount ?: 0, - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = HeaderUtils.startPadding), ) } OverviewText( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index aea22bf1..3a95440f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -37,14 +37,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleResumeEffect -import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ExtrasItem import com.github.damontecres.wholphin.data.model.BaseItem @@ -69,10 +65,12 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.ExpandablePlayButton import com.github.damontecres.wholphin.ui.components.GenreText +import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.Optional import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.QuickDetails +import com.github.damontecres.wholphin.ui.components.TitleOrLogo import com.github.damontecres.wholphin.ui.components.TrailerButton import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog @@ -386,7 +384,6 @@ fun SeriesDetailsContent( Column( modifier = Modifier - .padding(vertical = 16.dp) .fillMaxSize(), ) { LazyColumn( @@ -397,18 +394,19 @@ fun SeriesDetailsContent( item { SeriesDetailsHeader( series = series, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, overviewOnClick = overviewOnClick, modifier = Modifier .fillMaxWidth() .bringIntoViewRequester(bringIntoViewRequester) - .padding(top = 32.dp, bottom = 16.dp), + .padding(top = HeaderUtils.topPadding, bottom = 16.dp), ) Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier - .padding(start = 8.dp) + .padding(start = HeaderUtils.startPadding) .focusRequester(focusRequesters[HEADER_ROW]) .focusRestorer(playFocusRequester) .focusGroup() @@ -684,6 +682,7 @@ fun SeriesDetailsContent( @Composable fun SeriesDetailsHeader( series: BaseItem, + showLogo: Boolean, overviewOnClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -693,24 +692,25 @@ fun SeriesDetailsHeader( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier, ) { - Text( - text = series.name ?: stringResource(R.string.unknown), - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold), - maxLines = 1, - overflow = TextOverflow.Ellipsis, + TitleOrLogo( + item = series, + showLogo = showLogo, modifier = Modifier .fillMaxWidth(.75f) - .padding(start = 8.dp), + .padding(start = HeaderUtils.startPadding), ) Column( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.fillMaxWidth(.60f), ) { - QuickDetails(series.ui.quickDetails, null, Modifier.padding(start = 8.dp)) + QuickDetails( + series.ui.quickDetails, + null, + Modifier.padding(start = HeaderUtils.startPadding), + ) dto.genres?.letNotEmpty { - GenreText(it, Modifier.padding(start = 8.dp, bottom = 8.dp)) + GenreText(it, Modifier.padding(start = HeaderUtils.startPadding, bottom = 8.dp)) } dto.overview?.let { overview -> OverviewText( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index 8acc32f1..b32bbd30 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -52,9 +52,10 @@ import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.PersonRow import com.github.damontecres.wholphin.ui.components.ErrorMessage +import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.LoadingPage -import com.github.damontecres.wholphin.ui.components.SeriesName import com.github.damontecres.wholphin.ui.components.TabRow +import com.github.damontecres.wholphin.ui.components.TitleOrLogo import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp @@ -143,10 +144,12 @@ fun SeriesOverviewContent( .bringIntoViewRequester(bringIntoViewRequester), ) { val paddingValues = - if (preferences.appPreferences.interfacePreferences.showClock) { - PaddingValues(start = 0.dp, end = 184.dp) - } else { - PaddingValues(start = 0.dp, end = 16.dp) + remember(preferences.appPreferences.interfacePreferences.showClock) { + if (preferences.appPreferences.interfacePreferences.showClock) { + PaddingValues(start = 0.dp, end = 184.dp) + } else { + PaddingValues(start = 0.dp, end = 16.dp) + } } TabRow( selectedTabIndex = selectedTabIndex, @@ -164,7 +167,11 @@ fun SeriesOverviewContent( .padding(bottom = 4.dp) .fillMaxWidth(), ) - SeriesName(series.name, Modifier.padding(start = 8.dp)) + TitleOrLogo( + item = series, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, + modifier = Modifier.padding(start = HeaderUtils.startPadding), + ) FocusedEpisodeHeader( preferences = preferences, ep = focusedEpisode, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt index 97b974ff..e30be9b3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt @@ -250,6 +250,8 @@ fun SeerrDiscoverPage( overviewTwoLines = true, quickDetails = details, timeRemaining = null, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, + logoImageUrl = null, // TODO modifier = Modifier .padding(top = 24.dp, bottom = 16.dp, start = 32.dp) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 733ddef2..94281b78 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -39,7 +38,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -61,9 +59,12 @@ import com.github.damontecres.wholphin.ui.components.DialogPopup import com.github.damontecres.wholphin.ui.components.EpisodeName import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.FocusableItemRow +import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.QuickDetails import com.github.damontecres.wholphin.ui.components.RowColumnItem +import com.github.damontecres.wholphin.ui.components.TitleOrLogo +import com.github.damontecres.wholphin.ui.components.rememberLogoUrl import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.detail.MoreDialogActions @@ -172,6 +173,7 @@ fun HomePage( loadingState = refreshing, showClock = preferences.appPreferences.interfacePreferences.showClock, onUpdateBackdrop = viewModel::updateBackdrop, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, modifier = modifier, ) dialog?.let { params -> @@ -222,6 +224,7 @@ fun HomePageContent( onClickPlay: (RowColumn, BaseItem) -> Unit, showClock: Boolean, onUpdateBackdrop: (BaseItem) -> Unit, + showLogo: Boolean, modifier: Modifier = Modifier, loadingState: LoadingState? = null, listState: LazyListState = rememberLazyListState(), @@ -230,10 +233,8 @@ fun HomePageContent( headerComposable: @Composable (focusedItem: BaseItem?) -> Unit = { focusedItem -> HomePageHeader( item = focusedItem, - modifier = - Modifier - .padding(top = 48.dp, bottom = 32.dp, start = 8.dp) - .fillMaxHeight(.33f), + showLogo = showLogo, + modifier = HeaderUtils.modifier, ) }, ) { @@ -410,6 +411,7 @@ fun HomePageContent( @Composable fun HomePageHeader( item: BaseItem?, + showLogo: Boolean, modifier: Modifier = Modifier, ) { val isEpisode = item?.type == BaseItemKind.EPISODE @@ -421,6 +423,8 @@ fun HomePageHeader( overviewTwoLines = isEpisode, quickDetails = item?.ui?.quickDetails ?: AnnotatedString(""), timeRemaining = item?.timeRemainingOrRuntime, + showLogo = showLogo, + logoImageUrl = rememberLogoUrl(item), modifier = modifier, ) } @@ -433,31 +437,28 @@ fun HomePageHeader( overviewTwoLines: Boolean, quickDetails: AnnotatedString?, timeRemaining: Duration?, + showLogo: Boolean, + logoImageUrl: String?, modifier: Modifier = Modifier, ) { Column( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier, ) { - title?.let { - Text( - text = it, - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold), - color = MaterialTheme.colorScheme.onBackground, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth(.75f), - ) - } + TitleOrLogo( + title = title, + logoImageUrl = logoImageUrl, + showLogo = showLogo, + modifier = Modifier.fillMaxWidth(.75f), + ) Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier - .fillMaxWidth(.6f) - .fillMaxHeight(), + .fillMaxWidth(.6f), ) { - subtitle?.let { - EpisodeName(it) + if (subtitle != null) { + EpisodeName(subtitle) } QuickDetails(quickDetails ?: AnnotatedString(""), timeRemaining) val overviewModifier = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt index fb025c02..a2a1ac63 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsPage.kt @@ -32,6 +32,7 @@ import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.preferences.AppPreferences +import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.ConfirmDialog import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.detail.search.SearchForDialog @@ -51,6 +52,7 @@ val settingsWidth = 360.dp @Composable fun HomeSettingsPage( + preferences: UserPreferences, modifier: Modifier, viewModel: HomeSettingsViewModel = hiltViewModel(), ) { @@ -310,6 +312,7 @@ fun HomeSettingsPage( listState = listState, takeFocus = false, showEmptyRows = true, + showLogo = preferences.appPreferences.interfacePreferences.showLogos, modifier = Modifier .fillMaxHeight() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 7c41c6d1..5591f56d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -69,7 +69,7 @@ fun DestinationContent( } is Destination.HomeSettings -> { - HomeSettingsPage(modifier) + HomeSettingsPage(preferences, modifier) } is Destination.PlaybackList, diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index ad8c7a20..ce9080a7 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -167,6 +167,7 @@ message InterfacePreferences { SubtitlePreferences hdr_subtitles_preferences = 10; ScreensaverPreferences screensaver_preference = 11; bool enable_media_management = 12; + bool show_logos = 13; } message AdvancedPreferences { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fc71cd52..b7dbbf3b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -747,5 +747,6 @@ Continue Select all Separate types + Prefer showing logos for titles From 66f060dccbc66b1aa0fa5b155ad3c5251c260bc5 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sun, 29 Mar 2026 10:52:25 -0400 Subject: [PATCH 3/3] 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 --- DEVELOPMENT.md | 1 + .../damontecres/wholphin/data/ExtrasItem.kt | 15 +++ .../wholphin/data/filter/ItemFilterBy.kt | 5 + .../wholphin/data/model/AudioItem.kt | 8 ++ .../wholphin/data/model/BaseItem.kt | 13 +++ .../wholphin/data/model/Chapter.kt | 3 + .../wholphin/data/model/DiscoverItem.kt | 11 +- .../wholphin/data/model/GetItemsFilter.kt | 11 +- .../wholphin/data/model/ItemPlayback.kt | 4 + .../data/model/ItemTrackModification.kt | 5 + .../wholphin/data/model/JellyfinServer.kt | 9 ++ .../wholphin/data/model/LibraryDisplayInfo.kt | 5 + ...rPreferences.kt => NavDrawerPinnedItem.kt} | 3 + .../damontecres/wholphin/data/model/Person.kt | 3 + .../wholphin/data/model/PlaybackEffect.kt | 3 + .../data/model/PlaybackLanguageChoice.kt | 4 + .../wholphin/data/model/Playlist.kt | 5 + .../wholphin/data/model/SeerrPermission.kt | 3 + .../wholphin/data/model/SeerrServer.kt | 12 ++ .../wholphin/data/model/Trailer.kt | 9 ++ .../wholphin/data/model/VideoFilter.kt | 2 +- .../wholphin/services/AppUpgradeHandler.kt | 10 ++ .../wholphin/services/BackdropService.kt | 23 ++++ .../wholphin/services/DatePlayedService.kt | 20 ++++ .../wholphin/services/DeviceProfileService.kt | 7 +- .../wholphin/services/ExtrasService.kt | 11 ++ .../wholphin/services/FavoriteWatchManager.kt | 3 + .../wholphin/services/HomeSettingsService.kt | 12 ++ .../wholphin/services/LatestNextUpService.kt | 108 +++--------------- .../services/MediaManagementService.kt | 15 ++- .../wholphin/services/MediaReportService.kt | 9 ++ .../wholphin/services/MusicService.kt | 55 ++++++++- .../wholphin/services/NavDrawerService.kt | 16 +++ .../wholphin/services/PeopleFavorites.kt | 3 + .../wholphin/services/PlaylistCreator.kt | 16 +++ .../wholphin/services/RefreshRateService.kt | 9 ++ .../wholphin/services/ScreensaverService.kt | 18 +++ .../wholphin/services/ServerEventListener.kt | 3 + .../wholphin/services/StreamChoiceService.kt | 15 +++ .../wholphin/services/TrailerService.kt | 3 + .../wholphin/services/UpdateChecker.kt | 22 +++- .../services/UserPreferencesService.kt | 3 + .../wholphin/services/hilt/AppModule.kt | 22 ++++ .../tvprovider/TvProviderSchedulerService.kt | 7 ++ .../services/tvprovider/TvProviderWorker.kt | 5 + .../wholphin/ui/components/Button.kt | 5 + .../wholphin/ui/components/Dialogs.kt | 3 + .../wholphin/ui/components/EditTextBox.kt | 5 +- .../wholphin/ui/components/FilterByButton.kt | 8 ++ .../wholphin/ui/components/GenreCardGrid.kt | 6 + .../wholphin/ui/components/GenreText.kt | 5 + .../wholphin/ui/components/ItemGrid.kt | 3 + .../wholphin/ui/components/LicenseInfo.kt | 3 + .../wholphin/ui/components/OverviewText.kt | 3 + .../ui/components/RecommendedContent.kt | 6 +- .../ui/components/SelectedLeadingContent.kt | 5 + .../wholphin/ui/components/SliderBar.kt | 18 ++- .../wholphin/ui/components/TimeDisplay.kt | 3 + .../ui/components/ViewOptionsDialog.kt | 10 ++ .../wholphin/ui/data/AddPlaylistViewModel.kt | 4 + .../wholphin/ui/data/ItemDetailsDialogInfo.kt | 3 + .../wholphin/ui/detail/CardGrid.kt | 3 + .../wholphin/ui/detail/DetailUtils.kt | 3 +- .../wholphin/ui/detail/PlaylistDetails.kt | 2 +- .../ui/detail/livetv/LiveTvViewModel.kt | 23 +++- .../damontecres/wholphin/ui/nav/Backdrop.kt | 6 + .../damontecres/wholphin/ui/nav/NavDrawer.kt | 11 ++ .../ui/nav/NavigationDrawerAndroid.kt | 5 + .../wholphin/ui/playback/CurrentMediaInfo.kt | 5 + .../wholphin/ui/playback/CurrentPlayback.kt | 5 + .../ui/playback/DownloadSubtitlesDialog.kt | 10 +- .../wholphin/ui/playback/PlaybackControls.kt | 15 ++- .../wholphin/ui/playback/PlaybackDialog.kt | 8 ++ .../wholphin/ui/playback/PlaybackOverlay.kt | 2 + .../wholphin/ui/playback/PlaybackPage.kt | 2 +- .../wholphin/ui/playback/PlaybackViewModel.kt | 2 +- .../ui/playback/PlayerLoadingState.kt | 39 ------- .../wholphin/ui/playback/SeekBar.kt | 16 ++- .../wholphin/ui/playback/SeekPreviewImage.kt | 30 ++--- .../wholphin/ui/playback/SimpleMediaStream.kt | 5 + .../wholphin/ui/playback/SkipIndicator.kt | 3 + .../wholphin/ui/playback/SubtitleDelay.kt | 3 + .../ui/playback/SubtitleSearchUtils.kt | 32 +++--- .../ui/playback/TrackSelectionUtils.kt | 6 + .../damontecres/wholphin/util/BlockingList.kt | 12 ++ .../wholphin/util/CrashReportSenderFactory.kt | 3 + .../damontecres/wholphin/util/DebugLogTree.kt | 3 + .../damontecres/wholphin/util/FocusPair.kt | 9 -- .../wholphin/util/RememberTabManager.kt | 3 + .../wholphin/util/TransformList.kt | 3 + .../damontecres/wholphin/util/Version.kt | 3 + app/src/patches/play_store.patch | 30 +++-- 92 files changed, 719 insertions(+), 222 deletions(-) rename app/src/main/java/com/github/damontecres/wholphin/data/model/{ServerPreferences.kt => NavDrawerPinnedItem.kt} (85%) delete mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayerLoadingState.kt delete mode 100644 app/src/main/java/com/github/damontecres/wholphin/util/FocusPair.kt diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6856ebb2..390b2d63 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -14,6 +14,7 @@ The app uses: * [Room](https://developer.android.com/training/data-storage/room) & [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) for local data storage * [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) for dependency injection * [Media3/ExoPlayer](https://developer.android.com/media/media3/exoplayer) for media playback +* [MPV/libmpv](https://github.com/mpv-player/mpv) for media playback * [Coil](https://coil-kt.github.io/coil/) for image loading * [OkHttp](https://square.github.io/okhttp/) for HTTP requests diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt index 585e1079..ede44e08 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt @@ -8,12 +8,18 @@ import com.github.damontecres.wholphin.ui.nav.Destination import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.api.ExtraType +/** + * Represents "extras" for media such as behind-the-scenes or deleted scenes + */ sealed interface ExtrasItem { val parentId: UUID val type: ExtraType val destination: Destination val title: String? + /** + * Represents multiple extras of the same type + */ data class Group( override val parentId: UUID, override val type: ExtraType, @@ -25,6 +31,9 @@ sealed interface ExtrasItem { override val title: String? = null } + /** + * Represents a single extra + */ data class Single( override val parentId: UUID, override val type: ExtraType, @@ -38,6 +47,9 @@ sealed interface ExtrasItem { } } +/** + * Converts [ExtraType] to the string resource ID + */ @get:StringRes val ExtraType.stringRes: Int get() = @@ -56,6 +68,9 @@ val ExtraType.stringRes: Int ExtraType.SHORT -> R.string.shorts } +/** + * Converts [ExtraType] to the plural resource ID + */ @get:PluralsRes val ExtraType.pluralRes: Int get() = diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/filter/ItemFilterBy.kt b/app/src/main/java/com/github/damontecres/wholphin/data/filter/ItemFilterBy.kt index fec0fb8d..1d536f28 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/filter/ItemFilterBy.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/filter/ItemFilterBy.kt @@ -50,6 +50,11 @@ val DefaultPlaylistItemsOptions = DecadeFilter, ) +/** + * A way to filter libraries + * + * Gets and sets values within a [GetItemsFilter] + */ sealed interface ItemFilterBy { @get:StringRes val stringRes: Int diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt index 49b3c694..9c8009db 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt @@ -5,6 +5,14 @@ import org.jellyfin.sdk.model.extensions.ticks import java.util.UUID import kotlin.time.Duration +/** + * Represents audio or a song as a stripped down [BaseItem] since there may be a lot of these created. + * + * Typically added to a MediaItem as the tag for reference later + * + * The "key" can be used by a Compose LazyList key function as it will uniquely identify this particular + * audio even if the same song is added to the queue multiple times + */ @Stable data class AudioItem( val key: Long = keyTracker++, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index 83683976..cf7461ea 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -29,6 +29,9 @@ import java.util.Locale import java.util.UUID import kotlin.time.Duration +/** + * Wrapper for [BaseItemDto] with shortcuts for various UI elements + */ @Serializable @Stable data class BaseItem( @@ -92,6 +95,9 @@ data class BaseItem( @Transient val timeRemainingOrRuntime: Duration? = data.timeRemaining ?: data.runTimeTicks?.ticks + /** + * Contains pre computed UI elements that would be expensive to create on the main thread + */ @Transient val ui = BaseItemUi( @@ -178,6 +184,9 @@ data class BaseItem( it.dayOfMonth.toString().padStart(2, '0') }?.toIntOrNull() + /** + * Convert this [BaseItem] into a [Destination] to navigate to its page in the app + */ fun destination(index: Int? = null): Destination { if (destinationOverride != null) return destinationOverride val result = @@ -228,6 +237,7 @@ data class BaseItem( } companion object { + @Deprecated("Use regular constructor instead") fun from( dto: BaseItemDto, api: ApiClient, @@ -249,6 +259,9 @@ data class BaseItemUi( val quickDetails: AnnotatedString, ) +/** + * Create the special [Destination.FilteredCollection] for the given genre information + */ fun createGenreDestination( genreId: UUID, genreName: String, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt index 3444ac54..3e07964c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt @@ -7,6 +7,9 @@ import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.extensions.ticks import kotlin.time.Duration +/** + * Represents a chapter within a video + */ data class Chapter( val name: String?, val position: Duration, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt index 0cf88ff1..db03ff48 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt @@ -16,6 +16,9 @@ import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.time.LocalDate import java.util.UUID +/** + * The type of a Seerr/Discover object with mapping to the Jellyfin [BaseItemKind] + */ @Serializable enum class SeerrItemType( val baseItemKind: BaseItemKind?, @@ -46,6 +49,9 @@ enum class SeerrItemType( } } +/** + * How available is a particular discovered item within the Jellyfin server + */ @Serializable enum class SeerrAvailability( val status: Int, @@ -64,7 +70,7 @@ enum class SeerrAvailability( } /** - * An item provided by a discovery service (ie Seerr). It may exist on the JF server as well. + * An item provided by a discovery service (ie Seerr). It may exist on the JF server as well, see [availability]. */ @Stable @Serializable @@ -104,6 +110,9 @@ data class DiscoverItem( } } +/** + * A rating for a discovered item which is usually fetched separately from the item + */ data class DiscoverRating( val criticRating: Int?, val audienceRating: Float?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt index f819c7c3..d0d32988 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt @@ -14,6 +14,9 @@ import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Filter for a collection folder + */ @Serializable data class CollectionFolderFilter( val nameOverride: String? = null, @@ -24,6 +27,9 @@ data class CollectionFolderFilter( val useSavedLibraryDisplayInfo: Boolean = true, ) +/** + * A sort of simplified filter which can be [applyTo] a [GetItemsRequest] or [GetPersonsRequest] to add or remove filters + */ @Serializable data class GetItemsFilter( val favorite: Boolean? = null, @@ -52,7 +58,7 @@ data class GetItemsFilter( } /** - * Clear all of the values for the given filters + * Clear all the values for the given filters */ fun delete(filterOptions: List>): GetItemsFilter { var newFilter = this @@ -128,6 +134,9 @@ data class GetItemsFilter( isFavorite = favorite, ) + /** + * Merge another [GetItemsFilter] onto this one, replacing only unset values + */ fun merge(filter: GetItemsFilter): GetItemsFilter = this.copy( favorite = favorite ?: filter.favorite, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt index 94a30475..b31261fd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt @@ -13,6 +13,10 @@ import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Store the media source and audio/subtitle tracks chosen for a specific media item + * + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemTrackModification.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemTrackModification.kt index e8bbfeb4..3888e438 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemTrackModification.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemTrackModification.kt @@ -9,6 +9,11 @@ import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Store modifications to an audio/subtitle track in a media item + * + * For example, the subtitle delay + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt index 2c516272..0b1759ee 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt @@ -17,6 +17,9 @@ import org.jellyfin.sdk.model.ServerVersion import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Represents a Jellyfin server + */ @Entity(tableName = "servers") @Serializable data class JellyfinServer( @@ -29,6 +32,9 @@ data class JellyfinServer( val serverVersion: ServerVersion? by lazy { version?.let(ServerVersion::fromString) } } +/** + * Represents a Jellyfin user for a particular server + */ @Entity( tableName = "users", foreignKeys = [ @@ -59,6 +65,9 @@ data class JellyfinUser( "JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()})" } +/** + * Represents the relationship between [JellyfinServer] and its [JellyfinUser] + */ data class JellyfinServerUsers( @Embedded val server: JellyfinServer, @Relation( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt index e5bcafcb..edfc797a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt @@ -18,6 +18,11 @@ import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Stores the filter, sort, and view options a user changes for a library + * + * This allows for restoring these settings whenever the user navigates to the library + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/NavDrawerPinnedItem.kt similarity index 85% rename from app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt rename to app/src/main/java/com/github/damontecres/wholphin/data/model/NavDrawerPinnedItem.kt index 2b503023..e312a2b1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/NavDrawerPinnedItem.kt @@ -9,6 +9,9 @@ enum class NavPinType { UNPINNED, } +/** + * Stores preference information about nav drawer items such as its order and whether to show or put in More + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt index ac4a4710..4e639009 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt @@ -12,6 +12,9 @@ import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.PersonKind +/** + * Represents a person in some media such as an actor or director + */ @Stable data class Person( val id: UUID, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackEffect.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackEffect.kt index 3c5cf6a0..0e7bcd37 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackEffect.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackEffect.kt @@ -5,6 +5,9 @@ import androidx.room.Entity import org.jellyfin.sdk.model.api.BaseItemKind import java.util.UUID +/** + * Store effects applied to some media such as color or hue adjustments + */ @Entity(tableName = "playback_effects", primaryKeys = ["jellyfinUserRowId", "itemId", "type"]) data class PlaybackEffect( val jellyfinUserRowId: Int, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt index f452e2f1..fb54dfcd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt @@ -9,6 +9,10 @@ import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Stores the language choices for a series so they can be applied automatically to other episodes + * without the user needing to explicitly choose the tracks + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt index c4cd3792..698a5f34 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt @@ -6,6 +6,11 @@ import androidx.compose.runtime.setValue import org.jellyfin.sdk.model.api.MediaType import java.util.UUID +/** + * Tracks playback of multiple items. Points to the current media with function to advance or go to previous ones. + * + * This is not the same thing as a Jellyfin server playlist + */ class Playlist( items: List, startIndex: Int = 0, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt index 1e9f963d..a6886cf2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt @@ -2,6 +2,9 @@ package com.github.damontecres.wholphin.data.model import com.github.damontecres.wholphin.services.SeerrUserConfig +/** + * Permission levels for a user on a Seerr server + */ enum class SeerrPermission( private val flag: Int, ) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt index e29305d2..311379e4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt @@ -13,6 +13,9 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.serializer.UUIDSerializer +/** + * Represents a Seerr server instance + */ @Entity( tableName = "seerr_servers", indices = [Index("url", unique = true)], @@ -26,6 +29,9 @@ data class SeerrServer( val version: String? = null, ) +/** + * Represents a user on a [SeerrServer] + */ @Entity( tableName = "seerr_users", foreignKeys = [ @@ -56,12 +62,18 @@ data class SeerrUser( "SeerrUser(jellyfinUserRowId=$jellyfinUserRowId, serverId=$serverId, authMethod=$authMethod, username=$username, password?=${password.isNotNullOrBlank()}, credential?=${credential.isNotNullOrBlank()})" } +/** + * The method used to authenticate a user to the server + */ enum class SeerrAuthMethod { LOCAL, JELLYFIN, API_KEY, } +/** + * Represents the relationship between a [SeerrServer] and its [SeerrUser]s + */ data class SeerrServerUsers( @Embedded val server: SeerrServer, @Relation( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt index 943789db..01e473b2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt @@ -1,9 +1,15 @@ package com.github.damontecres.wholphin.data.model +/** + * Represents a trailer for media + */ sealed interface Trailer { val name: String } +/** + * A [Trailer] stored on the Jellyfin server + */ data class LocalTrailer( val baseItem: BaseItem, ) : Trailer { @@ -11,6 +17,9 @@ data class LocalTrailer( get() = baseItem.name ?: "" } +/** + * A [Trailer] available via a remote URL, such as YouTube + */ data class RemoteTrailer( override val name: String, val url: String, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/VideoFilter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/VideoFilter.kt index e71db95c..93543e23 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/VideoFilter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/VideoFilter.kt @@ -14,7 +14,7 @@ import androidx.media3.effect.ScaleAndRotateTransformation import androidx.room.Ignore /** - * Modifications to a video playback + * Modifications to an image or video playback */ data class VideoFilter( val rotation: Int = 0, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 07b5fc7e..c1507d9f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -32,6 +32,9 @@ import java.io.File import javax.inject.Inject import javax.inject.Singleton +/** + * Handles any changes needed when the app in upgraded on the device such as setting new preferences + */ @Singleton class AppUpgradeHandler @Inject @@ -61,6 +64,7 @@ class AppUpgradeHandler Timber.i( "App updated: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode", ) + // Store the previous and new version info prefs.edit(true) { putString(VERSION_NAME_PREVIOUS_KEY, previousVersion) putLong(VERSION_CODE_PREVIOUS_KEY, previousVersionCode) @@ -83,6 +87,9 @@ class AppUpgradeHandler } } + /** + * Copies the font file used by MPV subtitles to the app's files directory + */ fun copySubfont(overwrite: Boolean) { try { val fontFileName = "subfont.ttf" @@ -111,6 +118,9 @@ class AppUpgradeHandler const val VERSION_CODE_CURRENT_KEY = "version.current.code" } + /** + * Perform any needed upgrades + */ suspend fun upgradeApp( previous: Version, current: Version, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt index 832d4137..1bf34929 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt @@ -32,6 +32,11 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +/** + * Stores state for the backdrop of the app shown on non-full screen pages + * + * This is usually the backdrop of the currently focused media + */ @Singleton @OptIn(FlowPreview::class) class BackdropService @@ -46,6 +51,9 @@ class BackdropService private val _backdropFlow = MutableStateFlow(BackdropResult.NONE) val backdropFlow = _backdropFlow + /** + * Update the backdrop to use the specified item + */ suspend fun submit(item: BaseItem) = withContext(Dispatchers.IO) { val imageUrl = @@ -57,8 +65,14 @@ class BackdropService submit(item.id.toString(), imageUrl) } + /** + * Update the backdrop to use the specified discovered item + */ suspend fun submit(item: DiscoverItem) = submit("discover_${item.id}", item.backDropUrl) + /** + * Update the backdrop to use the specified URL + */ suspend fun submit( itemId: String, imageUrl: String?, @@ -74,6 +88,9 @@ class BackdropService } } + /** + * Remove the backdrop, such as when switching pages + */ suspend fun clearBackdrop() { _backdropFlow.update { BackdropResult.NONE @@ -224,6 +241,9 @@ class BackdropService } } +/** + * The result from determining the backdrop URL and extracted colors for the dynamic backdrop + */ data class BackdropResult( val itemId: String?, val imageUrl: String?, @@ -245,6 +265,9 @@ data class BackdropResult( } } +/** + * The colors extracted from an image + */ data class ExtractedColors( val primary: Color, val secondary: Color, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt index 205e12bf..9c4cfa5c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt @@ -27,6 +27,9 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton +/** + * Caches when media was lasted played. This is mostly used by the combined Continue Watching & Next Up home row. + */ @Singleton class DatePlayedService @Inject @@ -80,6 +83,12 @@ class DatePlayedService } } + /** + * Get the logical timestamp when the item was last played. + * + * This is calculated using lastest of the actual last played timestamp of the item, + * the previous episode's last played timestamp, and the episode's premiere date + */ suspend fun getLastPlayed(item: BaseItem): LocalDateTime? = withContext(Dispatchers.IO) { val seriesId = item.data.seriesId @@ -93,6 +102,11 @@ class DatePlayedService } } + /** + * Remove the cached last date played for the given item's series + * + * Used for when a user plays this item + */ fun invalidate(item: BaseItem) { item.data.seriesId?.let { seriesId -> Timber.d("Invalidating %s", seriesId) @@ -100,6 +114,9 @@ class DatePlayedService } } + /** + * Remove the cached last data played for the given item or its series + */ suspend fun invalidate(itemId: UUID) { val seriesId = api.userLibraryApi.getItem(itemId = itemId).content.let { @@ -121,6 +138,9 @@ class DatePlayedService } } +/** + * Invalidates the date played cached when the current user changes + */ @ActivityScoped class DatePlayedInvalidationService @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt index 976fb2c8..a001eb6a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt @@ -14,6 +14,9 @@ import org.jellyfin.sdk.model.api.DeviceProfile import javax.inject.Inject import javax.inject.Singleton +/** + * Creates and caches the device direct play/transcoding profile sent to the server for ExoPlayer + */ @Singleton class DeviceProfileService @Inject @@ -21,7 +24,7 @@ class DeviceProfileService @param:ApplicationContext private val context: Context, ) { val mediaCodecCapabilitiesTest by lazy { - // Created lazily below on the IO thread since it cn take time + // Created lazily below on another thread since it cn take time MediaCodecCapabilitiesTest(context) } private val mutex = Mutex() @@ -33,7 +36,7 @@ class DeviceProfileService prefs: PlaybackPreferences, serverVersion: ServerVersion?, ): DeviceProfile = - withContext(Dispatchers.IO) { + withContext(Dispatchers.Default) { mutex.withLock { val newConfig = DeviceProfileConfiguration( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt index e952177b..65b74083 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt @@ -9,12 +9,20 @@ import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +/** + * Get extras for media + * + * @see [ExtrasItem] + */ @Singleton class ExtrasService @Inject constructor( private val api: ApiClient, ) { + /** + * Get the [ExtrasItem]s for the given item + */ suspend fun getExtras(itemId: UUID): List { val extrasMap = api.userLibraryApi @@ -42,6 +50,9 @@ class ExtrasService } } +/** + * The order which extras should be shown + */ private val ExtraType.sortOrder: Int get() = when (this) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt b/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt index a68a6fe5..cdaa0db4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt @@ -8,6 +8,9 @@ import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +/** + * Handles toggling media as favorited or watched + */ @Singleton class FavoriteWatchManager @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index 0b3b0887..2abe6a1e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -63,6 +63,9 @@ import java.io.File import javax.inject.Inject import javax.inject.Singleton +/** + * Handles getting home page settings and data + */ @Singleton class HomeSettingsService @Inject @@ -84,6 +87,9 @@ class HomeSettingsService allowTrailingComma = true } + /** + * The current home page settings + */ val currentSettings = MutableStateFlow(HomePageResolvedSettings.EMPTY) /** @@ -245,6 +251,9 @@ class HomeSettingsService currentSettings.update { resolvedSettings } } + /** + * Resolve the settings and set them to be the current settings + */ suspend fun updateCurrent(settings: HomePageSettings) { val resolvedRows = settings.rows.mapIndexed { index, config -> @@ -317,6 +326,9 @@ class HomeSettingsService return HomePageResolvedSettings(rowConfig) } + /** + * Create home page settings from the user's web UI home page settings + */ suspend fun parseFromWebConfig(userId: UUID): HomePageResolvedSettings? { val customPrefs = api.displayPreferencesApi diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt index f6e02df2..ce945b69 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt @@ -1,10 +1,8 @@ package com.github.damontecres.wholphin.services import android.content.Context -import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.SlimItemFields -import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.supportItemKinds import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -16,12 +14,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi -import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.api.client.extensions.userViewsApi import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.UserDto -import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import org.jellyfin.sdk.model.api.request.GetNextUpRequest import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest import timber.log.Timber @@ -31,6 +24,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds +/** + * Get continue watching and next up items for users + */ @Singleton class LatestNextUpService @Inject @@ -39,6 +35,9 @@ class LatestNextUpService private val api: ApiClient, private val datePlayedService: DatePlayedService, ) { + /** + * Get resume (continue watching) items for a user + */ suspend fun getResume( userId: UUID, limit: Int, @@ -60,13 +59,6 @@ class LatestNextUpService remove(BaseItemKind.EPISODE) } }, -// enableImageTypes = -// listOf( -// ImageType.PRIMARY, -// ImageType.THUMB, -// ImageType.BACKDROP, -// ImageType.LOGO, -// ), ) val items = api.itemsApi @@ -77,6 +69,9 @@ class LatestNextUpService return items } + /** + * Get next up items for a user + */ suspend fun getNextUp( userId: UUID, limit: Int, @@ -98,13 +93,6 @@ class LatestNextUpService enableUserData = true, enableRewatching = enableRewatching, nextUpDateCutoff = nextUpDateCutoff, -// enableImageTypes = -// listOf( -// ImageType.PRIMARY, -// ImageType.THUMB, -// ImageType.BACKDROP, -// ImageType.LOGO, -// ), ) val nextUp = api.tvShowsApi @@ -115,65 +103,11 @@ class LatestNextUpService return nextUp } - suspend fun getLatest( - user: UserDto, - limit: Int, - includedIds: List, - ): List { - val excluded = user.configuration?.latestItemsExcludes.orEmpty() - val views by api.userViewsApi.getUserViews() - val latestData = - views.items - .filter { - it.id in includedIds && it.id !in excluded && - it.collectionType in supportedLatestCollectionTypes - }.map { view -> - val title = - view.name?.let { context.getString(R.string.recently_added_in, it) } - ?: context.getString(R.string.recently_added) - val request = - GetLatestMediaRequest( - fields = SlimItemFields, - imageTypeLimit = 1, - parentId = view.id, - groupItems = true, - limit = limit, - isPlayed = null, // Server will handle user's preference - ) - LatestData(title, request) - } - - return latestData - } - - suspend fun loadLatest(latestData: List): List { - val rows = - latestData.mapNotNull { (title, request) -> - try { - val latest = - api.userLibraryApi - .getLatestMedia(request) - .content - .map { BaseItem.from(it, api, true) } - if (latest.isNotEmpty()) { - HomeRowLoadingState.Success( - title = title, - items = latest, - ) - } else { - null - } - } catch (ex: Exception) { - Timber.e(ex, "Exception fetching %s", title) - HomeRowLoadingState.Error( - title = title, - exception = ex, - ) - } - } - return rows - } - + /** + * Create the combined Continue Watching & Next Up items + * + * @see [DatePlayedService] + */ suspend fun buildCombined( resume: List, nextUp: List, @@ -207,17 +141,3 @@ class LatestNextUpService return@withContext result } } - -val supportedLatestCollectionTypes = - setOf( - CollectionType.MOVIES, - CollectionType.TVSHOWS, - CollectionType.HOMEVIDEOS, - // Exclude Live TV because a recording folder view will be used instead - null, // Recordings & mixed collection types - ) - -data class LatestData( - val title: String, - val request: GetLatestMediaRequest, -) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MediaManagementService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MediaManagementService.kt index a13881a9..beab6e64 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MediaManagementService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MediaManagementService.kt @@ -19,6 +19,9 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +/** + * Service to manage media such as deletions + */ @Singleton class MediaManagementService @Inject @@ -45,6 +48,9 @@ class MediaManagementService return canDelete(item, appPreferences) } + /** + * Check if the item can be deleted. This means the app setting is enabled and the user has permission. + */ fun canDelete( item: BaseItem, appPreferences: AppPreferences, @@ -62,10 +68,14 @@ class MediaManagementService } } + /** + * Delete the item. + * + * This item will be sent through [deletedItemFlow] for other services or view models to react. + */ suspend fun deleteItem(item: BaseItem): DeleteResult { try { Timber.i("Deleting %s", item.id) - // TODO enable api.libraryApi.deleteItem(item.id) _deletedItemFlow.emit(DeletedItem(item)) return DeleteResult.Success @@ -88,6 +98,9 @@ sealed interface DeleteResult { ) : DeleteResult } +/** + * Convenience function to delete an item and show a Toast based on success or error + */ fun ViewModel.deleteItem( context: Context, mediaManagementService: MediaManagementService, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MediaReportService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MediaReportService.kt index 14a93ba2..d1a1543a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MediaReportService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MediaReportService.kt @@ -21,6 +21,9 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +/** + * Send media info to the server + */ @Singleton class MediaReportService @Inject @@ -39,6 +42,9 @@ class MediaReportService encodeDefaults = false } + /** + * Fetch the media info and send it to the server + */ fun sendReportFor(itemId: UUID) { ioScope.launchIO(ExceptionHandler(autoToast = true)) { val item = api.userLibraryApi.getItem(itemId = itemId).content @@ -46,6 +52,9 @@ class MediaReportService } } + /** + * Send the media report for the given item + */ suspend fun sendReportFor(item: BaseItemDto) { val sources = item.mediaSources ?: api.userLibraryApi diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt index be3ceac7..002be20e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt @@ -63,6 +63,11 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.seconds +/** + * Manage the global state for playing music + * + * Has functions for modifying the queue + */ @OptIn(UnstableApi::class) @Singleton class MusicService @@ -104,6 +109,11 @@ class MusicService private var activityTracker: TrackActivityPlaybackListener? = null private var websocketJob: Job? = null + /** + * Start music playback + * + * Sets up the media session, activity tracking, and actual playback + */ suspend fun start() { if (mediaSession == null) { mutex.withLock { @@ -130,6 +140,9 @@ class MusicService } } + /** + * Stop music playback + */ suspend fun stop() { mutex.withLock { Timber.i("Stopping music") @@ -191,6 +204,9 @@ class MusicService addAllToQueue(items, startIndex) } + /** + * Replace the queue with the given items + */ suspend fun setQueue( items: List, shuffled: Boolean, @@ -208,6 +224,9 @@ class MusicService } } + /** + * Add an item to the specified index of the queue. If no index specified, it will be added to the end. + */ suspend fun addToQueue( item: BaseItem, index: Int? = null, @@ -229,6 +248,11 @@ class MusicService } } + /** + * Add all the items in teh list to end of the queue + * + * @param startIndex The index to start from within the source list + */ suspend fun addAllToQueue( list: BlockingList, startIndex: Int, @@ -255,6 +279,9 @@ class MusicService updateQueueSize() } + /** + * Converts a [BaseItem] into a [MediaItem] setting an [AudioItem] as its tag + */ private fun convert(audio: BaseItem): MediaItem { val url = api.universalAudioApi.getUniversalAudioStreamUrl( @@ -282,6 +309,9 @@ class MusicService .build() } + /** + * Updates the state for when the queue changes + */ private suspend fun updateQueueSize() { // val ids = // withContext(Dispatchers.Default) { @@ -306,6 +336,9 @@ class MusicService } } + /** + * Move an item within the queue + */ suspend fun moveQueue( index: Int, direction: MoveDirection, @@ -314,6 +347,9 @@ class MusicService updateQueueSize() } + /** + * Move an item within the queue + */ suspend fun moveQueue( index: Int, newIndex: Int, @@ -322,6 +358,9 @@ class MusicService updateQueueSize() } + /** + * Start playback at the given index of the queue + */ suspend fun playIndex(index: Int) { onMain { player.seekTo(index, 0L) @@ -330,6 +369,9 @@ class MusicService // MusicPlayerListener will update state } + /** + * Play this item next after the current, ie add the item as the next index in the queue + */ suspend fun playNext(song: BaseItem) { val mediaItem = convert(song) onMain { @@ -341,6 +383,9 @@ class MusicService updateQueueSize() } + /** + * From the item at the given index from the queue + */ suspend fun removeFromQueue(index: Int) { onMain { player.removeMediaItem(index) } updateQueueSize() @@ -353,7 +398,10 @@ class MusicService return result } - fun subscribe(): Job = + /** + * Subscribes to the server websocket to receive playback commands + */ + private fun subscribe(): Job = api.webSocket .subscribe() .onEach { message -> @@ -503,6 +551,11 @@ private class MusicPlayerListener( } } +/** + * Remember the queue currently playing + * + * @see MusicServiceState + */ @Composable fun rememberQueue( player: Player, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/NavDrawerService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/NavDrawerService.kt index 8dbf49ee..68486430 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/NavDrawerService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/NavDrawerService.kt @@ -38,6 +38,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.hours +/** + * Gets the items to show in the nav drawer + */ @Singleton class NavDrawerService @Inject @@ -54,6 +57,7 @@ class NavDrawerService val state: StateFlow = _state init { + // Handle updating the nav drawer when the user changes serverRepository.currentUser .asFlow() .combine(serverRepository.currentUserDto.asFlow()) { user, userDto -> @@ -74,10 +78,13 @@ class NavDrawerService showToast(context, "Error fetching user's views") }.launchIn(coroutineScope) + // Handle when the user has logged into a Seerr server seerrServerRepository.active .onEach { discoverActive -> _state.update { it.copy(discoverEnabled = discoverActive) } }.launchIn(coroutineScope) + + // Handle when music is actively playing or not coroutineScope.launchDefault { musicService.state.collectLatest { music -> Timber.v("MusicService updated") @@ -114,6 +121,9 @@ class NavDrawerService } } + /** + * Get all the libraries the user has access to + */ suspend fun getAllUserLibraries( userId: UUID, tvAccess: Boolean, @@ -147,6 +157,9 @@ class NavDrawerService return libraries } + /** + * Get the libraries that the user has not "pinned". These will show in the More section. + */ suspend fun getFilteredUserLibraries( user: JellyfinUser, tvAccess: Boolean, @@ -161,6 +174,9 @@ class NavDrawerService return libraries } + /** + * Update the current state of the nav drawer items + */ suspend fun updateNavDrawer( user: JellyfinUser, userDto: UserDto, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PeopleFavorites.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PeopleFavorites.kt index 6a35dda5..1df73d18 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PeopleFavorites.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PeopleFavorites.kt @@ -10,6 +10,9 @@ import org.jellyfin.sdk.api.client.extensions.itemsApi import javax.inject.Inject import javax.inject.Singleton +/** + * Gets people in media, specifically to check if they are favorited or not + */ @Singleton class PeopleFavorites @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt index b47ae33f..fbbc9b70 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt @@ -38,6 +38,11 @@ import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +/** + * Create [Playlist]s (not Jellyfin server playlist) for playback + * + * Used to create a queue of episodes or from Play All button + */ @Singleton class PlaylistCreator @Inject @@ -74,6 +79,9 @@ class PlaylistCreator return Playlist(episodes, startIndex) } + /** + * Create from a server playlist ID + */ suspend fun createFromPlaylistId( playlistId: UUID, startIndex: Int?, @@ -138,6 +146,11 @@ class PlaylistCreator return Playlist(items, 0) } + /** + * Create a [Playlist] contextually based on the given item. + * + * For example, an episode creates a queue of next up episodes in the series, or a movie creates one for its parts if needed + */ suspend fun createFrom( item: BaseItemDto, startIndex: Int = 0, @@ -275,6 +288,9 @@ class PlaylistCreator } } + /** + * Get the playlists on the server for a given media type + */ suspend fun getServerPlaylists( mediaType: MediaType?, scope: CoroutineScope, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt index 06d116a4..91df9518 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt @@ -22,6 +22,9 @@ import javax.inject.Singleton import kotlin.math.roundToInt import kotlin.time.Duration.Companion.seconds +/** + * Handles determining whether the refresh rate and/or resolution of the display need to changed when playing media + */ @Singleton class RefreshRateService @Inject @@ -119,6 +122,9 @@ class RefreshRateService MainActivity.instance.changeDisplayMode(0) } + /** + * Listens for the display to change so we known the refresh rate or resolution is updated + */ private class Listener( val displayId: Int, ) : DisplayManager.DisplayListener { @@ -213,6 +219,9 @@ class RefreshRateService } } +/** + * Wrapper for a [Display.Mode] + */ data class DisplayMode( val modeId: Int, val physicalWidth: Int, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt index c81893b3..6252a2a7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt @@ -38,6 +38,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds +/** + * Handles the queue of items to show on the screensaver, both in-app or OS + */ @Singleton class ScreensaverService @Inject @@ -67,6 +70,9 @@ class ScreensaverService }.launchIn(scope) } + /** + * Reset the timer before showing the in-app screensaver + */ fun pulse() { waitJob?.cancel() if (_state.value.enabled) { @@ -95,6 +101,9 @@ class ScreensaverService } } + /** + * Immediately start the in-app screensaver + */ fun start() { _state.update { it.copy( @@ -104,6 +113,9 @@ class ScreensaverService } } + /** + * Immediately stop the in-app screensaver + */ fun stop(cancelJob: Boolean) { _state.update { it.copy( @@ -114,6 +126,9 @@ class ScreensaverService if (cancelJob) waitJob?.cancel() } + /** + * Signal to the OS for keeping the screen on such as during playback or when the in-app screensaver is active + */ fun keepScreenOn(keep: Boolean) { scope.launchDefault { val screensaverEnabled = _state.value.enabled @@ -136,6 +151,9 @@ class ScreensaverService keepScreenOn.update { keep } } + /** + * Create a flow of items to show on the screensaver + */ fun createItemFlow(scope: CoroutineScope): Flow = flow { val prefs = diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt index f71c902a..979c90bd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt @@ -25,6 +25,9 @@ import org.jellyfin.sdk.model.api.MediaType import timber.log.Timber import javax.inject.Inject +/** + * Listens for basic messages from the server such as messages + */ @ActivityScoped class ServerEventListener @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt index 6de45f50..05eac34e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt @@ -20,6 +20,9 @@ import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +/** + * Manage the track choices for media + */ @Singleton class StreamChoiceService @Inject @@ -94,6 +97,9 @@ class StreamChoiceService result } + /** + * Returns the audio stream that should play + */ suspend fun chooseAudioStream( source: MediaSourceInfo, seriesId: UUID?, @@ -108,6 +114,9 @@ class StreamChoiceService } } + /** + * Returns the audio stream that should play + */ fun chooseAudioStream( candidates: List, itemPlayback: ItemPlayback?, @@ -135,6 +144,9 @@ class StreamChoiceService } } + /** + * Returns the subtitle stream that should play + */ suspend fun chooseSubtitleStream( source: MediaSourceInfo, audioStream: MediaStream?, @@ -196,6 +208,9 @@ class StreamChoiceService )?.index } + /** + * Returns the subtitle stream that should play + */ fun chooseSubtitleStream( audioStreamLang: String?, candidates: List, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt index 3f676cba..6be37807 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt @@ -15,6 +15,9 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi import javax.inject.Inject import javax.inject.Singleton +/** + * Gets trailers for media + */ @Singleton class TrailerService @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt index 709549cf..9e0665dd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt @@ -45,6 +45,9 @@ import javax.inject.Singleton import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds +/** + * Checks if an app update is available + */ @Singleton class UpdateChecker @Inject @@ -65,6 +68,11 @@ class UpdateChecker val ACTIVE = true } + /** + * If the app hasn't recently checked, check for any updates and if there is one, show a toast message + * + * This is safe to call many times because it will only show the toast at most once every 12 hours + */ suspend fun maybeShowUpdateToast( updateUrl: String, showNegativeToast: Boolean = false, @@ -112,11 +120,17 @@ class UpdateChecker } } + /** + * Get the currently installed version + */ fun getInstalledVersion(): Version { val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0) - return Version.Companion.fromString(pkgInfo.versionName!!) + return Version.fromString(pkgInfo.versionName!!) } + /** + * Get the latest released version + */ suspend fun getLatestRelease(updateUrl: String): Release? { return withContext(Dispatchers.IO) { val request = @@ -161,6 +175,9 @@ class UpdateChecker } } + /** + * Download and install an update + */ suspend fun installRelease( release: Release, callback: DownloadCallback, @@ -263,6 +280,9 @@ class UpdateChecker return targetFile } + /** + * Check if the app has permission to write the download + */ fun hasPermissions(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || ( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UserPreferencesService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UserPreferencesService.kt index 67e940b5..7c7fce11 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UserPreferencesService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UserPreferencesService.kt @@ -9,6 +9,9 @@ import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton +/** + * Get the current user's [UserPreferences] + */ @Singleton class UserPreferencesService @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt index a44ab65b..38071cac 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt @@ -34,26 +34,48 @@ import org.jellyfin.sdk.model.DeviceInfo import javax.inject.Qualifier import javax.inject.Singleton +/** + * An [OkHttpClient] that includes the user's access token when making requests + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class AuthOkHttpClient +/** + * A basic [OkHttpClient] that does not include auth + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class StandardOkHttpClient +/** + * A [CoroutineScope] with [Dispatchers.IO] + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoCoroutineScope +/** + * A [CoroutineScope] with [Dispatchers.Default] + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class DefaultCoroutineScope +/** + * [Dispatchers.IO] + * + * @see IoCoroutineScope + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoDispatcher +/** + * [Dispatchers.Default] + * + * @see DefaultCoroutineScope + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class DefaultDispatcher diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderSchedulerService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderSchedulerService.kt index 297cbd64..fb40c008 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderSchedulerService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderSchedulerService.kt @@ -24,6 +24,9 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration +/** + * Schedules the [TvProviderWorker] to update the OS resume watching row + */ @ActivityScoped class TvProviderSchedulerService @Inject @@ -44,6 +47,7 @@ class TvProviderSchedulerService workManager.cancelUniqueWork(TvProviderWorker.WORK_NAME) if (supportsTvProvider) { if (user != null) { + // Schedule a new worker whenever the user changes activity.lifecycleScope.launchIO(ExceptionHandler()) { Timber.i("Scheduling TvProviderWorker for ${user.user}") workManager @@ -70,6 +74,9 @@ class TvProviderSchedulerService } } + /** + * Run the [TvProviderWorker] as a one-off instead of scheduled + */ fun launchOneTimeRefresh() { if (supportsTvProvider) { activity.lifecycleScope.launchIO(ExceptionHandler()) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt index 03ea0478..5a21d0d2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt @@ -43,6 +43,11 @@ import java.util.Date import java.util.UUID import kotlin.time.Duration.Companion.minutes +/** + * Updates the Android OS resume watching row for a given Jellyfin user + * + * @see TvProviderSchedulerService + */ @HiltWorker class TvProviderWorker @AssistedInject diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Button.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Button.kt index c65069bd..ff85886d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Button.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Button.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index 82aab6ba..37de91ce 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt index a85d9fc9..47b4ebc9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt @@ -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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/FilterByButton.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/FilterByButton.kt index a218b9c7..95926eed 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/FilterByButton.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/FilterByButton.kt @@ -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>, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt index d42eea5c..e85b1d02 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt index 3598fb93..c697b667 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt index 38591a67..13ce42a7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt index 67c56f12..6a2300c2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt @@ -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() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/OverviewText.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/OverviewText.kt index 7417028f..874585de 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/OverviewText.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/OverviewText.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt index db40ff0e..5eb1a210 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SelectedLeadingContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SelectedLeadingContent.kt index efc2ddac..d06fd652 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SelectedLeadingContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SelectedLeadingContent.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt index 64cfc78b..f87f5c98 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt index f5791e04..f70a4f6a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt index e4e6d350..74c00328 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt index 9e2141b5..5c4721c5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt index 072de4e1..5cf0789e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt @@ -42,6 +42,9 @@ data class ItemDetailsDialogInfo( val files: List, ) +/** + * Dialog showing metadata about an item + */ @Composable fun ItemDetailsDialog( info: ItemDetailsDialogInfo, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index a2b9e923..41f5e534 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -85,6 +85,9 @@ interface CardGridItem { val sortName: String } +/** + * Shows a vertical grid of [CardGridItem]s + */ @OptIn(ExperimentalFoundationApi::class) @Composable fun CardGrid( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt index b4618eda..080c1c4e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index bd50ce59..d50b7af5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt index 4276330d..ce3fbd5b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt @@ -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, - 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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt index 21520447..81028501 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index 6e2a2260..26068182 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationDrawerAndroid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationDrawerAndroid.kt index 30b9d1d6..0cca40c8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationDrawerAndroid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationDrawerAndroid.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt index e8dc7d1d..40b5e89b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt @@ -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?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt index 9c62f17b..b0f51e56 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt index e1a291e1..d4039f72 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt @@ -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) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt index 28f9b9d7..d43b6c27 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt @@ -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 BottomDialog( choices: List>, @@ -594,10 +605,6 @@ fun BottomDialog( } } -data class MoreButtonOptions( - val options: Map, -) - data class BottomDialogItem( val data: T, val headline: String, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt index a305023c..1968fac3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt index 3bc4ddb0..2b0247e7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt @@ -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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index cd39dbc2..3159c4dd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -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(null) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index f1be78dc..e9af15f1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -187,7 +187,7 @@ class PlaybackViewModel private var isPlaylist = false val playlist = MutableLiveData(Playlist(listOf())) - val subtitleSearch = MutableLiveData(null) + val subtitleSearchStatus = MutableLiveData(null) val subtitleSearchLanguage = MutableLiveData(Locale.current.language) val currentUserDto = serverRepository.currentUserDto diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayerLoadingState.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayerLoadingState.kt deleted file mode 100644 index c966a9aa..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayerLoadingState.kt +++ /dev/null @@ -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 { - 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 - } - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekBar.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekBar.kt index 92c6114b..37cb86f7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekBar.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekBar.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekPreviewImage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekPreviewImage.kt index 08fb7685..8be0b509 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekPreviewImage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekPreviewImage.kt @@ -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. diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt index 6444a268..d430258a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt @@ -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?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SkipIndicator.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SkipIndicator.kt index 3e417a91..c2344f5f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SkipIndicator.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SkipIndicator.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleDelay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleDelay.kt index 17792899..8c645c9b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleDelay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleDelay.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt index 021f4290..27188f57 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt @@ -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, - ) : 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 { 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 } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt index 201b5982..d056e6f3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt @@ -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.findExternalSubtitle(subtitleIndex: Int?): MediaStream? = } } +/** + * The result of [TrackSelectionUtils.createTrackSelections] + */ data class TrackSelectionResult( val trackSelectionParameters: TrackSelectionParameters, val audioSelected: Boolean, diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt b/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt index 8a6fc939..08207e53 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt @@ -3,12 +3,24 @@ package com.github.damontecres.wholphin.util import java.util.function.IntFunction import java.util.function.Predicate +/** + * A [List] which has function that will wait for a result + */ interface BlockingList : List { + /** + * Get the specified index, possibly blocking until it is available + */ suspend fun getBlocking(index: Int): T + /** + * Get first index that matches the given predicate, possibly blocking while searching + */ suspend fun indexOfBlocking(predicate: Predicate): Int companion object { + /** + * Create a [BlockingList] over a regular [List] + */ fun of(list: List): BlockingList = BlockingListWrapper(list) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt index c1630900..c334967f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt @@ -20,6 +20,9 @@ import org.json.JSONObject import timber.log.Timber import java.util.Date +/** + * Sends a crash report to the current server + */ @AutoService(ReportSenderFactory::class) class CrashReportSenderFactory : ReportSenderFactory { override fun create( diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/DebugLogTree.kt b/app/src/main/java/com/github/damontecres/wholphin/util/DebugLogTree.kt index 46b54466..b7f2e2c6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/DebugLogTree.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/DebugLogTree.kt @@ -4,6 +4,9 @@ import android.util.Log import com.github.damontecres.wholphin.BuildConfig import timber.log.Timber +/** + * Enable debug logging via [Timber] if enabled in the app settings + */ class DebugLogTree private constructor() : Timber.Tree() { // Only add logging for below INFO, production logger in WholphinApplication logs >=INFO override fun isLoggable( diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/FocusPair.kt b/app/src/main/java/com/github/damontecres/wholphin/util/FocusPair.kt deleted file mode 100644 index e44d531a..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/FocusPair.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.damontecres.wholphin.util - -import androidx.compose.ui.focus.FocusRequester - -data class FocusPair( - val row: Int, - val column: Int, - val focusRequester: FocusRequester, -) diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt index 1c69d947..46ec3606 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt @@ -3,6 +3,9 @@ package com.github.damontecres.wholphin.util import com.github.damontecres.wholphin.preferences.UserPreferences import java.util.UUID +/** + * Functions to remember tabs choices for a library + */ interface RememberTabManager { /** * If enabled, get the remembered tab index for the given item diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/TransformList.kt b/app/src/main/java/com/github/damontecres/wholphin/util/TransformList.kt index 61946d60..a0bfc08b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/TransformList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/TransformList.kt @@ -1,5 +1,8 @@ package com.github.damontecres.wholphin.util +/** + * A utility class to lazily transforms items from the source list + */ class TransformList( private val source: List, private val transform: (S) -> T, diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Version.kt b/app/src/main/java/com/github/damontecres/wholphin/util/Version.kt index 03303ac8..b670326b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/Version.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/Version.kt @@ -2,6 +2,9 @@ package com.github.damontecres.wholphin.util import kotlinx.serialization.Serializable +/** + * Represents a version in the format of `..--g` as output by `git describe` + */ @Serializable data class Version( val major: Int, diff --git a/app/src/patches/play_store.patch b/app/src/patches/play_store.patch index ef5fb96d..c1214f0e 100644 --- a/app/src/patches/play_store.patch +++ b/app/src/patches/play_store.patch @@ -1,14 +1,10 @@ -commit f82fb1a2d8ff9917a7bdb0bc7101a0474359ccdc -Author: Damontecres -Date: Sat Nov 22 13:00:55 2025 -0500 +# This patches Wholphin for releasing on app stores where self updating is not permitted - Setup for play store - -diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml -index 6d84299..12576af 100644 ---- a/app/src/main/AndroidManifest.xml -+++ b/app/src/main/AndroidManifest.xml -@@ -4,7 +4,6 @@ +diff --git i/app/src/main/AndroidManifest.xml w/app/src/main/AndroidManifest.xml +index 4479ae86..80596698 100644 +--- i/app/src/main/AndroidManifest.xml ++++ w/app/src/main/AndroidManifest.xml +@@ -6,7 +6,6 @@ @@ -16,7 +12,7 @@ index 6d84299..12576af 100644 -@@ -17,7 +16,7 @@ +@@ -20,7 +19,7 @@ android:required="false" /> -diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt -index c7ac435..fa42fe1 100644 ---- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt -+++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt -@@ -62,7 +62,7 @@ class UpdateChecker +diff --git i/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt w/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +index 9e0665dd..bc1e1c16 100644 +--- i/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt ++++ w/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +@@ -65,7 +65,7 @@ class UpdateChecker private val NOTE_REGEX = Regex("") @@ -37,4 +33,4 @@ index c7ac435..fa42fe1 100644 + val ACTIVE = false } - suspend fun maybeShowUpdateToast( + /**