Merge branch 'main' into fea/music

This commit is contained in:
Damontecres 2026-03-06 18:38:17 -05:00
commit 8ddd4bd747
No known key found for this signature in database
23 changed files with 1547 additions and 715 deletions

View file

@ -231,7 +231,6 @@ dependencies {
implementation(libs.androidx.media3.exoplayer.dash)
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.ui.compose)
implementation(libs.ass.media)
implementation(libs.coil.core)
implementation(libs.coil.compose)

View file

@ -251,7 +251,7 @@ suspend fun upgradeApp(
}
}
if (previous.isEqualOrBefore(Version.fromString("0.5.0-6-g0"))) {
if (previous.isEqualOrBefore(Version.fromString("0.5.3-0-g0"))) {
appPreferences.updateData {
it.updateScreensaverPreferences {
startDelay = ScreensaverPreference.DEFAULT_START_DELAY

View file

@ -10,28 +10,22 @@ import androidx.datastore.core.DataStore
import androidx.media3.common.C
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.Renderer
import androidx.media3.exoplayer.RenderersFactory
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer
import androidx.media3.exoplayer.video.VideoRendererEventListener
import androidx.media3.extractor.DefaultExtractorsFactory
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
import com.github.damontecres.wholphin.preferences.PlaybackPreferences
import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.peerless2012.ass.media.AssHandler
import io.github.peerless2012.ass.media.factory.AssRenderersFactory
import io.github.peerless2012.ass.media.kt.withAssMkvSupport
import io.github.peerless2012.ass.media.parser.AssSubtitleParserFactory
import io.github.peerless2012.ass.media.type.AssRenderType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.lang.reflect.Constructor
@ -52,17 +46,71 @@ class PlayerFactory
var currentPlayer: Player? = null
private set
fun createVideoPlayer(): Player {
if (currentPlayer?.isReleased == false) {
Timber.w("Player was not released before trying to create a new one!")
currentPlayer?.release()
}
val prefs = runBlocking { appPreferences.data.firstOrNull()?.playbackPreferences }
val backend = prefs?.playerBackend ?: AppPreference.PlayerBackendPref.defaultValue
val newPlayer =
when (backend) {
PlayerBackend.PREFER_MPV,
PlayerBackend.MPV,
-> {
val enableHardwareDecoding =
prefs?.mpvOptions?.enableHardwareDecoding
?: AppPreference.MpvHardwareDecoding.defaultValue
val useGpuNext =
prefs?.mpvOptions?.useGpuNext
?: AppPreference.MpvGpuNext.defaultValue
MpvPlayer(context, enableHardwareDecoding, useGpuNext)
.apply {
playWhenReady = true
}
}
PlayerBackend.EXO_PLAYER,
PlayerBackend.UNRECOGNIZED,
-> {
val extensions = prefs?.overrides?.mediaExtensionsEnabled
val decodeAv1 = prefs?.overrides?.decodeAv1 == true
Timber.v("extensions=$extensions")
val rendererMode =
when (extensions) {
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
}
ExoPlayer
.Builder(context)
.setRenderersFactory(
WholphinRenderersFactory(context, decodeAv1)
.setEnableDecoderFallback(true)
.setExtensionRendererMode(rendererMode),
).build()
.apply {
playWhenReady = true
}
}
}
currentPlayer = newPlayer
return newPlayer
}
suspend fun createVideoPlayer(
backend: PlayerBackend,
prefs: PlaybackPreferences,
): PlayerCreation {
): Player {
withContext(Dispatchers.Main) {
if (currentPlayer?.isReleased == false) {
Timber.w("Player was not released before trying to create a new one!")
currentPlayer?.release()
}
}
var assHandler: AssHandler? = null
val newPlayer =
when (backend) {
PlayerBackend.PREFER_MPV,
@ -77,9 +125,8 @@ class PlayerFactory
PlayerBackend.UNRECOGNIZED,
-> {
val extensions = prefs.overrides.mediaExtensionsEnabled
val directPlayAss = prefs.overrides.directPlayAss
val decodeAv1 = prefs.overrides.decodeAv1
Timber.v("extensions=$extensions, directPlayAss=$directPlayAss")
Timber.v("extensions=$extensions")
val rendererMode =
when (extensions) {
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
@ -87,42 +134,17 @@ class PlayerFactory
MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
}
val dataSourceFactory = DefaultDataSource.Factory(context)
val extractorsFactory = DefaultExtractorsFactory()
var renderersFactory: RenderersFactory =
WholphinRenderersFactory(context, decodeAv1)
.setEnableDecoderFallback(true)
.setExtensionRendererMode(rendererMode)
val mediaSourceFactory =
if (directPlayAss) {
assHandler = AssHandler(AssRenderType.OVERLAY_OPEN_GL)
val assSubtitleParserFactory = AssSubtitleParserFactory(assHandler)
renderersFactory = AssRenderersFactory(assHandler, renderersFactory)
DefaultMediaSourceFactory(
dataSourceFactory,
extractorsFactory.withAssMkvSupport(
assSubtitleParserFactory,
assHandler,
),
).setSubtitleParserFactory(assSubtitleParserFactory)
} else {
DefaultMediaSourceFactory(
dataSourceFactory,
extractorsFactory,
)
}
ExoPlayer
.Builder(context)
.setMediaSourceFactory(mediaSourceFactory)
.setRenderersFactory(renderersFactory)
.build()
.apply {
assHandler?.init(this)
}
.setRenderersFactory(
WholphinRenderersFactory(context, decodeAv1)
.setEnableDecoderFallback(true)
.setExtensionRendererMode(rendererMode),
).build()
}
}
currentPlayer = newPlayer
return PlayerCreation(newPlayer, assHandler)
return newPlayer
}
}
@ -135,11 +157,6 @@ val Player.isReleased: Boolean
}
}
data class PlayerCreation(
val player: Player,
val assHandler: AssHandler? = null,
)
// Code is adapted from https://github.com/androidx/media/blob/release/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java#L436
class WholphinRenderersFactory(
context: Context,

View file

@ -161,27 +161,32 @@ class ScreensaverService
if (pager.isEmpty()) {
emit(null)
} else {
val duration =
userPreferencesService
.getCurrent()
.appPreferences
.interfacePreferences.screensaverPreference.duration.milliseconds
while (true) {
val item = pager.getBlocking(index)
Timber.v("Next index=%s, item=%s", index, item?.id)
if (item != null) {
val backdropUrl =
if (item.type == BaseItemKind.PHOTO) {
api.libraryApi.getDownloadUrl(item.id)
} else {
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
}
val title =
if (item.type == BaseItemKind.PHOTO) {
item.data.premiereDate?.let {
formatDate(it.toLocalDate())
try {
val item = pager.getBlocking(index)
Timber.v("Next index=%s, item=%s", index, item?.id)
if (item != null) {
val backdropUrl =
if (item.type == BaseItemKind.PHOTO) {
api.libraryApi.getDownloadUrl(item.id)
} else {
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
}
} else {
item.title
}
val logoUrl = imageUrlService.getItemImageUrl(item, ImageType.LOGO)
if (backdropUrl != null) {
try {
val title =
if (item.type == BaseItemKind.PHOTO) {
item.data.premiereDate?.let {
formatDate(it.toLocalDate())
}
} else {
item.title
}
val logoUrl = imageUrlService.getItemImageUrl(item, ImageType.LOGO)
if (backdropUrl != null) {
context.imageLoader
.enqueue(
ImageRequest
@ -191,18 +196,17 @@ class ScreensaverService
).job
.await()
emit(CurrentItem(item, backdropUrl, logoUrl, title ?: ""))
} catch (_: CancellationException) {
break
delay(duration)
}
val duration =
userPreferencesService
.getCurrent()
.appPreferences
.interfacePreferences.screensaverPreference.duration.milliseconds
delay(duration)
}
} catch (_: CancellationException) {
break
} catch (ex: Exception) {
Timber.e(ex, "Error fetching next item")
delay(duration)
}
index++
if (index > pager.lastIndex) index = 0
}
}
}.flowOn(Dispatchers.Default).cancellable()

View file

@ -1,8 +1,5 @@
package com.github.damontecres.wholphin.ui.playback
import android.view.Gravity
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.compose.BackHandler
import androidx.annotation.Dimension
import androidx.annotation.OptIn
@ -45,18 +42,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.view.children
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.SubtitleView
import androidx.media3.ui.compose.PlayerSurface
@ -88,7 +83,6 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.Media3SubtitleOverride
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
import io.github.peerless2012.ass.media.widget.AssSubtitleView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -133,7 +127,8 @@ fun PlaybackPage(
LoadingState.Success -> {
val playerState by viewModel.currentPlayer.collectAsState()
PlaybackPageContent(
playerState = playerState!!,
player = playerState!!.player,
playerBackend = playerState!!.backend,
preferences = preferences,
destination = destination,
viewModel = viewModel,
@ -146,15 +141,13 @@ fun PlaybackPage(
@OptIn(UnstableApi::class)
@Composable
fun PlaybackPageContent(
playerState: PlayerState,
player: Player,
playerBackend: PlayerBackend,
preferences: UserPreferences,
destination: Destination,
modifier: Modifier = Modifier,
viewModel: PlaybackViewModel,
) {
val player = playerState.player
val playerBackend = playerState.backend
val prefs = preferences.appPreferences.playbackPreferences
val scope = rememberCoroutineScope()
val configuration = LocalConfiguration.current
@ -327,14 +320,10 @@ fun PlaybackPageContent(
.focusRequester(focusRequester)
.focusable(),
) {
var playerSurfaceSize by remember { mutableStateOf(IntSize.Zero) }
PlayerSurface(
player = player,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier =
scaledModifier.onSizeChanged {
playerSurfaceSize = it
},
modifier = scaledModifier,
)
if (presentationState.coverSurface) {
Box(
@ -427,7 +416,7 @@ fun PlaybackPageContent(
remember(subtitleSettings) { subtitleSettings.imageSubtitleOpacity / 100f }
// Subtitles
if (skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled && !presentationState.coverSurface) {
if (skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled) {
val maxSize by animateFloatAsState(if (controllerViewState.controlsVisible) .7f else 1f)
val isImageSubtitles = remember(cues) { cues.firstOrNull()?.bitmap != null }
AndroidView(
@ -438,42 +427,12 @@ fun PlaybackPageContent(
setFixedTextSize(Dimension.SP, it.fontSize.toFloat())
setBottomPaddingFraction(it.margin.toFloat() / 100f)
}
playerState.assHandler?.let { assHandler ->
if (prefs.overrides.directPlayAss) {
Timber.v("Adding AssSubtitleView")
addView(
AssSubtitleView(context, assHandler).apply {
layoutParams =
FrameLayout
.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
).apply { gravity = Gravity.CENTER }
},
)
}
}
}
},
update = {
it.setCues(cues)
Media3SubtitleOverride(subtitleSettings.calculateEdgeSize(density))
.apply(it)
it.children.firstOrNull { it is AssSubtitleView }?.let {
(it as? AssSubtitleView)?.apply {
val resized =
layoutParams.let { it.width != playerSurfaceSize.width || it.height != playerSurfaceSize.height }
if (resized) {
Timber.v("Resizing AssSubtitleView: $playerSurfaceSize")
layoutParams =
FrameLayout
.LayoutParams(
playerSurfaceSize.width,
playerSurfaceSize.height,
).apply { gravity = Gravity.CENTER }
}
}
}
},
onReset = {
it.setCues(null)

View file

@ -74,7 +74,6 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.peerless2012.ass.media.AssHandler
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -221,8 +220,7 @@ class PlaybackViewModel
isHdr: Boolean,
is4k: Boolean,
) {
val softwareDecoding =
!preferences.appPreferences.playbackPreferences.mpvOptions.enableHardwareDecoding
val softwareDecoding = !preferences.appPreferences.playbackPreferences.mpvOptions.enableHardwareDecoding
val playerBackend =
when (preferences.appPreferences.playbackPreferences.playerBackend) {
PlayerBackend.UNRECOGNIZED,
@ -241,14 +239,13 @@ class PlaybackViewModel
disconnectPlayer()
}
val playerCreation =
player =
playerFactory.createVideoPlayer(
playerBackend,
preferences.appPreferences.playbackPreferences,
)
this.player = playerCreation.player
currentPlayer.update {
PlayerState(playerCreation.player, playerBackend, playerCreation.assHandler)
PlayerState(player, playerBackend)
}
configurePlayer()
}
@ -1444,7 +1441,6 @@ class PlaybackViewModel
data class PlayerState(
val player: Player,
val backend: PlayerBackend,
val assHandler: AssHandler?,
)
data class MediaSegmentState(

View file

@ -67,7 +67,6 @@ import com.github.damontecres.wholphin.ui.playback.isDirectionalDpad
import com.github.damontecres.wholphin.ui.playback.isDpad
import com.github.damontecres.wholphin.ui.playback.isEnterKey
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState
import org.jellyfin.sdk.model.api.MediaType
import timber.log.Timber
import kotlin.math.abs
@ -89,429 +88,413 @@ fun SlideshowPage(
),
) {
val context = LocalContext.current
val loading by viewModel.loading.collectAsState()
when (val st = loading) {
is LoadingState.Error -> {
ErrorMessage(st, modifier)
val loadingState by viewModel.loadingState.observeAsState(ImageLoadingState.Loading)
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()
var zoomFactor by rememberSaveable { mutableFloatStateOf(1f) }
val isZoomed = zoomFactor * 100 > 102
var rotation by rememberSaveable { mutableFloatStateOf(0f) }
var showOverlay by rememberSaveable { mutableStateOf(false) }
var showFilterDialog by rememberSaveable { mutableStateOf(false) }
var panX by rememberSaveable { mutableFloatStateOf(0f) }
var panY by rememberSaveable { mutableFloatStateOf(0f) }
val slideshowControls =
object : SlideshowControls {
override fun startSlideshow() {
showOverlay = false
viewModel.startSlideshow()
}
override fun stopSlideshow() {
viewModel.stopSlideshow()
}
}
LoadingState.Loading,
LoadingState.Pending,
-> {
LoadingPage(modifier)
val rotateAnimation: Float by animateFloatAsState(
targetValue = rotation,
label = "image_rotation",
)
val zoomAnimation: Float by animateFloatAsState(
targetValue = zoomFactor,
label = "image_zoom",
)
val panXAnimation: Float by animateFloatAsState(
targetValue = panX,
label = "image_panX",
)
val panYAnimation: Float by animateFloatAsState(
targetValue = panY,
label = "image_panY",
)
val slideshowState by viewModel.slideshow.collectAsState()
val slideshowActive by viewModel.slideshowActive.collectAsState(false)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.tryRequestFocus()
}
val density = LocalDensity.current
val screenHeight = LocalWindowInfo.current.containerSize.height
val screenWidth = LocalWindowInfo.current.containerSize.width
val maxPanX = screenWidth * .75f
val maxPanY = screenHeight * .75f
fun reset(resetRotate: Boolean) {
zoomFactor = 1f
panX = 0f
panY = 0f
if (resetRotate) rotation = 0f
}
fun pan(
xFactor: Int,
yFactor: Int,
) {
if (xFactor != 0) {
panX = (panX + with(density) { xFactor.dp.toPx() }).coerceIn(-maxPanX, maxPanX)
}
if (yFactor != 0) {
panY = (panY + with(density) { yFactor.dp.toPx() }).coerceIn(-maxPanY, maxPanY)
}
}
LoadingState.Success -> {
val loadingState by viewModel.loadingState.observeAsState(ImageLoadingState.Loading)
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()
fun zoom(factor: Float) {
if (factor < 0) {
val diffFactor = factor / (zoomFactor - 1f)
// zooming out
val panXDiff = abs(panX * diffFactor)
val panYDiff = abs(panY * diffFactor)
if (DEBUG) {
Timber.d(
"zoomFactor=$zoomFactor, factor=$factor, panX=$panX, panY=$panY, panXDiff=$panXDiff, panYDiff=$panYDiff",
)
}
if (panX > 0f) {
panX -= panXDiff
} else if (panX < 0f) {
panX += panXDiff
}
if (panY > 0f) {
panY -= panYDiff
} else if (panY < 0f) {
panY += panYDiff
}
}
zoomFactor = (zoomFactor + factor).coerceIn(1f, 5f)
if (!isZoomed) {
// Always reset if not zoomed
panX = 0f
panY = 0f
}
}
var zoomFactor by rememberSaveable { mutableFloatStateOf(1f) }
val isZoomed = zoomFactor * 100 > 102
var rotation by rememberSaveable { mutableFloatStateOf(0f) }
var showOverlay by rememberSaveable { mutableStateOf(false) }
var showFilterDialog by rememberSaveable { mutableStateOf(false) }
var panX by rememberSaveable { mutableFloatStateOf(0f) }
var panY by rememberSaveable { mutableFloatStateOf(0f) }
LaunchedEffect(imageState) {
reset(true)
}
val player = viewModel.player
val presentationState = rememberPresentationState(player)
LaunchedEffect(slideshowActive) {
player.repeatMode =
if (slideshowState.enabled) Player.REPEAT_MODE_OFF else Player.REPEAT_MODE_ONE
}
val slideshowControls =
object : SlideshowControls {
override fun startSlideshow() {
showOverlay = false
viewModel.startSlideshow()
}
var longPressing by remember { mutableStateOf(false) }
override fun stopSlideshow() {
viewModel.stopSlideshow()
}
}
val rotateAnimation: Float by animateFloatAsState(
targetValue = rotation,
label = "image_rotation",
)
val zoomAnimation: Float by animateFloatAsState(
targetValue = zoomFactor,
label = "image_zoom",
)
val panXAnimation: Float by animateFloatAsState(
targetValue = panX,
label = "image_panX",
)
val panYAnimation: Float by animateFloatAsState(
targetValue = panY,
label = "image_panY",
val contentModifier =
Modifier
.clickable(
interactionSource = null,
indication = null,
onClick = {
showOverlay = !showOverlay
},
)
val slideshowState by viewModel.slideshow.collectAsState()
val slideshowActive by viewModel.slideshowActive.collectAsState(false)
Box(
modifier =
modifier
.background(Color.Black)
.focusRequester(focusRequester)
.focusable()
.onKeyEvent {
val isOverlayShowing = showOverlay || showFilterDialog
var result = false
if (!isOverlayShowing) {
if (longPressing && it.type == KeyEventType.KeyUp) {
// User stopped long pressing, so cancel the zooming action, but still consume the event so it doesn't move the image
longPressing = false
return@onKeyEvent true
}
longPressing =
it.nativeKeyEvent.isLongPress ||
it.nativeKeyEvent.repeatCount > 0
if (longPressing) {
when (it.key) {
Key.DirectionUp -> zoom(.05f)
Key.DirectionDown -> zoom(-.05f)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.tryRequestFocus()
}
val density = LocalDensity.current
val screenHeight = LocalWindowInfo.current.containerSize.height
val screenWidth = LocalWindowInfo.current.containerSize.width
val maxPanX = screenWidth * .75f
val maxPanY = screenHeight * .75f
fun reset(resetRotate: Boolean) {
zoomFactor = 1f
panX = 0f
panY = 0f
if (resetRotate) rotation = 0f
}
fun pan(
xFactor: Int,
yFactor: Int,
) {
if (xFactor != 0) {
panX = (panX + with(density) { xFactor.dp.toPx() }).coerceIn(-maxPanX, maxPanX)
}
if (yFactor != 0) {
panY = (panY + with(density) { yFactor.dp.toPx() }).coerceIn(-maxPanY, maxPanY)
}
}
fun zoom(factor: Float) {
if (factor < 0) {
val diffFactor = factor / (zoomFactor - 1f)
// zooming out
val panXDiff = abs(panX * diffFactor)
val panYDiff = abs(panY * diffFactor)
if (DEBUG) {
Timber.d(
"zoomFactor=$zoomFactor, factor=$factor, panX=$panX, panY=$panY, panXDiff=$panXDiff, panYDiff=$panYDiff",
)
}
if (panX > 0f) {
panX -= panXDiff
} else if (panX < 0f) {
panX += panXDiff
}
if (panY > 0f) {
panY -= panYDiff
} else if (panY < 0f) {
panY += panYDiff
}
}
zoomFactor = (zoomFactor + factor).coerceIn(1f, 5f)
if (!isZoomed) {
// Always reset if not zoomed
panX = 0f
panY = 0f
}
}
LaunchedEffect(imageState) {
reset(true)
}
val player = viewModel.player
val presentationState = rememberPresentationState(player)
LaunchedEffect(slideshowActive) {
player.repeatMode =
if (slideshowState.enabled) Player.REPEAT_MODE_OFF else Player.REPEAT_MODE_ONE
}
var longPressing by remember { mutableStateOf(false) }
val contentModifier =
Modifier
.clickable(
interactionSource = null,
indication = null,
onClick = {
showOverlay = !showOverlay
},
)
Box(
modifier =
modifier
.background(Color.Black)
.focusRequester(focusRequester)
.focusable()
.onKeyEvent {
val isOverlayShowing = showOverlay || showFilterDialog
var result = false
if (!isOverlayShowing) {
if (longPressing && it.type == KeyEventType.KeyUp) {
// User stopped long pressing, so cancel the zooming action, but still consume the event so it doesn't move the image
longPressing = false
return@onKeyEvent true
}
longPressing =
it.nativeKeyEvent.isLongPress ||
it.nativeKeyEvent.repeatCount > 0
if (longPressing) {
when (it.key) {
Key.DirectionUp -> zoom(.05f)
Key.DirectionDown -> zoom(-.05f)
// These work, but feel awkward because Up/Down zoom, so you can't long press them to pan
// Key.DirectionLeft -> panX += with(density) { 15.dp.toPx() }
// Key.DirectionRight -> panX -= with(density) { 15.dp.toPx() }
}
return@onKeyEvent true
}
}
if (it.type != KeyEventType.KeyUp) {
result = false
} else if (!isOverlayShowing && isZoomed && isDirectionalDpad(it)) {
// Image is zoomed in
when (it.key) {
Key.DirectionLeft -> pan(30, 0)
Key.DirectionRight -> pan(-30, 0)
Key.DirectionUp -> pan(0, 30)
Key.DirectionDown -> pan(0, -30)
}
result = true
} else if (!isOverlayShowing && isZoomed && it.key == Key.Back) {
reset(false)
result = true
} else if (!isOverlayShowing && (it.key == Key.DirectionLeft || it.key == Key.DirectionRight)) {
when (it.key) {
Key.DirectionLeft, Key.DirectionUpLeft, Key.DirectionDownLeft -> {
if (!viewModel.previousImage()) {
Toast
.makeText(
context,
R.string.slideshow_at_beginning,
Toast.LENGTH_SHORT,
).show()
}
}
Key.DirectionRight, Key.DirectionUpRight, Key.DirectionDownRight -> {
if (!viewModel.nextImage()) {
Toast
.makeText(
context,
R.string.no_more_images,
Toast.LENGTH_SHORT,
).show()
}
}
}
} else if (isOverlayShowing && it.key == Key.Back) {
showOverlay = false
viewModel.unpauseSlideshow()
result = true
} else if (!isOverlayShowing && (isDpad(it) || isEnterKey(it))) {
showOverlay = true
viewModel.pauseSlideshow()
result = true
}
if (result) {
// Handled the key, so reset the slideshow timer
viewModel.pulseSlideshow()
}
result
},
) {
when (loadingState) {
ImageLoadingState.Error -> {
ErrorMessage("Error loading image", null, modifier)
}
ImageLoadingState.Loading -> {
LoadingPage(modifier)
}
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 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),
)
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()
},
)
// These work, but feel awkward because Up/Down zoom, so you can't long press them to pan
// Key.DirectionLeft -> panX += with(density) { 15.dp.toPx() }
// Key.DirectionRight -> panX -= with(density) { 15.dp.toPx() }
}
return@onKeyEvent true
}
}
}
AnimatedVisibility(
showOverlay,
enter = slideInVertically { it },
exit = slideOutVertically { it },
modifier = Modifier.align(Alignment.BottomStart),
) {
imageState?.let { imageState ->
ImageOverlay(
if (it.type != KeyEventType.KeyUp) {
result = false
} else if (!isOverlayShowing && isZoomed && isDirectionalDpad(it)) {
// Image is zoomed in
when (it.key) {
Key.DirectionLeft -> pan(30, 0)
Key.DirectionRight -> pan(-30, 0)
Key.DirectionUp -> pan(0, 30)
Key.DirectionDown -> pan(0, -30)
}
result = true
} else if (!isOverlayShowing && isZoomed && it.key == Key.Back) {
reset(false)
result = true
} else if (!isOverlayShowing && (it.key == Key.DirectionLeft || it.key == Key.DirectionRight)) {
when (it.key) {
Key.DirectionLeft, Key.DirectionUpLeft, Key.DirectionDownLeft -> {
if (!viewModel.previousImage()) {
Toast
.makeText(
context,
R.string.slideshow_at_beginning,
Toast.LENGTH_SHORT,
).show()
}
}
Key.DirectionRight, Key.DirectionUpRight, Key.DirectionDownRight -> {
if (!viewModel.nextImage()) {
Toast
.makeText(
context,
R.string.no_more_images,
Toast.LENGTH_SHORT,
).show()
}
}
}
} else if (isOverlayShowing && it.key == Key.Back) {
showOverlay = false
viewModel.unpauseSlideshow()
result = true
} else if (!isOverlayShowing && (isDpad(it) || isEnterKey(it))) {
showOverlay = true
viewModel.pauseSlideshow()
result = true
}
if (result) {
// Handled the key, so reset the slideshow timer
viewModel.pulseSlideshow()
}
result
},
) {
when (loadingState) {
ImageLoadingState.Error -> {
ErrorMessage("Error loading image", null, modifier)
}
ImageLoadingState.Loading -> {
LoadingPage(modifier)
}
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 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),
)
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
.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()
.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()
},
)
}
}
AnimatedVisibility(showFilterDialog) {
ImageFilterDialog(
filter = imageFilter,
showVideoOptions = false,
showSaveGalleryButton = true,
onChange = viewModel::updateImageFilter,
onClickSave = viewModel::saveImageFilter,
onClickSaveGallery = viewModel::saveGalleryFilter,
onDismissRequest = {
showFilterDialog = false
viewModel.unpauseSlideshow()
viewModel.pulseSlideshow()
},
)
}
}
}
AnimatedVisibility(
showOverlay,
enter = slideInVertically { it },
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()
},
)
}
}
AnimatedVisibility(showFilterDialog) {
ImageFilterDialog(
filter = imageFilter,
showVideoOptions = false,
showSaveGalleryButton = true,
onChange = viewModel::updateImageFilter,
onClickSave = viewModel::saveImageFilter,
onClickSaveGallery = viewModel::saveGalleryFilter,
onDismissRequest = {
showFilterDialog = false
viewModel.unpauseSlideshow()
viewModel.pulseSlideshow()
},
)
}
}
}

View file

@ -16,7 +16,6 @@ import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.PlaybackEffect
import com.github.damontecres.wholphin.data.model.VideoFilter
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.PlayerFactory
import com.github.damontecres.wholphin.services.ScreensaverService
@ -31,7 +30,6 @@ import com.github.damontecres.wholphin.ui.util.ThrottledLiveData
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.LoadingState
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -79,8 +77,9 @@ class SlideshowViewModel
fun create(slideshow: Destination.Slideshow): SlideshowViewModel
}
lateinit var player: Player
private set
val player by lazy {
playerFactory.createVideoPlayer()
}
private var saveFilters = true
@ -105,8 +104,6 @@ class SlideshowViewModel
private val _image = MutableLiveData<ImageState>()
val image: LiveData<ImageState> = _image
val loading = MutableStateFlow<LoadingState>(LoadingState.Pending)
val loadingState = MutableLiveData<ImageLoadingState>(ImageLoadingState.Loading)
private val _imageFilter = MutableLiveData(VideoFilter())
val imageFilter = ThrottledLiveData(_imageFilter, 500L)
@ -116,67 +113,58 @@ class SlideshowViewModel
init {
addCloseable {
screensaverService.keepScreenOn(false)
if (this@SlideshowViewModel::player.isInitialized) {
player.removeListener(this@SlideshowViewModel)
player.release()
}
player.removeListener(this@SlideshowViewModel)
player.release()
}
player.addListener(this@SlideshowViewModel)
viewModelScope.launchIO {
try {
val appPreferences = userPreferencesService.getCurrent().appPreferences
val playerCreation =
playerFactory.createVideoPlayer(
backend = PlayerBackend.EXO_PLAYER,
appPreferences.playbackPreferences,
)
player = playerCreation.player
player.addListener(this@SlideshowViewModel)
val photoPrefs = appPreferences.photoPreferences
slideshowDelay =
photoPrefs.slideshowDuration.takeIf { it >= AppPreference.SlideshowDuration.min }
?: AppPreference.SlideshowDuration.defaultValue
val includeItemTypes =
if (photoPrefs.slideshowPlayVideos) {
listOf(BaseItemKind.PHOTO, BaseItemKind.VIDEO)
} else {
listOf(BaseItemKind.PHOTO)
}
val request =
slideshowSettings.filter.filter.applyTo(
GetItemsRequest(
parentId = slideshowSettings.parentId,
includeItemTypes = includeItemTypes,
fields = PhotoItemFields,
recursive = true,
sortBy = listOf(slideshowSettings.sortAndDirection.sort),
sortOrder = listOf(slideshowSettings.sortAndDirection.direction),
),
)
serverRepository.currentUser.value?.let { user ->
val filter =
playbackEffectDao
.getPlaybackEffect(
user.rowId,
slideshowSettings.parentId,
BaseItemKind.PHOTO_ALBUM,
)?.videoFilter
if (filter != null) {
Timber.v("Got filter for album %s", slideshowSettings.parentId)
albumImageFilter = filter
}
val photoPrefs = userPreferencesService.getCurrent().appPreferences.photoPreferences
slideshowDelay =
photoPrefs.slideshowDuration.takeIf { it >= AppPreference.SlideshowDuration.min }
?: AppPreference.SlideshowDuration.defaultValue
// val album =
// api.userLibraryApi
// .getItem(
// itemId = slideshowSettings.parentId,
// ).content
// .let { BaseItem(it, false) }
// this@SlideshowViewModel.album.setValueOnMain(album)
val includeItemTypes =
if (photoPrefs.slideshowPlayVideos) {
listOf(BaseItemKind.PHOTO, BaseItemKind.VIDEO)
} else {
listOf(BaseItemKind.PHOTO)
}
val request =
slideshowSettings.filter.filter.applyTo(
GetItemsRequest(
parentId = slideshowSettings.parentId,
includeItemTypes = includeItemTypes,
fields = PhotoItemFields,
recursive = true,
sortBy = listOf(slideshowSettings.sortAndDirection.sort),
sortOrder = listOf(slideshowSettings.sortAndDirection.direction),
),
)
serverRepository.currentUser.value?.let { user ->
val filter =
playbackEffectDao
.getPlaybackEffect(
user.rowId,
slideshowSettings.parentId,
BaseItemKind.PHOTO_ALBUM,
)?.videoFilter
if (filter != null) {
Timber.v("Got filter for album %s", slideshowSettings.parentId)
albumImageFilter = filter
}
val pager =
ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
.init(slideshowSettings.index)
this@SlideshowViewModel._pager.setValueOnMain(pager)
loading.update { LoadingState.Success }
updatePosition(slideshowSettings.index)?.join()
if (slideshowSettings.startSlideshow) onMain { startSlideshow() }
} catch (ex: Exception) {
Timber.e(ex, "Error")
loading.update { LoadingState.Error(ex) }
}
val pager =
ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
.init(slideshowSettings.index)
this@SlideshowViewModel._pager.setValueOnMain(pager)
updatePosition(slideshowSettings.index)?.join()
if (slideshowSettings.startSlideshow) onMain { startSlideshow() }
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -59,7 +59,7 @@
<string name="tv_guide">指南</string>
<string name="updates">更新</string>
<string name="version">版本</string>
<string name="video_scale">畫面比例</string>
<string name="video_scale">畫面縮放比例</string>
<string name="video">影片</string>
<string name="videos">影片</string>
<string name="watch_live">觀看直播</string>
@ -104,7 +104,7 @@
<string name="direct_play_ass">直接播放 ASS 字幕</string>
<string name="direct_play_pgs">直接播放 PGS 字幕</string>
<string name="ffmpeg_extension_pref">使用 FFmpeg 解碼模組</string>
<string name="global_content_scale">預設畫面比例</string>
<string name="global_content_scale">預設縮放比例</string>
<string name="install_update">安裝更新</string>
<string name="installed_version">安裝版本</string>
<string name="max_homepage_items">首頁每列項目上限</string>
@ -362,7 +362,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 +427,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 +450,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 +472,64 @@
<string name="dark_gray">深灰色</string>
<string name="yellow">黃色</string>
<string name="cyan">青色</string>
<string name="magenta">紅色</string>
<string name="magenta">紅色</string>
<string name="subtitle_edge_outline">外框</string>
<string name="subtitle_edge_shadow">陰影</string>
<string name="background_style_wrap">貼合文字</string>
<string name="background_style_boxed">長方底框</string>
<string name="prefer_mpv">優先使用 MPV</string>
<string name="player_backend_options_subtitles_prefer_mpv">僅 HDR 使用 ExoPlayer 播放</string>
<string name="favorite_items">我的最愛的 %s</string>
<string name="display_preset_default">Wholphin 預設樣式</string>
<string name="display_preset_compact">Wholphin 緊湊樣式</string>
<string name="display_preset_series_thumb">劇集縮圖樣式</string>
<string name="display_preset_episode_thumbnails">單集縮圖樣式</string>
<string name="aspect_ratios_poster">海報 (2:3)</string>
<string name="aspect_ratios_16_9">16:9</string>
<string name="aspect_ratios_4_3">4:3</string>
<string name="aspect_ratios_square">正方形 (1:1)</string>
<string name="image_type_primary">主封面</string>
<string name="image_type_thumb">橫式縮圖</string>
<string name="backdrop_style_dynamic">動態色彩背景</string>
<string name="backdrop_style_image">僅背景圖</string>
<string name="enter_url_api_key"><![CDATA[輸入 URL 及 API 金鑰]]></string>
<string name="api_key">API 金鑰</string>
<string name="seerr_login">登入 Seerr 伺服器</string>
<string name="seerr_jellyfin_user">Jellyfin 帳號</string>
<string name="seerr_local_user">Seerr 帳號</string>
<string name="search_and_download_subtitles"><![CDATA[搜尋和下載字幕]]></string>
<string name="no_subtitles_found">未找到遠端字幕</string>
<string name="series_continueing">至今</string>
<plurals name="minutes">
<item quantity="other">%s 分鐘</item>
</plurals>
<string name="start_after">啟動時間</string>
<string name="animate">動態效果</string>
<string name="max_age_rating">年齡分級上限</string>
<string name="no_max">不限制</string>
<string name="for_all_ages">適合所有年齡</string>
<string name="up_to_age">最高至 %s 歲</string>
<string name="screensaver">螢幕保護程式</string>
<string name="screensaver_settings">螢幕保護程式設定</string>
<string name="start_screensaver">立即開始</string>
<string name="duration">切換間隔</string>
<string name="photos">照片</string>
<string name="include_types">包含類型</string>
<string name="include_types_summary">要顯示圖片的類型</string>
<string name="in_app_screensaver">使用內建螢幕保護程式</string>
<string name="guest_star">客串演員</string>
<string name="actor">演員</string>
<string name="composer">作曲</string>
<string name="writer">編劇</string>
<string name="producer">製作人</string>
<string name="conductor">指揮</string>
<string name="lyricist">作詞</string>
<string name="arranger">編曲</string>
<string name="engineer">錄音師</string>
<string name="mixer">混音師</string>
<string name="creator">創作者</string>
<string name="artist">演出者</string>
<string name="delete_item">確定要刪除此項目嗎?</string>
<string name="show_media_management">顯示媒體管理選項</string>
<string name="search_for">搜尋 %s</string>
</resources>

View file

@ -312,7 +312,7 @@
<string name="check_for_updates">Check for updates</string>
<string name="combine_continue_next_summary">Applies to TV Series only</string>
<string name="combine_continue_next"><![CDATA[Combine Continue Watching & Next Up]]></string>
<string name="direct_play_ass">Use libass for ASS subtitles</string>
<string name="direct_play_ass">Direct play ASS subtitles</string>
<string name="direct_play_pgs">Direct play PGS subtitles</string>
<string name="downmix_stereo">Always downmix to stereo</string>
<string name="ffmpeg_extension_pref">Use FFmpeg decoder module</string>

View file

@ -40,7 +40,6 @@ preferenceKtx = "1.2.1"
tvprovider = "1.1.0"
workRuntimeKtx = "2.11.1"
paletteKtx = "1.0.0"
assMedia = "0.4.0"
kotlinxCoroutinesTest = "1.10.2"
coreTesting = "2.2.0"
openapi-generator = "7.20.0"
@ -135,8 +134,6 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t
androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" }
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
ass-media = { group = "io.github.peerless2012", name = "ass-media", version.ref = "assMedia" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }