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/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..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, @@ -282,7 +292,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 +301,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/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 b28dded4..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 @@ -886,7 +898,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..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,13 +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.ImageType -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 @@ -32,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 @@ -40,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, @@ -61,12 +59,6 @@ class LatestNextUpService remove(BaseItemKind.EPISODE) } }, - enableImageTypes = - listOf( - ImageType.PRIMARY, - ImageType.THUMB, - ImageType.BACKDROP, - ), ) 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, @@ -108,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, @@ -200,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 4231d9b9..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?, @@ -114,7 +122,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), @@ -133,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, @@ -270,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/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/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/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/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/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/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 ae7edb08..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, @@ -207,6 +211,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/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/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/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/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/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/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/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/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/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 5f1c9455..edea557d 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 @@ -175,7 +175,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 f12e80bf..b9e51bb9 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 @@ -188,7 +188,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/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 1245917b..ca8c32be 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 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( + /** diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d92707f..55c85740 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"