diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d6302d73..6d075fd8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -25,14 +25,17 @@ After forking and cloning your fork, you can import the project into Android Stu You need a compatible Android Studio version for the configured AGP. This is generally `Narwhal 3 Feature Drop | 2025.1.3` or newer. See https://developer.android.com/build/releases/gradle-plugin and [`libs.versions.toml](./gradle/libs.versions.toml). +### Code organization + +Code is split into several packages: +- `data` - app-specific data models and services +- `preferences` - Non-UI related code for user settings and preferences +- `services` - hilt injectable services often used by ViewModels for API calls +- `ui` - User interface code and ViewModels +- `util` - Utility classes and functions + ### Native components -Wholphin includes some optional native components that are compiled outside of the normal Android gradle build process. - -These components are not generally required to build or test the app during development. - -If you want to build any of them locally, you must have the [Android NDK](https://developer.android.com/ndk) installed. - #### FFmpeg decoder module Wholphin ships with [media3 ffmpeg decoder module](https://github.com/androidx/media/blob/release/libraries/decoder_ffmpeg/README.md). diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index a06735c4..fbacf6e8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -27,20 +27,20 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Surface import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.AppUpgradeHandler +import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver +import com.github.damontecres.wholphin.services.ServerEventListener +import com.github.damontecres.wholphin.services.UpdateChecker +import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.ui.CoilConfig import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.ApplicationContent import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.theme.WholphinTheme -import com.github.damontecres.wholphin.util.AppUpgradeHandler -import com.github.damontecres.wholphin.util.PlaybackLifecycleObserver -import com.github.damontecres.wholphin.util.ServerEventListener -import com.github.damontecres.wholphin.util.UpdateChecker import com.github.damontecres.wholphin.util.profile.createDeviceProfile import dagger.hilt.android.AndroidEntryPoint import okhttp3.OkHttpClient 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 e3ffbcbd..2188e9b7 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 @@ -2,7 +2,7 @@ package com.github.damontecres.wholphin.data.model import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.util.seasonEpisode +import com.github.damontecres.wholphin.ui.seasonEpisode import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import org.jellyfin.sdk.api.client.ApiClient 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 09acd57e..6330113e 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 @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.request.GetItemsRequest +import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.serializer.UUIDSerializer import java.util.UUID @@ -19,6 +20,7 @@ data class GetItemsFilter( val studios: List? = null, val tags: List? = null, val includeItemTypes: List? = null, + val override: GetItemsFilterOverride = GetItemsFilterOverride.NONE, ) { fun applyTo(req: GetItemsRequest) = req.copy( @@ -31,4 +33,14 @@ data class GetItemsFilter( studioIds = studios, tags = tags, ) + + fun applyTo(req: GetPersonsRequest) = + req.copy( + isFavorite = favorite, + ) +} + +enum class GetItemsFilterOverride { + NONE, + PERSON, } 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 398d333d..3430640e 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 @@ -13,6 +13,7 @@ data class Person( val role: String?, val type: PersonKind, val imageUrl: String?, + val favorite: Boolean, ) { companion object { fun fromDto( @@ -25,6 +26,21 @@ data class Person( role = dto.role, type = dto.type, imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY), + favorite = false, + ) + + fun fromDto( + dto: BaseItemPerson, + favorite: Boolean, + api: ApiClient, + ): Person = + Person( + id = dto.id, + name = dto.name, + role = dto.role, + type = dto.type, + imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY), + favorite = favorite, ) } } 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 a4ab91ad..c4cd3792 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 @@ -1,34 +1,10 @@ package com.github.damontecres.wholphin.data.model -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue -import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.ui.DefaultItemFields -import com.github.damontecres.wholphin.ui.indexOfFirstOrNull -import com.github.damontecres.wholphin.ui.toServerString -import com.github.damontecres.wholphin.util.ApiRequestPager -import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler -import com.github.damontecres.wholphin.util.GetItemsRequestHandler -import com.github.damontecres.wholphin.util.GetPlaylistItemsRequestHandler -import com.github.damontecres.wholphin.util.TransformList -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineScope -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.playlistsApi -import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CreatePlaylistDto import org.jellyfin.sdk.model.api.MediaType -import org.jellyfin.sdk.model.api.PlaylistUserPermissions -import org.jellyfin.sdk.model.api.request.GetEpisodesRequest -import org.jellyfin.sdk.model.api.request.GetItemsRequest -import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest -import org.jellyfin.sdk.model.serializer.toUUIDOrNull import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton class Playlist( items: List, @@ -71,107 +47,3 @@ data class PlaylistInfo( val count: Int, val mediaType: MediaType, ) - -@Singleton -class PlaylistCreator - @Inject - constructor( - @param:ApplicationContext private val context: Context, - private val api: ApiClient, - private val serverRepository: ServerRepository, - ) { - suspend fun createFromEpisode( - seriesId: UUID, - episodeId: UUID, - ): Playlist { - val request = - GetEpisodesRequest( - seriesId = seriesId, - fields = DefaultItemFields, - startItemId = episodeId, - limit = Playlist.MAX_SIZE, - ) - val episodes = GetEpisodesRequestHandler.execute(api, request).content.items - val startIndex = - episodes.indexOfFirstOrNull { it.id == episodeId } - ?: throw IllegalStateException("Episode $episodeId was not returned") - return Playlist(episodes.map { BaseItem.from(it, api) }, startIndex) - } - - suspend fun createFromPlaylistId( - playlistId: UUID, - startIndex: Int?, - shuffled: Boolean, - ): Playlist { - val request = - GetPlaylistItemsRequest( - playlistId = playlistId, - fields = DefaultItemFields, - startIndex = startIndex, - limit = Playlist.MAX_SIZE, - ) - val items = GetPlaylistItemsRequestHandler.execute(api, request).content.items - var baseItems = items.map { BaseItem.from(it, api) } - if (shuffled) { - baseItems = baseItems.shuffled() - } - return Playlist(baseItems, 0) - } - - suspend fun getPlaylists( - mediaType: MediaType?, - scope: CoroutineScope, - ): List { - val request = - GetItemsRequest( - includeItemTypes = listOf(BaseItemKind.PLAYLIST), - mediaTypes = mediaType?.let { listOf(mediaType) }, - recursive = true, - ) - val pager = ApiRequestPager(api, request, GetItemsRequestHandler, scope).init() - return TransformList(pager) { - it?.let { - PlaylistInfo( - id = it.id, - name = it.name ?: context.getString(R.string.unknown), - count = it.data.childCount ?: 0, - mediaType = it.data.mediaType, - ) - } - } - } - - suspend fun createPlaylist( - name: String, - initialItems: List, - ): UUID? = - serverRepository.currentUser.value?.let { user -> - api.playlistsApi - .createPlaylist( - CreatePlaylistDto( - name = name, - ids = initialItems, - users = listOf(PlaylistUserPermissions(user.id, true)), - isPublic = false, - ), - ).content.id - .toUUIDOrNull() - } - - suspend fun addToPlaylist( - playlistId: UUID, - itemId: UUID, - ) { - api.playlistsApi.addItemToPlaylist(playlistId, listOf(itemId)) - } - - suspend fun removeFromPlaylist( - playlistId: UUID, - itemId: UUID, - ) { - api.playlistsApi.removeItemFromPlaylist( - playlistId.toServerString(), - listOf(itemId.toServerString()), - ) - } - } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt similarity index 89% rename from app/src/main/java/com/github/damontecres/wholphin/util/AppUpgradeHandler.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index ccfd0e42..ba7de582 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.wholphin.util +package com.github.damontecres.wholphin.services import android.content.Context import android.os.Build @@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.updateInterfacePreferences import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel +import com.github.damontecres.wholphin.util.Version import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import java.io.File @@ -51,8 +52,8 @@ class AppUpgradeHandler copySubfont(true) upgradeApp( context, - Version.fromString(previousVersion ?: "0.0.0"), - Version.fromString(newVersion), + Version.Companion.fromString(previousVersion ?: "0.0.0"), + Version.Companion.fromString(newVersion), appPreferences, ) } catch (ex: Exception) { @@ -96,7 +97,7 @@ suspend fun upgradeApp( current: Version, appPreferences: DataStore, ) { - if (previous.isEqualOrBefore(Version.fromString("0.1.0-2-g0"))) { + if (previous.isEqualOrBefore(Version.Companion.fromString("0.1.0-2-g0"))) { appPreferences.updateData { it.updatePlaybackOverrides { ac3Supported = AppPreference.Ac3Supported.defaultValue @@ -106,21 +107,21 @@ suspend fun upgradeApp( } } } - if (previous.isEqualOrBefore(Version.fromString("0.2.3-6-g0"))) { + if (previous.isEqualOrBefore(Version.Companion.fromString("0.2.3-6-g0"))) { appPreferences.updateData { it.updateInterfacePreferences { navDrawerSwitchOnFocus = AppPreference.NavDrawerSwitchOnFocus.defaultValue } } } - if (previous.isEqualOrBefore(Version.fromString("0.2.5-11-g0"))) { + if (previous.isEqualOrBefore(Version.Companion.fromString("0.2.5-11-g0"))) { appPreferences.updateData { it.updateInterfacePreferences { showClock = AppPreference.ShowClock.defaultValue } } } - if (previous.isEqualOrBefore(Version.fromString("0.2.7-1-g0"))) { + if (previous.isEqualOrBefore(Version.Companion.fromString("0.2.7-1-g0"))) { PreferencesViewModel.resetSubtitleSettings(appPreferences) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/FavoriteWatchManager.kt b/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt similarity index 95% rename from app/src/main/java/com/github/damontecres/wholphin/util/FavoriteWatchManager.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt index 72540435..6a3e9a68 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/FavoriteWatchManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/FavoriteWatchManager.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.wholphin.util +package com.github.damontecres.wholphin.services import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.playStateApi diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationManager.kt b/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt similarity index 88% rename from app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationManager.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt index d07a9f9b..d9e18ef5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavigationManager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/NavigationManager.kt @@ -1,6 +1,7 @@ -package com.github.damontecres.wholphin.ui.nav +package com.github.damontecres.wholphin.services import androidx.navigation3.runtime.NavKey +import com.github.damontecres.wholphin.ui.nav.Destination import org.acra.ACRA import timber.log.Timber import javax.inject.Inject @@ -16,7 +17,7 @@ class NavigationManager var backStack: MutableList = mutableListOf() /** - * Go to the specified [Destination] + * Go to the specified [com.github.damontecres.wholphin.ui.nav.Destination] */ fun navigateTo(destination: Destination) { backStack.add(destination) @@ -70,7 +71,7 @@ class NavigationManager private fun log() { val dest = backStack.lastOrNull().toString() - Timber.i("Current Destination: %s", dest) + Timber.Forest.i("Current Destination: %s", dest) ACRA.errorReporter.putCustomData("destination", dest) } } 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 new file mode 100644 index 00000000..b4967b03 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PeopleFavorites.kt @@ -0,0 +1,34 @@ +package com.github.damontecres.wholphin.services + +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.ui.letNotEmpty +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.itemsApi +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PeopleFavorites + @Inject + constructor( + private val api: ApiClient, + ) { + suspend fun getPeopleFor(item: BaseItem): List = + item.data.people.orEmpty().map { it.id }.chunked(50).let { chunks -> + val favorites = + chunks + .map { + api.itemsApi + .getItems(ids = it) + .content.items + }.flatten() + .associateBy({ it.id }, { it.userData?.isFavorite ?: false }) + val people = + item.data.people + ?.letNotEmpty { people -> + people.map { Person.fromDto(it, favorites[it.id] ?: false, api) } + }.orEmpty() + people + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlaybackLifecycleObserver.kt similarity index 93% rename from app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/PlaybackLifecycleObserver.kt index be93c292..86906280 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/PlaybackLifecycleObserver.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaybackLifecycleObserver.kt @@ -1,9 +1,8 @@ -package com.github.damontecres.wholphin.util +package com.github.damontecres.wholphin.services import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager import dagger.hilt.android.scopes.ActivityRetainedScoped import javax.inject.Inject diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt similarity index 98% rename from app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt index f841fa9c..57dc781b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/PlayerFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt @@ -1,6 +1,6 @@ @file:OptIn(markerClass = [UnstableApi::class]) -package com.github.damontecres.wholphin.util +package com.github.damontecres.wholphin.services import android.content.Context import androidx.annotation.OptIn 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 new file mode 100644 index 00000000..c7730d9e --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt @@ -0,0 +1,135 @@ +package com.github.damontecres.wholphin.services + +import android.content.Context +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.ServerRepository +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.Playlist +import com.github.damontecres.wholphin.data.model.PlaylistInfo +import com.github.damontecres.wholphin.ui.DefaultItemFields +import com.github.damontecres.wholphin.ui.indexOfFirstOrNull +import com.github.damontecres.wholphin.ui.toServerString +import com.github.damontecres.wholphin.util.ApiRequestPager +import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler +import com.github.damontecres.wholphin.util.GetItemsRequestHandler +import com.github.damontecres.wholphin.util.GetPlaylistItemsRequestHandler +import com.github.damontecres.wholphin.util.TransformList +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.playlistsApi +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.CreatePlaylistDto +import org.jellyfin.sdk.model.api.MediaType +import org.jellyfin.sdk.model.api.PlaylistUserPermissions +import org.jellyfin.sdk.model.api.request.GetEpisodesRequest +import org.jellyfin.sdk.model.api.request.GetItemsRequest +import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest +import org.jellyfin.sdk.model.serializer.toUUIDOrNull +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PlaylistCreator + @Inject + constructor( + @param:ApplicationContext private val context: Context, + private val api: ApiClient, + private val serverRepository: ServerRepository, + ) { + suspend fun createFromEpisode( + seriesId: UUID, + episodeId: UUID, + ): Playlist { + val request = + GetEpisodesRequest( + seriesId = seriesId, + fields = DefaultItemFields, + startItemId = episodeId, + limit = Playlist.Companion.MAX_SIZE, + ) + val episodes = GetEpisodesRequestHandler.execute(api, request).content.items + val startIndex = + episodes.indexOfFirstOrNull { it.id == episodeId } + ?: throw IllegalStateException("Episode $episodeId was not returned") + return Playlist(episodes.map { BaseItem.Companion.from(it, api) }, startIndex) + } + + suspend fun createFromPlaylistId( + playlistId: UUID, + startIndex: Int?, + shuffled: Boolean, + ): Playlist { + val request = + GetPlaylistItemsRequest( + playlistId = playlistId, + fields = DefaultItemFields, + startIndex = startIndex, + limit = Playlist.Companion.MAX_SIZE, + ) + val items = GetPlaylistItemsRequestHandler.execute(api, request).content.items + var baseItems = items.map { BaseItem.Companion.from(it, api) } + if (shuffled) { + baseItems = baseItems.shuffled() + } + return Playlist(baseItems, 0) + } + + suspend fun getPlaylists( + mediaType: MediaType?, + scope: CoroutineScope, + ): List { + val request = + GetItemsRequest( + includeItemTypes = listOf(BaseItemKind.PLAYLIST), + mediaTypes = mediaType?.let { listOf(mediaType) }, + recursive = true, + ) + val pager = ApiRequestPager(api, request, GetItemsRequestHandler, scope).init() + return TransformList(pager) { + it?.let { + PlaylistInfo( + id = it.id, + name = it.name ?: context.getString(R.string.unknown), + count = it.data.childCount ?: 0, + mediaType = it.data.mediaType, + ) + } + } + } + + suspend fun createPlaylist( + name: String, + initialItems: List, + ): UUID? = + serverRepository.currentUser.value?.let { user -> + api.playlistsApi + .createPlaylist( + CreatePlaylistDto( + name = name, + ids = initialItems, + users = listOf(PlaylistUserPermissions(user.id, true)), + isPublic = false, + ), + ).content.id + .toUUIDOrNull() + } + + suspend fun addToPlaylist( + playlistId: UUID, + itemId: UUID, + ) { + api.playlistsApi.addItemToPlaylist(playlistId, listOf(itemId)) + } + + suspend fun removeFromPlaylist( + playlistId: UUID, + itemId: UUID, + ) { + api.playlistsApi.removeItemFromPlaylist( + playlistId.toServerString(), + listOf(itemId.toServerString()), + ) + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/ServerEventListener.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt similarity index 98% rename from app/src/main/java/com/github/damontecres/wholphin/util/ServerEventListener.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt index f792f2f9..f71c902a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/ServerEventListener.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ServerEventListener.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.wholphin.util +package com.github.damontecres.wholphin.services import android.content.Context import android.widget.Toast diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt similarity index 96% rename from app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt index bb9ec887..f43df5cc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/ThemeSongPlayer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ThemeSongPlayer.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.wholphin.util +package com.github.damontecres.wholphin.services import android.content.Context import androidx.annotation.OptIn @@ -8,8 +8,8 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import com.github.damontecres.wholphin.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.preferences.ThemeSongVolume +import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.util.profile.Codec import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers 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 new file mode 100644 index 00000000..42f56b04 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/services/TrailerService.kt @@ -0,0 +1,81 @@ +package com.github.damontecres.wholphin.services + +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.LocalTrailer +import com.github.damontecres.wholphin.data.model.RemoteTrailer +import com.github.damontecres.wholphin.data.model.Trailer +import com.github.damontecres.wholphin.ui.nav.Destination +import dagger.hilt.android.qualifiers.ApplicationContext +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.userLibraryApi +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TrailerService + @Inject + constructor( + @param:ApplicationContext private val context: Context, + private val api: ApiClient, + ) { + suspend fun getTrailers(item: BaseItem): List { + val remoteTrailers = + item.data.remoteTrailers + ?.mapNotNull { t -> + t.url?.let { url -> + val name = + t.name + // TODO would be nice to clean up the trailer name +// ?.replace(item.name ?: "", "") +// ?.removePrefix(" - ") + ?: context.getString(R.string.trailer) + RemoteTrailer(name, url) + } + }.orEmpty() + .sortedBy { it.name } + val localTrailerCount = item.data.localTrailerCount ?: 0 + val localTrailers = + if (localTrailerCount > 0) { + api.userLibraryApi.getLocalTrailers(item.id).content.map { + LocalTrailer(BaseItem.Companion.from(it, api)) + } + } else { + listOf() + } + return localTrailers + remoteTrailers + } + + companion object { + /** + * Note: This is explicitly not a member function because the injected Context is not an Activity. + * We want to start the intent without a new task which requires the Activity context + * + * This can be provided by LocalContext.current from Compose + */ + fun onClick( + context: Context, + trailer: Trailer, + navigateTo: (Destination) -> Unit, + ) { + when (trailer) { + is LocalTrailer -> + navigateTo.invoke( + Destination.Playback( + itemId = trailer.baseItem.id, + item = trailer.baseItem, + positionMs = 0L, + ), + ) + + is RemoteTrailer -> { + val intent = Intent(Intent.ACTION_VIEW, trailer.url.toUri()) + context.startActivity(intent) + } + } + } + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt similarity index 97% rename from app/src/main/java/com/github/damontecres/wholphin/util/UpdateChecker.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt index 144e566d..4b6b2b42 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/UpdateChecker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.wholphin.util +package com.github.damontecres.wholphin.services import android.Manifest import android.content.ContentValues @@ -15,9 +15,10 @@ import androidx.core.content.FileProvider import androidx.core.content.edit import androidx.preference.PreferenceManager import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.hilt.StandardOkHttpClient +import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.showToast +import com.github.damontecres.wholphin.util.Version import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -110,7 +111,7 @@ class UpdateChecker fun getInstalledVersion(): Version { val pkgInfo = context.packageManager.getPackageInfo(context.packageName, 0) - return Version.fromString(pkgInfo.versionName!!) + return Version.Companion.fromString(pkgInfo.versionName!!) } suspend fun getLatestRelease(updateUrl: String): Release? { @@ -135,7 +136,7 @@ class UpdateChecker if (it.isSuccessful && it.body != null) { val result = Json.parseToJsonElement(it.body!!.string()) val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull - val version = Version.tryFromString(name) + val version = Version.Companion.tryFromString(name) val publishedAt = result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull @@ -236,7 +237,7 @@ class UpdateChecker val targetFile = fallbackDownload(it, callback) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val intent = Intent(Intent.ACTION_INSTALL_PACKAGE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) intent.data = FileProvider.getUriForFile( context, diff --git a/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt similarity index 96% rename from app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt index 6d55525d..8d2d2e79 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/hilt/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.wholphin.hilt +package com.github.damontecres.wholphin.services.hilt import android.annotation.SuppressLint import android.content.Context @@ -10,9 +10,9 @@ import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.updateInterfacePreferences +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.FavoriteWatchManagerImpl import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.FavoriteWatchManager -import com.github.damontecres.wholphin.util.FavoriteWatchManagerImpl import com.github.damontecres.wholphin.util.RememberTabManager import dagger.Module import dagger.Provides diff --git a/app/src/main/java/com/github/damontecres/wholphin/hilt/DatabaseModule.kt b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt similarity index 97% rename from app/src/main/java/com/github/damontecres/wholphin/hilt/DatabaseModule.kt rename to app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt index 441a9033..dbf61912 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/hilt/DatabaseModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.wholphin.hilt +package com.github.damontecres.wholphin.services.hilt import android.content.Context import androidx.datastore.core.DataStore diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt similarity index 98% rename from app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt rename to app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt index 9622dee7..1e5c1ae3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/Formatting.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Formatting.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.wholphin.util +package com.github.damontecres.wholphin.ui import android.os.Build import androidx.annotation.StringRes @@ -10,7 +10,7 @@ import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.data.model.chooseStream import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.util.languageName import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.MediaSegmentType import org.jellyfin.sdk.model.api.MediaStream diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt index 364daccb..9eafb1ca 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/EpisodeCard.kt @@ -31,7 +31,7 @@ import androidx.tv.material3.Text import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.enableMarquee -import com.github.damontecres.wholphin.util.seasonEpisode +import com.github.damontecres.wholphin.ui.seasonEpisode import kotlinx.coroutines.delay @Composable @@ -105,7 +105,10 @@ fun EpisodeCard( modifier = Modifier .align(Alignment.TopEnd) - .background(AppColors.TransparentBlack50, shape = RoundedCornerShape(8.dp)), + .background( + AppColors.TransparentBlack50, + shape = RoundedCornerShape(8.dp), + ), ) { Text( text = dto?.seasonEpisode ?: "", @@ -116,7 +119,10 @@ fun EpisodeCard( } Column( verticalArrangement = Arrangement.spacedBy(0.dp), - modifier = Modifier.padding(bottom = spaceBelow).fillMaxWidth(), + modifier = + Modifier + .padding(bottom = spaceBelow) + .fillMaxWidth(), ) { Text( text = dto?.seriesName ?: "", diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt index 07545a53..dcde2ff0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/PersonCard.kt @@ -72,8 +72,8 @@ fun PersonCard( ItemCardImage( imageUrl = item.imageUrl, name = item.name, - showOverlay = false, - favorite = false, + showOverlay = true, + favorite = item.favorite, watched = false, unwatchedCount = -1, watchedPercent = null, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index 2426c44a..17ddd578 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -34,8 +34,11 @@ import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.GetItemsFilter +import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel @@ -46,13 +49,12 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetItemsRequestHandler +import com.github.damontecres.wholphin.util.GetPersonsHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import dagger.hilt.android.lifecycle.HiltViewModel @@ -69,6 +71,7 @@ import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.MediaType import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.request.GetItemsRequest +import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.serializer.toUUIDOrNull import java.util.UUID import javax.inject.Inject @@ -107,13 +110,12 @@ class CollectionFolderViewModel } val sortAndDirection = - if (initialSortAndDirection == null) { - serverRepository.currentUser.value?.let { user -> - libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection - } ?: SortAndDirection.DEFAULT - } else { - SortAndDirection.DEFAULT - } + initialSortAndDirection + ?: ( + serverRepository.currentUser.value?.let { user -> + libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection + } ?: SortAndDirection.DEFAULT + ) loadResults(sortAndDirection, recursive, filter) } @@ -143,7 +145,6 @@ class CollectionFolderViewModel recursive: Boolean, filter: GetItemsFilter, ) { - val item = item.value viewModelScope.launch(Dispatchers.IO) { withContext(Dispatchers.Main) { pager.value = listOf() @@ -151,62 +152,7 @@ class CollectionFolderViewModel this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection this@CollectionFolderViewModel.filter.value = filter } - val includeItemTypes = - when (item?.data?.collectionType) { - CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE) - CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES) - CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO) - CollectionType.MUSIC -> - listOf( - BaseItemKind.AUDIO, - BaseItemKind.MUSIC_ARTIST, - BaseItemKind.MUSIC_ALBUM, - ) - - CollectionType.BOXSETS -> listOf(BaseItemKind.BOX_SET) - CollectionType.PLAYLISTS -> listOf(BaseItemKind.PLAYLIST) - - else -> listOf() - } - val request = - filter.applyTo( - GetItemsRequest( - parentId = item?.id, - enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), - includeItemTypes = includeItemTypes, - recursive = recursive, - excludeItemIds = item?.let { listOf(item.id) }, - sortBy = - buildList { - add(sortAndDirection.sort) - if (sortAndDirection.sort != ItemSortBy.SORT_NAME) { - add(ItemSortBy.SORT_NAME) - } - if (item?.data?.collectionType == CollectionType.MOVIES) { - add(ItemSortBy.PRODUCTION_YEAR) - } - }, - sortOrder = - buildList { - add(sortAndDirection.direction) - if (sortAndDirection.sort != ItemSortBy.SORT_NAME) { - add(SortOrder.ASCENDING) - } - if (item?.data?.collectionType == CollectionType.MOVIES) { - add(SortOrder.ASCENDING) - } - }, - fields = SlimItemFields, - ), - ) - val newPager = - ApiRequestPager( - api, - request, - GetItemsRequestHandler, - viewModelScope, - useSeriesForPrimary = true, - ) + val newPager = createPager(sortAndDirection, recursive, filter) newPager.init() if (newPager.isNotEmpty()) newPager.getBlocking(0) withContext(Dispatchers.Main) { @@ -216,6 +162,93 @@ class CollectionFolderViewModel } } + private fun createPager( + sortAndDirection: SortAndDirection, + recursive: Boolean, + filter: GetItemsFilter, + ): ApiRequestPager { + val item = item.value + return when (filter.override) { + GetItemsFilterOverride.NONE -> { + val includeItemTypes = + when (item?.data?.collectionType) { + CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE) + CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES) + CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO) + CollectionType.MUSIC -> + listOf( + BaseItemKind.AUDIO, + BaseItemKind.MUSIC_ARTIST, + BaseItemKind.MUSIC_ALBUM, + ) + + CollectionType.BOXSETS -> listOf(BaseItemKind.BOX_SET) + CollectionType.PLAYLISTS -> listOf(BaseItemKind.PLAYLIST) + + else -> listOf() + } + val request = + filter.applyTo( + GetItemsRequest( + parentId = item?.id, + enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), + includeItemTypes = includeItemTypes, + recursive = recursive, + excludeItemIds = item?.let { listOf(item.id) }, + sortBy = + buildList { + add(sortAndDirection.sort) + if (sortAndDirection.sort != ItemSortBy.SORT_NAME) { + add(ItemSortBy.SORT_NAME) + } + if (item?.data?.collectionType == CollectionType.MOVIES) { + add(ItemSortBy.PRODUCTION_YEAR) + } + }, + sortOrder = + buildList { + add(sortAndDirection.direction) + if (sortAndDirection.sort != ItemSortBy.SORT_NAME) { + add(SortOrder.ASCENDING) + } + if (item?.data?.collectionType == CollectionType.MOVIES) { + add(SortOrder.ASCENDING) + } + }, + fields = SlimItemFields, + ), + ) + val newPager = + ApiRequestPager( + api, + request, + GetItemsRequestHandler, + viewModelScope, + useSeriesForPrimary = true, + ) + newPager + } + + GetItemsFilterOverride.PERSON -> { + val request = + filter.applyTo( + GetPersonsRequest( + enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), + ), + ) + val newPager = + ApiRequestPager( + api, + request, + GetPersonsHandler, + viewModelScope, + useSeriesForPrimary = true, + ) + newPager + } + } + } + suspend fun positionOfLetter(letter: Char): Int? = withContext(Dispatchers.IO) { item.value?.let { item -> @@ -437,12 +470,14 @@ fun CollectionFolderGridContent( modifier = Modifier.fillMaxWidth(), ) } - SortByButton( - sortOptions = sortOptions, - current = sortAndDirection, - onSortChange = onSortChange, - modifier = Modifier, - ) + if (sortOptions.isNotEmpty()) { + SortByButton( + sortOptions = sortOptions, + current = sortAndDirection, + onSortChange = onSortChange, + modifier = Modifier, + ) + } } CardGrid( pager = pager, 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 2949428d..40e74e8d 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 @@ -16,12 +16,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.GetItemsFilter +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.cards.GridCard import com.github.damontecres.wholphin.ui.detail.CardGrid import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.GetGenresRequestHandler 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 55e37c73..3a561785 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 @@ -18,6 +18,8 @@ import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel import com.github.damontecres.wholphin.ui.data.RowColumn @@ -27,9 +29,7 @@ import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.main.HomePageContent -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.util.ApiRequestPager -import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingState import kotlinx.coroutines.flow.MutableStateFlow @@ -42,7 +42,7 @@ abstract class RecommendedViewModel( ) : ViewModel() { abstract fun init() - abstract val rows: MutableStateFlow> + abstract val rows: MutableStateFlow> val loading = MutableLiveData(LoadingState.Loading) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt index 48388419..22cd6175 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMovie.kt @@ -8,13 +8,13 @@ import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler import com.github.damontecres.wholphin.util.GetSuggestionsRequestHandler @@ -57,13 +57,13 @@ class RecommendedMovieViewModel } override val rows = - MutableStateFlow>( + MutableStateFlow>( rowTitles .map { HomeRowLoadingState.Pending( context.getString(it), ) - }.toMutableList(), + }, ) override fun init() { @@ -166,6 +166,9 @@ class RecommendedMovieViewModel val result = try { pager.init() + if (pager.isNotEmpty()) { + pager.getBlocking(0) + } HomeRowLoadingState.Success(title, pager) } catch (ex: Exception) { Timber.e(ex, "Error fetching %s", title) @@ -182,7 +185,7 @@ class RecommendedMovieViewModel row: HomeRowLoadingState, ) { rows.update { current -> - current.apply { set(position, row) } + current.toMutableList().apply { set(position, row) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt index f504c614..7144d010 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedTvShow.kt @@ -8,13 +8,13 @@ import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetNextUpRequestHandler import com.github.damontecres.wholphin.util.GetResumeItemsRequestHandler @@ -59,13 +59,13 @@ class RecommendedTvShowViewModel } override val rows = - MutableStateFlow>( + MutableStateFlow>( rowTitles .map { HomeRowLoadingState.Pending( context.getString(it), ) - }.toMutableList(), + }, ) override fun init() { @@ -210,7 +210,7 @@ class RecommendedTvShowViewModel row: HomeRowLoadingState, ) { rows.update { current -> - current.apply { set(position, row) } + current.toMutableList().apply { set(position, row) } } } 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 2287804c..555ac5fa 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 @@ -5,7 +5,7 @@ import android.widget.Toast import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.github.damontecres.wholphin.data.model.PlaylistCreator +import com.github.damontecres.wholphin.services.PlaylistCreator import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.setValueOnMain 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 63b904b6..d305bfe1 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 @@ -14,11 +14,11 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.Text import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.ui.byteRateSuffixes import com.github.damontecres.wholphin.ui.components.ScrollableDialog +import com.github.damontecres.wholphin.ui.formatBytes import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.letNotEmpty -import com.github.damontecres.wholphin.util.byteRateSuffixes -import com.github.damontecres.wholphin.util.formatBytes import com.github.damontecres.wholphin.util.languageName import org.jellyfin.sdk.model.api.AudioSpatialFormat import org.jellyfin.sdk.model.api.MediaSourceInfo diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt index 3cb6635b..634be82f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/SortAndDirection.kt @@ -79,5 +79,6 @@ fun getStringRes(sort: ItemSortBy): Int = ItemSortBy.CRITIC_RATING -> R.string.critic_rating ItemSortBy.PLAY_COUNT -> R.string.play_count ItemSortBy.AIRED_EPISODE_ORDER -> R.string.aired_episode_order + ItemSortBy.DEFAULT -> R.string.default_track else -> throw IllegalArgumentException("Unsupported sort option: $sort") } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt index d7e364fa..83974283 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt @@ -31,6 +31,7 @@ import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.TabRow @@ -40,7 +41,6 @@ import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.RememberTabManager 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 5fabe479..d62d0f73 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 @@ -288,7 +288,6 @@ fun buildMoreDialogItemsForHome( fun buildMoreDialogItemsForPerson( context: Context, person: Person, -// favorite: Boolean, actions: MoreDialogActions, ): List = buildList { @@ -301,14 +300,13 @@ fun buildMoreDialogItemsForPerson( actions.navigateTo(Destination.MediaItem(itemId, BaseItemKind.PERSON)) }, ) - // TODO need way to get person's favorite status -// add( -// DialogItem( -// text = if (favorite) R.string.remove_favorite else R.string.add_favorite, -// iconStringRes = R.string.fa_heart, -// iconColor = if (favorite) Color.Red else Color.Unspecified, -// ) { -// actions.onClickFavorite.invoke(itemId, !favorite) -// }, -// ) + add( + DialogItem( + text = if (person.favorite) R.string.remove_favorite else R.string.add_favorite, + iconStringRes = R.string.fa_heart, + iconColor = if (person.favorite) Color.Red else Color.Unspecified, + ) { + actions.onClickFavorite.invoke(itemId, !person.favorite) + }, + ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt index 0afa076d..dac2b915 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt @@ -25,6 +25,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.GetItemsFilter +import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid import com.github.damontecres.wholphin.ui.components.ErrorMessage @@ -32,11 +33,15 @@ import com.github.damontecres.wholphin.ui.components.TabRow import com.github.damontecres.wholphin.ui.data.EpisodeSortOptions import com.github.damontecres.wholphin.ui.data.MovieSortOptions import com.github.damontecres.wholphin.ui.data.SeriesSortOptions +import com.github.damontecres.wholphin.ui.data.SortAndDirection +import com.github.damontecres.wholphin.ui.data.VideoSortOptions import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.nav.NavDrawerItem import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.tryRequestFocus import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder @Composable fun FavoritesPage( @@ -59,6 +64,9 @@ fun FavoritesPage( stringResource(R.string.movies), stringResource(R.string.tv_shows), stringResource(R.string.episodes), + stringResource(R.string.videos), + stringResource(R.string.playlists), + stringResource(R.string.people), ) var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } val focusRequester = remember { FocusRequester() } @@ -165,6 +173,81 @@ fun FavoritesPage( }, ) } + 3 -> { + CollectionFolderGrid( + preferences = preferences, + onClickItem = { _, item -> onClickItem.invoke(item) }, + itemId = "${NavDrawerItem.Favorites.id}_videos", + initialFilter = + GetItemsFilter( + favorite = true, + includeItemTypes = listOf(BaseItemKind.VIDEO), + ), + showTitle = false, + recursive = true, + sortOptions = VideoSortOptions, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + positionCallback = { columns, position -> + showHeader = position < columns + }, + ) + } + 4 -> { + CollectionFolderGrid( + preferences = preferences, + onClickItem = { _, item -> onClickItem.invoke(item) }, + itemId = "${NavDrawerItem.Favorites.id}_playlists", + initialFilter = + GetItemsFilter( + favorite = true, + includeItemTypes = listOf(BaseItemKind.PLAYLIST), + ), + showTitle = false, + recursive = true, + sortOptions = VideoSortOptions, + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + positionCallback = { columns, position -> + showHeader = position < columns + }, + ) + } + + 5 -> { + CollectionFolderGrid( + preferences = preferences, + onClickItem = { _, item -> onClickItem.invoke(item) }, + itemId = "${NavDrawerItem.Favorites.id}_people", + initialFilter = + GetItemsFilter( + favorite = true, + override = GetItemsFilterOverride.PERSON, + ), + initialSortAndDirection = + SortAndDirection( + ItemSortBy.DEFAULT, + SortOrder.ASCENDING, + ), + showTitle = false, + recursive = true, + sortOptions = listOf(), + modifier = + Modifier + .padding(start = 16.dp) + .fillMaxSize() + .focusRequester(focusRequester), + positionCallback = { columns, position -> + showHeader = position < columns + }, + ) + } else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt index db2ff356..5a33edcb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PersonPage.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -39,18 +41,22 @@ import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.components.ErrorMessage +import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingRow import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo +import com.github.damontecres.wholphin.ui.formatDate +import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.rememberPosition import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.theme.WholphinTheme @@ -60,7 +66,6 @@ import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.RowLoadingState -import com.github.damontecres.wholphin.util.formatDate import dagger.hilt.android.lifecycle.HiltViewModel import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.model.api.BaseItemKind @@ -78,6 +83,7 @@ class PersonViewModel constructor( api: ApiClient, val navigationManager: NavigationManager, + private val favoriteWatchManager: FavoriteWatchManager, ) : LoadingItemViewModel(api) { val movies = MutableLiveData(RowLoadingState.Pending) val series = MutableLiveData(RowLoadingState.Pending) @@ -144,6 +150,13 @@ class PersonViewModel } } } + + fun setFavorite(favorite: Boolean) { + viewModelScope.launchIO { + favoriteWatchManager.setFavorite(itemUuid, favorite) + fetchAndSetItem(itemUuid) + } + } } @Composable @@ -180,6 +193,7 @@ fun PersonPage( birthdate = person.data.premiereDate?.toLocalDate(), deathdate = person.data.endDate?.toLocalDate(), birthPlace = person.data.productionLocations?.firstOrNull(), + favorite = person.favorite, movies = movies, series = series, episodes = episodes, @@ -187,6 +201,9 @@ fun PersonPage( viewModel.navigationManager.navigateTo(item.destination()) }, overviewOnClick = { showOverviewDialog = true }, + favoriteOnClick = { + viewModel.setFavorite(!person.favorite) + }, modifier = modifier, ) AnimatedVisibility(showOverviewDialog) { @@ -220,11 +237,13 @@ fun PersonPageContent( birthdate: LocalDate?, deathdate: LocalDate?, birthPlace: String?, + favorite: Boolean, movies: RowLoadingState, series: RowLoadingState, episodes: RowLoadingState, onClickItem: (Int, BaseItem) -> Unit, overviewOnClick: () -> Unit, + favoriteOnClick: () -> Unit, modifier: Modifier = Modifier, ) { val focusRequester = remember { FocusRequester() } @@ -237,8 +256,10 @@ fun PersonPageContent( headerFocusRequester.tryRequestFocus() } } + var focusedOnHeader by remember { mutableStateOf(true) } LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), + userScrollEnabled = !focusedOnHeader, modifier = modifier .padding(start = 16.dp), @@ -251,13 +272,18 @@ fun PersonPageContent( birthdate = birthdate, birthPlace = birthPlace, deathdate = deathdate, + favorite = favorite, overviewOnClick = overviewOnClick, + favoriteOnClick = favoriteOnClick, modifier = Modifier .fillMaxWidth() .heightIn(max = 480.dp) .padding(top = 16.dp, bottom = 40.dp) - .focusRequester(headerFocusRequester), + .focusRequester(headerFocusRequester) + .onFocusChanged { + focusedOnHeader = it.hasFocus + }, ) } item { @@ -321,7 +347,9 @@ fun PersonHeader( birthdate: LocalDate?, deathdate: LocalDate?, birthPlace: String?, + favorite: Boolean, overviewOnClick: () -> Unit, + favoriteOnClick: () -> Unit, modifier: Modifier = Modifier, ) { val bringIntoViewRequester = remember { BringIntoViewRequester() } @@ -380,9 +408,9 @@ fun PersonHeader( overflow = TextOverflow.Ellipsis, ) } - birthPlace?.let { + if (birthPlace.isNotNullOrBlank()) { Text( - text = stringResource(R.string.birthplace) + ": $it", + text = stringResource(R.string.birthplace) + ": $birthPlace", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleSmall, maxLines = 1, @@ -411,9 +439,12 @@ fun PersonHeader( ) } val interactionSource = remember { MutableInteractionSource() } - val focused by interactionSource.collectIsFocusedAsState() - if (focused) { - LaunchedEffect(Unit) { + val buttonInteractionSource = remember { MutableInteractionSource() } + val focused = + interactionSource.collectIsFocusedAsState().value || + buttonInteractionSource.collectIsFocusedAsState().value + LaunchedEffect(focused) { + if (focused) { bringIntoViewRequester.bringIntoView() } } @@ -424,6 +455,14 @@ fun PersonHeader( interactionSource = interactionSource, modifier = Modifier.padding(top = 8.dp), ) + ExpandableFaButton( + title = if (favorite) R.string.remove_favorite else R.string.add_favorite, + iconStringRes = R.string.fa_heart, + onClick = favoriteOnClick, + iconColor = if (favorite) Color.Red else Color.Unspecified, + interactionSource = buttonInteractionSource, + modifier = Modifier, + ) } } } @@ -440,6 +479,8 @@ private fun PersonPreview() { birthPlace = "Phoenix, Arizona, USA", deathdate = LocalDate.of(2025, 2, 1), overviewOnClick = {}, + favorite = true, + favoriteOnClick = {}, modifier = Modifier.fillMaxWidth(), ) } 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 5ab05521..9d3b6eb5 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 @@ -56,6 +56,7 @@ import androidx.tv.material3.surfaceColorAtElevation import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.DefaultItemFields import com.github.damontecres.wholphin.ui.cards.ItemCardImage import com.github.damontecres.wholphin.ui.components.DialogItem @@ -70,7 +71,6 @@ import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt index 45578f4b..0b124977 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/DvrSchedule.kt @@ -34,17 +34,17 @@ import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager +import com.github.damontecres.wholphin.ui.seasonEpisode import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.seasonEpisode import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext 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 e8040056..6e4be864 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 @@ -8,12 +8,12 @@ import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.WholphinApplication import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.util.ExceptionHandler diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt index 933b20ef..c8f39df7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/ProgramDialog.kt @@ -36,9 +36,9 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.seasonEpisode import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.seasonEpisode import java.time.LocalDateTime import java.time.ZoneId diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt index 5dc0993c..c7d329a9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetails.kt @@ -1,6 +1,5 @@ package com.github.damontecres.wholphin.ui.detail.movie -import android.content.Intent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -54,6 +53,7 @@ import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.ChapterRow import com.github.damontecres.wholphin.ui.cards.ItemRow @@ -277,22 +277,8 @@ fun MovieDetails( items = items, ) }, - trailerOnClick = { trailer -> - when (trailer) { - is LocalTrailer -> - viewModel.navigateTo( - Destination.Playback( - itemId = trailer.baseItem.id, - item = trailer.baseItem, - positionMs = 0L, - ), - ) - - is RemoteTrailer -> { - val intent = Intent(Intent.ACTION_VIEW, trailer.url.toUri()) - context.startActivity(intent) - } - } + trailerOnClick = { + TrailerService.onClick(context, it, viewModel::navigateTo) }, modifier = modifier, ) @@ -567,7 +553,7 @@ fun TrailerRow( LazyRow( state = state, horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp), modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt index 45382fc6..c4d12ff7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieDetailsHeader.kt @@ -32,11 +32,11 @@ import com.github.damontecres.wholphin.ui.components.DotSeparatedRow import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.SimpleStarRating import com.github.damontecres.wholphin.ui.components.TitleValueText +import com.github.damontecres.wholphin.ui.getAudioDisplay +import com.github.damontecres.wholphin.ui.getSubtitleDisplay import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.roundMinutes import com.github.damontecres.wholphin.ui.timeRemaining -import com.github.damontecres.wholphin.util.getAudioDisplay -import com.github.damontecres.wholphin.util.getSubtitleDisplay import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.PersonKind import org.jellyfin.sdk.model.extensions.ticks diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt index 51c7dafa..4318f2a7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/movie/MovieViewModel.kt @@ -4,29 +4,27 @@ import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.ItemPlaybackRepository import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.ItemPlayback -import com.github.damontecres.wholphin.data.model.LocalTrailer import com.github.damontecres.wholphin.data.model.Person -import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.preferences.ThemeSongVolume +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.services.PeopleFavorites +import com.github.damontecres.wholphin.services.ThemeSongPlayer +import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.ThemeSongPlayer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -56,6 +54,8 @@ class MovieViewModel val itemPlaybackRepository: ItemPlaybackRepository, private val themeSongPlayer: ThemeSongPlayer, private val favoriteWatchManager: FavoriteWatchManager, + private val peopleFavorites: PeopleFavorites, + private val trailerService: TrailerService, @Assisted val itemId: UUID, ) : ViewModel() { @AssistedFactory @@ -107,39 +107,16 @@ class MovieViewModel loading.value = LoadingState.Success } viewModelScope.launchIO { - val remoteTrailers = - item.data.remoteTrailers - ?.mapNotNull { t -> - t.url?.let { url -> - val name = - t.name - // TODO would be nice to clean up the trailer name -// ?.replace(item.name ?: "", "") -// ?.removePrefix(" - ") - ?: context.getString(R.string.trailer) - RemoteTrailer(name, url) - } - }.orEmpty() - .sortedBy { it.name } - val localTrailerCount = item.data.localTrailerCount ?: 0 - val localTrailers = - if (localTrailerCount > 0) { - api.userLibraryApi.getLocalTrailers(itemId).content.map { - LocalTrailer(BaseItem.Companion.from(it, api)) - } - } else { - listOf() - } + val trailers = trailerService.getTrailers(item) withContext(Dispatchers.Main) { - this@MovieViewModel.trailers.value = localTrailers + remoteTrailers + this@MovieViewModel.trailers.value = trailers } } + viewModelScope.launchIO { + val people = peopleFavorites.getPeopleFor(item) + this@MovieViewModel.people.setValueOnMain(people) + } withContext(Dispatchers.Main) { - people.value = - item.data.people - ?.letNotEmpty { people -> - people.map { Person.fromDto(it, api) } - }.orEmpty() chapters.value = Chapter.fromDto(item.data, api) } if (!similar.isInitialized) { @@ -171,7 +148,14 @@ class MovieViewModel favorite: Boolean, ) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { favoriteWatchManager.setFavorite(itemId, favorite) + val item = item.value fetchAndSetItem() + if (item != null && itemId != item.id) { + viewModelScope.launchIO { + val people = peopleFavorites.getPeopleFor(item) + this@MovieViewModel.people.setValueOnMain(people) + } + } } fun savePlayVersion( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt index ec4c4b6f..42d8d991 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeFooter.kt @@ -17,9 +17,9 @@ import com.github.damontecres.wholphin.data.model.chooseStream import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.ExpandablePlayButtons import com.github.damontecres.wholphin.ui.components.TitleValueText +import com.github.damontecres.wholphin.ui.getAudioDisplay +import com.github.damontecres.wholphin.ui.getSubtitleDisplay import com.github.damontecres.wholphin.ui.isNotNullOrBlank -import com.github.damontecres.wholphin.util.getAudioDisplay -import com.github.damontecres.wholphin.util.getSubtitleDisplay import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.extensions.ticks import kotlin.time.Duration diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt index 77d7e386..664aab2f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/FocusedEpisodeHeader.kt @@ -16,10 +16,10 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.components.DotSeparatedRow import com.github.damontecres.wholphin.ui.components.OverviewText import com.github.damontecres.wholphin.ui.components.SimpleStarRating +import com.github.damontecres.wholphin.ui.formatDateTime import com.github.damontecres.wholphin.ui.roundMinutes +import com.github.damontecres.wholphin.ui.seasonEpisode import com.github.damontecres.wholphin.ui.timeRemaining -import com.github.damontecres.wholphin.util.formatDateTime -import com.github.damontecres.wholphin.util.seasonEpisode import org.jellyfin.sdk.model.extensions.ticks @Composable diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt index 983e5798..25806ce1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesDetails.kt @@ -46,7 +46,9 @@ import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.cards.PersonRow @@ -71,6 +73,7 @@ import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson +import com.github.damontecres.wholphin.ui.detail.movie.TrailerRow import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination @@ -102,6 +105,7 @@ fun SeriesDetails( val item by viewModel.item.observeAsState() val seasons by viewModel.seasons.observeAsState(listOf()) + val trailers by viewModel.trailers.observeAsState(listOf()) val people by viewModel.people.observeAsState(listOf()) val similar by viewModel.similar.observeAsState(listOf()) @@ -133,6 +137,7 @@ fun SeriesDetails( preferences = preferences, series = item, seasons = seasons, + trailers = trailers, people = people, similar = similar, played = played, @@ -174,6 +179,9 @@ fun SeriesDetails( val favorite = item.data.userData?.isFavorite ?: false viewModel.setFavorite(item.id, !favorite, null) }, + trailerOnClick = { + TrailerService.onClick(context, it, viewModel::navigateTo) + }, moreActions = MoreDialogActions( navigateTo = { viewModel.navigateTo(it) }, @@ -244,7 +252,8 @@ fun SeriesDetails( private const val HEADER_ROW = 0 private const val SEASONS_ROW = HEADER_ROW + 1 private const val PEOPLE_ROW = SEASONS_ROW + 1 -private const val SIMILAR_ROW = PEOPLE_ROW + 1 +private const val TRAILER_ROW = PEOPLE_ROW + 1 +private const val SIMILAR_ROW = TRAILER_ROW + 1 @Composable fun SeriesDetailsContent( @@ -252,6 +261,7 @@ fun SeriesDetailsContent( series: BaseItem, seasons: List, similar: List, + trailers: List, people: List, played: Boolean, favorite: Boolean, @@ -262,6 +272,7 @@ fun SeriesDetailsContent( playOnClick: () -> Unit, watchOnClick: () -> Unit, favoriteOnClick: () -> Unit, + trailerOnClick: (Trailer) -> Unit, moreActions: MoreDialogActions, modifier: Modifier = Modifier, ) { @@ -452,6 +463,21 @@ fun SeriesDetailsContent( ) } } + if (trailers.isNotEmpty()) { + item { + TrailerRow( + trailers = trailers, + onClickTrailer = { + position = TRAILER_ROW + trailerOnClick.invoke(it) + }, + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequesters[TRAILER_ROW]), + ) + } + } if (similar.isNotEmpty()) { item { ItemRow( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt index 7d18dfec..8fc9ff2d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverview.kt @@ -39,9 +39,9 @@ import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems import com.github.damontecres.wholphin.ui.equalsNotNull import com.github.damontecres.wholphin.ui.indexOfFirstOrNull import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.seasonEpisode import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.seasonEpisode import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.api.ImageType diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index cc326b24..302da4e3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -49,11 +49,11 @@ import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.TabRow +import com.github.damontecres.wholphin.ui.formatDateTime import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.tryRequestFocus -import com.github.damontecres.wholphin.util.formatDateTime import kotlin.time.Duration @Composable diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt index 33a5e476..16335b2d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesViewModel.kt @@ -10,25 +10,27 @@ import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Person +import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.preferences.ThemeSongVolume import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.services.PeopleFavorites +import com.github.damontecres.wholphin.services.ThemeSongPlayer +import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.detail.ItemViewModel import com.github.damontecres.wholphin.ui.equalsNotNull import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.ThemeSongPlayer import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -61,12 +63,15 @@ class SeriesViewModel private val itemPlaybackRepository: ItemPlaybackRepository, private val themeSongPlayer: ThemeSongPlayer, private val favoriteWatchManager: FavoriteWatchManager, + private val peopleFavorites: PeopleFavorites, + private val trailerService: TrailerService, ) : ItemViewModel(api) { private lateinit var seriesId: UUID private lateinit var prefs: UserPreferences val loading = MutableLiveData(LoadingState.Loading) val seasons = MutableLiveData>(listOf()) val episodes = MutableLiveData(EpisodeList.Loading) + val trailers = MutableLiveData>(listOf()) val people = MutableLiveData>(listOf()) val similar = MutableLiveData>() @@ -109,11 +114,16 @@ class SeriesViewModel this@SeriesViewModel.seasons.value = seasons episodes.value = episodeInfo loading.value = LoadingState.Success - people.value = - item.data.people - ?.letNotEmpty { people -> - people.map { Person.fromDto(it, api) } - }.orEmpty() + } + viewModelScope.launchIO { + val trailers = trailerService.getTrailers(item) + withContext(Dispatchers.Main) { + this@SeriesViewModel.trailers.value = trailers + } + } + viewModelScope.launchIO { + val people = peopleFavorites.getPeopleFor(item) + this@SeriesViewModel.people.setValueOnMain(people) } if (!similar.isInitialized) { viewModelScope.launchIO { @@ -255,7 +265,11 @@ class SeriesViewModel if (listIndex != null) { refreshEpisode(itemId, listIndex) } else { - fetchItem(seriesId) + val item = fetchItem(seriesId) + viewModelScope.launchIO { + val people = peopleFavorites.getPeopleFor(item) + this@SeriesViewModel.people.setValueOnMain(people) + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index e73f0cb6..72b84490 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -62,15 +62,15 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions import com.github.damontecres.wholphin.ui.detail.PlaylistDialog import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome +import com.github.damontecres.wholphin.ui.formatDateTime import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.roundMinutes +import com.github.damontecres.wholphin.ui.seasonEpisode import com.github.damontecres.wholphin.ui.timeRemaining import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.formatDateTime -import com.github.damontecres.wholphin.util.seasonEpisode import kotlinx.coroutines.delay import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaType @@ -360,9 +360,15 @@ fun HomePageContent( ), ).onFocusChanged { if (it.isFocused) { + val nonEmptyRowBefore = + homeRows + .subList(0, rowIndex) + .count { + it is HomeRowLoadingState.Success && it.items.isEmpty() + } onFocusPosition?.invoke( RowColumn( - rowIndex, + rowIndex - nonEmptyRowBefore, index, ), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt index d24062a5..388ec0c6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomeViewModel.kt @@ -9,12 +9,12 @@ import com.github.damontecres.wholphin.data.NavDrawerItemRepository import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.SlimItemFields -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.FavoriteWatchManager import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt index 244edbcc..2a57fd34 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt @@ -33,6 +33,7 @@ import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.cards.EpisodeCard @@ -42,7 +43,6 @@ import com.github.damontecres.wholphin.ui.components.SearchEditTextBox import com.github.damontecres.wholphin.ui.data.RowColumn import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.rememberPosition import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ApiRequestPager diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt index 94cc5e33..ddd881b5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/ApplicationContent.kt @@ -10,6 +10,7 @@ import androidx.navigation3.ui.NavDisplay import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.components.ErrorMessage import org.jellyfin.sdk.model.api.DeviceProfile 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 ce69e8ce..e41db910 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 @@ -68,7 +68,9 @@ import com.github.damontecres.wholphin.data.NavDrawerItemRepository import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.FontAwesome +import com.github.damontecres.wholphin.ui.TimeFormatter import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption @@ -76,7 +78,6 @@ import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.spacedByWithFooter import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.tryRequestFocus -import com.github.damontecres.wholphin.util.TimeFormatter import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay 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 4842f740..557dbcce 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 @@ -39,13 +39,13 @@ import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.abbreviateNumber import com.github.damontecres.wholphin.ui.components.DialogItem import com.github.damontecres.wholphin.ui.components.DialogItemDivider import com.github.damontecres.wholphin.ui.components.EditTextBox import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.tryRequestFocus -import com.github.damontecres.wholphin.util.abbreviateNumber import org.jellyfin.sdk.model.api.RemoteSubtitleInfo @Composable 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 2a1afe61..04561939 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 @@ -65,9 +65,9 @@ import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.indexOfFirstOrNull import com.github.damontecres.wholphin.ui.seekBack import com.github.damontecres.wholphin.ui.seekForward +import com.github.damontecres.wholphin.ui.stringRes import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.stringRes import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch 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 240b3b26..b50c279b 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 @@ -51,13 +51,13 @@ import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.TimeFormatter import com.github.damontecres.wholphin.ui.cards.ChapterCard import com.github.damontecres.wholphin.ui.cards.SeasonCard import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.letNotEmpty import com.github.damontecres.wholphin.ui.tryRequestFocus -import com.github.damontecres.wholphin.util.TimeFormatter import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import org.jellyfin.sdk.model.api.MediaSegmentDto 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 7b0c7046..d9d1003e 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 @@ -75,11 +75,11 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle +import com.github.damontecres.wholphin.ui.seasonEpisode +import com.github.damontecres.wholphin.ui.stringRes import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.seasonEpisode -import com.github.damontecres.wholphin.util.stringRes import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch 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 aa4c10a5..368746e6 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 @@ -25,7 +25,6 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Playlist -import com.github.damontecres.wholphin.data.model.PlaylistCreator import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.data.model.chooseSource import com.github.damontecres.wholphin.data.model.chooseStream @@ -35,9 +34,13 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.ShowNextUpWhen import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.services.PlayerFactory +import com.github.damontecres.wholphin.services.PlaylistCreator +import com.github.damontecres.wholphin.ui.formatDateTime import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager +import com.github.damontecres.wholphin.ui.seasonEpisodePadded import com.github.damontecres.wholphin.ui.seekBack import com.github.damontecres.wholphin.ui.seekForward import com.github.damontecres.wholphin.ui.setValueOnMain @@ -47,13 +50,10 @@ import com.github.damontecres.wholphin.util.EqualityMutableLiveData import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.PlayerFactory import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener import com.github.damontecres.wholphin.util.TrackSupport import com.github.damontecres.wholphin.util.checkForSupport -import com.github.damontecres.wholphin.util.formatDateTime import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile -import com.github.damontecres.wholphin.util.seasonEpisodePadded import com.github.damontecres.wholphin.util.subtitleMimeTypes import com.github.damontecres.wholphin.util.supportItemKinds import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt index 57701e6b..33cd8b56 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt @@ -14,10 +14,10 @@ import com.github.damontecres.wholphin.data.model.NavPinType import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.resetSubtitles import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.NavDrawerItem -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.RememberTabManager diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt index 4d02def5..4c608779 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt @@ -50,22 +50,22 @@ import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.DownloadCallback +import com.github.damontecres.wholphin.services.NavigationManager +import com.github.damontecres.wholphin.services.Release +import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage +import com.github.damontecres.wholphin.ui.formatBytes import com.github.damontecres.wholphin.ui.ifElse -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.theme.WholphinTheme -import com.github.damontecres.wholphin.util.DownloadCallback import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.Release -import com.github.damontecres.wholphin.util.UpdateChecker import com.github.damontecres.wholphin.util.Version -import com.github.damontecres.wholphin.util.formatBytes import com.mikepenz.markdown.m3.Markdown import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt index 9e962992..11202a31 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerViewModel.kt @@ -9,9 +9,9 @@ import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.JellyfinServerDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.JellyfinServer +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.util.LoadingState import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt index abd7d4c4..cd1d6daf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserViewModel.kt @@ -7,8 +7,8 @@ import com.github.damontecres.wholphin.data.JellyfinServerDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinUser +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt index 2cc5bb3d..3156bb3d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/ApiRequestPager.kt @@ -18,6 +18,7 @@ import org.jellyfin.sdk.api.client.Response import org.jellyfin.sdk.api.client.extensions.genresApi import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.liveTvApi +import org.jellyfin.sdk.api.client.extensions.personsApi import org.jellyfin.sdk.api.client.extensions.playlistsApi import org.jellyfin.sdk.api.client.extensions.suggestionsApi import org.jellyfin.sdk.api.client.extensions.tvShowsApi @@ -28,6 +29,7 @@ import org.jellyfin.sdk.model.api.request.GetEpisodesRequest import org.jellyfin.sdk.model.api.request.GetGenresRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetNextUpRequest +import org.jellyfin.sdk.model.api.request.GetPersonsRequest import org.jellyfin.sdk.model.api.request.GetPlaylistItemsRequest import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest import org.jellyfin.sdk.model.api.request.GetSuggestionsRequest @@ -365,3 +367,22 @@ val GetProgramsDtoHandler = request: GetProgramsDto, ): Response = api.liveTvApi.getPrograms(request) } + +val GetPersonsHandler = + object : RequestHandler { + override fun prepare( + request: GetPersonsRequest, + startIndex: Int, + limit: Int, + enableTotalRecordCount: Boolean, + ): GetPersonsRequest = + request.copy( +// startIndex = startIndex, + limit = limit, + ) + + override suspend fun execute( + api: ApiClient, + request: GetPersonsRequest, + ): Response = api.personsApi.getPersons((request)) + } 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 0afdca90..43d3e45a 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 @@ -2,7 +2,7 @@ package com.github.damontecres.wholphin.util import android.content.Context import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.hilt.AppModule +import com.github.damontecres.wholphin.services.hilt.AppModule import com.google.auto.service.AutoService import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac425f5e..53eb9b05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,6 +82,7 @@ Playback Speed Playback Playlist + Playlists Preview User Profile Settings Queue @@ -135,6 +136,7 @@ Version Video Scale Video + Videos Watch live %1$d years old Play with transcoding