diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6856ebb2..390b2d63 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -14,6 +14,7 @@ The app uses: * [Room](https://developer.android.com/training/data-storage/room) & [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) for local data storage * [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) for dependency injection * [Media3/ExoPlayer](https://developer.android.com/media/media3/exoplayer) for media playback +* [MPV/libmpv](https://github.com/mpv-player/mpv) for media playback * [Coil](https://coil-kt.github.io/coil/) for image loading * [OkHttp](https://square.github.io/okhttp/) for HTTP requests diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt index 585e1079..ede44e08 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ExtrasItem.kt @@ -8,12 +8,18 @@ import com.github.damontecres.wholphin.ui.nav.Destination import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.api.ExtraType +/** + * Represents "extras" for media such as behind-the-scenes or deleted scenes + */ sealed interface ExtrasItem { val parentId: UUID val type: ExtraType val destination: Destination val title: String? + /** + * Represents multiple extras of the same type + */ data class Group( override val parentId: UUID, override val type: ExtraType, @@ -25,6 +31,9 @@ sealed interface ExtrasItem { override val title: String? = null } + /** + * Represents a single extra + */ data class Single( override val parentId: UUID, override val type: ExtraType, @@ -38,6 +47,9 @@ sealed interface ExtrasItem { } } +/** + * Converts [ExtraType] to the string resource ID + */ @get:StringRes val ExtraType.stringRes: Int get() = @@ -56,6 +68,9 @@ val ExtraType.stringRes: Int ExtraType.SHORT -> R.string.shorts } +/** + * Converts [ExtraType] to the plural resource ID + */ @get:PluralsRes val ExtraType.pluralRes: Int get() = diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/filter/ItemFilterBy.kt b/app/src/main/java/com/github/damontecres/wholphin/data/filter/ItemFilterBy.kt index fec0fb8d..1d536f28 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/filter/ItemFilterBy.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/filter/ItemFilterBy.kt @@ -50,6 +50,11 @@ val DefaultPlaylistItemsOptions = DecadeFilter, ) +/** + * A way to filter libraries + * + * Gets and sets values within a [GetItemsFilter] + */ sealed interface ItemFilterBy { @get:StringRes val stringRes: Int diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt index 49b3c694..9c8009db 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/AudioItem.kt @@ -5,6 +5,14 @@ import org.jellyfin.sdk.model.extensions.ticks import java.util.UUID import kotlin.time.Duration +/** + * Represents audio or a song as a stripped down [BaseItem] since there may be a lot of these created. + * + * Typically added to a MediaItem as the tag for reference later + * + * The "key" can be used by a Compose LazyList key function as it will uniquely identify this particular + * audio even if the same song is added to the queue multiple times + */ @Stable data class AudioItem( val key: Long = keyTracker++, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index 83683976..cf7461ea 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -29,6 +29,9 @@ import java.util.Locale import java.util.UUID import kotlin.time.Duration +/** + * Wrapper for [BaseItemDto] with shortcuts for various UI elements + */ @Serializable @Stable data class BaseItem( @@ -92,6 +95,9 @@ data class BaseItem( @Transient val timeRemainingOrRuntime: Duration? = data.timeRemaining ?: data.runTimeTicks?.ticks + /** + * Contains pre computed UI elements that would be expensive to create on the main thread + */ @Transient val ui = BaseItemUi( @@ -178,6 +184,9 @@ data class BaseItem( it.dayOfMonth.toString().padStart(2, '0') }?.toIntOrNull() + /** + * Convert this [BaseItem] into a [Destination] to navigate to its page in the app + */ fun destination(index: Int? = null): Destination { if (destinationOverride != null) return destinationOverride val result = @@ -228,6 +237,7 @@ data class BaseItem( } companion object { + @Deprecated("Use regular constructor instead") fun from( dto: BaseItemDto, api: ApiClient, @@ -249,6 +259,9 @@ data class BaseItemUi( val quickDetails: AnnotatedString, ) +/** + * Create the special [Destination.FilteredCollection] for the given genre information + */ fun createGenreDestination( genreId: UUID, genreName: String, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt index 3444ac54..3e07964c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Chapter.kt @@ -7,6 +7,9 @@ import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.extensions.ticks import kotlin.time.Duration +/** + * Represents a chapter within a video + */ data class Chapter( val name: String?, val position: Duration, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt index 0cf88ff1..db03ff48 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt @@ -16,6 +16,9 @@ import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.time.LocalDate import java.util.UUID +/** + * The type of a Seerr/Discover object with mapping to the Jellyfin [BaseItemKind] + */ @Serializable enum class SeerrItemType( val baseItemKind: BaseItemKind?, @@ -46,6 +49,9 @@ enum class SeerrItemType( } } +/** + * How available is a particular discovered item within the Jellyfin server + */ @Serializable enum class SeerrAvailability( val status: Int, @@ -64,7 +70,7 @@ enum class SeerrAvailability( } /** - * An item provided by a discovery service (ie Seerr). It may exist on the JF server as well. + * An item provided by a discovery service (ie Seerr). It may exist on the JF server as well, see [availability]. */ @Stable @Serializable @@ -104,6 +110,9 @@ data class DiscoverItem( } } +/** + * A rating for a discovered item which is usually fetched separately from the item + */ data class DiscoverRating( val criticRating: Int?, val audienceRating: Float?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt index f819c7c3..d0d32988 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt @@ -14,6 +14,9 @@ import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Filter for a collection folder + */ @Serializable data class CollectionFolderFilter( val nameOverride: String? = null, @@ -24,6 +27,9 @@ data class CollectionFolderFilter( val useSavedLibraryDisplayInfo: Boolean = true, ) +/** + * A sort of simplified filter which can be [applyTo] a [GetItemsRequest] or [GetPersonsRequest] to add or remove filters + */ @Serializable data class GetItemsFilter( val favorite: Boolean? = null, @@ -52,7 +58,7 @@ data class GetItemsFilter( } /** - * Clear all of the values for the given filters + * Clear all the values for the given filters */ fun delete(filterOptions: List>): GetItemsFilter { var newFilter = this @@ -128,6 +134,9 @@ data class GetItemsFilter( isFavorite = favorite, ) + /** + * Merge another [GetItemsFilter] onto this one, replacing only unset values + */ fun merge(filter: GetItemsFilter): GetItemsFilter = this.copy( favorite = favorite ?: filter.favorite, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt index 94a30475..b31261fd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemPlayback.kt @@ -13,6 +13,10 @@ import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Store the media source and audio/subtitle tracks chosen for a specific media item + * + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemTrackModification.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemTrackModification.kt index e8bbfeb4..3888e438 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemTrackModification.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/ItemTrackModification.kt @@ -9,6 +9,11 @@ import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Store modifications to an audio/subtitle track in a media item + * + * For example, the subtitle delay + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt index 2c516272..0b1759ee 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/JellyfinServer.kt @@ -17,6 +17,9 @@ import org.jellyfin.sdk.model.ServerVersion import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Represents a Jellyfin server + */ @Entity(tableName = "servers") @Serializable data class JellyfinServer( @@ -29,6 +32,9 @@ data class JellyfinServer( val serverVersion: ServerVersion? by lazy { version?.let(ServerVersion::fromString) } } +/** + * Represents a Jellyfin user for a particular server + */ @Entity( tableName = "users", foreignKeys = [ @@ -59,6 +65,9 @@ data class JellyfinUser( "JellyfinUser(rowId=$rowId, id=$id, name=$name, serverId=$serverId, accessToken?=${accessToken.isNotNullOrBlank()}, pin?=${pin.isNotNullOrBlank()})" } +/** + * Represents the relationship between [JellyfinServer] and its [JellyfinUser] + */ data class JellyfinServerUsers( @Embedded val server: JellyfinServer, @Relation( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt index e5bcafcb..edfc797a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/LibraryDisplayInfo.kt @@ -18,6 +18,11 @@ import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Stores the filter, sort, and view options a user changes for a library + * + * This allows for restoring these settings whenever the user navigates to the library + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/NavDrawerPinnedItem.kt similarity index 85% rename from app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt rename to app/src/main/java/com/github/damontecres/wholphin/data/model/NavDrawerPinnedItem.kt index 2b503023..e312a2b1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/ServerPreferences.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/NavDrawerPinnedItem.kt @@ -9,6 +9,9 @@ enum class NavPinType { UNPINNED, } +/** + * Stores preference information about nav drawer items such as its order and whether to show or put in More + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt index ac4a4710..4e639009 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Person.kt @@ -12,6 +12,9 @@ import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.PersonKind +/** + * Represents a person in some media such as an actor or director + */ @Stable data class Person( val id: UUID, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackEffect.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackEffect.kt index 3c5cf6a0..0e7bcd37 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackEffect.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackEffect.kt @@ -5,6 +5,9 @@ import androidx.room.Entity import org.jellyfin.sdk.model.api.BaseItemKind import java.util.UUID +/** + * Store effects applied to some media such as color or hue adjustments + */ @Entity(tableName = "playback_effects", primaryKeys = ["jellyfinUserRowId", "itemId", "type"]) data class PlaybackEffect( val jellyfinUserRowId: Int, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt index f452e2f1..fb54dfcd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/PlaybackLanguageChoice.kt @@ -9,6 +9,10 @@ import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID +/** + * Stores the language choices for a series so they can be applied automatically to other episodes + * without the user needing to explicitly choose the tracks + */ @Entity( foreignKeys = [ ForeignKey( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt index c4cd3792..698a5f34 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt @@ -6,6 +6,11 @@ import androidx.compose.runtime.setValue import org.jellyfin.sdk.model.api.MediaType import java.util.UUID +/** + * Tracks playback of multiple items. Points to the current media with function to advance or go to previous ones. + * + * This is not the same thing as a Jellyfin server playlist + */ class Playlist( items: List, startIndex: Int = 0, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt index 1e9f963d..a6886cf2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt @@ -2,6 +2,9 @@ package com.github.damontecres.wholphin.data.model import com.github.damontecres.wholphin.services.SeerrUserConfig +/** + * Permission levels for a user on a Seerr server + */ enum class SeerrPermission( private val flag: Int, ) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt index e29305d2..311379e4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrServer.kt @@ -13,6 +13,9 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.serializer.UUIDSerializer +/** + * Represents a Seerr server instance + */ @Entity( tableName = "seerr_servers", indices = [Index("url", unique = true)], @@ -26,6 +29,9 @@ data class SeerrServer( val version: String? = null, ) +/** + * Represents a user on a [SeerrServer] + */ @Entity( tableName = "seerr_users", foreignKeys = [ @@ -56,12 +62,18 @@ data class SeerrUser( "SeerrUser(jellyfinUserRowId=$jellyfinUserRowId, serverId=$serverId, authMethod=$authMethod, username=$username, password?=${password.isNotNullOrBlank()}, credential?=${credential.isNotNullOrBlank()})" } +/** + * The method used to authenticate a user to the server + */ enum class SeerrAuthMethod { LOCAL, JELLYFIN, API_KEY, } +/** + * Represents the relationship between a [SeerrServer] and its [SeerrUser]s + */ data class SeerrServerUsers( @Embedded val server: SeerrServer, @Relation( diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt index 943789db..01e473b2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Trailer.kt @@ -1,9 +1,15 @@ package com.github.damontecres.wholphin.data.model +/** + * Represents a trailer for media + */ sealed interface Trailer { val name: String } +/** + * A [Trailer] stored on the Jellyfin server + */ data class LocalTrailer( val baseItem: BaseItem, ) : Trailer { @@ -11,6 +17,9 @@ data class LocalTrailer( get() = baseItem.name ?: "" } +/** + * A [Trailer] available via a remote URL, such as YouTube + */ data class RemoteTrailer( override val name: String, val url: String, diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/VideoFilter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/VideoFilter.kt index e71db95c..93543e23 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/VideoFilter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/VideoFilter.kt @@ -14,7 +14,7 @@ import androidx.media3.effect.ScaleAndRotateTransformation import androidx.room.Ignore /** - * Modifications to a video playback + * Modifications to an image or video playback */ data class VideoFilter( val rotation: Int = 0, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 07b5fc7e..c1507d9f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -32,6 +32,9 @@ import java.io.File import javax.inject.Inject import javax.inject.Singleton +/** + * Handles any changes needed when the app in upgraded on the device such as setting new preferences + */ @Singleton class AppUpgradeHandler @Inject @@ -61,6 +64,7 @@ class AppUpgradeHandler Timber.i( "App updated: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode", ) + // Store the previous and new version info prefs.edit(true) { putString(VERSION_NAME_PREVIOUS_KEY, previousVersion) putLong(VERSION_CODE_PREVIOUS_KEY, previousVersionCode) @@ -83,6 +87,9 @@ class AppUpgradeHandler } } + /** + * Copies the font file used by MPV subtitles to the app's files directory + */ fun copySubfont(overwrite: Boolean) { try { val fontFileName = "subfont.ttf" @@ -111,6 +118,9 @@ class AppUpgradeHandler const val VERSION_CODE_CURRENT_KEY = "version.current.code" } + /** + * Perform any needed upgrades + */ suspend fun upgradeApp( previous: Version, current: Version, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt index 832d4137..1bf34929 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/BackdropService.kt @@ -32,6 +32,11 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +/** + * Stores state for the backdrop of the app shown on non-full screen pages + * + * This is usually the backdrop of the currently focused media + */ @Singleton @OptIn(FlowPreview::class) class BackdropService @@ -46,6 +51,9 @@ class BackdropService private val _backdropFlow = MutableStateFlow(BackdropResult.NONE) val backdropFlow = _backdropFlow + /** + * Update the backdrop to use the specified item + */ suspend fun submit(item: BaseItem) = withContext(Dispatchers.IO) { val imageUrl = @@ -57,8 +65,14 @@ class BackdropService submit(item.id.toString(), imageUrl) } + /** + * Update the backdrop to use the specified discovered item + */ suspend fun submit(item: DiscoverItem) = submit("discover_${item.id}", item.backDropUrl) + /** + * Update the backdrop to use the specified URL + */ suspend fun submit( itemId: String, imageUrl: String?, @@ -74,6 +88,9 @@ class BackdropService } } + /** + * Remove the backdrop, such as when switching pages + */ suspend fun clearBackdrop() { _backdropFlow.update { BackdropResult.NONE @@ -224,6 +241,9 @@ class BackdropService } } +/** + * The result from determining the backdrop URL and extracted colors for the dynamic backdrop + */ data class BackdropResult( val itemId: String?, val imageUrl: String?, @@ -245,6 +265,9 @@ data class BackdropResult( } } +/** + * The colors extracted from an image + */ data class ExtractedColors( val primary: Color, val secondary: Color, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt index 205e12bf..9c4cfa5c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/DatePlayedService.kt @@ -27,6 +27,9 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton +/** + * Caches when media was lasted played. This is mostly used by the combined Continue Watching & Next Up home row. + */ @Singleton class DatePlayedService @Inject @@ -80,6 +83,12 @@ class DatePlayedService } } + /** + * Get the logical timestamp when the item was last played. + * + * This is calculated using lastest of the actual last played timestamp of the item, + * the previous episode's last played timestamp, and the episode's premiere date + */ suspend fun getLastPlayed(item: BaseItem): LocalDateTime? = withContext(Dispatchers.IO) { val seriesId = item.data.seriesId @@ -93,6 +102,11 @@ class DatePlayedService } } + /** + * Remove the cached last date played for the given item's series + * + * Used for when a user plays this item + */ fun invalidate(item: BaseItem) { item.data.seriesId?.let { seriesId -> Timber.d("Invalidating %s", seriesId) @@ -100,6 +114,9 @@ class DatePlayedService } } + /** + * Remove the cached last data played for the given item or its series + */ suspend fun invalidate(itemId: UUID) { val seriesId = api.userLibraryApi.getItem(itemId = itemId).content.let { @@ -121,6 +138,9 @@ class DatePlayedService } } +/** + * Invalidates the date played cached when the current user changes + */ @ActivityScoped class DatePlayedInvalidationService @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt index 976fb2c8..a001eb6a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/DeviceProfileService.kt @@ -14,6 +14,9 @@ import org.jellyfin.sdk.model.api.DeviceProfile import javax.inject.Inject import javax.inject.Singleton +/** + * Creates and caches the device direct play/transcoding profile sent to the server for ExoPlayer + */ @Singleton class DeviceProfileService @Inject @@ -21,7 +24,7 @@ class DeviceProfileService @param:ApplicationContext private val context: Context, ) { val mediaCodecCapabilitiesTest by lazy { - // Created lazily below on the IO thread since it cn take time + // Created lazily below on another thread since it cn take time MediaCodecCapabilitiesTest(context) } private val mutex = Mutex() @@ -33,7 +36,7 @@ class DeviceProfileService prefs: PlaybackPreferences, serverVersion: ServerVersion?, ): DeviceProfile = - withContext(Dispatchers.IO) { + withContext(Dispatchers.Default) { mutex.withLock { val newConfig = DeviceProfileConfiguration( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt index e952177b..65b74083 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ExtrasService.kt @@ -9,12 +9,20 @@ import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +/** + * Get extras for media + * + * @see [ExtrasItem] + */ @Singleton class ExtrasService @Inject constructor( private val api: ApiClient, ) { + /** + * Get the [ExtrasItem]s for the given item + */ suspend fun getExtras(itemId: UUID): List { val extrasMap = api.userLibraryApi @@ -42,6 +50,9 @@ class ExtrasService } } +/** + * The order which extras should be shown + */ private val ExtraType.sortOrder: Int get() = when (this) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt b/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt index a68a6fe5..cdaa0db4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt @@ -8,6 +8,9 @@ import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +/** + * Handles toggling media as favorited or watched + */ @Singleton class FavoriteWatchManager @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index 0b3b0887..2abe6a1e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -63,6 +63,9 @@ import java.io.File import javax.inject.Inject import javax.inject.Singleton +/** + * Handles getting home page settings and data + */ @Singleton class HomeSettingsService @Inject @@ -84,6 +87,9 @@ class HomeSettingsService allowTrailingComma = true } + /** + * The current home page settings + */ val currentSettings = MutableStateFlow(HomePageResolvedSettings.EMPTY) /** @@ -245,6 +251,9 @@ class HomeSettingsService currentSettings.update { resolvedSettings } } + /** + * Resolve the settings and set them to be the current settings + */ suspend fun updateCurrent(settings: HomePageSettings) { val resolvedRows = settings.rows.mapIndexed { index, config -> @@ -317,6 +326,9 @@ class HomeSettingsService return HomePageResolvedSettings(rowConfig) } + /** + * Create home page settings from the user's web UI home page settings + */ suspend fun parseFromWebConfig(userId: UUID): HomePageResolvedSettings? { val customPrefs = api.displayPreferencesApi diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt index f6e02df2..ce945b69 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/LatestNextUpService.kt @@ -1,10 +1,8 @@ package com.github.damontecres.wholphin.services import android.content.Context -import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.SlimItemFields -import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.supportItemKinds import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -16,12 +14,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi -import org.jellyfin.sdk.api.client.extensions.userLibraryApi -import org.jellyfin.sdk.api.client.extensions.userViewsApi import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.UserDto -import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import org.jellyfin.sdk.model.api.request.GetNextUpRequest import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest import timber.log.Timber @@ -31,6 +24,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds +/** + * Get continue watching and next up items for users + */ @Singleton class LatestNextUpService @Inject @@ -39,6 +35,9 @@ class LatestNextUpService private val api: ApiClient, private val datePlayedService: DatePlayedService, ) { + /** + * Get resume (continue watching) items for a user + */ suspend fun getResume( userId: UUID, limit: Int, @@ -60,13 +59,6 @@ class LatestNextUpService remove(BaseItemKind.EPISODE) } }, -// enableImageTypes = -// listOf( -// ImageType.PRIMARY, -// ImageType.THUMB, -// ImageType.BACKDROP, -// ImageType.LOGO, -// ), ) val items = api.itemsApi @@ -77,6 +69,9 @@ class LatestNextUpService return items } + /** + * Get next up items for a user + */ suspend fun getNextUp( userId: UUID, limit: Int, @@ -98,13 +93,6 @@ class LatestNextUpService enableUserData = true, enableRewatching = enableRewatching, nextUpDateCutoff = nextUpDateCutoff, -// enableImageTypes = -// listOf( -// ImageType.PRIMARY, -// ImageType.THUMB, -// ImageType.BACKDROP, -// ImageType.LOGO, -// ), ) val nextUp = api.tvShowsApi @@ -115,65 +103,11 @@ class LatestNextUpService return nextUp } - suspend fun getLatest( - user: UserDto, - limit: Int, - includedIds: List, - ): List { - val excluded = user.configuration?.latestItemsExcludes.orEmpty() - val views by api.userViewsApi.getUserViews() - val latestData = - views.items - .filter { - it.id in includedIds && it.id !in excluded && - it.collectionType in supportedLatestCollectionTypes - }.map { view -> - val title = - view.name?.let { context.getString(R.string.recently_added_in, it) } - ?: context.getString(R.string.recently_added) - val request = - GetLatestMediaRequest( - fields = SlimItemFields, - imageTypeLimit = 1, - parentId = view.id, - groupItems = true, - limit = limit, - isPlayed = null, // Server will handle user's preference - ) - LatestData(title, request) - } - - return latestData - } - - suspend fun loadLatest(latestData: List): List { - val rows = - latestData.mapNotNull { (title, request) -> - try { - val latest = - api.userLibraryApi - .getLatestMedia(request) - .content - .map { BaseItem.from(it, api, true) } - if (latest.isNotEmpty()) { - HomeRowLoadingState.Success( - title = title, - items = latest, - ) - } else { - null - } - } catch (ex: Exception) { - Timber.e(ex, "Exception fetching %s", title) - HomeRowLoadingState.Error( - title = title, - exception = ex, - ) - } - } - return rows - } - + /** + * Create the combined Continue Watching & Next Up items + * + * @see [DatePlayedService] + */ suspend fun buildCombined( resume: List, nextUp: List, @@ -207,17 +141,3 @@ class LatestNextUpService return@withContext result } } - -val supportedLatestCollectionTypes = - setOf( - CollectionType.MOVIES, - CollectionType.TVSHOWS, - CollectionType.HOMEVIDEOS, - // Exclude Live TV because a recording folder view will be used instead - null, // Recordings & mixed collection types - ) - -data class LatestData( - val title: String, - val request: GetLatestMediaRequest, -) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MediaManagementService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MediaManagementService.kt index a13881a9..beab6e64 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MediaManagementService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MediaManagementService.kt @@ -19,6 +19,9 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +/** + * Service to manage media such as deletions + */ @Singleton class MediaManagementService @Inject @@ -45,6 +48,9 @@ class MediaManagementService return canDelete(item, appPreferences) } + /** + * Check if the item can be deleted. This means the app setting is enabled and the user has permission. + */ fun canDelete( item: BaseItem, appPreferences: AppPreferences, @@ -62,10 +68,14 @@ class MediaManagementService } } + /** + * Delete the item. + * + * This item will be sent through [deletedItemFlow] for other services or view models to react. + */ suspend fun deleteItem(item: BaseItem): DeleteResult { try { Timber.i("Deleting %s", item.id) - // TODO enable api.libraryApi.deleteItem(item.id) _deletedItemFlow.emit(DeletedItem(item)) return DeleteResult.Success @@ -88,6 +98,9 @@ sealed interface DeleteResult { ) : DeleteResult } +/** + * Convenience function to delete an item and show a Toast based on success or error + */ fun ViewModel.deleteItem( context: Context, mediaManagementService: MediaManagementService, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MediaReportService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MediaReportService.kt index 14a93ba2..d1a1543a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MediaReportService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MediaReportService.kt @@ -21,6 +21,9 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +/** + * Send media info to the server + */ @Singleton class MediaReportService @Inject @@ -39,6 +42,9 @@ class MediaReportService encodeDefaults = false } + /** + * Fetch the media info and send it to the server + */ fun sendReportFor(itemId: UUID) { ioScope.launchIO(ExceptionHandler(autoToast = true)) { val item = api.userLibraryApi.getItem(itemId = itemId).content @@ -46,6 +52,9 @@ class MediaReportService } } + /** + * Send the media report for the given item + */ suspend fun sendReportFor(item: BaseItemDto) { val sources = item.mediaSources ?: api.userLibraryApi diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt index be3ceac7..002be20e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt @@ -63,6 +63,11 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.seconds +/** + * Manage the global state for playing music + * + * Has functions for modifying the queue + */ @OptIn(UnstableApi::class) @Singleton class MusicService @@ -104,6 +109,11 @@ class MusicService private var activityTracker: TrackActivityPlaybackListener? = null private var websocketJob: Job? = null + /** + * Start music playback + * + * Sets up the media session, activity tracking, and actual playback + */ suspend fun start() { if (mediaSession == null) { mutex.withLock { @@ -130,6 +140,9 @@ class MusicService } } + /** + * Stop music playback + */ suspend fun stop() { mutex.withLock { Timber.i("Stopping music") @@ -191,6 +204,9 @@ class MusicService addAllToQueue(items, startIndex) } + /** + * Replace the queue with the given items + */ suspend fun setQueue( items: List, shuffled: Boolean, @@ -208,6 +224,9 @@ class MusicService } } + /** + * Add an item to the specified index of the queue. If no index specified, it will be added to the end. + */ suspend fun addToQueue( item: BaseItem, index: Int? = null, @@ -229,6 +248,11 @@ class MusicService } } + /** + * Add all the items in teh list to end of the queue + * + * @param startIndex The index to start from within the source list + */ suspend fun addAllToQueue( list: BlockingList, startIndex: Int, @@ -255,6 +279,9 @@ class MusicService updateQueueSize() } + /** + * Converts a [BaseItem] into a [MediaItem] setting an [AudioItem] as its tag + */ private fun convert(audio: BaseItem): MediaItem { val url = api.universalAudioApi.getUniversalAudioStreamUrl( @@ -282,6 +309,9 @@ class MusicService .build() } + /** + * Updates the state for when the queue changes + */ private suspend fun updateQueueSize() { // val ids = // withContext(Dispatchers.Default) { @@ -306,6 +336,9 @@ class MusicService } } + /** + * Move an item within the queue + */ suspend fun moveQueue( index: Int, direction: MoveDirection, @@ -314,6 +347,9 @@ class MusicService updateQueueSize() } + /** + * Move an item within the queue + */ suspend fun moveQueue( index: Int, newIndex: Int, @@ -322,6 +358,9 @@ class MusicService updateQueueSize() } + /** + * Start playback at the given index of the queue + */ suspend fun playIndex(index: Int) { onMain { player.seekTo(index, 0L) @@ -330,6 +369,9 @@ class MusicService // MusicPlayerListener will update state } + /** + * Play this item next after the current, ie add the item as the next index in the queue + */ suspend fun playNext(song: BaseItem) { val mediaItem = convert(song) onMain { @@ -341,6 +383,9 @@ class MusicService updateQueueSize() } + /** + * From the item at the given index from the queue + */ suspend fun removeFromQueue(index: Int) { onMain { player.removeMediaItem(index) } updateQueueSize() @@ -353,7 +398,10 @@ class MusicService return result } - fun subscribe(): Job = + /** + * Subscribes to the server websocket to receive playback commands + */ + private fun subscribe(): Job = api.webSocket .subscribe() .onEach { message -> @@ -503,6 +551,11 @@ private class MusicPlayerListener( } } +/** + * Remember the queue currently playing + * + * @see MusicServiceState + */ @Composable fun rememberQueue( player: Player, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/NavDrawerService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/NavDrawerService.kt index 8dbf49ee..68486430 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/NavDrawerService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/NavDrawerService.kt @@ -38,6 +38,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.hours +/** + * Gets the items to show in the nav drawer + */ @Singleton class NavDrawerService @Inject @@ -54,6 +57,7 @@ class NavDrawerService val state: StateFlow = _state init { + // Handle updating the nav drawer when the user changes serverRepository.currentUser .asFlow() .combine(serverRepository.currentUserDto.asFlow()) { user, userDto -> @@ -74,10 +78,13 @@ class NavDrawerService showToast(context, "Error fetching user's views") }.launchIn(coroutineScope) + // Handle when the user has logged into a Seerr server seerrServerRepository.active .onEach { discoverActive -> _state.update { it.copy(discoverEnabled = discoverActive) } }.launchIn(coroutineScope) + + // Handle when music is actively playing or not coroutineScope.launchDefault { musicService.state.collectLatest { music -> Timber.v("MusicService updated") @@ -114,6 +121,9 @@ class NavDrawerService } } + /** + * Get all the libraries the user has access to + */ suspend fun getAllUserLibraries( userId: UUID, tvAccess: Boolean, @@ -147,6 +157,9 @@ class NavDrawerService return libraries } + /** + * Get the libraries that the user has not "pinned". These will show in the More section. + */ suspend fun getFilteredUserLibraries( user: JellyfinUser, tvAccess: Boolean, @@ -161,6 +174,9 @@ class NavDrawerService return libraries } + /** + * Update the current state of the nav drawer items + */ suspend fun updateNavDrawer( user: JellyfinUser, userDto: UserDto, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PeopleFavorites.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PeopleFavorites.kt index 6a35dda5..1df73d18 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PeopleFavorites.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PeopleFavorites.kt @@ -10,6 +10,9 @@ import org.jellyfin.sdk.api.client.extensions.itemsApi import javax.inject.Inject import javax.inject.Singleton +/** + * Gets people in media, specifically to check if they are favorited or not + */ @Singleton class PeopleFavorites @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt index b47ae33f..fbbc9b70 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt @@ -38,6 +38,11 @@ import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +/** + * Create [Playlist]s (not Jellyfin server playlist) for playback + * + * Used to create a queue of episodes or from Play All button + */ @Singleton class PlaylistCreator @Inject @@ -74,6 +79,9 @@ class PlaylistCreator return Playlist(episodes, startIndex) } + /** + * Create from a server playlist ID + */ suspend fun createFromPlaylistId( playlistId: UUID, startIndex: Int?, @@ -138,6 +146,11 @@ class PlaylistCreator return Playlist(items, 0) } + /** + * Create a [Playlist] contextually based on the given item. + * + * For example, an episode creates a queue of next up episodes in the series, or a movie creates one for its parts if needed + */ suspend fun createFrom( item: BaseItemDto, startIndex: Int = 0, @@ -275,6 +288,9 @@ class PlaylistCreator } } + /** + * Get the playlists on the server for a given media type + */ suspend fun getServerPlaylists( mediaType: MediaType?, scope: CoroutineScope, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt index 06d116a4..91df9518 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt @@ -22,6 +22,9 @@ import javax.inject.Singleton import kotlin.math.roundToInt import kotlin.time.Duration.Companion.seconds +/** + * Handles determining whether the refresh rate and/or resolution of the display need to changed when playing media + */ @Singleton class RefreshRateService @Inject @@ -119,6 +122,9 @@ class RefreshRateService MainActivity.instance.changeDisplayMode(0) } + /** + * Listens for the display to change so we known the refresh rate or resolution is updated + */ private class Listener( val displayId: Int, ) : DisplayManager.DisplayListener { @@ -213,6 +219,9 @@ class RefreshRateService } } +/** + * Wrapper for a [Display.Mode] + */ data class DisplayMode( val modeId: Int, val physicalWidth: Int, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt index c81893b3..6252a2a7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt @@ -38,6 +38,9 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds +/** + * Handles the queue of items to show on the screensaver, both in-app or OS + */ @Singleton class ScreensaverService @Inject @@ -67,6 +70,9 @@ class ScreensaverService }.launchIn(scope) } + /** + * Reset the timer before showing the in-app screensaver + */ fun pulse() { waitJob?.cancel() if (_state.value.enabled) { @@ -95,6 +101,9 @@ class ScreensaverService } } + /** + * Immediately start the in-app screensaver + */ fun start() { _state.update { it.copy( @@ -104,6 +113,9 @@ class ScreensaverService } } + /** + * Immediately stop the in-app screensaver + */ fun stop(cancelJob: Boolean) { _state.update { it.copy( @@ -114,6 +126,9 @@ class ScreensaverService if (cancelJob) waitJob?.cancel() } + /** + * Signal to the OS for keeping the screen on such as during playback or when the in-app screensaver is active + */ fun keepScreenOn(keep: Boolean) { scope.launchDefault { val screensaverEnabled = _state.value.enabled @@ -136,6 +151,9 @@ class ScreensaverService keepScreenOn.update { keep } } + /** + * Create a flow of items to show on the screensaver + */ fun createItemFlow(scope: CoroutineScope): Flow = flow { val prefs = diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt index f71c902a..979c90bd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt @@ -25,6 +25,9 @@ import org.jellyfin.sdk.model.api.MediaType import timber.log.Timber import javax.inject.Inject +/** + * Listens for basic messages from the server such as messages + */ @ActivityScoped class ServerEventListener @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt index 6de45f50..05eac34e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/StreamChoiceService.kt @@ -20,6 +20,9 @@ import java.util.UUID import javax.inject.Inject import javax.inject.Singleton +/** + * Manage the track choices for media + */ @Singleton class StreamChoiceService @Inject @@ -94,6 +97,9 @@ class StreamChoiceService result } + /** + * Returns the audio stream that should play + */ suspend fun chooseAudioStream( source: MediaSourceInfo, seriesId: UUID?, @@ -108,6 +114,9 @@ class StreamChoiceService } } + /** + * Returns the audio stream that should play + */ fun chooseAudioStream( candidates: List, itemPlayback: ItemPlayback?, @@ -135,6 +144,9 @@ class StreamChoiceService } } + /** + * Returns the subtitle stream that should play + */ suspend fun chooseSubtitleStream( source: MediaSourceInfo, audioStream: MediaStream?, @@ -196,6 +208,9 @@ class StreamChoiceService )?.index } + /** + * Returns the subtitle stream that should play + */ fun chooseSubtitleStream( audioStreamLang: String?, candidates: List, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt index 3f676cba..6be37807 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt @@ -15,6 +15,9 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi import javax.inject.Inject import javax.inject.Singleton +/** + * Gets trailers for media + */ @Singleton class TrailerService @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt index 709549cf..9e0665dd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt @@ -45,6 +45,9 @@ import javax.inject.Singleton import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds +/** + * Checks if an app update is available + */ @Singleton class UpdateChecker @Inject @@ -65,6 +68,11 @@ class UpdateChecker val ACTIVE = true } + /** + * If the app hasn't recently checked, check for any updates and if there is one, show a toast message + * + * This is safe to call many times because it will only show the toast at most once every 12 hours + */ suspend fun maybeShowUpdateToast( updateUrl: String, showNegativeToast: Boolean = false, @@ -112,11 +120,17 @@ class UpdateChecker } } + /** + * Get the currently installed version + */ fun getInstalledVersion(): Version { val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0) - return Version.Companion.fromString(pkgInfo.versionName!!) + return Version.fromString(pkgInfo.versionName!!) } + /** + * Get the latest released version + */ suspend fun getLatestRelease(updateUrl: String): Release? { return withContext(Dispatchers.IO) { val request = @@ -161,6 +175,9 @@ class UpdateChecker } } + /** + * Download and install an update + */ suspend fun installRelease( release: Release, callback: DownloadCallback, @@ -263,6 +280,9 @@ class UpdateChecker return targetFile } + /** + * Check if the app has permission to write the download + */ fun hasPermissions(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || ( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UserPreferencesService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UserPreferencesService.kt index 67e940b5..7c7fce11 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UserPreferencesService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UserPreferencesService.kt @@ -9,6 +9,9 @@ import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton +/** + * Get the current user's [UserPreferences] + */ @Singleton class UserPreferencesService @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt index a44ab65b..38071cac 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt @@ -34,26 +34,48 @@ import org.jellyfin.sdk.model.DeviceInfo import javax.inject.Qualifier import javax.inject.Singleton +/** + * An [OkHttpClient] that includes the user's access token when making requests + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class AuthOkHttpClient +/** + * A basic [OkHttpClient] that does not include auth + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class StandardOkHttpClient +/** + * A [CoroutineScope] with [Dispatchers.IO] + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoCoroutineScope +/** + * A [CoroutineScope] with [Dispatchers.Default] + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class DefaultCoroutineScope +/** + * [Dispatchers.IO] + * + * @see IoCoroutineScope + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoDispatcher +/** + * [Dispatchers.Default] + * + * @see DefaultCoroutineScope + */ @Qualifier @Retention(AnnotationRetention.BINARY) annotation class DefaultDispatcher diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderSchedulerService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderSchedulerService.kt index 297cbd64..fb40c008 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderSchedulerService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderSchedulerService.kt @@ -24,6 +24,9 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.toJavaDuration +/** + * Schedules the [TvProviderWorker] to update the OS resume watching row + */ @ActivityScoped class TvProviderSchedulerService @Inject @@ -44,6 +47,7 @@ class TvProviderSchedulerService workManager.cancelUniqueWork(TvProviderWorker.WORK_NAME) if (supportsTvProvider) { if (user != null) { + // Schedule a new worker whenever the user changes activity.lifecycleScope.launchIO(ExceptionHandler()) { Timber.i("Scheduling TvProviderWorker for ${user.user}") workManager @@ -70,6 +74,9 @@ class TvProviderSchedulerService } } + /** + * Run the [TvProviderWorker] as a one-off instead of scheduled + */ fun launchOneTimeRefresh() { if (supportsTvProvider) { activity.lifecycleScope.launchIO(ExceptionHandler()) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt index 03ea0478..5a21d0d2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/tvprovider/TvProviderWorker.kt @@ -43,6 +43,11 @@ import java.util.Date import java.util.UUID import kotlin.time.Duration.Companion.minutes +/** + * Updates the Android OS resume watching row for a given Jellyfin user + * + * @see TvProviderSchedulerService + */ @HiltWorker class TvProviderWorker @AssistedInject diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Button.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Button.kt index c65069bd..ff85886d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Button.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Button.kt @@ -35,6 +35,11 @@ import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.Surface import androidx.tv.material3.Text +/** + * This is a re-implementation of [androidx.tv.material3.Button] with altered sizing, padding, colors, etc + * + * This allows for creating smaller and fully circular Buttons. + */ @Composable fun Button( onClick: () -> Unit, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index 82aab6ba..37de91ce 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -90,6 +90,9 @@ sealed interface DialogItemEntry data object DialogItemDivider : DialogItemEntry +/** + * An item within a [DialogPopup] + */ data class DialogItem( val headlineContent: @Composable () -> Unit, val onClick: () -> Unit, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt index a85d9fc9..47b4ebc9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt @@ -34,7 +34,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -49,8 +48,10 @@ import androidx.tv.material3.MaterialTheme import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.theme.WholphinTheme -import com.google.protobuf.value +/** + * An input field for text customized for TV entry + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun EditTextBox( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/FilterByButton.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/FilterByButton.kt index a218b9c7..95926eed 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/FilterByButton.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/FilterByButton.kt @@ -55,6 +55,14 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.tryRequestFocus import java.util.UUID +/** + * Button for filtering data. + * + * Clicking on it will show a drop-down menu of filterable options. Many of these show a second drop-down menu when clicked. + * + * @see GetItemsFilter + * @see ItemFilterBy + */ @Composable fun FilterByButton( filterOptions: List>, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt index d42eea5c..e85b1d02 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt @@ -156,6 +156,9 @@ private val genreCache by lazy { } } +/** + * Create a mapping from genre IDs to image URLs using random items within each genre + */ suspend fun getGenreImageMap( api: ApiClient, userId: UUID?, @@ -230,6 +233,9 @@ data class Genre( override val sortName: String get() = name } +/** + * Show an optimized grid of genres for a library + */ @Composable fun GenreCardGrid( itemId: UUID, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt index 3598fb93..c697b667 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreText.kt @@ -11,6 +11,11 @@ import androidx.tv.material3.Text private const val MAX_TO_SHOW = 4 +/** + * Display a comma separated list of genres + * + * Only the first few will be shown + */ @Composable fun GenreText( genres: List, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt index 38591a67..13ce42a7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ItemGrid.kt @@ -82,6 +82,9 @@ class ItemGridViewModel } } +/** + * Display a grid of a list of arbitrary item IDs such as for [com.github.damontecres.wholphin.data.ExtrasItem] + */ @Composable fun ItemGrid( destination: Destination.ItemGrid, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt index 67c56f12..6a2300c2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/LicenseInfo.kt @@ -6,6 +6,9 @@ import androidx.compose.ui.Modifier import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer +/** + * Displays dependencies' license information to comply with attribution + */ @Composable fun LicenseInfo(modifier: Modifier = Modifier) { val libraries by produceLibraries() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/OverviewText.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/OverviewText.kt index 7417028f..874585de 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/OverviewText.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/OverviewText.kt @@ -23,6 +23,9 @@ import androidx.tv.material3.Text import com.github.damontecres.wholphin.ui.playOnClickSound import com.github.damontecres.wholphin.ui.playSoundOnFocus +/** + * Show the overview text for an item. Uses a fixed size and allows for clicking. + */ @Composable fun OverviewText( overview: String, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt index db40ff0e..5eb1a210 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedContent.kt @@ -43,6 +43,7 @@ import com.github.damontecres.wholphin.ui.rememberPosition import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingState +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -50,8 +51,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import org.jellyfin.sdk.model.api.MediaType import java.util.UUID +/** + * Abstract [ViewModel] for the "Recommended" tab for a library + */ abstract class RecommendedViewModel( - val context: Context, + @param:ApplicationContext val context: Context, val navigationManager: NavigationManager, val favoriteWatchManager: FavoriteWatchManager, val mediaReportService: MediaReportService, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SelectedLeadingContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SelectedLeadingContent.kt index efc2ddac..d06fd652 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SelectedLeadingContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SelectedLeadingContent.kt @@ -13,6 +13,11 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.tv.material3.LocalContentColor +/** + * Shows a dot indicator + * + * Intended for using within the `leadingContent` of a [androidx.tv.material3.ListItem] + */ @Composable fun BoxScope.SelectedLeadingContent( selected: Boolean, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt index 64cfc78b..f87f5c98 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/SliderBar.kt @@ -126,16 +126,19 @@ data class SliderColors( @Composable fun default() = SliderColors( - activeFocused = SliderActiveColor(true), - activeUnfocused = SliderActiveColor(false), - inactiveFocused = SliderInactiveColor(true), - inactiveUnfocused = SliderInactiveColor(false), + activeFocused = sliderActiveColor(true), + activeUnfocused = sliderActiveColor(false), + inactiveFocused = sliderInactiveColor(true), + inactiveUnfocused = sliderInactiveColor(false), ) } } +/** + * Determines the active color for the slider. This is the "filled" left side of the slider + */ @Composable -fun SliderActiveColor(focused: Boolean): Color { +fun sliderActiveColor(focused: Boolean): Color { val theme = LocalTheme.current return when (theme) { AppThemeColors.UNRECOGNIZED, @@ -165,8 +168,11 @@ fun SliderActiveColor(focused: Boolean): Color { } } +/** + * Determines the inactive color for the slider. This is background of "unfilled" right side of the slider. + */ @Composable -fun SliderInactiveColor(focused: Boolean): Color { +fun sliderInactiveColor(focused: Boolean): Color { val theme = LocalTheme.current return when (theme) { AppThemeColors.UNRECOGNIZED, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt index f5791e04..f70a4f6a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TimeDisplay.kt @@ -12,6 +12,9 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.ui.util.LocalClock +/** + * Displays the [LocalClock] in the upper right corner of the parent [androidx.compose.foundation.layout.Box] + */ @Composable fun BoxScope.TimeDisplay(modifier: Modifier = Modifier) { val timeString by LocalClock.current.timeString diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt index e4e6d350..74c00328 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ViewOptionsDialog.kt @@ -39,6 +39,11 @@ import com.github.damontecres.wholphin.ui.tryRequestFocus import kotlinx.serialization.Serializable import org.jellyfin.sdk.model.api.ImageType +/** + * A dialog that shows the controls for making changes to a [ViewOptions] object. The caller must manage the state of the [ViewOptions]. + * + * It displays the [AppPreference] objects from [ViewOptions.OPTIONS] + */ @Composable fun ViewOptionsDialog( viewOptions: ViewOptions, @@ -105,6 +110,11 @@ fun ViewOptionsDialog( } } +/** + * Stores the customizable view changes from the user + * + * This is used to determine how the UI looks such as card size and shape + */ @Serializable data class ViewOptions( val columns: Int = 6, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt index 9e2141b5..5c4721c5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt @@ -19,6 +19,10 @@ import timber.log.Timber import java.util.UUID import javax.inject.Inject +/** + * A supplementary [ViewModel] for adding items to a server playlist + * @see com.github.damontecres.wholphin.ui.detail.PlaylistDialog + */ @HiltViewModel class AddPlaylistViewModel @Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt index 072de4e1..5cf0789e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/ItemDetailsDialogInfo.kt @@ -42,6 +42,9 @@ data class ItemDetailsDialogInfo( val files: List, ) +/** + * Dialog showing metadata about an item + */ @Composable fun ItemDetailsDialog( info: ItemDetailsDialogInfo, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index a2b9e923..41f5e534 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -85,6 +85,9 @@ interface CardGridItem { val sortName: String } +/** + * Shows a vertical grid of [CardGridItem]s + */ @OptIn(ExperimentalFoundationApi::class) @Composable fun CardGrid( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt index b4618eda..080c1c4e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/DetailUtils.kt @@ -49,9 +49,8 @@ enum class ClearChosenStreams { * If there are any (ie one or more) subtitle tracks, adds an option to disable or pick one * * @param item the media item to build for, typically an Episode or Movie - * @param series the item's series or null if not a TV episode; a non-null value will include a "Go to Series" option + * @param seriesId the item's series or null if not a TV episode; a non-null value will include a "Go to Series" option * @param sourceId the item's media source UUID - * @param navigateTo a function to trigger a navigation * @param onChooseVersion callback to pick a version of the item * @param onChooseTracks callback to pick a track for the given type of the item * @param onShowOverview callback to show overview dialog with media information diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt index bd50ce59..d50b7af5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistDetails.kt @@ -256,7 +256,7 @@ class PlaylistViewModel /** * This method tries to determine the [MediaType] of a playlist * - * In theory, the server will set the type, but sometime it doesn't + * In theory, the server will set the type, but sometimes it doesn't */ private suspend fun determineMediaType() { // Use the type the server says diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt index 4276330d..ce3fbd5b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt @@ -172,6 +172,9 @@ class LiveTvViewModel } } + /** + * Creates a list of [LocalDateTime] for each hour from now + */ private fun buildGuideTimes() = buildList { val start = LocalDateTime.now().roundDownToHalfHour() @@ -194,15 +197,22 @@ class LiveTvViewModel loading.setValueOnMain(LoadingState.Success) } + /** + * Get live TV programs for a subset of channels + * + * @param guideStart The start timestamp to fetch from + * @param channels The full list of channels + * @param channelIndices The indices of which channels to fetch programs for + */ private suspend fun fetchPrograms( guideStart: LocalDateTime, channels: List, - range: IntRange, + channelIndices: IntRange, ) = mutex.withLock { val maxStartDate = guideStart.plusHours(MAX_HOURS).minusMinutes(1) val minEndDate = guideStart.plusMinutes(1L) - val channelsToFetch = channels.subList(range.first, range.last + 1) - Timber.v("Fetching programs for $range channels ${channelsToFetch.size}") + val channelsToFetch = channels.subList(channelIndices.first, channelIndices.last + 1) + Timber.v("Fetching programs for $channelIndices channels ${channelsToFetch.size}") val request = GetProgramsDto( maxStartDate = maxStartDate, @@ -390,7 +400,7 @@ class LiveTvViewModel Timber.d("Got ${fetchedPrograms.size} programs & ${finalProgramList.size} total programs") withContext(Dispatchers.Main) { this@LiveTvViewModel.programs.value = - FetchedPrograms(range, finalProgramList, programsByChannel) + FetchedPrograms(channelIndices, finalProgramList, programsByChannel) } } @@ -472,6 +482,11 @@ class LiveTvViewModel private var focusLoadingJob: Job? = null + /** + * Callback when focusing on the EPG grid + * + * This determines if more programs/channels should be fetched based on the current position + */ fun onFocusChannel(position: RowColumn) { channels.value?.let { channels -> val fetchedRange = programs.value!!.range diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt index 21520447..81028501 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Backdrop.kt @@ -37,6 +37,9 @@ import com.github.damontecres.wholphin.ui.CrossFadeFactory import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +/** + * Shows the current backdrop images provided by [com.github.damontecres.wholphin.services.BackdropService] + */ @Composable fun Backdrop( drawerIsOpen: Boolean, @@ -59,6 +62,9 @@ fun Backdrop( ) } +/** + * Shows the current backdrop images provided by the [BackdropResult] + */ @Composable fun Backdrop( backdrop: BackdropResult, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index 6e2a2260..26068182 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -157,6 +157,9 @@ class NavDrawerViewModel fun getUserImage(user: JellyfinUser): String = api.imageApi.getUserImageUrl(user.id) + /** + * Determine which nav drawer item should be highlighted as currently selected + */ fun updateSelectedIndex() { viewModelScope.launchDefault { val asDestinations = @@ -215,6 +218,11 @@ class NavDrawerViewModel } } +/** + * An item that can be shown in the nav drawer + * + * Some are built-in such as Favorites. Others are created dynamically for libraries. + */ sealed interface NavDrawerItem { val id: String @@ -242,6 +250,9 @@ sealed interface NavDrawerItem { } } +/** + * A server provided nav drawer item, typically a library + */ data class ServerNavDrawerItem( val itemId: UUID, val name: String, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationDrawerAndroid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationDrawerAndroid.kt index 30b9d1d6..0cca40c8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationDrawerAndroid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationDrawerAndroid.kt @@ -57,6 +57,11 @@ import androidx.tv.material3.NavigationDrawerItemDefaults import androidx.tv.material3.NavigationDrawerScope import androidx.tv.material3.rememberDrawerState +/** + * This is a re-implementation of [androidx.tv.material3.ModalNavigationDrawer]. + * + * It changes padding, sizing, and animations that couldn't be changed in the Androidx implementation. + */ @Composable fun ModalNavigationDrawer( drawerContent: @Composable NavigationDrawerScope.(DrawerValue) -> Unit, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt index e8dc7d1d..40b5e89b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentMediaInfo.kt @@ -8,6 +8,11 @@ import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.TrickplayInfo import org.jellyfin.sdk.model.extensions.ticks +/** + * Metadata about the currently playing media + * + * @see CurrentPlayback + */ data class CurrentMediaInfo( val sourceId: String?, val videoStream: SimpleVideoStream?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt index 9c62f17b..b0f51e56 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/CurrentPlayback.kt @@ -8,6 +8,11 @@ import org.jellyfin.sdk.model.api.PlayMethod import org.jellyfin.sdk.model.api.TranscodingInfo import kotlin.time.Duration +/** + * Information about how the current media is being played such transcoding and decoder info + * + * @see CurrentMediaInfo + */ data class CurrentPlayback( val item: BaseItem, val tracks: List, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt index e1a291e1..d4039f72 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt @@ -51,7 +51,7 @@ import org.jellyfin.sdk.model.api.RemoteSubtitleInfo @Composable fun DownloadSubtitlesContent( - state: SubtitleSearch, + state: SubtitleSearchStatus, language: String, onSearch: (String) -> Unit, onClickDownload: (RemoteSubtitleInfo) -> Unit, @@ -59,7 +59,7 @@ fun DownloadSubtitlesContent( modifier: Modifier = Modifier, ) { when (val s = state) { - SubtitleSearch.Searching -> { + SubtitleSearchStatus.Searching -> { Wrapper { Text( text = stringResource(R.string.searching), @@ -69,7 +69,7 @@ fun DownloadSubtitlesContent( } } - SubtitleSearch.Downloading -> { + SubtitleSearchStatus.Downloading -> { Wrapper { Text( text = stringResource(R.string.downloading), @@ -79,11 +79,11 @@ fun DownloadSubtitlesContent( } } - is SubtitleSearch.Error -> { + is SubtitleSearchStatus.Error -> { Wrapper { ErrorMessage(null, s.ex, modifier) } } - is SubtitleSearch.Success -> { + is SubtitleSearchStatus.Success -> { val dialogItems = convertRemoteSubtitles(s.options, onClickDownload) val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt index 28f9b9d7..d43b6c27 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt @@ -84,6 +84,9 @@ import org.jellyfin.sdk.model.extensions.ticks import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +/** + * Possible actions the user can take during playback + */ sealed interface PlaybackAction { data object ShowDebug : PlaybackAction @@ -114,6 +117,9 @@ sealed interface PlaybackAction { data object Next : PlaybackAction } +/** + * UI for actual playback controls such as seek bar, play/pause and seek buttons + */ @OptIn(UnstableApi::class) @Composable fun PlaybackControls( @@ -515,6 +521,11 @@ fun PlaybackFaButton( } } +/** + * Show a dialog aligned to the bottom of the screen instead of centered. + * + * @param gravity The [Gravity] to align the dialog to left or right + */ @Composable fun BottomDialog( choices: List>, @@ -594,10 +605,6 @@ fun BottomDialog( } } -data class MoreButtonOptions( - val options: Map, -) - data class BottomDialogItem( val data: T, val headline: String, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt index a305023c..1968fac3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt @@ -62,6 +62,14 @@ data class PlaybackSettings( val playbackSpeedEnabled: Boolean, ) +/** + * Centralized UI component for displaying dialogs during playback + * + * Typically, the user will click something generating a [PlaybackAction] which translates into the + * [PlaybackDialogType] determining which dialog is shown by this component. + * + * @see PlaybackAction + */ @Composable fun PlaybackDialog( enableSubtitleDelay: Boolean, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt index 3bc4ddb0..2b0247e7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt @@ -575,6 +575,8 @@ enum class OverlayViewState { /** * A wrapper for the playback controls to show title and other information, plus the actual controls + * + * @see PlaybackControls */ @Composable fun Controller( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index cd39dbc2..3159c4dd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -168,7 +168,7 @@ fun PlaybackPageContent( val nextUp by viewModel.nextUp.observeAsState(null) val playlist by viewModel.playlist.observeAsState(Playlist(listOf())) - val subtitleSearch by viewModel.subtitleSearch.observeAsState(null) + val subtitleSearch by viewModel.subtitleSearchStatus.observeAsState(null) val subtitleSearchLanguage by viewModel.subtitleSearchLanguage.observeAsState(Locale.current.language) var playbackDialog by remember { mutableStateOf(null) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index f1be78dc..e9af15f1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -187,7 +187,7 @@ class PlaybackViewModel private var isPlaylist = false val playlist = MutableLiveData(Playlist(listOf())) - val subtitleSearch = MutableLiveData(null) + val subtitleSearchStatus = MutableLiveData(null) val subtitleSearchLanguage = MutableLiveData(Locale.current.language) val currentUserDto = serverRepository.currentUserDto diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayerLoadingState.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayerLoadingState.kt deleted file mode 100644 index c966a9aa..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayerLoadingState.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.github.damontecres.wholphin.ui.playback - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.media3.common.Player -import androidx.media3.common.listen -import androidx.media3.common.util.UnstableApi - -@UnstableApi -@Composable -fun rememberPlayerLoadingState(player: Player): PlayerLoadingState { - val state = remember(player) { PlayerLoadingState(player) } - LaunchedEffect(player) { - state.observe() - } - return state -} - -@UnstableApi -class PlayerLoadingState( - private val player: Player, -) : State { - override var value by mutableStateOf(player.isLoading) - private set - - suspend fun observe() { - value = player.isLoading - player.listen { - if (it.contains(Player.EVENT_IS_LOADING_CHANGED)) { - value = player.isLoading - } - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekBar.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekBar.kt index 92c6114b..37cb86f7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekBar.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekBar.kt @@ -49,6 +49,11 @@ import androidx.tv.material3.MaterialTheme import kotlinx.coroutines.FlowPreview import kotlin.time.Duration +/** + * This is a seek bar which seeks by a percentage of the duration instead of a fixed amount of time + * + * For example, if [intervals] is 10, then each seek will be 10% of total media duration + */ @Composable fun SteppedSeekBarImpl( progress: Float, @@ -97,6 +102,9 @@ fun SteppedSeekBarImpl( ) } +/** + * A seek (or scrubber) bar which seeks forward or back a fixed amount of time per move + */ @OptIn(FlowPreview::class) @Composable fun IntervalSeekBarImpl( @@ -147,8 +155,14 @@ fun IntervalSeekBarImpl( ) } +/** + * Actually renders the seek bar. It has callbacks for when the user moves to the left or right + * + * @see IntervalSeekBarImpl + * @see SteppedSeekBarImpl + */ @Composable -fun SeekBarDisplay( +private fun SeekBarDisplay( progress: Float, bufferedProgress: Float, durationMs: Long, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekPreviewImage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekPreviewImage.kt index 08fb7685..8be0b509 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekPreviewImage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SeekPreviewImage.kt @@ -1,5 +1,6 @@ package com.github.damontecres.wholphin.ui.playback +import androidx.annotation.FloatRange import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -52,20 +53,21 @@ fun Modifier.offsetByPercent( * * @param xPercentage percent offset between 0 inclusive and 1 inclusive */ -fun Modifier.offsetByPercent(xPercentage: Float) = - this.then( - Modifier.layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - layout(placeable.width, placeable.height) { - placeable.placeRelative( - x = - ((constraints.maxWidth * xPercentage).toInt() - placeable.width / 2) - .coerceIn(0, constraints.maxWidth - placeable.width), - y = 0, - ) - } - }, - ) +fun Modifier.offsetByPercent( + @FloatRange(0.0, 1.0) xPercentage: Float, +) = this.then( + Modifier.layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + placeable.placeRelative( + x = + ((constraints.maxWidth * xPercentage).toInt() - placeable.width / 2) + .coerceIn(0, constraints.maxWidth - placeable.width), + y = 0, + ) + } + }, +) /** * Show trickplay preview image. This composable assumes the provided URL is for the correct index. diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt index 6444a268..d430258a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SimpleMediaStream.kt @@ -5,6 +5,11 @@ import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.util.StreamFormatting.mediaStreamDisplayTitle import org.jellyfin.sdk.model.api.MediaStream +/** + * A slimmer [MediaStream] with minimal data for UI purposes + * + * @see SimpleVideoStream + */ data class SimpleMediaStream( val index: Int, val streamTitle: String?, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SkipIndicator.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SkipIndicator.kt index 3e417a91..c2344f5f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SkipIndicator.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SkipIndicator.kt @@ -25,6 +25,9 @@ import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.ifElse import kotlin.math.abs +/** + * Shows a rotating indicator when seeking. Displays the amount of seconds in the [durationMs]. + */ @Composable fun SkipIndicator( durationMs: Long, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleDelay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleDelay.kt index 17792899..8c645c9b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleDelay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleDelay.kt @@ -30,6 +30,9 @@ import kotlin.time.Duration.Companion.seconds private val delayIncrements = listOf(50.milliseconds, 250.milliseconds, 1.seconds) +/** + * Shows buttons for adding to or subtracting from the given [Duration], ie subtitle delay + */ @Composable fun SubtitleDelay( delay: Duration, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt index 021f4290..27188f57 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt @@ -20,23 +20,26 @@ import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.RemoteSubtitleInfo import timber.log.Timber -sealed interface SubtitleSearch { - data object Searching : SubtitleSearch +sealed interface SubtitleSearchStatus { + data object Searching : SubtitleSearchStatus - data object Downloading : SubtitleSearch + data object Downloading : SubtitleSearchStatus data class Success( val options: List, - ) : SubtitleSearch + ) : SubtitleSearchStatus data class Error( val message: String?, val ex: Exception?, - ) : SubtitleSearch + ) : SubtitleSearchStatus } +/** + * Trigger a search for subtitles in the given language for the currently playing media + */ fun PlaybackViewModel.searchForSubtitles(language: String = Locale.current.language) { - subtitleSearch.value = SubtitleSearch.Searching + subtitleSearchStatus.value = SubtitleSearchStatus.Searching subtitleSearchLanguage.value = language viewModelScope.launchIO { try { @@ -52,23 +55,26 @@ fun PlaybackViewModel.searchForSubtitles(language: String = Locale.current.langu compareByDescending { it.communityRating } .thenByDescending { it.downloadCount }, ) - subtitleSearch.setValueOnMain(SubtitleSearch.Success(results)) + subtitleSearchStatus.setValueOnMain(SubtitleSearchStatus.Success(results)) } } catch (ex: Exception) { Timber.e(ex, "Exception while searching for subtitles") - subtitleSearch.setValueOnMain(SubtitleSearch.Error(null, ex)) + subtitleSearchStatus.setValueOnMain(SubtitleSearchStatus.Error(null, ex)) } } } +/** + * Download the remote subtitles and attempt to activate them once complete + */ fun PlaybackViewModel.downloadAndSwitchSubtitles( subtitleId: String?, wasPlaying: Boolean, ) { if (subtitleId == null) { - subtitleSearch.value = SubtitleSearch.Error("Subtitle has no ID", null) + subtitleSearchStatus.value = SubtitleSearchStatus.Error("Subtitle has no ID", null) } else { - subtitleSearch.value = SubtitleSearch.Downloading + subtitleSearchStatus.value = SubtitleSearchStatus.Downloading viewModelScope.launchIO { try { currentItemPlayback.value?.let { @@ -161,7 +167,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles( ) } } - subtitleSearch.setValueOnMain(null) + subtitleSearchStatus.setValueOnMain(null) withContext(Dispatchers.Main) { if (wasPlaying) { player.play() @@ -170,12 +176,12 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles( } } catch (ex: Exception) { Timber.e(ex, "Exception while downloading subtitles: $subtitleId") - subtitleSearch.setValueOnMain(SubtitleSearch.Error(null, ex)) + subtitleSearchStatus.setValueOnMain(SubtitleSearchStatus.Error(null, ex)) } } } } fun PlaybackViewModel.cancelSubtitleSearch() { - subtitleSearch.value = null + subtitleSearchStatus.value = null } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt index 201b5982..d056e6f3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/TrackSelectionUtils.kt @@ -15,6 +15,9 @@ import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod import timber.log.Timber import kotlin.math.max +/** + * Functions for selecting which audio & subtitle tracks to activate in the [androidx.media3.common.Player] + */ object TrackSelectionUtils { @OptIn(UnstableApi::class) fun createTrackSelections( @@ -258,6 +261,9 @@ fun List.findExternalSubtitle(subtitleIndex: Int?): MediaStream? = } } +/** + * The result of [TrackSelectionUtils.createTrackSelections] + */ data class TrackSelectionResult( val trackSelectionParameters: TrackSelectionParameters, val audioSelected: Boolean, diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt b/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt index 8a6fc939..08207e53 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/BlockingList.kt @@ -3,12 +3,24 @@ package com.github.damontecres.wholphin.util import java.util.function.IntFunction import java.util.function.Predicate +/** + * A [List] which has function that will wait for a result + */ interface BlockingList : List { + /** + * Get the specified index, possibly blocking until it is available + */ suspend fun getBlocking(index: Int): T + /** + * Get first index that matches the given predicate, possibly blocking while searching + */ suspend fun indexOfBlocking(predicate: Predicate): Int companion object { + /** + * Create a [BlockingList] over a regular [List] + */ fun of(list: List): BlockingList = BlockingListWrapper(list) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt index c1630900..c334967f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/CrashReportSenderFactory.kt @@ -20,6 +20,9 @@ import org.json.JSONObject import timber.log.Timber import java.util.Date +/** + * Sends a crash report to the current server + */ @AutoService(ReportSenderFactory::class) class CrashReportSenderFactory : ReportSenderFactory { override fun create( diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/DebugLogTree.kt b/app/src/main/java/com/github/damontecres/wholphin/util/DebugLogTree.kt index 46b54466..b7f2e2c6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/DebugLogTree.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/DebugLogTree.kt @@ -4,6 +4,9 @@ import android.util.Log import com.github.damontecres.wholphin.BuildConfig import timber.log.Timber +/** + * Enable debug logging via [Timber] if enabled in the app settings + */ class DebugLogTree private constructor() : Timber.Tree() { // Only add logging for below INFO, production logger in WholphinApplication logs >=INFO override fun isLoggable( diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/FocusPair.kt b/app/src/main/java/com/github/damontecres/wholphin/util/FocusPair.kt deleted file mode 100644 index e44d531a..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/FocusPair.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.damontecres.wholphin.util - -import androidx.compose.ui.focus.FocusRequester - -data class FocusPair( - val row: Int, - val column: Int, - val focusRequester: FocusRequester, -) diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt index 1c69d947..46ec3606 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt @@ -3,6 +3,9 @@ package com.github.damontecres.wholphin.util import com.github.damontecres.wholphin.preferences.UserPreferences import java.util.UUID +/** + * Functions to remember tabs choices for a library + */ interface RememberTabManager { /** * If enabled, get the remembered tab index for the given item diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/TransformList.kt b/app/src/main/java/com/github/damontecres/wholphin/util/TransformList.kt index 61946d60..a0bfc08b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/TransformList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/TransformList.kt @@ -1,5 +1,8 @@ package com.github.damontecres.wholphin.util +/** + * A utility class to lazily transforms items from the source list + */ class TransformList( private val source: List, private val transform: (S) -> T, diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Version.kt b/app/src/main/java/com/github/damontecres/wholphin/util/Version.kt index 03303ac8..b670326b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/Version.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/Version.kt @@ -2,6 +2,9 @@ package com.github.damontecres.wholphin.util import kotlinx.serialization.Serializable +/** + * Represents a version in the format of `..--g` as output by `git describe` + */ @Serializable data class Version( val major: Int, diff --git a/app/src/patches/play_store.patch b/app/src/patches/play_store.patch index ef5fb96d..c1214f0e 100644 --- a/app/src/patches/play_store.patch +++ b/app/src/patches/play_store.patch @@ -1,14 +1,10 @@ -commit f82fb1a2d8ff9917a7bdb0bc7101a0474359ccdc -Author: Damontecres -Date: Sat Nov 22 13:00:55 2025 -0500 +# This patches Wholphin for releasing on app stores where self updating is not permitted - Setup for play store - -diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml -index 6d84299..12576af 100644 ---- a/app/src/main/AndroidManifest.xml -+++ b/app/src/main/AndroidManifest.xml -@@ -4,7 +4,6 @@ +diff --git i/app/src/main/AndroidManifest.xml w/app/src/main/AndroidManifest.xml +index 4479ae86..80596698 100644 +--- i/app/src/main/AndroidManifest.xml ++++ w/app/src/main/AndroidManifest.xml +@@ -6,7 +6,6 @@ @@ -16,7 +12,7 @@ index 6d84299..12576af 100644 -@@ -17,7 +16,7 @@ +@@ -20,7 +19,7 @@ android:required="false" /> -diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt -index c7ac435..fa42fe1 100644 ---- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt -+++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt -@@ -62,7 +62,7 @@ class UpdateChecker +diff --git i/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt w/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +index 9e0665dd..bc1e1c16 100644 +--- i/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt ++++ w/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +@@ -65,7 +65,7 @@ class UpdateChecker private val NOTE_REGEX = Regex("") @@ -37,4 +33,4 @@ index c7ac435..fa42fe1 100644 + val ACTIVE = false } - suspend fun maybeShowUpdateToast( + /**