Merge branch 'main' into fea/libass-android-support

This commit is contained in:
Damontecres 2026-03-17 13:26:47 -04:00
commit a126869a09
No known key found for this signature in database
69 changed files with 3079 additions and 870 deletions

View file

@ -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

View file

@ -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),
)
}
}
}
}

View file

@ -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<AppPreferences>
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<UserPreferences?>(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<AppPreferences?>(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(),
)
}
}
}
}

View file

@ -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(

View file

@ -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<AppPreferences>,
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<AppPreferences>,
) {
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<AppPreferences>,
) {
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)),
)
}
}
}
}
}

View file

@ -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()

View file

@ -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)
}
}

View file

@ -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<SeerrUser?> =
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.user }
val currentUserId: Flow<Int?> = 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"
}

View file

@ -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<DiscoverItem> =
api.searchApi
.discoverMoviesGet(page = page)
.results
?.map(::DiscoverItem)
?.map { createDiscoverItem(it) }
.orEmpty()
suspend fun trending(page: Int = 1): List<DiscoverItem> =
api.searchApi
.discoverTrendingGet(page = page)
.results
?.map(::DiscoverItem)
?.map { createDiscoverItem(it) }
.orEmpty()
suspend fun upcomingMovies(page: Int = 1): List<DiscoverItem> =
api.searchApi
.discoverMoviesUpcomingGet(page = page)
.results
?.map(::DiscoverItem)
?.map { createDiscoverItem(it) }
.orEmpty()
suspend fun upcomingTv(page: Int = 1): List<DiscoverItem> =
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(),
)
}

View file

@ -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<String> = 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
}

View file

@ -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)
}
}

View file

@ -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),
)
}
}

View file

@ -771,6 +771,9 @@ fun CollectionFolderGrid(
onClickDelete = {
showDeleteDialog = PositionItem(position, item)
},
onClickGoTo = {
onClickItem.invoke(position, it)
},
),
),
onDismissRequest = { moreDialog.makeAbsent() },

View file

@ -632,6 +632,7 @@ fun chooseStream(
streams: List<MediaStream>,
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) },
)
},
)
},
)

View file

@ -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)
}
}
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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 =

View file

@ -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
}

View file

@ -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<ItemDetailsDialogInfo?>(null) }
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(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<Season>,
seasons: List<RequestSeason>,
similar: List<DiscoverItem>,
recommended: List<DiscoverItem>,
trailers: List<Trailer>,
@ -299,6 +292,7 @@ fun DiscoverSeriesDetailsContent(
SeerrAvailability.from(series.mediaInfo?.status)
?: SeerrAvailability.UNKNOWN,
requestOnClick = requestOnClick,
pendingOnClick = requestOnClick,
cancelOnClick = cancelOnClick,
moreOnClick = moreOnClick,
goToOnClick = goToOnClick,

View file

@ -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<TvDetails?>(null)
val rating = MutableLiveData<DiscoverRating?>(null)
val seasons = MutableLiveData<List<Season>>(listOf())
val seasons = MutableLiveData<List<RequestSeason>>(listOf())
val trailers = MutableLiveData<List<Trailer>>(listOf())
val people = MutableLiveData<List<DiscoverItem>>(listOf())
val similar = MutableLiveData<List<DiscoverItem>>()
@ -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<Int, SeerrAvailability>()
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<Int>,
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()
}
}
}

View file

@ -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") {

View file

@ -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<RequestSeason>,
onSubmit: (Set<Int>, Boolean) -> Unit,
request4kEnabled: Boolean,
modifier: Modifier,
) {
val allSeasonNumbers = remember(seasons) { seasons.mapNotNull { it.season.seasonNumber }.toSet() }
val selected =
remember {
mutableStateSetOf<Int>(
*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<RequestSeason>,
request4kEnabled: Boolean,
onSubmit: (Set<Int>, 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),
)
}
}

View file

@ -86,6 +86,13 @@ fun EpisodeDetails(
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(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,
)
}
},

View file

@ -114,6 +114,13 @@ fun MovieDetails(
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(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,
)
}
},

View file

@ -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<ItemDetailsDialogInfo?>(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 {

View file

@ -125,20 +125,6 @@ fun SeriesOverview(
var rowFocused by rememberInt()
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(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,
)
}
},

View file

@ -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>(PeopleInItem())
val discovered = MutableStateFlow<List<DiscoverItem>>(listOf())
val discoverSeries = MutableStateFlow<DiscoverItem?>(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) }
}
}
}

View file

@ -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 -> {

View file

@ -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))
}

View file

@ -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,

View file

@ -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 <T> BottomDialog(
gravity: Int,
currentChoice: BottomDialogItem<T>? = 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 <T> BottomDialog(
val interactionSource = remember { MutableInteractionSource() }
ListItem(
selected = choice == currentChoice,
enabled = choice.enabled,
onClick = {
onDismissRequest()
onSelectChoice(index, choice)
@ -524,6 +531,7 @@ fun <T> BottomDialog(
}
},
interactionSource = interactionSource,
modifier = Modifier.focusRequester(focusRequesters[index]),
)
}
}
@ -539,6 +547,7 @@ data class BottomDialogItem<T>(
val data: T,
val headline: String,
val supporting: String?,
val enabled: Boolean = true,
)
@PreviewTvSpec

View file

@ -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]),
)
}
}

View file

@ -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

View file

@ -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()

View file

@ -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,
)
}

View file

@ -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<HttpUrl> {
val urls = mutableListOf<String>()
val urls = mutableListOf<HttpUrl>()
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
}

View file

@ -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) {

View file

@ -97,7 +97,6 @@ class SlideshowViewModel
var slideshowDelay by Delegates.notNull<Long>()
// private val album = MutableLiveData<BaseItem>()
private val _pager = MutableLiveData<ApiRequestPager<GetItemsRequest>>()
val pager: LiveData<List<BaseItem?>> = _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)

View file

@ -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)

View file

@ -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 -> {

View file

@ -146,17 +146,17 @@ class ApiRequestPager<T>(
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

View file

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about">حول</string>
<string name="active_recordings">التسجيلات النشطة</string>
<string name="add_favorite">إضافة للمفضلة</string>
<string name="add_server">إضافة خادم</string>
<string name="add_user">إضافة مستخدم</string>
<string name="audio">الصوت</string>
<string name="birthplace">مكان الميلاد</string>
<string name="bitrate">معدل البت</string>
<string name="born">وُلد</string>
<string name="cancel_recording">إلغاء التسجيل</string>
<string name="cancel_series_recording">إلغاء تسجيل المسلسل</string>
<string name="cancel">إلغاء</string>
<string name="chapters">الفصول</string>
<string name="choose_stream">اختيار %1$s</string>
<string name="clear_image_cache">مسح ذاكرة التخزين المؤقت للصور</string>
<string name="collection">مجموعة</string>
<string name="collections">المجموعات</string>
<string name="commercial">تجاري</string>
<string name="community_rating">تقييم المجتمع</string>
<string name="compose_error_message_title">حدث خطأ! اضغط على الزر لإرسال السجلات إلى خادمك.</string>
<string name="confirm">تأكيد</string>
<string name="continue_watching">تابع المشاهدة</string>
<string name="critic_rating">تقييم النقاد</string>
<string name="decimal_seconds">%.2f ثانية</string>
<string name="default_track">الافتراضي</string>
<string name="delete">حذف</string>
<string name="died">تُوفي</string>
<string name="directed_by">إخراج %1$s</string>
<string name="director">المخرج</string>
<string name="disabled">مُعطّل</string>
<string name="discovered_servers">الخوادم المكتشفة</string>
<string name="download_and_update"><![CDATA[تنزيل وتحديث]]></string>
<string name="downloading">تنزيل…</string>
<string name="enabled">مُفعّل</string>
<string name="ends_at">ينتهي عند %1$s</string>
<string name="enter_server_url">أدخل عنوان IP للخادم أو رابط URL</string>
<string name="enter_server_address">أدخل عنوان الخادم</string>
<string name="episodes">الحلقات</string>
<string name="error_loading_collection">خطأ في تحميل المجموعة %1$s</string>
<string name="external_track">خارجي</string>
<string name="favorites">المفضلة</string>
<string name="file_size">الحجم</string>
<string name="forced_track">إجبارية</string>
<string name="genres">الأنواع</string>
<string name="go_to_series">انتقل إلى المسلسل</string>
<string name="go_to">انتقل إلى</string>
<string name="hide_controller_timeout">إخفاء عناصر التحكم في التشغيل</string>
<string name="hide_debug_info">إخفاء معلومات تصحيح الأخطاء</string>
<string name="hide">إخفاء</string>
<string name="home">الرئيسية</string>
<string name="immediate">فوري</string>
<string name="intro">المقدمة</string>
<string name="jump_letters">#ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
<string name="library">المكتبة</string>
<string name="license_info">معلومات الترخيص</string>
<string name="live_tv">تلفزيون مباشر</string>
<string name="loading">تحميل…</string>
<string name="mark_entire_series_as_played">هل تريد تعيين المسلسل بأكمله كمُشاهَد؟</string>
<string name="mark_entire_series_as_unplayed">هل تريد تعيين المسلسل بأكمله كغير مشاهد؟</string>
<string name="mark_unwatched">تعيين كغير مشاهد</string>
<string name="mark_watched">تعيين كمشاهد</string>
<string name="max_bitrate">الحد الأقصى لمعدل البت</string>
<string name="more_like_this">المزيد من هذا القبيل</string>
<string name="more">المزيد</string>
<plurals name="movies">
<item quantity="zero">الأفلام</item>
<item quantity="one"></item>
<item quantity="two"></item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
</plurals>
<string name="name">الاسم</string>
<string name="next_up">التالي</string>
<string name="no_data">لا توجد بيانات</string>
<string name="no_results">لا توجد نتائج</string>
<string name="no_servers_found">لم يتم العثور على خوادم</string>
<string name="no_scheduled_recordings">لا توجد تسجيلات مجدولة</string>
<string name="no_update_available">لا يوجد تحديث متاح</string>
<string name="none">لا شيء</string>
<string name="only_forced_subtitles">الترجمات الإجبارية فقط</string>
<string name="outro">خاتمة</string>
<string name="path">المسار</string>
<plurals name="people">
<item quantity="zero">الأشخاص</item>
<item quantity="one"></item>
<item quantity="two"></item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
</plurals>
<string name="play_count">عدد مرات التشغيل</string>
<string name="play_from_here">تشغيل من هنا</string>
<string name="play">تشغيل</string>
<string name="playback_debug_info">إظهار معلومات التشغيل التفصيلية</string>
<string name="playback_speed">سرعة التشغيل</string>
<string name="playback">التشغيل</string>
<string name="playlist">قائمة التشغيل</string>
<string name="playlists">قوائم التشغيل</string>
<string name="preview">معاينة</string>
<string name="profile_specific_settings">إعدادات ملف تعريف المستخدم</string>
<string name="queue">قائمة الانتظار</string>
<string name="recap">ملخص ما سبق</string>
<string name="recently_added_in">أُضيف مؤخرًا في %1$s</string>
<string name="recently_added">أُضيف مؤخرًا</string>
<string name="recently_recorded">التسجيلات الأخيرة</string>
<string name="recently_released">أحدث الإصدارات</string>
<string name="recommended">مقترح</string>
<string name="record_program">تسجيل البرنامج</string>
<string name="record_series">تسجيل المسلسل</string>
<string name="remove_favorite">إزالة من المفضلة</string>
<string name="restart">إعادة تشغيل</string>
<string name="resume">استئناف</string>
<string name="save">حفظ</string>
<string name="search_and_download"><![CDATA[بحث وتنزيل]]></string>
<string name="search">بحث</string>
<string name="searching">جارٍ البحث…</string>
<string name="voice_search">البحث الصوتي</string>
<string name="voice_starting">جارٍ البدء</string>
<string name="voice_search_prompt">تحدث للبحث</string>
<string name="processing">جارٍ المعالجة</string>
<string name="press_back_to_cancel">اضغط رجوع للإلغاء</string>
<string name="voice_error_audio">خطأ في تسجيل الصوت</string>
<string name="voice_error_client">خطأ في التعرف على الصوت</string>
<string name="voice_error_permissions">مطلوب إذن الوصول للميكروفون</string>
<string name="voice_error_network">خطأ في الشبكة</string>
<string name="voice_error_network_timeout">انتهت مهلة الشبكة</string>
<string name="voice_error_no_match">لم يتم التعرف على الكلام</string>
</resources>

View file

@ -401,6 +401,7 @@
<string name="clear_track_choices">Vyčistit výběr skladeb</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">True HD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>

View file

@ -397,6 +397,7 @@
<string name="clear_track_choices">Ryd valg af spor</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>

View file

@ -199,12 +199,12 @@
<string name="italic_font">Kursivschrift</string>
<string name="runtime_sort">Laufzeit</string>
<plurals name="theme_songs">
<item quantity="one">Theme Songs</item>
<item quantity="other"/>
<item quantity="one">Titelsong</item>
<item quantity="other">Titelsongs</item>
</plurals>
<plurals name="theme_videos">
<item quantity="one">Theme Videos</item>
<item quantity="other"/>
<item quantity="one">Titelvideo</item>
<item quantity="other">Titelvideos</item>
</plurals>
<plurals name="clips">
<item quantity="one">Clip</item>
@ -224,11 +224,11 @@
</plurals>
<plurals name="shorts">
<item quantity="one">Shorts</item>
<item quantity="other"/>
<item quantity="other">Kurzvideos</item>
</plurals>
<string name="combine_continue_next_summary">Trifft nur auf Serien zu</string>
<string name="direct_play_ass">Direct play ASS subtitles</string>
<string name="direct_play_pgs">Direct play PGS subtitles</string>
<string name="direct_play_ass">Libass für ASS Untertitel verwenden</string>
<string name="direct_play_pgs">Direktwiedergabe PGS Untertitel</string>
<string name="downmix_stereo">Immer zu Stereo heruntermischen</string>
<string name="ffmpeg_extension_pref">FFmpeg decoder Modul verwenden</string>
<string name="global_content_scale">Standard Skalierung</string>
@ -251,8 +251,8 @@
<string name="font_size">Schriftgröße</string>
<string name="font_color">Schriftfarbe</string>
<string name="font_opacity">Schriftart Deckkraft</string>
<string name="edge_style">Rand Stil</string>
<string name="edge_color">Rand Farbe</string>
<string name="edge_style">Stil der Umrandung</string>
<string name="edge_color">Farbe der Umrandung</string>
<string name="background_opacity">Hintergrund Deckktraft</string>
<string name="background_style">Hintergrund Stil</string>
<string name="subtitle_style">Untertitel Stil</string>
@ -329,7 +329,7 @@
<string name="use_server_credentials">Login via Server</string>
<string name="press_enter_to_confirm">Drücke die mittlere Taste zur Bestätigung</string>
<string name="image_cache_size">Größe des Festplatten-Caches für Bilder (MB)</string>
<string name="cast_and_crew">Besetzung &amp; Mitwirkende</string>
<string name="cast_and_crew"><![CDATA[Besetzung & Mitwirkende]]></string>
<string name="container">Container</string>
<string name="codec">Codec</string>
<string name="level">Level</string>
@ -365,6 +365,7 @@
<string name="request_4k">In 4K anfragen</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -432,4 +433,122 @@
<string name="play_videos_during_slideshow">Videos während der Diashow abspielen</string>
<string name="max_days_next_up">Maximale Tage in \"Als nächstes\"</string>
<string name="quick_connect">Quick Connect</string>
<plurals name="minutes">
<item quantity="one">%s Minute</item>
<item quantity="other">%s Minuten</item>
</plurals>
<string name="interlaced">Interlaced</string>
<string name="nal">NAL</string>
<string name="quick_connect_summary">Gewähre anderen Geräten Zugriff auf deinen Account</string>
<string name="quick_connect_code">Eingabe Quick Connect Code</string>
<string name="no_limit">Keine Beschränkung</string>
<string name="add_row">Reihe hinzufügen</string>
<string name="genres_in">Genres in %1$s</string>
<string name="recently_released_in">kürzlich veröffentlicht in %1$s</string>
<string name="height">Höhe</string>
<string name="apply_all_rows">Auf alle Reihen anwenden</string>
<string name="add_row_for">Reihe hinzufügen für %1$s</string>
<string name="overwrite_server_settings">Überschreibe Einstellungen auf dem Server?</string>
<string name="overwrite_local_settings">Überschreibe lokale Einstellungen?</string>
<string name="suggestions_for">Vorschläge für %1$s</string>
<string name="send_media_info_log_to_server">Sende Media Info Log zum Server</string>
<string name="customize_home">Anpassung Homepage</string>
<string name="for_episodes">Für Episoden</string>
<string name="display_presets_description">Vorlagen um schnell alle Reihen zu gestalten</string>
<string name="customize_home_summary">Wähle Reihen und Bilder auf der Homepage</string>
<string name="start_after">Starte anschließend</string>
<string name="display_preset_series_thumb">Serien Vorschaubilder</string>
<string name="display_preset_episode_thumbnails">Episoden Vorschaubilder</string>
<string name="seerr_login">Einloggen zum Seerr Server</string>
<string name="seerr_jellyfin_user">Jellyfin User</string>
<string name="show_media_management">Zeige Media Management Optionen</string>
<string name="decrease_all_cards_size">Größe für alle Karten reduzieren</string>
<string name="settings_saved">Einstellungen gespeichert</string>
<string name="max_age_rating">Maximale Altersfreigabe</string>
<string name="no_max">Keine Begrenzung</string>
<string name="for_all_ages">Ohne Altersbeschränkung</string>
<string name="up_to_age">Ab %s Jahre</string>
<string name="screensaver">Bildschirmschoner</string>
<string name="screensaver_settings">Bildschirmschoner-Einstellungen</string>
<string name="start_screensaver">Bildschirmschoner starten</string>
<string name="duration">Länge</string>
<string name="photos">Fotos</string>
<string name="include_types">Arten einschließen</string>
<string name="include_types_summary">Für welche Arten von Einträgen sollen Bilder angezeigt werden</string>
<string name="content_scale_fit">Angepasst</string>
<string name="content_scale_crop">Zugeschnitten</string>
<string name="content_scale_fill">Ausgefüllt</string>
<string name="skip_ignore">Ignorieren</string>
<string name="skip_automatically">Automatisch überspringen</string>
<string name="skip_ask">Vor dem Überspringen nachfragen</string>
<string name="ffmpeg_fallback">FFmpeg nur verwenden, sofern keine anderen Decoder vorhanden sind</string>
<string name="ffmpeg_prefer">FFmpeg statt der vorhandenen Decoder verwenden</string>
<string name="ffmpeg_never">FFmpeg Decoder nie verwenden</string>
<string name="next_up_playback_end">Am Ende einer Playlist</string>
<string name="next_up_outro">Während der Credits/Outro</string>
<string name="white">Weiß</string>
<string name="light_gray">Hellgrau</string>
<string name="dark_gray">Dunkelgrau</string>
<string name="yellow">Gelb</string>
<string name="cyan">Cyan</string>
<string name="magenta">Magenta</string>
<string name="subtitle_edge_outline">Umrandung</string>
<string name="subtitle_edge_shadow">Schatten</string>
<string name="prefer_mpv">MPV bevorzugen</string>
<string name="player_backend_options_subtitles_prefer_mpv">ExoPlayer für die HDR Wiedergabe verwenden</string>
<string name="aspect_ratios_poster">Poster (2:3)</string>
<string name="aspect_ratios_16_9">16:9</string>
<string name="aspect_ratios_4_3">4:3</string>
<string name="aspect_ratios_square">Quadrat (1:1)</string>
<string name="image_type_primary">Primär</string>
<string name="backdrop_style_dynamic">Bild mit dynamischen Farben</string>
<string name="api_key">API Schlüssel</string>
<string name="seerr_local_user">Lokaler Benutzer</string>
<string name="search_for">Suche %s</string>
<string name="display_presets">Design-Vorlagen</string>
<string name="animate">Animieren</string>
<string name="content_scale_fill_width">Breite ausfüllen</string>
<string name="content_scale_fill_height">Höhe ausfüllen</string>
<string name="display_preset_default">Wholphin Standard</string>
<string name="display_preset_compact">Wholphin Kompakt</string>
<string name="volume_lowest">Niedrigste</string>
<string name="volume_low">Niedrig</string>
<string name="volume_medium">mittel</string>
<string name="volume_high">Hoch</string>
<string name="volume_full">Voll</string>
<string name="purple">Lila</string>
<string name="orange">Orange</string>
<string name="black">Schwarz</string>
<string name="backdrop_style_image">Nur Bild</string>
<string name="series_continueing">Aktuell</string>
<string name="in_app_screensaver">Eingebauten Bildschirmschoner nutzen</string>
<string name="delete_item">Bist du sicher, dass du diesen Eintrag löschen möchtest?</string>
<string name="actor">Schauspieler</string>
<string name="composer">Komponist</string>
<string name="writer">Autor</string>
<string name="guest_star">Nebenrolle</string>
<string name="producer">Produzent</string>
<string name="conductor">Dirigent</string>
<string name="lyricist">Liedtext-Schreiber</string>
<string name="artist">Künstler</string>
<string name="favorite_items">%s als Favorit speichern</string>
<string name="home_rows">Startseite - Reihen</string>
<string name="load_from_server">Vom Server Benutzerprofil laden</string>
<string name="save_to_server">Im Server Benutzerprofil speichern</string>
<string name="load_from_web_client">Vom Webclient laden</string>
<string name="increase_all_cards_size">Größe für alle Karten erhöhen</string>
<string name="use_thumb_images">Vorschaubilder benutzen</string>
<string name="no_subtitles_found">Es wurden keine Untertitel gefunden</string>
<string name="arranger">Arrangeur</string>
<string name="engineer">Ingenieur</string>
<string name="bold_blue">Dunkelblau</string>
<string name="image_subtitle_opacity">Untertitel-Transparenz</string>
<string name="image_type_thumb">Vorschaubild</string>
<string name="use_series">Serien Vorschaubilder verwenden</string>
<string name="enter_url_api_key"><![CDATA[URL & API Key eingeben]]></string>
<string name="search_and_download_subtitles"><![CDATA[Untertitel suchen & herunterladen]]></string>
<string name="creator">Ersteller</string>
<string name="background_style_wrap">Gestreckt</string>
<string name="background_style_boxed">Zentriert</string>
<string name="mixer">Mischer</string>
</resources>

View file

@ -35,7 +35,7 @@
<string name="name">Nombre</string>
<string name="no_results">Sin resultados</string>
<string name="none">Ninguno</string>
<string name="outro">Outro</string>
<string name="outro">Créditos finales</string>
<string name="people">Personas</string>
<string name="play">Reproducir</string>
<string name="playback">Reproducción</string>
@ -80,7 +80,7 @@
<string name="create_playlist">Crear nueva lista de reproducción</string>
<string name="add_to_playlist">Agregar a la lista de reproducción</string>
<string name="one_click_pause">Pausar con un clic</string>
<string name="one_click_pause_summary_on">Presiona el centro del D-Pad para pausar/reproducir</string>
<string name="one_click_pause_summary_on">Pulsa el botón central de la cruceta para reproducir/pausar</string>
<string name="success">Éxito</string>
<string name="runtime_sort">Duración</string>
<string name="extras">Extras</string>
@ -264,8 +264,8 @@
<string name="skip_back_preference">Retroceder</string>
<string name="skip_commercials_behavior">Comportamiento al saltar anuncios</string>
<string name="skip_forward_preference">Avanzar</string>
<string name="skip_intro_behavior">Comportamiento al saltar la intro</string>
<string name="skip_outro_behavior">Comportamiento al saltar el outro</string>
<string name="skip_intro_behavior">Comportamiento al saltar la cabecera</string>
<string name="skip_outro_behavior">Comportamiento al saltar créditos finales</string>
<string name="skip_previews_behavior">Comportamiento al saltar avances</string>
<string name="skip_recap_behavior">Comportamiento al saltar resumen</string>
<string name="update_available">Actualización disponible</string>
@ -291,8 +291,8 @@
<string name="skip_segment_commercial">Saltar anuncios</string>
<string name="skip_segment_preview">Saltar avance</string>
<string name="skip_segment_recap">Saltar resumen</string>
<string name="skip_segment_outro">Saltar outro</string>
<string name="skip_segment_intro">Saltar intro</string>
<string name="skip_segment_outro">Saltar créditos finales</string>
<string name="skip_segment_intro">Saltar cabecera</string>
<string name="played">Reproducido</string>
<string name="filter">FIltrar</string>
<string name="year">Año</string>
@ -401,6 +401,7 @@
<string name="clear_track_choices">Opciones de pista claras</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -485,8 +486,8 @@
<string name="content_scale_fill">Rellenar</string>
<string name="content_scale_fill_width">Ajustar al ancho</string>
<string name="content_scale_fill_height">Ajustar al alto</string>
<string name="display_preset_series_thumb">Miniaturas de las series</string>
<string name="display_preset_episode_thumbnails">Miniaturas de los episodios</string>
<string name="display_preset_series_thumb">Miniatura de la serie</string>
<string name="display_preset_episode_thumbnails">Miniatura del episodio</string>
<string name="volume_lowest">Mínimo</string>
<string name="volume_low">Bajo</string>
<string name="volume_medium">Medio</string>
@ -534,4 +535,38 @@
<string name="search_and_download_subtitles"><![CDATA[Buscar y descargar subtítulos]]></string>
<string name="no_subtitles_found">No se encontraron subtítulos externos</string>
<string name="series_continueing">Actualidad</string>
<plurals name="minutes">
<item quantity="one">%s minuto</item>
<item quantity="many">%s minutos</item>
<item quantity="other">%s minutos</item>
</plurals>
<string name="start_after">Comenzar después de</string>
<string name="animate">Animación</string>
<string name="max_age_rating">Clasificación por edad máxima</string>
<string name="for_all_ages">Para todas las edades</string>
<string name="up_to_age">Menores de %s</string>
<string name="screensaver">Salvapantallas</string>
<string name="screensaver_settings">Configuración de salvapantallas</string>
<string name="start_screensaver">Iniciar salvapantallas</string>
<string name="duration">Duración</string>
<string name="photos">Fotos</string>
<string name="include_types">Incluir tipos</string>
<string name="include_types_summary">Qué tipos de elementos mostrar</string>
<string name="in_app_screensaver">Usar salvapantallas de la aplicación</string>
<string name="no_max">Sin límite</string>
<string name="actor">Intérprete</string>
<string name="composer">Compositor</string>
<string name="writer">Guionista</string>
<string name="guest_star">Estrella invitada</string>
<string name="producer">Productor</string>
<string name="conductor">Director musical</string>
<string name="lyricist">Letrista</string>
<string name="arranger">Arreglista</string>
<string name="engineer">Ingeniero de sonido</string>
<string name="mixer">Ingeniero de mezcla</string>
<string name="creator">Creador</string>
<string name="artist">Artista</string>
<string name="delete_item">¿Estás seguro de que quieres eliminar este elemento?</string>
<string name="show_media_management">Mostrar opciones de gestión de medios</string>
<string name="search_for">Buscar %s</string>
</resources>

View file

@ -367,6 +367,7 @@
<string name="only_forced_subtitles">Vaid sundkorras kuvatud subtiitrid</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>

View file

@ -221,8 +221,8 @@
<string name="font_opacity">Opacité de la police</string>
<string name="edge_style">Style des contours</string>
<string name="edge_color">Couleur des contours</string>
<string name="background_opacity">Opacité de larrière-plan</string>
<string name="background_style">Style de larrière-plan</string>
<string name="background_opacity">Opacité de l\'arrière-plan</string>
<string name="background_style">Style de l\'arrière-plan</string>
<string name="subtitle_style">Style des sous-titres</string>
<string name="background_color">Couleur de fond</string>
<string name="reset">Réinitialiser</string>
@ -234,10 +234,10 @@
<string name="exoplayer_options">Options ExoPlayer</string>
<string name="skip_segment_unknown">Passer le segment</string>
<string name="skip_segment_commercial">Passer les publicités</string>
<string name="skip_segment_preview">Passer laperçu</string>
<string name="skip_segment_preview">Passer l\'aperçu</string>
<string name="skip_segment_recap">Passer le récapitulatif</string>
<string name="skip_segment_outro">Passer le générique de fin</string>
<string name="skip_segment_intro">Passer lintro</string>
<string name="skip_segment_intro">Passer l\'intro</string>
<string name="played">Lu</string>
<string name="filter">Filtre</string>
<string name="year">Année</string>
@ -275,7 +275,7 @@
<string name="play_theme_music">Lire la musique de thème</string>
<string name="skip_back_on_resume_preference">Revenir en arrière à la reprise de la lecture</string>
<string name="skip_commercials_behavior">Comportement de saut des publicités</string>
<string name="skip_intro_behavior">Comportement de saut de lintro</string>
<string name="skip_intro_behavior">Comportement de saut de l\'intro</string>
<string name="skip_outro_behavior">Comportement de saut du générique de fin</string>
<string name="skip_previews_behavior">Comportement de saut des aperçus</string>
<string name="skip_recap_behavior">Comportement de saut du récapitulatif</string>
@ -289,7 +289,7 @@
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<string name="combine_continue_next"><![CDATA[Combiner "Continuer à regarder" & "À suivre"]]></string>
<string name="combine_continue_next"><![CDATA[Combiner \"Continuer à regarder\" & \"À suivre\"]]></string>
<string name="download_and_update"><![CDATA[Télécharger & Mettre à jour]]></string>
<string name="search_and_download"><![CDATA[Rechercher & Télécharger]]></string>
<string name="tv_dvr_schedule">Calendrier du DVR</string>
@ -382,6 +382,7 @@
<string name="no_trailers">Pas de bandes-annonces</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -480,4 +481,89 @@
<string name="display_presets_description">Préréglages intégrés pour styliser rapidement toutes les lignes</string>
<string name="customize_home_summary">Choisissez des lignes et des images sur la page d\'accueil</string>
<string name="favorite_items">Favori %s</string>
<plurals name="minutes">
<item quantity="one">%s minute</item>
<item quantity="many">%s minutes</item>
<item quantity="other">%s minute</item>
</plurals>
<string name="start_after">Démarrer après</string>
<string name="animate">Animer</string>
<string name="max_age_rating">Classification par âge maximale</string>
<string name="no_max">Pas de maximum</string>
<string name="for_all_ages">Pour tous les âges</string>
<string name="up_to_age">Jusqu\'à l\'âge de %s</string>
<string name="screensaver">Écran de veille</string>
<string name="screensaver_settings">Paramètres de l\'économiseur d\'écran</string>
<string name="start_screensaver">Lancer l\'économiseur d\'écran</string>
<string name="duration">Durée</string>
<string name="photos">Photos</string>
<string name="include_types">Inclure les types</string>
<string name="include_types_summary">Quels types d\'éléments afficher des images pour</string>
<string name="content_scale_fit">Ajustement</string>
<string name="content_scale_crop">Recadrer</string>
<string name="content_scale_fill">Remplir</string>
<string name="content_scale_fill_width">Largeur de remplissage</string>
<string name="content_scale_fill_height">Hauteur de remplissage</string>
<string name="display_preset_default">Wholphin Par défaut</string>
<string name="display_preset_compact">Wholphin Compact</string>
<string name="display_preset_series_thumb">Images miniatures de la série</string>
<string name="display_preset_episode_thumbnails">Images miniatures de l\'épisode</string>
<string name="volume_lowest">Le plus bas</string>
<string name="volume_low">Faible</string>
<string name="volume_medium">Moyen</string>
<string name="volume_high">Haute</string>
<string name="volume_full">Complet</string>
<string name="purple">Violet</string>
<string name="orange">Orange</string>
<string name="bold_blue">Bleu gras</string>
<string name="black">Noir</string>
<string name="skip_ignore">Ignorer</string>
<string name="skip_automatically">Passer automatiquement</string>
<string name="skip_ask">Demander à passer</string>
<string name="ffmpeg_fallback">N\'utilisez FFmpeg que si aucun décodeur intégré n\'existe</string>
<string name="ffmpeg_prefer">Préférez utiliser FFmpeg plutôt que les décodeurs intégrés</string>
<string name="ffmpeg_never">N\'utilisez jamais les décodeurs FFmpeg</string>
<string name="next_up_playback_end">À la fin de la lecture</string>
<string name="next_up_outro">Pendant le générique de fin/outro</string>
<string name="white">Blanc</string>
<string name="light_gray">Gris clair</string>
<string name="dark_gray">Gris foncé</string>
<string name="yellow">Jaune</string>
<string name="cyan">Cyan</string>
<string name="magenta">Magenta</string>
<string name="subtitle_edge_outline">Contour</string>
<string name="subtitle_edge_shadow">Ombre</string>
<string name="background_style_wrap">Envelopper</string>
<string name="background_style_boxed">Encadré</string>
<string name="prefer_mpv">Préférer MPV</string>
<string name="player_backend_options_subtitles_prefer_mpv">Utiliser ExoPlayer pour la lecture HDR</string>
<string name="aspect_ratios_poster">Affiche (2:3)</string>
<string name="aspect_ratios_16_9">16:9</string>
<string name="aspect_ratios_4_3">4:3</string>
<string name="aspect_ratios_square">Carré (1:1)</string>
<string name="image_type_primary">Primaire</string>
<string name="image_type_thumb">Miniature</string>
<string name="backdrop_style_dynamic">Image avec couleur dynamique</string>
<string name="backdrop_style_image">Image uniquement</string>
<string name="enter_url_api_key">Entrez l\'URL et la clé API</string>
<string name="api_key">Clé API</string>
<string name="seerr_login">Se connecter au serveur Seerr</string>
<string name="seerr_jellyfin_user">Utilisateur Jellyfin</string>
<string name="seerr_local_user">Utilisateur local</string>
<string name="search_and_download_subtitles">Rechercher et télécharger des sous-titres</string>
<string name="no_subtitles_found">Aucun sous-titre distant n\'a été trouvé</string>
<string name="series_continueing">Présent</string>
<string name="in_app_screensaver">Utiliser l\'économiseur d\'écran intégré</string>
<string name="actor">Acteur</string>
<string name="composer">Compositeur</string>
<string name="writer">Écrivain</string>
<string name="guest_star">Invité spécial</string>
<string name="producer">Producteur</string>
<string name="conductor">Conducteur</string>
<string name="lyricist">Parolier</string>
<string name="arranger">Arrangeur</string>
<string name="engineer">Ingénieur</string>
<string name="mixer">Mélangeur</string>
<string name="creator">Créateur</string>
<string name="artist">Artiste</string>
</resources>

View file

@ -242,8 +242,8 @@
<string name="font_size">Ukuran font</string>
<string name="font_color">Warna font</string>
<string name="font_opacity">Transparansi font</string>
<string name="edge_style">Jenis garis tepi</string>
<string name="edge_color">Warna garis tepi</string>
<string name="edge_style">Jenis pinggiran</string>
<string name="edge_color">Warna pinggiran</string>
<string name="background_opacity">Transparansi latar belakang</string>
<string name="background_style">Gaya latar belakang</string>
<string name="subtitle_style">Gaya subtitel</string>
@ -318,7 +318,7 @@
<string name="require_pin_code">Membutuhkan PIN untuk profil</string>
<string name="will_remove_pin">PIN akan dihapus</string>
<string name="view_options">Opsi tampilan</string>
<string name="edge_size">Ukuran garis tepi</string>
<string name="edge_size">Ukuran pinggiran</string>
<string name="automatic">Otomatis</string>
<string name="username_or_password">Gunakan nama pengguna/kata sandi</string>
<string name="subtitle_margin">Batas tepi</string>
@ -369,6 +369,7 @@
<string name="clear_track_choices">Hapus pilihan trek</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -446,4 +447,79 @@
<string name="display_presets_description">Preset bawaan untuk kustomisasi cepat semua baris</string>
<string name="customize_home_summary">Pilih baris dan gambar di halaman beranda</string>
<string name="favorite_items">Favoritkan %s</string>
<string name="artist">Artis</string>
<plurals name="minutes">
<item quantity="other">%s menit</item>
</plurals>
<string name="start_after">Mulai setelah</string>
<string name="animate">Animasi</string>
<string name="max_age_rating">Rating usia maks</string>
<string name="no_max">Tidak ada maks</string>
<string name="for_all_ages">Untuk semua umur</string>
<string name="up_to_age">Maks. usia %s</string>
<string name="screensaver">Screensaver</string>
<string name="screensaver_settings">Pengaturan screensaver</string>
<string name="start_screensaver">Mulai screensaver</string>
<string name="duration">Durasi</string>
<string name="photos">Foto</string>
<string name="include_types">Sertakan jenis</string>
<string name="include_types_summary">Tampilkan gambar untuk jenis item</string>
<string name="display_preset_default">Wholphin Bawaan</string>
<string name="display_preset_compact">Wholphin Ringkas</string>
<string name="display_preset_series_thumb">Gambar Thumbnail Serial</string>
<string name="display_preset_episode_thumbnails">Gambar Thumbnail Episode</string>
<string name="volume_lowest">Terendah</string>
<string name="volume_low">Rendah</string>
<string name="volume_medium">Sedang</string>
<string name="volume_high">Tinggi</string>
<string name="volume_full">Penuh</string>
<string name="purple">Ungu</string>
<string name="orange">Oranye</string>
<string name="bold_blue">Biru Tegas</string>
<string name="black">Hitam</string>
<string name="skip_ignore">Abaikan</string>
<string name="skip_automatically">Lewati otomatis</string>
<string name="skip_ask">Tanya sebelum melewati</string>
<string name="ffmpeg_fallback">Gunakan FFmpeg hanya jika dekoder bawaan tidak ada</string>
<string name="ffmpeg_prefer">Utamakan FFmpeg daripada dekoder bawaan</string>
<string name="ffmpeg_never">Jangan pernah gunakan dekoder FFmpeg</string>
<string name="next_up_playback_end">Di akhir pemutaran</string>
<string name="next_up_outro">Selama kredit akhir/outro</string>
<string name="white">Putih</string>
<string name="light_gray">Abu-abu muda</string>
<string name="dark_gray">Abu-abu tua</string>
<string name="yellow">Kuning</string>
<string name="cyan">Sian</string>
<string name="magenta">Magenta</string>
<string name="subtitle_edge_outline">Garis luar</string>
<string name="prefer_mpv">Utamakan MPV</string>
<string name="player_backend_options_subtitles_prefer_mpv">Gunakan ExoPlayer untuk pemutaran HDR</string>
<string name="aspect_ratios_poster">Poster (2:3)</string>
<string name="aspect_ratios_16_9">16:9</string>
<string name="aspect_ratios_4_3">4:3</string>
<string name="aspect_ratios_square">Persegi (1:1)</string>
<string name="image_type_primary">Utama</string>
<string name="image_type_thumb">Thumbnail</string>
<string name="backdrop_style_dynamic">Gambar dengan warna dinamis</string>
<string name="backdrop_style_image">Hanya gambar</string>
<string name="enter_url_api_key"><![CDATA[Enter URL & API Key]]></string>
<string name="api_key">API Key</string>
<string name="seerr_login">Masuk ke server Seerr</string>
<string name="seerr_jellyfin_user">Pengguna Jellyfin</string>
<string name="seerr_local_user">Pengguna lokal</string>
<string name="search_and_download_subtitles"><![CDATA[Search & download subtitles]]></string>
<string name="no_subtitles_found">Subtitel remote tidak ditemukan</string>
<string name="series_continueing">Tayang</string>
<string name="in_app_screensaver">Gunakan screensaver dalam aplikasi</string>
<string name="actor">Aktor</string>
<string name="composer">Komposer</string>
<string name="writer">Penulis</string>
<string name="guest_star">Bintang tamu</string>
<string name="producer">Produser</string>
<string name="conductor">Konduktor</string>
<string name="lyricist">Penulis lirik</string>
<string name="arranger">Aransemen</string>
<string name="engineer">Teknisi</string>
<string name="mixer">Mixer</string>
<string name="creator">Kreator</string>
</resources>

View file

@ -383,6 +383,7 @@
<string name="only_forced_subtitles">Solo sottotitoli forzati</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -480,4 +481,92 @@
<string name="use_series">Usa immagine delle serie</string>
<string name="customize_home_summary">Scegli righe e immagini nella home page</string>
<string name="favorite_items">Preferito %s</string>
<string name="content_scale_fit">Adatta</string>
<string name="content_scale_crop">Ritaglia</string>
<string name="content_scale_fill">Riempi</string>
<string name="content_scale_fill_width">Riempi larghezza</string>
<string name="content_scale_fill_height">Riempi altezza</string>
<string name="display_preset_default">Wholphin predefinito</string>
<string name="display_preset_compact">Wholphin Compatto</string>
<string name="display_preset_series_thumb">Immagini miniatura serie</string>
<string name="display_preset_episode_thumbnails">Immagini miniatura episodio</string>
<string name="volume_lowest">Minimo</string>
<string name="volume_low">Basso</string>
<string name="volume_medium">Medio</string>
<string name="volume_high">Alto</string>
<string name="volume_full">Massimo</string>
<string name="purple">Viola</string>
<string name="orange">Arancione</string>
<string name="bold_blue">Blu intenso</string>
<string name="black">Nero</string>
<string name="skip_ignore">Ignora</string>
<string name="skip_automatically">Salta automaticamente</string>
<string name="skip_ask">Chiedi se saltare</string>
<string name="ffmpeg_fallback">Usa FFmpeg solo se non esiste un decodificatore integrato</string>
<string name="ffmpeg_prefer">Preferisci FFmpeg ai decoder integrati</string>
<string name="ffmpeg_never">Non usare mai i decoder FFmpeg</string>
<string name="next_up_playback_end">Alla fine della riproduzione</string>
<string name="next_up_outro">Durante i titoli di coda</string>
<string name="white">Bianco</string>
<string name="light_gray">Grigio chiaro</string>
<string name="dark_gray">Grigio scuro</string>
<string name="yellow">Giallo</string>
<string name="cyan">Ciano</string>
<string name="magenta">Magenta</string>
<string name="subtitle_edge_outline">Contorno</string>
<string name="subtitle_edge_shadow">Ombra</string>
<string name="background_style_wrap">Avvolgi</string>
<string name="background_style_boxed">Con riquadro</string>
<string name="prefer_mpv">Preferisci MVP</string>
<string name="player_backend_options_subtitles_prefer_mpv">Usa ExoPlayer per la riproduzione HDR</string>
<string name="aspect_ratios_poster">Copertina (2:3)</string>
<string name="aspect_ratios_16_9">16:9</string>
<string name="aspect_ratios_4_3">4:3</string>
<string name="aspect_ratios_square">Quadrato (1:1)</string>
<string name="image_type_primary">Principale</string>
<string name="image_type_thumb">Miniatura</string>
<string name="backdrop_style_dynamic">Immagine con colore dinamico</string>
<string name="backdrop_style_image">Solo immagine</string>
<string name="enter_url_api_key"><![CDATA[Inserisci URL e chiave API]]></string>
<string name="api_key">Chiave API</string>
<string name="seerr_login">Accedi al server Seerr</string>
<string name="seerr_jellyfin_user">Utente Jellyfin</string>
<string name="seerr_local_user">Utente locale</string>
<string name="search_and_download_subtitles"><![CDATA[Cerca e scarica sottotitoli]]></string>
<string name="no_subtitles_found">Nessun sottotitolo remoto trovato</string>
<string name="series_continueing">In corso</string>
<plurals name="minutes">
<item quantity="one">%s minuto</item>
<item quantity="many">%s minuti</item>
<item quantity="other">%s minuti</item>
</plurals>
<string name="start_after">Inizia dopo</string>
<string name="animate">Anima</string>
<string name="max_age_rating">Età massima</string>
<string name="for_all_ages">Per tutte le età</string>
<string name="up_to_age">Fino a %s anni</string>
<string name="screensaver">Salvaschermo</string>
<string name="screensaver_settings">Impostazioni salvaschermo</string>
<string name="start_screensaver">Avvia salvaschermo</string>
<string name="duration">Durata</string>
<string name="photos">Foto</string>
<string name="include_types">Includi tipi</string>
<string name="include_types_summary">Quali tipi di elementi mostrare nelle immagini</string>
<string name="no_max">Nessun massimo</string>
<string name="in_app_screensaver">Usa salvaschermo dell\'app</string>
<string name="composer">Compositore</string>
<string name="writer">Sceneggiatore</string>
<string name="actor">Attore</string>
<string name="producer">Produttore</string>
<string name="guest_star">Ospite speciale</string>
<string name="conductor">Direttore dorchestra</string>
<string name="lyricist">Paroliere</string>
<string name="arranger">Arrangiatore</string>
<string name="engineer">Ingegnere del suono</string>
<string name="mixer">Tecnico di mixaggio</string>
<string name="artist">Artista</string>
<string name="creator">Creatore</string>
<string name="search_for">Cerca %s</string>
<string name="delete_item">Sei sicuro di voler cancellare questo elemento?</string>
<string name="show_media_management">Mostra opzioni di gestione dei media</string>
</resources>

View file

@ -65,8 +65,8 @@
<string name="more">Mais</string>
<plurals name="movies">
<item quantity="one">Filmes</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<string name="name">Nome</string>
<string name="next_up">A Seguir</string>
@ -81,8 +81,8 @@
<string name="path">Caminho</string>
<plurals name="people">
<item quantity="one">Pessoas</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<string name="play_count">Contagem de Reproduções</string>
<string name="play_from_here">Reproduzir a partir daqui</string>
@ -152,8 +152,8 @@
<string name="trailer">Trailer</string>
<plurals name="trailers">
<item quantity="one">Trailers</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<string name="tv_dvr_schedule">Agenda do DVR</string>
<string name="tv_guide">Guia</string>
@ -161,8 +161,8 @@
<string name="tv_seasons">Temporadas</string>
<plurals name="tv_shows">
<item quantity="one">Programas de TV</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<string name="ui_interface">Interface</string>
<string name="unknown">Desconhecido</string>
@ -190,53 +190,53 @@
<string name="extras">Extras</string>
<plurals name="other_extras">
<item quantity="one">Outro</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="behind_the_scenes">
<item quantity="one">Por Trás das Cenas</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="theme_songs">
<item quantity="one">Músicas Tema</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="theme_videos">
<item quantity="one">Vídeos Tema</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="clips">
<item quantity="one">Clipes</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="deleted_scenes">
<item quantity="one">Cenas Deletadas</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="interviews">
<item quantity="one">Entrevistas</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="scenes">
<item quantity="one">Cenas</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="samples">
<item quantity="one">Amostras</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="shorts">
<item quantity="one">Curtas</item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<string name="favorite_items">Favoritar %s</string>
<plurals name="downloads">
@ -405,6 +405,7 @@
<string name="clear_track_choices">Limpar escolhas de faixas</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>

View file

@ -121,7 +121,7 @@
<string name="top_unwatched">Mais Votados Não Assistidos</string>
<string name="trailer">Trailer</string>
<plurals name="trailers">
<item quantity="one">Trailers</item>
<item quantity="one">Trailer</item>
<item quantity="many">Trailers</item>
<item quantity="other">Trailers</item>
</plurals>
@ -382,6 +382,7 @@
<string name="clear_track_choices">Apagar seleções de faixas</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -463,8 +464,8 @@
<string name="apply_all_rows">Aplicar a todas as linhas</string>
<string name="customize_home">Personalizar página inicial</string>
<string name="home_rows">Linhas da página inicial</string>
<string name="load_from_server">Carregar do servidor</string>
<string name="save_to_server">Gravar para o servidor</string>
<string name="load_from_server">Carregar do perfil de utilizador do servidor</string>
<string name="save_to_server">Guardar no perfil de utilizador do servidor</string>
<string name="load_from_web_client">Carregar a partir do cliente web</string>
<string name="use_series">Usar imagem da série</string>
<string name="add_row_for">Adicionar linha para %1$s</string>
@ -479,4 +480,90 @@
<string name="display_presets_description">Predefinições embarcadas para definir todas as linhas rapidamente</string>
<string name="customize_home_summary">Escolha as linhas e imagens na página inicial</string>
<string name="decrease_all_cards_size">Diminuir o tamanho de todos os cartões</string>
<string name="favorite_items">Favoritos %s</string>
<plurals name="minutes">
<item quantity="one">%s minuto</item>
<item quantity="many">%s minutos</item>
<item quantity="other">%s minutos</item>
</plurals>
<string name="start_after">Começar depois</string>
<string name="animate">Animado</string>
<string name="max_age_rating">Idade máxima</string>
<string name="no_max">Sem máximo</string>
<string name="for_all_ages">Para todas as idades</string>
<string name="up_to_age">Até a idade %s</string>
<string name="screensaver">Proteção de ecrã</string>
<string name="screensaver_settings">Definições de protecção de ecrã</string>
<string name="start_screensaver">Iniciar protecção de ecrã</string>
<string name="duration">Duração</string>
<string name="photos">Fotos</string>
<string name="include_types">Incluir tipos</string>
<string name="include_types_summary">Que tipos de itens devem mostrar imagens</string>
<string name="content_scale_fit">Ajustar</string>
<string name="content_scale_crop">Recortar</string>
<string name="content_scale_fill">Preencher</string>
<string name="content_scale_fill_width">Preencher largura</string>
<string name="content_scale_fill_height">Preencher altura</string>
<string name="display_preset_default">Padrão do Wholphin</string>
<string name="display_preset_compact">Wholphin Compacto</string>
<string name="display_preset_series_thumb">Imagens em miniatura da série</string>
<string name="display_preset_episode_thumbnails">Imagens em miniatura do episódio</string>
<string name="volume_lowest">Mínimo</string>
<string name="volume_low">Baixo</string>
<string name="volume_medium">Médio</string>
<string name="volume_high">Alto</string>
<string name="volume_full">Máximo</string>
<string name="purple">Roxo</string>
<string name="orange">Laranja</string>
<string name="bold_blue">Azul intenso</string>
<string name="black">Preto</string>
<string name="skip_ignore">Ignorar</string>
<string name="skip_automatically">Saltar automaticamente</string>
<string name="skip_ask">Pedir para saltar</string>
<string name="ffmpeg_fallback">Usa FFmpeg apenas se não houver um descodificador integrado</string>
<string name="ffmpeg_prefer">Prefere o FFmpeg aos descodificadores integrados</string>
<string name="ffmpeg_never">Nunca usar os descodificadores FFmpeg</string>
<string name="next_up_playback_end">No final da reprodução</string>
<string name="next_up_outro">Durante os créditos finais</string>
<string name="white">Branco</string>
<string name="light_gray">Cinzento claro</string>
<string name="dark_gray">Cinzento escuro</string>
<string name="yellow">Amarelo</string>
<string name="cyan">Ciano</string>
<string name="magenta">Magenta</string>
<string name="subtitle_edge_outline">Contorno</string>
<string name="subtitle_edge_shadow">Sombra</string>
<string name="background_style_wrap">Envolver</string>
<string name="background_style_boxed">Com caixa</string>
<string name="prefer_mpv">Preferir MPV</string>
<string name="player_backend_options_subtitles_prefer_mpv">Usar ExoPlayer para reprodução HDR</string>
<string name="aspect_ratios_poster">Poster (2:3)</string>
<string name="aspect_ratios_16_9">16:9</string>
<string name="aspect_ratios_4_3">4:3</string>
<string name="aspect_ratios_square">Quadrado (1:1)</string>
<string name="image_type_primary">Primária</string>
<string name="image_type_thumb">Miniatura</string>
<string name="backdrop_style_dynamic">Imagem com cor dinâmica</string>
<string name="backdrop_style_image">Apenas imagem</string>
<string name="enter_url_api_key"><![CDATA[Inserir URL e Chave API]]></string>
<string name="api_key">Chave API</string>
<string name="seerr_login">Iniciar sessão no servidor Seerr</string>
<string name="seerr_jellyfin_user">Utilizador Jellyfin</string>
<string name="seerr_local_user">Utilizador local</string>
<string name="search_and_download_subtitles"><![CDATA[Procurar e transferir legendas]]></string>
<string name="no_subtitles_found">Não foram encontradas legendas remotas</string>
<string name="series_continueing">Em curso</string>
<string name="in_app_screensaver">Usar protetor de ecrã da aplicação</string>
<string name="actor">Ator</string>
<string name="composer">Compositor</string>
<string name="writer">Argumentista</string>
<string name="guest_star">Convidado especial</string>
<string name="producer">Produtor</string>
<string name="conductor">Director musicar</string>
<string name="lyricist">Letrista</string>
<string name="arranger">Arranjador</string>
<string name="engineer">Engenheiro de som</string>
<string name="mixer">Técnico de mixagem</string>
<string name="creator">Criador</string>
<string name="artist">Artista</string>
</resources>

View file

@ -146,9 +146,9 @@
<string name="trailer">Trailer</string>
<plurals name="trailers">
<item quantity="one">Trailery</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<string name="tv_dvr_schedule">DVR rozvrh</string>
<string name="tv_guide">Sprievodca</string>
@ -156,9 +156,9 @@
<string name="tv_seasons">Série</string>
<plurals name="tv_shows">
<item quantity="one">Seriály</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<string name="ui_interface">Rozhranie</string>
<string name="unknown">Neznámy</string>
@ -186,69 +186,69 @@
<string name="extras">Bonusový materiál</string>
<plurals name="other_extras">
<item quantity="one">Ostatné</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="behind_the_scenes">
<item quantity="one">Zo zákulisia</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="theme_songs">
<item quantity="one">Tématické piesne</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="theme_videos">
<item quantity="one">Tématické videá</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="clips">
<item quantity="one">Klipy</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="deleted_scenes">
<item quantity="one">Vymazané scény</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="interviews">
<item quantity="one">Rozhovory</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="scenes">
<item quantity="one">Scény</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="samples">
<item quantity="one">Vzorky</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="featurettes">
<item quantity="one">Krátkometrážne filmy</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<plurals name="shorts">
<item quantity="one">Krátke videá</item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
<item quantity="few"/>
<item quantity="many"/>
<item quantity="other"/>
</plurals>
<string name="favorite_items">Obľúbené %s</string>
<plurals name="downloads">

View file

@ -277,4 +277,49 @@
<string name="enter_pin">Skriv in PIN</string>
<string name="sign_in_auto">Logga in automatiskt</string>
<string name="use_server_credentials">Logga in via server</string>
<string name="enter_server_address">Ange server-adress</string>
<string name="voice_error_network">Nätverksfel</string>
<string name="voice_error_unknown">Okänt fel</string>
<plurals name="minutes">
<item quantity="one">%s minut</item>
<item quantity="other">%s minuter</item>
</plurals>
<string name="bold_font">Fet stil</string>
<string name="dolby_atmos">Dolby Atmos</string>
<string name="discard_change">Avbryt ändringar?</string>
<string name="require_pin_code">Kräv PIN för profilen</string>
<string name="resolution">Upplösning</string>
<string name="language">Språk</string>
<string name="yes">Ja</string>
<string name="no">Nej</string>
<string name="live_tv_repeat">Upprepa</string>
<string name="password">Lösenord</string>
<string name="username">Användarnamn</string>
<string name="creator">Skapare</string>
<string name="engineer">Ingenjör</string>
<string name="actor">Skådespelare</string>
<string name="composer">Kompositör</string>
<string name="writer">Författare</string>
<string name="producer">Producent</string>
<string name="backdrop_style_image">Endast bild</string>
<string name="white">Vit</string>
<string name="light_gray">Ljusgrå</string>
<string name="dark_gray">Mörkgrå</string>
<string name="yellow">Gul</string>
<string name="purple">Lila</string>
<string name="black">Svart</string>
<string name="for_all_ages">För alla åldrar</string>
<string name="up_to_age">Upp till %s år</string>
<string name="screensaver">Skärmsläckare</string>
<string name="screensaver_settings">Inställningar Skärmsläckare</string>
<string name="start_screensaver">Starta skärmsläckare</string>
<string name="photos">Bilder</string>
<string name="settings_saved">Inställningar sparade</string>
<string name="rotate_left">Rotera vänster</string>
<string name="rotate_right">Rotera höger</string>
<string name="zoom_in">Zooma in</string>
<string name="zoom_out">Zooma ut</string>
<string name="no_limit">Ingen gräns</string>
<string name="add_row">Lägg till rad</string>
<string name="press_back_to_cancel">Tryck bakåt för att avbryta</string>
</resources>

View file

@ -256,7 +256,7 @@
<string name="check_for_updates">Güncellemeleri kontrol et</string>
<string name="combine_continue_next_summary">Yalnızca diziler için geçerlidir</string>
<string name="combine_continue_next"><![CDATA[İzlemeye Devam Et ve Sıradaki Bölümleri Birleştir]]></string>
<string name="direct_play_ass">ASS altyazıları doğrudan oynat</string>
<string name="direct_play_ass">ASS altyazıları için libass kullan</string>
<string name="direct_play_pgs">PGS altyazıları doğrudan oynat</string>
<string name="downmix_stereo">Her zaman Stereo\'ya dönüştür</string>
<string name="ffmpeg_extension_pref">FFmpeg kod çözücü kullan</string>
@ -394,6 +394,7 @@
<string name="clear_track_choices">Parça seçimlerini temizle</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -472,4 +473,91 @@
<string name="display_presets_description">Tüm satırları hızlıca stilize etmek için hazır ayarlar</string>
<string name="favorite_items">Favori %s</string>
<string name="customize_home_summary">Ana sayfadaki satırları ve görselleri seçin</string>
<string name="content_scale_fit">Sığdır</string>
<string name="content_scale_crop">Kırp</string>
<string name="content_scale_fill">Doldur</string>
<string name="content_scale_fill_width">Genişliği Doldur</string>
<string name="content_scale_fill_height">Yüksekliği Doldur</string>
<string name="display_preset_default">Wholphin Varsayılan</string>
<string name="display_preset_compact">Wholphin Kompakt</string>
<string name="display_preset_series_thumb">Dizi Küçük Resimleri</string>
<string name="display_preset_episode_thumbnails">Bölüm Küçük Resimleri</string>
<string name="volume_lowest">En Düşük</string>
<string name="volume_low">Düşük</string>
<string name="volume_medium">Orta</string>
<string name="volume_high">Yüksek</string>
<string name="volume_full">Tam</string>
<string name="purple">Mor</string>
<string name="orange">Turuncu</string>
<string name="bold_blue">Koyu Mavi</string>
<string name="black">Siyah</string>
<string name="skip_ignore">Yoksay</string>
<string name="skip_automatically">Otomatik Atla</string>
<string name="skip_ask">Atlamak için sor</string>
<string name="ffmpeg_fallback">Dahili çözücü yoksa FFmpeg kullan</string>
<string name="ffmpeg_prefer">Dahili çözücüler yerine FFmpeg\'i tercih et</string>
<string name="ffmpeg_never">FFmpeg çözücülerini asla kullanma</string>
<string name="next_up_playback_end">Oynatma sonunda</string>
<string name="next_up_outro">Kapanış bölümünde / Jenerik sırasında</string>
<string name="white">Beyaz</string>
<string name="light_gray">ık Gri</string>
<string name="dark_gray">Koyu Gri</string>
<string name="yellow">Sarı</string>
<string name="cyan">Camgöbeği</string>
<string name="magenta">Macenta</string>
<string name="subtitle_edge_outline">Kenarlık</string>
<string name="subtitle_edge_shadow">Gölge</string>
<string name="background_style_wrap">Kapla</string>
<string name="background_style_boxed">Kutu içinde</string>
<string name="prefer_mpv">MPV\'yi tercih et</string>
<string name="player_backend_options_subtitles_prefer_mpv">HDR oynatma için ExoPlayer kullan</string>
<string name="aspect_ratios_poster">Poster (2:3)</string>
<string name="aspect_ratios_16_9">16:9</string>
<string name="aspect_ratios_4_3">4:3</string>
<string name="aspect_ratios_square">Kare (1:1)</string>
<string name="image_type_primary">Birincil</string>
<string name="image_type_thumb">Küçük Resim</string>
<string name="backdrop_style_dynamic">Dinamik renkli görsel</string>
<string name="backdrop_style_image">Sadece görsel</string>
<string name="enter_url_api_key"><![CDATA[URL ve API Anahtarını Girin]]></string>
<string name="api_key">API Anahtarı</string>
<string name="seerr_login">Seerr sunucusuna giriş yap</string>
<string name="seerr_jellyfin_user">Jellyfin kullanıcısı</string>
<string name="seerr_local_user">Yerel kullanıcı</string>
<string name="search_and_download_subtitles"><![CDATA[Altyazı ara ve indir]]></string>
<string name="no_subtitles_found">Uzak altyazı bulunamadı</string>
<string name="series_continueing">Mevcut</string>
<plurals name="minutes">
<item quantity="one">%s dakika</item>
<item quantity="other">%s dakika</item>
</plurals>
<string name="start_after">Başlama süresi</string>
<string name="animate">Oynatma Süresi</string>
<string name="max_age_rating">Maksimum yaş sınırı</string>
<string name="no_max">Sınır yok</string>
<string name="for_all_ages">Genel İzleyici</string>
<string name="up_to_age">%s yaşa kadar</string>
<string name="screensaver">Ekran koruyucu</string>
<string name="screensaver_settings">Ekran koruyucu ayarları</string>
<string name="start_screensaver">Ekran koruyucuyu başlat</string>
<string name="duration">Süre</string>
<string name="photos">Fotoğraflar</string>
<string name="include_types">İçerik türleri</string>
<string name="include_types_summary">Görsel gösterilecek öğe türleri</string>
<string name="in_app_screensaver">Uygulama içi ekran koruyucuyu kullan</string>
<string name="actor">Aktör</string>
<string name="composer">Besteci</string>
<string name="writer">Yazar</string>
<string name="guest_star">Konuk sanatçı</string>
<string name="producer">Yapımcı</string>
<string name="conductor">Orkestra şefi</string>
<string name="lyricist">Söz yazarı</string>
<string name="arranger">Aranjör</string>
<string name="engineer">Mühendis</string>
<string name="mixer">Karıştırıcı</string>
<string name="creator">Yaratıcı</string>
<string name="artist">Sanatçı</string>
<string name="delete_item">Bu öğeyi silmek istediğinizden emin misiniz?</string>
<string name="show_media_management">Medya yönetim seçeneklerini göster</string>
<string name="search_for">Ara %s</string>
</resources>

View file

@ -432,6 +432,7 @@
<string name="clear_track_choices">Чіткий вибір доріжки</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>

View file

@ -150,7 +150,7 @@
<string name="font">字体</string>
<string name="background">背景</string>
<string name="success">成功</string>
<string name="official_rating">年龄分级</string>
<string name="official_rating">内容分级</string>
<string name="runtime_sort">播放时长</string>
<string name="extras">附加内容</string>
<plurals name="other_extras">
@ -208,7 +208,7 @@
<string name="check_for_updates">检查更新</string>
<string name="combine_continue_next_summary">仅适用于电视剧</string>
<string name="combine_continue_next"><![CDATA[合并继续观看与即将播放]]></string>
<string name="direct_play_ass">直接播放 ASS 字幕</string>
<string name="direct_play_ass">使用 libass 处理 ASS 字幕</string>
<string name="direct_play_pgs">直接播放 PGS 字幕</string>
<string name="downmix_stereo">始终降混为立体声</string>
<string name="ffmpeg_extension_pref">使用 FFmpeg 解码模块</string>
@ -351,6 +351,7 @@
<string name="only_forced_subtitles">仅强制字幕</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -446,4 +447,90 @@
<string name="display_presets_description">可快速设置所有行样式的内置预设</string>
<string name="customize_home_summary">选择首页上的行和图片</string>
<string name="favorite_items">收藏的 %s</string>
<string name="content_scale_fit">适应</string>
<string name="content_scale_crop">裁剪</string>
<string name="content_scale_fill">填充</string>
<string name="content_scale_fill_width">填充宽度</string>
<string name="content_scale_fill_height">填充高度</string>
<string name="display_preset_default">Wholphin 默认</string>
<string name="display_preset_compact">Wholphin 紧凑</string>
<string name="display_preset_series_thumb">剧集缩略图</string>
<string name="display_preset_episode_thumbnails">分集缩略图</string>
<string name="purple">紫色</string>
<string name="orange">橙色</string>
<string name="bold_blue">深蓝色</string>
<string name="black">黑色</string>
<string name="skip_ignore">忽略</string>
<string name="skip_automatically">自动跳过</string>
<string name="skip_ask">询问后跳过</string>
<string name="ffmpeg_fallback">仅在无内置解码器时使用 FFmpeg</string>
<string name="ffmpeg_prefer">优先使用 FFmpeg 解码器而非内置解码器</string>
<string name="ffmpeg_never">从不使用 FFmpeg 解码器</string>
<string name="next_up_playback_end">播放结束时</string>
<string name="next_up_outro">播到片尾时</string>
<string name="white">白色</string>
<string name="light_gray">浅灰色</string>
<string name="dark_gray">深灰色</string>
<string name="yellow">黄色</string>
<string name="cyan">青色</string>
<string name="magenta">紫红色</string>
<string name="subtitle_edge_outline">轮廓</string>
<string name="subtitle_edge_shadow">阴影</string>
<string name="prefer_mpv">优先使用 mpv</string>
<string name="player_backend_options_subtitles_prefer_mpv">HDR 播放使用 ExoPlayer</string>
<string name="aspect_ratios_poster">海报 (2:3)</string>
<string name="aspect_ratios_16_9">16:9</string>
<string name="aspect_ratios_4_3">4:3</string>
<string name="aspect_ratios_square">正方形 (1:1)</string>
<string name="image_type_primary">主图</string>
<string name="image_type_thumb">缩略图</string>
<string name="backdrop_style_dynamic">动态配色图片</string>
<string name="backdrop_style_image">仅图片</string>
<string name="enter_url_api_key"><![CDATA[输入 URL 与 API 密钥]]></string>
<string name="api_key">API 密钥</string>
<string name="seerr_login">登录 Seerr 服务器</string>
<string name="seerr_jellyfin_user">Jellyfin 用户</string>
<string name="seerr_local_user">本地用户</string>
<string name="search_and_download_subtitles"><![CDATA[搜索并下载字幕]]></string>
<string name="no_subtitles_found">未找到远程字幕</string>
<string name="volume_lowest">最低音量</string>
<string name="volume_low">低音量</string>
<string name="volume_medium">中等音量</string>
<string name="volume_high">高音量</string>
<string name="volume_full">最大音量</string>
<string name="background_style_wrap">环绕式</string>
<string name="background_style_boxed">框式</string>
<string name="series_continueing">当前</string>
<plurals name="minutes">
<item quantity="other">%s 分钟</item>
</plurals>
<string name="animate">动画</string>
<string name="start_after">启动时间</string>
<string name="max_age_rating">最高年龄分级</string>
<string name="no_max">无上限</string>
<string name="for_all_ages">所有年龄</string>
<string name="up_to_age">最高至 %s 岁</string>
<string name="screensaver">屏幕保护程序</string>
<string name="screensaver_settings">屏幕保护程序设置</string>
<string name="start_screensaver">启动屏幕保护程序</string>
<string name="duration">时长</string>
<string name="photos">照片</string>
<string name="include_types">包含类型</string>
<string name="include_types_summary">要显示图片的项目类型</string>
<string name="in_app_screensaver">使用应用内屏幕保护程序</string>
<string name="actor">演员</string>
<string name="composer">作曲</string>
<string name="writer">编剧</string>
<string name="guest_star">客串</string>
<string name="producer">制片</string>
<string name="conductor">指挥</string>
<string name="lyricist">作词</string>
<string name="arranger">编曲</string>
<string name="engineer">录音</string>
<string name="mixer">混音</string>
<string name="creator">主创</string>
<string name="artist">艺人</string>
<string name="show_media_management">显示媒体管理选项</string>
<string name="delete_item">是否确定要删除此项目?</string>
<string name="search_for">搜索 %s</string>
</resources>

View file

@ -59,7 +59,7 @@
<string name="tv_guide">指南</string>
<string name="updates">更新</string>
<string name="version">版本</string>
<string name="video_scale">畫面比例</string>
<string name="video_scale">畫面縮放比例</string>
<string name="video">影片</string>
<string name="videos">影片</string>
<string name="watch_live">觀看直播</string>
@ -104,7 +104,7 @@
<string name="direct_play_ass">直接播放 ASS 字幕</string>
<string name="direct_play_pgs">直接播放 PGS 字幕</string>
<string name="ffmpeg_extension_pref">使用 FFmpeg 解碼模組</string>
<string name="global_content_scale">預設畫面比例</string>
<string name="global_content_scale">預設縮放比例</string>
<string name="install_update">安裝更新</string>
<string name="installed_version">安裝版本</string>
<string name="max_homepage_items">首頁每列項目上限</string>
@ -349,6 +349,7 @@
<string name="no_trailers">無預告片</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -362,7 +363,7 @@
<string name="remove_seerr_server">移除 Seerr 伺服器</string>
<string name="seerr_server_added">已新增 Seerr 伺服器</string>
<string name="password">密碼</string>
<string name="username">使用者名稱</string>
<string name="username">帳號</string>
<string name="url">URL</string>
<string name="trending">當前趨勢</string>
<string name="upcoming_movies">即將上映的電影</string>
@ -427,18 +428,18 @@
<string name="suggestions_for">推薦的 %1$s</string>
<string name="increase_all_cards_size">放大所有海報尺寸</string>
<string name="decrease_all_cards_size">縮小所有海報尺寸</string>
<string name="use_thumb_images">使用橫縮圖</string>
<string name="use_thumb_images">使用橫縮圖</string>
<string name="add_row">新增一列</string>
<string name="apply_all_rows">套用到所有列</string>
<string name="home_rows">首頁各列</string>
<string name="add_row_for">為 %1$s 新增列項目</string>
<string name="display_presets_description">快速設定所有項目為內建預設樣式</string>
<string name="customize_home_summary">選擇首頁要顯示的項目及圖片樣式</string>
<string name="customize_home_summary">選擇首頁要顯示的項目及圖片樣式</string>
<string name="height">高度</string>
<string name="customize_home">自訂首頁</string>
<string name="load_from_server">從伺服器載入設定</string>
<string name="save_to_server">保存設定到伺服器</string>
<string name="load_from_web_client">從網頁版載入</string>
<string name="load_from_web_client">從網頁版載入設定</string>
<string name="use_series">使用劇集圖片</string>
<string name="overwrite_server_settings">覆蓋伺服器上的設定?</string>
<string name="overwrite_local_settings">覆蓋本地設定?</string>
@ -450,11 +451,11 @@
<string name="content_scale_fill">拉伸填滿</string>
<string name="content_scale_fill_width">填滿寬度</string>
<string name="content_scale_fill_height">填滿高度</string>
<string name="volume_lowest">最低</string>
<string name="volume_low"></string>
<string name="volume_medium">中等</string>
<string name="volume_high"></string>
<string name="volume_full">最大</string>
<string name="volume_lowest">最低音量</string>
<string name="volume_low">音量</string>
<string name="volume_medium">中等音量</string>
<string name="volume_high">音量</string>
<string name="volume_full">最大音量</string>
<string name="purple">紫色</string>
<string name="orange">橘色</string>
<string name="bold_blue">深藍色</string>
@ -472,10 +473,64 @@
<string name="dark_gray">深灰色</string>
<string name="yellow">黃色</string>
<string name="cyan">青色</string>
<string name="magenta">紅色</string>
<string name="magenta">紅色</string>
<string name="subtitle_edge_outline">外框</string>
<string name="subtitle_edge_shadow">陰影</string>
<string name="background_style_wrap">貼合文字</string>
<string name="background_style_boxed">長方底框</string>
<string name="prefer_mpv">優先使用 MPV</string>
<string name="player_backend_options_subtitles_prefer_mpv">僅 HDR 使用 ExoPlayer 播放</string>
<string name="favorite_items">我的最愛的 %s</string>
<string name="display_preset_default">Wholphin 預設樣式</string>
<string name="display_preset_compact">Wholphin 緊湊樣式</string>
<string name="display_preset_series_thumb">劇集縮圖樣式</string>
<string name="display_preset_episode_thumbnails">單集縮圖樣式</string>
<string name="aspect_ratios_poster">海報 (2:3)</string>
<string name="aspect_ratios_16_9">16:9</string>
<string name="aspect_ratios_4_3">4:3</string>
<string name="aspect_ratios_square">正方形 (1:1)</string>
<string name="image_type_primary">主封面</string>
<string name="image_type_thumb">橫式縮圖</string>
<string name="backdrop_style_dynamic">動態色彩背景</string>
<string name="backdrop_style_image">僅背景圖</string>
<string name="enter_url_api_key"><![CDATA[輸入 URL 及 API 金鑰]]></string>
<string name="api_key">API 金鑰</string>
<string name="seerr_login">登入 Seerr 伺服器</string>
<string name="seerr_jellyfin_user">Jellyfin 帳號</string>
<string name="seerr_local_user">Seerr 帳號</string>
<string name="search_and_download_subtitles"><![CDATA[搜尋和下載字幕]]></string>
<string name="no_subtitles_found">未找到遠端字幕</string>
<string name="series_continueing">至今</string>
<plurals name="minutes">
<item quantity="other">%s 分鐘</item>
</plurals>
<string name="start_after">啟動時間</string>
<string name="animate">動態效果</string>
<string name="max_age_rating">年齡分級上限</string>
<string name="no_max">不限制</string>
<string name="for_all_ages">適合所有年齡</string>
<string name="up_to_age">最高至 %s 歲</string>
<string name="screensaver">螢幕保護程式</string>
<string name="screensaver_settings">螢幕保護程式設定</string>
<string name="start_screensaver">立即開始</string>
<string name="duration">切換間隔</string>
<string name="photos">照片</string>
<string name="include_types">包含類型</string>
<string name="include_types_summary">要顯示圖片的類型</string>
<string name="in_app_screensaver">使用內建螢幕保護程式</string>
<string name="guest_star">客串演員</string>
<string name="actor">演員</string>
<string name="composer">作曲</string>
<string name="writer">編劇</string>
<string name="producer">製作人</string>
<string name="conductor">指揮</string>
<string name="lyricist">作詞</string>
<string name="arranger">編曲</string>
<string name="engineer">錄音師</string>
<string name="mixer">混音師</string>
<string name="creator">創作者</string>
<string name="artist">演出者</string>
<string name="delete_item">確定要刪除此項目嗎?</string>
<string name="show_media_management">顯示媒體管理選項</string>
<string name="search_for">搜尋 %s</string>
</resources>

View file

@ -451,6 +451,7 @@
<string name="clear_track_choices">Clear track choices</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dts_hd_ma">DTS:HD MA</string>
<string name="truehd">TrueHD</string>
<string name="dolby_digital">DD</string>
<string name="dolby_digital_plus">DD+</string>
@ -719,5 +720,6 @@
<string name="creator">Creator</string>
<string name="artist">Artist</string>
<string name="search_for">Search %s</string>
<string name="select_all">Select all</string>
</resources>

View file

@ -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

View file

@ -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"))
}
}

View file

@ -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)
}
}

View file

@ -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"
}

View file

@ -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"

Binary file not shown.

View file

@ -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

2
gradlew vendored
View file

@ -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/.

View file

@ -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