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 e2afbfa0..9cb740ab 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -53,6 +53,7 @@ import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.theme.WholphinTheme +import com.github.damontecres.wholphin.ui.util.ProvideLocalClock import com.github.damontecres.wholphin.util.DebugLogTree import com.github.damontecres.wholphin.util.ExceptionHandler import dagger.hilt.android.AndroidEntryPoint @@ -92,9 +93,6 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var updateChecker: UpdateChecker - @Inject - lateinit var appUpgradeHandler: AppUpgradeHandler - @Inject lateinit var playbackLifecycleObserver: PlaybackLifecycleObserver @@ -135,11 +133,7 @@ class MainActivity : AppCompatActivity() { instance = this Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}") lifecycle.addObserver(playbackLifecycleObserver) - if (savedInstanceState == null) { - lifecycleScope.launchIO { - appUpgradeHandler.copySubfont(false) - } - } + viewModel.serverRepository.currentUser.observe(this) { user -> if (user?.hasPin == true) { window?.setFlags( @@ -211,19 +205,21 @@ class MainActivity : AppCompatActivity() { true, appThemeColors = appPreferences.interfacePreferences.appThemeColors, ) { - val requestedDestination = - remember(intent) { - intent?.let(::extractDestination) ?: Destination.Home() - } - MainContent( - backStack = setupNavigationManager.backStack, - navigationManager = navigationManager, - appPreferences = appPreferences, - backdropService = backdropService, - screensaverService = screensaverService, - requestedDestination = requestedDestination, - modifier = Modifier.fillMaxSize(), - ) + ProvideLocalClock { + val requestedDestination = + remember(intent) { + intent?.let(::extractDestination) ?: Destination.Home() + } + MainContent( + backStack = setupNavigationManager.backStack, + navigationManager = navigationManager, + appPreferences = appPreferences, + backdropService = backdropService, + screensaverService = screensaverService, + requestedDestination = requestedDestination, + modifier = Modifier.fillMaxSize(), + ) + } } } } @@ -246,7 +242,6 @@ class MainActivity : AppCompatActivity() { Timber.d("onResume") lifecycleScope.launchDefault { screensaverService.pulse() - appUpgradeHandler.run() } } @@ -381,10 +376,13 @@ class MainActivityViewModel private val navigationManager: SetupNavigationManager, private val deviceProfileService: DeviceProfileService, private val backdropService: BackdropService, + private val appUpgradeHandler: AppUpgradeHandler, ) : ViewModel() { fun appStart() { viewModelScope.launchIO { try { + appUpgradeHandler.run() + appUpgradeHandler.copySubfont(false) val prefs = preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance() val userHasPin = serverRepository.currentUser.value?.hasPin == true diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt index 982240a4..5bade867 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt @@ -96,39 +96,37 @@ fun MainContent( backdropService.clearBackdrop() } val current = key.current - ProvideLocalClock { - val preferences = - remember(appPreferences) { - UserPreferences(appPreferences) - } - var showContent by remember { - mutableStateOf(true) + val preferences = + remember(appPreferences) { + UserPreferences(appPreferences) } - LifecycleEventEffect(Lifecycle.Event.ON_STOP) { - if (!appPreferences.signInAutomatically) { - showContent = false - } + var showContent by remember { + mutableStateOf(true) + } + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { + if (!appPreferences.signInAutomatically) { + showContent = false } + } - if (showContent) { - ApplicationContent( - user = current.user, - server = current.server, - startDestination = requestedDestination, - navigationManager = navigationManager, - preferences = preferences, - modifier = Modifier.fillMaxSize(), + if (showContent) { + ApplicationContent( + user = current.user, + server = current.server, + startDestination = requestedDestination, + navigationManager = navigationManager, + preferences = preferences, + modifier = Modifier.fillMaxSize(), + ) + } else { + Box( + modifier = Modifier.size(200.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.border, + modifier = Modifier.align(Alignment.Center), ) - } else { - Box( - modifier = Modifier.size(200.dp), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.border, - modifier = Modifier.align(Alignment.Center), - ) - } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt b/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt index 4d77c3a8..a4c09ad4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt @@ -5,11 +5,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView +import androidx.datastore.core.DataStore import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.lifecycleScope @@ -18,14 +20,19 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner +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.services.ScreensaverService import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.ui.components.AppScreensaverContent +import com.github.damontecres.wholphin.ui.launchDefault import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.util.ProvideLocalClock import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import org.jellyfin.sdk.model.serializer.toUUIDOrNull import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -33,11 +40,14 @@ import kotlin.time.Duration.Companion.milliseconds class WholphinDreamService : DreamService(), SavedStateRegistryOwner { + @Inject + lateinit var serverRepository: ServerRepository + @Inject lateinit var screensaverService: ScreensaverService @Inject - lateinit var userPreferencesService: UserPreferencesService + lateinit var preferencesDataStore: DataStore private val lifecycleRegistry = LifecycleRegistry(this) @@ -54,6 +64,12 @@ class WholphinDreamService : savedStateRegistryController.performRestore(null) lifecycleRegistry.currentState = Lifecycle.State.CREATED + lifecycleScope.launchDefault { + if (serverRepository.current.value == null) { + val prefs = preferencesDataStore.data.first() + serverRepository.restoreSession(prefs.currentServerId.toUUIDOrNull(), prefs.currentUserId.toUUIDOrNull()) + } + } } override fun onAttachedToWindow() { @@ -64,23 +80,25 @@ class WholphinDreamService : setViewTreeLifecycleOwner(this@WholphinDreamService) setViewTreeSavedStateRegistryOwner(this@WholphinDreamService) setContent { - var prefs by remember { mutableStateOf(null) } - LaunchedEffect(Unit) { - userPreferencesService.flow.collectLatest { prefs = it } - } - prefs?.let { prefs -> - WholphinTheme(appThemeColors = prefs.appPreferences.interfacePreferences.appThemeColors) { - ProvideLocalClock { - val screensaverPrefs = - prefs.appPreferences.interfacePreferences.screensaverPreference - val currentItem by itemFlow.collectAsState(null) - AppScreensaverContent( - currentItem = currentItem, - showClock = screensaverPrefs.showClock, - duration = screensaverPrefs.duration.milliseconds, - animate = screensaverPrefs.animate, - modifier = Modifier.fillMaxSize(), - ) + val user by serverRepository.currentUser.observeAsState() + if (user != null) { + var prefs by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + preferencesDataStore.data.collectLatest { prefs = it } + } + prefs?.let { prefs -> + WholphinTheme(appThemeColors = prefs.interfacePreferences.appThemeColors) { + ProvideLocalClock { + val screensaverPrefs = prefs.interfacePreferences.screensaverPreference + val currentItem by itemFlow.collectAsState(null) + AppScreensaverContent( + currentItem = currentItem, + showClock = screensaverPrefs.showClock, + duration = screensaverPrefs.duration.milliseconds, + animate = screensaverPrefs.animate, + modifier = Modifier.fillMaxSize(), + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt index a274caa1..0cf88ff1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/DiscoverItem.kt @@ -2,25 +2,17 @@ package com.github.damontecres.wholphin.data.model -import com.github.damontecres.wholphin.api.seerr.model.CreditCast -import com.github.damontecres.wholphin.api.seerr.model.CreditCrew -import com.github.damontecres.wholphin.api.seerr.model.MovieDetails +import androidx.compose.runtime.Stable import com.github.damontecres.wholphin.api.seerr.model.MovieMovieIdRatingsGet200Response -import com.github.damontecres.wholphin.api.seerr.model.MovieResult -import com.github.damontecres.wholphin.api.seerr.model.TvDetails -import com.github.damontecres.wholphin.api.seerr.model.TvResult import com.github.damontecres.wholphin.api.seerr.model.TvTvIdRatingsGet200Response -import com.github.damontecres.wholphin.services.SeerrSearchResult import com.github.damontecres.wholphin.ui.detail.CardGridItem import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.toLocalDate import com.github.damontecres.wholphin.util.LocalDateSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.serializer.UUIDSerializer -import org.jellyfin.sdk.model.serializer.toUUIDOrNull import java.time.LocalDate import java.util.UUID @@ -74,6 +66,7 @@ enum class SeerrAvailability( /** * An item provided by a discovery service (ie Seerr). It may exist on the JF server as well. */ +@Stable @Serializable data class DiscoverItem( val id: Int, @@ -83,17 +76,14 @@ data class DiscoverItem( val overview: String?, val availability: SeerrAvailability, @Serializable(LocalDateSerializer::class) val releaseDate: LocalDate?, - val posterPath: String?, - val backdropPath: String?, + val posterUrl: String?, + val backDropUrl: String?, val jellyfinItemId: UUID?, ) : CardGridItem { override val gridId: String get() = id.toString() override val playable: Boolean = false override val sortName: String get() = title ?: "" - val backDropUrl: String? get() = backdropPath?.let { "https://image.tmdb.org/t/p/w1920_and_h1080_multi_faces$it" } - val posterUrl: String? get() = posterPath?.let { "https://image.tmdb.org/t/p/w500$it" } - val destination: Destination get() { val jfType = @@ -112,103 +102,6 @@ data class DiscoverItem( Destination.DiscoveredItem(this) } } - - constructor(movie: MovieResult) : this( - id = movie.id, - type = SeerrItemType.MOVIE, - title = movie.title, - subtitle = null, - overview = movie.overview, - availability = SeerrAvailability.from(movie.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(movie.releaseDate), - posterPath = movie.posterPath, - backdropPath = movie.backdropPath, - jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(movie: MovieDetails) : this( - id = movie.id ?: -1, - type = SeerrItemType.MOVIE, - title = movie.title, - subtitle = null, - overview = movie.overview, - availability = SeerrAvailability.from(movie.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(movie.releaseDate), - posterPath = movie.posterPath, - backdropPath = movie.backdropPath, - jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(tv: TvResult) : this( - id = tv.id!!, - type = SeerrItemType.TV, - title = tv.name, - subtitle = null, - overview = tv.overview, - availability = SeerrAvailability.from(tv.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(tv.firstAirDate), - posterPath = tv.posterPath, - backdropPath = tv.backdropPath, - jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(tv: TvDetails) : this( - id = tv.id!!, - type = SeerrItemType.TV, - title = tv.name, - subtitle = null, - overview = tv.overview, - availability = SeerrAvailability.from(tv.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(tv.firstAirDate), - posterPath = tv.posterPath, - backdropPath = tv.backdropPath, - jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(search: SeerrSearchResult) : this( - id = search.id, - type = SeerrItemType.fromString(search.mediaType), - title = search.title ?: search.name, - subtitle = null, - overview = search.overview, - availability = - SeerrAvailability.from(search.mediaInfo?.status) - ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(search.releaseDate ?: search.firstAirDate), - posterPath = search.posterPath, - backdropPath = search.backdropPath, - jellyfinItemId = search.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(credit: CreditCast) : this( - id = credit.id!!, - type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), - title = credit.name ?: credit.title, - subtitle = credit.character, - overview = credit.overview, - availability = - SeerrAvailability.from(credit.mediaInfo?.status) - ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(credit.firstAirDate), - posterPath = credit.posterPath ?: credit.profilePath, - backdropPath = credit.backdropPath, - jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) - - constructor(credit: CreditCrew) : this( - id = credit.id!!, - type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), - title = credit.name ?: credit.title, - subtitle = credit.job, - overview = credit.overview, - availability = - SeerrAvailability.from(credit.mediaInfo?.status) - ?: SeerrAvailability.UNKNOWN, - releaseDate = toLocalDate(credit.firstAirDate), - posterPath = credit.posterPath ?: credit.profilePath, - backdropPath = credit.backdropPath, - jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), - ) } data class DiscoverRating( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 23d6cef6..ca720af3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -6,6 +6,7 @@ import androidx.core.content.edit import androidx.datastore.core.DataStore import androidx.preference.PreferenceManager import com.github.damontecres.wholphin.WholphinApplication +import com.github.damontecres.wholphin.data.SeerrServerDao import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.ScreensaverPreference @@ -21,6 +22,7 @@ import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings +import com.github.damontecres.wholphin.ui.setup.seerr.migrateSeerrUrl import com.github.damontecres.wholphin.util.Version import dagger.hilt.android.qualifiers.ApplicationContext import org.jellyfin.sdk.model.api.BaseItemKind @@ -35,9 +37,14 @@ class AppUpgradeHandler constructor( @param:ApplicationContext private val context: Context, private val appPreferences: DataStore, + private val seerrServerDao: SeerrServerDao, ) { suspend fun run() { - val pkgInfo = WholphinApplication.instance.packageManager.getPackageInfo(WholphinApplication.instance.packageName, 0) + val pkgInfo = + WholphinApplication.instance.packageManager.getPackageInfo( + WholphinApplication.instance.packageName, + 0, + ) val prefs = PreferenceManager.getDefaultSharedPreferences(context) val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null) val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1) @@ -62,14 +69,16 @@ class AppUpgradeHandler try { copySubfont(true) upgradeApp( - context, - Version.Companion.fromString(previousVersion ?: "0.0.0"), - Version.Companion.fromString(newVersion), + Version.fromString(previousVersion ?: "0.0.0"), + Version.fromString(newVersion), appPreferences, ) } catch (ex: Exception) { Timber.e(ex, "Exception during app upgrade") } + Timber.i("App upgrade complete") + } else { + Timber.d("No app update needed") } } @@ -100,110 +109,108 @@ class AppUpgradeHandler const val VERSION_NAME_CURRENT_KEY = "version.current.name" const val VERSION_CODE_CURRENT_KEY = "version.current.code" } - } -suspend fun upgradeApp( - context: Context, - previous: Version, - current: Version, - appPreferences: DataStore, -) { - if (previous.isEqualOrBefore(Version.fromString("0.1.0-2-g0"))) { - appPreferences.updateData { - it.updatePlaybackOverrides { - ac3Supported = AppPreference.Ac3Supported.defaultValue - downmixStereo = AppPreference.DownMixStereo.defaultValue - directPlayAss = AppPreference.DirectPlayAss.defaultValue - directPlayPgs = AppPreference.DirectPlayPgs.defaultValue - } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.2.3-6-g0"))) { - appPreferences.updateData { - it.updateInterfacePreferences { - navDrawerSwitchOnFocus = AppPreference.NavDrawerSwitchOnFocus.defaultValue - } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.2.5-11-g0"))) { - appPreferences.updateData { - it.updateInterfacePreferences { - showClock = AppPreference.ShowClock.defaultValue - } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.2.7-1-g0"))) { - PreferencesViewModel.resetSubtitleSettings(appPreferences) - } - if (previous.isEqualOrBefore(Version.fromString("0.3.2-4-g0"))) { - appPreferences.updateData { - it.updateSubtitlePreferences { - margin = SubtitleSettings.Margin.defaultValue.toInt() - } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.4"))) { - appPreferences.updateData { - it.updateAdvancedPreferences { - if (imageDiskCacheSizeBytes < (AppPreference.ImageDiskCacheSize.min * AppPreference.MEGA_BIT)) { - imageDiskCacheSizeBytes = - AppPreference.ImageDiskCacheSize.defaultValue * AppPreference.MEGA_BIT + suspend fun upgradeApp( + previous: Version, + current: Version, + appPreferences: DataStore, + ) { + if (previous.isEqualOrBefore(Version.fromString("0.1.0-2-g0"))) { + appPreferences.updateData { + it.updatePlaybackOverrides { + ac3Supported = AppPreference.Ac3Supported.defaultValue + downmixStereo = AppPreference.DownMixStereo.defaultValue + directPlayAss = AppPreference.DirectPlayAss.defaultValue + directPlayPgs = AppPreference.DirectPlayPgs.defaultValue + } } } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.4-2-g0"))) { - appPreferences.updateData { - it.updateMpvOptions { - useGpuNext = AppPreference.MpvGpuNext.defaultValue - } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.4-4-g0"))) { - appPreferences.updateData { - it.update { - signInAutomatically = AppPreference.SignInAuto.defaultValue - } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.5-0-g0"))) { - appPreferences.updateData { - it.updateSubtitlePreferences { - if (edgeThickness < 1) { - edgeThickness = SubtitleSettings.EdgeThickness.defaultValue.toInt() + if (previous.isEqualOrBefore(Version.fromString("0.2.3-6-g0"))) { + appPreferences.updateData { + it.updateInterfacePreferences { + navDrawerSwitchOnFocus = AppPreference.NavDrawerSwitchOnFocus.defaultValue + } } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.3.5-56-g0"))) { - appPreferences.updateData { - it.updateLiveTvPreferences { - showHeader = AppPreference.LiveTvShowHeader.defaultValue - favoriteChannelsAtBeginning = - AppPreference.LiveTvFavoriteChannelsBeginning.defaultValue - sortByRecentlyWatched = - AppPreference.LiveTvChannelSortByWatched.defaultValue - colorCodePrograms = - AppPreference.LiveTvColorCodePrograms.defaultValue - } - } - } - - if (previous.isEqualOrBefore(Version.fromString("0.3.6-52-g0"))) { - if (Build.MODEL.equals("shield android tv", ignoreCase = true)) { - appPreferences.updateData { - it.updateMpvOptions { - useGpuNext = false + if (previous.isEqualOrBefore(Version.fromString("0.2.5-11-g0"))) { + appPreferences.updateData { + it.updateInterfacePreferences { + showClock = AppPreference.ShowClock.defaultValue + } + } + } + if (previous.isEqualOrBefore(Version.fromString("0.2.7-1-g0"))) { + PreferencesViewModel.resetSubtitleSettings(appPreferences) + } + if (previous.isEqualOrBefore(Version.fromString("0.3.2-4-g0"))) { + appPreferences.updateData { + it.updateSubtitlePreferences { + margin = SubtitleSettings.Margin.defaultValue.toInt() + } } } - } - } - // TODO temporarily disabled until some MPV bugs are fixed + if (previous.isEqualOrBefore(Version.fromString("0.3.4"))) { + appPreferences.updateData { + it.updateAdvancedPreferences { + if (imageDiskCacheSizeBytes < (AppPreference.ImageDiskCacheSize.min * AppPreference.MEGA_BIT)) { + imageDiskCacheSizeBytes = + AppPreference.ImageDiskCacheSize.defaultValue * AppPreference.MEGA_BIT + } + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.3.4-2-g0"))) { + appPreferences.updateData { + it.updateMpvOptions { + useGpuNext = AppPreference.MpvGpuNext.defaultValue + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.3.4-4-g0"))) { + appPreferences.updateData { + it.update { + signInAutomatically = AppPreference.SignInAuto.defaultValue + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.3.5-0-g0"))) { + appPreferences.updateData { + it.updateSubtitlePreferences { + if (edgeThickness < 1) { + edgeThickness = SubtitleSettings.EdgeThickness.defaultValue.toInt() + } + } + } + } + if (previous.isEqualOrBefore(Version.fromString("0.3.5-56-g0"))) { + appPreferences.updateData { + it.updateLiveTvPreferences { + showHeader = AppPreference.LiveTvShowHeader.defaultValue + favoriteChannelsAtBeginning = + AppPreference.LiveTvFavoriteChannelsBeginning.defaultValue + sortByRecentlyWatched = + AppPreference.LiveTvChannelSortByWatched.defaultValue + colorCodePrograms = + AppPreference.LiveTvColorCodePrograms.defaultValue + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.3.6-52-g0"))) { + if (Build.MODEL.equals("shield android tv", ignoreCase = true)) { + appPreferences.updateData { + it.updateMpvOptions { + useGpuNext = false + } + } + } + } + + // TODO temporarily disabled until some MPV bugs are fixed // if (previous.isEqualOrBefore(Version.fromString("0.4.0-1-g0"))) { // appPreferences.updateData { // it.updatePlaybackPreferences { playerBackend = PlayerBackend.PREFER_MPV } @@ -211,56 +218,67 @@ suspend fun upgradeApp( // showToast(context, context.getString(R.string.upgrade_mpv_toast), Toast.LENGTH_LONG) // } - if (previous.isEqualOrBefore(Version.fromString("0.4.0-2-g0"))) { - appPreferences.updateData { - it.updateMpvOptions { - useGpuNext = false + if (previous.isEqualOrBefore(Version.fromString("0.4.0-2-g0"))) { + appPreferences.updateData { + it.updateMpvOptions { + useGpuNext = false + } + } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.4.1-6-g0"))) { - appPreferences.updateData { - it.updateInterfacePreferences { - subtitlesPreferences = - subtitlesPreferences - .toBuilder() - .apply { - imageSubtitleOpacity = SubtitleSettings.ImageOpacity.defaultValue.toInt() - }.build() - // Copy current subtitle prefs as HDR ones - hdrSubtitlesPreferences = subtitlesPreferences.toBuilder().build() + if (previous.isEqualOrBefore(Version.fromString("0.4.1-6-g0"))) { + appPreferences.updateData { + it.updateInterfacePreferences { + subtitlesPreferences = + subtitlesPreferences + .toBuilder() + .apply { + imageSubtitleOpacity = + SubtitleSettings.ImageOpacity.defaultValue.toInt() + }.build() + // Copy current subtitle prefs as HDR ones + hdrSubtitlesPreferences = subtitlesPreferences.toBuilder().build() + } + } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.4.1-7-g0"))) { - appPreferences.updateData { - it.updatePhotoPreferences { - slideshowDuration = AppPreference.SlideshowDuration.defaultValue + if (previous.isEqualOrBefore(Version.fromString("0.4.1-7-g0"))) { + appPreferences.updateData { + it.updatePhotoPreferences { + slideshowDuration = AppPreference.SlideshowDuration.defaultValue + } + } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.4.1-14-g0"))) { - appPreferences.updateData { - it.updateHomePagePreferences { - maxDaysNextUp = AppPreference.MaxDaysNextUp.defaultValue.toInt() + if (previous.isEqualOrBefore(Version.fromString("0.4.1-14-g0"))) { + appPreferences.updateData { + it.updateHomePagePreferences { + maxDaysNextUp = AppPreference.MaxDaysNextUp.defaultValue.toInt() + } + } } - } - } - if (previous.isEqualOrBefore(Version.fromString("0.5.0-6-g0"))) { - appPreferences.updateData { - it.updateScreensaverPreferences { - startDelay = ScreensaverPreference.DEFAULT_START_DELAY - duration = ScreensaverPreference.DEFAULT_DURATION - animate = ScreensaverPreference.Animate.defaultValue - maxAgeFilter = ScreensaverPreference.DEFAULT_MAX_AGE - clearItemTypes() - addItemTypes(BaseItemKind.MOVIE.serialName) - addItemTypes(BaseItemKind.SERIES.serialName) + if (previous.isEqualOrBefore(Version.fromString("0.5.3-0-g0"))) { + appPreferences.updateData { + it.updateScreensaverPreferences { + startDelay = ScreensaverPreference.DEFAULT_START_DELAY + duration = ScreensaverPreference.DEFAULT_DURATION + animate = ScreensaverPreference.Animate.defaultValue + maxAgeFilter = ScreensaverPreference.DEFAULT_MAX_AGE + clearItemTypes() + addItemTypes(BaseItemKind.MOVIE.serialName) + addItemTypes(BaseItemKind.SERIES.serialName) + } + } + } + + if (previous.isEqualOrBefore(Version.fromString("0.5.4-0-g0"))) { + seerrServerDao.getServers().forEach { + val server = it.server + seerrServerDao.updateServer( + server.copy(url = migrateSeerrUrl(server.url)), + ) + } } } } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt index 5d73ec21..c81893b3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt @@ -161,27 +161,32 @@ class ScreensaverService if (pager.isEmpty()) { emit(null) } else { + val duration = + userPreferencesService + .getCurrent() + .appPreferences + .interfacePreferences.screensaverPreference.duration.milliseconds while (true) { - val item = pager.getBlocking(index) - Timber.v("Next index=%s, item=%s", index, item?.id) - if (item != null) { - val backdropUrl = - if (item.type == BaseItemKind.PHOTO) { - api.libraryApi.getDownloadUrl(item.id) - } else { - imageUrlService.getItemImageUrl(item, ImageType.BACKDROP) - } - val title = - if (item.type == BaseItemKind.PHOTO) { - item.data.premiereDate?.let { - formatDate(it.toLocalDate()) + try { + val item = pager.getBlocking(index) + Timber.v("Next index=%s, item=%s", index, item?.id) + if (item != null) { + val backdropUrl = + if (item.type == BaseItemKind.PHOTO) { + api.libraryApi.getDownloadUrl(item.id) + } else { + imageUrlService.getItemImageUrl(item, ImageType.BACKDROP) } - } else { - item.title - } - val logoUrl = imageUrlService.getItemImageUrl(item, ImageType.LOGO) - if (backdropUrl != null) { - try { + val title = + if (item.type == BaseItemKind.PHOTO) { + item.data.premiereDate?.let { + formatDate(it.toLocalDate()) + } + } else { + item.title + } + val logoUrl = imageUrlService.getItemImageUrl(item, ImageType.LOGO) + if (backdropUrl != null) { context.imageLoader .enqueue( ImageRequest @@ -191,18 +196,17 @@ class ScreensaverService ).job .await() emit(CurrentItem(item, backdropUrl, logoUrl, title ?: "")) - } catch (_: CancellationException) { - break + delay(duration) } - val duration = - userPreferencesService - .getCurrent() - .appPreferences - .interfacePreferences.screensaverPreference.duration.milliseconds - delay(duration) } + } catch (_: CancellationException) { + break + } catch (ex: Exception) { + Timber.e(ex, "Error fetching next item") + delay(duration) } index++ + if (index > pager.lastIndex) index = 0 } } }.flowOn(Dispatchers.Default).cancellable() diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt index c68cd3d2..5c62655f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrApi.kt @@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.services import com.github.damontecres.wholphin.api.seerr.SeerrApiClient import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl import okhttp3.OkHttpClient /** @@ -24,6 +25,6 @@ class SeerrApi( baseUrl: String, apiKey: String?, ) { - api = SeerrApiClient(baseUrl, apiKey, okHttpClient) + api = SeerrApiClient(createSeerrApiUrl(baseUrl), apiKey, okHttpClient) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt index 970e80c3..0f073af6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt @@ -19,17 +19,19 @@ import com.github.damontecres.wholphin.data.model.SeerrUser import com.github.damontecres.wholphin.data.model.hasPermission import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl import com.github.damontecres.wholphin.util.LoadingState import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.supervisorScope import okhttp3.OkHttpClient +import org.jellyfin.sdk.model.api.ImageType import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -57,6 +59,7 @@ class SeerrServerRepository connection.map { (it as? SeerrConnectionStatus.Success)?.current?.server } val currentUser: Flow = connection.map { (it as? SeerrConnectionStatus.Success)?.current?.user } + val currentUserId: Flow = current.map { it?.config?.id } /** * Whether Seerr integration is currently active of not @@ -70,10 +73,11 @@ class SeerrServerRepository } fun error( - serverUrl: String, + server: SeerrServer, + user: SeerrUser, exception: Exception, ) { - _connection.update { SeerrConnectionStatus.Error(serverUrl, exception) } + _connection.update { SeerrConnectionStatus.Error(server, user, exception) } seerrApi.update("", null) } @@ -161,7 +165,7 @@ class SeerrServerRepository val apiKey = passwordOrApiKey.takeIf { authMethod == SeerrAuthMethod.API_KEY } val api = SeerrApiClient( - url, + createSeerrApiUrl(url), apiKey, okHttpClient .newBuilder() @@ -174,8 +178,13 @@ class SeerrServerRepository } suspend fun removeServerForCurrentUser(): Boolean { - val current = current.firstOrNull() ?: return false - val rows = seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId) + val user = + when (val conn = connection.first()) { + SeerrConnectionStatus.NotConfigured -> return false + is SeerrConnectionStatus.Error -> conn.user + is SeerrConnectionStatus.Success -> conn.current.user + } + val rows = seerrServerDao.deleteUser(user) clear() return rows > 0 } @@ -190,7 +199,8 @@ sealed interface SeerrConnectionStatus { data object NotConfigured : SeerrConnectionStatus data class Error( - val serverUrl: String, + val server: SeerrServer, + val user: SeerrUser, val ex: Exception, ) : SeerrConnectionStatus @@ -316,10 +326,31 @@ class UserSwitchListener "Error logging into %s", server.url, ) - seerrServerRepository.error(server.url, ex) + seerrServerRepository.error(server, seerrUser, ex) } } } } } } + +fun CurrentSeerr?.imageUrlBuilder( + imageType: ImageType, + path: String?, +): String? { + if (this == null) return null + val cacheImages = serverConfig.cacheImages == true + val base = + if (cacheImages) { + server.url.removeSuffix("/") + "/imageproxy/tmdb" + } else { + "https://image.tmdb.org" + } + val prefix = + when (imageType) { + ImageType.PRIMARY -> "/t/p/w500" + ImageType.BACKDROP -> "/t/p/w1920_and_h1080_multi_faces" + else -> throw IllegalArgumentException("Image type not supported: $imageType") + } + return "${base}${prefix}$path" +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt index e38ba41b..0c1801ac 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrService.kt @@ -1,11 +1,25 @@ package com.github.damontecres.wholphin.services import com.github.damontecres.wholphin.api.seerr.SeerrApiClient +import com.github.damontecres.wholphin.api.seerr.model.CreditCast +import com.github.damontecres.wholphin.api.seerr.model.CreditCrew +import com.github.damontecres.wholphin.api.seerr.model.MediaInfo +import com.github.damontecres.wholphin.api.seerr.model.MovieDetails +import com.github.damontecres.wholphin.api.seerr.model.MovieResult import com.github.damontecres.wholphin.api.seerr.model.SearchGet200ResponseResultsInner +import com.github.damontecres.wholphin.api.seerr.model.TvDetails +import com.github.damontecres.wholphin.api.seerr.model.TvResult import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.DiscoverItem +import com.github.damontecres.wholphin.data.model.SeerrAvailability +import com.github.damontecres.wholphin.data.model.SeerrItemType +import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.toLocalDate import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ImageType +import org.jellyfin.sdk.model.serializer.toUUIDOrNull import javax.inject.Inject import javax.inject.Singleton @@ -22,6 +36,7 @@ class SeerrService constructor( private val seerApi: SeerrApi, private val seerrServerRepository: SeerrServerRepository, + private val imageUrlService: ImageUrlService, ) { val api: SeerrApiClient get() = seerApi.api @@ -40,35 +55,35 @@ class SeerrService api.searchApi .discoverTvGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() suspend fun discoverMovies(page: Int = 1): List = api.searchApi .discoverMoviesGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() suspend fun trending(page: Int = 1): List = api.searchApi .discoverTrendingGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() suspend fun upcomingMovies(page: Int = 1): List = api.searchApi .discoverMoviesUpcomingGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() suspend fun upcomingTv(page: Int = 1): List = api.searchApi .discoverTvUpcomingGet(page = page) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() /** @@ -89,14 +104,14 @@ class SeerrService api.moviesApi .movieMovieIdSimilarGet(movieId = it) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } } BaseItemKind.SERIES, BaseItemKind.SEASON, BaseItemKind.EPISODE -> { api.tvApi .tvTvIdSimilarGet(tvId = it) .results - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } } BaseItemKind.PERSON -> { @@ -106,12 +121,12 @@ class SeerrService val cast = credits.cast ?.take(25) - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() val crew = credits.crew ?.take(25) - ?.map(::DiscoverItem) + ?.map { createDiscoverItem(it) } .orEmpty() cast + crew } @@ -125,4 +140,186 @@ class SeerrService } else { null } + + suspend fun getTvSeries(item: BaseItem): TvDetails? = + if (active.first()) { + item.data.providerIds + ?.get("Tmdb") + ?.toIntOrNull() + ?.let { tvId -> + api.tvApi.tvTvIdGet(tvId = tvId) + } + } else { + null + } + + private suspend fun createImageUrl( + imageType: ImageType, + path: String?, + mediaInfo: MediaInfo?, + ): String? { + if (mediaInfo != null) { + val itemId = + if (mediaInfo.jellyfinMediaId.isNotNullOrBlank()) { + mediaInfo.jellyfinMediaId.toUUIDOrNull() + } else if (mediaInfo.jellyfinMediaId4k.isNotNullOrBlank()) { + mediaInfo.jellyfinMediaId4k.toUUIDOrNull() + } else { + null + } + if (itemId != null) { + return imageUrlService.getItemImageUrl( + itemId = itemId, + imageType = imageType, + ) + } + } + val current = seerrServerRepository.current.firstOrNull() ?: return null + val cacheImages = current.serverConfig.cacheImages == true + val base = + if (cacheImages) { + current.server.url.removeSuffix("/") + "/imageproxy/tmdb" + } else { + "https://image.tmdb.org" + } + val prefix = + when (imageType) { + ImageType.PRIMARY -> "/t/p/w500" + ImageType.BACKDROP -> "/t/p/w1920_and_h1080_multi_faces" + else -> throw IllegalArgumentException("Image type not supported: $imageType") + } + return "${base}${prefix}$path" + } + + suspend fun createDiscoverItem(movie: MovieResult): DiscoverItem = + DiscoverItem( + id = movie.id, + type = SeerrItemType.MOVIE, + title = movie.title, + subtitle = null, + overview = movie.overview, + availability = + SeerrAvailability.from(movie.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(movie.releaseDate), + posterUrl = createImageUrl(ImageType.PRIMARY, movie.posterPath, movie.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, movie.backdropPath, movie.mediaInfo), + jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(movie: MovieDetails): DiscoverItem = + DiscoverItem( + id = movie.id ?: -1, + type = SeerrItemType.MOVIE, + title = movie.title, + subtitle = null, + overview = movie.overview, + availability = + SeerrAvailability.from(movie.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(movie.releaseDate), + posterUrl = createImageUrl(ImageType.PRIMARY, movie.posterPath, movie.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, movie.backdropPath, movie.mediaInfo), + jellyfinItemId = movie.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(tv: TvResult): DiscoverItem = + DiscoverItem( + id = tv.id!!, + type = SeerrItemType.TV, + title = tv.name, + subtitle = null, + overview = tv.overview, + availability = + SeerrAvailability.from(tv.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(tv.firstAirDate), + posterUrl = createImageUrl(ImageType.PRIMARY, tv.posterPath, tv.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, tv.backdropPath, tv.mediaInfo), + jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(tv: TvDetails): DiscoverItem = + DiscoverItem( + id = tv.id!!, + type = SeerrItemType.TV, + title = tv.name, + subtitle = null, + overview = tv.overview, + availability = + SeerrAvailability.from(tv.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(tv.firstAirDate), + posterUrl = createImageUrl(ImageType.PRIMARY, tv.posterPath, tv.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, tv.backdropPath, tv.mediaInfo), + jellyfinItemId = tv.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(search: SeerrSearchResult): DiscoverItem = + DiscoverItem( + id = search.id, + type = SeerrItemType.fromString(search.mediaType), + title = search.title ?: search.name, + subtitle = null, + overview = search.overview, + availability = + SeerrAvailability.from(search.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(search.releaseDate ?: search.firstAirDate), + posterUrl = createImageUrl(ImageType.PRIMARY, search.posterPath, search.mediaInfo), + backDropUrl = createImageUrl(ImageType.BACKDROP, search.backdropPath, search.mediaInfo), + jellyfinItemId = search.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(credit: CreditCast): DiscoverItem = + DiscoverItem( + id = credit.id!!, + type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), + title = credit.name ?: credit.title, + subtitle = credit.character, + overview = credit.overview, + availability = + SeerrAvailability.from(credit.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(credit.firstAirDate), + posterUrl = + createImageUrl( + ImageType.PRIMARY, + credit.posterPath ?: credit.profilePath, + credit.mediaInfo, + ), + backDropUrl = + createImageUrl( + ImageType.BACKDROP, + credit.backdropPath, + credit.mediaInfo, + ), + jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) + + suspend fun createDiscoverItem(credit: CreditCrew): DiscoverItem = + DiscoverItem( + id = credit.id!!, + type = SeerrItemType.fromString(credit.mediaType, SeerrItemType.PERSON), + title = credit.name ?: credit.title, + subtitle = credit.job, + overview = credit.overview, + availability = + SeerrAvailability.from(credit.mediaInfo?.status) + ?: SeerrAvailability.UNKNOWN, + releaseDate = toLocalDate(credit.firstAirDate), + posterUrl = + createImageUrl( + ImageType.PRIMARY, + credit.posterPath ?: credit.profilePath, + credit.mediaInfo, + ), + backDropUrl = + createImageUrl( + ImageType.BACKDROP, + credit.backdropPath, + credit.mediaInfo, + ), + jellyfinItemId = credit.mediaInfo?.jellyfinMediaId?.toUUIDOrNull(), + ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt index f4aa80dc..709549cf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt @@ -14,7 +14,9 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.github.damontecres.wholphin.BuildConfig import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.services.UpdateChecker.Companion.ASSET_NAME import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.showToast @@ -51,9 +53,8 @@ class UpdateChecker @param:StandardOkHttpClient private val okHttpClient: OkHttpClient, ) { companion object { - // TODO apk names - private const val ASSET_NAME = "Wholphin" - private const val APK_NAME = "$ASSET_NAME.apk" + const val ASSET_NAME = "Wholphin" + const val APK_NAME = "$ASSET_NAME.apk" private const val APK_MIME_TYPE = "application/vnd.android.package-archive" @@ -118,11 +119,6 @@ class UpdateChecker suspend fun getLatestRelease(updateUrl: String): Release? { return withContext(Dispatchers.IO) { - val preferRelease = - PreferenceManager - .getDefaultSharedPreferences(context) - .getBoolean("updatePreferRelease", true) - val request = Request .Builder() @@ -140,7 +136,7 @@ class UpdateChecker val downloadUrl = result.jsonObject["assets"] ?.jsonArray - ?.let { assets -> getDownloadUrl(assets, preferRelease) } + ?.let { assets -> getDownloadUrl(assets, BuildConfig.DEBUG) } Timber.v("version=$version, downloadUrl=$downloadUrl") if (version != null) { val notes = @@ -165,35 +161,6 @@ class UpdateChecker } } - private fun getDownloadUrl( - assets: JsonArray, - preferRelease: Boolean, - ): String? { - val abiSuffix = Build.SUPPORTED_ABIS.firstOrNull().let { if (it != null) "-$it" else "" } - val releaseSuffix = if (preferRelease) "-release" else "-debug" - val preferredNames = - listOf( - "$ASSET_NAME${releaseSuffix}$abiSuffix.apk", - "$ASSET_NAME$releaseSuffix.apk", - "$ASSET_NAME.apk", - ) - var preferredAsset: JsonObject? = null - outer@ for (name in preferredNames) { - for (asset in assets) { - val assetName = - asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull - if (name == assetName) { - preferredAsset = asset.jsonObject - break@outer - } - } - } - return preferredAsset - ?.get("browser_download_url") - ?.jsonPrimitive - ?.contentOrNull - } - suspend fun installRelease( release: Release, callback: DownloadCallback, @@ -387,3 +354,33 @@ suspend fun copyTo( } return bytesCopied } + +fun getDownloadUrl( + assets: JsonArray, + debug: Boolean, + supportedAbis: List = Build.SUPPORTED_ABIS.toList(), +): String? { + val abiSuffix = supportedAbis.firstOrNull().let { if (it != null) "-$it" else "" } + val releaseSuffix = if (debug) "-debug" else "-release" + val preferredNames = + buildList { + add("$ASSET_NAME${releaseSuffix}$abiSuffix.apk") + add("$ASSET_NAME$releaseSuffix.apk") + if (!debug) add("$ASSET_NAME.apk") + } + var preferredAsset: JsonObject? = null + outer@ for (name in preferredNames) { + for (asset in assets) { + val assetName = + asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull + if (name == assetName) { + preferredAsset = asset.jsonObject + break@outer + } + } + } + return preferredAsset + ?.get("browser_download_url") + ?.jsonPrimitive + ?.contentOrNull +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt index 991a8e98..a44ab65b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt @@ -171,7 +171,7 @@ object AppModule { if (preferences.appPreferences.interfacePreferences.rememberSelectedTab) { scope.launch(ExceptionHandler()) { appPreference.updateData { - preferences.appPreferences.updateInterfacePreferences { + it.updateInterfacePreferences { putRememberedTabs(key(itemId), tabIndex) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/AppScreensaver.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/AppScreensaver.kt index d26675b1..fe4ae115 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/AppScreensaver.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/AppScreensaver.kt @@ -135,7 +135,6 @@ fun AppScreensaverContent( ) var logoError by remember(currentItem) { mutableStateOf(false) } - val alignment = listOf(Alignment.BottomStart, Alignment.BottomEnd).random() if (!logoError) { AsyncImage( model = @@ -150,7 +149,7 @@ fun AppScreensaverContent( }, modifier = Modifier - .align(alignment) + .align(Alignment.BottomStart) .size(width = 240.dp, height = 120.dp) .padding(16.dp), ) @@ -158,7 +157,7 @@ fun AppScreensaverContent( Box( modifier = Modifier - .align(alignment) + .align(Alignment.BottomStart) .padding(16.dp) .fillMaxWidth(.5f) .fillMaxHeight(.3f), @@ -169,7 +168,7 @@ fun AppScreensaverContent( style = MaterialTheme.typography.displaySmall, maxLines = 2, overflow = TextOverflow.Ellipsis, - modifier = Modifier.align(alignment), + modifier = Modifier.align(Alignment.BottomStart), ) } } 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 8415c7a6..a4e4ef22 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 @@ -771,6 +771,9 @@ fun CollectionFolderGrid( onClickDelete = { showDeleteDialog = PositionItem(position, item) }, + onClickGoTo = { + onClickItem.invoke(position, it) + }, ), ), onDismissRequest = { moreDialog.makeAbsent() }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index a96bc859..82aab6ba 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -632,6 +632,7 @@ fun chooseStream( streams: List, currentIndex: Int?, type: MediaStreamType, + preferredSubtitleLanguage: String?, onClick: (Int) -> Unit, ): DialogParams = DialogParams( @@ -669,22 +670,32 @@ fun chooseStream( ) } addAll( - streams.filter { it.type == type }.mapIndexed { index, stream -> - val simpleStream = SimpleMediaStream.from(context, stream, true) - DialogItem( - selected = currentIndex == stream.index, - leadingContent = { - SelectedLeadingContent(currentIndex == stream.index) - }, - headlineContent = { - Text(text = simpleStream.streamTitle ?: simpleStream.displayTitle) - }, - supportingContent = { - if (simpleStream.streamTitle != null) Text(text = simpleStream.displayTitle) - }, - onClick = { onClick.invoke(stream.index) }, - ) - }, + streams + .filter { it.type == type } + .let { + if (type == MediaStreamType.SUBTITLE && preferredSubtitleLanguage.isNotNullOrBlank()) { + it.sortedByDescending { it.language != null && it.language == preferredSubtitleLanguage } + } else { + it + } + }.mapIndexed { index, stream -> + val simpleStream = SimpleMediaStream.from(context, stream, true) + DialogItem( + selected = currentIndex == stream.index, + leadingContent = { + SelectedLeadingContent(currentIndex == stream.index) + }, + headlineContent = { + Text( + text = simpleStream.streamTitle ?: simpleStream.displayTitle, + ) + }, + supportingContent = { + if (simpleStream.streamTitle != null) Text(text = simpleStream.displayTitle) + }, + onClick = { onClick.invoke(stream.index) }, + ) + }, ) }, ) 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 009c4268..9e2141b5 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,6 +5,7 @@ import android.widget.Toast import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.services.PlaylistCreator import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState import com.github.damontecres.wholphin.ui.launchIO @@ -14,6 +15,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import org.jellyfin.sdk.model.api.MediaType +import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -43,7 +45,13 @@ class AddPlaylistViewModel itemId: UUID, ) { viewModelScope.launchIO(ExceptionHandler(autoToast = true)) { - playlistCreator.addToServerPlaylist(playlistId, itemId) + try { + playlistCreator.addToServerPlaylist(playlistId, itemId) + showToast(context, context.getString(R.string.success), Toast.LENGTH_SHORT) + } catch (ex: Exception) { + Timber.e(ex, "Error adding %s to playlist %s", itemId, playlistId) + showToast(context, "Error: ${ex.localizedMessage}", Toast.LENGTH_SHORT) + } } } @@ -55,6 +63,8 @@ class AddPlaylistViewModel val playlistId = playlistCreator.createServerPlaylist(playlistName, listOf(itemId)) if (playlistId == null) { showToast(context, "Error creating playlist", Toast.LENGTH_LONG) + } else { + showToast(context, context.getString(R.string.success), Toast.LENGTH_SHORT) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt index 4ddc41c1..562af214 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderPhotoAlbum.kt @@ -59,7 +59,7 @@ fun CollectionFolderPhotoAlbum( index = index, filter = CollectionFolderFilter(filter = viewModel.filter.value ?: GetItemsFilter()), sortAndDirection = viewModel.sortAndDirection.value ?: SortAndDirection.DEFAULT, - recursive = true, + recursive = recursive, startSlideshow = false, ) } else { 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 f34c0228..5fe13871 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 @@ -30,6 +30,7 @@ data class MoreDialogActions( val onClickAddPlaylist: (UUID) -> Unit, val onSendMediaInfo: (UUID) -> Unit, val onClickDelete: (BaseItem) -> Unit, + val onClickGoTo: (BaseItem) -> Unit = { navigateTo(it.destination()) }, ) enum class ClearChosenStreams { @@ -246,7 +247,7 @@ fun buildMoreDialogItemsForHome( context.getString(R.string.go_to), Icons.Default.ArrowForward, ) { - actions.navigateTo(item.destination()) + actions.onClickGoTo(item) }, ) if (item.type in supportedPlayableTypes) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt index a50e848d..3edd7dd6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt @@ -103,7 +103,7 @@ class DiscoverMovieViewModel ) { Timber.v("Init for movie %s", item.id) val movie = fetchAndSetItem().await() - val discoveredItem = DiscoverItem(movie) + val discoveredItem = seerrService.createDiscoverItem(movie) backdropService.submit(discoveredItem) updateCanCancel() @@ -121,7 +121,7 @@ class DiscoverMovieViewModel seerrService.api.moviesApi .movieMovieIdSimilarGet(movieId = item.id, page = 1) .results - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() similar.setValueOnMain(result) } @@ -130,7 +130,7 @@ class DiscoverMovieViewModel seerrService.api.moviesApi .movieMovieIdRecommendationsGet(movieId = item.id, page = 1) .results - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() recommended.setValueOnMain(result) } @@ -138,11 +138,11 @@ class DiscoverMovieViewModel val people = movie.credits ?.cast - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() + movie.credits ?.crew - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() this@DiscoverMovieViewModel.people.setValueOnMain(people) val trailers = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt index 32d38aa5..f7d50f31 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverPersonPage.kt @@ -67,11 +67,11 @@ class DiscoverPersonViewModel .let { credits -> val cast = credits.cast - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() val crew = credits.crew - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() cast + crew } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt index 3c036534..12acfd37 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt @@ -35,7 +35,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.api.seerr.model.Season import com.github.damontecres.wholphin.api.seerr.model.TvDetails import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.DiscoverItem @@ -99,6 +98,7 @@ fun DiscoverSeriesDetails( var overviewDialog by remember { mutableStateOf(null) } var seasonDialog by remember { mutableStateOf(null) } var moreDialog by remember { mutableStateOf(null) } + var showRequestSeasonDialog by remember { mutableStateOf(false) } val requestStr = stringResource(R.string.request) val request4kStr = stringResource(R.string.request_4k) @@ -159,26 +159,7 @@ fun DiscoverSeriesDetails( trailers = trailers, requestOnClick = { item.id?.let { id -> - if (request4kEnabled) { - moreDialog = - DialogParams( - fromLongClick = false, - title = item.name ?: "", - items = - listOf( - DialogItem( - text = requestStr, - onClick = { viewModel.request(id, false) }, - ), - DialogItem( - text = request4kStr, - onClick = { viewModel.request(id, true) }, - ), - ), - ) - } else { - viewModel.request(id, false) - } + showRequestSeasonDialog = true } }, cancelOnClick = { @@ -218,6 +199,18 @@ fun DiscoverSeriesDetails( waitToLoad = params.fromLongClick, ) } + if (showRequestSeasonDialog) { + RequestSeasonsDialog( + title = item?.name ?: "", + seasons = seasons, + request4kEnabled = request4kEnabled, + onSubmit = { seasons, is4k -> + item?.id?.let { viewModel.request(it, seasons, is4k) } + showRequestSeasonDialog = false + }, + onDismissRequest = { showRequestSeasonDialog = false }, + ) + } } private const val HEADER_ROW = 0 @@ -234,7 +227,7 @@ fun DiscoverSeriesDetailsContent( series: TvDetails, rating: DiscoverRating?, canCancel: Boolean, - seasons: List, + seasons: List, similar: List, recommended: List, trailers: List, @@ -299,6 +292,7 @@ fun DiscoverSeriesDetailsContent( SeerrAvailability.from(series.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN, requestOnClick = requestOnClick, + pendingOnClick = requestOnClick, cancelOnClick = cancelOnClick, moreOnClick = moreOnClick, goToOnClick = goToOnClick, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt index f7b11137..54b3d98f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt @@ -6,12 +6,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.api.seerr.model.RelatedVideo import com.github.damontecres.wholphin.api.seerr.model.RequestPostRequest -import com.github.damontecres.wholphin.api.seerr.model.Season +import com.github.damontecres.wholphin.api.seerr.model.RequestRequestIdPutRequest import com.github.damontecres.wholphin.api.seerr.model.TvDetails import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.DiscoverItem import com.github.damontecres.wholphin.data.model.DiscoverRating import com.github.damontecres.wholphin.data.model.RemoteTrailer +import com.github.damontecres.wholphin.data.model.SeerrAvailability import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.NavigationManager @@ -33,6 +34,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update @@ -63,7 +65,7 @@ class DiscoverSeriesViewModel val tvSeries = MutableLiveData(null) val rating = MutableLiveData(null) - val seasons = MutableLiveData>(listOf()) + val seasons = MutableLiveData>(listOf()) val trailers = MutableLiveData>(listOf()) val people = MutableLiveData>(listOf()) val similar = MutableLiveData>() @@ -100,9 +102,10 @@ class DiscoverSeriesViewModel ) { Timber.v("Init for tv %s", item.id) val tv = fetchAndSetItem().await() - val discoveredItem = DiscoverItem(tv) + val discoveredItem = seerrService.createDiscoverItem(tv) backdropService.submit(discoveredItem) + updateSeasonStatus() updateCanCancel() withContext(Dispatchers.Main) { @@ -118,7 +121,7 @@ class DiscoverSeriesViewModel seerrService.api.tvApi .tvTvIdSimilarGet(tvId = item.id, page = 1) .results - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() similar.setValueOnMain(result) } @@ -127,7 +130,7 @@ class DiscoverSeriesViewModel seerrService.api.tvApi .tvTvIdRecommendationsGet(tvId = item.id, page = 1) .results - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() recommended.setValueOnMain(result) } @@ -135,11 +138,11 @@ class DiscoverSeriesViewModel val people = tv.credits ?.cast - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() + tv.credits ?.crew - ?.map(::DiscoverItem) + ?.map { seerrService.createDiscoverItem(it) } .orEmpty() this@DiscoverSeriesViewModel.people.setValueOnMain(people) @@ -157,6 +160,43 @@ class DiscoverSeriesViewModel navigationManager.navigateTo(destination) } + private suspend fun updateSeasonStatus() { + tvSeries.value?.let { tv -> + val seasonStatus = mutableMapOf() + tv.seasons?.forEach { + it.seasonNumber?.let { + seasonStatus[it] = SeerrAvailability.UNKNOWN + } + } + val tvStatus = + SeerrAvailability.from(tv.mediaInfo?.status) ?: SeerrAvailability.UNKNOWN + tv.mediaInfo + ?.requests + ?.forEach { + it.seasons?.mapNotNull { season -> + season.seasonNumber?.let { + val current = seasonStatus[season.seasonNumber] + val new = + SeerrAvailability + .from(season.status) + ?.takeIf { it != SeerrAvailability.UNKNOWN } ?: tvStatus + if (current == null || new.status > current.status) { + seasonStatus[season.seasonNumber] = new + } + } + } + } + Timber.v("seasonStatus=%s", seasonStatus) + val requestSeasons = + seasonStatus.mapNotNull { (seasonNumber, availability) -> + tv.seasons?.firstOrNull { it.seasonNumber == seasonNumber }?.let { + RequestSeason(it, availability) + } + } + seasons.setValueOnMain(requestSeasons) + } + } + private suspend fun updateCanCancel() { val user = userConfig.firstOrNull() val canCancel = canUserCancelRequest(user, tvSeries.value?.mediaInfo?.requests) @@ -165,20 +205,43 @@ class DiscoverSeriesViewModel fun request( id: Int, + seasons: Set, is4k: Boolean, ) { viewModelScope.launchIO { - val request = - seerrService.api.requestApi.requestPost( - RequestPostRequest( - is4k = is4k, - mediaId = id, - mediaType = RequestPostRequest.MediaType.TV, - seasons = RequestPostRequest.Seasons.ALL, // TODO handle picking seasons - ), - ) - fetchAndSetItem().await() - updateCanCancel() + tvSeries.value?.let { tv -> + val currentRequest = + tv.mediaInfo?.requests?.firstOrNull { + it.requestedBy?.id == + seerrServerRepository.currentUserId.first() + } + if (currentRequest != null) { + Timber.v("User has pending request, will update") + seerrService.api.requestApi.requestRequestIdPut( + requestId = currentRequest.id.toString(), + requestRequestIdPutRequest = + RequestRequestIdPutRequest( + is4k = is4k, + mediaType = RequestRequestIdPutRequest.MediaType.TV, + seasons = seasons.toList(), + ), + ) + } else { + Timber.v("New request for %s seasons", seasons.size) + seerrService.api.requestApi.requestPost( + RequestPostRequest( + is4k = is4k, + mediaId = id, + mediaType = RequestPostRequest.MediaType.TV, + seasons = seasons.toList(), + ), + ) + } + + fetchAndSetItem().await() + updateSeasonStatus() + updateCanCancel() + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/ExpandableDiscoverButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/ExpandableDiscoverButtons.kt index 4b73a0b2..2c3fc036 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/ExpandableDiscoverButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/ExpandableDiscoverButtons.kt @@ -37,6 +37,7 @@ fun ExpandableDiscoverButtons( trailerOnClick: (Trailer) -> Unit, buttonOnFocusChanged: (FocusState) -> Unit, modifier: Modifier = Modifier, + pendingOnClick: () -> Unit = {}, ) { val firstFocus = remember { FocusRequester() } LazyRow( @@ -89,7 +90,7 @@ fun ExpandableDiscoverButtons( SeerrAvailability.PENDING, SeerrAvailability.PROCESSING, -> { - // TODO? + pendingOnClick.invoke() } SeerrAvailability.PARTIALLY_AVAILABLE, @@ -109,6 +110,21 @@ fun ExpandableDiscoverButtons( .onFocusChanged(buttonOnFocusChanged), ) } + if (availability == SeerrAvailability.PARTIALLY_AVAILABLE) { + item("request_partial") { + ExpandableFaButton( + title = R.string.request, + iconStringRes = R.string.fa_download, + onClick = { + requestOnClick.invoke() + }, + enabled = availability == SeerrAvailability.PARTIALLY_AVAILABLE, + modifier = + Modifier + .onFocusChanged(buttonOnFocusChanged), + ) + } + } if (canCancel) { item("cancel") { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt new file mode 100644 index 00000000..4a56cb88 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/RequestSeasons.kt @@ -0,0 +1,323 @@ +package com.github.damontecres.wholphin.ui.detail.discover + +import android.content.res.Configuration.UI_MODE_TYPE_TELEVISION +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableStateSetOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Button +import androidx.tv.material3.ClickableSurfaceDefaults +import androidx.tv.material3.ListItem +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.Switch +import androidx.tv.material3.Text +import androidx.tv.material3.contentColorFor +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.api.seerr.model.Season +import com.github.damontecres.wholphin.data.model.SeerrAvailability +import com.github.damontecres.wholphin.ui.cards.AvailableIndicator +import com.github.damontecres.wholphin.ui.cards.PartiallyAvailableIndicator +import com.github.damontecres.wholphin.ui.cards.PendingIndicator +import com.github.damontecres.wholphin.ui.components.BasicDialog +import com.github.damontecres.wholphin.ui.theme.WholphinTheme + +data class RequestSeason( + val season: Season, + val availability: SeerrAvailability, +) + +@Composable +fun RequestSeasons( + title: String, + seasons: List, + onSubmit: (Set, Boolean) -> Unit, + request4kEnabled: Boolean, + modifier: Modifier, +) { + val allSeasonNumbers = remember(seasons) { seasons.mapNotNull { it.season.seasonNumber }.toSet() } + val selected = + remember { + mutableStateSetOf( + *seasons + .mapNotNull { + if (it.availability > SeerrAvailability.UNKNOWN) { + it.season.seasonNumber + } else { + null + } + }.toTypedArray(), + ) + } + var is4k by remember { mutableStateOf(false) } + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier, + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + LazyColumn { + item { + val isSelected = selected.containsAll(allSeasonNumbers) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + ClickSwitch( + label = stringResource(R.string.select_all), + checked = isSelected, + onClick = { + if (isSelected) { + selected.removeAll(allSeasonNumbers) + } else { + selected.addAll(allSeasonNumbers) + } + }, + ) + Button( + onClick = { onSubmit.invoke(selected, is4k) }, + ) { + Text( + text = stringResource(R.string.submit), + ) + } + } + } + if (request4kEnabled) { + item { + ClickSwitch( + label = stringResource(R.string.request_4k), + checked = is4k, + onClick = { is4k = !is4k }, + ) + } + } + itemsIndexed(seasons) { index, season -> + val seasonNumber = season.season.seasonNumber + val isSelected = seasonNumber in selected + SeasonListItem( + season = season, + selected = isSelected, + onClick = { + if (isSelected) { + selected.remove(seasonNumber) + } else { + seasonNumber?.let { selected.add(it) } + } + }, + modifier = Modifier, + ) + } + if (seasons.size > 7) { + item { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + ) { + Button( + onClick = { onSubmit.invoke(selected, is4k) }, + ) { + Text( + text = stringResource(R.string.submit), + ) + } + } + } + } + } + } +} + +@Composable +fun SeasonListItem( + season: RequestSeason, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ListItem( + selected = false, + headlineContent = { + Text( + text = + season.season.name + ?: (stringResource(R.string.tv_season) + " ${season.season.seasonNumber}"), + ) + }, + supportingContent = { + season.season.episodeCount?.let { + Text( + // TODO should use plurals string + text = "${season.season.episodeCount} " + stringResource(R.string.episodes), + ) + } + }, + leadingContent = { + when (season.availability) { + SeerrAvailability.UNKNOWN -> {} + + SeerrAvailability.DELETED -> {} + + SeerrAvailability.PENDING, + SeerrAvailability.PROCESSING, + -> { + PendingIndicator() + } + + SeerrAvailability.PARTIALLY_AVAILABLE -> { + PartiallyAvailableIndicator() + } + + SeerrAvailability.AVAILABLE -> { + AvailableIndicator() + } + } + }, + trailingContent = { + Row { + Switch( + checked = selected, + onCheckedChange = { + onClick.invoke() + }, + ) + } + }, + onClick = onClick, + modifier = modifier, + ) +} + +@Composable +private fun ClickSurface( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + Surface( + colors = + ClickableSurfaceDefaults.colors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + focusedContainerColor = MaterialTheme.colorScheme.inverseSurface, + focusedContentColor = contentColorFor(MaterialTheme.colorScheme.inverseSurface), + pressedContainerColor = MaterialTheme.colorScheme.inverseSurface, + pressedContentColor = contentColorFor(MaterialTheme.colorScheme.inverseSurface), + ), + onClick = onClick, + content = content, + modifier = modifier, + ) +} + +@Composable +private fun ClickSwitch( + label: String, + checked: Boolean, + onClick: () -> Unit, +) { + ClickSurface( + onClick = onClick, + modifier = Modifier, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .padding(horizontal = 8.dp) + .height(54.dp), + ) { + Switch( + checked = checked, + onCheckedChange = {}, + modifier = Modifier.padding(end = 8.dp), + ) + Text( + text = label, + ) + } + } +} + +@Composable +fun RequestSeasonsDialog( + title: String, + seasons: List, + request4kEnabled: Boolean, + onSubmit: (Set, Boolean) -> Unit, + onDismissRequest: () -> Unit, +) { + BasicDialog( + onDismissRequest = onDismissRequest, + ) { + RequestSeasons( + title = title, + seasons = seasons, + request4kEnabled = request4kEnabled, + onSubmit = onSubmit, + modifier = Modifier.padding(16.dp), + ) + } +} + +@Preview( + device = "spec:parent=tv_1080p", + backgroundColor = 0xFF383535, + uiMode = UI_MODE_TYPE_TELEVISION, + heightDp = 800, +) +@Composable +fun RequestSeasonsPreview() { + val seasons = + List(10) { + RequestSeason( + season = + Season( + seasonNumber = it + 1, + episodeCount = 10 + it, + ), + availability = + if (it < 3) { + SeerrAvailability.AVAILABLE + } else { + SeerrAvailability.UNKNOWN + }, + ) + } + + WholphinTheme { + RequestSeasons( + title = "Series title", + seasons = seasons, + request4kEnabled = true, + onSubmit = { _, _ -> }, + modifier = Modifier.width(400.dp), + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt index ee3cc4a2..e341923b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/episode/EpisodeDetails.kt @@ -86,6 +86,13 @@ fun EpisodeDetails( var showDeleteDialog by remember { mutableStateOf(null) } val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) + val preferredSubtitleLanguage = + viewModel.serverRepository.currentUserDto + .observeAsState() + .value + ?.configuration + ?.subtitleLanguagePreference + val moreActions = MoreDialogActions( navigateTo = viewModel::navigateTo, @@ -200,6 +207,7 @@ fun EpisodeDetails( type, ) }, + preferredSubtitleLanguage = preferredSubtitleLanguage, ) } }, 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 a59ecde1..87a5dd75 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 @@ -114,6 +114,13 @@ fun MovieDetails( val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) var showDeleteDialog by remember { mutableStateOf(null) } + val preferredSubtitleLanguage = + viewModel.serverRepository.currentUserDto + .observeAsState() + .value + ?.configuration + ?.subtitleLanguagePreference + val moreActions = MoreDialogActions( navigateTo = viewModel::navigateTo, @@ -246,6 +253,7 @@ fun MovieDetails( type, ) }, + preferredSubtitleLanguage = preferredSubtitleLanguage, ) } }, 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 81beb82f..4f8e4366 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 @@ -1,6 +1,9 @@ package com.github.damontecres.wholphin.ui.detail.series import android.content.Context +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -120,6 +123,7 @@ fun SeriesDetails( val people by viewModel.people.observeAsState(listOf()) val similar by viewModel.similar.observeAsState(listOf()) val discovered by viewModel.discovered.collectAsState() + val discoverSeries by viewModel.discoverSeries.collectAsState() val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending) var overviewDialog by remember { mutableStateOf(null) } @@ -230,6 +234,12 @@ fun SeriesDetails( onClickExtra = { _, extra -> viewModel.navigateTo(extra.destination) }, + discoverSeries = discoverSeries, + onClickDiscoverSeries = { + discoverSeries?.let { + viewModel.navigateTo(Destination.DiscoveredItem(it)) + } + }, discovered = discovered, onClickDiscover = { index, item -> viewModel.navigateTo(item.destination) @@ -350,6 +360,8 @@ fun SeriesDetailsContent( onClickExtra: (Int, ExtrasItem) -> Unit, moreActions: MoreDialogActions, onClickDiscover: (Int, DiscoverItem) -> Unit, + discoverSeries: DiscoverItem?, + onClickDiscoverSeries: () -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -487,6 +499,25 @@ fun SeriesDetailsContent( }, ) } + AnimatedVisibility( + visible = discoverSeries != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + ExpandableFaButton( + title = R.string.discover, + iconStringRes = R.string.fa_magnifying_glass_plus, + onClick = onClickDiscoverSeries, + modifier = + Modifier.onFocusChanged { + if (it.isFocused) { + scope.launch(ExceptionHandler()) { + bringIntoViewRequester.bringIntoView() + } + } + }, + ) + } } } item { 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 3ed615e9..84180f20 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 @@ -125,20 +125,6 @@ fun SeriesOverview( var rowFocused by rememberInt() var showDeleteDialog by remember { mutableStateOf(null) } - LaunchedEffect(episodes) { - episodes?.let { episodes -> - if (episodes is EpisodeList.Success) { - if (episodes.episodes.isNotEmpty()) { - // TODO focus on first episode when changing seasons? -// firstItemFocusRequester.requestFocus() - episodes.episodes.getOrNull(position.episodeRowIndex)?.let { - viewModel.refreshEpisode(it.id, position.episodeRowIndex) - } - } - } - } - } - LaunchedEffect(position, episodes) { val focusedEpisode = (episodes as? EpisodeList.Success) @@ -152,6 +138,13 @@ fun SeriesOverview( } val chosenStreams by viewModel.chosenStreams.observeAsState(null) + val preferredSubtitleLanguage = + viewModel.serverRepository.currentUserDto + .observeAsState() + .value + ?.configuration + ?.subtitleLanguagePreference + when (val state = loading) { is LoadingState.Error -> { ErrorMessage(state, modifier) @@ -266,6 +259,7 @@ fun SeriesOverview( type, ) }, + preferredSubtitleLanguage = preferredSubtitleLanguage, ) } }, 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 53924431..2d044670 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 @@ -58,6 +58,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update @@ -123,6 +124,7 @@ class SeriesViewModel val peopleInEpisode = MutableLiveData(PeopleInItem()) val discovered = MutableStateFlow>(listOf()) + val discoverSeries = MutableStateFlow(null) val position = MutableStateFlow(SeriesOverviewPosition(0, 0)) @@ -232,6 +234,24 @@ class SeriesViewModel val results = seerrService.similar(item).orEmpty() discovered.update { results } } + viewModelScope.launchIO { + seerrService.active.collectLatest { active -> + val tv = + if (active) { + try { + seerrService + .getTvSeries(item) + ?.let { seerrService.createDiscoverItem(it) } + } catch (ex: Exception) { + Timber.e(ex) + null + } + } else { + null + } + discoverSeries.update { tv } + } + } } mediaManagementService.deletedItemFlow .onEach { deletedItem -> @@ -369,12 +389,6 @@ class SeriesViewModel withContext(Dispatchers.Main) { this@SeriesViewModel.episodes.value = episodes } - if (currentEpisodes == null || currentEpisodes.seasonId != seasonId) { - (episodes as? EpisodeList.Success) - ?.let { - it.episodes.getOrNull(it.initialEpisodeIndex) - }?.let { lookupPeopleInEpisode(it) } - } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt index 85863650..6ed79a06 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrRequestsPage.kt @@ -85,13 +85,13 @@ class SeerrRequestsViewModel seerrService.api.moviesApi .movieMovieIdGet( movieId = request.media.tmdbId, - ).let { DiscoverItem(it) } + ).let { seerrService.createDiscoverItem(it) } } SeerrItemType.TV -> { seerrService.api.tvApi .tvTvIdGet(tvId = request.media.tmdbId) - .let { DiscoverItem(it) } + .let { seerrService.createDiscoverItem(it) } } SeerrItemType.PERSON -> { 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 b5839aad..a7fade4e 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 @@ -165,7 +165,7 @@ class SearchViewModel val results = seerrService .search(query) - .map { DiscoverItem(it) } + .map { seerrService.createDiscoverItem(it) } .filter { it.type == SeerrItemType.MOVIE || it.type == SeerrItemType.TV } seerrResults.setValueOnMain(SearchResult.SuccessSeerr(results)) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 0888e721..c0e4ce26 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -209,7 +209,7 @@ fun DestinationContent( CollectionFolderPhotoAlbum( preferences = preferences, itemId = destination.itemId, - recursive = true, + recursive = false, modifier = modifier, ) } @@ -387,10 +387,19 @@ fun CollectionFolder( } CollectionType.HOMEVIDEOS, + CollectionType.PHOTOS, + -> { + CollectionFolderPhotoAlbum( + preferences = preferences, + itemId = destination.itemId, + recursive = recursiveOverride ?: false, + modifier = modifier, + ) + } + CollectionType.MUSICVIDEOS, CollectionType.MUSIC, CollectionType.BOOKS, - CollectionType.PHOTOS, -> { CollectionFolderGeneric( preferences, 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 75ac0951..8ad9282c 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 @@ -8,7 +8,6 @@ import androidx.annotation.OptIn import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -24,7 +23,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable @@ -37,7 +35,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment 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 @@ -64,6 +61,7 @@ import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.components.Button import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent import com.github.damontecres.wholphin.ui.components.TextButton +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.skipStringRes @@ -470,6 +468,14 @@ fun BottomDialog( gravity: Int, currentChoice: BottomDialogItem? = null, ) { + val focusRequesters = remember(choices.size) { List(choices.size) { FocusRequester() } } + if (currentChoice != null) { + LaunchedEffect(Unit) { + choices.indexOfFirstOrNull { it == currentChoice }?.let { + focusRequesters.getOrNull(it)?.tryRequestFocus() + } + } + } // TODO enforcing a width ends up ignore the gravity Dialog( onDismissRequest = onDismissRequest, @@ -504,6 +510,7 @@ fun BottomDialog( val interactionSource = remember { MutableInteractionSource() } ListItem( selected = choice == currentChoice, + enabled = choice.enabled, onClick = { onDismissRequest() onSelectChoice(index, choice) @@ -524,6 +531,7 @@ fun BottomDialog( } }, interactionSource = interactionSource, + modifier = Modifier.focusRequester(focusRequesters[index]), ) } } @@ -539,6 +547,7 @@ data class BottomDialogItem( val data: T, val headline: String, val supporting: String?, + val enabled: Boolean = true, ) @PreviewTvSpec diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt index c6d7b622..a305023c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt @@ -14,9 +14,12 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource @@ -32,6 +35,8 @@ import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent +import com.github.damontecres.wholphin.ui.indexOfFirstOrNull +import com.github.damontecres.wholphin.ui.tryRequestFocus import kotlin.time.Duration enum class PlaybackDialogType { @@ -54,6 +59,7 @@ data class PlaybackSettings( val contentScale: ContentScale, val subtitleDelay: Duration, val hasSubtitleDownloadPermission: Boolean, + val playbackSpeedEnabled: Boolean, ) @Composable @@ -137,6 +143,7 @@ fun PlaybackDialog( data = PlaybackDialogType.PLAYBACK_SPEED, headline = stringResource(R.string.playback_speed), supporting = settings.playbackSpeed.toString(), + enabled = settings.playbackSpeedEnabled, ), ) if (enableVideoScale) { @@ -395,6 +402,14 @@ fun StreamChoiceBottomDialog( gravity: Int, currentChoice: Int? = null, ) { + val focusRequesters = remember(choices.size) { List(choices.size) { FocusRequester() } } + if (currentChoice != null) { + LaunchedEffect(Unit) { + choices.indexOfFirstOrNull { it.index == currentChoice }?.let { + focusRequesters.getOrNull(it)?.tryRequestFocus() + } + } + } // TODO enforcing a width ends up ignore the gravity Dialog( onDismissRequest = onDismissRequest, @@ -445,6 +460,7 @@ fun StreamChoiceBottomDialog( if (choice.streamTitle != null) Text(choice.displayTitle) }, interactionSource = interactionSource, + modifier = Modifier.focusRequester(focusRequesters[index]), ) } } 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 c7a4bf10..799bb4a3 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 @@ -637,6 +637,9 @@ fun PlaybackPageContent( subtitleDelay = subtitleDelay, hasSubtitleDownloadPermission = remember(userDto) { userDto?.policy?.let { it.isAdministrator || it.enableSubtitleManagement } == true }, + // TODO Passing through audio prevents changing playback speed + // See https://github.com/damontecres/Wholphin/issues/164 + playbackSpeedEnabled = playerBackend == PlayerBackend.MPV || currentPlayback?.audioDecoder != null, ), onDismissRequest = { playbackDialog = null diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 4f652360..7eabeec1 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 @@ -449,11 +449,20 @@ class PlaybackViewModel // Create the correct player for the media createPlayer(videoStream?.hdr == true, videoStream?.is4k == true) - + val subtitleLanguagePreference = + serverRepository.currentUserDto.value + ?.configuration + ?.subtitleLanguagePreference val subtitleStreams = mediaSource.mediaStreams ?.filter { it.type == MediaStreamType.SUBTITLE } - ?.map { + .let { + if (subtitleLanguagePreference.isNotNullOrBlank()) { + it?.sortedByDescending { it.language != null && subtitleLanguagePreference == it.language } + } else { + it + } + }?.map { SimpleMediaStream.from(context, it, true) }.orEmpty() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt index 729e7085..c214d3cb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt @@ -153,7 +153,7 @@ fun PreferencesContent( try { System.loadLibrary("mpv") System.loadLibrary("player") - } catch (ex: Exception) { + } catch (ex: UnsatisfiedLinkError) { Timber.w(ex, "Could not load libmpv") showToast(context, "MPV is not supported on this device") viewModel.preferenceDataStore.updateData { @@ -400,7 +400,7 @@ fun PreferencesContent( when (val conn = seerrConnection) { is SeerrConnectionStatus.Error -> { SeerrDialogMode.Error( - conn.serverUrl, + conn.server.url, conn.ex, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt index 0f62f12f..7c05194a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/seerr/SwitchSeerrViewModel.kt @@ -98,6 +98,7 @@ class SwitchSeerrViewModel ) } } + serverConnectionStatus.update { LoadingState.Success } } else { val message = results @@ -126,37 +127,50 @@ class SwitchSeerrViewModel } fun createUrls(url: String): List { - val urls = mutableListOf() + val urls = mutableListOf() if (url.startsWith("http://") || url.startsWith("https://")) { - urls.add(url) val httpUrl = url.toHttpUrl() + urls.add(httpUrl) if (HttpUrl.defaultPort(httpUrl.scheme) == httpUrl.port) { - urls.add("$url:5055") + urls.add(httpUrl.newBuilder().port(5055).build()) } } else { - urls.add("http://$url") val httpUrl = "http://$url".toHttpUrl() + urls.add(httpUrl) if (httpUrl.port == 80) { - urls.add("https://$url") - urls.add("http://$url:5055") - urls.add("https://$url:5055") + urls.add(httpUrl.newBuilder().scheme("https").build()) + urls.add(httpUrl.newBuilder().port(5055).build()) + urls.add( + httpUrl + .newBuilder() + .scheme("https") + .port(5055) + .build(), + ) } else { - urls.add("https://$url") + urls.add(httpUrl.newBuilder().scheme("https").build()) } } - return urls.map { cleanUrl(it).toHttpUrl() } + return urls } -private fun cleanUrl(url: String) = - if (!url.endsWith("/api/v1")) { +fun createSeerrApiUrl(url: String): String = + if (url.isBlank()) { + url + } else if (url.endsWith("/api/v1") || url.endsWith("/api/v1/")) { + url + } else { url .toHttpUrl() .newBuilder() - .apply { - addPathSegment("api") - addPathSegment("v1") - }.build() + .addPathSegment("api") + .addPathSegment("v1") + .build() .toString() - } else { - url } + +fun migrateSeerrUrl(url: String): String { + var url = url.removeSuffix("/api/v1/").removeSuffix("/api/v1") + if (!url.endsWith("/")) url += "/" + return url +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowPage.kt index 3b57017e..faccb4cc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowPage.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -53,13 +52,16 @@ import androidx.media3.ui.compose.modifiers.resizeWithContentScale import androidx.media3.ui.compose.state.rememberPresentationState import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import coil3.annotation.ExperimentalCoilApi import coil3.compose.SubcomposeAsyncImage +import coil3.compose.useExistingImageAsPlaceholder import coil3.request.ImageRequest -import coil3.request.crossfade +import coil3.request.transitionFactory import coil3.size.Size import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.VideoFilter import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.CrossFadeFactory import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.nav.Destination @@ -71,12 +73,14 @@ import com.github.damontecres.wholphin.util.LoadingState import org.jellyfin.sdk.model.api.MediaType import timber.log.Timber import kotlin.math.abs +import kotlin.time.Duration.Companion.milliseconds private const val TAG = "ImagePage" private const val DEBUG = false @SuppressLint("ConfigurationScreenWidthHeight") @OptIn(UnstableApi::class) +@kotlin.OptIn(ExperimentalCoilApi::class) @Composable fun SlideshowPage( slideshow: Destination.Slideshow, @@ -107,7 +111,7 @@ fun SlideshowPage( val imageFilter by viewModel.imageFilter.observeAsState(VideoFilter()) val position by viewModel.position.observeAsState(0) val pager by viewModel.pager.observeAsState() - val imageState by viewModel.image.observeAsState() +// val imageState by viewModel.image.observeAsState() var zoomFactor by rememberSaveable { mutableFloatStateOf(1f) } val isZoomed = zoomFactor * 100 > 102 @@ -211,9 +215,6 @@ fun SlideshowPage( } } - LaunchedEffect(imageState) { - reset(true) - } val player = viewModel.player val presentationState = rememberPresentationState(player) LaunchedEffect(slideshowActive) { @@ -223,16 +224,6 @@ fun SlideshowPage( var longPressing by remember { mutableStateOf(false) } - val contentModifier = - Modifier - .clickable( - interactionSource = null, - indication = null, - onClick = { - showOverlay = !showOverlay - }, - ) - Box( modifier = modifier @@ -317,150 +308,145 @@ fun SlideshowPage( result }, ) { - when (loadingState) { + when (val st = loadingState) { ImageLoadingState.Error -> { ErrorMessage("Error loading image", null, modifier) } ImageLoadingState.Loading -> { - LoadingPage(modifier) + LoadingPage(modifier, false) } is ImageLoadingState.Success -> { - imageState?.let { imageState -> - if (imageState.image.data.mediaType == MediaType.VIDEO) { - LaunchedEffect(imageState.id) { - val mediaItem = - MediaItem - .Builder() - .setUri(imageState.url) - .build() - player.setMediaItem(mediaItem) - player.repeatMode = - if (slideshowState.enabled) { - Player.REPEAT_MODE_OFF - } else { - Player.REPEAT_MODE_ONE - } - player.prepare() - player.play() - viewModel.pulseSlideshow(Long.MAX_VALUE) - } - LifecycleStartEffect(Unit) { - onStopOrDispose { - player.stop() + val imageState = st.image + LaunchedEffect(imageState) { + reset(true) + } + if (imageState.image.data.mediaType == MediaType.VIDEO) { + LaunchedEffect(imageState.id) { + val mediaItem = + MediaItem + .Builder() + .setUri(imageState.url) + .build() + player.setMediaItem(mediaItem) + player.repeatMode = + if (slideshowState.enabled) { + Player.REPEAT_MODE_OFF + } else { + Player.REPEAT_MODE_ONE } + player.prepare() + player.play() + viewModel.pulseSlideshow(Long.MAX_VALUE) + } + LifecycleStartEffect(Unit) { + onStopOrDispose { + player.stop() } - val contentScale = ContentScale.Fit - val scaledModifier = - contentModifier.resizeWithContentScale( - contentScale, - presentationState.videoSizeDp, - ) - PlayerSurface( - player = player, - surfaceType = SURFACE_TYPE_SURFACE_VIEW, - modifier = - scaledModifier - .fillMaxSize() - .graphicsLayer { - scaleX = zoomAnimation - scaleY = zoomAnimation - translationX = panXAnimation - translationY = panYAnimation - }.rotate(rotateAnimation), + } + val contentScale = ContentScale.Fit + val scaledModifier = + Modifier.resizeWithContentScale( + contentScale, + presentationState.videoSizeDp, ) - if (presentationState.coverSurface) { - Box( - Modifier - .matchParentSize() - .background(Color.Black), - ) - } - } else { - val colorFilter = - remember(imageState.id, imageFilter) { - if (imageFilter.hasImageFilter()) { - ColorMatrixColorFilter(imageFilter.colorMatrix) - } else { - null - } - } - // If the image loading is large, show the thumbnail while waiting - // TODO - val showLoadingThumbnail = true - SubcomposeAsyncImage( - modifier = - contentModifier - .fillMaxSize() - .graphicsLayer { - scaleX = zoomAnimation - scaleY = zoomAnimation - translationX = panXAnimation - translationY = panYAnimation - - val xTransform = - (screenWidth - panXAnimation) / (screenWidth * 2) - val yTransform = - (screenHeight - panYAnimation) / (screenHeight * 2) - if (DEBUG) { - Timber.d( - "graphicsLayer: xTransform=$xTransform, yTransform=$yTransform", - ) - } - - transformOrigin = - TransformOrigin(xTransform, yTransform) - }.rotate(rotateAnimation), - model = - ImageRequest - .Builder(LocalContext.current) - .data(imageState.url) - .size(Size.ORIGINAL) - .crossfade(!showLoadingThumbnail) - .build(), - contentDescription = null, - contentScale = ContentScale.Fit, - colorFilter = colorFilter, - error = { - Text( - modifier = - Modifier - .align(Alignment.Center), - text = "Error loading image", - color = MaterialTheme.colorScheme.onBackground, - ) - }, - loading = { - ImageLoadingPlaceholder( - thumbnailUrl = imageState.thumbnailUrl, - showThumbnail = showLoadingThumbnail, - colorFilter = colorFilter, - modifier = Modifier.fillMaxSize(), - ) - }, - // Ensure that if an image takes a long time to load, it won't be skipped - onLoading = { - viewModel.pulseSlideshow(Long.MAX_VALUE) - }, - onSuccess = { - viewModel.pulseSlideshow() - }, - onError = { - Timber.e( - it.result.throwable, - "Error loading image ${imageState.id}", - ) - Toast - .makeText( - context, - "Error loading image: ${it.result.throwable.localizedMessage}", - Toast.LENGTH_LONG, - ).show() - viewModel.pulseSlideshow() - }, + PlayerSurface( + player = player, + surfaceType = SURFACE_TYPE_SURFACE_VIEW, + modifier = + scaledModifier + .fillMaxSize() + .graphicsLayer { + scaleX = zoomAnimation + scaleY = zoomAnimation + translationX = panXAnimation + translationY = panYAnimation + }.rotate(rotateAnimation), + ) + if (presentationState.coverSurface) { + Box( + Modifier + .matchParentSize() + .background(Color.Black), ) } + } else { + val colorFilter = + remember(imageState.id, imageFilter) { + if (imageFilter.hasImageFilter()) { + ColorMatrixColorFilter(imageFilter.colorMatrix) + } else { + null + } + } + // If the image loading is large, show the thumbnail while waiting + // TODO + val showLoadingThumbnail = true + SubcomposeAsyncImage( + modifier = + Modifier + .fillMaxSize() + .graphicsLayer { + scaleX = zoomAnimation + scaleY = zoomAnimation + translationX = panXAnimation + translationY = panYAnimation + + val xTransform = + (screenWidth - panXAnimation) / (screenWidth * 2) + val yTransform = + (screenHeight - panYAnimation) / (screenHeight * 2) + if (DEBUG) { + Timber.d( + "graphicsLayer: xTransform=$xTransform, yTransform=$yTransform", + ) + } + + transformOrigin = + TransformOrigin(xTransform, yTransform) + }.rotate(rotateAnimation), + model = + ImageRequest + .Builder(LocalContext.current) + .data(imageState.url) + .size(Size.ORIGINAL) + .transitionFactory(CrossFadeFactory(750.milliseconds)) + .useExistingImageAsPlaceholder(true) + .build(), + contentDescription = null, + contentScale = ContentScale.Fit, + colorFilter = colorFilter, + error = { + Text( + modifier = + Modifier + .align(Alignment.Center), + text = "Error loading image", + color = MaterialTheme.colorScheme.onBackground, + ) + }, + // Ensure that if an image takes a long time to load, it won't be skipped + onLoading = { + viewModel.pulseSlideshow(Long.MAX_VALUE) + }, + onSuccess = { + viewModel.pulseSlideshow() + }, + onError = { + Timber.e( + it.result.throwable, + "Error loading image ${imageState.id}", + ) + Toast + .makeText( + context, + "Error loading image: ${it.result.throwable.localizedMessage}", + Toast.LENGTH_LONG, + ).show() + viewModel.pulseSlideshow() + }, + ) } } } @@ -470,30 +456,37 @@ fun SlideshowPage( exit = slideOutVertically { it }, modifier = Modifier.align(Alignment.BottomStart), ) { - imageState?.let { imageState -> - ImageOverlay( - modifier = - contentModifier - .fillMaxWidth() - .background(AppColors.TransparentBlack50), - onDismiss = { showOverlay = false }, - player = player, - slideshowControls = slideshowControls, - slideshowEnabled = slideshowState.enabled, - image = imageState, - position = position, - count = pager?.size ?: -1, - onClickItem = {}, - onLongClickItem = {}, - onZoom = ::zoom, - onRotate = { rotation += it }, - onReset = { reset(true) }, - onShowFilterDialogClick = { - showFilterDialog = true - showOverlay = false - viewModel.pauseSlideshow() - }, - ) + when (val st = loadingState) { + ImageLoadingState.Error -> {} + + ImageLoadingState.Loading -> {} + + is ImageLoadingState.Success -> { + val imageState = st.image + ImageOverlay( + modifier = + Modifier + .fillMaxWidth() + .background(AppColors.TransparentBlack50), + onDismiss = { showOverlay = false }, + player = player, + slideshowControls = slideshowControls, + slideshowEnabled = slideshowState.enabled, + image = imageState, + position = position, + count = pager?.size ?: -1, + onClickItem = {}, + onLongClickItem = {}, + onZoom = ::zoom, + onRotate = { rotation += it }, + onReset = { reset(true) }, + onShowFilterDialogClick = { + showFilterDialog = true + showOverlay = false + viewModel.pauseSlideshow() + }, + ) + } } } AnimatedVisibility(showFilterDialog) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt index b86814ac..f9abe15d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/slideshow/SlideshowViewModel.kt @@ -97,7 +97,6 @@ class SlideshowViewModel var slideshowDelay by Delegates.notNull() - // private val album = MutableLiveData() private val _pager = MutableLiveData>() val pager: LiveData> = _pager.map { it } val position = MutableLiveData(0) @@ -148,7 +147,7 @@ class SlideshowViewModel parentId = slideshowSettings.parentId, includeItemTypes = includeItemTypes, fields = PhotoItemFields, - recursive = true, + recursive = slideshowSettings.recursive, sortBy = listOf(slideshowSettings.sortAndDirection.sort), sortOrder = listOf(slideshowSettings.sortAndDirection.direction), ), @@ -205,7 +204,8 @@ class SlideshowViewModel _pager.value?.let { pager -> viewModelScope.launchIO { try { - val image = pager.getBlocking(position) + val image = + if (position in pager.indices) pager.getBlocking(position) else null Timber.v("Got image for $position: ${image != null}") if (image != null) { this@SlideshowViewModel.position.setValueOnMain(position) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt index d07f86ff..bca5ae64 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/util/LocalClock.kt @@ -34,7 +34,7 @@ data class Clock( fun ProvideLocalClock(content: @Composable () -> Unit) { val clock = remember { Clock() } LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { + withContext(Dispatchers.Default) { while (isActive) { val now = LocalDateTime.now() val time = TimeFormatter.format(now) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/util/StreamFormatting.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/util/StreamFormatting.kt index ca6e4027..15d35b3b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/util/StreamFormatting.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/util/StreamFormatting.kt @@ -92,12 +92,13 @@ object StreamFormatting { context.getString(R.string.dolby_atmos) } - profile?.contains("DTS:X", true) == true -> { - context.getString(R.string.dts_x) - } - - profile?.contains("DTS:HD", true) == true -> { - context.getString(R.string.dts_hd) + profile?.contains("DTS", true) == true -> { + when { + profile.contains("X", true) -> context.getString(R.string.dts_x) + profile.contains("MA", true) -> context.getString(R.string.dts_hd_ma) + profile.contains("HD", true) -> context.getString(R.string.dts_hd) + else -> context.getString(R.string.dts) + } } else -> { 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 4b30c418..08f9318e 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 @@ -146,17 +146,17 @@ class ApiRequestPager( position: Int, itemId: UUID, ) { - val item = - api.userLibraryApi.getItem(itemId).content.let { - BaseItem.from( - it, - api, - useSeriesForPrimary, - ) - } - val pageNumber = position / pageSize - val index = position - pageNumber * pageSize mutex.withLock { + val item = + api.userLibraryApi.getItem(itemId).content.let { + BaseItem.from( + it, + api, + useSeriesForPrimary, + ) + } + val pageNumber = position / pageSize + val index = position - pageNumber * pageSize val page = cachedPages.getIfPresent(pageNumber) if (page != null && index in page.indices) { page[index] = item diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 00000000..327f969b --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,131 @@ + + + حول + التسجيلات النشطة + إضافة للمفضلة + إضافة خادم + إضافة مستخدم + الصوت + مكان الميلاد + معدل البت + وُلد + إلغاء التسجيل + إلغاء تسجيل المسلسل + إلغاء + الفصول + اختيار %1$s + مسح ذاكرة التخزين المؤقت للصور + مجموعة + المجموعات + تجاري + تقييم المجتمع + حدث خطأ! اضغط على الزر لإرسال السجلات إلى خادمك. + تأكيد + تابع المشاهدة + تقييم النقاد + %.2f ثانية + الافتراضي + حذف + تُوفي + إخراج %1$s + المخرج + مُعطّل + الخوادم المكتشفة + + تنزيل… + مُفعّل + ينتهي عند %1$s + أدخل عنوان IP للخادم أو رابط URL + أدخل عنوان الخادم + الحلقات + خطأ في تحميل المجموعة %1$s + خارجي + المفضلة + الحجم + إجبارية + الأنواع + انتقل إلى المسلسل + انتقل إلى + إخفاء عناصر التحكم في التشغيل + إخفاء معلومات تصحيح الأخطاء + إخفاء + الرئيسية + فوري + المقدمة + #ABCDEFGHIJKLMNOPQRSTUVWXYZ + المكتبة + معلومات الترخيص + تلفزيون مباشر + تحميل… + هل تريد تعيين المسلسل بأكمله كمُشاهَد؟ + هل تريد تعيين المسلسل بأكمله كغير مشاهد؟ + تعيين كغير مشاهد + تعيين كمشاهد + الحد الأقصى لمعدل البت + المزيد من هذا القبيل + المزيد + + الأفلام + + + + + + + الاسم + التالي + لا توجد بيانات + لا توجد نتائج + لم يتم العثور على خوادم + لا توجد تسجيلات مجدولة + لا يوجد تحديث متاح + لا شيء + الترجمات الإجبارية فقط + خاتمة + المسار + + الأشخاص + + + + + + + عدد مرات التشغيل + تشغيل من هنا + تشغيل + إظهار معلومات التشغيل التفصيلية + سرعة التشغيل + التشغيل + قائمة التشغيل + قوائم التشغيل + معاينة + إعدادات ملف تعريف المستخدم + قائمة الانتظار + ملخص ما سبق + أُضيف مؤخرًا في %1$s + أُضيف مؤخرًا + التسجيلات الأخيرة + أحدث الإصدارات + مقترح + تسجيل البرنامج + تسجيل المسلسل + إزالة من المفضلة + إعادة تشغيل + استئناف + حفظ + + بحث + جارٍ البحث… + البحث الصوتي + جارٍ البدء + تحدث للبحث + جارٍ المعالجة + اضغط رجوع للإلغاء + خطأ في تسجيل الصوت + خطأ في التعرف على الصوت + مطلوب إذن الوصول للميكروفون + خطأ في الشبكة + انتهت مهلة الشبكة + لم يتم التعرف على الكلام + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8f32bc3a..e41bd2b5 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -401,6 +401,7 @@ Vyčistit výběr skladeb DTS:X DTS:HD + DTS:HD MA True HD DD DD+ diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 229bc4c8..7fdb5d82 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -397,6 +397,7 @@ Ryd valg af spor DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 013f5ccd..628c465a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -199,12 +199,12 @@ Kursivschrift Laufzeit - Theme Songs - + Titelsong + Titelsongs - Theme Videos - + Titelvideo + Titelvideos Clip @@ -224,11 +224,11 @@ Shorts - + Kurzvideos Trifft nur auf Serien zu - Direct play ASS subtitles - Direct play PGS subtitles + Libass für ASS Untertitel verwenden + Direktwiedergabe PGS Untertitel Immer zu Stereo heruntermischen FFmpeg decoder Modul verwenden Standard Skalierung @@ -251,8 +251,8 @@ Schriftgröße Schriftfarbe Schriftart Deckkraft - Rand Stil - Rand Farbe + Stil der Umrandung + Farbe der Umrandung Hintergrund Deckktraft Hintergrund Stil Untertitel Stil @@ -329,7 +329,7 @@ Login via Server Drücke die mittlere Taste zur Bestätigung Größe des Festplatten-Caches für Bilder (MB) - Besetzung & Mitwirkende + Container Codec Level @@ -365,6 +365,7 @@ In 4K anfragen DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -432,4 +433,122 @@ Videos während der Diashow abspielen Maximale Tage in \"Als nächstes\" Quick Connect + + %s Minute + %s Minuten + + Interlaced + NAL + Gewähre anderen Geräten Zugriff auf deinen Account + Eingabe Quick Connect Code + Keine Beschränkung + Reihe hinzufügen + Genres in %1$s + kürzlich veröffentlicht in %1$s + Höhe + Auf alle Reihen anwenden + Reihe hinzufügen für %1$s + Überschreibe Einstellungen auf dem Server? + Überschreibe lokale Einstellungen? + Vorschläge für %1$s + Sende Media Info Log zum Server + Anpassung Homepage + Für Episoden + Vorlagen um schnell alle Reihen zu gestalten + Wähle Reihen und Bilder auf der Homepage + Starte anschließend + Serien Vorschaubilder + Episoden Vorschaubilder + Einloggen zum Seerr Server + Jellyfin User + Zeige Media Management Optionen + Größe für alle Karten reduzieren + Einstellungen gespeichert + Maximale Altersfreigabe + Keine Begrenzung + Ohne Altersbeschränkung + Ab %s Jahre + Bildschirmschoner + Bildschirmschoner-Einstellungen + Bildschirmschoner starten + Länge + Fotos + Arten einschließen + Für welche Arten von Einträgen sollen Bilder angezeigt werden + Angepasst + Zugeschnitten + Ausgefüllt + Ignorieren + Automatisch überspringen + Vor dem Überspringen nachfragen + FFmpeg nur verwenden, sofern keine anderen Decoder vorhanden sind + FFmpeg statt der vorhandenen Decoder verwenden + FFmpeg Decoder nie verwenden + Am Ende einer Playlist + Während der Credits/Outro + Weiß + Hellgrau + Dunkelgrau + Gelb + Cyan + Magenta + Umrandung + Schatten + MPV bevorzugen + ExoPlayer für die HDR Wiedergabe verwenden + Poster (2:3) + 16:9 + 4:3 + Quadrat (1:1) + Primär + Bild mit dynamischen Farben + API Schlüssel + Lokaler Benutzer + Suche %s + Design-Vorlagen + Animieren + Breite ausfüllen + Höhe ausfüllen + Wholphin Standard + Wholphin Kompakt + Niedrigste + Niedrig + mittel + Hoch + Voll + Lila + Orange + Schwarz + Nur Bild + Aktuell + Eingebauten Bildschirmschoner nutzen + Bist du sicher, dass du diesen Eintrag löschen möchtest? + Schauspieler + Komponist + Autor + Nebenrolle + Produzent + Dirigent + Liedtext-Schreiber + Künstler + %s als Favorit speichern + Startseite - Reihen + Vom Server Benutzerprofil laden + Im Server Benutzerprofil speichern + Vom Webclient laden + Größe für alle Karten erhöhen + Vorschaubilder benutzen + Es wurden keine Untertitel gefunden + Arrangeur + Ingenieur + Dunkelblau + Untertitel-Transparenz + Vorschaubild + Serien Vorschaubilder verwenden + + + Ersteller + Gestreckt + Zentriert + Mischer diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b4ef66da..e228ac4d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -35,7 +35,7 @@ Nombre Sin resultados Ninguno - Outro + Créditos finales Personas Reproducir Reproducción @@ -80,7 +80,7 @@ Crear nueva lista de reproducción Agregar a la lista de reproducción Pausar con un clic - Presiona el centro del D-Pad para pausar/reproducir + Pulsa el botón central de la cruceta para reproducir/pausar Éxito Duración Extras @@ -264,8 +264,8 @@ Retroceder Comportamiento al saltar anuncios Avanzar - Comportamiento al saltar la intro - Comportamiento al saltar el outro + Comportamiento al saltar la cabecera + Comportamiento al saltar créditos finales Comportamiento al saltar avances Comportamiento al saltar resumen Actualización disponible @@ -291,8 +291,8 @@ Saltar anuncios Saltar avance Saltar resumen - Saltar outro - Saltar intro + Saltar créditos finales + Saltar cabecera Reproducido FIltrar Año @@ -401,6 +401,7 @@ Opciones de pista claras DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -485,8 +486,8 @@ Rellenar Ajustar al ancho Ajustar al alto - Miniaturas de las series - Miniaturas de los episodios + Miniatura de la serie + Miniatura del episodio Mínimo Bajo Medio @@ -534,4 +535,38 @@ No se encontraron subtítulos externos Actualidad + + %s minuto + %s minutos + %s minutos + + Comenzar después de + Animación + Clasificación por edad máxima + Para todas las edades + Menores de %s + Salvapantallas + Configuración de salvapantallas + Iniciar salvapantallas + Duración + Fotos + Incluir tipos + Qué tipos de elementos mostrar + Usar salvapantallas de la aplicación + Sin límite + Intérprete + Compositor + Guionista + Estrella invitada + Productor + Director musical + Letrista + Arreglista + Ingeniero de sonido + Ingeniero de mezcla + Creador + Artista + ¿Estás seguro de que quieres eliminar este elemento? + Mostrar opciones de gestión de medios + Buscar %s diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 10b611eb..85a803a8 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -367,6 +367,7 @@ Vaid sundkorras kuvatud subtiitrid DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 538fd523..312caa8e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -221,8 +221,8 @@ Opacité de la police Style des contours Couleur des contours - Opacité de l’arrière-plan - Style de l’arrière-plan + Opacité de l\'arrière-plan + Style de l\'arrière-plan Style des sous-titres Couleur de fond Réinitialiser @@ -234,10 +234,10 @@ Options ExoPlayer Passer le segment Passer les publicités - Passer l’aperçu + Passer l\'aperçu Passer le récapitulatif Passer le générique de fin - Passer l’intro + Passer l\'intro Lu Filtre Année @@ -275,7 +275,7 @@ Lire la musique de thème Revenir en arrière à la reprise de la lecture Comportement de saut des publicités - Comportement de saut de l’intro + Comportement de saut de l\'intro Comportement de saut du générique de fin Comportement de saut des aperçus Comportement de saut du récapitulatif @@ -289,7 +289,7 @@ - + Calendrier du DVR @@ -382,6 +382,7 @@ Pas de bandes-annonces DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -480,4 +481,89 @@ Préréglages intégrés pour styliser rapidement toutes les lignes Choisissez des lignes et des images sur la page d\'accueil Favori %s + + %s minute + %s minutes + %s minute + + Démarrer après + Animer + Classification par âge maximale + Pas de maximum + Pour tous les âges + Jusqu\'à l\'âge de %s + Écran de veille + Paramètres de l\'économiseur d\'écran + Lancer l\'économiseur d\'écran + Durée + Photos + Inclure les types + Quels types d\'éléments afficher des images pour + Ajustement + Recadrer + Remplir + Largeur de remplissage + Hauteur de remplissage + Wholphin Par défaut + Wholphin Compact + Images miniatures de la série + Images miniatures de l\'épisode + Le plus bas + Faible + Moyen + Haute + Complet + Violet + Orange + Bleu gras + Noir + Ignorer + Passer automatiquement + Demander à passer + N\'utilisez FFmpeg que si aucun décodeur intégré n\'existe + Préférez utiliser FFmpeg plutôt que les décodeurs intégrés + N\'utilisez jamais les décodeurs FFmpeg + À la fin de la lecture + Pendant le générique de fin/outro + Blanc + Gris clair + Gris foncé + Jaune + Cyan + Magenta + Contour + Ombre + Envelopper + Encadré + Préférer MPV + Utiliser ExoPlayer pour la lecture HDR + Affiche (2:3) + 16:9 + 4:3 + Carré (1:1) + Primaire + Miniature + Image avec couleur dynamique + Image uniquement + Entrez l\'URL et la clé API + Clé API + Se connecter au serveur Seerr + Utilisateur Jellyfin + Utilisateur local + Rechercher et télécharger des sous-titres + Aucun sous-titre distant n\'a été trouvé + Présent + Utiliser l\'économiseur d\'écran intégré + Acteur + Compositeur + Écrivain + Invité spécial + Producteur + Conducteur + Parolier + Arrangeur + Ingénieur + Mélangeur + Créateur + Artiste diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 356732cd..dc94ff96 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -242,8 +242,8 @@ Ukuran font Warna font Transparansi font - Jenis garis tepi - Warna garis tepi + Jenis pinggiran + Warna pinggiran Transparansi latar belakang Gaya latar belakang Gaya subtitel @@ -318,7 +318,7 @@ Membutuhkan PIN untuk profil PIN akan dihapus Opsi tampilan - Ukuran garis tepi + Ukuran pinggiran Otomatis Gunakan nama pengguna/kata sandi Batas tepi @@ -369,6 +369,7 @@ Hapus pilihan trek DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -446,4 +447,79 @@ Preset bawaan untuk kustomisasi cepat semua baris Pilih baris dan gambar di halaman beranda Favoritkan %s + Artis + + %s menit + + Mulai setelah + Animasi + Rating usia maks + Tidak ada maks + Untuk semua umur + Maks. usia %s + Screensaver + Pengaturan screensaver + Mulai screensaver + Durasi + Foto + Sertakan jenis + Tampilkan gambar untuk jenis item + Wholphin Bawaan + Wholphin Ringkas + Gambar Thumbnail Serial + Gambar Thumbnail Episode + Terendah + Rendah + Sedang + Tinggi + Penuh + Ungu + Oranye + Biru Tegas + Hitam + Abaikan + Lewati otomatis + Tanya sebelum melewati + Gunakan FFmpeg hanya jika dekoder bawaan tidak ada + Utamakan FFmpeg daripada dekoder bawaan + Jangan pernah gunakan dekoder FFmpeg + Di akhir pemutaran + Selama kredit akhir/outro + Putih + Abu-abu muda + Abu-abu tua + Kuning + Sian + Magenta + Garis luar + Utamakan MPV + Gunakan ExoPlayer untuk pemutaran HDR + Poster (2:3) + 16:9 + 4:3 + Persegi (1:1) + Utama + Thumbnail + Gambar dengan warna dinamis + Hanya gambar + + API Key + Masuk ke server Seerr + Pengguna Jellyfin + Pengguna lokal + + Subtitel remote tidak ditemukan + Tayang + Gunakan screensaver dalam aplikasi + Aktor + Komposer + Penulis + Bintang tamu + Produser + Konduktor + Penulis lirik + Aransemen + Teknisi + Mixer + Kreator diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a3764b96..596d898f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -383,6 +383,7 @@ Solo sottotitoli forzati DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -480,4 +481,92 @@ Usa immagine delle serie Scegli righe e immagini nella home page Preferito %s + Adatta + Ritaglia + Riempi + Riempi larghezza + Riempi altezza + Wholphin predefinito + Wholphin Compatto + Immagini miniatura serie + Immagini miniatura episodio + Minimo + Basso + Medio + Alto + Massimo + Viola + Arancione + Blu intenso + Nero + Ignora + Salta automaticamente + Chiedi se saltare + Usa FFmpeg solo se non esiste un decodificatore integrato + Preferisci FFmpeg ai decoder integrati + Non usare mai i decoder FFmpeg + Alla fine della riproduzione + Durante i titoli di coda + Bianco + Grigio chiaro + Grigio scuro + Giallo + Ciano + Magenta + Contorno + Ombra + Avvolgi + Con riquadro + Preferisci MVP + Usa ExoPlayer per la riproduzione HDR + Copertina (2:3) + 16:9 + 4:3 + Quadrato (1:1) + Principale + Miniatura + Immagine con colore dinamico + Solo immagine + + Chiave API + Accedi al server Seerr + Utente Jellyfin + Utente locale + + Nessun sottotitolo remoto trovato + In corso + + %s minuto + %s minuti + %s minuti + + Inizia dopo + Anima + Età massima + Per tutte le età + Fino a %s anni + Salvaschermo + Impostazioni salvaschermo + Avvia salvaschermo + Durata + Foto + Includi tipi + Quali tipi di elementi mostrare nelle immagini + Nessun massimo + Usa salvaschermo dell\'app + Compositore + Sceneggiatore + Attore + Produttore + Ospite speciale + Direttore d’orchestra + Paroliere + Arrangiatore + Ingegnere del suono + Tecnico di mixaggio + Artista + Creatore + Cerca %s + Sei sicuro di voler cancellare questo elemento? + Mostra opzioni di gestione dei media diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 76bf308f..94da1f4d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -65,8 +65,8 @@ Mais Filmes - - + + Nome A Seguir @@ -81,8 +81,8 @@ Caminho Pessoas - - + + Contagem de Reproduções Reproduzir a partir daqui @@ -152,8 +152,8 @@ Trailer Trailers - - + + Agenda do DVR Guia @@ -161,8 +161,8 @@ Temporadas Programas de TV - - + + Interface Desconhecido @@ -190,53 +190,53 @@ Extras Outro - - + + Por Trás das Cenas - - + + Músicas Tema - - + + Vídeos Tema - - + + Clipes - - + + Cenas Deletadas - - + + Entrevistas - - + + Cenas - - + + Amostras - - + + Curtas - - + + Favoritar %s @@ -405,6 +405,7 @@ Limpar escolhas de faixas DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 1fb5fbb5..b4f44186 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -121,7 +121,7 @@ Mais Votados Não Assistidos Trailer - Trailers + Trailer Trailers Trailers @@ -382,6 +382,7 @@ Apagar seleções de faixas DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -463,8 +464,8 @@ Aplicar a todas as linhas Personalizar página inicial Linhas da página inicial - Carregar do servidor - Gravar para o servidor + Carregar do perfil de utilizador do servidor + Guardar no perfil de utilizador do servidor Carregar a partir do cliente web Usar imagem da série Adicionar linha para %1$s @@ -479,4 +480,90 @@ Predefinições embarcadas para definir todas as linhas rapidamente Escolha as linhas e imagens na página inicial Diminuir o tamanho de todos os cartões + Favoritos %s + + %s minuto + %s minutos + %s minutos + + Começar depois + Animado + Idade máxima + Sem máximo + Para todas as idades + Até a idade %s + Proteção de ecrã + Definições de protecção de ecrã + Iniciar protecção de ecrã + Duração + Fotos + Incluir tipos + Que tipos de itens devem mostrar imagens + Ajustar + Recortar + Preencher + Preencher largura + Preencher altura + Padrão do Wholphin + Wholphin Compacto + Imagens em miniatura da série + Imagens em miniatura do episódio + Mínimo + Baixo + Médio + Alto + Máximo + Roxo + Laranja + Azul intenso + Preto + Ignorar + Saltar automaticamente + Pedir para saltar + Usa FFmpeg apenas se não houver um descodificador integrado + Prefere o FFmpeg aos descodificadores integrados + Nunca usar os descodificadores FFmpeg + No final da reprodução + Durante os créditos finais + Branco + Cinzento claro + Cinzento escuro + Amarelo + Ciano + Magenta + Contorno + Sombra + Envolver + Com caixa + Preferir MPV + Usar ExoPlayer para reprodução HDR + Poster (2:3) + 16:9 + 4:3 + Quadrado (1:1) + Primária + Miniatura + Imagem com cor dinâmica + Apenas imagem + + Chave API + Iniciar sessão no servidor Seerr + Utilizador Jellyfin + Utilizador local + + Não foram encontradas legendas remotas + Em curso + Usar protetor de ecrã da aplicação + Ator + Compositor + Argumentista + Convidado especial + Produtor + Director musicar + Letrista + Arranjador + Engenheiro de som + Técnico de mixagem + Criador + Artista diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 195565df..dfb498ba 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -146,9 +146,9 @@ Trailer Trailery - - - + + + DVR rozvrh Sprievodca @@ -156,9 +156,9 @@ Série Seriály - - - + + + Rozhranie Neznámy @@ -186,69 +186,69 @@ Bonusový materiál Ostatné - - - + + + Zo zákulisia - - - + + + Tématické piesne - - - + + + Tématické videá - - - + + + Klipy - - - + + + Vymazané scény - - - + + + Rozhovory - - - + + + Scény - - - + + + Vzorky - - - + + + Krátkometrážne filmy - - - + + + Krátke videá - - - + + + Obľúbené %s diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index b7d13882..cef8bb90 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -277,4 +277,49 @@ Skriv in PIN Logga in automatiskt Logga in via server + Ange server-adress + Nätverksfel + Okänt fel + + %s minut + %s minuter + + Fet stil + Dolby Atmos + Avbryt ändringar? + Kräv PIN för profilen + Upplösning + Språk + Ja + Nej + Upprepa + Lösenord + Användarnamn + Skapare + Ingenjör + Skådespelare + Kompositör + Författare + Producent + Endast bild + Vit + Ljusgrå + Mörkgrå + Gul + Lila + Svart + För alla åldrar + Upp till %s år + Skärmsläckare + Inställningar Skärmsläckare + Starta skärmsläckare + Bilder + Inställningar sparade + Rotera vänster + Rotera höger + Zooma in + Zooma ut + Ingen gräns + Lägg till rad + Tryck bakåt för att avbryta diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index aec35e54..4e4c1838 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -256,7 +256,7 @@ Güncellemeleri kontrol et Yalnızca diziler için geçerlidir - ASS altyazıları doğrudan oynat + ASS altyazıları için libass kullan PGS altyazıları doğrudan oynat Her zaman Stereo\'ya dönüştür FFmpeg kod çözücü kullan @@ -394,6 +394,7 @@ Parça seçimlerini temizle DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -472,4 +473,91 @@ Tüm satırları hızlıca stilize etmek için hazır ayarlar Favori %s Ana sayfadaki satırları ve görselleri seçin + Sığdır + Kırp + Doldur + Genişliği Doldur + Yüksekliği Doldur + Wholphin Varsayılan + Wholphin Kompakt + Dizi Küçük Resimleri + Bölüm Küçük Resimleri + En Düşük + Düşük + Orta + Yüksek + Tam + Mor + Turuncu + Koyu Mavi + Siyah + Yoksay + Otomatik Atla + Atlamak için sor + Dahili çözücü yoksa FFmpeg kullan + Dahili çözücüler yerine FFmpeg\'i tercih et + FFmpeg çözücülerini asla kullanma + Oynatma sonunda + Kapanış bölümünde / Jenerik sırasında + Beyaz + Açık Gri + Koyu Gri + Sarı + Camgöbeği + Macenta + Kenarlık + Gölge + Kapla + Kutu içinde + MPV\'yi tercih et + HDR oynatma için ExoPlayer kullan + Poster (2:3) + 16:9 + 4:3 + Kare (1:1) + Birincil + Küçük Resim + Dinamik renkli görsel + Sadece görsel + + API Anahtarı + Seerr sunucusuna giriş yap + Jellyfin kullanıcısı + Yerel kullanıcı + + Uzak altyazı bulunamadı + Mevcut + + %s dakika + %s dakika + + Başlama süresi + Oynatma Süresi + Maksimum yaş sınırı + Sınır yok + Genel İzleyici + %s yaşa kadar + Ekran koruyucu + Ekran koruyucu ayarları + Ekran koruyucuyu başlat + Süre + Fotoğraflar + İçerik türleri + Görsel gösterilecek öğe türleri + Uygulama içi ekran koruyucuyu kullan + Aktör + Besteci + Yazar + Konuk sanatçı + Yapımcı + Orkestra şefi + Söz yazarı + Aranjör + Mühendis + Karıştırıcı + Yaratıcı + Sanatçı + Bu öğeyi silmek istediğinizden emin misiniz? + Medya yönetim seçeneklerini göster + Ara %s diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1a31690d..f7835121 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -432,6 +432,7 @@ Чіткий вибір доріжки DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 28875ea9..b78e3322 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -150,7 +150,7 @@ 字体 背景 成功 - 年龄分级 + 内容分级 播放时长 附加内容 @@ -208,7 +208,7 @@ 检查更新 仅适用于电视剧 - 直接播放 ASS 字幕 + 使用 libass 处理 ASS 字幕 直接播放 PGS 字幕 始终降混为立体声 使用 FFmpeg 解码模块 @@ -351,6 +351,7 @@ 仅强制字幕 DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -446,4 +447,90 @@ 可快速设置所有行样式的内置预设 选择首页上的行和图片 收藏的 %s + 适应 + 裁剪 + 填充 + 填充宽度 + 填充高度 + Wholphin 默认 + Wholphin 紧凑 + 剧集缩略图 + 分集缩略图 + 紫色 + 橙色 + 深蓝色 + 黑色 + 忽略 + 自动跳过 + 询问后跳过 + 仅在无内置解码器时使用 FFmpeg + 优先使用 FFmpeg 解码器而非内置解码器 + 从不使用 FFmpeg 解码器 + 播放结束时 + 播到片尾时 + 白色 + 浅灰色 + 深灰色 + 黄色 + 青色 + 紫红色 + 轮廓 + 阴影 + 优先使用 mpv + HDR 播放使用 ExoPlayer + 海报 (2:3) + 16:9 + 4:3 + 正方形 (1:1) + 主图 + 缩略图 + 动态配色图片 + 仅图片 + + API 密钥 + 登录 Seerr 服务器 + Jellyfin 用户 + 本地用户 + + 未找到远程字幕 + 最低音量 + 低音量 + 中等音量 + 高音量 + 最大音量 + 环绕式 + 框式 + 当前 + + %s 分钟 + + 动画 + 启动时间 + 最高年龄分级 + 无上限 + 所有年龄 + 最高至 %s 岁 + 屏幕保护程序 + 屏幕保护程序设置 + 启动屏幕保护程序 + 时长 + 照片 + 包含类型 + 要显示图片的项目类型 + 使用应用内屏幕保护程序 + 演员 + 作曲 + 编剧 + 客串 + 制片 + 指挥 + 作词 + 编曲 + 录音 + 混音 + 主创 + 艺人 + 显示媒体管理选项 + 是否确定要删除此项目? + 搜索 %s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 6670d10e..c96c987f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -59,7 +59,7 @@ 指南 更新 版本 - 畫面比例 + 畫面縮放比例 影片 影片 觀看直播 @@ -104,7 +104,7 @@ 直接播放 ASS 字幕 直接播放 PGS 字幕 使用 FFmpeg 解碼模組 - 預設畫面比例 + 預設縮放比例 安裝更新 安裝版本 首頁每列項目上限 @@ -349,6 +349,7 @@ 無預告片 DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -362,7 +363,7 @@ 移除 Seerr 伺服器 已新增 Seerr 伺服器 密碼 - 使用者名稱 + 帳號 URL 當前趨勢 即將上映的電影 @@ -427,18 +428,18 @@ 推薦的 %1$s 放大所有海報尺寸 縮小所有海報尺寸 - 使用橫向縮圖 + 使用橫式縮圖 新增一列 套用到所有列 首頁各列 為 %1$s 新增列項目 快速設定所有項目為內建預設樣式 - 選擇首頁要顯示的列項目及圖片樣式 + 選擇首頁要顯示的項目及圖片樣式 高度 自訂首頁 從伺服器載入設定 保存設定到伺服器 - 從網頁版載入 + 從網頁版載入設定 使用劇集圖片 覆蓋伺服器上的設定? 覆蓋本地設定? @@ -450,11 +451,11 @@ 拉伸填滿 填滿寬度 填滿高度 - 最低 - - 中等 - - 最大 + 最低音量 + 低音量 + 中等音量 + 高音量 + 最大音量 紫色 橘色 深藍色 @@ -472,10 +473,64 @@ 深灰色 黃色 青色 - 洋紅色 + 桃紅色 外框 陰影 貼合文字 長方底框 優先使用 MPV + 僅 HDR 使用 ExoPlayer 播放 + 我的最愛的 %s + Wholphin 預設樣式 + Wholphin 緊湊樣式 + 劇集縮圖樣式 + 單集縮圖樣式 + 海報 (2:3) + 16:9 + 4:3 + 正方形 (1:1) + 主封面 + 橫式縮圖 + 動態色彩背景 + 僅背景圖 + + API 金鑰 + 登入 Seerr 伺服器 + Jellyfin 帳號 + Seerr 帳號 + + 未找到遠端字幕 + 至今 + + %s 分鐘 + + 啟動時間 + 動態效果 + 年齡分級上限 + 不限制 + 適合所有年齡 + 最高至 %s 歲 + 螢幕保護程式 + 螢幕保護程式設定 + 立即開始 + 切換間隔 + 照片 + 包含類型 + 要顯示圖片的類型 + 使用內建螢幕保護程式 + 客串演員 + 演員 + 作曲 + 編劇 + 製作人 + 指揮 + 作詞 + 編曲 + 錄音師 + 混音師 + 創作者 + 演出者 + 確定要刪除此項目嗎? + 顯示媒體管理選項 + 搜尋 %s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5136c83a..8dda4f85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -451,6 +451,7 @@ Clear track choices DTS:X DTS:HD + DTS:HD MA TrueHD DD DD+ @@ -719,5 +720,6 @@ Creator Artist Search %s + Select all diff --git a/app/src/main/seerr/seerr-api.yml b/app/src/main/seerr/seerr-api.yml index d619d042..4c21b699 100644 --- a/app/src/main/seerr/seerr-api.yml +++ b/app/src/main/seerr/seerr-api.yml @@ -711,6 +711,8 @@ components: type: boolean series4kEnabled: type: boolean + cacheImages: + type: boolean MovieResult: type: object required: @@ -1086,6 +1088,11 @@ components: type: array items: $ref: '#/components/schemas/Episode' + status: + type: integer + example: 0 + description: Status of the request. 1 = PENDING APPROVAL, 2 = APPROVED, 3 = DECLINED + readOnly: true TvDetails: type: object properties: @@ -1268,6 +1275,10 @@ components: type: string type: type: string + seasons: + type: array + items: + $ref: '#/components/schemas/Season' required: - id - status @@ -6135,16 +6146,10 @@ paths: type: integer example: 123 seasons: - # oneOf: - # - type: array - # items: - # type: integer - # minimum: 0 - # - type: string - # enum: [all] - # TODO - type: string - enum: [all] + type: array + items: + type: integer + minimum: 0 nullable: true is4k: type: boolean diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt index a4acf93f..c19ffb81 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestSeerr.kt @@ -1,7 +1,9 @@ package com.github.damontecres.wholphin.test +import com.github.damontecres.wholphin.ui.setup.seerr.createSeerrApiUrl import com.github.damontecres.wholphin.ui.setup.seerr.createUrls -import org.junit.Assert +import com.github.damontecres.wholphin.ui.setup.seerr.migrateSeerrUrl +import org.junit.Assert.assertEquals import org.junit.Test class TestSeerr { @@ -13,12 +15,12 @@ class TestSeerr { val expected = listOf( - "http://jellyseerr.com/api/v1", - "https://jellyseerr.com/api/v1", - "http://jellyseerr.com:5055/api/v1", - "https://jellyseerr.com:5055/api/v1", + "http://jellyseerr.com/", + "https://jellyseerr.com/", + "http://jellyseerr.com:5055/", + "https://jellyseerr.com:5055/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -29,10 +31,10 @@ class TestSeerr { val expected = listOf( - "https://jellyseerr.com/api/v1", - "https://jellyseerr.com:5055/api/v1", + "https://jellyseerr.com/", + "https://jellyseerr.com:5055/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -43,10 +45,10 @@ class TestSeerr { val expected = listOf( - "http://jellyseerr.com/api/v1", - "http://jellyseerr.com:5055/api/v1", + "http://jellyseerr.com/", + "http://jellyseerr.com:5055/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -57,10 +59,10 @@ class TestSeerr { val expected = listOf( - "http://jellyseerr.com:5055/api/v1", - "https://jellyseerr.com:5055/api/v1", + "http://jellyseerr.com:5055/", + "https://jellyseerr.com:5055/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -71,10 +73,10 @@ class TestSeerr { val expected = listOf( - "http://10.0.0.2:443/api/v1", - "https://10.0.0.2/api/v1", + "http://10.0.0.2:443/", + "https://10.0.0.2/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) } @Test @@ -85,9 +87,87 @@ class TestSeerr { val expected = listOf( - "http://10.0.0.2:8080/api/v1", - "https://10.0.0.2:8080/api/v1", + "http://10.0.0.2:8080/", + "https://10.0.0.2:8080/", ) - Assert.assertEquals(expected, urls) + assertEquals(expected, urls) + } + + @Test + fun testCreateUrls7() { + val urls = + createUrls("http://10.0.0.2:80") + .map { it.toString() } + + val expected = + listOf( + "http://10.0.0.2/", + "http://10.0.0.2:5055/", + ) + assertEquals(expected, urls) + } + + @Test + fun testCreateUrls8() { + val urls = + createUrls("https://10.0.0.2:443") + .map { it.toString() } + + val expected = + listOf( + "https://10.0.0.2/", + "https://10.0.0.2:5055/", + ) + assertEquals(expected, urls) + } + + @Test + fun `Test createUrls for path`() { + val urls = + createUrls("https://jellyseerr.com/seerr/") + .map { it.toString() } + + val expected = + listOf( + "https://jellyseerr.com/seerr/", + "https://jellyseerr.com:5055/seerr/", + ) + assertEquals(expected, urls) + } + + @Test + fun `Test build api url`() { + var url = "https://jellyseerr.com/" + assertEquals("https://jellyseerr.com/api/v1", createSeerrApiUrl(url)) + + url = "https://jellyseerr.com/path" + assertEquals("https://jellyseerr.com/path/api/v1", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com:5055/" + assertEquals("http://jellyseerr.com:5055/api/v1", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com:7878/path/" + assertEquals("http://jellyseerr.com:7878/path/api/v1", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com/api/v1" + assertEquals("http://jellyseerr.com/api/v1", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com/api/v1/" + assertEquals("http://jellyseerr.com/api/v1/", createSeerrApiUrl(url)) + + url = "http://jellyseerr.com/path/api/v1" + assertEquals("http://jellyseerr.com/path/api/v1", createSeerrApiUrl(url)) + } + + @Test + fun `Test migration`() { + assertEquals("http://10.0.0.2/", migrateSeerrUrl("http://10.0.0.2/api/v1")) + assertEquals("https://10.0.0.2/", migrateSeerrUrl("https://10.0.0.2/api/v1")) + assertEquals("http://10.0.0.2:5055/", migrateSeerrUrl("http://10.0.0.2:5055/api/v1")) + assertEquals("http://10.0.0.2/", migrateSeerrUrl("http://10.0.0.2/api/v1/")) + assertEquals("http://10.0.0.2/path/", migrateSeerrUrl("http://10.0.0.2/path/api/v1")) + assertEquals("http://10.0.0.2/api/v1/", migrateSeerrUrl("http://10.0.0.2/api/v1/api/v1")) + assertEquals("http://10.0.0.2/", migrateSeerrUrl("http://10.0.0.2/")) + assertEquals("http://10.0.0.2/", migrateSeerrUrl("http://10.0.0.2")) } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt new file mode 100644 index 00000000..ca8ef324 --- /dev/null +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt @@ -0,0 +1,56 @@ +package com.github.damontecres.wholphin.test + +import com.github.damontecres.wholphin.services.getDownloadUrl +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.nio.file.Paths +import kotlin.io.path.readText + +class TestUpdateChecker { + lateinit var releaseJson: JsonObject + val assetsJson: JsonArray by lazy { releaseJson["assets"]!!.jsonArray } + + @Before + fun setup() { + val resource = javaClass.classLoader?.getResource("release_develop.json") + Assert.assertNotNull(resource) + val fileContents = Paths.get(resource!!.toURI()).readText() + releaseJson = Json.parseToJsonElement(fileContents).jsonObject + } + + @Test + fun `Release chooses release`() { + val url = getDownloadUrl(assetsJson, false, listOf()) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk", url) + } + + @Test + fun `Choose abi`() { + val url = getDownloadUrl(assetsJson, false, listOf("arm64-v8a")) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-arm64-v8a.apk", url) + } + + @Test + fun `Choose unknown abi`() { + val url = getDownloadUrl(assetsJson, false, listOf("unknown")) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk", url) + } + + @Test + fun `Debug chooses debug`() { + val url = getDownloadUrl(assetsJson, true, listOf()) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug.apk", url) + } + + @Test + fun `Choose debug abi`() { + val url = getDownloadUrl(assetsJson, true, listOf("arm64-v8a")) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-arm64-v8a.apk", url) + } +} diff --git a/app/src/test/resources/release_develop.json b/app/src/test/resources/release_develop.json new file mode 100644 index 00000000..df48106f --- /dev/null +++ b/app/src/test/resources/release_develop.json @@ -0,0 +1,475 @@ +{ + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/297064448", + "assets_url": "https://api.github.com/repos/damontecres/Wholphin/releases/297064448/assets", + "upload_url": "https://uploads.github.com/repos/damontecres/Wholphin/releases/297064448/assets{?name,label}", + "html_url": "https://github.com/damontecres/Wholphin/releases/tag/develop", + "id": 297064448, + "author": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "node_id": "RE_kwDOPzwRps4RtNgA", + "tag_name": "develop", + "target_commitish": "main", + "name": "v0.5.3-8-g627b89f1", + "draft": false, + "immutable": false, + "prerelease": true, + "created_at": "2026-03-14T20:47:08Z", + "updated_at": "2026-03-14T20:54:31Z", + "published_at": "2026-03-14T20:54:31Z", + "assets": [ + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937404", + "id": 373937404, + "node_id": "RA_kwDOPzwRps4WSdT8", + "name": "Wholphin-debug-0.5.3-8-g627b89f1-44-arm64-v8a.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 65936692, + "digest": "sha256:ede68f14374dea27a92063d1aab765265cf5386c93533b01e7c666c4b8a6ea99", + "download_count": 0, + "created_at": "2026-03-14T20:54:23Z", + "updated_at": "2026-03-14T20:54:26Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-0.5.3-8-g627b89f1-44-arm64-v8a.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937407", + "id": 373937407, + "node_id": "RA_kwDOPzwRps4WSdT_", + "name": "Wholphin-debug-0.5.3-8-g627b89f1-44-armeabi-v7a.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 62726455, + "digest": "sha256:c2c6d0443af1302a8aa8a70426605e6ca67345b98a74b5b1d22b85e7de9012c2", + "download_count": 0, + "created_at": "2026-03-14T20:54:24Z", + "updated_at": "2026-03-14T20:54:25Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-0.5.3-8-g627b89f1-44-armeabi-v7a.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937406", + "id": 373937406, + "node_id": "RA_kwDOPzwRps4WSdT-", + "name": "Wholphin-debug-0.5.3-8-g627b89f1-44.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 99552740, + "digest": "sha256:d0f5bbbe0c6f6bc31f681d70d60a5730dd0dbc83f28e66922fde27f4e60cda05", + "download_count": 0, + "created_at": "2026-03-14T20:54:24Z", + "updated_at": "2026-03-14T20:54:26Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-0.5.3-8-g627b89f1-44.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937405", + "id": 373937405, + "node_id": "RA_kwDOPzwRps4WSdT9", + "name": "Wholphin-debug-arm64-v8a.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 65936692, + "digest": "sha256:ede68f14374dea27a92063d1aab765265cf5386c93533b01e7c666c4b8a6ea99", + "download_count": 0, + "created_at": "2026-03-14T20:54:23Z", + "updated_at": "2026-03-14T20:54:26Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-arm64-v8a.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937408", + "id": 373937408, + "node_id": "RA_kwDOPzwRps4WSdUA", + "name": "Wholphin-debug-armeabi-v7a.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 62726455, + "digest": "sha256:c2c6d0443af1302a8aa8a70426605e6ca67345b98a74b5b1d22b85e7de9012c2", + "download_count": 0, + "created_at": "2026-03-14T20:54:24Z", + "updated_at": "2026-03-14T20:54:26Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-armeabi-v7a.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937422", + "id": 373937422, + "node_id": "RA_kwDOPzwRps4WSdUO", + "name": "Wholphin-debug.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 99552740, + "digest": "sha256:d0f5bbbe0c6f6bc31f681d70d60a5730dd0dbc83f28e66922fde27f4e60cda05", + "download_count": 0, + "created_at": "2026-03-14T20:54:26Z", + "updated_at": "2026-03-14T20:54:29Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937424", + "id": 373937424, + "node_id": "RA_kwDOPzwRps4WSdUQ", + "name": "Wholphin-release-0.5.3-8-g627b89f1-44-arm64-v8a.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 56651710, + "digest": "sha256:bf010482a8b8626fb93bbb3285667b22a41c3548286d3319bda92c71eedba672", + "download_count": 0, + "created_at": "2026-03-14T20:54:26Z", + "updated_at": "2026-03-14T20:54:28Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-0.5.3-8-g627b89f1-44-arm64-v8a.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937427", + "id": 373937427, + "node_id": "RA_kwDOPzwRps4WSdUT", + "name": "Wholphin-release-0.5.3-8-g627b89f1-44-armeabi-v7a.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 53441542, + "digest": "sha256:5b8aee8f79fd5ae26dbd5de4a1b4d38b90bc6b35cd2e7fa5f2683909463da390", + "download_count": 0, + "created_at": "2026-03-14T20:54:26Z", + "updated_at": "2026-03-14T20:54:28Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-0.5.3-8-g627b89f1-44-armeabi-v7a.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937431", + "id": 373937431, + "node_id": "RA_kwDOPzwRps4WSdUX", + "name": "Wholphin-release-0.5.3-8-g627b89f1-44.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 90267831, + "digest": "sha256:bc10956a072a5a07c372a67ee8732171470529b468fd7d85a277a738604c34c5", + "download_count": 0, + "created_at": "2026-03-14T20:54:27Z", + "updated_at": "2026-03-14T20:54:29Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-0.5.3-8-g627b89f1-44.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937439", + "id": 373937439, + "node_id": "RA_kwDOPzwRps4WSdUf", + "name": "Wholphin-release-arm64-v8a.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 56651710, + "digest": "sha256:bf010482a8b8626fb93bbb3285667b22a41c3548286d3319bda92c71eedba672", + "download_count": 1, + "created_at": "2026-03-14T20:54:27Z", + "updated_at": "2026-03-14T20:54:29Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-arm64-v8a.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937452", + "id": 373937452, + "node_id": "RA_kwDOPzwRps4WSdUs", + "name": "Wholphin-release-armeabi-v7a.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 53441542, + "digest": "sha256:5b8aee8f79fd5ae26dbd5de4a1b4d38b90bc6b35cd2e7fa5f2683909463da390", + "download_count": 0, + "created_at": "2026-03-14T20:54:28Z", + "updated_at": "2026-03-14T20:54:30Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-armeabi-v7a.apk" + }, + { + "url": "https://api.github.com/repos/damontecres/Wholphin/releases/assets/373937453", + "id": 373937453, + "node_id": "RA_kwDOPzwRps4WSdUt", + "name": "Wholphin-release.apk", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/vnd.android.package-archive", + "state": "uploaded", + "size": 90267831, + "digest": "sha256:bc10956a072a5a07c372a67ee8732171470529b468fd7d85a277a738604c34c5", + "download_count": 1, + "created_at": "2026-03-14T20:54:28Z", + "updated_at": "2026-03-14T20:54:31Z", + "browser_download_url": "https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk" + } + ], + "tarball_url": "https://api.github.com/repos/damontecres/Wholphin/tarball/develop", + "zipball_url": "https://api.github.com/repos/damontecres/Wholphin/zipball/develop", + "body": "### `main` development build\n\nThis pre-release tracks the development build of Wholphin from the `main` unstable branch.\n\nSee https://github.com/damontecres/Wholphin/releases/latest for the latest stable release.\n\nIf you want to update to this version in-app, change Settings->Advanced->Update URL to https://api.github.com/repos/damontecres/Wholphin/releases/tags/develop (replace `latest` with `tags/develop`).\n\n[See changes since `v0.5.3`](https://github.com/damontecres/Wholphin/compare/v0.5.3...develop)\n" +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a4de2268..e6470883 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,11 +8,11 @@ desugar_jdk_libs = "2.1.5" hiltNavigationCompose = "1.3.0" hiltWork = "1.3.0" kache = "2.1.1" -kotlin = "2.3.10" +kotlin = "2.3.20" ksp = "2.3.6" -coreKtx = "1.17.0" +coreKtx = "1.18.0" appcompat = "1.7.1" -composeBom = "2026.02.01" +composeBom = "2026.03.00" mockk = "1.14.9" robolectric = "4.16.1" multiplatformMarkdownRenderer = "0.39.2" @@ -20,18 +20,18 @@ okhttpBom = "5.3.2" programguide = "1.6.0" slf4j2Timber = "1.2" timber = "5.0.1" -tvFoundation = "1.0.0-alpha12" +tvFoundation = "1.0.0-beta01" tvMaterial = "1.0.1" lifecycleRuntimeKtx = "2.10.0" -activityCompose = "1.12.4" -androidx-media3 = "1.9.2" +activityCompose = "1.13.0" +androidx-media3 = "1.9.3" coil = "3.4.0" jellyfin-sdk = "1.7.1" nav3Core = "1.0.1" lifecycleViewmodelNav3 = "2.10.0" material3AdaptiveNav3 = "1.0.0-alpha03" protobuf = "0.9.6" -datastore = "1.2.0" +datastore = "1.2.1" kotlinx-serialization = "1.10.0" protobuf-javalite = "4.34.0" hilt = "2.59.2" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 61285a65..d997cfc6 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f78a6a..dbc3ce4a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index adff685a..0262dcbd 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/scripts/mpv/get_dependencies.sh b/scripts/mpv/get_dependencies.sh index 7ca18de8..74bff6d6 100755 --- a/scripts/mpv/get_dependencies.sh +++ b/scripts/mpv/get_dependencies.sh @@ -30,7 +30,11 @@ clone "https://gitlab.freedesktop.org/freetype/freetype.git" "VER-2-14-1" freety clone "https://github.com/libass/libass" "0.17.4" libass -clone "https://github.com/haasn/libplacebo" "v7.351.0" libplacebo --recurse-submodules +rm -rf libplacebo +git clone "https://github.com/haasn/libplacebo" --single-branch -b "master" --recurse-submodules libplacebo +pushd libplacebo || exit +git checkout 1bd8d2d6a715bc870bdffa6759e62c419000dd51 +popd || exit clone "https://github.com/mpv-player/mpv" "v0.41.0" mpv