mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Merge branch 'main' into fea/libass-android-support
This commit is contained in:
commit
a126869a09
69 changed files with 3079 additions and 870 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -771,6 +771,9 @@ fun CollectionFolderGrid(
|
|||
onClickDelete = {
|
||||
showDeleteDialog = PositionItem(position, item)
|
||||
},
|
||||
onClickGoTo = {
|
||||
onClickItem.invoke(position, it)
|
||||
},
|
||||
),
|
||||
),
|
||||
onDismissRequest = { moreDialog.makeAbsent() },
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
131
app/src/main/res/values-ar/strings.xml
Normal file
131
app/src/main/res/values-ar/strings.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 l’arrière-plan</string>
|
||||
<string name="background_style">Style de l’arriè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 l’aperç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 l’intro</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 l’intro</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 d’orchestra</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">Açı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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
475
app/src/test/resources/release_develop.json
Normal file
475
app/src/test/resources/release_develop.json
Normal 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"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue